Treatment Plan — Pricing-Toggle Hardening + Reusable Custom Clinical Phase — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: When a clinic turns OFF documentSettings.treatmentPlanPricingEnabled, hide the 4 remaining money leaks in the treatment-plan UI; and let plan authors quick-add reusable, clinic-wide custom Clinical Phases.
Architecture: Pricing leaks are gated client-side with the existing treatmentPlanPricingEnabled state (no new data plumbing). Custom phases are stored in the existing clinics.documentSettings JSONB (no migration) and written via a new treatment-plans-scoped POST endpoint (so doctors, who can’t call admin-only PUT /clinics/:id, can still add). A pure, unit-tested merge helper handles validation/dedupe.
Tech Stack: Hono (server routes), Drizzle ORM, Vitest, React + shadcn/ui (Select, Dialog), TanStack Query.
Spec: docs/superpowers/specs/2026-06-10-treatment-plan-pricing-hide-and-custom-phase-design.md
File structure
- Create
server/src/lib/treatment-plan-phases.ts—DEFAULT_TREATMENT_PLAN_PHASESconst + puremergeCustomPhase()(validation + case-insensitive dedupe). - Create
server/src/lib/treatment-plan-phases.test.ts— Vitest unit tests for the helper. - Modify
server/src/schema/clinics.ts— addtreatmentPlanCustomPhases?: string[]to thedocumentSettings$type. - Modify
server/src/routes/treatment-plans.ts— newPOST /phaseshandler. - Modify
ui/src/lib/serverComm.ts—addTreatmentPlanPhase()client fn. - Modify
ui/src/components/doctor/TreatmentPlanning.tsx— 4 pricing gates + custom-phase state/dropdown/dialog. - Modify
docs/api-reference.md— document the new endpoint.
Task 1: Pure phase-merge helper (TDD)
Files:-
Create:
server/src/lib/treatment-plan-phases.ts -
Test:
server/src/lib/treatment-plan-phases.test.ts - Step 1: Write the failing test
server/src/lib/treatment-plan-phases.test.ts:
- Step 2: Run the test to verify it fails
cd server && npx vitest run src/lib/treatment-plan-phases.test.ts
Expected: FAIL — cannot resolve ./treatment-plan-phases (module does not exist yet).
- Step 3: Write the implementation
server/src/lib/treatment-plan-phases.ts:
- Step 4: Run the test to verify it passes
cd server && npx vitest run src/lib/treatment-plan-phases.test.ts
Expected: PASS — all 8 tests green.
- Step 5: Commit
Task 2: Schema field for custom phases (no migration)
Files:-
Modify:
server/src/schema/clinics.ts:62(inside thedocumentSettings$type) - Step 1: Add the field to the type
server/src/schema/clinics.ts, immediately after the treatmentPlanPricingEnabled?: boolean; line (currently line 62), add:
- Step 2: Verify it type-checks
cd server && npx tsc --noEmit
Expected: PASS (no new type errors). This is a JSONB $type change only — no DB migration is required because sanitizeDocumentSettingsForStorage (document-numbering.ts:434) spreads all keys.
- Step 3: Commit
Task 3: POST /treatment-plans/phases endpoint
Files:
- Modify:
server/src/routes/treatment-plans.ts(add a new handler; e.g. right after thePOST '/'create handler that ends ~line 460)
getReadDb, clinics, eq, and, AppError, handleError are imported; the route is guarded by requirePermissionByMethod('clinical.treatment_plans.view', 'clinical.treatment_plans.create'), which maps the POST method to the create permission — so any plan author (doctor/admin) is authorized.
- Step 1: Add the helper import
server/src/routes/treatment-plans.ts, add to the imports:
- Step 2: Add the handler
POST '/' create handler):
- Step 3: Verify the route file type-checks
cd server && npx tsc --noEmit
Expected: PASS. (If eq is reported missing, it is already imported in this file at the create handler — confirm and reuse; do NOT add a duplicate import.)
- Step 4: Confirm the permission guard covers the new POST
server/src/routes/treatment-plans.ts and confirm requirePermissionByMethod(...) is applied via a wildcard .use(...) so it covers all methods/paths including the new POST /phases. If it is per-path instead, attach the same guard to this route explicitly. Expected: POST is gated by clinical.treatment_plans.create.
- Step 5: Commit
Task 4: addTreatmentPlanPhase client function
Files:
-
Modify:
ui/src/lib/serverComm.ts(add nearupdateClinicNotificationPrefs, ~line 2533, which shows the fetchWithAuth POST pattern) - Step 1: Add the function
- Step 2: Verify it type-checks
cd ui && npx tsc --noEmit
Expected: PASS.
- Step 3: Commit
Task 5: Gate the 4 pricing leaks (visual verification)
Files:- Modify:
ui/src/components/doctor/TreatmentPlanning.tsx(lines ~1006–1021, ~1063, ~1070, ~1114–1116, ~1162–1180, ~1408–1414)
treatmentPlanPricingEnabled state and cn are already in scope in this file.
- Step 1: Leak #1 — procedure picker price (~line 1410)
- Step 2: Leak #2 — “Est. Pipeline” stat card + grid columns (~lines 1007–1021)
<Card> (the one containing formatCurrency(stats.totalValue ...), lines ~1015–1021) in a conditional:
- Step 3: Leak #3 — list “Est. Cost” column header + cell + empty-state colSpan
colSpan={6} to:
- Step 4: Leak #4 — preview “Est. Cost” card + its grid (~lines 1163–1180)
<Card> (lines ~1172–1179) in a conditional:
- Step 5: Type-check
cd ui && npx tsc --noEmit
Expected: PASS.
- Step 6: Visual verification (required — tsc does not prove UI)
documentSettings.treatmentPlanPricingEnabled = false (via the Document Settings toggle in the running app, or read it off an already-off clinic). Run the UI, open the Treatment Planning page and the Case Architect, and confirm with a screenshot / Playwright snapshot:
- Plans list: no “Est. Cost” column header or cells; stats grid shows 3 cards (no “Est. Pipeline”), reflowed cleanly with no empty slot.
- Preview panel: “Est. Cost” card gone; “Status” card spans full width.
- Procedure picker dropdown: no prices beside procedure names.
- Editor Case Projection + per-procedure cost: already hidden (regression check). Then flip the toggle ON and confirm all money reappears.
- Step 7: Commit
Task 6: Custom-phase dropdown + add dialog
Files:-
Modify:
ui/src/components/doctor/TreatmentPlanning.tsx(imports; module-level sentinel; component state ~line 284; clinic-load effect ~line 290; phase Select ~lines 1428–1436; new Dialog in render) - Step 1: Add imports
../ui/* imports (after the dialog file’s standard shadcn exports):
addTreatmentPlanPhase to the existing import statement that already brings in getClinicDetails from @/lib/serverComm (find that import and append the name to its braces).
- Step 2: Add the module-level sentinel
phaseOptions array (line 133):
- Step 3: Add component state
treatmentPlanPricingEnabled state (~line 284), add:
- Step 4: Load custom phases in the clinic-load effect
getClinicDetails(...).then(clinic => { ... }) block (~line 292, right after setTreatmentPlanPricingEnabled(...)), add:
- Step 5: Add the save handler
updateProcedure):
- Step 6: Wire the phase dropdown (~lines 1428–1436)
<Select> block:
- Step 7: Add the dialog to the render tree
<RightPreviewPanel ... />):
- Step 8: Type-check
cd ui && npx tsc --noEmit
Expected: PASS.
- Step 9: Visual + functional verification
- Confirm the 5 defaults plus a trailing ”+ Add custom phase…” item.
- Click it → dialog opens → type “Phase VI: Implant Surgery” → Save → toast, dialog closes, the procedure’s phase shows the new value.
- Reload the page, open a NEW plan / new procedure → confirm “Phase VI: Implant Surgery” now appears in the dropdown (persisted clinic-wide).
- Re-adding the same name (any case) is idempotent (no duplicate item). Empty / >60 chars shows the inline error. Capture a screenshot of the dropdown with the custom phase present.
- Step 10: Commit
Task 7: API reference docs
Files:-
Modify:
docs/api-reference.md - Step 1: Document the endpoint
- Step 2: Commit
Task 8: Full build verification
- Step 1: Server tests + typecheck
cd server && npx vitest run src/lib/treatment-plan-phases.test.ts && npx tsc --noEmit
Expected: tests PASS, no type errors.
- Step 2: UI typecheck + build
cd ui && npx tsc --noEmit && npm run build
Expected: clean build (per the stale-dist landmine memory, a passing build is required, not just tsc).
- Step 3: Final review
git add -A). Deployment is a separate, user-initiated step via the odontox-commit-deploy skill.
Self-review notes
- Spec coverage: All 4 leaks (Task 5) + custom-phase storage/endpoint/UI (Tasks 1–4, 6) + persona/permission (Task 3 guard) + docs (Task 7) are covered. Billing tab deliberately untouched.
- Type consistency:
mergeCustomPhase/MergePhaseResult/DEFAULT_TREATMENT_PLAN_PHASESnames match across Tasks 1 & 3;addTreatmentPlanPhasereturns{ phases }consumed identically in Task 6;ADD_PHASE_SENTINELdefined once (Task 6 Step 2) and used once. - No migration: documentSettings is JSONB and the sanitizer spreads all keys (verified).

