Skip to content

Commit 0816489

Browse files
authored
Merge pull request #613 from code-corps/537-add-invoices
537 add invoices
2 parents d90fda6 + d84edeb commit 0816489

40 files changed

Lines changed: 751 additions & 192 deletions

lib/code_corps/analytics/in_memory_api.ex

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ defmodule CodeCorps.Analytics.InMemoryAPI do
55
Each function should have the same signature as `CodeCorps.Analytics.SegmentAPI` and simply return `nil`.
66
"""
77

8-
def identify(_user_id, _traits), do: nil
9-
def track(_user_id, _event_name, _properties), do: nil
8+
require Logger
9+
10+
def identify(user_id, _traits), do: log_identify(user_id)
11+
12+
def track(user_id, event_name, _properties), do: log_track(user_id, event_name)
13+
14+
defp log_identify(user_id) do
15+
Logger.info "Called identify for User #{user_id}"
16+
end
17+
18+
defp log_track(user_id, event_name) do
19+
Logger.info "Called track for event #{event_name} for User #{user_id}"
20+
end
1021
end

lib/code_corps/analytics/segment.ex

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

18-
alias CodeCorps.Comment
19-
alias CodeCorps.OrganizationMembership
20-
alias CodeCorps.Task
21-
alias CodeCorps.User
22-
alias CodeCorps.UserCategory
23-
alias CodeCorps.UserRole
24-
alias CodeCorps.UserSkill
18+
alias CodeCorps.{Comment, OrganizationMembership, StripeInvoice, Task, User, UserCategory, UserRole, UserSkill}
2519
alias Ecto.Changeset
2620

2721
@api Application.get_env(:code_corps, :analytics)
@@ -37,6 +31,7 @@ defmodule CodeCorps.Analytics.Segment do
3731
end
3832
def get_event_name(:created, %OrganizationMembership{}), do: "Requested Organization Membership"
3933
def get_event_name(:edited, %OrganizationMembership{}), do: "Approved Organization Membership"
34+
def get_event_name(:payment_succeeded, %StripeInvoice{}), do: "Processed Subscription Payment"
4035
def get_event_name(:created, %UserCategory{}), do: "Added User Category"
4136
def get_event_name(:created, %UserSkill{}), do: "Added User Skill"
4237
def get_event_name(:created, %UserRole{}), do: "Added User Role"
@@ -61,13 +56,16 @@ defmodule CodeCorps.Analytics.Segment do
6156
def track({:ok, record}, action, %Plug.Conn{} = conn) when action in @actions_without_properties do
6257
action_name = get_event_name(action, record)
6358
do_track(conn, action_name)
64-
6559
{:ok, record}
6660
end
6761
def track({:ok, record}, action, %Plug.Conn{} = conn) do
6862
action_name = get_event_name(action, record)
6963
do_track(conn, action_name, properties(record))
70-
64+
{:ok, record}
65+
end
66+
def track({:ok, %{user_id: user_id} = record}, action, nil) do
67+
action_name = get_event_name(action, record)
68+
do_track(user_id, action_name, properties(record))
7169
{:ok, record}
7270
end
7371
def track({:error, %Changeset{} = changeset}, _action, _conn), do: {:error, changeset}
@@ -98,14 +96,13 @@ defmodule CodeCorps.Analytics.Segment do
9896
|> Enum.join(" ")
9997
end
10098

101-
defp do_track(conn, event_name, properties) do
99+
defp do_track(conn_or_user, event_name, properties \\ %{})
100+
defp do_track(%Plug.Conn{} = conn, event_name, properties) do
102101
@api.track(conn.assigns[:current_user].id, event_name, properties)
103102
conn
104103
end
105-
106-
defp do_track(conn, event_name) do
107-
@api.track(conn.assigns[:current_user].id, event_name, %{})
108-
conn
104+
defp do_track(user_id, event_name, properties) do
105+
@api.track(user_id, event_name, properties)
109106
end
110107

111108
defp properties(comment = %Comment{}) do
@@ -125,6 +122,17 @@ defmodule CodeCorps.Analytics.Segment do
125122
organization_id: organization_membership.organization.id
126123
}
127124
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
128+
129+
%{
130+
currency: currency,
131+
invoice_id: invoice.id,
132+
revenue: revenue,
133+
user_id: invoice.user_id
134+
}
135+
end
128136
defp properties(task = %Task{}) do
129137
%{
130138
task: task.title,
@@ -154,10 +162,12 @@ defmodule CodeCorps.Analytics.Segment do
154162
skill_id: user_skill.skill.id
155163
}
156164
end
165+
157166
defp properties(_struct) do
158167
%{}
159168
end
160169

170+
161171
defp traits(user) do
162172
%{
163173
admin: user.admin,

lib/code_corps/stripe_service/adapters/stripe_connect_customer.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ defmodule CodeCorps.StripeService.Adapters.StripeConnectCustomerAdapter do
1313
{:ok, result}
1414
end
1515

16-
@non_stripe_attributes ["stripe_connect_account_id", "stripe_platform_customer_id"]
16+
@non_stripe_attributes ["stripe_connect_account_id", "stripe_platform_customer_id", "user_id"]
1717

1818
defp add_non_stripe_attributes(%{} = params, %{} = attributes) do
1919
attributes
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
defmodule CodeCorps.StripeService.Adapters.StripeInvoiceAdapter do
2+
alias CodeCorps.{Repo, StripeConnectCustomer, StripeConnectSubscription}
3+
4+
import CodeCorps.MapUtils, only: [keys_to_string: 1]
5+
import CodeCorps.StripeService.Util, only: [transform_map: 2]
6+
7+
# Mapping of stripe record attributes to locally stored attributes
8+
# Format is {:local_key, [:nesting, :of, :stripe, :keys]}
9+
@stripe_mapping [
10+
{:id_from_stripe, [:id]},
11+
{:amount_due, [:amount_due]},
12+
{:application_fee, [:application_fee]},
13+
{:attempt_count, [:attempt_count]},
14+
{:attempted, [:attempted]},
15+
{:charge_id_from_stripe, [:charge]},
16+
{:closed, [:closed]},
17+
{:currency, [:currency]},
18+
{:customer_id_from_stripe, [:customer]},
19+
{:date, [:date]},
20+
{:description, [:description]},
21+
{:ending_balance, [:ending_balance]},
22+
{:forgiven, [:forgiven]},
23+
{:next_payment_attempt, [:next_payment_attempt]},
24+
{:paid, [:paid]},
25+
{:period_end, [:period_end]},
26+
{:period_start, [:period_start]},
27+
{:receipt_number, [:receipt_number]},
28+
{:starting_balance, [:starting_balance]},
29+
{:statement_descriptor, [:statement_descriptor]},
30+
{:subscription_id_from_stripe, [:subscription]},
31+
{:subscription_proration_date, [:subscription_proration_date]},
32+
{:subtotal, [:subtotal]},
33+
{:tax, [:tax]},
34+
{:tax_percent, [:tax_percent]},
35+
{:total, [:total]},
36+
{:webhooks_delivered_at, [:webhooks_delivered_at]},
37+
]
38+
39+
@doc """
40+
Transforms a `%Stripe.Invoice{}` and a set of local attributes into a
41+
map of parameters used to create or update a `StripeInvoice` record.
42+
"""
43+
def to_params(%Stripe.Invoice{} = stripe_invoice) do
44+
result =
45+
stripe_invoice
46+
|> Map.from_struct
47+
|> transform_map(@stripe_mapping)
48+
|> add_stripe_connect_subscription_id
49+
|> add_user_id
50+
|> keys_to_string
51+
52+
{:ok, result}
53+
end
54+
55+
defp add_stripe_connect_subscription_id(%{subscription_id_from_stripe: subscription_id_from_stripe} = map) do
56+
%StripeConnectSubscription{id: id} =
57+
StripeConnectSubscription
58+
|> Repo.get_by(id_from_stripe: subscription_id_from_stripe)
59+
Map.put(map, :stripe_connect_subscription_id, id)
60+
end
61+
62+
defp add_user_id(%{customer_id_from_stripe: customer_id_from_stripe} = map) do
63+
%StripeConnectCustomer{user_id: user_id} =
64+
StripeConnectCustomer
65+
|> Repo.get_by(id_from_stripe: customer_id_from_stripe)
66+
Map.put(map, :user_id, user_id)
67+
end
68+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
defmodule CodeCorps.StripeService.Events.InvoicePaymentSucceeded do
2+
def handle(%{"data" => %{"object" => %{"id" => id_from_stripe, "customer" => customer_id_from_stripe}}}) do
3+
CodeCorps.StripeService.StripeInvoiceService.create(id_from_stripe, customer_id_from_stripe)
4+
end
5+
end

lib/code_corps/stripe_service/stripe_connect_customer.ex

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,27 @@
11
defmodule CodeCorps.StripeService.StripeConnectCustomerService do
2-
alias CodeCorps.Repo
32
alias CodeCorps.StripeService.Adapters.StripeConnectCustomerAdapter
4-
alias CodeCorps.StripeConnectAccount
5-
alias CodeCorps.StripeConnectCustomer
6-
alias CodeCorps.StripePlatformCustomer
3+
alias CodeCorps.{Repo, StripeConnectAccount, StripeConnectCustomer, StripePlatformCustomer, User}
74

85
import CodeCorps.MapUtils, only: [rename: 3, keys_to_string: 1]
96
import Ecto.Query # needed for match
107

118
@api Application.get_env(:code_corps, :stripe)
129

13-
def find_or_create(%StripePlatformCustomer{} = platform_customer, %StripeConnectAccount{} = connect_account) do
10+
def find_or_create(%StripePlatformCustomer{} = platform_customer, %StripeConnectAccount{} = connect_account, %User{} = user) do
1411
case get_from_db(connect_account.id, platform_customer.id) do
1512
%StripeConnectCustomer{} = existing_customer ->
1613
{:ok, existing_customer}
1714
nil ->
18-
create(platform_customer, connect_account)
15+
create(platform_customer, connect_account, user)
1916
end
2017
end
2118

2219
def update(%StripeConnectCustomer{id_from_stripe: id_from_stripe, stripe_connect_account: connect_account}, attributes) do
2320
@api.Customer.update(id_from_stripe, attributes, connect_account: connect_account.id_from_stripe)
2421
end
2522

26-
defp create(%StripePlatformCustomer{} = platform_customer, %StripeConnectAccount{} = connect_account) do
27-
attributes = platform_customer |> create_non_stripe_attributes(connect_account)
23+
defp create(%StripePlatformCustomer{} = platform_customer, %StripeConnectAccount{} = connect_account, %User{} = user) do
24+
attributes = create_non_stripe_attributes(platform_customer, connect_account, user)
2825
stripe_attributes = create_stripe_attributes(platform_customer)
2926

3027
with {:ok, customer} <-
@@ -45,12 +42,13 @@ defmodule CodeCorps.StripeService.StripeConnectCustomerService do
4542
|> Repo.one
4643
end
4744

48-
defp create_non_stripe_attributes(platform_customer, connect_account) do
45+
defp create_non_stripe_attributes(platform_customer, connect_account, user) do
4946
platform_customer
5047
|> Map.from_struct
5148
|> Map.take([:id])
5249
|> rename(:id, :stripe_platform_customer_id)
5350
|> Map.put(:stripe_connect_account_id, connect_account.id)
51+
|> Map.put(:user_id, user.id)
5452
|> keys_to_string
5553
end
5654

lib/code_corps/stripe_service/stripe_connect_subscription.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ defmodule CodeCorps.StripeService.StripeConnectSubscriptionService do
7777
platform_customer <- user.stripe_platform_customer,
7878
connect_account <- project.organization.stripe_connect_account,
7979
plan <- project.stripe_connect_plan,
80-
{:ok, connect_customer} <- StripeConnectCustomerService.find_or_create(platform_customer, connect_account),
80+
{:ok, connect_customer} <- StripeConnectCustomerService.find_or_create(platform_customer, connect_account, user),
8181
{:ok, connect_card} <- StripeConnectCardService.find_or_create(platform_card, connect_customer, platform_customer, connect_account),
8282
create_attributes <- to_create_attributes(connect_card, connect_customer, plan, attributes),
8383
{:ok, subscription} <- @api.Subscription.create(create_attributes, connect_account: connect_account.id_from_stripe),
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule CodeCorps.StripeService.StripeInvoiceService do
2+
3+
alias CodeCorps.{Repo, StripeConnectAccount, StripeConnectCustomer, StripeInvoice}
4+
alias CodeCorps.StripeService.Adapters.StripeInvoiceAdapter
5+
6+
@api Application.get_env(:code_corps, :stripe)
7+
8+
@spec create(binary, binary) :: {:ok, %StripeInvoice{}} | {:error, %Ecto.Changeset{}}
9+
def create(invoice_id_from_stripe, customer_id_from_stripe) do
10+
with account_id <- get_connect_account(customer_id_from_stripe),
11+
{:ok, %Stripe.Invoice{} = invoice} <- @api.Invoice.retrieve(invoice_id_from_stripe, connect_account: account_id),
12+
{:ok, params} <- StripeInvoiceAdapter.to_params(invoice)
13+
do
14+
%StripeInvoice{}
15+
|> StripeInvoice.create_changeset(params)
16+
|> Repo.insert
17+
|> CodeCorps.Analytics.Segment.track(:payment_succeeded, nil)
18+
end
19+
end
20+
21+
defp get_connect_account(customer_id_from_stripe) do
22+
%StripeConnectCustomer{stripe_connect_account: %StripeConnectAccount{id_from_stripe: stripe_connect_account_id}} =
23+
StripeConnectCustomer
24+
|> Repo.get_by(id_from_stripe: customer_id_from_stripe)
25+
|> Repo.preload([:stripe_connect_account])
26+
stripe_connect_account_id
27+
end
28+
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
@@ -19,5 +19,6 @@ defmodule CodeCorps.StripeService.WebhookProcessing.ConnectEventHandler do
1919
defp do_handle("account.external_account.created", attributes), do: Events.ConnectExternalAccountCreated.handle(attributes)
2020
defp do_handle("customer.subscription.deleted", attributes), do: Events.CustomerSubscriptionDeleted.handle(attributes)
2121
defp do_handle("customer.subscription.updated", attributes), do: Events.CustomerSubscriptionUpdated.handle(attributes)
22+
defp do_handle("invoice.payment_succeeded", attributes), do: Events.InvoicePaymentSucceeded.handle(attributes)
2223
defp do_handle(_, _), do: {:ok, :unhandled_event}
2324
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
defmodule CodeCorps.StripeTesting.Invoice do
2+
def retrieve(id, _) do
3+
{:ok, invoice(id)}
4+
end
5+
6+
defp invoice(id) do
7+
%Stripe.Invoice{
8+
amount_due: 1000,
9+
application_fee: 50,
10+
attempt_count: 1,
11+
attempted: true,
12+
charge: "ch_123",
13+
closed: true,
14+
currency: "usd",
15+
customer: "cus_123",
16+
date: 1_483_553_506,
17+
description: nil,
18+
discount: nil,
19+
ending_balance: 0,
20+
forgiven: false,
21+
id: id,
22+
livemode: false,
23+
metadata: %{},
24+
next_payment_attempt: nil,
25+
paid: true,
26+
period_end: 1_483_553_506,
27+
period_start: 1_483_553_506,
28+
receipt_number: nil,
29+
starting_balance: 0,
30+
statement_descriptor: nil,
31+
subscription: "sub_123",
32+
subscription_proration_date: nil,
33+
subtotal: 1000,
34+
tax: nil,
35+
tax_percent: nil,
36+
total: 1000,
37+
webhooks_delivered_at: 1_483_553_511
38+
}
39+
end
40+
end

0 commit comments

Comments
 (0)