Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions lib/algora/bounties/bounties.ex
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,8 @@ defmodule Algora.Bounties do
net_amount: amount,
total_fee: Money.sub!(gross_amount, amount),
line_items: line_items,
group_id: tx_group_id
group_id: tx_group_id,
idempotency_key: "session-#{Nanoid.generate()}"
}),
{:ok, _transactions} <-
create_transaction_pairs(%{
Expand All @@ -641,7 +642,7 @@ defmodule Algora.Bounties do
end

@spec create_invoice(
%{owner: User.t(), amount: Money.t()},
%{owner: User.t(), amount: Money.t(), idempotency_key: String.t()},
opts :: [
ticket_ref: %{owner: String.t(), repo: String.t(), number: integer()},
tip_id: String.t(),
Expand All @@ -651,7 +652,7 @@ defmodule Algora.Bounties do
]
) ::
{:ok, PSP.invoice()} | {:error, atom()}
def create_invoice(%{owner: owner, amount: amount}, opts \\ []) do
def create_invoice(%{owner: owner, amount: amount, idempotency_key: idempotency_key}, opts \\ []) do
tx_group_id = Nanoid.generate()

line_items =
Expand All @@ -672,7 +673,8 @@ defmodule Algora.Bounties do
net_amount: amount,
total_fee: Money.sub!(gross_amount, amount),
line_items: line_items,
group_id: tx_group_id
group_id: tx_group_id,
idempotency_key: idempotency_key
}),
{:ok, _transactions} <-
create_transaction_pairs(%{
Expand All @@ -686,15 +688,19 @@ defmodule Algora.Bounties do
}),
{:ok, customer} <- Payments.fetch_or_create_customer(owner),
{:ok, invoice} <-
PSP.Invoice.create(%{
auto_advance: false,
customer: customer.provider_id
}),
PSP.Invoice.create(
%{
auto_advance: false,
customer: customer.provider_id
},
%{idempotency_key: idempotency_key}
),
{:ok, _line_items} <-
line_items
|> Enum.map(&LineItem.to_invoice_item(&1, invoice, customer))
|> Enum.reduce_while({:ok, []}, fn params, {:ok, acc} ->
case PSP.Invoiceitem.create(params) do
|> Enum.with_index()
|> Enum.reduce_while({:ok, []}, fn {params, index}, {:ok, acc} ->
case PSP.Invoiceitem.create(params, %{idempotency_key: "#{idempotency_key}-#{index}"}) do
{:ok, item} -> {:cont, {:ok, [item | acc]}}
{:error, error} -> {:halt, {:error, error}}
end
Expand All @@ -711,7 +717,8 @@ defmodule Algora.Bounties do
net_amount: net_amount,
total_fee: total_fee,
line_items: line_items,
group_id: group_id
group_id: group_id,
idempotency_key: idempotency_key
}) do
%Transaction{}
|> change(%{
Expand All @@ -724,12 +731,14 @@ defmodule Algora.Bounties do
net_amount: net_amount,
total_fee: total_fee,
line_items: Util.normalize_struct(line_items),
group_id: group_id
group_id: group_id,
idempotency_key: idempotency_key
})
|> Algora.Validations.validate_positive(:gross_amount)
|> Algora.Validations.validate_positive(:net_amount)
|> Algora.Validations.validate_positive(:total_fee)
|> foreign_key_constraint(:user_id)
|> unique_constraint([:idempotency_key])
|> Repo.insert()
end

Expand Down
27 changes: 17 additions & 10 deletions lib/algora/contracts/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ defmodule Algora.Contracts do
defp maybe_generate_invoice(contract, charge) do
invoice_params = %{auto_advance: false, customer: contract.client.customer.provider_id}

with {:ok, invoice} <- Invoice.create(invoice_params),
with {:ok, invoice} <- Invoice.create(invoice_params, %{idempotency_key: "contract-#{contract.id}"}),
{:ok, _line_items} <- create_line_items(contract, invoice, charge.line_items) do
{:ok, invoice}
end
Expand Down Expand Up @@ -441,14 +441,19 @@ defmodule Algora.Contracts do
end

defp create_line_items(contract, invoice, line_items) do
Enum.reduce_while(line_items, {:ok, []}, fn line_item, {:ok, acc} ->
case Algora.PSP.Invoiceitem.create(%{
invoice: invoice.id,
customer: contract.client.customer.provider_id,
amount: MoneyUtils.to_minor_units(line_item.amount),
currency: to_string(line_item.amount.currency),
description: line_item.description
}) do
line_items
|> Enum.with_index()
|> Enum.reduce_while({:ok, []}, fn {line_item, index}, {:ok, acc} ->
case Algora.PSP.Invoiceitem.create(
%{
invoice: invoice.id,
customer: contract.client.customer.provider_id,
amount: MoneyUtils.to_minor_units(line_item.amount),
currency: to_string(line_item.amount.currency),
description: line_item.description
},
%{idempotency_key: "contract-#{contract.id}-#{index}"}
) do
{:ok, item} -> {:cont, {:ok, [item | acc]}}
{:error, error} -> {:halt, {:error, error}}
end
Expand All @@ -460,7 +465,9 @@ defmodule Algora.Contracts do
defp maybe_pay_invoice(contract, invoice, txs) do
pm_id = contract.client.customer.default_payment_method.provider_id

case Invoice.pay(invoice.id, %{off_session: true, payment_method: pm_id}) do
case Invoice.pay(invoice.id, %{off_session: true, payment_method: pm_id}, %{
idempotency_key: "contract-#{contract.id}"
}) do
{:ok, stripe_invoice} ->
if stripe_invoice.paid, do: release_funds(contract, stripe_invoice, txs)
{:ok, stripe_invoice}
Expand Down
5 changes: 4 additions & 1 deletion lib/algora/payments/schemas/transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defmodule Algora.Payments.Transaction do
field :succeeded_at, :utc_datetime_usec
field :reversed_at, :utc_datetime_usec
field :group_id, :string
field :idempotency_key, :string

belongs_to :timesheet, Algora.Contracts.Timesheet
belongs_to :contract, Contract
Expand Down Expand Up @@ -63,7 +64,8 @@ defmodule Algora.Payments.Transaction do
:contract_id,
:original_contract_id,
:user_id,
:succeeded_at
:succeeded_at,
:idempotency_key
])
|> validate_required([
:id,
Expand All @@ -79,6 +81,7 @@ defmodule Algora.Payments.Transaction do
:original_contract_id,
:user_id
])
|> unique_constraint([:idempotency_key])
|> foreign_key_constraint(:user_id)
|> foreign_key_constraint(:contract_id)
|> foreign_key_constraint(:original_contract_id)
Expand Down
89 changes: 70 additions & 19 deletions lib/algora/psp/psp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,75 @@ defmodule Algora.PSP do

@type error :: Stripe.Error.t()

@type invoice :: Stripe.Invoice.t()
@type metadata :: %{
optional(String.t()) => String.t()
}

@type invoice :: Algora.PSP.Invoice.t()
defmodule Invoice do
@moduledoc false
@type t :: Stripe.Invoice.t()

@spec create(params, options) :: {:ok, t} | {:error, Algora.PSP.error()}
when params:
%{
optional(:auto_advance) => boolean,
:customer => Stripe.id() | Stripe.Customer.t()
}
| %{},
options: %{
:idempotency_key => String.t()
}
def create(params, opts), do: Algora.PSP.client(__MODULE__).create(params, Keyword.new(opts))

@spec pay(Stripe.id() | t, params, options) :: {:ok, t} | {:error, Algora.PSP.error()}
when params:
%{
optional(:off_session) => boolean,
optional(:payment_method) => String.t()
}
| %{},
options: %{
:idempotency_key => String.t()
}
def pay(invoice_id, params, opts), do: Algora.PSP.client(__MODULE__).pay(invoice_id, params, Keyword.new(opts))

def create(params), do: Algora.PSP.client(__MODULE__).create(params)
def pay(invoice_id, params), do: Algora.PSP.client(__MODULE__).pay(invoice_id, params)
def retrieve(id), do: Algora.PSP.client(__MODULE__).retrieve(id)
def retrieve(id, opts), do: Algora.PSP.client(__MODULE__).retrieve(id, opts)
def retrieve(id, opts), do: Algora.PSP.client(__MODULE__).retrieve(id, Keyword.new(opts))
end

@type invoiceitem :: Stripe.Invoiceitem.t()
@type invoiceitem :: Algora.PSP.Invoiceitem.t()
defmodule Invoiceitem do
@moduledoc false

def create(params), do: Algora.PSP.client(__MODULE__).create(params)
@type t :: Stripe.Invoiceitem.t()

@spec create(params, options) :: {:ok, t} | {:error, Algora.PSP.error()}
when params:
%{
optional(:amount) => integer,
:currency => String.t(),
:customer => Stripe.id() | Stripe.Customer.t(),
optional(:description) => String.t(),
optional(:invoice) => Stripe.id() | Stripe.Invoice.t()
}
| %{},
options: %{
:idempotency_key => String.t()
}
def create(params, opts), do: Algora.PSP.client(__MODULE__).create(params, Keyword.new(opts))
end

@type transfer :: Stripe.Transfer.t()
@type transfer :: Algora.PSP.Transfer.t()
defmodule Transfer do
@moduledoc false
@type t :: Stripe.Transfer.t()

@spec create(params, options) :: {:ok, Algora.PSP.transfer()} | {:error, Algora.PSP.error()}
@spec create(params, options) :: {:ok, t} | {:error, Algora.PSP.error()}
when params: %{
:amount => pos_integer,
:currency => String.t(),
:destination => String.t(),
optional(:metadata) => Stripe.Types.metadata(),
optional(:metadata) => Algora.PSP.metadata(),
optional(:source_transaction) => String.t(),
optional(:transfer_group) => String.t(),
optional(:description) => String.t(),
Expand All @@ -64,73 +106,82 @@ defmodule Algora.PSP do
def create(params, opts), do: Algora.PSP.client(__MODULE__).create(params, Keyword.new(opts))
end

@type session :: Stripe.Session.t()
@type session :: Algora.PSP.Session.t()
defmodule Session do
@moduledoc false

@type t :: Stripe.Session.t()
@type line_item_data :: Stripe.Session.line_item_data()
@type payment_intent_data :: Stripe.Session.payment_intent_data()

def create(params), do: Algora.PSP.client(__MODULE__).create(params)
end

@type payment_method :: Stripe.PaymentMethod.t()
@type payment_method :: Algora.PSP.PaymentMethod.t()
defmodule PaymentMethod do
@moduledoc false

@type t :: Stripe.PaymentMethod.t()
def attach(params), do: Algora.PSP.client(__MODULE__).attach(params)
def retrieve(id), do: Algora.PSP.client(__MODULE__).retrieve(id)
end

@type payment_intent :: Stripe.PaymentIntent.t()
@type payment_intent :: Algora.PSP.PaymentIntent.t()
defmodule PaymentIntent do
@moduledoc false

@type t :: Stripe.PaymentIntent.t()
def create(params), do: Algora.PSP.client(__MODULE__).create(params)
end

@type setup_intent :: Stripe.SetupIntent.t()
@type setup_intent :: Algora.PSP.SetupIntent.t()
defmodule SetupIntent do
@moduledoc false

@type t :: Stripe.SetupIntent.t()
def retrieve(id, params), do: Algora.PSP.client(__MODULE__).retrieve(id, params)
end

@type customer :: Stripe.Customer.t()
@type customer :: Algora.PSP.Customer.t()
defmodule Customer do
@moduledoc false

@type t :: Stripe.Customer.t()
def retrieve(id), do: Algora.PSP.client(__MODULE__).retrieve(id)
def create(params), do: Algora.PSP.client(__MODULE__).create(params)
end

@type account :: Stripe.Account.t()
@type account :: Algora.PSP.Account.t()
defmodule Account do
@moduledoc false

@type t :: Stripe.Account.t()
def retrieve(id), do: Algora.PSP.client(__MODULE__).retrieve(id)
def create(params), do: Algora.PSP.client(__MODULE__).create(params)
def delete(id), do: Algora.PSP.client(__MODULE__).delete(id)
end

@type account_link :: Stripe.AccountLink.t()
@type account_link :: Algora.PSP.AccountLink.t()
defmodule AccountLink do
@moduledoc false

@type t :: Stripe.AccountLink.t()
def create(params), do: Algora.PSP.client(__MODULE__).create(params)
end

@type login_link :: Stripe.LoginLink.t()
@type login_link :: Algora.PSP.LoginLink.t()
defmodule LoginLink do
@moduledoc false

@type t :: Stripe.LoginLink.t()
def create(params), do: Algora.PSP.client(__MODULE__).create(params)
end

@type balance_transaction :: Stripe.BalanceTransaction.t()
@type balance_transaction :: Algora.PSP.BalanceTransaction.t()
defmodule BalanceTransaction do
@moduledoc false

@type t :: Stripe.BalanceTransaction.t()
def retrieve(id), do: Algora.PSP.client(__MODULE__).retrieve(id)
end
end
17 changes: 12 additions & 5 deletions lib/algora_web/controllers/webhooks/github_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,14 @@ defmodule AlgoraWeb.Webhooks.GithubController do

autopay_result =
if autopayable_bounty do
idempotency_key = "bounty-#{autopayable_bounty.id}"

with {:ok, invoice} <-
Bounties.create_invoice(
%{
owner: autopayable_bounty.owner,
amount: autopayable_bounty.amount
amount: autopayable_bounty.amount,
idempotency_key: idempotency_key
},
ticket_ref: %{
owner: payload["repository"]["owner"]["login"],
Expand All @@ -152,10 +155,14 @@ defmodule AlgoraWeb.Webhooks.GithubController do
claims: claims
),
{:ok, _invoice} <-
Algora.PSP.Invoice.pay(invoice, %{
payment_method: autopayable_bounty.owner.customer.default_payment_method.provider_id,
off_session: true
}) do
Algora.PSP.Invoice.pay(
invoice,
%{
payment_method: autopayable_bounty.owner.customer.default_payment_method.provider_id,
off_session: true
},
%{idempotency_key: idempotency_key}
) do
Logger.info("Autopay successful (#{autopayable_bounty.owner.name} - #{autopayable_bounty.amount}).")
:ok
else
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Algora.Repo.Migrations.AddIdempotencyKeyToTransaction do
use Ecto.Migration

def change do
alter table(:transactions) do
add :idempotency_key, :string
end

create unique_index(:transactions, [:idempotency_key])
end
end
Loading