Skip to content

Commit

Permalink
Implement find_or_create behavior for external accounts, fix existing…
Browse files Browse the repository at this point in the history
… tests, add external account created event to explicit ignore
  • Loading branch information
begedin committed Jan 16, 2017
1 parent 6f27f5c commit d1641bd
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 68 deletions.
24 changes: 17 additions & 7 deletions lib/code_corps/stripe_service/adapters/stripe_external_account.ex
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
defmodule CodeCorps.StripeService.Adapters.StripeExternalAccountAdapter do
import CodeCorps.MapUtils, only: [rename: 3]

alias CodeCorps.MapUtils
alias CodeCorps.StripeConnectAccount

@stripe_attributes [
:account, :account_holder_name, :account_holder_type, :bank_name, :country,
:account_holder_name, :account_holder_type, :bank_name, :country,
:currency, :default_for_currency, :fingerprint, :id, :last4,
:routing_number, :status
]

def to_params(%Stripe.ExternalAccount{} = bank_account, stripe_connect_account_id \\ nil) do
def to_params(%Stripe.ExternalAccount{} = external_account, %StripeConnectAccount{} = connect_account) do
params =
bank_account
external_account
|> Map.from_struct
|> Map.take(@stripe_attributes)
|> rename(:id, :id_from_stripe)
|> rename(:account, :account_id_from_stripe)
|> Map.put(:stripe_connect_account_id, stripe_connect_account_id)
|> MapUtils.rename(:id, :id_from_stripe)
|> add_association_attributes(connect_account)

{:ok, params}
end

defp add_association_attributes(attributes, %StripeConnectAccount{} = connect_account) do
association_attributes = build_association_attributes(connect_account)
attributes |> Map.merge(association_attributes)
end

defp build_association_attributes(%StripeConnectAccount{id: id, id_from_stripe: id_from_stripe}) do
%{account_id_from_stripe: id_from_stripe, stripe_connect_account_id: id}
end
end
47 changes: 44 additions & 3 deletions lib/code_corps/stripe_service/stripe_connect_account_service.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule CodeCorps.StripeService.StripeConnectAccountService do
alias CodeCorps.{Repo, StripeConnectAccount}
alias CodeCorps.{Repo, StripeConnectAccount, StripeExternalAccount}
alias CodeCorps.StripeService.Adapters.{StripeConnectAccountAdapter}
alias CodeCorps.StripeService.StripeConnectExternalAccountService
alias Ecto.Multi

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

Expand Down Expand Up @@ -45,10 +47,49 @@ defmodule CodeCorps.StripeService.StripeConnectAccountService do
end
end

# updates a StripeConnectAccount record with combined information from the provided
# Stripe.Account record and an optional attributes map
defp update_local_account(%StripeConnectAccount{} = local_account, %Stripe.Account{} = api_account, attributes \\ %{}) do
with {:ok, params} <- StripeConnectAccountAdapter.to_params(api_account, attributes) do
local_account |> StripeConnectAccount.webhook_update_changeset(params) |> Repo.update
# TODO: Also update external account
changeset = local_account |> StripeConnectAccount.webhook_update_changeset(params)

multi = Multi.new
|> Multi.update(:stripe_connect_account, changeset)
|> Multi.run(:process_external_accounts, &process_external_accounts(&1, api_account))

case Repo.transaction(multi) do
{:ok, %{stripe_connect_account: stripe_connect_account, process_external_accounts: _}} ->
{:ok, stripe_connect_account}
{:error, :stripe_connect_account, %Ecto.Changeset{} = changeset, %{}} ->
{:error, changeset}
{:error, failed_operation, failed_value, _changes_so_far} ->
{:error, failed_operation, failed_value}
end
end
end

# goes through all Stripe.ExternalAccount objects within the retrieved Stripe.Account object,
# then either retrieves or creates a StripeExternalAccount object for each of them
defp process_external_accounts(
%{stripe_connect_account: %StripeConnectAccount{} = connect_account},
%Stripe.Account{external_accounts: %{data: external_account_list}}
) do
external_account_list
|> Enum.map(&find_or_create_external_account(&1, connect_account))
|> Enum.map(&take_record/1)
|> aggregate_records
end

# retrieves or creates a StripeExternalAccount object associated to the provided
# Stripe.ExternalAccount and StripeConnectAccount objects
defp find_or_create_external_account(%Stripe.ExternalAccount{} = api_external_account, connect_account) do
case Repo.get_by(StripeExternalAccount, id_from_stripe: api_external_account.id) do
nil -> StripeConnectExternalAccountService.create(api_external_account, connect_account)
%StripeExternalAccount{} = local_external_account -> {:ok, local_external_account}
end
end

defp take_record({:ok, %StripeExternalAccount{} = external_account}), do: external_account

defp aggregate_records(results), do: {:ok, results}
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,9 @@ defmodule CodeCorps.StripeService.StripeConnectExternalAccountService do

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

def create(id_from_stripe, account_id_from_stripe) do
with {:ok, %Stripe.ExternalAccount{} = external_account} <- @api.ExternalAccount.retrieve(id_from_stripe, connect_account: account_id_from_stripe),
{:ok, %StripeConnectAccount{} = connect_account} <- get_connect_account(account_id_from_stripe),
{:ok, params} <- StripeExternalAccountAdapter.to_params(external_account, connect_account.id)
do
%StripeExternalAccount{}
|> StripeExternalAccount.changeset(params)
|> Repo.insert
else
failure -> failure
end
end

defp get_connect_account(account_id_from_stripe) do
case Repo.get_by(StripeConnectAccount, id_from_stripe: account_id_from_stripe) do
nil -> {:error, :not_found}
record -> {:ok, record}
def create(%Stripe.ExternalAccount{} = external_account, %StripeConnectAccount{} = connect_account) do
with {:ok, params} <- StripeExternalAccountAdapter.to_params(external_account, connect_account) do
%StripeExternalAccount{} |> StripeExternalAccount.changeset(params) |> Repo.insert
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ defmodule CodeCorps.StripeService.WebhookProcessing.ConnectEventHandler do
def handle_event(%{type: type} = attributes), do: do_handle(type, attributes)

defp do_handle("account.updated", attributes), do: Events.AccountUpdated.handle(attributes)
defp do_handle("account.external_account.created", attributes), do: Events.ConnectExternalAccountCreated.handle(attributes)
defp do_handle("customer.subscription.deleted", attributes), do: Events.CustomerSubscriptionDeleted.handle(attributes)
defp do_handle("customer.subscription.updated", attributes), do: Events.CustomerSubscriptionUpdated.handle(attributes)
defp do_handle("invoice.payment_succeeded", attributes), do: Events.InvoicePaymentSucceeded.handle(attributes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.IgnoredEventHandler do
alias CodeCorps.{StripeEvent, Repo}

@ignored_event_types [
"account.external_account.created",
"application_fee.created",
"customer.created",
"customer.source.created",
Expand Down Expand Up @@ -39,6 +40,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.IgnoredEventHandler do
end

@spec get_reason(String.t) :: String.t
defp get_reason("account.external_account.created"), do: "External accounts are stored locally upon updating a connect account."
defp get_reason("application_fee.created"), do: "We don't make use of the application fee object."
defp get_reason("customer.created"), do: "Customers are only created from the client."
defp get_reason("customer.source.created"), do: "Cards are only created from the client. No need to handle"
Expand Down
2 changes: 1 addition & 1 deletion lib/code_corps/stripe_testing/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ defmodule CodeCorps.StripeTesting.Account do
defp add_external_account(%{"id" => account_id, "external_account" => external_account_id} = map) do
external_accounts_map = %{
"object" => "list",
"data" => [%{"id" => external_account_id}],
"data" => [%{"id" => external_account_id, "object" => "bank_account"}],
"has_more" => false,
"total_count" => 1,
"url" => "/v1/accounts/#{account_id}/external_accounts"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule CodeCorps.StripeService.Adapters.StripeExternalAccountTest do
use ExUnit.Case, async: true
use CodeCorps.ModelCase

import CodeCorps.StripeService.Adapters.StripeExternalAccountAdapter, only: [to_params: 1]
import CodeCorps.StripeService.Adapters.StripeExternalAccountAdapter, only: [to_params: 2]

@stripe_external_account %Stripe.ExternalAccount{
id: "ba_19SSZG2eZvKYlo2CXnmzYU5H",
Expand All @@ -22,7 +22,6 @@ defmodule CodeCorps.StripeService.Adapters.StripeExternalAccountTest do

@local_map %{
id_from_stripe: "ba_19SSZG2eZvKYlo2CXnmzYU5H",
account_id_from_stripe: "acct_1032D82eZvKYlo2C",
account_holder_name: "Jane Austen",
account_holder_type: "individual",
bank_name: "STRIPE TEST BANK",
Expand All @@ -32,14 +31,22 @@ defmodule CodeCorps.StripeService.Adapters.StripeExternalAccountTest do
fingerprint: "1JWtPxqbdX5Gamtc",
last4: "6789",
routing_number: "110000000",
status: "new",
stripe_connect_account_id: nil
status: "new"
}

describe "to_params/2" do
test "converts from stripe map to local properly" do
{:ok, result} = to_params(@stripe_external_account)
assert result == @local_map
connect_account = insert(:stripe_connect_account)

attrs_from_connect_account = %{
stripe_connect_account_id: connect_account.id,
account_id_from_stripe: connect_account.id_from_stripe
}

expected_result = @local_map |> Map.merge(attrs_from_connect_account)

{:ok, result} = to_params(@stripe_external_account, connect_account)
assert result == expected_result
end
end
end
Original file line number Diff line number Diff line change
@@ -1,29 +1,19 @@
defmodule CodeCorps.StripeService.StripeConnectExternalAccountServiceTest do
use ExUnit.Case, async: true

use CodeCorps.ModelCase

alias CodeCorps.StripeService.StripeConnectExternalAccountService

describe "create" do
test "creates a StripeExternalAccount" do
id_from_stripe = "ba_testing123"
account_id_from_stripe = "acct_123"

connect_account = insert(:stripe_connect_account, id_from_stripe: account_id_from_stripe)
api_external_account = %Stripe.ExternalAccount{id: "bnk_123"}
local_connect_account = insert(:stripe_connect_account)

{:ok, %CodeCorps.StripeExternalAccount{} = external_account} =
StripeConnectExternalAccountService.create(id_from_stripe, account_id_from_stripe)

assert external_account.id_from_stripe == id_from_stripe
assert external_account.stripe_connect_account_id == connect_account.id
end

test "returns {:error, :not_found} if there's no associated stripe connect account" do
id_from_stripe = "ba_testing123"
account_id_from_stripe = "acct_123"
StripeConnectExternalAccountService.create(api_external_account, local_connect_account)

assert {:error, :not_found} == StripeConnectExternalAccountService.create(id_from_stripe, account_id_from_stripe)
assert external_account.id_from_stripe == "bnk_123"
assert external_account.stripe_connect_account_id == local_connect_account.id
assert external_account.account_id_from_stripe == local_connect_account.id_from_stripe
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule CodeCorps.StripeService.WebhookProcessing.EventHandlerTest do
}

alias CodeCorps.{
StripeEvent, StripeExternalAccount, StripeInvoice, StripePlatformCard, StripePlatformCustomer
StripeEvent, StripeInvoice, StripePlatformCard, StripePlatformCustomer
}

defmodule CodeCorps.StripeService.WebhookProcessing.EventHandlerTest.StubObject do
Expand Down Expand Up @@ -58,21 +58,6 @@ defmodule CodeCorps.StripeService.WebhookProcessing.EventHandlerTest do
end

describe "connect events" do
test "handles account.external_account.created" do
connect_account = insert(:stripe_connect_account)
event = build_event(
"account.external_account.created",
%Stripe.ExternalAccount{id: "ext_123", account: connect_account.id_from_stripe}
)

{:ok, event} = EventHandler.handle(event, ConnectEventHandler)
assert event.object_type == "external_account"
assert event.object_id == "ext_123"
assert event.status == "processed"

assert Repo.get_by(StripeExternalAccount, id_from_stripe: "ext_123")
end

test "handles account.updated" do
connect_account = insert(:stripe_connect_account)
event = build_event("account.updated", %Stripe.Account{id: connect_account.id_from_stripe})
Expand Down

0 comments on commit d1641bd

Please sign in to comment.