Stripe Connect domain facade.
Wraps the Accrue.Processor Connect callbacks with:
with_account/2— process-dictionary scoped block that threads astripe_accountid through every nested processor call via the:accrue_connected_account_idkey. This is the same keyAccrue.Processor.Stripe.resolve_stripe_account/1reads, and the bundled Oban middleware restores it across the enqueue → perform boundary.create_account/2..list_accounts/1dual bang/tuple facade (mirrorsAccrue.BillingPortal.Session).- Local projection upsert via
Accrue.Connect.Projection.decompose/1Accrue.Connect.Account.changeset/2, wrapped in a singleAccrue.Repo.transact/1block withAccrue.Events.record_multi/3so the state mutation + audit row commit atomically.
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.
Bang variant of create_account_link/2. Raises on failure.
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).
Bang variant of delete_account/2.
Clears the connected-account scope from the process dictionary.
Creates a Stripe Connect destination charge.
Bang variant of destination_charge/2. Raises on failure.
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).
Bang variant of fetch_account/2.
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").
Bang variant of reject_account/3.
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{}}.
Bang variant of retrieve_account/2.
Creates a separate charge and transfer flow.
Bang variant of separate_charge_and_transfer/2. Raises on failure.
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.
Bang variant of update_account/3.
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
@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).
@spec create_account!( map() | keyword(), keyword() ) :: Accrue.Connect.Account.t()
Bang variant of create_account/2. Raises on failure.
@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"
@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.
@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.
@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.
@spec current_account_id() :: String.t() | nil
Reads the currently-scoped connected account id from the pdict (or nil).
@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).
@spec delete_account!( String.t(), keyword() ) :: Accrue.Connect.Account.t()
Bang variant of delete_account/2.
@spec delete_account_id() :: :ok
Clears the connected-account scope from the process dictionary.
@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 viaAccrue.Connect.platform_fee/2and 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_..."
)
@spec destination_charge!( map() | keyword(), keyword() ) :: Accrue.Billing.Charge.t()
Bang variant of destination_charge/2. Raises on failure.
@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).
@spec fetch_account!( String.t(), keyword() ) :: Accrue.Connect.Account.t()
Bang variant of fetch_account/2.
Lists connected accounts through the processor (pass-through).
@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}
@spec platform_fee!( Accrue.Money.t(), keyword() ) :: Accrue.Money.t()
Bang variant of platform_fee/2. Raises on validation failure.
@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.
@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").
@spec reject_account!(String.t(), String.t(), keyword()) :: Accrue.Connect.Account.t()
Bang variant of reject_account/3.
@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).
@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{}}.
@spec retrieve_account!( String.t(), keyword() ) :: Accrue.Connect.Account.t()
Bang variant of retrieve_account/2.
@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:
Processor.create_charge/2on the PLATFORM (noStripe-Accountheader, notransfer_data). Charges the customer against the platform balance.Processor.create_transfer/2to the connected account,source_transactionlinked 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)
@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.
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
Bang variant of transfer/2. Raises on failure.
@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.
@spec update_account!(String.t(), map(), keyword()) :: Accrue.Connect.Account.t()
Bang variant of update_account/3.
@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).