Accrue.Processor.Fake (accrue v0.3.0)

Copy Markdown View Source

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/0 and moves only via advance/2 (or advance_subscription/2 for subscription-aware clock crossing), mirroring Stripe test-clock semantics.
  • Clean resetreset/0 zeros all counters, clears state, and restores the clock to the epoch. Call in setup blocks.
  • Scripted responsesscripted_response/2 programs a one-shot return value for a named op so tests can simulate processor failures (card declined, rate limit) without mocking.
  • Subscription transitionstransition/3 moves a subscription to any status, optionally synthesizing customer.subscription.updated webhooks in-process.
  • Trial crossingadvance_subscription/2 advances the clock and, if the crossing period includes trial_end - 3d or trial_end, synthesizes customer.subscription.trial_will_end or customer.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
end

Id 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

accounts()

@spec accounts() :: [map()]

Returns all stored connect accounts (always platform-scoped — connected accounts are never themselves nested under another connected account).

advance(server \\ __MODULE__, seconds)

@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.

advance_subscription(stripe_id, opts)

@spec advance_subscription(
  String.t() | nil,
  keyword()
) :: :ok

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_end when crossing trial_end - 3d
  • customer.subscription.updated (with status: :active) when crossing trial_end

Pass synthesize_webhooks: false to skip the in-process event dispatch (useful for tests that only care about the state side effects).

call_count(callback)

@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.

charges_on(scope)

@spec charges_on(String.t() | :platform) :: [map()]

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

current_time(server \\ __MODULE__)

@spec current_time(GenServer.server()) :: DateTime.t()

Returns the current in-memory clock value.

customers_on(scope)

@spec customers_on(String.t() | :platform) :: [map()]

Returns all customers stored under scope. Scope is either a binary "acct_..." (connected account) or the :platform atom (no with_account/2 wrapper).

id_prefixes()

@spec id_prefixes() :: %{required(atom()) => String.t()}

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.

meter_events_for(stripe_customer_id)

@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).

now()

@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.

reset()

@spec reset() :: :ok

Resets all counters, stored resources, scripts, and the clock.

reset_preserve_connect()

@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.

scripted_response(op, result)

@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{...}})

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Starts the Fake processor with a fixed name.

stub(callback, fun)

@spec stub(atom(), (... -> term())) :: :ok

Overrides one behaviour callback with a custom function for the lifetime of the GenServer (until reset/0). Intended for per-test stubbing.

subscriptions_on(scope)

@spec subscriptions_on(String.t() | :platform) :: [map()]

transfers_on(scope)

@spec transfers_on(String.t() | :platform) :: [map()]

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.

transition(stripe_id, new_status, opts \\ [])

@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.