Skip to content

Commit 61ffdea

Browse files
authored
Merge pull request #642 from code-corps/640-create-external-account-when-updating-connect-account
Extend StripeConnectAccountService to also create associated external account record on update
2 parents 329d0b2 + dfb8e1e commit 61ffdea

17 files changed

Lines changed: 417 additions & 155 deletions

lib/code_corps/stripe_service/adapters/stripe_connect_account.ex

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,15 @@ defmodule CodeCorps.StripeService.Adapters.StripeConnectAccountAdapter do
113113
map |> add_external_account(stripe_account)
114114
end
115115

116-
defp add_external_account(map, %Stripe.Account{external_accounts: %{data: []}}), do: map
117-
defp add_external_account(map, %Stripe.Account{external_accounts: %{data: [head | _]}}), do: map |> do_add_external_account(head)
118-
defp do_add_external_account(map, %{"id" => id}), do: map |> Map.put(:external_account, id)
116+
defp add_external_account(map, %Stripe.Account{external_accounts: %Stripe.List{data: list}}) do
117+
latest = list |> List.last
118+
map |> do_add_external_account(latest)
119+
end
120+
121+
defp do_add_external_account(map, nil), do: map
122+
defp do_add_external_account(map, %Stripe.ExternalAccount{id: id}) do
123+
map |> Map.put(:external_account, id)
124+
end
119125

120126
defp remove_attributes(%{"legal_entity_verification_status" => "verified"} = attributes) do
121127
attributes |> Map.delete("legal_entity_verification_document")
Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
defmodule CodeCorps.StripeService.Adapters.StripeExternalAccountAdapter do
2-
import CodeCorps.MapUtils, only: [rename: 3]
2+
3+
alias CodeCorps.MapUtils
4+
alias CodeCorps.StripeConnectAccount
35

46
@stripe_attributes [
5-
:account, :account_holder_name, :account_holder_type, :bank_name, :country,
7+
:account_holder_name, :account_holder_type, :bank_name, :country,
68
:currency, :default_for_currency, :fingerprint, :id, :last4,
79
:routing_number, :status
810
]
911

10-
def to_params(%Stripe.ExternalAccount{} = bank_account, stripe_connect_account_id \\ nil) do
12+
def to_params(%Stripe.ExternalAccount{} = external_account, %StripeConnectAccount{} = connect_account) do
1113
params =
12-
bank_account
14+
external_account
1315
|> Map.from_struct
1416
|> Map.take(@stripe_attributes)
15-
|> rename(:id, :id_from_stripe)
16-
|> rename(:account, :account_id_from_stripe)
17-
|> Map.put(:stripe_connect_account_id, stripe_connect_account_id)
17+
|> MapUtils.rename(:id, :id_from_stripe)
18+
|> add_association_attributes(connect_account)
1819

1920
{:ok, params}
2021
end
22+
23+
defp add_association_attributes(attributes, %StripeConnectAccount{} = connect_account) do
24+
association_attributes = build_association_attributes(connect_account)
25+
attributes |> Map.merge(association_attributes)
26+
end
27+
28+
defp build_association_attributes(%StripeConnectAccount{id: id, id_from_stripe: id_from_stripe}) do
29+
%{account_id_from_stripe: id_from_stripe, stripe_connect_account_id: id}
30+
end
2131
end

lib/code_corps/stripe_service/stripe_connect_account.ex

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
defmodule CodeCorps.StripeService.StripeConnectAccountService do
2+
alias CodeCorps.{Repo, StripeConnectAccount, StripeExternalAccount}
3+
alias CodeCorps.StripeService.Adapters.{StripeConnectAccountAdapter}
4+
alias CodeCorps.StripeService.StripeConnectExternalAccountService
5+
alias Ecto.Multi
6+
7+
import Ecto.Query, only: [where: 3]
8+
9+
@api Application.get_env(:code_corps, :stripe)
10+
11+
@doc """
12+
Used to create a remote `Stripe.Account` record as well as an associated local
13+
`StripeConnectAccount` record.
14+
"""
15+
def create(attributes) do
16+
with {:ok, from_params} <- StripeConnectAccountAdapter.from_params(attributes),
17+
{:ok, %Stripe.Account{} = account} <- @api.Account.create(from_params),
18+
{:ok, params} <- StripeConnectAccountAdapter.to_params(account, attributes)
19+
do
20+
%StripeConnectAccount{} |> StripeConnectAccount.create_changeset(params) |> Repo.insert
21+
else
22+
failure -> failure
23+
end
24+
end
25+
26+
@doc """
27+
Used to update both the local `StripeConnectAccount` as well as the remote `Stripe.Account`,
28+
using attributes sent by the client
29+
"""
30+
def update(%StripeConnectAccount{id_from_stripe: id_from_stripe} = local_account, %{} = attributes) do
31+
with {:ok, from_params} <- StripeConnectAccountAdapter.from_params(attributes),
32+
{:ok, %Stripe.Account{} = api_account} <- @api.Account.update(id_from_stripe, from_params)
33+
do
34+
update_local_account(local_account, api_account, attributes)
35+
end
36+
end
37+
38+
@doc """
39+
Used to update the local `StripeConnectAccount` record using data retrieved from the Stripe API
40+
"""
41+
def update_from_stripe(id_from_stripe) do
42+
with {:ok, %Stripe.Account{} = api_account} <- @api.Account.retrieve(id_from_stripe),
43+
%StripeConnectAccount{} = local_account <- Repo.get_by(StripeConnectAccount, id_from_stripe: id_from_stripe)
44+
do
45+
update_local_account(local_account, api_account)
46+
else
47+
nil -> {:error, :not_found}
48+
failure -> failure
49+
end
50+
end
51+
52+
# updates a StripeConnectAccount record with combined information from the provided
53+
# Stripe.Account record and an optional attributes map
54+
defp update_local_account(
55+
%StripeConnectAccount{} = local_account,
56+
%Stripe.Account{} = api_account,
57+
attributes \\ %{}
58+
) do
59+
with {:ok, params} <- StripeConnectAccountAdapter.to_params(api_account, attributes) do
60+
changeset = local_account |> StripeConnectAccount.webhook_update_changeset(params)
61+
62+
multi = Multi.new
63+
|> Multi.update(:stripe_connect_account, changeset)
64+
|> Multi.run(:process_external_accounts, &process_external_accounts(&1, api_account))
65+
66+
case Repo.transaction(multi) do
67+
{:ok, %{stripe_connect_account: stripe_connect_account, process_external_accounts: _}} ->
68+
{:ok, stripe_connect_account}
69+
{:error, :stripe_connect_account, %Ecto.Changeset{} = changeset, %{}} ->
70+
{:error, changeset}
71+
{:error, failed_operation, failed_value, _changes_so_far} ->
72+
{:error, failed_operation, failed_value}
73+
end
74+
end
75+
end
76+
77+
# goes through all Stripe.ExternalAccount objects within the retrieved Stripe.Account object,
78+
# then either retrieves or creates a StripeExternalAccount object for each of them
79+
defp process_external_accounts(_, %Stripe.Account{external_accounts: %{data: []}}), do: {:ok, []}
80+
defp process_external_accounts(
81+
%{stripe_connect_account: %StripeConnectAccount{} = connect_account},
82+
%Stripe.Account{external_accounts: %{data: new_external_account_list}}
83+
) do
84+
StripeExternalAccount
85+
|> where([e], e.stripe_connect_account_id == ^connect_account.id)
86+
|> Repo.delete_all
87+
88+
new_external_account_list
89+
|> (fn(list) -> [List.last(list)] end).() # We only support one external account for now
90+
|> Enum.map(&find_or_create_external_account(&1, connect_account))
91+
|> Enum.map(&take_record/1)
92+
|> aggregate_records
93+
end
94+
95+
# retrieves or creates a StripeExternalAccount object associated to the provided
96+
# Stripe.ExternalAccount and StripeConnectAccount objects
97+
# returns {:ok, list_of_created_external_account_records}
98+
defp find_or_create_external_account(%Stripe.ExternalAccount{} = api_external_account, connect_account) do
99+
case Repo.get_by(StripeExternalAccount, id_from_stripe: api_external_account.id) do
100+
nil -> StripeConnectExternalAccountService.create(api_external_account, connect_account)
101+
%StripeExternalAccount{} = local_external_account -> {:ok, local_external_account}
102+
end
103+
end
104+
105+
defp take_record({:ok, %StripeExternalAccount{} = external_account}), do: external_account
106+
107+
defp aggregate_records(results), do: {:ok, results}
108+
end

lib/code_corps/stripe_service/stripe_connect_external_account_service.ex

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,9 @@ defmodule CodeCorps.StripeService.StripeConnectExternalAccountService do
44

55
@api Application.get_env(:code_corps, :stripe)
66

7-
def create(id_from_stripe, account_id_from_stripe) do
8-
with {:ok, %Stripe.ExternalAccount{} = external_account} <- @api.ExternalAccount.retrieve(id_from_stripe, connect_account: account_id_from_stripe),
9-
{:ok, %StripeConnectAccount{} = connect_account} <- get_connect_account(account_id_from_stripe),
10-
{:ok, params} <- StripeExternalAccountAdapter.to_params(external_account, connect_account.id)
11-
do
12-
%StripeExternalAccount{}
13-
|> StripeExternalAccount.changeset(params)
14-
|> Repo.insert
15-
else
16-
failure -> failure
17-
end
18-
end
19-
20-
defp get_connect_account(account_id_from_stripe) do
21-
case Repo.get_by(StripeConnectAccount, id_from_stripe: account_id_from_stripe) do
22-
nil -> {:error, :not_found}
23-
record -> {:ok, record}
7+
def create(%Stripe.ExternalAccount{} = external_account, %StripeConnectAccount{} = connect_account) do
8+
with {:ok, params} <- StripeExternalAccountAdapter.to_params(external_account, connect_account) do
9+
%StripeExternalAccount{} |> StripeExternalAccount.changeset(params) |> Repo.insert
2410
end
2511
end
2612
end

lib/code_corps/stripe_service/webhook_processing/connect_event_handler.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ 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("account.external_account.created", attributes), do: Events.ConnectExternalAccountCreated.handle(attributes)
2019
defp do_handle("customer.subscription.deleted", attributes), do: Events.CustomerSubscriptionDeleted.handle(attributes)
2120
defp do_handle("customer.subscription.updated", attributes), do: Events.CustomerSubscriptionUpdated.handle(attributes)
2221
defp do_handle("invoice.payment_succeeded", attributes), do: Events.InvoicePaymentSucceeded.handle(attributes)

lib/code_corps/stripe_service/webhook_processing/ignored_event_handler.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.IgnoredEventHandler do
22
alias CodeCorps.{StripeEvent, Repo}
33

44
@ignored_event_types [
5+
"account.external_account.created",
56
"application_fee.created",
67
"customer.created",
78
"customer.source.created",
@@ -39,6 +40,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.IgnoredEventHandler do
3940
end
4041

4142
@spec get_reason(String.t) :: String.t
43+
defp get_reason("account.external_account.created"), do: "External accounts are stored locally upon updating a connect account."
4244
defp get_reason("application_fee.created"), do: "We don't make use of the application fee object."
4345
defp get_reason("customer.created"), do: "Customers are only created from the client."
4446
defp get_reason("customer.source.created"), do: "Cards are only created from the client. No need to handle"

lib/code_corps/stripe_testing/account.ex

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
defmodule CodeCorps.StripeTesting.Account do
2+
import CodeCorps.StripeTesting.Helpers
3+
24
def create(attributes) do
35
{:ok, create_stripe_record(attributes)}
46
end
57

8+
def retrieve("account_with_multiple_external_accounts") do
9+
{:ok, load_fixture(Stripe.Account, "account_with_multiple_external_accounts")}
10+
end
11+
612
def retrieve(id) do
713
{:ok, create_stripe_record(%{"id" => id})}
814
end
@@ -60,7 +66,7 @@ defmodule CodeCorps.StripeTesting.Account do
6066
defp add_external_account(%{"id" => account_id, "external_account" => external_account_id} = map) do
6167
external_accounts_map = %{
6268
"object" => "list",
63-
"data" => [%{"id" => external_account_id}],
69+
"data" => [%{"id" => external_account_id, "object" => "bank_account"}],
6470
"has_more" => false,
6571
"total_count" => 1,
6672
"url" => "/v1/accounts/#{account_id}/external_accounts"

0 commit comments

Comments
 (0)