# Free tier verification (while demo is Enterprise) Proof that `cmd/seed-demo` assigning **Enterprise** to Local Demo Co does **not** break forever-Free packaging or gates for new signups. **Date:** 2026-08-04 **Related:** [free-tier.md](free-tier.md), [demo-user.md](demo-user.md) ## Expected matrix | Check | Free signup | Demo (Enterprise) | |-------|-------------|-------------------| | Plan name | `Free` | `Enterprise` | | `monthly_credits` | **0** | `1000000` | | Wallet remaining | **0** | ≈1M | | `can_use_ai` | `false` | `true` | | `can_use_eprel` | `false` | `true` | | AI-only job (`title` / `description` / `enhance`) | **402** `ai_requires_upgrade` | Allowed (credits) | | EPREL-only job | **402** `eprel_requires_upgrade` | Allowed | | Full / normalize process | **202** → completes; `ai_enhance` + `eprel` **skipped** | Full AI path | ## Live smoke (2026-08-04) API: `http://127.0.0.1:8080` · Postgres: `localhost:5433` ### 1. Public plan rows after Enterprise seed ```sql SELECT name, monthly_credits, max_products, is_custom FROM plans WHERE lower(name) IN ('free','enterprise') ORDER BY name; ``` | name | monthly_credits | max_products | is_custom | |------|----------------:|-------------:|:---------:| | Free | **0** | 100 | f | | Enterprise | 1000000 | null | t | ### 2. New Free signup path `POST /api/auth/register` → `201` → `ProvisionFreePlan`. Observed (`/api/auth/me` → `credits`): - `plan.name=Free`, `monthly_credits=0` - `remaining_credits=0`, `can_use_ai=false`, `can_use_eprel=false`, `is_free_plan=true` ### 3. 402 on AI (Free company with a raw product) | Request | Status | Body code | |---------|-------:|-----------| | `POST /api/processing/jobs` `processing_type=title` | **402** | `ai_requires_upgrade` | | `processing_type=description` | **402** | `ai_requires_upgrade` | | `processing_type=enhance` | **402** | `ai_requires_upgrade` | | `processing_type=eprel_only` | **402** | `eprel_requires_upgrade` | Example body: ```json { "code": "ai_requires_upgrade", "error": "ai features require a paid plan or AI credits — upgrade your plan or add AI credits", "upgrade_url": "/pricing" } ``` ### 4. Non-AI process works `POST /api/processing/jobs` `processing_type=full` → **202**, job **completed**: - `normalize` / `parse_specs` / `fill_fields` → `done` - `eprel` → `skipped` (`entitlement_can_use_eprel`) - `ai_enhance` → `skipped` (`entitlement_can_use_ai`) ### 5. Demo still Enterprise `demo@descrybe.local` → `/api/billing/credits`: `plan.name=Enterprise`, `can_use_ai=true`, remaining ≈ 1M. ## Regression fixed `ProvisionFreePlan` previously fell back to `SELECT id FROM plans ORDER BY id LIMIT 1` when Free was missing. After migration + `seed-demo`, **Enterprise can have a lower id than Free**, so that fallback could assign Enterprise to a new signup. Fallback removed — only the Free row is assigned (best-effort no-op if Free is absent after `EnsureDefaultPlans`). `seed-demo` now **fails closed** if Free `monthly_credits ≠ 0` or `max_products ≠ 100` after `EnsureDefaultPlans`. ## Automated checks ```powershell cd apps/api go test ./internal/billing/ ./internal/processing/ -count=1 go build ./... # Optional DB smokes (requires migrated + seed-demo DB): $env:DATABASE_URL = "postgres://descrybe:descrybe@localhost:5433/descrybe?sslmode=disable" go test ./cmd/seed-demo/ -count=1 -v ``` Unit coverage: - `TestComputeEntitlements` — Free 0 credits → no AI/EPREL - `TestDefaultPublicPlansFreeZeroCredits` — Free packaging locked at 0 / 100 SKUs - `TestDefaultPublicPlansEnterpriseUnlimited` — Enterprise 1M / unlimited SKUs - `TestFreePlanUnaffectedByEnterpriseSeed` — live Free row after demo seed - `TestLocalDemoCoEnterpriseCredits` — demo company still Enterprise ## Re-run checklist 1. `go run ./cmd/seed-demo` (demo → Enterprise; Free row still 0 credits) 2. Register a fresh user → Free / 0 credits / `can_use_ai=false` 3. Start `title` job → 402 `ai_requires_upgrade` 4. Start `full` job → 202; AI/EPREL steps skipped 5. Login as demo → Enterprise credits unchanged