Deterministic in-memory Accrue.Processor adapter for tests and demos.
The Fake is Accrue's primary test surface. It implements
the Accrue.Processor behaviour entirely in-process with a GenServer +
struct state:
- Deterministic ids per resource with 5-digit zero-padded counters:
cus_fake_00001,sub_fake_00001,in_fake_00001,pi_fake_00001,si_fake_00001,pm_fake_00001,ch_fake_00001,re_fake_00001. - Test clock — all timestamps derive from an in-memory clock that
starts at
Accrue.Processor.Fake.State.epoch/0and moves only viaadvance/2(oradvance_subscription/2for subscription-aware clock crossing), mirroring Stripe test-clock semantics. - Clean reset —
reset/0zeros all counters, clears state, and restores the clock to the epoch. Call insetupblocks. - Scripted responses —
scripted_response/2programs a one-shot return value for a named op so tests can simulate processor failures (card declined, rate limit) without mocking. - Subscription transitions —
transition/3moves a subscription to any status, optionally synthesizingcustomer.subscription.updatedwebhooks in-process. - Trial crossing —
advance_subscription/2advances the clock and, if the crossing period includestrial_end - 3dortrial_end, synthesizescustomer.subscription.trial_will_endorcustomer.subscription.updated(status→active) events.
Startup
The Fake is a GenServer with a fixed name (__MODULE__). It is not
started by Accrue.Application — tests that need it call:
setup do
case Accrue.Processor.Fake.start_link([]) do
{:ok, _} -> :ok
{:error, {:already_started, _}} -> :ok
end
:ok = Accrue.Processor.Fake.reset()
:ok
endId prefixes
Prefixes are module attributes so they are greppable and future-proof:
@customer_prefix "cus_fake_"
@subscription_prefix "sub_fake_"
@invoice_prefix "in_fake_"
@payment_intent_prefix "pi_fake_"
@setup_intent_prefix "si_fake_"
@payment_method_prefix "pm_fake_"
@charge_prefix "ch_fake_"
@refund_prefix "re_fake_"
@event_prefix "evt_fake_"
Summary
Functions
Returns all stored connect accounts (always platform-scoped — connected accounts are never themselves nested under another connected account).
Advances the in-memory clock by seconds seconds. Existing Phase 1
API — preserved for tests that only need to push the clock without
any subscription-aware webhook synthesis.
Subscription-aware clock advance (D3-82). Advances the Fake clock by
opts[:days] * 86400 + opts[:seconds] and, if stripe_id references
a subscription with a trial_end, synthesizes
Returns the number of times callback has been invoked against this
Fake since the last reset/0. Used by Phase 5 Plan 05 tests to
count distinct processor calls through separate_charge_and_transfer.
Returns a specification to start this module under a supervisor.
Returns the current in-memory clock value.
Returns all customers stored under scope. Scope is either a
binary "acct_..." (connected account) or the :platform atom
(no with_account/2 wrapper).
Returns the id prefix map for the Fake adapter (D-20). Phase 3 resource types already have counter slots and prefixes reserved here so growing the callback list never churns id shapes.
Returns the Fake-stored meter events for the given customer (by
processor_id) in insertion order. Test helper only — the Fake never
exposes meter events through the behaviour (Stripe doesn't either).
Alias of current_time/0 — the canonical name used by Accrue.Clock
when the runtime env is :test (D3-86). Kept as a thin wrapper so
callers don't have to remember to pass a server argument, and so the
grep pattern Fake.now is stable across the codebase.
Resets all counters, stored resources, scripts, and the clock.
Full reset like reset/0, but preserves Connect account rows and the
:connect_account counter.
Pre-programs a one-shot return value for the named op. The next call to that op consumes the scripted response; subsequent calls fall back to the default in-memory behaviour.
Starts the Fake processor with a fixed name.
Overrides one behaviour callback with a custom function for the lifetime
of the GenServer (until reset/0). Intended for per-test stubbing.
Returns all stored transfers filtered by scope. Transfers are always
platform-scoped (the platform is the party initiating the transfer),
but the filter parameter is accepted for API symmetry with the other
*_on/1 helpers.
Transitions a stored subscription to new_status. By default
synthesizes a customer.subscription.updated event in-process; pass
synthesize_webhooks: false to skip.
Functions
@spec accounts() :: [map()]
Returns all stored connect accounts (always platform-scoped — connected accounts are never themselves nested under another connected account).
@spec advance(GenServer.server(), integer()) :: :ok
Advances the in-memory clock by seconds seconds. Existing Phase 1
API — preserved for tests that only need to push the clock without
any subscription-aware webhook synthesis.
Accepts an optional server argument for tests that explicitly name the GenServer.
Subscription-aware clock advance (D3-82). Advances the Fake clock by
opts[:days] * 86400 + opts[:seconds] and, if stripe_id references
a subscription with a trial_end, synthesizes:
customer.subscription.trial_will_endwhen crossingtrial_end - 3dcustomer.subscription.updated(withstatus: :active) when crossingtrial_end
Pass synthesize_webhooks: false to skip the in-process event
dispatch (useful for tests that only care about the state side
effects).
@spec call_count(atom()) :: non_neg_integer()
Returns the number of times callback has been invoked against this
Fake since the last reset/0. Used by Phase 5 Plan 05 tests to
count distinct processor calls through separate_charge_and_transfer.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec current_time(GenServer.server()) :: DateTime.t()
Returns the current in-memory clock value.
Returns all customers stored under scope. Scope is either a
binary "acct_..." (connected account) or the :platform atom
(no with_account/2 wrapper).
Returns the id prefix map for the Fake adapter (D-20). Phase 3 resource types already have counter slots and prefixes reserved here so growing the callback list never churns id shapes.
@spec meter_events_for(Accrue.Billing.Customer.t() | String.t()) :: [map()]
Returns the Fake-stored meter events for the given customer (by
processor_id) in insertion order. Test helper only — the Fake never
exposes meter events through the behaviour (Stripe doesn't either).
@spec now() :: DateTime.t()
Alias of current_time/0 — the canonical name used by Accrue.Clock
when the runtime env is :test (D3-86). Kept as a thin wrapper so
callers don't have to remember to pass a server argument, and so the
grep pattern Fake.now is stable across the codebase.
@spec reset() :: :ok
Resets all counters, stored resources, scripts, and the clock.
@spec reset_preserve_connect() :: :ok
Full reset like reset/0, but preserves Connect account rows and the
:connect_account counter.
Accrue.BillingCase uses this in setup/1 so async billing tests do not
wipe in-memory Connect state while Accrue.ConnectCase (or other modules)
are mid-flight on the shared named Fake GenServer.
@spec scripted_response(atom(), {:ok, map()} | {:error, Exception.t()}) :: :ok
Pre-programs a one-shot return value for the named op. The next call to that op consumes the scripted response; subsequent calls fall back to the default in-memory behaviour.
Fake.scripted_response(:create_subscription, {:error, %Accrue.CardError{...}})
@spec start_link(keyword()) :: GenServer.on_start()
Starts the Fake processor with a fixed name.
Overrides one behaviour callback with a custom function for the lifetime
of the GenServer (until reset/0). Intended for per-test stubbing.
Returns all stored transfers filtered by scope. Transfers are always
platform-scoped (the platform is the party initiating the transfer),
but the filter parameter is accepted for API symmetry with the other
*_on/1 helpers.
@spec transition(String.t(), atom(), keyword()) :: {:ok, map()} | {:error, Accrue.APIError.t()}
Transitions a stored subscription to new_status. By default
synthesizes a customer.subscription.updated event in-process; pass
synthesize_webhooks: false to skip.