Skip to content

Commit ea738c2

Browse files
committed
add stripe connect flow
1 parent 3459241 commit ea738c2

File tree

8 files changed

+219
-20
lines changed

8 files changed

+219
-20
lines changed

lib/algora/accounts/schemas/user.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ defmodule Algora.Accounts.User do
236236
:need_avatar,
237237
:website_url,
238238
:bio,
239+
:country,
239240
:location,
240241
:timezone
241242
])

lib/algora/integrations/stripe/connect_countries.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,6 @@ defmodule Algora.Stripe.ConnectCountries do
125125
{"Uzbekistan", "UZ"},
126126
{"Vietnam", "VN"}
127127
]
128+
129+
def list_codes, do: Enum.map(list(), &elem(&1, 1))
128130
end

lib/algora/payments/payments.ex

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ defmodule Algora.Payments do
22
@moduledoc false
33
import Ecto.Query
44

5+
alias Algora.Accounts
6+
alias Algora.Accounts.User
7+
alias Algora.Payments.Account
58
alias Algora.Payments.Customer
69
alias Algora.Payments.PaymentMethod
710
alias Algora.Payments.Transaction
811
alias Algora.Repo
12+
alias Algora.Util
913

1014
require Logger
1115

@@ -128,4 +132,95 @@ defmodule Algora.Payments do
128132
|> order_by([t], desc: t.inserted_at)
129133
|> Repo.all()
130134
end
135+
136+
def get_account(user_id, region) do
137+
Account
138+
|> where([a], a.user_id == ^user_id and a.region == ^region)
139+
|> Repo.one()
140+
end
141+
142+
@spec create_account(user :: User.t(), attrs :: %{optional(atom()) => any()}) ::
143+
{:ok, Account.t()} | {:error, any()}
144+
def create_account(user, attrs) do
145+
with {:ok, stripe_account} <- create_stripe_account(attrs) do
146+
attrs = %{
147+
provider: "stripe",
148+
provider_id: stripe_account.id,
149+
provider_meta: Util.normalize_struct(stripe_account),
150+
type: attrs.type,
151+
region: :US,
152+
user_id: user.id,
153+
country: attrs.country
154+
}
155+
156+
%Account{}
157+
|> Account.changeset(attrs)
158+
|> Repo.insert()
159+
end
160+
end
161+
162+
@spec create_stripe_account(attrs :: %{optional(atom()) => any()}) ::
163+
{:ok, Stripe.Account.t()} | {:error, Stripe.Error.t()}
164+
defp create_stripe_account(%{country: country, type: type}) do
165+
case Stripe.Account.create(%{country: country, type: type}) do
166+
{:ok, account} -> {:ok, account}
167+
{:error, _reason} -> Stripe.Account.create(%{type: type})
168+
end
169+
end
170+
171+
@spec create_account_link(account :: Account.t(), base_url :: String.t()) ::
172+
{:ok, Stripe.AccountLink.t()} | {:error, Stripe.Error.t()}
173+
def create_account_link(account, base_url) do
174+
Stripe.AccountLink.create(%{
175+
account: account.provider_id,
176+
refresh_url: "#{base_url}/callbacks/stripe/refresh",
177+
return_url: "#{base_url}/callbacks/stripe/return",
178+
type: "account_onboarding"
179+
})
180+
end
181+
182+
@spec create_login_link(account :: Account.t()) ::
183+
{:ok, Stripe.Account.t()} | {:error, Stripe.Error.t()}
184+
def create_login_link(account) do
185+
Stripe.Account.create_login_link(account.provider_id, %{})
186+
end
187+
188+
@spec refresh_stripe_account(user_id :: binary()) ::
189+
{:ok, Account.t()} | {:error, :account_not_found} | {:error, any()}
190+
def refresh_stripe_account(user_id) do
191+
case get_account(user_id, :US) do
192+
nil ->
193+
{:error, :account_not_found}
194+
195+
account ->
196+
with {:ok, stripe_account} <- Stripe.Account.retrieve(account.provider_id) do
197+
attrs = %{
198+
charges_enabled: stripe_account.charges_enabled,
199+
details_submitted: stripe_account.details_submitted,
200+
country: stripe_account.country,
201+
service_agreement: stripe_account.tos_acceptance.service_agreement,
202+
provider_meta: Util.normalize_struct(stripe_account)
203+
}
204+
205+
account
206+
|> Account.changeset(attrs)
207+
|> Repo.update()
208+
|> case do
209+
{:ok, updated_account} ->
210+
if stripe_account.charges_enabled do
211+
account.user_id
212+
|> Accounts.get_user!()
213+
|> Accounts.update_settings(%{country: stripe_account.country})
214+
end
215+
216+
# TODO: enqueue pending transfers
217+
218+
{:ok, updated_account}
219+
220+
error ->
221+
error
222+
end
223+
end
224+
end
225+
end
131226
end

lib/algora/payments/schemas/account.ex

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ defmodule Algora.Payments.Account do
22
@moduledoc false
33
use Algora.Schema
44

5+
alias Algora.Stripe
6+
57
@derive {Inspect, except: [:provider_meta]}
68
typed_schema "accounts" do
7-
field :provider, :string
8-
field :provider_id, :string
9-
field :provider_meta, :map
9+
field :provider, :string, null: false
10+
field :provider_id, :string, null: false
11+
field :provider_meta, :map, null: false
1012

1113
field :name, :string
12-
field :details_submitted, :boolean, default: false
13-
field :charges_enabled, :boolean, default: false
14+
field :details_submitted, :boolean, default: false, null: false
15+
field :charges_enabled, :boolean, default: false, null: false
1416
field :service_agreement, :string
15-
field :country, :string
16-
field :type, Ecto.Enum, values: [:standard, :express]
17-
field :region, Ecto.Enum, values: [:US, :EU]
18-
field :stale, :boolean, default: false
17+
field :country, :string, null: false
18+
field :type, Ecto.Enum, values: [:standard, :express], null: false
19+
field :region, Ecto.Enum, values: [:US, :EU], null: false
20+
field :stale, :boolean, default: false, null: false
1921

20-
belongs_to :user, Algora.Accounts.User
22+
belongs_to :user, Algora.Accounts.User, null: false
2123

2224
timestamps()
2325
end
@@ -34,8 +36,25 @@ defmodule Algora.Payments.Account do
3436
:country,
3537
:type,
3638
:region,
37-
:stale
39+
:stale,
40+
:user_id
41+
])
42+
|> validate_required([
43+
:provider,
44+
:provider_id,
45+
:provider_meta,
46+
:details_submitted,
47+
:charges_enabled,
48+
:country,
49+
:type,
50+
:region,
51+
:stale,
52+
:user_id
3853
])
39-
|> validate_required([:provider, :provider_id, :provider_meta])
54+
|> validate_inclusion(:type, [:standard, :express])
55+
|> validate_inclusion(:region, [:US, :EU])
56+
|> validate_inclusion(:country, Stripe.ConnectCountries.list_codes())
57+
|> foreign_key_constraint(:user_id)
58+
|> generate_id()
4059
end
4160
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule AlgoraWeb.StripeCallbackController do
2+
use AlgoraWeb, :controller
3+
4+
alias Algora.Payments
5+
6+
def refresh(conn, params), do: refresh_stripe_account(conn, params)
7+
def return(conn, params), do: refresh_stripe_account(conn, params)
8+
9+
defp refresh_stripe_account(conn, _params) do
10+
case conn.assigns[:current_user] do
11+
nil ->
12+
conn
13+
|> redirect(to: ~p"/auth/login")
14+
|> halt()
15+
16+
current_user ->
17+
case Payments.refresh_stripe_account(current_user.id) do
18+
{:ok, _account} ->
19+
redirect(conn, to: ~p"/user/transactions")
20+
21+
{:error, _reason} ->
22+
conn
23+
|> put_flash(:error, "Failed to refresh Stripe account")
24+
|> redirect(to: ~p"/user/transactions")
25+
end
26+
end
27+
end
28+
end

lib/algora_web/live/user/transactions_live.ex

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,44 @@ defmodule AlgoraWeb.User.TransactionsLive do
6363

6464
case changeset do
6565
%{valid?: true} = changeset ->
66-
# TODO: Actually create the payout account
67-
IO.inspect(changeset.changes, label: "Would create payout account with")
68-
69-
{:noreply,
70-
socket
71-
|> put_flash(:info, "Payout account created!")
72-
|> assign(:show_payout_drawer, false)}
66+
# Get or create Stripe account
67+
account = Payments.get_account(socket.assigns.current_user.id, :US)
68+
69+
result =
70+
if is_nil(account) do
71+
Payments.create_account(socket.assigns.current_user, %{
72+
country: changeset.changes.country,
73+
type: "express"
74+
})
75+
else
76+
{:ok, account}
77+
end
78+
79+
case result do
80+
{:ok, account} ->
81+
if account.charges_enabled do
82+
if account.type == :express do
83+
{:ok, %{url: url}} = Payments.create_login_link(account)
84+
85+
{:noreply, redirect(socket, external: url)}
86+
else
87+
{:noreply,
88+
socket
89+
|> put_flash(:info, "Account already set up!")
90+
|> assign(:show_payout_drawer, false)}
91+
end
92+
else
93+
{:ok, %{url: url}} = Payments.create_account_link(account, AlgoraWeb.Endpoint.url())
94+
95+
{:noreply, redirect(socket, external: url)}
96+
end
97+
98+
{:error, _reason} ->
99+
{:noreply,
100+
socket
101+
|> put_flash(:error, "Failed to create payout account")
102+
|> assign(:show_payout_drawer, false)}
103+
end
73104

74105
%{valid?: false} = changeset ->
75106
{:noreply, assign(socket, :payout_account_form, to_form(changeset))}
@@ -224,7 +255,7 @@ defmodule AlgoraWeb.User.TransactionsLive do
224255
</div>
225256
<.drawer show={@show_payout_drawer} on_cancel="close_drawer" direction="right">
226257
<.drawer_header>
227-
<.drawer_title>Create Payout Account</.drawer_title>
258+
<.drawer_title>Payout Account</.drawer_title>
228259
<.drawer_description>Create a payout account to receive your earnings</.drawer_description>
229260
</.drawer_header>
230261
<.drawer_content class="mt-4">

lib/algora_web/router.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ defmodule AlgoraWeb.Router do
4444

4545
get "/set_context/:context", ContextController, :set
4646

47+
get "/callbacks/stripe/refresh", StripeCallbackController, :refresh
48+
get "/callbacks/stripe/return", StripeCallbackController, :return
4749
get "/callbacks/:provider/oauth", OAuthCallbackController, :new
4850
get "/callbacks/:provider/installation", InstallationCallbackController, :new
4951
get "/auth/logout", OAuthCallbackController, :sign_out
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule Algora.Repo.Migrations.UpdateAccounts do
2+
use Ecto.Migration
3+
4+
def up do
5+
alter table(:accounts) do
6+
modify :service_agreement, :string, null: true
7+
end
8+
9+
drop index(:accounts, [:user_id])
10+
create unique_index(:accounts, [:user_id, :region])
11+
end
12+
13+
def down do
14+
alter table(:accounts) do
15+
modify :service_agreement, :string, null: false
16+
end
17+
18+
create index(:accounts, [:user_id])
19+
drop unique_index(:accounts, [:user_id, :region])
20+
end
21+
end

0 commit comments

Comments
 (0)