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
- active offer resolution (
- 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