diff --git a/lib/algora/bounties/bounties.ex b/lib/algora/bounties/bounties.ex index 926875530..6faed3e17 100644 --- a/lib/algora/bounties/bounties.ex +++ b/lib/algora/bounties/bounties.ex @@ -871,7 +871,8 @@ defmodule Algora.Bounties do PSP.Invoice.create( %{ auto_advance: false, - customer: customer.provider_id + customer: customer.provider_id, + metadata: %{"version" => Payments.metadata_version(), "group_id" => tx_group_id} }, %{idempotency_key: idempotency_key} ), diff --git a/lib/algora/contracts/contracts.ex b/lib/algora/contracts/contracts.ex index 9be089f24..f003266e8 100644 --- a/lib/algora/contracts/contracts.ex +++ b/lib/algora/contracts/contracts.ex @@ -400,6 +400,7 @@ defmodule Algora.Contracts do defp maybe_generate_invoice(_contract, nil), do: {:ok, nil} defp maybe_generate_invoice(contract, charge) do + # TODO: add metadata to invoice %{"version" => Payments.metadata_version(), "group_id" => tx_group_id} invoice_params = %{auto_advance: false, customer: contract.client.customer.provider_id} with {:ok, invoice} <- Invoice.create(invoice_params, %{idempotency_key: "contract-#{contract.id}"}), diff --git a/lib/algora/psp/psp.ex b/lib/algora/psp/psp.ex index 9e33f80d7..ed71a36e2 100644 --- a/lib/algora/psp/psp.ex +++ b/lib/algora/psp/psp.ex @@ -39,6 +39,7 @@ defmodule Algora.PSP do when params: %{ optional(:auto_advance) => boolean, + :metadata => Algora.PSP.metadata(), :customer => Stripe.id() | Stripe.Customer.t() } | %{}, diff --git a/lib/algora_web/controllers/webhooks/github_controller.ex b/lib/algora_web/controllers/webhooks/github_controller.ex index 4c423b310..af617e86b 100644 --- a/lib/algora_web/controllers/webhooks/github_controller.ex +++ b/lib/algora_web/controllers/webhooks/github_controller.ex @@ -453,7 +453,7 @@ defmodule AlgoraWeb.Webhooks.GithubController do %{ owner: owner, amount: amount, - idempotency_key: "invoice-#{recipient.provider_login}-#{webhook.delivery}" + idempotency_key: "tip-#{recipient.provider_login}-#{webhook.delivery}" }, ticket_ref: ticket_ref, tip_id: tip.id, diff --git a/lib/algora_web/controllers/webhooks/stripe_controller.ex b/lib/algora_web/controllers/webhooks/stripe_controller.ex index 1a85d1763..cee5b63d1 100644 --- a/lib/algora_web/controllers/webhooks/stripe_controller.ex +++ b/lib/algora_web/controllers/webhooks/stripe_controller.ex @@ -49,11 +49,77 @@ defmodule AlgoraWeb.Webhooks.StripeController do @tracked_events ["charge.succeeded", "transfer.created", "checkout.session.completed"] - defp process_event(%Stripe.Event{ - type: "charge.succeeded", - data: %{object: %Stripe.Charge{metadata: %{"version" => @metadata_version, "group_id" => group_id}}} - }) + defp process_event( + %Stripe.Event{ + type: "charge.succeeded", + data: %{object: %Stripe.Charge{metadata: %{"version" => @metadata_version, "group_id" => group_id}}} + } = event + ) when is_binary(group_id) do + process_charge_succeeded(event, group_id) + end + + defp process_event( + %Stripe.Event{type: "charge.succeeded", data: %{object: %Stripe.Charge{invoice: invoice_id}}} = event + ) do + with {:ok, invoice} <- Algora.PSP.Invoice.retrieve(invoice_id), + %{"version" => @metadata_version, "group_id" => group_id} <- invoice.metadata do + process_charge_succeeded(event, group_id) + end + end + + defp process_event(%Stripe.Event{ + type: "transfer.created", + data: %{object: %Stripe.Transfer{metadata: %{"version" => @metadata_version}} = transfer} + }) do + with {:ok, transaction} <- Repo.fetch_by(Transaction, provider: "stripe", provider_id: transfer.id), + {:ok, _transaction} <- maybe_update_transaction(transaction, transfer), + {:ok, _job} <- Oban.insert(Bounties.Jobs.NotifyTransfer.new(%{transfer_id: transaction.id})) do + Payments.broadcast() + {:ok, nil} + else + error -> + Logger.error("Failed to update transaction: #{inspect(error)}") + {:error, :failed_to_update_transaction} + end + end + + defp process_event(%Stripe.Event{ + type: "checkout.session.completed", + data: %{object: %Stripe.Session{customer: customer_id, mode: "setup", setup_intent: setup_intent_id}} + }) do + with {:ok, setup_intent} <- Algora.PSP.SetupIntent.retrieve(setup_intent_id, %{}), + pm_id = setup_intent.payment_method, + {:ok, payment_method} <- Algora.PSP.PaymentMethod.attach(%{payment_method: pm_id, customer: customer_id}), + {:ok, customer} <- Repo.fetch_by(Customer, provider: "stripe", provider_id: customer_id), + {:ok, _} <- Payments.create_payment_method(customer, payment_method) do + Payments.broadcast() + :ok + end + end + + defp process_event(%Stripe.Event{type: type} = event) when type in @tracked_events do + Algora.Admin.alert("Unhandled Stripe event: #{event.type} #{event.id}", :error) + :ok + end + + defp process_event(_event), do: :ok + + defp maybe_update_transaction(transaction, transfer) do + if transaction.status == :succeeded do + {:ok, transaction} + else + transaction + |> change(%{ + status: :succeeded, + succeeded_at: DateTime.utc_now(), + provider_meta: Util.normalize_struct(transfer) + }) + |> Repo.update() + end + end + + defp process_charge_succeeded(%Stripe.Event{type: "charge.succeeded"}, group_id) when is_binary(group_id) do Repo.transact(fn -> {_, txs} = Repo.update_all(from(t in Transaction, where: t.group_id == ^group_id, select: t), @@ -118,57 +184,6 @@ defmodule AlgoraWeb.Webhooks.StripeController do end) end - defp process_event(%Stripe.Event{ - type: "transfer.created", - data: %{object: %Stripe.Transfer{metadata: %{"version" => @metadata_version}} = transfer} - }) do - with {:ok, transaction} <- Repo.fetch_by(Transaction, provider: "stripe", provider_id: transfer.id), - {:ok, _transaction} <- maybe_update_transaction(transaction, transfer), - {:ok, _job} <- Oban.insert(Bounties.Jobs.NotifyTransfer.new(%{transfer_id: transaction.id})) do - Payments.broadcast() - {:ok, nil} - else - error -> - Logger.error("Failed to update transaction: #{inspect(error)}") - {:error, :failed_to_update_transaction} - end - end - - defp process_event(%Stripe.Event{ - type: "checkout.session.completed", - data: %{object: %Stripe.Session{customer: customer_id, mode: "setup", setup_intent: setup_intent_id}} - }) do - with {:ok, setup_intent} <- Algora.PSP.SetupIntent.retrieve(setup_intent_id, %{}), - pm_id = setup_intent.payment_method, - {:ok, payment_method} <- Algora.PSP.PaymentMethod.attach(%{payment_method: pm_id, customer: customer_id}), - {:ok, customer} <- Repo.fetch_by(Customer, provider: "stripe", provider_id: customer_id), - {:ok, _} <- Payments.create_payment_method(customer, payment_method) do - Payments.broadcast() - :ok - end - end - - defp process_event(%Stripe.Event{type: type} = event) when type in @tracked_events do - Algora.Admin.alert("Unhandled Stripe event: #{event.type} #{event.id}", :error) - :ok - end - - defp process_event(_event), do: :ok - - defp maybe_update_transaction(transaction, transfer) do - if transaction.status == :succeeded do - {:ok, transaction} - else - transaction - |> change(%{ - status: :succeeded, - succeeded_at: DateTime.utc_now(), - provider_meta: Util.normalize_struct(transfer) - }) - |> Repo.update() - end - end - defp notify_event(%Stripe.Event{} = event, :ok) do discord_payload = %{ payload: %{