Test mode
Test mode
Test mode is an isolated data plane. Every tenant-state row carries a livemode boolean;
a test credential reads and writes only livemode=false rows, a live credential only
livemode=true rows. Isolation is enforced deny-by-default by a global Eloquent scope, so a
live token can never see (or, via find/update, touch) a test row — a cross-mode lookup simply
returns nothing, and the request 404s.
What is isolated
The plane partition covers the tenant-state objects an integrator creates:
- organizations, subscriptions, invoices, credit notes
- coupons and redemptions, seat assignments, wallet adjustments
- payment retries, issued licenses, webhook endpoints and deliveries
The catalog is shared, not partitioned — products, plans, prices and meters are the seller's catalog and exist once, referenced from both planes. A test subscription is created against a real plan. This is a deliberate boundary: you experiment with subscriptions, invoices and dunning against your real catalog, not a copy of it.
Organization ids. An organization's primary key is its Cbox ID handle, so a test org and a live org cannot share the same id (the key is unique across both planes). Provision your sandbox orgs under distinct handles (e.g.
org_test_acme).
How cross-mode access is denied
A single ambient billing context holds the request's plane, resolved from the credential:
- an API request resolves its plane from the token's
mode(liveortest); - the console resolves it from the operator's test-mode toggle (a session flag).
The context defaults to live, so anything that never enters test mode — the scheduled
billing passes, every existing request — is unchanged. A global LivemodeScope on each
partitioned model reads the context and constrains every query to the current plane. Rows are
stamped with their plane on create from the same context; livemode is never mass-assignable,
so a request cannot forge the plane it writes into.
The fake test gateway
In test mode, payments route through a fake gateway and can never reach a real
Stripe/live account — the routing decision is made once, centrally, from the billing context.
A test charge settles or declines deterministically, controlled by the bound test clock's
charge_outcome (succeed by default, decline to exercise dunning). No card is vaulted and
no network call is made.
No real emails
Test-mode notifications are captured, never delivered. Every billing lifecycle email (invoice issued, receipt, payment-failed, trial-ending, …) is logged and recorded in test mode instead of being queued to the mailer, so an integrator can drive a year of renewals and dunning without a single message reaching a real inbox.
No real outbound webhooks
Outbound webhook endpoints are plane-partitioned too: a test-mode event only matches a test-mode endpoint, so a sandbox run never posts to a production receiver.
Getting a test API token
Mint one from the console (Settings → API tokens → New token → Mode: Test) or extend the
token model's issue() with BillingMode::Test. A test token's plaintext carries a cbt_
prefix (live tokens carry cbl_) so the plane is obvious at a glance and the two can never be
confused. Send it as the Authorization: Bearer credential exactly like a live token — the
plane is resolved from the token, not the endpoint.