Skip to content

Commit 0527a35

Browse files
authored
Merge pull request #653 from code-corps/649-track-charge-succeeded-event-on-connect
Add stripe charge model and event tracking
2 parents 2db90e6 + 0026355 commit 0527a35

File tree

21 files changed

+601
-41
lines changed

21 files changed

+601
-41
lines changed

lib/code_corps/analytics/segment.ex

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,18 @@ defmodule CodeCorps.Analytics.Segment do
1515
```
1616
"""
1717

18-
alias CodeCorps.{Comment, OrganizationMembership, StripeInvoice, Task, User, UserCategory, UserRole, UserSkill}
18+
alias CodeCorps.{
19+
Comment,
20+
OrganizationMembership,
21+
StripeConnectCharge,
22+
StripeInvoice,
23+
Task,
24+
User,
25+
UserCategory,
26+
UserRole,
27+
UserSkill
28+
}
29+
1930
alias Ecto.Changeset
2031

2132
@api Application.get_env(:code_corps, :analytics)
@@ -26,9 +37,7 @@ defmodule CodeCorps.Analytics.Segment do
2637
Uses the action on the record to determine the event name that should be passed in for the `track` call.
2738
"""
2839
@spec get_event_name(atom, struct) :: String.t
29-
def get_event_name(action, _) when action in @actions_without_properties do
30-
friendly_action_name(action)
31-
end
40+
def get_event_name(action, _) when action in @actions_without_properties, do: friendly_action_name(action)
3241
def get_event_name(:created, %OrganizationMembership{}), do: "Requested Organization Membership"
3342
def get_event_name(:edited, %OrganizationMembership{}), do: "Approved Organization Membership"
3443
def get_event_name(:payment_succeeded, %StripeInvoice{}), do: "Processed Subscription Payment"
@@ -63,13 +72,15 @@ defmodule CodeCorps.Analytics.Segment do
6372
do_track(conn, action_name, properties(record))
6473
{:ok, record}
6574
end
66-
def track({:ok, %{user_id: user_id} = record}, action, nil) do
75+
def track({:error, %Changeset{} = changeset}, _action, _conn), do: {:error, changeset}
76+
77+
def track({:error, errors}, :deleted, _conn), do: {:error, errors}
78+
79+
def track({:ok, %{user_id: user_id} = record}, action) do
6780
action_name = get_event_name(action, record)
6881
do_track(user_id, action_name, properties(record))
6982
{:ok, record}
7083
end
71-
def track({:error, %Changeset{} = changeset}, _action, _conn), do: {:error, changeset}
72-
def track({:error, errors}, :deleted, _conn), do: {:error, errors}
7384

7485
@doc """
7586
Calls `track` with the "Signed In" event in the configured API module.
@@ -122,15 +133,15 @@ defmodule CodeCorps.Analytics.Segment do
122133
organization_id: organization_membership.organization.id
123134
}
124135
end
125-
defp properties(invoice = %StripeInvoice{}) do
126-
revenue = invoice.total / 100 # TODO: this only works for some currencies
127-
currency = String.capitalize(invoice.currency) # ISO 4127 format
136+
defp properties(charge = %StripeConnectCharge{}) do
137+
revenue = charge.amount / 100 # TODO: this only works for some currencies
138+
currency = String.capitalize(charge.currency) # ISO 4127 format
128139

129140
%{
141+
charge_id: charge.id,
130142
currency: currency,
131-
invoice_id: invoice.id,
132143
revenue: revenue,
133-
user_id: invoice.user_id
144+
user_id: charge.user_id
134145
}
135146
end
136147
defp properties(task = %Task{}) do
@@ -167,7 +178,6 @@ defmodule CodeCorps.Analytics.Segment do
167178
%{}
168179
end
169180

170-
171181
defp traits(user) do
172182
%{
173183
admin: user.admin,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
defmodule CodeCorps.StripeService.Adapters.StripeConnectChargeAdapter do
2+
3+
alias CodeCorps.{
4+
StripeConnectAccount, StripeConnectCustomer, Repo
5+
}
6+
7+
alias CodeCorps.StripeService.Util
8+
9+
# Mapping of stripe record attributes to locally stored attributes
10+
# Format is {:local_key, [:nesting, :of, :stripe, :keys]}
11+
#
12+
# TODO:
13+
#
14+
# Relationships we have
15+
# * customer, user
16+
# Relationship we store but do not define
17+
# * application, invoice, connect account, balance transaction, source transfer, transfer
18+
@stripe_mapping [
19+
{:amount, [:amount]},
20+
{:amount_refunded, [:amount_refunded]},
21+
{:application_id_from_stripe, [:application]},
22+
{:application_fee_id_from_stripe, [:application_fee]},
23+
{:balance_transaction_id_from_stripe, [:balance_transaction]},
24+
{:captured, [:captured]},
25+
{:created, [:created]},
26+
{:currency, [:currency]},
27+
{:customer_id_from_stripe, [:customer]},
28+
{:description, [:description]},
29+
{:failure_code, [:failure_code]},
30+
{:failure_message, [:failure_message]},
31+
{:id_from_stripe, [:id]},
32+
{:invoice_id_from_stripe, [:invoice]},
33+
{:paid, [:paid]},
34+
{:refunded, [:refunded]},
35+
{:review_id_from_stripe, [:review]},
36+
{:source_transfer_id_from_stripe, [:source_transfer]},
37+
{:statement_descriptor, [:statement_descriptor]},
38+
{:status, [:status]}
39+
]
40+
41+
@doc """
42+
Transforms a `%Stripe.Charge{}` and a set of local attributes into a
43+
map of parameters used to create or update a `StripeConnectCharge` record.
44+
"""
45+
def to_params(%Stripe.Charge{} = stripe_charge, %StripeConnectAccount{id: connect_account_id}) do
46+
result =
47+
stripe_charge
48+
|> Map.from_struct
49+
|> Util.transform_map(@stripe_mapping)
50+
|> Map.put(:stripe_connect_account_id, connect_account_id)
51+
|> add_other_associations()
52+
53+
{:ok, result}
54+
end
55+
56+
defp add_other_associations(%{customer_id_from_stripe: customer_id_from_stripe} = attributes) do
57+
%StripeConnectCustomer{id: customer_id, user_id: user_id} =
58+
Repo.get_by(StripeConnectCustomer, id_from_stripe: customer_id_from_stripe)
59+
60+
attributes
61+
|> Map.put(:user_id, user_id)
62+
|> Map.put(:stripe_connect_customer_id, customer_id)
63+
end
64+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
defmodule CodeCorps.StripeService.Events.ConnectChargeSucceeded do
2+
def handle(%{data: %{object: %{id: id_from_stripe}}, user_id: connect_account_id_from_stripe}) do
3+
CodeCorps.StripeService.StripeConnectChargeService.create(id_from_stripe, connect_account_id_from_stripe)
4+
end
5+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
defmodule CodeCorps.StripeService.StripeConnectChargeService do
2+
alias CodeCorps.{
3+
Repo, StripeConnectAccount, StripeConnectCharge
4+
}
5+
alias CodeCorps.StripeService.Adapters.StripeConnectChargeAdapter
6+
7+
@api Application.get_env(:code_corps, :stripe)
8+
9+
def create(id_from_stripe, connect_account_id_from_stripe) do
10+
with %StripeConnectAccount{} = stripe_connect_account <- Repo.get_by(StripeConnectAccount, id_from_stripe: connect_account_id_from_stripe),
11+
{:ok, %Stripe.Charge{} = api_charge} <- @api.Charge.retrieve(id_from_stripe, connect_account: connect_account_id_from_stripe),
12+
{:ok, params} = StripeConnectChargeAdapter.to_params(api_charge, stripe_connect_account)
13+
do
14+
%StripeConnectCharge{}
15+
|> StripeConnectCharge.create_changeset(params)
16+
|> Repo.insert
17+
|> CodeCorps.Analytics.Segment.track(:created)
18+
end
19+
end
20+
end

lib/code_corps/stripe_service/stripe_invoice_service.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ defmodule CodeCorps.StripeService.StripeInvoiceService do
1414
%StripeInvoice{}
1515
|> StripeInvoice.create_changeset(params)
1616
|> Repo.insert
17-
|> CodeCorps.Analytics.Segment.track(:payment_succeeded, nil)
1817
else
1918
failure -> failure
2019
end

lib/code_corps/stripe_service/webhook_processing/connect_event_handler.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.ConnectEventHandler do
1616
def handle_event(%{type: type} = attributes), do: do_handle(type, attributes)
1717

1818
defp do_handle("account.updated", attributes), do: Events.AccountUpdated.handle(attributes)
19+
defp do_handle("charge.succeeded", attributes), do: Events.ConnectChargeSucceeded.handle(attributes)
1920
defp do_handle("customer.subscription.deleted", attributes), do: Events.CustomerSubscriptionDeleted.handle(attributes)
2021
defp do_handle("customer.subscription.updated", attributes), do: Events.CustomerSubscriptionUpdated.handle(attributes)
2122
defp do_handle("invoice.payment_succeeded", attributes), do: Events.InvoicePaymentSucceeded.handle(attributes)

lib/code_corps/stripe_service/webhook_processing/event_handler.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ defmodule CodeCorps.StripeService.WebhookProcessing.EventHandler do
4040
end
4141
end
4242

43-
defp call_ignored_handler(local_event, handler), do: IgnoredEventHandler.handle(local_event, handler)
43+
defp call_ignored_handler(%StripeEvent{} = local_event, handler), do: IgnoredEventHandler.handle(local_event, handler)
4444

45-
defp call_handler(api_event, local_event, handler) do
45+
defp call_handler(%Stripe.Event{} = api_event, %StripeEvent{} = local_event, handler) do
4646
# results are multiple, so we convert the tuple to list for easier matching
47-
case api_event |> handler.handle_event |> Tuple.to_list do
47+
user_id = Map.get(local_event, :user_id)
48+
event = api_event |> Map.merge(%{user_id: user_id})
49+
case event |> handler.handle_event |> Tuple.to_list do
4850
[:ok, :unhandled_event] -> local_event |> set_unhandled
4951
[:ok | _results] -> local_event |> set_processed
5052
[:error | _error] -> local_event |> set_errored

lib/code_corps/stripe_service/webhook_processing/webhook_processor.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.WebhookProcessor do
4040
with user_id <- event_params |> Map.get("user_id"),
4141
{:ok, %Stripe.Event{} = api_event} <- retrieve_event_from_api(id, user_id)
4242
do
43-
EventHandler.handle(api_event, handler)
43+
EventHandler.handle(api_event, handler, user_id)
4444
end
4545
end
4646

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
defmodule CodeCorps.StripeTesting.Charge do
2+
import CodeCorps.StripeTesting.Helpers
3+
4+
def retrieve(_id, _opts) do
5+
{:ok, load_fixture(Stripe.Charge, "charge")}
6+
end
7+
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"id": "charge",
3+
"object": "charge",
4+
"amount": 100,
5+
"amount_refunded": 0,
6+
"application": null,
7+
"application_fee": null,
8+
"balance_transaction": "test_balance_transaction_for_charge",
9+
"captured": true,
10+
"created": 1484869309,
11+
"currency": "usd",
12+
"customer": "test_customer_for_charge",
13+
"description": "Test Charge (created for fixture)",
14+
"destination": null,
15+
"dispute": null,
16+
"failure_code": null,
17+
"failure_message": null,
18+
"fraud_details": {},
19+
"invoice": null,
20+
"livemode": false,
21+
"metadata": {
22+
},
23+
"order": null,
24+
"outcome": null,
25+
"paid": true,
26+
"receipt_email": null,
27+
"receipt_number": null,
28+
"refunded": false,
29+
"refunds": {
30+
"object": "list",
31+
"data": [],
32+
"has_more": false,
33+
"total_count": 0,
34+
"url": "/v1/charges/ch_123/refunds"
35+
},
36+
"review": null,
37+
"shipping": null,
38+
"source": {
39+
"id": "card_123",
40+
"object": "card",
41+
"address_city": null,
42+
"address_country": null,
43+
"address_line1": null,
44+
"address_line1_check": null,
45+
"address_line2": null,
46+
"address_state": null,
47+
"address_zip": null,
48+
"address_zip_check": null,
49+
"brand": "Visa",
50+
"country": "US",
51+
"customer": null,
52+
"cvc_check": null,
53+
"dynamic_last4": null,
54+
"exp_month": 8,
55+
"exp_year": 2018,
56+
"funding": "credit",
57+
"last4": "4242",
58+
"metadata": {
59+
},
60+
"name": null,
61+
"tokenization_method": null
62+
},
63+
"source_transfer": null,
64+
"statement_descriptor": null,
65+
"status": "succeeded"
66+
}

0 commit comments

Comments
 (0)