Skip to content

feat(billing): Wave 4b — cohort test-mode (rzp_test_*) checkout routing#268

Merged
mastermanas805 merged 2 commits into
masterfrom
ci/wave4b-cohort-test-key-checkout
Jun 6, 2026
Merged

feat(billing): Wave 4b — cohort test-mode (rzp_test_*) checkout routing#268
mastermanas805 merged 2 commits into
masterfrom
ci/wave4b-cohort-test-key-checkout

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Wave 4b (Part 1 of 2) — api: synthetic test-cohort → rzp_test_* checkout routing

Enables CI to drive a real Razorpay TEST-mode hosted checkout + test-card payment (free user → upgrade → Pro) with NO real money and without the live-recurring approval that blocks prod — test mode has no recurring gate. The live billing path is provably untouched.

Design: docs/ci/01-CI-INTEGRATION-DESIGN.md §"Razorpay test-card payment E2E". Companion: instanode-web PR (the UI payment E2E spec) lands after this.

What changed

Config (all default "" = INERT; never leak in any response — same NEVER-leak contract as RAZORPAY_KEY_ID):

  • RAZORPAY_TEST_KEY_ID / RAZORPAY_TEST_KEY_SECRET / RAZORPAY_TEST_WEBHOOK_SECRET
  • RAZORPAY_TEST_PLAN_ID_{HOBBY,HOBBY_PLUS,PRO}

Checkout routing (CreateCheckoutAPI):

  • cohort team + test key/secret + tier test-plan configured → resolveCheckoutTestMode swaps to the test plan_id and routes the create through rzp_test_* creds (via a private subBody flag stripped before Razorpay), bypassing the live-key / billing_not_configured guards.
  • cohort + test mode unset/partial → inert: existing synthetic_test_cohort 403 skip; never mints against the live plan; no crash.
  • non-cohort → ALWAYS the live path, regardless of test-key config.
  • DB blip on is_test_cohortfail CLOSED (live path) so a real customer is never routed through the test account.

Webhook (/razorpay/webhook): try-both verification — live secret first, then RAZORPAY_TEST_WEBHOOK_SECRET — so a real TEST-mode subscription.charged/activated upgrades the cohort team. Live webhooks unaffected; unset test secret is a no-op; constant-time.

Tests (internal/handlers green)

  • Pure-function inert proofs (no DB, run every CI): testModeConfigured inert-when-unset matrix; razorpayTestPlanIDFor self-serve-tiers-only.
  • DB-gated routing: cohort uses TEST plan (not live); inert-when-test-keys-unset (403, no mint); inert-when-tier-has-no-test-plan; non-cohort always live; key-leak contract on the test path.
  • Webhook try-both: test secret accepted, live still accepted, wrong rejected 400, test-secret-inert-when-unset.

Operator enable (no approval wait)

  1. Razorpay TEST dashboard → API Keys → generate rzp_test_* key id + secret.
  2. Create TEST subscription plans for hobby/hobby_plus/pro → note plan_ids.
  3. Razorpay TEST → Webhooks → add the prod /razorpay/webhook URL, copy the test webhook secret.
  4. Set on the api deploy env: RAZORPAY_TEST_KEY_ID/SECRET/WEBHOOK_SECRET + RAZORPAY_TEST_PLAN_ID_{HOBBY,HOBBY_PLUS,PRO}.
  5. Add the same as GitHub Actions secrets for the nightly UI payment E2E (web PR).

Until then the whole path is inert — live billing is unaffected.

🤖 Generated with Claude Code

Enables a synthetic test-cohort team (teams.is_test_cohort=true, mig 067) to
drive a REAL Razorpay TEST-mode hosted checkout + test-card payment in CI with
NO real money and WITHOUT the live-recurring approval that blocks prod (test
mode has no recurring gate). The live billing path is provably untouched.

docs/ci/01-CI-INTEGRATION-DESIGN.md §"Razorpay test-card payment E2E".

Config (all default "" = INERT; never leak in any response):
  RAZORPAY_TEST_KEY_ID / _SECRET / _WEBHOOK_SECRET
  RAZORPAY_TEST_PLAN_ID_{HOBBY,HOBBY_PLUS,PRO}

Routing (CreateCheckoutAPI):
  - cohort + test key/secret + tier test-plan set → mint via rzp_test_*
    (resolveCheckoutTestMode), swap to the test plan_id, route the create
    through test creds via a private subBody flag (subBodyTestModeKey, stripped
    before Razorpay), and bypass the live-key / billing_not_configured guards.
  - cohort + test mode unset/partial → inert: existing synthetic_test_cohort
    403 skip; never mints against the live plan; no crash.
  - non-cohort → ALWAYS the live path, regardless of test-key config.
  - DB blip on is_test_cohort → fail CLOSED (live path), never route a real
    customer through the test account.

Webhook: try-both verification (live secret first, then RAZORPAY_TEST_WEBHOOK_
SECRET) so a real TEST-mode subscription.charged/activated upgrades the cohort
team; live webhooks unaffected; unset test secret is a no-op; constant-time.

Tests (internal/handlers green): pure-function inert proofs (no DB, always run)
+ DB-gated routing (cohort uses test plan, inert-when-unset, inert-when-no-plan,
non-cohort live path, key-leak contract) + webhook try-both/inert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 enabled auto-merge (squash) June 6, 2026 01:47
…s (100% patch)

The 100%-patch diff-cover gate flagged two uncovered branches:
  - billing.go:305-307 — the rzp_test_* key-swap inside the PRODUCTION default
    CreateSubscription closure (tests override the field, so the default body
    with the flag-true branch never ran). Added ExerciseCreateSubscriptionTestMode
    which invokes the default closure WITH subBodyTestModeKey set.
  - billing.go:597-602 — resolveCheckoutTestMode fail-CLOSED branch on an
    is_test_cohort DB error. Added TestResolveCheckoutTestMode_FailsClosedOnDBError
    (closed *sql.DB → IsTestCohort errors → useTest=false).

Both confirmed covered via go tool cover on the previously-missing line ranges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mastermanas805 mastermanas805 merged commit 3ab0350 into master Jun 6, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant