Skip to content

feat(teams): is_test_cohort + api-side synthetic skip-guards (W0 / PR-1)#246

Merged
mastermanas805 merged 5 commits into
masterfrom
w0/teams-is-test-cohort-2026-06-04
Jun 4, 2026
Merged

feat(teams): is_test_cohort + api-side synthetic skip-guards (W0 / PR-1)#246
mastermanas805 merged 5 commits into
masterfrom
w0/teams-is-test-cohort-2026-06-04

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

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 all
real teams until a seeder sets it. Zero external effect until used.

What's here

  • Migration 067 teams.is_test_cohort BOOLEAN NOT NULL DEFAULT false (next
    number after 066 on master — the plan's "063" was stale vs the live count).
    Tiny partial index on the true rows only; forward-only with documented
    rollback.
  • ModelTeam.IsTestCohort scanned in CreateTeam / GetTeamByID /
    GetTeamByRazorpaySubscriptionID; IsTestCohort(teamID) lookup helper +
    SetTestCohort setter for the seeder. No public endpoint mutates the flag.
  • api-side skip-guardsCreateCheckoutAPI and 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.

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_cohort column this PR adds is what they will filter on.

Tests (failing-then-passing)

  • Handler guard, both endpoints: test-cohort rejected 403 synthetic_test_cohort;
    normal team passes the guard; fail-open on a DB error.
  • Model: IsTestCohort/SetTestCohort branch coverage (sqlmock) + DB-backed
    migration smoke (column exists, defaults false, round-trips via setter +
    GetTeamByID scan). 100% patch coverage on new model funcs + rejectIfTestCohort.

Contract (Rule 22)

No user-facing contract surface changes: is_test_cohort is internal and the
synthetic_test_cohort 403 is unreachable by any real caller (no public flag
mutation), so OpenAPI/docs need no sync.

Gate

go build ./... + go vet ./... clean; cohort + affected team tests green
against the test DB. The 3 make gate reds (app_id NULL scan in
deployment_ttl_test, users_github_id_key dup in TestLinkGitHubID) are
pre-existing local-env flakes — verified identical on the stashed
origin/master baseline; CI (fresh per-run DB) is authoritative.

🤖 Generated with Claude Code

…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>
@mastermanas805 mastermanas805 enabled auto-merge (squash) June 4, 2026 18:21
mastermanas805 and others added 4 commits June 5, 2026 00:10
… (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 mastermanas805 merged commit 262d10d into master Jun 4, 2026
18 checks passed
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>
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