Billing Testing Runbook
Manual and automated validation strategy for subscriptions, one-time checkout, webhooks, and enforcement.
Purpose
This runbook defines how to validate billing safely before release.
It combines:
unit tests (pure logic)
scripted integration checks (contract-level flows)
manual Stripe webhook validation in staging
Config Source of Truth
Billing uses a hybrid source model:
Environment variables remain the source of truth for Stripe secrets.
billing_productsstores operator-managed product prices, internally managed Stripe price IDs, and mirrored Stripe Product/Price metadata.billing_stripe_couponsandbilling_stripe_promotion_codesstore a read-side mirror of Stripe discounts for CMS/MCP usage.system_settingskeybilling.config.v1stores non-secret global billing config:grace period days
billing_plansstores plan feature limits:media upload and storage limits per plan
seat limits per plan
Runtime fallback is deterministic:
valid DB payload wins for non-secret fields
missing/invalid DB payload falls back to env defaults
legacy
billing.config.v1payloads with embedded plan settings are normalized to the current grace-period-only shape
Automated Checks
Unit tests
Run:
bun testThese tests cover:
Stripe status to product status mapping
grace period behavior
seat calculations and invitation eligibility
checkout permission rules by role and active organization
billing flags behavior (
billing-enabled,billing-enforcement-enabled)organization billing page-model rules:
active offer resolution (
free,pro_subscription,business_subscription,pro_lifetime)CTA disabled reasons
internal activity aggregation
purchased code access gating from authz + active paid billing state
durable code-access grant lifecycle for one-time purchases and subscription status changes
GitHub username normalization and repository provisioning configuration
Scripted integration checks
Run:
bun run testThe test runner executes all scripts in src/lib/scripts/tests/test-*.ts, including:
test-billing-checkout-permissions.tstest-billing-webhook-contract.tstest-billing-seat-limits.tstest-billing-one-time-lifetime.tstest-billing-products-admin.tstest-billing-organization-view.ts
Stripe catalog mirror
When updating product prices through admin or MCP, validate that:
Yayaw creates or reuses the expected Stripe Product.
The Stripe Price type matches the Yayaw product type (
recurringfor subscriptions,one_timefor lifetime checkout).Recurring prices match the expected interval (
monthoryear).A new Stripe Price is created when amount, currency, or interval changes, and the previous Price is archived for new checkouts.
Mirrored amount, currency, Product name, active state, and sync timestamp are persisted.
yayaw_stripe_discounts_syncmirrors coupons and promotion codes without treating Yayaw as the source of truth.
Manual failed-event replay validation
After forcing a one-time webhook failure in staging, validate replay:
bun run billing:replay-webhooksExpected result:
Failed event is retried once.
Event status moves from
failedtoprocessed.Entitlement is still idempotent (no duplicate side effects).
Manual Stripe Validation (Staging)
Prerequisites
Staging environment configured with Stripe keys.
App deployed and reachable.
Stripe CLI installed and authenticated.
Start webhook forwarding
Use Stripe CLI to forward subscription events to the Better Auth endpoint:
stripe listen --forward-to https://<staging-domain>/api/auth/stripe/webhookIf your Better Auth Stripe endpoint is routed under the catch-all auth route, keep this forward target aligned with your plugin setup.
Forward one-time events to the custom webhook endpoint:
stripe listen --forward-to https://<staging-domain>/api/billing/stripe/webhookTrigger key events
Run these commands (or equivalent actions in Dashboard):
stripe trigger checkout.session.completed
stripe trigger invoice.payment_failed
stripe trigger invoice.paid
stripe trigger customer.subscription.deletedValidate expected outcomes
checkout.session.completedsets subscription to active state.invoice.payment_failedmoves organization to grace period.After grace threshold, organization becomes restricted for premium actions.
invoice.paidrestores active state.customer.subscription.deletedsets canceled state.Duplicate webhook delivery is idempotent (no duplicated side effects).
One-time
checkout.session.completedon/api/billing/stripe/webhookgrantspro_lifetimeentitlement.One-time checkout also creates a
billing_code_access_grantsrow keyed by Stripe checkout session.Subscription checkout creates or refreshes a
billing_code_access_grantsrow keyed by Stripe subscription ID.Scheduled subscription cancellation keeps the grant active until period end, and subscription deletion revokes the grant.
Organization with
pro_lifetimeentitlement remains active even if subscription status is delinquent.Failed one-time events can be replayed with
billing:replay-webhooksand recovered.Successful checkout redirects to
/dashboard/organization/code-access, where deliverables are unlocked only whencode-access:readand active paid billing state or an active durable grant pass.With GitHub provisioning configured, an unlocked user can submit a GitHub username and create a
code_access_github_accountsrow for the configured repository.
Internal Activity Timeline Validation
The organization billing activity list is intentionally not a full Stripe finance ledger. Validate that the UI timeline reflects only:
latest subscription snapshot
entitlement grants
code-access grants
GitHub repository access records
persisted one-time webhook events scoped to the active organization
Also validate the split UX surfaces:
/dashboard/organization/billingshows active plan summary and activity/dashboard/organization/planshosts checkout actions/dashboard/organization/code-accessshows purchased deliverables after a successful checkout
Regression Checklist
With billing-enabled = false:
Existing auth flows still work.
Organization settings and members pages still work.
Navigation does not regress.
With billing-enabled = true and billing-enforcement-enabled = false:
Billing UI and checkout paths are available.
No hard blocking for invitations.
With both flags enabled:
Seat and status restrictions are enforced server-side.
Invitation blocking matches contract tests.
Troubleshoot esbuild / Drizzle Hangs
If local commands are blocked on docs or drizzle generation:
Use timeout-safe commands:
bun run docs:generatebun run db:generatebun run db:push
If timeout persists, reinstall dependencies and rerun:
rm -rf node_modulesbun install
Release Gate
Before merge:
bun run check
bunx tsc --noEmit
bun run test
bun run docs:generate
bun run docs:check-links
bun run docs:check-translations
bun run docs:llm:check
bun run build