Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
59 lines
2.8 KiB
Markdown
59 lines
2.8 KiB
Markdown
# 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.
|