# 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.