You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The CreateCheckoutAPI handler at billing.go:695 short-circuited every
team-tier checkout with `400 tier_unavailable` ("Team tier is under
active development"). Meanwhile the marketing surface sells Team @
$199/mo (instanode-web/PricingPage.tsx), the dashboard PricingGrid
sells Team (post PR #106), and llms.txt advertises self-serve Team
checkout. Three customer-facing surfaces, one API answer: the
highest-AOV prospect lands on `tier_unavailable` mid-funnel.
CEO BIZ-1 ship call (2026-05-29 memo): "Either accept Team Razorpay
checkout, OR update marketing + llms.txt to say 'contact sales'.
Pick (a). Deadline: 5 business days."
## Change
- billing.go switch: add `team` to the hobby/hobby_plus/pro allow-list.
razorpayPlanIDFor + the shared 503 billing_not_configured branch
already handle the team tier; the operator-config gap (RAZORPAY_PLAN_ID_TEAM
empty on prod until Razorpay-recurring is enabled — known operator
follow-up, see project memory project_razorpay_recurring_not_enabled.md)
now surfaces as 503 (clear operator signal) instead of 400
tier_unavailable (a customer signal that the tier itself doesn't exist).
- billing.go:3001 ChangePlanAPI: remove the same Team-tier guard for
the upgrade-via-dashboard path. Same justification.
- openapi.go: `plan` enum on POST /api/v1/billing/checkout and
/api/v1/billing/change-plan now includes "team"; 400 description no
longer mentions tier_unavailable.
- helpers.go: drop the now-orphan `tier_unavailable` entry from
codeToAgentAction (no handler emits it anymore;
TestCodeToAgentAction_NoOrphans flagged it).
- agent_action_contract_test.go: drop "tier_unavailable" from the
expected-codes list (paired with the helpers.go drop).
## Bundled fix: API FINDING-2 (JWT plan field rename)
The OnboardingClaims `SuggestedPlan` field is serialised as JSON tag
"plan". UI walkthrough + @API code analysis converged: zero handler
call sites read it; server-side tier comes from
teams.plan_tier='free' literal + Razorpay subscription.charged webhook.
JSON tag renamed to "suggested_plan" so the wire format signals
"advisory, not load-bearing". Companion PR on InstaNode-dev/common
(#35) renames the parallel struct that worker/provisioner share. No
runtime impact — the only emitter (provision_helper.go:572) still uses
the Go field name.
## Regression tests
| Test | Lock-down |
|---|---|
| TestCov2_Checkout_TeamTierAccepted | Team checkout returns 200 + Razorpay short_url with RAZORPAY_PLAN_ID_TEAM set |
| TestCov2_Checkout_TeamTierYearlyAccepted | Same for plan_frequency=yearly |
| TestCov2_Checkout_TeamTierNotConfigured | Operator-config gap → 503 billing_not_configured (not 400 tier_unavailable) |
| TestCov2_ChangePlan_TeamTierAccepted | ChangePlan team→no_subscription (downstream guard), not tier_unavailable |
| TestResidualChangePlan_TeamTier_Accepted | Same pattern from the residual-coverage harness |
| TestCheckout_PlanFrequency_TeamTier* | No-DB path: team is no longer tier_unavailable on any frequency |
| TestCheckout_RejectsUnknownPlan | Negative case still 400 invalid_plan with team in the accepted-list message |
| TestClaimDoesNotTrustJWTPlanField | Re-signs an onboarding JWT with SuggestedPlan="team", reusing the registered JTI, posts to /claim, asserts the resulting team.plan_tier='free' (not 'team') and resource tier='free'. Locks the API FINDING-2 boundary. |
Targeted run green locally:
cd /tmp/fix-api-team && TEST_DATABASE_URL=… go test ./internal/handlers/ \
-run "TestCheckout_PlanFrequency_TeamTier|TestCheckout_RejectsUnknownPlan|TestCov2_Checkout_TeamTier|TestCov2_ChangePlan_TeamTier|TestResidualChangePlan_TeamTier_Accepted|TestClaimDoesNotTrustJWTPlanField|TestAgentActionContract_RegistryCoverage|TestCodeToAgentAction_NoOrphans|TestOnboarding_PostClaim" -short -count=1 -p 1
# PASS · ok instant.dev/internal/handlers 2.169s
Remaining failures in the full -p 1 sweep (TestDBNew_*, TestQueue_*,
TestBulkTwin_*, TestProvisionFinal2_OverCapNoExistingResource,
TestGRPCProvision_DB_AnonymousDedup_ReturnsExisting) are NATS /
customer-DB infra-dependent and were failing pre-change on the same
fresh test DB — documented in memory feedback_coverage_measure_per_package_not_dotdotdot.md.
## Coverage block (rule 17)
Symptom: POST /api/v1/billing/checkout {"plan":"team"}
returns 400 tier_unavailable on every cycle.
Enumeration: rg -F 'tier_unavailable' /tmp/fix-api-team
Sites found: 2 emitters in billing.go (checkout L695, ChangePlan
L3001), 1 entry in codeToAgentAction, 2 mentions in
openapi.go descriptions, 1 expected-codes list in
agent_action_contract_test.go, 3 existing tests
that asserted tier_unavailable.
Sites touched: 6 — both emitters flipped, registry entry dropped,
OpenAPI descriptions updated, agent_action_contract_test
expected list updated, 3 existing tests inverted to
assert the new positive contract.
Coverage test: TestCodeToAgentAction_NoOrphans + TestAgentActionContract_RegistryCoverage
(both pre-existing) — any reintroduction of
tier_unavailable into emitter code without updating
the registry, OR vice versa, fails the gate.
Live verified: pending; see PR checklist after deploy.
## Surface checklist (rule 22)
- [x] api/plans.yaml — team + team_yearly already defined (no change).
- [x] common/plans/plans.go defaultYAML — already in sync (no change).
- [x] api/internal/handlers/openapi.go — `plan` enum + 400 description updated for checkout AND change-plan AND the legacy /billing/checkout alias.
- [ ] content/llms.txt — verify "Team @ $199/mo, self-serve Razorpay path is rolling out" copy is no longer aspirational. Operator action — content repo has no auto-deploy (project memory operator follow-ups).
- [x] dashboard/src/components/upgradeCopy.ts — sweeped, no `tier_unavailable` references.
- [x] instanode-web/src/pages/PricingPage.tsx — pricing already lists Team @ $199/mo (no change needed).
## Operator follow-up
- Confirm `RAZORPAY_PLAN_ID_TEAM` + `RAZORPAY_PLAN_ID_TEAM_ANNUAL`
are populated in the prod k8s `instant-config` ConfigMap. If not,
Team checkout will return 503 billing_not_configured (correct
operator signal). Track under project memory
project_razorpay_recurring_not_enabled.md.
## Companion PR
InstaNode-dev/common#35 — mirrors the JWT JSON tag rename in the
shared module consumed by worker/provisioner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: internal/handlers/helpers.go
+7-4Lines changed: 7 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -129,10 +129,13 @@ var codeToAgentAction = map[string]errorCodeMeta{
129
129
AgentAction: "Tell the user this feature requires the Pro plan or higher. Upgrade at https://instanode.dev/pricing — takes 30 seconds.",
130
130
UpgradeURL: "https://instanode.dev/pricing",
131
131
},
132
-
"tier_unavailable": {
133
-
AgentAction: "Tell the user this resource type isn't available on their plan. Upgrade to Pro at https://instanode.dev/pricing to unlock it.",
134
-
UpgradeURL: "https://instanode.dev/pricing",
135
-
},
132
+
// "tier_unavailable" was removed 2026-05-29 along with the Team-tier
133
+
// checkout/change-plan guards (CEO BIZ-1). The only emitters of this
134
+
// code lived in those two billing branches; with both gone, the
135
+
// codeToAgentAction entry was orphan-flagged by
136
+
// TestCodeToAgentAction_NoOrphans. If a future feature reintroduces
137
+
// a "tier is genuinely unavailable" surface, re-add the entry here
138
+
// and emit it from the new site in the same PR.
136
139
"rate_limit_exceeded": {
137
140
AgentAction: "Tell the user they've sent too many requests in a short window. Wait 60 seconds and retry — or upgrade to Pro at https://instanode.dev/pricing for higher limits.",
0 commit comments