feat(billing): Wave 4b — cohort test-mode (rzp_test_*) checkout routing#268
Merged
Merged
Conversation
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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 asRAZORPAY_KEY_ID):RAZORPAY_TEST_KEY_ID/RAZORPAY_TEST_KEY_SECRET/RAZORPAY_TEST_WEBHOOK_SECRETRAZORPAY_TEST_PLAN_ID_{HOBBY,HOBBY_PLUS,PRO}Checkout routing (
CreateCheckoutAPI):resolveCheckoutTestModeswaps to the testplan_idand routes the create throughrzp_test_*creds (via a privatesubBodyflag stripped before Razorpay), bypassing the live-key /billing_not_configuredguards.synthetic_test_cohort403 skip; never mints against the live plan; no crash.is_test_cohort→ fail CLOSED (live path) so a real customer is never routed through the test account.Webhook (
/razorpay/webhook): try-both verification — live secret first, thenRAZORPAY_TEST_WEBHOOK_SECRET— so a real TEST-modesubscription.charged/activatedupgrades the cohort team. Live webhooks unaffected; unset test secret is a no-op; constant-time.Tests (
internal/handlersgreen)testModeConfiguredinert-when-unset matrix;razorpayTestPlanIDForself-serve-tiers-only.Operator enable (no approval wait)
rzp_test_*key id + secret./razorpay/webhookURL, copy the test webhook secret.RAZORPAY_TEST_KEY_ID/SECRET/WEBHOOK_SECRET+RAZORPAY_TEST_PLAN_ID_{HOBBY,HOBBY_PLUS,PRO}.Until then the whole path is inert — live billing is unaffected.
🤖 Generated with Claude Code