Accrue.Connect (accrue v0.3.0)

Copy Markdown View Source

Stripe Connect domain facade.

Wraps the Accrue.Processor Connect callbacks with:

Soft-delete semantics: delete_account/2 tombstones the local row via deauthorized_at rather than hard-deleting it (audit-friendly).

Summary

Functions

Creates a new connected account through the configured processor, then upserts the local accrue_connect_accounts row and records an "connect.account.created" event in the same transaction.

Bang variant of create_account/2. Raises on failure.

Creates a Stripe Connect Account Link for hosted onboarding or account-update flows.

Creates a Stripe Express dashboard Login Link for a connected account.

Bang variant of create_login_link/2. Raises on failure.

Reads the currently-scoped connected account id from the pdict (or nil).

Deletes a connected account through the processor and tombstones the local row via deauthorized_at (soft delete — audit trail is never hard-deleted).

Clears the connected-account scope from the process dictionary.

Creates a Stripe Connect destination charge.

Local-first fetch: returns the persisted %Account{} row by stripe account id, falling back to retrieve_account/2 on miss (which upserts the local row as a side-effect).

Lists connected accounts through the processor (pass-through).

Computes a platform fee as a pure Accrue.Money value.

Bang variant of platform_fee/2. Raises on validation failure.

Writes the connected-account scope to the process dictionary without restoring afterwards. Used by Accrue.Plug.PutConnectedAccount and the bundled Oban middleware, where the scope lifetime matches the request/job lifetime rather than a lexical block.

Rejects a connected account through the processor. reason is a bare string per the Stripe API (e.g. "fraud", "terms_of_service").

Normalizes a caller-supplied account reference to a bare stripe account id string. Accepts %Account{}, a binary, or nil (returns nil — caller-side authorization is the host's responsibility).

Retrieves a connected account through the processor and upserts the local row (force_status_changeset path — out-of-order webhooks can arrive before first retrieve). Returns {:ok, %Account{}}.

Creates a separate charge and transfer flow.

Creates a standalone Transfer from the platform balance to a connected account.

Bang variant of transfer/2. Raises on failure.

Updates a connected account through the processor. Nested params (capabilities:, settings: %{payouts: %{schedule: ...}}) are forwarded verbatim to Stripe.

Runs fun with the connected-account scope set in the process dictionary, restoring the prior value (or clearing it) in an after block even if fun raises. Mirrors Accrue.Stripe.with_api_version/2.

Functions

create_account(params, opts \\ [])

@spec create_account(
  map() | keyword(),
  keyword()
) :: {:ok, Accrue.Connect.Account.t()} | {:error, term()}

Creates a new connected account through the configured processor, then upserts the local accrue_connect_accounts row and records an "connect.account.created" event in the same transaction.

Options

See @create_schema in the module source for the full NimbleOptions schema. :type is required (no default — host explicitly picks :standard/:express/:custom).

create_account!(params, opts \\ [])

@spec create_account!(
  map() | keyword(),
  keyword()
) :: Accrue.Connect.Account.t()

Bang variant of create_account/2. Raises on failure.

create_account_link(account, opts \\ [])

@spec create_account_link(
  Accrue.Connect.Account.t() | String.t(),
  keyword()
) :: {:ok, Accrue.Connect.AccountLink.t()} | {:error, term()}

Creates a Stripe Connect Account Link for hosted onboarding or account-update flows.

Accepts either an %Account{} struct, a bare "acct_..." binary, or a map with an :account key. :return_url and :refresh_url are required per @account_link_schema.

Returns {:ok, %Accrue.Connect.AccountLink{}} on success. The returned struct masks its :url field in Inspect output — treat the URL as a short-lived bearer credential and redirect the user immediately.

Options

  • :return_url (required) — where Stripe redirects on completion
  • :refresh_url (required) — where Stripe redirects if the link expires
  • :type"account_onboarding" (default) or "account_update"
  • :collect"currently_due" (default) or "eventually_due"

create_account_link!(account, opts \\ [])

@spec create_account_link!(
  Accrue.Connect.Account.t() | String.t(),
  keyword()
) :: Accrue.Connect.AccountLink.t()

Bang variant of create_account_link/2. Raises on failure.

create_login_link(account, opts \\ [])

@spec create_login_link(
  Accrue.Connect.Account.t() | String.t(),
  keyword()
) :: {:ok, Accrue.Connect.LoginLink.t()} | {:error, term()}

Creates a Stripe Express dashboard Login Link for a connected account.

Only Express accounts are supported. Standard and Custom accounts are rejected locally before reaching the processor to avoid leaking "acct_X is Standard" via a Stripe 400 error payload. The local row is consulted first; on a miss the account is retrieved from the processor.

Returns {:ok, %Accrue.Connect.LoginLink{}} on success. The returned struct masks its :url field in Inspect output — treat the URL as a short-lived bearer credential and redirect the user immediately.

create_login_link!(account, opts \\ [])

@spec create_login_link!(
  Accrue.Connect.Account.t() | String.t(),
  keyword()
) :: Accrue.Connect.LoginLink.t()

Bang variant of create_login_link/2. Raises on failure.

current_account_id()

@spec current_account_id() :: String.t() | nil

Reads the currently-scoped connected account id from the pdict (or nil).

delete_account(acct_id, opts \\ [])

@spec delete_account(
  String.t(),
  keyword()
) :: {:ok, Accrue.Connect.Account.t()} | {:error, term()}

Deletes a connected account through the processor and tombstones the local row via deauthorized_at (soft delete — audit trail is never hard-deleted).

delete_account!(acct_id, opts \\ [])

@spec delete_account!(
  String.t(),
  keyword()
) :: Accrue.Connect.Account.t()

Bang variant of delete_account/2.

delete_account_id()

@spec delete_account_id() :: :ok

Clears the connected-account scope from the process dictionary.

destination_charge(params, opts \\ [])

@spec destination_charge(
  map() | keyword(),
  keyword()
) :: {:ok, Accrue.Billing.Charge.t()} | {:error, term()}

Creates a Stripe Connect destination charge.

A destination charge is a single platform-scoped charge whose transfer_data.destination points at a connected account. Stripe handles the platform-to-connected-account settlement on your behalf; you do not need to issue a separate Transfer. An optional :application_fee_amount (pre-computed via Accrue.Connect.platform_fee/2) is forwarded to Stripe verbatim.

This call is always PLATFORM-scoped. The Stripe-Account header is explicitly unset regardless of any with_account/2 scope the caller may be inside — Pitfall 2 would otherwise cause Stripe to 400.

Required parameters

  • :amount%Accrue.Money{} gross charge amount
  • :destination%Accrue.Connect.Account{} struct or "acct_..." binary
  • :customer%Accrue.Billing.Customer{} struct

Optional parameters

  • :application_fee_amount%Accrue.Money{} platform fee. Compute via Accrue.Connect.platform_fee/2 and pass through — fees are caller-injected (never auto-applied).
  • :description, :metadata, :statement_descriptor
  • :payment_method — processor payment method id

Returns {:ok, %Accrue.Billing.Charge{}} on success. The local charge row is persisted and bundled with the resolved destination account via the charge's data jsonb field.

Examples

{:ok, fee} = Accrue.Connect.platform_fee(Accrue.Money.new(10_000, :usd))

{:ok, %Accrue.Billing.Charge{} = charge} =
  Accrue.Connect.destination_charge(
    %{
      amount: Accrue.Money.new(10_000, :usd),
      destination: connected_account,
      customer: customer
    },
    application_fee_amount: fee,
    payment_method: "pm_..."
  )

destination_charge!(params, opts \\ [])

@spec destination_charge!(
  map() | keyword(),
  keyword()
) :: Accrue.Billing.Charge.t()

Bang variant of destination_charge/2. Raises on failure.

fetch_account(acct_id, opts \\ [])

@spec fetch_account(
  String.t(),
  keyword()
) :: {:ok, Accrue.Connect.Account.t()} | {:error, term()}

Local-first fetch: returns the persisted %Account{} row by stripe account id, falling back to retrieve_account/2 on miss (which upserts the local row as a side-effect).

fetch_account!(acct_id, opts \\ [])

@spec fetch_account!(
  String.t(),
  keyword()
) :: Accrue.Connect.Account.t()

Bang variant of fetch_account/2.

list_accounts(opts \\ [])

@spec list_accounts(keyword()) :: {:ok, map()} | {:error, term()}

Lists connected accounts through the processor (pass-through).

platform_fee(gross, opts \\ [])

@spec platform_fee(
  Accrue.Money.t(),
  keyword()
) :: {:ok, Accrue.Money.t()} | {:error, Exception.t()}

Computes a platform fee as a pure Accrue.Money value.

Caller-inject semantics. This helper returns the computed fee; it does NOT auto-apply the value to any charge or transfer. Callers thread the result into application_fee_amount: on their own charge/transfer calls so the fee line is always auditable at the call site.

See Accrue.Connect.PlatformFee for the full computation order and clamp semantics. Defaults come from the :platform_fee sub-key of the :connect config (Accrue.Config.get!(:connect)), which ships with Stripe's standard 2.9% baseline and no fixed/min/max.

Examples

iex> {:ok, fee} = Accrue.Connect.platform_fee(
...>   Accrue.Money.new(10_000, :usd),
...>   percent: Decimal.new("2.9"),
...>   fixed: Accrue.Money.new(30, :usd)
...> )
iex> fee
%Accrue.Money{amount_minor: 320, currency: :usd}

platform_fee!(gross, opts \\ [])

@spec platform_fee!(
  Accrue.Money.t(),
  keyword()
) :: Accrue.Money.t()

Bang variant of platform_fee/2. Raises on validation failure.

put_account_id(id)

@spec put_account_id(String.t() | nil) :: :ok

Writes the connected-account scope to the process dictionary without restoring afterwards. Used by Accrue.Plug.PutConnectedAccount and the bundled Oban middleware, where the scope lifetime matches the request/job lifetime rather than a lexical block.

reject_account(acct_id, reason, opts \\ [])

@spec reject_account(String.t(), String.t(), keyword()) ::
  {:ok, Accrue.Connect.Account.t()} | {:error, term()}

Rejects a connected account through the processor. reason is a bare string per the Stripe API (e.g. "fraud", "terms_of_service").

reject_account!(acct_id, reason, opts \\ [])

@spec reject_account!(String.t(), String.t(), keyword()) :: Accrue.Connect.Account.t()

Bang variant of reject_account/3.

resolve_account_id(id)

@spec resolve_account_id(Accrue.Connect.Account.t() | String.t() | nil) ::
  String.t() | nil

Normalizes a caller-supplied account reference to a bare stripe account id string. Accepts %Account{}, a binary, or nil (returns nil — caller-side authorization is the host's responsibility).

retrieve_account(acct_id, opts \\ [])

@spec retrieve_account(
  String.t(),
  keyword()
) :: {:ok, Accrue.Connect.Account.t()} | {:error, term()}

Retrieves a connected account through the processor and upserts the local row (force_status_changeset path — out-of-order webhooks can arrive before first retrieve). Returns {:ok, %Account{}}.

retrieve_account!(acct_id, opts \\ [])

@spec retrieve_account!(
  String.t(),
  keyword()
) :: Accrue.Connect.Account.t()

Bang variant of retrieve_account/2.

separate_charge_and_transfer(params, opts \\ [])

@spec separate_charge_and_transfer(
  map() | keyword(),
  keyword()
) ::
  {:ok, %{charge: Accrue.Billing.Charge.t(), transfer: map()}}
  | {:error, term()}

Creates a separate charge and transfer flow.

Two distinct API calls:

  1. Processor.create_charge/2 on the PLATFORM (no Stripe-Account header, no transfer_data). Charges the customer against the platform balance.
  2. Processor.create_transfer/2 to the connected account, source_transaction linked to the charge above, moving a caller-specified amount from the platform balance to the connected-account balance.

Use this flow when the platform needs explicit control over the transfer step — e.g. delayed transfers, split destinations, or transfers that are not a fixed percentage of the gross.

Returns {:ok, %{charge: %Charge{}, transfer: map()}} on success.

If the transfer step fails after the charge succeeded, returns {:error, {:transfer_failed, %Charge{}, error}} so callers can reconcile — the charge row is persisted but no transfer exists.

Required parameters

  • :amount%Money{} gross charge amount
  • :customer%Accrue.Billing.Customer{} struct
  • :destination — connected account
  • :transfer_amount%Money{} amount to forward (platform keeps the difference as its fee; Accrue does NOT compute this for you — caller-inject semantics)

separate_charge_and_transfer!(params, opts \\ [])

@spec separate_charge_and_transfer!(
  map() | keyword(),
  keyword()
) :: %{charge: Accrue.Billing.Charge.t(), transfer: map()}

Bang variant of separate_charge_and_transfer/2. Raises on failure.

transfer(params, opts \\ [])

@spec transfer(
  map() | keyword(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Creates a standalone Transfer from the platform balance to a connected account.

This is the bare Transfers API — a thin wrapper over Processor.create_transfer/2. Use when you need to manually move funds outside a charge flow (e.g. revenue share payouts from an accumulated platform balance).

Accrue does not ship a dedicated accrue_connect_transfers table; each successful call appends a connect.transfer row to accrue_events via Accrue.Events.record/1.

Returns {:ok, map()} (the bare processor response).

Required parameters

  • :amount%Money{}
  • :destination — connected account

transfer!(params, opts \\ [])

@spec transfer!(
  map() | keyword(),
  keyword()
) :: map()

Bang variant of transfer/2. Raises on failure.

update_account(acct_id, params, opts \\ [])

@spec update_account(String.t(), map(), keyword()) ::
  {:ok, Accrue.Connect.Account.t()} | {:error, term()}

Updates a connected account through the processor. Nested params (capabilities:, settings: %{payouts: %{schedule: ...}}) are forwarded verbatim to Stripe.

update_account!(acct_id, params, opts \\ [])

@spec update_account!(String.t(), map(), keyword()) :: Accrue.Connect.Account.t()

Bang variant of update_account/3.

with_account(account_or_id, fun)

@spec with_account(Accrue.Connect.Account.t() | String.t() | nil, (-> result)) ::
  result
when result: var

Runs fun with the connected-account scope set in the process dictionary, restoring the prior value (or clearing it) in an after block even if fun raises. Mirrors Accrue.Stripe.with_api_version/2.

Accepts a stripe account id string, a %Accrue.Connect.Account{} struct, or nil (nil clears any existing scope for the block's lifetime — useful for temporarily stepping back to platform scope from inside a nested block).