Skip to content

Commit b254f39

Browse files
committed
implement 3 step flow
1 parent 2db92f3 commit b254f39

File tree

6 files changed

+231
-140
lines changed

6 files changed

+231
-140
lines changed

lib/algora/bounties/bounties.ex

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,9 @@ defmodule Algora.Bounties do
805805
bounty: bounty,
806806
claims: claims,
807807
recipient: opts[:recipient],
808-
capture_method: :manual
808+
capture_method: :manual,
809+
success_url: opts[:success_url],
810+
cancel_url: opts[:cancel_url]
809811
)
810812
end
811813

@@ -883,7 +885,9 @@ defmodule Algora.Bounties do
883885
bounty: Bounty.t(),
884886
claims: [Claim.t()],
885887
recipient: User.t(),
886-
capture_method: :automatic | :automatic_async | :manual
888+
capture_method: :automatic | :automatic_async | :manual,
889+
success_url: String.t(),
890+
cancel_url: String.t()
887891
]
888892
) ::
889893
{:ok, String.t()} | {:error, atom()}
@@ -903,11 +907,12 @@ defmodule Algora.Bounties do
903907
metadata: %{"version" => Payments.metadata_version(), "group_id" => tx_group_id}
904908
}
905909

906-
payment_intent_data =
910+
{payment_intent_data, session_opts} =
907911
if capture_method = opts[:capture_method] do
908-
Map.put(payment_intent_data, :capture_method, capture_method)
912+
{Map.put(payment_intent_data, :capture_method, capture_method),
913+
[success_url: opts[:success_url], cancel_url: opts[:cancel_url]]}
909914
else
910-
payment_intent_data
915+
{payment_intent_data, []}
911916
end
912917

913918
gross_amount = LineItem.gross_amount(line_items)
@@ -939,7 +944,12 @@ defmodule Algora.Bounties do
939944
group_id: tx_group_id
940945
}),
941946
{:ok, session} <-
942-
Payments.create_stripe_session(owner, Enum.map(line_items, &LineItem.to_stripe/1), payment_intent_data) do
947+
Payments.create_stripe_session(
948+
owner,
949+
Enum.map(line_items, &LineItem.to_stripe/1),
950+
payment_intent_data,
951+
session_opts
952+
) do
943953
{:ok, session.url}
944954
end
945955
end)

lib/algora/payments/payments.ex

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ defmodule Algora.Payments do
55

66
alias Algora.Accounts
77
alias Algora.Accounts.User
8+
alias Algora.Bounties
9+
alias Algora.Bounties.Bounty
10+
alias Algora.Bounties.Claim
11+
alias Algora.Bounties.Tip
12+
alias Algora.Contracts.Contract
813
alias Algora.MoneyUtils
914
alias Algora.Payments.Account
1015
alias Algora.Payments.Customer
@@ -31,18 +36,19 @@ defmodule Algora.Payments do
3136
@spec create_stripe_session(
3237
user :: User.t(),
3338
line_items :: [PSP.Session.line_item_data()],
34-
payment_intent_data :: PSP.Session.payment_intent_data()
39+
payment_intent_data :: PSP.Session.payment_intent_data(),
40+
opts :: Keyword.t()
3541
) ::
3642
{:ok, PSP.session()} | {:error, PSP.error()}
37-
def create_stripe_session(user, line_items, payment_intent_data) do
43+
def create_stripe_session(user, line_items, payment_intent_data, opts \\ []) do
3844
with {:ok, customer} <- fetch_or_create_customer(user) do
3945
opts = %{
4046
mode: "payment",
4147
customer: customer.provider_id,
4248
billing_address_collection: "required",
4349
line_items: line_items,
44-
success_url: "#{AlgoraWeb.Endpoint.url()}/payment/success",
45-
cancel_url: "#{AlgoraWeb.Endpoint.url()}/payment/canceled",
50+
success_url: opts[:success_url] || "#{AlgoraWeb.Endpoint.url()}/payment/success",
51+
cancel_url: opts[:cancel_url] || "#{AlgoraWeb.Endpoint.url()}/payment/canceled",
4652
payment_intent_data: payment_intent_data
4753
}
4854

@@ -544,4 +550,140 @@ defmodule Algora.Payments do
544550
{:error, error}
545551
end
546552
end
553+
554+
def process_charge(%Stripe.Event{type: "charge.succeeded", data: %{object: %Stripe.Charge{}}}, group_id)
555+
when not is_binary(group_id) do
556+
{:error, :invalid_group_id}
557+
end
558+
559+
def process_charge(
560+
"charge.succeeded",
561+
%Stripe.Charge{id: charge_id, captured: false, payment_intent: payment_intent_id},
562+
group_id
563+
) do
564+
Repo.transact(fn ->
565+
Repo.update_all(from(t in Transaction, where: t.group_id == ^group_id, where: t.type == :charge),
566+
set: [
567+
status: :requires_capture,
568+
provider: "stripe",
569+
provider_id: charge_id,
570+
provider_charge_id: charge_id,
571+
provider_payment_intent_id: payment_intent_id
572+
]
573+
)
574+
575+
broadcast()
576+
{:ok, nil}
577+
end)
578+
end
579+
580+
def process_charge(
581+
"charge.captured",
582+
%Stripe.Charge{id: charge_id, captured: true, payment_intent: payment_intent_id},
583+
group_id
584+
) do
585+
Repo.transact(fn ->
586+
Repo.update_all(from(t in Transaction, where: t.group_id == ^group_id, where: t.type == :charge),
587+
set: [
588+
status: :succeeded,
589+
provider: "stripe",
590+
provider_id: charge_id,
591+
provider_charge_id: charge_id,
592+
provider_payment_intent_id: payment_intent_id
593+
]
594+
)
595+
596+
Repo.update_all(from(t in Transaction, where: t.group_id == ^group_id, where: t.type != :charge),
597+
set: [
598+
status: :requires_release,
599+
provider: "stripe",
600+
provider_id: charge_id,
601+
provider_charge_id: charge_id,
602+
provider_payment_intent_id: payment_intent_id
603+
]
604+
)
605+
606+
broadcast()
607+
{:ok, nil}
608+
end)
609+
end
610+
611+
def process_charge(
612+
"charge.succeeded",
613+
%Stripe.Charge{id: charge_id, captured: true, payment_intent: payment_intent_id},
614+
group_id
615+
) do
616+
Repo.transact(fn ->
617+
{_, txs} =
618+
Repo.update_all(from(t in Transaction, where: t.group_id == ^group_id, select: t),
619+
set: [
620+
status: :succeeded,
621+
succeeded_at: DateTime.utc_now(),
622+
provider: "stripe",
623+
provider_id: charge_id,
624+
provider_charge_id: charge_id,
625+
provider_payment_intent_id: payment_intent_id
626+
]
627+
)
628+
629+
bounty_ids = txs |> Enum.map(& &1.bounty_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
630+
tip_ids = txs |> Enum.map(& &1.tip_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
631+
contract_ids = txs |> Enum.map(& &1.contract_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
632+
claim_ids = txs |> Enum.map(& &1.claim_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
633+
634+
Repo.update_all(from(b in Bounty, where: b.id in ^bounty_ids), set: [status: :paid])
635+
Repo.update_all(from(t in Tip, where: t.id in ^tip_ids), set: [status: :paid])
636+
Repo.update_all(from(c in Contract, where: c.id in ^contract_ids), set: [status: :paid])
637+
# TODO: add and use a new "paid" status for claims
638+
Repo.update_all(from(c in Claim, where: c.id in ^claim_ids), set: [status: :approved])
639+
640+
activities_result =
641+
txs
642+
|> Enum.filter(&(&1.type == :credit))
643+
|> Enum.reduce_while(:ok, fn tx, :ok ->
644+
case Repo.insert_activity(tx, %{type: :transaction_succeeded, notify_users: [tx.user_id]}) do
645+
{:ok, _} -> {:cont, :ok}
646+
error -> {:halt, error}
647+
end
648+
end)
649+
650+
jobs_result =
651+
txs
652+
|> Enum.filter(&(&1.type == :credit))
653+
|> Enum.reduce_while(:ok, fn credit, :ok ->
654+
case fetch_active_account(credit.user_id) do
655+
{:ok, _account} ->
656+
case %{credit_id: credit.id}
657+
|> Jobs.ExecutePendingTransfer.new()
658+
|> Oban.insert() do
659+
{:ok, _job} -> {:cont, :ok}
660+
error -> {:halt, error}
661+
end
662+
663+
{:error, :no_active_account} ->
664+
case %{credit_id: credit.id}
665+
|> Bounties.Jobs.PromptPayoutConnect.new()
666+
|> Oban.insert() do
667+
{:ok, _job} -> {:cont, :ok}
668+
error -> {:halt, error}
669+
end
670+
end
671+
end)
672+
673+
with txs when txs != [] <- txs,
674+
:ok <- activities_result,
675+
:ok <- jobs_result do
676+
broadcast()
677+
{:ok, nil}
678+
else
679+
{:error, reason} ->
680+
Logger.error("Failed to update transactions: #{inspect(reason)}")
681+
{:error, :failed_to_update_transactions}
682+
683+
error ->
684+
Logger.error("Failed to update transactions: #{inspect(error)}")
685+
{:error, :failed_to_update_transactions}
686+
end
687+
end)
688+
end
547689
end

lib/algora/payments/schemas/transaction.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule Algora.Payments.Transaction do
77
alias Algora.Types.Money
88

99
@transaction_types [:charge, :transfer, :reversal, :debit, :credit, :deposit, :withdrawal]
10-
@transaction_statuses [:initialized, :processing, :requires_capture, :succeeded, :failed, :canceled]
10+
@transaction_statuses [:initialized, :processing, :requires_capture, :requires_release, :succeeded, :failed, :canceled]
1111

1212
@derive {Inspect, except: [:provider_meta]}
1313
typed_schema "transactions" do

lib/algora/psp/psp.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ defmodule Algora.PSP do
133133
def capture(id, params \\ %{}), do: Algora.PSP.client(__MODULE__).capture(id, params)
134134
end
135135

136+
@type charge :: Algora.PSP.Charge.t()
137+
defmodule Charge do
138+
@moduledoc false
139+
140+
@type t :: Stripe.Charge.t()
141+
def retrieve(id), do: Algora.PSP.client(__MODULE__).retrieve(id)
142+
end
143+
136144
@type setup_intent :: Algora.PSP.SetupIntent.t()
137145
defmodule SetupIntent do
138146
@moduledoc false

lib/algora_web/controllers/webhooks/stripe_controller.ex

Lines changed: 7 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,8 @@ defmodule AlgoraWeb.Webhooks.StripeController do
22
@behaviour Stripe.WebhookHandler
33

44
import Ecto.Changeset
5-
import Ecto.Query
65

76
alias Algora.Bounties
8-
alias Algora.Bounties.Bounty
9-
alias Algora.Bounties.Claim
10-
alias Algora.Bounties.Tip
11-
alias Algora.Contracts.Contract
127
alias Algora.Payments
138
alias Algora.Payments.Customer
149
alias Algora.Payments.Transaction
@@ -47,21 +42,19 @@ defmodule AlgoraWeb.Webhooks.StripeController do
4742
{:error, error}
4843
end
4944

50-
defp process_event(
51-
%Stripe.Event{
52-
type: type,
53-
data: %{object: %Stripe.Charge{metadata: %{"version" => @metadata_version, "group_id" => group_id}}}
54-
} = event
55-
)
45+
defp process_event(%Stripe.Event{
46+
type: type,
47+
data: %{object: %Stripe.Charge{metadata: %{"version" => @metadata_version, "group_id" => group_id}} = charge}
48+
})
5649
when type in ["charge.succeeded", "charge.captured"] and is_binary(group_id) do
57-
process_charge_succeeded(event, group_id)
50+
Payments.process_charge(type, charge, group_id)
5851
end
5952

60-
defp process_event(%Stripe.Event{type: type, data: %{object: %Stripe.Charge{invoice: invoice_id}}} = event)
53+
defp process_event(%Stripe.Event{type: type, data: %{object: %Stripe.Charge{invoice: invoice_id} = charge}})
6154
when type in ["charge.succeeded", "charge.captured"] do
6255
with {:ok, invoice} <- Algora.PSP.Invoice.retrieve(invoice_id),
6356
%{"version" => @metadata_version, "group_id" => group_id} <- invoice.metadata do
64-
process_charge_succeeded(event, group_id)
57+
Payments.process_charge(type, charge, group_id)
6558
end
6659
end
6760

@@ -127,96 +120,6 @@ defmodule AlgoraWeb.Webhooks.StripeController do
127120
end
128121
end
129122

130-
defp process_charge_succeeded(
131-
%Stripe.Event{
132-
type: type,
133-
data: %{object: %Stripe.Charge{id: charge_id, captured: captured, payment_intent: payment_intent_id}}
134-
},
135-
group_id
136-
)
137-
when type in ["charge.succeeded", "charge.captured"] and is_binary(group_id) do
138-
Repo.transact(fn ->
139-
status = if captured, do: :succeeded, else: :requires_capture
140-
succeeded_at = if captured, do: DateTime.utc_now()
141-
142-
{_, txs} =
143-
Repo.update_all(from(t in Transaction, where: t.group_id == ^group_id, select: t),
144-
set: [
145-
status: status,
146-
succeeded_at: succeeded_at,
147-
provider: "stripe",
148-
provider_id: charge_id,
149-
provider_charge_id: charge_id,
150-
provider_payment_intent_id: payment_intent_id
151-
]
152-
)
153-
154-
if status == :succeeded do
155-
bounty_ids = txs |> Enum.map(& &1.bounty_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
156-
tip_ids = txs |> Enum.map(& &1.tip_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
157-
contract_ids = txs |> Enum.map(& &1.contract_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
158-
claim_ids = txs |> Enum.map(& &1.claim_id) |> Enum.reject(&is_nil/1) |> Enum.uniq()
159-
160-
Repo.update_all(from(b in Bounty, where: b.id in ^bounty_ids), set: [status: :paid])
161-
Repo.update_all(from(t in Tip, where: t.id in ^tip_ids), set: [status: :paid])
162-
Repo.update_all(from(c in Contract, where: c.id in ^contract_ids), set: [status: :paid])
163-
# TODO: add and use a new "paid" status for claims
164-
Repo.update_all(from(c in Claim, where: c.id in ^claim_ids), set: [status: :approved])
165-
166-
activities_result =
167-
txs
168-
|> Enum.filter(&(&1.type == :credit))
169-
|> Enum.reduce_while(:ok, fn tx, :ok ->
170-
case Repo.insert_activity(tx, %{type: :transaction_succeeded, notify_users: [tx.user_id]}) do
171-
{:ok, _} -> {:cont, :ok}
172-
error -> {:halt, error}
173-
end
174-
end)
175-
176-
jobs_result =
177-
txs
178-
|> Enum.filter(&(&1.type == :credit))
179-
|> Enum.reduce_while(:ok, fn credit, :ok ->
180-
case Payments.fetch_active_account(credit.user_id) do
181-
{:ok, _account} ->
182-
case %{credit_id: credit.id}
183-
|> Payments.Jobs.ExecutePendingTransfer.new()
184-
|> Oban.insert() do
185-
{:ok, _job} -> {:cont, :ok}
186-
error -> {:halt, error}
187-
end
188-
189-
{:error, :no_active_account} ->
190-
case %{credit_id: credit.id}
191-
|> Bounties.Jobs.PromptPayoutConnect.new()
192-
|> Oban.insert() do
193-
{:ok, _job} -> {:cont, :ok}
194-
error -> {:halt, error}
195-
end
196-
end
197-
end)
198-
199-
with txs when txs != [] <- txs,
200-
:ok <- activities_result,
201-
:ok <- jobs_result do
202-
Payments.broadcast()
203-
{:ok, nil}
204-
else
205-
{:error, reason} ->
206-
Logger.error("Failed to update transactions: #{inspect(reason)}")
207-
{:error, :failed_to_update_transactions}
208-
209-
_error ->
210-
Logger.error("Failed to update transactions")
211-
{:error, :failed_to_update_transactions}
212-
end
213-
else
214-
Payments.broadcast()
215-
{:ok, nil}
216-
end
217-
end)
218-
end
219-
220123
defp alert(%Stripe.Event{} = event, :ok) do
221124
Algora.Admin.alert("Stripe event: #{event.type} #{event.id} https://dashboard.stripe.com/logs?success=true", :info)
222125
end

0 commit comments

Comments
 (0)