From 57cc8db8c8857b3c3550121636443bfaad7da48b Mon Sep 17 00:00:00 2001 From: Manas Srivastava Date: Tue, 12 May 2026 23:12:48 +0530 Subject: [PATCH] plans: restore free tier in Default() to mirror anonymous MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The api repo's plans tests (TestDefault_AllStandardTiersPresent, TestAll_ReturnsAllPlans, TestFreeTier_MirrorsAnonymous) require a `free` tier in the default registry. The api-level plans.yaml already defines `free` as a byte-for-byte clone of `anonymous` (same limits, same features) — the only difference being audience (free = claimed-but-unpaid teams, anonymous = pre-claim agents). Both still get reaped at 24h, so the pay-from-day-one policy holds. The `free` tier is real product surface, not test scaffolding: - api/internal/handlers/billing.go:361 sets tier="free" for unpaid teams - api/internal/handlers/webhook.go:411-416 reaps both anonymous and free - api/internal/handlers/openapi.go advertises "free" in 3 schemas - api/internal/models/resource_elevate_test.go uses tier "free" - api/internal/handlers/onboarding_test.go asserts tier == "free" The FREE-TIER-RECYCLE-2026-05-12.md plan also depends on `free` existing in the registry (Option B email-gate falls into this tier). Mirroring rule: anonymous and free must stay byte-identical so that an anonymous->free flip at claim time cannot widen or narrow quotas. Co-Authored-By: Claude Opus 4.7 (1M context) --- plans/plans.go | 29 +++++++++++++++++++++++++++++ plans/plans_test.go | 6 +++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/plans/plans.go b/plans/plans.go index 7411ccf..52d867f 100644 --- a/plans/plans.go +++ b/plans/plans.go @@ -385,6 +385,35 @@ plans: alerts: false custom_domains: false sla: false + # free mirrors anonymous exactly. anonymous is pre-claim (no team_id); + # free is claimed-but-unpaid (team_id set, no Razorpay subscription). + # Limits + features must stay byte-for-byte identical to anonymous so an + # anonymous->free flip at claim time can't widen or narrow quotas. The + # 24h reaper still applies — pay-from-day-one policy holds for both. + free: + display_name: "Free" + price_monthly_cents: 0 + trial_days: 0 + limits: + provisions_per_day: 5 + postgres_storage_mb: 10 + postgres_connections: 2 + redis_memory_mb: 5 + redis_commands_per_day: 1000 + mongodb_storage_mb: 5 + mongodb_connections: 2 + mongodb_ops_per_minute: 100 + queue_storage_mb: 1024 + storage_storage_mb: 10 + webhook_requests_stored: 100 + team_members: 1 + vault_max_entries: 0 + vault_envs_allowed: [] + deployments_apps: 0 + features: + alerts: false + custom_domains: false + sla: false hobby: display_name: "Hobby" price_monthly_cents: 900 diff --git a/plans/plans_test.go b/plans/plans_test.go index b3117bc..8a41358 100644 --- a/plans/plans_test.go +++ b/plans/plans_test.go @@ -18,7 +18,7 @@ func TestDefault_LoadsWithoutError(t *testing.T) { func TestDefault_AllStandardTiersPresent(t *testing.T) { r := plans.Default() - for _, tier := range []string{"anonymous", "hobby", "pro", "team", "growth"} { + for _, tier := range []string{"anonymous", "free", "hobby", "pro", "team", "growth"} { p := r.Get(tier) assert.Equal(t, tier, p.Name, "tier %q must be in default registry", tier) } @@ -102,8 +102,8 @@ func TestLoad_InvalidYAML_ReturnsError(t *testing.T) { func TestAll_ReturnsAllPlans(t *testing.T) { r := plans.Default() all := r.All() - assert.Len(t, all, 5, "default registry must have 5 plans") - for _, name := range []string{"anonymous", "hobby", "pro", "team", "growth"} { + assert.Len(t, all, 6, "default registry must have 6 plans (anonymous, free, hobby, pro, team, growth)") + for _, name := range []string{"anonymous", "free", "hobby", "pro", "team", "growth"} { assert.Contains(t, all, name) } }