feat(teams): is_test_cohort + api-side synthetic skip-guards (W0 / PR-1)#246
Merged
Merged
Conversation
…W0 / PR-1) Cohort-isolation foundation for the continuous synthetic-monitoring program (docs/sessions/2026-06-04/TEST-ACCOUNTS-AND-NR-SYNTHETICS-PLAN.md §1.5/§1.6). Inert by default: every existing team is is_test_cohort=false, so behaviour is unchanged for all real teams until a seeder sets it. Zero external effect until synthetic accounts use it. - Migration 067: teams.is_test_cohort BOOLEAN NOT NULL DEFAULT false + tiny partial index on the true rows only. Forward-only (rollback documented). - Model: Team.IsTestCohort scanned in CreateTeam / GetTeamByID / GetTeamByRazorpaySubscriptionID; IsTestCohort(teamID) lookup helper + SetTestCohort setter (seeder-only — no public endpoint mutates the flag). - api-side skip-guards: CreateCheckoutAPI + ChangePlanAPI reject a test-cohort team with 403 synthetic_test_cohort BEFORE any Razorpay charge call (fail-open on a DB blip so a real customer is never blocked). These are the only api-side charge-initiation surfaces; every other §1.6 guard (quota nudge, churn, expiry/TTL emailers, billing reconciler, lifecycle/digest email) is worker-side and deferred to the follow-up worker PR. Tests: handler guard (test-cohort rejected, normal team passes, fail-open on DB error) for both endpoints; model helper/setter branches (sqlmock) + DB-backed migration smoke (column exists, defaults false, round-trips). 100% patch coverage on new model funcs + rejectIfTestCohort. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (W0) W0 (#246) added is_test_cohort to the GetTeamByID SELECT; the redeploy mock helper's NewRows must include it or sqlmock scan mismatches → the 7 TestDeployNew_Redeploy_*/TestDeployRedeploy_* tests failed build-and-test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ast radius) W0 added is_test_cohort to GetTeamByID/CreateTeam/GetTeamByRazorpaySubscriptionID + the Team struct, which broke ~all sqlmock tests mocking those queries (deploy_redeploy + admin/impersonate build-and-test failures). The only consumer is the dedicated models.IsTestCohort(teamID) point-lookup used by the billing guard, so revert the column from the 3 main SELECTs + the struct field and keep ONLY the dedicated IsTestCohort()/SetTestCohort() helpers. No mock resync needed; migration 067 + guards + tests unchanged. DB round-trip test now asserts via the helper, not a struct field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erted SELECT Follow-on to the blast-radius revert: the shared teamCols()/teamRow() mock helpers still carried is_test_cohort after the SELECT reverted to 6 columns, breaking TestCreateTeam_Branches / TestGetTeamByID_Branches / TestGetTeamByRazorpaySubscriptionID_Branches. Back to 6 columns. Full models -short suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…anic) W0's rejectIfTestCohort runs models.IsTestCohort(ctx, h.db, teamID) at the top of CreateCheckoutAPI/ChangePlanAPI. billing_checkout_dedup_test.go wires NewBillingHandler(nil, ...) (db-independent — it only exercises the Redis SETNX dedup guard), so the new DB query panicked on a nil *sql.DB in CI build-and-test. Skip the cohort check when h.db == nil (prod always has a db). Targeted dedup + cohort + checkout tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mastermanas805
added a commit
that referenced
this pull request
Jun 4, 2026
…no self-serve cancel/downgrade, webhook tier transitions (#247) Closes the matrix's biggest gap (USER-FLOW-INVENTORY-AND-TEST-MATRIX.md §E): the revenue-critical billing block had near-zero real-backend integration coverage. New DB-backed tests (handlers package, TEST_DATABASE_URL), all NEW files — no edits to billing.go or existing billing_*_test.go (avoids #246 W0). - Registry-iterating purchasable-set assertion (§E3): drives EVERY plans.Registry tier through the real CreateCheckoutAPI handler; asserts the set that reaches the Razorpay CreateSubscription seam is EXACTLY {hobby, hobby_plus, pro}. Reds if Team is re-enabled or a new tier silently becomes chargeable (rule 18). Team → 400 tier_not_yet_available; growth → invalid_plan; both checkout + change-plan surfaces gated. - No self-serve cancel/downgrade (§E10): router.go source-scan negative assertion (no non-admin cancel/downgrade route) + ChangePlanAPI rejects every lower/equal-tier target with downgrade_not_self_serve + support agent_action, asserting the team tier is left UNCHANGED. same_plan edge covered. - Webhook tier transitions (§E4/E5/E6/E7): subscription.charged upgrade elevates plan_tier AND promotes all active resources (rule 5); subscription.cancelled downgrade drops plan_tier to the courtesy floor but LEAVES resource tiers (user-benefit asymmetry); bad signature → 400 invalid_signature, tier unchanged; unknown team → 404 team_not_found (rows-affected-0 / ErrTeamNotFound). - Checkout graceful failure: unconfigured plan_id → 503 billing_not_configured; live-key-in-nonprod → 503 billing_misconfigured (CreateSubscription never called). Reuses existing helpers (seedVerifiedTeamUser, cov2CheckoutApp, changePlanAppReal/Req, postCheckoutReq, cov2WebhookAppReal, signRazorpayPayload, makeSubscriptionChargedPayloadWithPlan, makeSubscriptionCancelledPayload) — none redefined. Handlers package green; the 20 unrelated handlers/models failures in full ./... are pre-existing local-env flakes (NATS/customer-DB/GitHub creds), verified to reproduce identically on clean origin/master — CI is authoritative. 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.
W0 — Cohort-isolation foundation (api-side portion of PR-1)
Implements the api-side slice of PR-1 from
docs/sessions/2026-06-04/TEST-ACCOUNTS-AND-NR-SYNTHETICS-PLAN.md§1.5/§1.6 —the prerequisite that makes continuous synthetic test accounts safe so they
never pollute the real funnel / billing / quota / email. Inert by default:
every existing team is
is_test_cohort=false; behaviour is unchanged for allreal teams until a seeder sets it. Zero external effect until used.
What's here
teams.is_test_cohort BOOLEAN NOT NULL DEFAULT false(nextnumber after 066 on master — the plan's "063" was stale vs the live count).
Tiny partial index on the
truerows only; forward-only with documentedrollback.
Team.IsTestCohortscanned inCreateTeam/GetTeamByID/GetTeamByRazorpaySubscriptionID;IsTestCohort(teamID)lookup helper +SetTestCohortsetter for the seeder. No public endpoint mutates the flag.CreateCheckoutAPIandChangePlanAPIreject atest-cohort team with
403 synthetic_test_cohortbefore any Razorpay chargecall, fail-open on a DB blip so a real customer is never blocked. These are
the only api-side charge-initiation surfaces.
Deferred to the follow-up worker PR (one-tree discipline)
All other §1.6 guards are worker-only (
worker/internal/jobs/): quota scan/nudge, churn predictor, expiry/TTL reaper + warning emails, billing reconciler,
checkout reconcile, payment-grace, lifecycle/weekly-digest email. The shared
teams.is_test_cohortcolumn this PR adds is what they will filter on.Tests (failing-then-passing)
403 synthetic_test_cohort;normal team passes the guard; fail-open on a DB error.
IsTestCohort/SetTestCohortbranch coverage (sqlmock) + DB-backedmigration smoke (column exists, defaults false, round-trips via setter +
GetTeamByIDscan). 100% patch coverage on new model funcs +rejectIfTestCohort.Contract (Rule 22)
No user-facing contract surface changes:
is_test_cohortis internal and thesynthetic_test_cohort403 is unreachable by any real caller (no public flagmutation), so OpenAPI/docs need no sync.
Gate
go build ./...+go vet ./...clean; cohort + affected team tests greenagainst the test DB. The 3
make gatereds (app_idNULL scan indeployment_ttl_test,users_github_id_keydup inTestLinkGitHubID) arepre-existing local-env flakes — verified identical on the stashed
origin/master baseline; CI (fresh per-run DB) is authoritative.
🤖 Generated with Claude Code