# Testing Accrue Billing Flows
The example host proves the end-to-end browser and integration path with `cd examples/accrue_host && mix verify.full`; use `mix verify` there for a faster bounded Fake-backed slice. [Runnable browser-proof commands and Playwright entry points](../../examples/accrue_host/README.md#proof-and-verification).
## Accrue package: automated proof suite
For the library itself, Accrue prefers ExUnit and deterministic helper-driven proofs whenever behavior is observable without a human clicking through a product UI.
- **`accrue/test/accrue/verification/`** — manifest tests that assert migration files + backing suites still exist (prevents silent drift).
- **`mix verify.uat_phase_04`** — runs the legacy `uat_phase_04` verify target plus the focused ExUnit files that prove metered billing, dunning, DLQ, multi-endpoint webhooks, checkout/portal, events query API, and telemetry ops/metrics.
- **`mix verify.uat`** — runs the whole `test/accrue/verification/` tree (add more `*_surrogate_test.exs` modules as coverage grows).
The repository CI already runs `mix test --warnings-as-errors` for `accrue/`, which includes every surrogate — there is no second duplicate suite to keep in sync.
## Fake-first Phoenix scenario
Start billing tests in the host app, not inside Accrue internals. This example belongs in a Phoenix `DataCase` and runs against the Fake Processor, test mailer, test PDF adapter, Oban test mode, and the normal webhook reducer path.
```elixir
defmodule MyApp.BillingTest do
use MyApp.DataCase, async: true
use Accrue.Test
use Oban.Testing, repo: MyApp.Repo
setup do
Accrue.Test.setup_fake_processor()
Accrue.Test.setup_mailer_test()
Accrue.Test.setup_pdf_test()
:ok
end
test "subscriber renews through the local billing flow" do
user = MyApp.AccountsFixtures.user_fixture()
assert {:ok, subscription} =
MyApp.Billing.subscribe(user, "price_monthly",
trial_days: 14,
payment_method: "pm_card_visa"
)
assert MyApp.Billing.subscription_status(user) == :trialing
assert_event_recorded(user, type: :subscription_created)
assert {:ok, _clock} = Accrue.Test.advance_clock(subscription, "1 month")
invoice = MyApp.Billing.latest_invoice(user)
assert invoice.status in [:open, :paid]
assert {:ok, _event} = Accrue.Test.trigger_event(:invoice_payment_failed, invoice)
assert_enqueued worker: Accrue.Workers.Mailer
assert_email_sent(:receipt, to: user.email)
assert_pdf_rendered(invoice)
assert_event_recorded(user, type: :subscription_created)
assert MyApp.Billing.account_state(user).billing_status in [:past_due, :active]
end
end
```
The point is the shape: the host `MyApp.Billing` context owns the public test surface, while `Accrue.Test` makes time, webhooks, email, PDFs, events, and jobs deterministic. You should be able to prove billing behavior without Stripe, Chrome, SMTP, or sleeps.
Scenario checklist: successful checkout, trial conversion, failed renewal, cancellation/grace period, invoice email/PDF, webhook replay, background jobs, and provider-parity tests.
## Successful checkout
Call the host checkout or subscribe function and assert both persisted state and user-visible side effects. Prefer `MyApp.Billing.subscribe/3`, `MyApp.Billing.create_checkout_session/2`, or the facade your installer generated. Assert the subscription row, the account-facing status, the event ledger entry, and any receipt email or invoice PDF the flow promises.
## Trial conversion
Use `Accrue.Test.advance_clock(subscription, "1 month")` or a precise keyword duration to cross a trial boundary. The assertion should be about host behavior: the billable account leaves trial state, renewal work is enqueued, and the side effects a customer would see are recorded.
```elixir
assert {:ok, _} = Accrue.Test.advance_clock(subscription, days: 14)
assert MyApp.Billing.subscription_status(user) in [:active, :past_due]
```
## Failed renewal and retry
Simulate payment failure with `Accrue.Test.trigger_event(:invoice_payment_failed, invoice)`. Assert the webhook row exists, the host billing state changes, dunning or retry work is queued, and customer communication is captured with `assert_email_sent/2`.
## Cancellation/grace period
Exercise the host cancellation API, then advance the Fake clock through the grace period. Assert the local subscription status with `MyApp.Billing`, not private Accrue reducer state. A good test proves when access stays available, when it stops, and which event is recorded for the transition.
## Invoice email/PDF
Use the test mailer and PDF adapter together. The email assertion proves the customer communication path; the PDF assertion proves invoice rendering was requested without requiring Chrome.
```elixir
assert_email_sent(:receipt, to: user.email)
assert_pdf_rendered(invoice)
```
## Webhook replay
Replays should enter the same reducer path as first delivery. Trigger or requeue the event, assert idempotent persisted state, and verify that duplicate delivery does not duplicate customer-visible side effects.
For the Braintree supportability closeout, keep this deterministic proof
bundle named explicitly:
- `test/accrue/webhooks/dlq_test.exs` for persisted `webhook replay`
semantics on dead and failed rows
- `test/mix/tasks/accrue_webhooks_replay_test.exs` for the bounded
`mix accrue.webhooks.replay` operator entry point
- `test/accrue/telemetry/portal_checkout_completed_test.exs` for the local
Braintree `accrue.portal.checkout.completed` completion path
- `examples/accrue_host/test/accrue_host_web/admin_webhook_replay_test.exs`
for the bounded host admin replay lane
Those lanes are still Fake-first and deterministic. They document the operator
story for persisted replay, local portal completion, and admin recovery without
making live-provider verification the default path.
## Background jobs
Use `Oban.Testing` for queue assertions and `perform_job/2` when the test needs to prove worker behavior. Keep queue names and worker modules host-visible so failures point to the app wiring a developer can fix.
## Provider-parity tests
Use the Fake Processor for normal test coverage, then keep a small provider-parity suite for behaviors where Stripe itself is the contract: SCA/3DS cards, Stripe test clocks, hosted checkout redirect behavior, and webhook signatures. Tag those tests separately so local development and CI do not depend on network calls by default.
For the finalized `gateway subscription core` contract, Fake is the
merge-blocking source of truth: direct subscription creation, vault
acquisition, webhook verify/parse, lifecycle projection rows, and the
bounded cancellation/customer-update semantics the public docs surface.
Provider-backed Stripe and Braintree checks stay in the explicitly
advisory provider-parity lane. Checkout and billing portal use the same shared facade across both providers, but the behavior stays provider-honest: Stripe returns upstream hosted URLs, while Braintree returns mounted local checkout and portal URLs.
The processor-support-matrix.md contract remains the maintainer-facing
reference for the bounded provider support story behind these tests.
The exception is documentation and recovery semantics that must stay
provider-honest. Braintree supportability is still proved locally: replay
tests, the synthetic `accrue.portal.checkout.completed` lane, and the bounded
host admin replay verifier together cover the required operator recovery story
without promoting live-provider checks to merge-blocking status.
## Usage metering (Fake)
Metered usage flows through `Accrue.Billing.report_usage/3`. With `Accrue.Test.setup_fake_processor/1` in `setup`, the configured processor is Fake so tests stay offline. Accrue inserts an `accrue_meter_events` row that starts `pending` inside the transaction, then moves to `reported` on the happy path after Fake acknowledges the meter event—treat the persisted `%Accrue.Billing.MeterEvent{}` as the primary contract.
Assert in this order: first load the `MeterEvent` row from `Repo` (`stripe_status`, `value`, `identifier`), then use `Accrue.Test.meter_events_for/1` when you need the processor-shaped payload Fake captured.
Call `Accrue.Billing.report_usage/3` with explicit `timestamp:` and an `operation_id:` when you need stable identifiers or idempotent replays; avoid relying on wall-clock alone for golden vectors.
Full NimbleOptions keys, defaults, and semantics follow the deep link to source doc path or HexDocs wording for `Accrue.Billing.report_usage/3` (`lib/accrue/billing.ex`); this guide does not duplicate that full option list.
For failure-path telemetry (`meter_reporting_failed` and related ops notes), see `guides/telemetry.md` while keeping happy-path tests anchored on `accrue_meter_events` rows. For the public API vs internal row vs Fake/Stripe processor map, see `guides/metering.md`.
## Helper reference
- `use Accrue.Test` imports mail, PDF, and event assertions and exposes the public action helpers.
- `Accrue.Test.setup_fake_processor/1` configures the Fake Processor for the test process.
- `Accrue.Test.meter_events_for/1` returns Fake-captured meter payloads after `Accrue.Billing.report_usage/3` (Fake-only; see Usage metering section above).
- `Accrue.Test.setup_mailer_test/1` captures `Accrue.Mailer.Test` deliveries in the current process mailbox.
- `Accrue.Test.setup_pdf_test/1` configures the invoice renderer test adapter so invoice PDF renders stay Chrome-free in the current process mailbox.
- `Accrue.Test.advance_clock/2` advances Fake time with readable strings, seconds, or keyword durations.
- `Accrue.Test.trigger_event/2` synthesizes a webhook event through Accrue ingest and the default handler path.
- `assert_email_sent/2` matches captured emails by type, recipient, assigns, or predicate.
- `assert_pdf_rendered/1` matches captured PDF renders by invoice id, contents, options, or predicate.
- `assert_event_recorded/1` and `assert_event_recorded/2` query the event ledger visible to the test sandbox.
## Host browser proof vs provider parity
The canonical **Phoenix + mounted admin + browser** proof for adopters lives in
`examples/accrue_host` (`mix verify.full`). That proof is Fake-backed. For how it sits next to Stripe test-mode canaries, see
`examples/accrue_host/docs/adoption-proof-matrix.md` and
`guides/testing-live-stripe.md`.
## External-provider appendix
Use real Stripe test mode only for parity checks that the Fake Processor cannot prove. Keep those tests tagged and skipped unless `STRIPE_TEST_SECRET_KEY` and the matching webhook secret are present. Never make real Stripe sandbox calls by default in the main unit or context suite.
Stripe test clocks are useful for provider-level lifecycle parity. They are not a replacement for `Accrue.Test.advance_clock/2` in host flow tests because they require network access and provider resources.
Use 3DS cards in the provider-parity suite to prove SCA branches such as `requires_action`, failed authentication, and successful retry. Keep those card numbers in fixtures or test data, not in production config.
live webhook forwarding is useful when validating local endpoint wiring and signatures. Treat it as an integration exercise: document the local route, keep secrets in environment variables, and assert that the same event shape is reduced locally.
## Footguns
- Testing Accrue internals instead of host flows makes the app look green while `MyApp.Billing` is broken.
- `Process.sleep` hides races and slows the suite. Use Fake time, Oban testing helpers, and explicit event triggers.
- Real Stripe sandbox calls by default make local and CI tests slow, flaky, and dependent on external state.
- mixing live/test keys can mutate real accounts or make failures misleading. Keep environment variable names explicit and never paste real keys into examples.
- Hiding webhook setup means nobody knows whether raw body capture, signature verification, and dispatch are actually wired.
- Failing to assert side effects misses the behaviors customers notice: emails, PDFs, jobs, and event ledger rows.
- Raw payloads, metadata blobs, card data, emails beyond variables such as `user.email`, API keys, and signing secrets do not belong in test logs or telemetry attributes.
## Finance handoff (Stripe RR, Sigma, Data Pipeline)
For **Stripe-native finance and reporting** — what Accrue stores vs Stripe, when to
use Revenue Recognition vs Sigma vs Data Pipeline, and explicit **non-accounting**
boundaries — see [Finance handoff](finance-handoff.md).