# 19 — Plan / role defaults alignment (agent 19/20) **Status:** Implemented end-to-end defaults for public ladder, custom enable-all, Legacy (A1), and staff role ceilings. **Sources:** `03-roles-matrix.md` / `.json`, `docs/plan-permissions/06-defaults-matrix.md`. --- ## PROBLEM Public ladder defaults, custom enable-all, and Legacy (A1) limited nav must agree across API resolve, seeds, admin UI profiles, and demo assignment — without treating A1 as custom all-ON. --- ## Effective matrices | Profile | Source of truth | Default behavior | |---------|-----------------|------------------| | Free | `freePlanFeatureOff` | AI / API keys / BYOK / campaigns off | | Starter | `starterPlanFeatureOff` | BYOK off | | Growth / Business | all registry ON | empty sparse overrides | | Enterprise | `is_custom=true` ? custom path | all ON | | Custom deals (Merkur, …) | `IsCustomPackage` | all ON (create materializes enable-all) | | **Legacy (A1)** | `legacyFeatureAllowlist` / `03-roles-matrix` | image-nav allow-list only; `processing.monitor` OFF | | Staff `admin` / `developer` | `DefaultStaffRoleAllows` | all feature keys ON; full `/admin` | | Staff `support_staff` | denied set in `staff_role_defaults.go` | support queue + assist; no billing mutation routes | Resolve order for plans: **Legacy ? Custom enable-all ? Free/Starter denials ? Growth+**. --- ## A1 / Local Demo Co | Piece | Behavior | |-------|----------| | Plan name patterns | `Legacy`, `A1*`, contains `legacy` / `a1 slovenija` | | Company cohort | `legacy_company_id = 97e1a309-3d23-4aa2-b518-8e8d7afdfec7` or name Local Demo Co / A1 | | Seed | `EnsureLegacyDefaults` upserts plan **Legacy** (Enterprise credit pack, unlimited SKUs, `is_legacy=true`) | | Assignment | A1 cohort companies get Legacy when missing or on non-legacy profile | | Demo | `seed-demo` assigns **Legacy** (not Enterprise) so capabilities match limited nav | | Repair | Full enable-all `plans.features` on legacy-named rows ? `SparseLegacyOverrides` (idempotent) | **ASSUMPTION:** Empty `{}` still resolves via `DefaultPlanFeatures` by name; seeder writes sparse false keys so admin UIs show an explicit matrix. --- ## Implementation map | Piece | Path | |-------|------| | Legacy detect + allow-list | `apps/api/internal/billing/legacy_plan.go` | | Legacy plan row + A1 assign | `apps/api/internal/billing/legacy_plan_seed.go` (`EnsureLegacyDefaults`) | | Sparse legacy helpers | `apps/api/internal/billing/legacy_plan_features.go` | | Feature seed hook | `EnsureDefaultFeatureSeeds` ? `EnsureLegacyPlanFeatureSeeds`; `EnsureDefaultPlans` ? `EnsureLegacyDefaults` | | Custom vs legacy | `IsCustomPackage` excludes legacy names | | Staff role defaults | `apps/api/internal/auth/staff_role_defaults.go` | | Schema | `apps/api/sql/schema/028_plan_is_legacy.sql` (`plans.is_legacy`) | | Admin UI profiles | `apps/web/src/lib/admin-plan-permissions.ts` (`LEGACY_FEATURE_ALLOWLIST`, profiles) | | Billing display | `isLegacyPlan` in `apps/web/src/lib/billing-display.ts` | --- ## Idempotency - `EnsureDefaultPlans` / `EnsureLegacyDefaults` / `EnsureLegacyPlanFeatureSeeds` are safe to re-run. - Non-empty **partial** admin feature maps on legacy plans are **not** wiped (only empty or full enable-all). - Global section gates use `ON CONFLICT DO NOTHING`. --- ## Verification ```bash cd apps/api go test ./internal/billing/ ./internal/auth/ -count=1 -run "DefaultPlanFeatures|Legacy|IsCustom|Sparse|StaffRole|Normalize|PlanAllows" ``` Integration (after migrate `028` + `seed-demo`): - `TestLocalDemoCoLegacyCredits` — Local Demo Co on Legacy with 1M credits - Capabilities: `processing.monitor` false; `catalog.products` / `feeds.list` true --- ## Coordination notes - Agent 5 owns overlapping legacy seed docs (`05-legacy-seed.md`); this file is the alignment checklist for agent 19/20. - Agent 6 wires `staff_role` schema/middleware; defaults here are the ceiling until that lands (`StaffRoleFromPlatformAdmin` bridges the boolean). - Agent 8 admin profiles should match `LEGACY_FEATURE_ALLOWLIST` ? Go `legacyFeatureAllowlist` (66 ON keys from `03-roles-matrix.json`).