Quota Gate — Manual Smoke Test
Run these checks against a staging tenant on the new tier model.
Pre-conditions
- Tenant is on Solo plan with feature flag
usage_quotas_v1ON. - Current period is the calendar month.
- Quota: jobs_created = 50, dvi_captures = 100, pdfs_rendered = 100, users_active = 3, bays_active = 2, storage_bytes = 5 GiB.
Jobs (50 → 51)
- POST 50 jobs in sequence → all 200.
- POST the 51st job → 403 with
error.code = quota_exceeded,error.metric = jobs_created,error.used = 50,error.limit = 50. - GET /api/billing/usage →
metrics.jobs_created.used = 50.
DVI (100 → 101)
- POST 100 dvi result media upload-urls → all 200.
- POST the 101st → 403 QuotaExceeded, metric=dvi_captures.
- Counter in usage_counters table for tenant, period=YYYY-MM, metric=dvi_captures, count=100.
PDFs (100 → 101)
- GET 100 invoice PDFs → all 200.
- GET the 101st → 403 QuotaExceeded, metric=pdfs_rendered.
Users (3 → 4)
- With 3 active users, POST /api/invites → 403 QuotaExceeded, metric=users_active.
Bays (2 → 3)
- With 2 active bays, POST /api/jobs/workshop-bays → 403, metric=bays_active.
Plan switch (Solo → Pro)
- As Solo, fill jobs_created to 50/50.
- Switch plan to Pro (via the platform admin tool).
- Within 5s, GET /api/billing/usage → metrics.jobs_created.limit=1500.
- POST a job → 200.
Period rollover
- Fill jobs_created to 50/50.
- Override
current_period()in the test to return "2026-07". - POST a job → 200 (new month, fresh quota).
- usage_counters has a row for both 2026-06 (count=50) and 2026-07 (count=1).
Cache fail-open
- Disconnect Postgres from the API process (block port in iptables).
- POST a job → 200 with a tracing::warn in the logs ("quota_gate: no snapshot, passing through").
- Reconnect Postgres → next request rehydrates the cache.
Sign-off
- All 8 sections pass.
- Tracing logs show
quota_gate: cache miss, passing throughonly during the fail-open test. - No 5xx responses in any of the 8 sections.
Rollout sequence (mirrors spec §10)
| Day | Action | Verification |
|---|---|---|
| 0 | feature_flag usage_quotas_v1 = off (default). New pricing.ts ships but /pricing still links the old page. | Check /pricing page renders old tier card. |
| 0–7 | Internal tenants only. Platform admin toggles flag ON per-tenant. | Internal tenant dashboard shows UsageBanner at low usage. |
| 7–21 | Pilot customers (GGDhaka, Hybrid Express, KL Auto Garage, Penang Moto Works). Flag ON per-tenant; 30-day "we won't enforce" grace period (operator can override any 403 manually). | Pilot customers see banner; no real enforcement. |
| 21 | /pricing flips to the new page. New signups land on the new tiers. | Visit /pricing; verify 4-tier grid + ENTERPRISE_ONLY matrix. |
| 60 | Grace ends. Quota gate enforces for all flag-enabled tenants. | Run smoke-test section 1–5 against a flagged-on tenant; expect 403s. |
| 240 | 30-day countdown banner starts for legacy-plan tenants (showing "Your current plan ends in 30 days — pick a new tier"). | Visit /pricing as legacy tenant; banner visible. |
| 270 | Legacy tenants migrated to new Starter tier at new price. | All legacy tenants now have plan_slug = 'starter' in tenants / subscriptions. |
Per-tenant override SQL
To enable the flag for a specific tenant (Day 0–7 and Day 7–21):
INSERT INTO tenant_features (tenant_id, feature_id, enabled)
SELECT '<tenant-uuid>', id, true
FROM features WHERE slug = 'usage_quotas_v1'
ON CONFLICT DO NOTHING;
To disable for a tenant (rollback):
UPDATE tenant_features SET enabled = false
WHERE tenant_id = '<tenant-uuid>'
AND feature_id = (SELECT id FROM features WHERE slug = 'usage_quotas_v1');
Global kill-switch
If a critical bug surfaces post-rollout, the platform team can disable globally:
UPDATE features SET enabled = false WHERE slug = 'usage_quotas_v1';
The change triggers feature_changed NOTIFY (via migration 016) and feature_cache reloads within seconds. The quota_gate middleware will then pass-through for all tenants.
Sign-off (rollout)
- Day 0: smoke test passes; flag OFF globally.
- Day 7: internal tenants have flag ON, all dashboards healthy.
- Day 21: pilot customers on flag,
/pricingflipped to new page. - Day 60: enforcement on, no 5xx spikes in observability.
- Day 240: countdown banner live for legacy tenants.
- Day 270: all legacy tenants on new Starter tier; legacy
pro/enterpriseplans deprecated in subscription_plans.