Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user