YYayaw

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:

  1. Environment variables remain the source of truth for Stripe secrets.
  2. billing_products stores operator-managed product prices, internally managed Stripe price IDs, and mirrored Stripe Product/Price metadata.
  3. billing_stripe_coupons and billing_stripe_promotion_codes store a read-side mirror of Stripe discounts for CMS/MCP usage.
  4. system_settings key billing.config.v1 stores non-secret global billing config:
    • grace period days
  5. billing_plans stores plan feature limits:
    • media upload and storage limits per plan
    • seat limits per plan
  6. Runtime fallback is deterministic:
    • valid DB payload wins for non-secret fields
    • missing/invalid DB payload falls back to env defaults
    • legacy billing.config.v1 payloads with embedded plan settings are normalized to the current grace-period-only shape

Automated Checks

Unit tests

Run:

bun test

These 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 test

The test runner executes all scripts in src/lib/scripts/tests/test-*.ts, including:

  • test-billing-checkout-permissions.ts
  • test-billing-webhook-contract.ts
  • test-billing-seat-limits.ts
  • test-billing-one-time-lifetime.ts
  • test-billing-products-admin.ts
  • test-billing-organization-view.ts

Stripe catalog mirror

When updating product prices through admin or MCP, validate that:

  1. Yayaw creates or reuses the expected Stripe Product.
  2. The Stripe Price type matches the Yayaw product type (recurring for subscriptions, one_time for lifetime checkout).
  3. Recurring prices match the expected interval (month or year).
  4. A new Stripe Price is created when amount, currency, or interval changes, and the previous Price is archived for new checkouts.
  5. Mirrored amount, currency, Product name, active state, and sync timestamp are persisted.
  6. yayaw_stripe_discounts_sync mirrors 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-webhooks

Expected result:

  1. Failed event is retried once.
  2. Event status moves from failed to processed.
  3. Entitlement is still idempotent (no duplicate side effects).

Manual Stripe Validation (Staging)

Prerequisites

  1. Staging environment configured with Stripe keys.
  2. App deployed and reachable.
  3. 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/webhook

If 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/webhook

Trigger 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.deleted

Validate expected outcomes

  1. checkout.session.completed sets subscription to active state.
  2. invoice.payment_failed moves organization to grace period.
  3. After grace threshold, organization becomes restricted for premium actions.
  4. invoice.paid restores active state.
  5. customer.subscription.deleted sets canceled state.
  6. Duplicate webhook delivery is idempotent (no duplicated side effects).
  7. One-time checkout.session.completed on /api/billing/stripe/webhook grants pro_lifetime entitlement.
  8. One-time checkout also creates a billing_code_access_grants row keyed by Stripe checkout session.
  9. Subscription checkout creates or refreshes a billing_code_access_grants row keyed by Stripe subscription ID.
  10. Scheduled subscription cancellation keeps the grant active until period end, and subscription deletion revokes the grant.
  11. Organization with pro_lifetime entitlement remains active even if subscription status is delinquent.
  12. Failed one-time events can be replayed with billing:replay-webhooks and recovered.
  13. Successful checkout redirects to /dashboard/organization/code-access, where deliverables are unlocked only when code-access:read and active paid billing state or an active durable grant pass.
  14. With GitHub provisioning configured, an unlocked user can submit a GitHub username and create a code_access_github_accounts row 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:

  1. latest subscription snapshot
  2. entitlement grants
  3. code-access grants
  4. GitHub repository access records
  5. persisted one-time webhook events scoped to the active organization

Also validate the split UX surfaces:

  1. /dashboard/organization/billing shows active plan summary and activity
  2. /dashboard/organization/plans hosts checkout actions
  3. /dashboard/organization/code-access shows purchased deliverables after a successful checkout

Regression Checklist

With billing-enabled = false:

  1. Existing auth flows still work.
  2. Organization settings and members pages still work.
  3. Navigation does not regress.

With billing-enabled = true and billing-enforcement-enabled = false:

  1. Billing UI and checkout paths are available.
  2. No hard blocking for invitations.

With both flags enabled:

  1. Seat and status restrictions are enforced server-side.
  2. Invitation blocking matches contract tests.

Troubleshoot esbuild / Drizzle Hangs

If local commands are blocked on docs or drizzle generation:

  1. Use timeout-safe commands:
    • bun run docs:generate
    • bun run db:generate
    • bun run db:push
  2. If timeout persists, reinstall dependencies and rerun:
    • rm -rf node_modules
    • bun 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