Files

59 lines
2.8 KiB
Markdown
Raw Permalink Normal View History

# 07 — Custom packages: enable-all / disable-all / global sections (agent 7/10)
## Behavior
### Custom detection (`IsCustomPackage`)
Matches existing product semantics — **not** `is_custom` alone:
| Case | Result |
|------|--------|
| `plans.is_custom = true` (includes seeded **Enterprise**) | custom |
| Plan name **not** in public ladder (`IsPublicProductPlan` = false: A1, Merkur, admin deals, …) | custom |
| Free / Starter / Growth / Business with `is_custom=false` | standard |
Public listing still uses `IsPublicProductPlan` / `ListPublicPlans`. Feature defaults use `IsCustomPackage`.
### On custom package **create** (`UpsertPlan`, `id == 0`)
1. Non-public names force `is_custom = true` (client-deal alignment).
2. If the request **omits** `features`, materialize **all registry keys = true** (`AllRegistryFeatures(true)`).
3. If the request **provides** `features` (even `{}`), that map is respected (replace semantics).
Updates (`id > 0`) do **not** auto-rewrite features when `features` is omitted.
### Per-plan helpers
| Service | HTTP |
|---------|------|
| `EnableAllPlanFeatures(planID)` | `POST /api/admin/plans/{planID}/features/enable-all` |
| `DisableAllPlanFeatures(planID)` | `POST /api/admin/plans/{planID}/features/disable-all` |
Both write the full registry into `plans.features` (all `true` / all `false`).
### Global section helpers (all plans)
| Service | HTTP |
|---------|------|
| `EnableSectionForAllPlans(section)` / `DisableSectionForAllPlans(section)` | wrappers around `SetSectionGate` |
| `SetSectionGate(section, enabled)` | `PUT /api/admin/feature-gates/sections/{section}` body `{ "enabled": bool }` |
Runtime: `effective = plan_allows ∩ global_section ∩ global_feature` (see `03-permission-contract.md`).
### Resolve note
Even with empty `features` `{}`, `PlanAllowsFeature` returns **true** for every key when `IsCustomPackage` — create-time materialization is for explicit admin UI state / enable-all parity.
## Files touched (agent 7)
- `apps/api/internal/billing/custom_package_features.go``IsCustomPackage`, `AllRegistryFeatures`, section wrappers, create prep
- `apps/api/internal/billing/custom_package_features_test.go`
- `apps/api/internal/billing/plan_features.go` — resolve/default use `IsCustomPackage`; `DisableAllPlanFeatures`
- `apps/api/internal/billing/service.go``UpsertPlan` create hook
- `apps/api/internal/billing/default_plan_features_seed.go` — sparse defaults use `IsCustomPackage`
- `apps/api/internal/httpapi/plan_features_handlers.go` — disable-all handler
- `apps/api/internal/httpapi/server.go` — route mount
- `apps/api/internal/httpapi/plan_features_handlers_test.go` — mount coverage
Shared with agents 4/5: `EnableAllPlanFeatures`, `SetSectionGate`, migration `026_plan_features.sql`, feature catalog.