Procedure Onboarding — Admin Portal UI — Design Spec¶
Status: DRAFT 2026-05-12 Owner: SD (CPO/CTO) Tier: 3 (cross-cutting feature, multi-PR, new pages + new APIs + schema deltas) Tracking: unblocks ADR-0018 §A (facility readiness scoring) + §H (admin portal — procedure onboarding screen) References: - ADR-0018 §A "Provider Flow", §H "Facility Capability Disclosure & Readiness Scoring", Amendment "Admin Console Operational Screens" §1 - admin-portal-users-tenants-feature.md — sibling admin-page spec, pattern reference -
app/models/procedure_requirement.py,app/models/provider_procedure.py,app/models/doctor_procedure.py,app/models/provider_facility.py-config/procedures.yaml— current seed source (17 procedures across orthopedics, cardiology, oncology, fertility, dental, bariatric, ophthalmology, transplant) -config/matching/parameters/— 12-domain × 147-parameter matching registry (loaderapp/services/matching/registry.py, referencedocs/reference/matching-parameters.md) -apps/admin-app/src/pages/Providers.tsx,apps/admin-app/src/pages/Tenants.tsx— design pattern + permission semantics to mirror
1. Goals + Non-Goals¶
Goals¶
Replace today's YAML-edit + seed-script + manual-SQL onboarding with a real admin UI so:
| Today (manual) | After this spec |
|---|---|
Edit config/procedures.yaml, run seed_graph.py, seed_procedures.py, seed_embeddings.py |
Click "Add procedure" wizard in admin portal |
psql INSERTs into provider_procedures to mark a hospital as offering a procedure |
Click a procedure row, edit the per-facility readiness/cost grid inline |
| Hard-coded scoring inputs in seed data | Forms write directly to the tables the matching engine reads, with validation |
| Zero audit trail on procedure-catalog changes | Every write emits an audit event (mirrors users/tenants spec §5) |
When this ships, the matching engine has real per-facility readiness inputs (cost ranges, success rates, capability flags) for every provider × procedure pair, unblocking ADR-0018 §A's "facility readiness scoring + capability seeding" deferral (75% → 100%).
Non-Goals¶
- Patient-facing. This is admin-only. The patient-facing 3-section readiness panel (ADR-0018 §H "Patient-Facing Display") is downstream consumer logic, not part of this spec.
- Procedure versioning history. Soft-edit only in MVP. Versioning is an open question (§11.1).
- Clinical-content authoring. This UI captures structured data (codes, ranges, scores). Patient-facing prose / explanations come from the prompt + LLM pipeline, not these admin forms.
- Provider self-service. Providers do NOT edit their own readiness scores in v1 — admin-mediated only. Self-service is Phase 2 (separate spec, ties to provider portal).
- Procedure deletion (hard). Soft-delete (
is_active = false) only, matching the tenants pattern (admin-portal-users-tenants-feature.md §3.3). - Matching algorithm config. ADR-0018 §H Screen 3 (matching weight sliders) is a sibling admin screen, separately specced.
- Capability catalog management. This spec consumes the
capability_codevocabulary from ADR-0018 §J but does not specify the CRUD UI for the capability vocabulary itself — that's a small sub-page deferred to follow-up (§9.3).
2. User Stories¶
All stories assume super_admin or platform_admin role. Per-tenant scoping notes per story.
| # | As a … | I want to … | So that … | Permission |
|---|---|---|---|---|
| US-1 | platform admin | see the full catalog of procedures in one list, filter by specialty / status / has-providers | I can audit what we offer without grepping YAML | procedure:read (granted to platform_admin, super_admin) |
| US-2 | platform admin | create a new procedure (e.g., "Robotic Knee Replacement") via a wizard with code, name, category, ICD-10 links, typical recovery days | I can add procedures without code deploys or seed-script runs | procedure:write |
| US-3 | platform admin | for a given procedure, see the grid of which providers/facilities offer it with their cost range, success rate, annual volume, and readiness score | I can spot data gaps + edit inline | procedure:read + provider:read |
| US-4 | platform admin | inline-edit a single (provider, procedure) cell — cost range, success rate, lead surgeon, technology — without a separate page navigation |
I can keep readiness data current as providers report updates | provider_procedure:write (granted to platform_admin) |
| US-5 | super admin | deactivate a procedure (soft) so it stops appearing in new matches, with a confirmation showing how many live cases reference it | I can retire procedures without breaking existing case data | procedure:write + admin:force for "deactivate with active cases" override |
| US-6 | platform admin | bulk-import 50-200 procedures from a YAML or CSV file, with a dry-run preview that shows what would be created/updated and validation errors per row | I can backfill the catalog from clinical sources without 200 individual form submissions | procedure:bulk_import (granted to super_admin only in v1; see §11.4) |
3. Data Model Deltas¶
3.1 Snapshot of what already exists (no changes)¶
| Table | Purpose | Tenancy |
|---|---|---|
procedure_requirements |
Procedure catalog: code, name, category, ICD-10/SNOMED, required_documents, required_tests, comorbidity_screening, contraindications, cost_range, recovery_timeline, travel_considerations, extra_metadata | Global (no tenant_id) — single shared catalog |
provider_procedures |
Join: (provider_id, procedure_code) → cost (min/avg/max cents), volume, success_rate_pct, complication_rate_pct, package_includes, avg_hospital_stay_days, avg_wait_time_days, lead_surgeon, technology_used | Global (matches the cross-tenant data sharing pattern; per-provider-tenant scoping derives from providers.tenant_id) |
doctor_procedures |
Join: (doctor_id, procedure_code) → annual_volume, lifetime_volume, success_rate, complication_rate_30day, readmission_rate_30day, infection_rate, average_recovery_days, proms_scores, technique, implant_brands_used, cost_range_usd_cents, notable_cases, is_active | Inherits tenant via doctor FK (doctor → provider → tenant) |
provider_facilities |
Provider amenities (lab unit, ICU, etc.) — name + image + sort order | Tenant-scoped (TenantScopedMixin) |
provider_capabilities (ADR-0018 §J) |
Not yet implemented. Diagnostic / operational / logistical capability declarations per provider | Tenant-scoped |
procedure_capability_requirements (ADR-0018 §J) |
Not yet implemented. Which capability_codes a procedure requires (critical / recommended / nice_to_have) | Global (mirrors procedure_requirements) |
3.2 Required schema deltas¶
Alembic migration is implementation work — not part of this spec. The deltas below define the shape; the implementer writes the migration with
DATABASE_URL_ADMIN, reversible downgrade, and CI green per the migration head sequencing rule in.claude/rules/definition-of-done.md.
Delta A — procedure_requirements additions:
| Column | Type | Default | Why |
|---|---|---|---|
is_active |
BOOL NOT NULL | true |
Soft-delete flag (US-5). No procedure is ever hard-deleted. |
display_order |
INT NOT NULL | 0 |
Stable sort within specialty for admin UI list. |
typical_recovery_days |
INT NULL | NULL | Surfaced today inside recovery_timeline JSONB; promote to a top-level column so the matching engine + admin UI both consume the same scalar. Existing data backfilled from JSONB during migration. |
keywords |
JSONB NOT NULL | '[]' |
Today only in YAML, consumed by intake_gates.py. Persist in DB so admin UI can edit without YAML round-trips. |
neo4j_slug |
VARCHAR(64) | NULL | Today only in YAML. Persist so the auto-sync hook in §5 knows the graph slug. |
created_by_user_id |
VARCHAR(36) NULL | NULL | Audit attribution. NULL for migration-seeded rows. |
updated_by_user_id |
VARCHAR(36) NULL | NULL | Audit attribution. |
deactivated_at |
TIMESTAMP NULL | NULL | When is_active flipped to false. |
Delta B — provider_procedures additions:
| Column | Type | Default | Why |
|---|---|---|---|
readiness_score |
NUMERIC(4,3) NULL | NULL | Computed (ADR-0018 §H formula) but persisted so admin UI doesn't recompute on every list query and audit history is preserved. Recomputed when underlying inputs change (capability rows, success rate, cost range). |
readiness_score_computed_at |
TIMESTAMP NULL | NULL | When the persisted score was last computed. Drives staleness banner in UI. |
acceptance_criteria |
JSONB NOT NULL | '{}' |
Facility-level acceptance rules per procedure (e.g., {"min_age": 18, "max_bmi": 45, "requires_clearance": ["cardiac"]}). Today implied by global contraindications only. |
currency_code |
CHAR(3) NOT NULL | 'USD' |
The existing *_cost_usd columns conflate field name with currency — admin needs to express ranges in the provider's local currency. Cents stored remain in this currency; conversion via currency_service. |
is_active |
BOOL NOT NULL | true |
Mirror of provider_procedures.offered semantics, renamed for consistency. Migration aliases offered → is_active (keep offered as a read-only view column until callers migrate). |
Delta C — new tables from ADR-0018 §J (created here if not already present at implementation time):
provider_capabilities (
id UUID PK,
provider_id VARCHAR(36) FK,
tenant_id VARCHAR(36) NOT NULL,
category ENUM('diagnostic','operational','logistical') NOT NULL,
capability_code VARCHAR(50) NOT NULL,
capability_name VARCHAR(100) NOT NULL,
status ENUM('available','limited','unavailable','external_arrangement') NOT NULL,
details JSONB NOT NULL DEFAULT '{}',
verified_at TIMESTAMP NULL,
verified_by ENUM('provider_self_reported','curaway_verified','patient_reported') NOT NULL DEFAULT 'provider_self_reported',
created_at, updated_at,
UNIQUE (provider_id, capability_code)
)
procedure_capability_requirements (
id UUID PK,
procedure_code VARCHAR(20) FK procedure_requirements(procedure_code),
capability_code VARCHAR(50) NOT NULL,
criticality ENUM('critical','recommended','nice_to_have') NOT NULL,
condition_note TEXT NULL,
created_at,
UNIQUE (procedure_code, capability_code)
)
The capability vocabulary (the capability_code enum value space) is seeded from a YAML registry at config/capabilities.yaml (new file, ~30-50 entries). CRUD on the vocabulary itself is admin-portal future work (§9.3) — for MVP, admins pick from the seeded list.
3.3 Audit + event types¶
New EventType enum values (mirrors users/tenants spec convention):
ADMIN_PROCEDURE_CREATEDADMIN_PROCEDURE_UPDATEDADMIN_PROCEDURE_DEACTIVATED/ADMIN_PROCEDURE_REACTIVATEDADMIN_PROVIDER_PROCEDURE_UPDATED(cell-level inline edit)ADMIN_PROVIDER_CAPABILITY_UPDATEDADMIN_PROCEDURE_BULK_IMPORT_RUN(withdry_runflag in payload)
Payload includes actor user_id, target procedure_code (+ provider_id for cell-level), before/after JSON diff. Append-only audit_logs table per existing pattern.
4. API Surface¶
All endpoints under /api/v1/admin/*. All require Clerk JWT + permission check. No {case_id} / {patient_id} in any path → no require_case_access / require_patient_access needed; tenant isolation handled by repository scoping where applicable.
4.1 Procedure catalog endpoints (new)¶
| Method | Path | Permission | Reuse? | Request | Response |
|---|---|---|---|---|---|
GET |
/api/v1/admin/procedures |
procedure:read |
NEW | Query: q, specialty, is_active, has_providers, page, page_size (default 25, max 100) |
{ items: ProcedureListItem[], total, page, page_size } |
GET |
/api/v1/admin/procedures/{procedure_code} |
procedure:read |
NEW | — | ProcedureDetail (full row + offered_by count + readiness score histogram) |
POST |
/api/v1/admin/procedures |
procedure:write |
NEW (X-Idempotency-Key required) |
ProcedureCreate body (code, name, category, icd10_primary[], keywords[], typical_recovery_days, required_documents[], required_tests[], comorbidity_screening[], contraindications[], cost_range{}, neo4j_slug) |
ProcedureDetail |
PATCH |
/api/v1/admin/procedures/{procedure_code} |
procedure:write |
NEW | Partial ProcedureUpdate |
ProcedureDetail |
POST |
/api/v1/admin/procedures/{procedure_code}/deactivate |
procedure:write (+ admin:force if active cases exist) |
NEW | { reason: string, force?: bool } |
ProcedureDetail |
POST |
/api/v1/admin/procedures/{procedure_code}/reactivate |
procedure:write |
NEW | — | ProcedureDetail |
POST |
/api/v1/admin/procedures/bulk-import |
procedure:bulk_import (super_admin only v1) |
NEW | multipart/form-data with file (YAML or CSV) + dry_run: bool (default true) |
{ would_create: [...], would_update: [...], would_skip: [...], errors: [{row, field, message}] }; if dry_run=false, applied: bool, summary: {...} |
4.2 Per-procedure × facility (provider_procedures) endpoints (new)¶
| Method | Path | Permission | Reuse? | Request | Response |
|---|---|---|---|---|---|
GET |
/api/v1/admin/procedures/{procedure_code}/facilities |
procedure:read + provider:read |
NEW | Query: q, country_code, is_active, page, page_size |
{ items: FacilityReadinessRow[], total } — each row = (provider, provider_procedure, readiness_score, capability_gap_summary) |
PATCH |
/api/v1/admin/procedures/{procedure_code}/facilities/{provider_id} |
provider_procedure:write |
NEW | Partial ProviderProcedureUpdate (cost min/avg/max, currency_code, success_rate_pct, complication_rate_pct, annual_volume, package_includes, avg_hospital_stay_days, avg_wait_time_days, lead_surgeon, technology_used, acceptance_criteria) |
FacilityReadinessRow (with recomputed readiness_score) |
POST |
/api/v1/admin/procedures/{procedure_code}/facilities |
provider_procedure:write |
NEW | { provider_id, ...ProviderProcedureCreate } |
FacilityReadinessRow |
DELETE |
/api/v1/admin/procedures/{procedure_code}/facilities/{provider_id} |
provider_procedure:write |
NEW | — | 204 (soft-set is_active=false; no hard delete) |
POST |
/api/v1/admin/procedures/{procedure_code}/facilities/{provider_id}/recompute-readiness |
provider_procedure:write |
NEW | — | { before_score, after_score, recomputed_at } |
4.3 Provider capability endpoints (new, but ADR-0018 §J also needs them for the provider portal — coordinate)¶
| Method | Path | Permission | Notes |
|---|---|---|---|
GET |
/api/v1/admin/providers/{provider_id}/capabilities |
provider:read |
Returns all 3 categories. Used as side-panel in the facilities grid. |
PUT |
/api/v1/admin/providers/{provider_id}/capabilities/{capability_code} |
provider:write |
Upsert. Bumps readiness_score_computed_at on every provider_procedure for this provider. |
DELETE |
/api/v1/admin/providers/{provider_id}/capabilities/{capability_code} |
provider:write |
Hard delete (capabilities are facts, not history). |
4.4 Reuse — what we do NOT add¶
- Existing
GET /api/v1/procedures/{slug}/requirements(app/routers/procedures.py) — patient-facing, stays as-is. Admin endpoints are a parallel surface. - Existing
POST /api/v1/admin/providers(#607, B3 from per-provider-tenant cutover) — provider onboarding stays out of scope here. - Audit log endpoint — write side reuses the existing event-emission pattern; read side reuses
GET /api/v1/admin/audit-logs(admin-portal-users-tenants spec §6).
4.5 Error codes (additions to docs/api/error-codes.md)¶
PROCEDURE_NOT_FOUND_404PROCEDURE_CODE_DUPLICATE_409(code must be globally unique)PROCEDURE_HAS_ACTIVE_CASES_409(deactivate guard; override withadmin:force+?force=true)PROCEDURE_BULK_IMPORT_VALIDATION_422(with per-row error list indetails)PROVIDER_PROCEDURE_NOT_FOUND_404PROVIDER_PROCEDURE_INVALID_COST_RANGE_422(min > avg, avg > max)CAPABILITY_CODE_UNKNOWN_422(not inconfig/capabilities.yaml)
5. UI Structure¶
Pattern-match Providers.tsx + Tenants.tsx exactly — same table layout, same filter bar, same modals/sheets, same toast pattern, same ApiErrorBoundary. No new design tokens. All Tailwind classes existing in admin-app today.
5.1 Routes (under /admin/*)¶
/admin/procedures → ProceduresListPage (US-1, US-6)
/admin/procedures/:code → ProcedureDetailPage (US-3, US-4, US-5)
/admin/procedures/:code/facilities/:id → opens FacilityReadinessSheet (modal over ProcedureDetailPage)
Sidebar nav: add "Procedures" below "Providers" (same nav layer, same icon convention — use lucide-react Stethoscope or ClipboardList).
5.2 ProceduresListPage (mirrors Providers.tsx)¶
- Top-right buttons:
[Bulk Import][+ Add Procedure] - Filter bar: search box (debounced 300ms — reuse
useDebouncedValue), specialty select, status select (Active/Inactive/All), "Has providers" toggle - Table columns: Code, Name, Specialty, ICD-10 (chip), Recovery (days), Offered by (N facilities), Status (Active/Inactive pill), Actions (kebab: View, Edit, Deactivate)
- Pagination: same
PaginationControlscomponent as Tenants - Empty / loading / error states identical to
Providers.tsx
5.3 ProcedureDetailPage¶
Layout: 2-column on desktop (md:grid-cols-[1fr_2fr]), stacked on mobile.
Left column — Procedure metadata card:
- Editable fields (inline EditProcedureDialog modal, same pattern as EditProviderDialog):
- Name, specialty, parent_procedure_code, typical_recovery_days
- ICD-10 codes (chip input), SNOMED codes (chip input), keywords (chip input)
- Required documents / tests / comorbidity screening / contraindications (chip lists)
- Cost range JSON editor (low/mid/high + currency)
- Travel considerations textarea
- Soft-delete toggle (Deactivate button → confirmation dialog showing active-case count)
Right column — Facilities × readiness grid:
- Header: "N facilities offer this procedure" + [+ Add facility] button
- Filter bar: search, country, status
- Table columns:
- Facility (provider name + city + country flag)
- Volume (annual_volume)
- Success rate %
- Cost range (formatted with Intl.NumberFormat per currency)
- Lead surgeon (name + id link)
- Readiness score (color-coded pill: green ≥85, amber 60-84, red <60; click to open capability gap breakdown)
- Status (Active/Inactive)
- Actions (kebab: Edit, Recompute readiness, Deactivate)
- Click row → opens FacilityReadinessSheet
5.4 FacilityReadinessSheet (right-side Sheet, mirrors Tenants drawer)¶
- 3 tabs:
- Performance — cost range, success/complication rates, annual_volume, avg stay days, wait time, lead surgeon, technology, package_includes
- Capabilities — read-only summary of provider's diagnostic/operational/logistical capability list with gap flags vs.
procedure_capability_requirements(jump-link to provider's full capability page) - Acceptance criteria — JSON-form editor (min/max age, BMI ceilings, required clearances, free-text exclusions)
- Footer:
[Save changes][Cancel]. Save is optimistic with toast on success (reuseToastBanner).
5.5 Bulk Import modal¶
Modeled after the AddMappingForm / AddProviderModal pattern but full-screen:
- Drag-and-drop file box (YAML or CSV, ≤2MB)
- "Dry-run" toggle (default ON)
[Validate]button → calls/bulk-import?dry_run=true- Result panel: 3 lists (would_create, would_update, would_skip) + errors table
- If no errors and user toggles dry-run OFF:
[Apply]confirms with a count summary
5.6 Design tokens reused (no new tokens)¶
- Status pills:
bg-status-success-bg / text-status-success-text(green), existingbg-status-warning-bg(amber),bg-status-error-bg(red),bg-muted / text-muted-foreground(inactive) - Brand color (Teal
#008B8B) inherited viabg-primaryTailwind token, no override - Typography: existing Montserrat from
apps/admin-appTailwind config - Touch targets:
min-h-[44px]per.claude/rules/definition-of-done.mdFRONTEND section
6. Onboarding Flow (Operational Workflow)¶
6.1 New procedure (single)¶
- Admin clicks
[+ Add Procedure]on list page → 4-step wizard - Step 1 — Identity: code (CPT or custom), name, specialty, parent_procedure_code (optional)
- Step 2 — Clinical: ICD-10 + SNOMED chip-pickers (autocomplete from existing seed lists), keywords, required_documents, required_tests, comorbidity_screening, contraindications
- Step 3 — Logistics: typical_recovery_days, cost_range {low, mid, high, currency}, travel_considerations free text
- Step 4 — Review + submit: preview card, on submit:
- POST
/api/v1/admin/procedures - Backend creates row + emits
ADMIN_PROCEDURE_CREATEDevent - Backend triggers Neo4j auto-sync (
POST /api/v1/admin/graph/procedure-sync— existing inadmin_graph.py) — creates:Procedurenode +REQUIRES_CAPABILITYedges fromprocedure_capability_requirements - Backend triggers Qdrant auto-embed of
name + description + keywords(existing pattern inseed_embeddings.py, wrapped into a service call) - Backend triggers
POST /api/v1/admin/system/bust-storefront-cache(existing #606 endpoint) - Admin lands on new procedure's detail page with empty facilities grid → adds facilities
6.2 New facility readiness for an existing procedure¶
- Admin opens procedure detail page →
[+ Add facility] - Modal: provider picker (autocomplete across all 42+ providers), then form for cost / volume / success rate / lead_surgeon / technology / acceptance_criteria
- POST
/api/v1/admin/procedures/{code}/facilities→ row created, readiness score computed inline - Cache bust + audit event emitted
6.3 Bulk import (50-200 procedures)¶
- Admin exports current
config/procedures.yamlshape from production (out-of-band — included as a one-click "Download current catalog" link in the bulk-import modal for v2; v1 = upload only) - Admin uploads YAML (preferred — matches existing seed shape) or CSV (column headers documented in tooltip)
- Dry-run shows full diff
- Admin applies → backend wraps in a single transaction:
- For each row: validate, upsert into
procedure_requirements, emitADMIN_PROCEDURE_CREATEDorADMIN_PROCEDURE_UPDATEDper row - At commit: single Neo4j sync batch + Qdrant batch embed + one cache bust
- Result page: summary counts + downloadable CSV of per-row outcomes
- If any row fails validation in non-dry-run, whole batch rolls back (atomic)
6.4 Deactivate procedure¶
- Admin clicks Deactivate → modal shows
live_case_countfromcases WHERE workflow_state->>'procedure_code' = :code AND state NOT IN ('case_complete','case_cancelled') - If count > 0: require
super_admin+?force=true+ reason field - On confirm: PATCH
is_active=false, setdeactivated_at, emitADMIN_PROCEDURE_DEACTIVATED - Procedure stays in matching engine queries for existing cases (matching by
procedure_codecontinues), but is hidden from new intake (intake_gates.py filtersis_active=true)
7. Validation Rules¶
7.1 Frontend (form-level, fail fast)¶
procedure_code: required, 3-20 chars,[A-Z0-9-]+(matches CPT pattern or[A-Z]{2,4}-\d{3}custom)name: required, 3-255 charsspecialty: required, must be one of the enum values seeded inconfig/specialties.yaml(orthopedics, cardiology, …)typical_recovery_days: integer, 0-365cost_range: if provided,low ≤ mid ≤ high, all > 0icd10_primary[]: each entry matches[A-Z]\d{2}(\.\d{1,3})?provider_procedure.{min,avg,max}_cost: integer cents,min ≤ avg ≤ maxsuccess_rate_pct: 0-100 inclusivecomplication_rate_pct: 0-100 inclusive;complication_rate + success_rate ≤ 100warning (not hard fail)
7.2 Backend (authoritative)¶
All FE rules re-validated. In addition:
procedure_codeglobal uniqueness — returnPROCEDURE_CODE_DUPLICATE_409(provider_id, procedure_code)uniqueness onprovider_procedures(existinguq_provider_procedureconstraint)capability_codemust exist inconfig/capabilities.yamlregistry —CAPABILITY_CODE_UNKNOWN_422currency_code: ISO 4217 (3 letters), must exist incurrency_servicerate table- Money values: integer cents only, no floats (per CLAUDE.md "All money in USD cents")
- Idempotency: all POST/PATCH/DELETE accept
X-Idempotency-Key; duplicate within 24h returns the original response - Soft-delete idempotency: deactivating an already-inactive procedure is a no-op
200(no error) - Bulk import: per-row validation; transactional — partial success not allowed
7.3 Tenant safety¶
- All write endpoints require
super_adminorplatform_admin(cross-tenant scope justified — procedures are global) provider_procedureswrites verifyproviders.is_active = truefor the target provider (cannot add readiness to a deactivated provider)- No raw
tenant_idfiltering onprocedure_requirements(global catalog) provider_capabilitiesreads/writes useBaseRepository._scoped_query(provider.tenant_id)(per-provider tenant)
8. Matching Engine Integration¶
The matching engine reads from these tables today (or will once §A is unblocked). This spec ensures every field the engine needs has a writable admin UI.
Matching parameter (from config/matching/parameters/) |
Reads from | Editable via |
|---|---|---|
clinical_context.case_volume_per_procedure |
provider_procedures.annual_volume |
FacilityReadinessSheet → Performance tab |
clinical_context.procedure_specialty_match |
procedure_requirements.category × providers.specialties |
EditProcedureDialog (Specialty field) |
outcomes_evidence.success_rate |
provider_procedures.success_rate_pct |
FacilityReadinessSheet → Performance tab |
outcomes_evidence.complication_rate |
provider_procedures.complication_rate_pct |
FacilityReadinessSheet → Performance tab |
cost_financial.cost_range |
provider_procedures.{min,avg,max}_cost_usd + currency_code |
FacilityReadinessSheet → Performance tab |
provider_capability.jci_accreditation |
providers.accreditations |
Existing Providers admin page (out of scope here) |
provider_capability.bed_count |
providers.bed_count |
Existing Providers admin page |
provider_capability.facility_readiness_score (NEW — derived) |
provider_procedures.readiness_score (persisted) |
Auto-computed; manual recompute via /recompute-readiness endpoint |
risk_safety.contraindication_match |
procedure_requirements.contraindications × provider_procedures.acceptance_criteria |
EditProcedureDialog + FacilityReadinessSheet → Acceptance tab |
post_op_continuity.typical_recovery_days |
procedure_requirements.typical_recovery_days (promoted column) |
EditProcedureDialog |
When admin saves a cell:
1. Row updated in Postgres
2. readiness_score recomputed and persisted (input: own row fields + linked provider_capabilities + procedure_capability_requirements)
3. readiness_score_computed_at = NOW()
4. Matching engine reads fresh data on next match request (no cache invalidation needed — engine queries each time)
5. If provider_capabilities changed → recompute readiness_score on every provider_procedure row for that provider (background QStash task — admin doesn't wait)
Registry coupling: the parameter registry IDs above are the contract. If the engine ever renames success_rate → success_rate_v2, that's a registry change (separate spec); this UI keeps writing to the same DB columns.
9. Phased Rollout¶
9.1 MVP (PR-1 — single PR target, ~7-9 working days)¶
- Backend:
- Alembic migration for Deltas A + B (no new tables yet — capabilities are PR-2)
GET / POST / PATCH / deactivate / reactivateon/api/v1/admin/proceduresGET / PATCH / POST / DELETEon/api/v1/admin/procedures/{code}/facilities- Repositories:
ProcedureRepository(BaseUnscopedRepository — global catalog),ProviderProcedureRepository(BaseUnscopedRepository for cross-tenant admin view) - Service layer:
procedure_admin_service.py,provider_procedure_admin_service.py - Audit event emission
- 2 new permissions seeded:
procedure:read,procedure:write,provider_procedure:write(granted toplatform_admin);procedure:bulk_import(super_admin only, no-op in MVP) - Frontend:
ProceduresListPage+ProcedureDetailPage+FacilityReadinessSheetEditProcedureDialog(procedure metadata)- Edit-cell modal for provider_procedures
- Deactivate confirmation dialog (with active-case count from new endpoint)
- Tests:
tests/test_admin_procedures.py(router + service)tests/test_route_access_scanner.pyalready covers{case_id}/{patient_id}— these new routes have neither, so scanner remains green- Frontend Vitest tests mirroring
Providers.test.tsx
9.2 Follow-ups (separate PRs, in order)¶
| PR | Scope | Est. agent-days |
|---|---|---|
| PR-2 | provider_capabilities + procedure_capability_requirements tables + capability UI panel + readiness-score-recompute background task |
5-6 |
| PR-3 | Bulk import (YAML+CSV, dry-run, transactional apply) | 3-4 |
| PR-4 | Capability vocabulary management page (/admin/capabilities) |
2-3 |
| PR-5 | Procedure-history viewer (audit log timeline filtered to a procedure_code) | 1-2 |
| PR-6 | "Recompute all readiness" admin button + QStash-backed batch job | 1-2 |
Deferred indefinitely (not in any PR): procedure versioning (e.g. v1/v2 of the same procedure code), provider self-service edits to their own readiness, patient-facing readiness display polish.
10. Estimated Implementation Size¶
| Layer | MVP (PR-1) | All follow-ups | Total |
|---|---|---|---|
| Backend (Sonnet, after spec approval) | 3.5 agent-days | 7 agent-days | 10.5 agent-days |
| Frontend (Sonnet) | 4 agent-days | 5 agent-days | 9 agent-days |
| Spec + review (Opus) | already done here | 1 day across follow-ups | 1 day |
| Migration + Neo4j sync wiring (Sonnet) | 1 agent-day | 1 agent-day | 2 agent-days |
| Tests + CI green (Sonnet) | 1.5 agent-days | 2 agent-days | 3.5 agent-days |
| Total | 10 agent-days | 15 agent-days | ~25 agent-days |
Calendar mapping: at ~1.5-2 agent-days/calendar-day with parallel dispatch, MVP = ~1 week, all follow-ups = ~2 weeks. Total ~3 calendar weeks.
This confirms the week-plan's 2-3 week FE budget if FE is the gating chain. With BE parallelism it lands closer to 2 weeks. SD's week plan estimate is realistic — no revision needed.
11. Open Questions for SD¶
11.1 Procedure versioning — yes or no for MVP?¶
ADR-0018 §H Screen 1 open question #1 asks "Do we need procedure versioning? (e.g., requirements change over time)". This spec defaults to NO — procedures are mutable in place; audit log captures change history. If you want versioning (e.g., "Total Knee Replacement v2 with robotic technique" as a separate row), say so before PR-1 because it shapes the schema.
11.2 Tenant-scoped vs global catalog¶
This spec defaults to global — one shared catalog. ADR-0018 §H open question #2 asks "Should procedures be tenant-scoped or global catalog?" Global is consistent with how procedure_requirements is modeled today (no tenant_id). If you want tenant-specific procedure variants (e.g., Apollo's custom hip protocol), this becomes a major spec rewrite — please confirm.
11.3 Who can edit readiness scores?¶
This spec splits permissions: procedure:write (catalog) for platform_admin+, but provider_procedure:write (per-cell readiness) for platform_admin+ too. Per ADR-0018 §H open question #3 — is super_admin only acceptable, or do we want platform_admin to do cell-level edits? Default: both can. Confirm.
11.4 Bulk import permission gate¶
I've put procedure:bulk_import behind super_admin only in v1 because bulk operations are highest blast-radius. If you want platform_admin to also bulk-import (e.g., Curaway clinical ops team without super_admin role), flag it.
11.5 Provider self-edit pathway¶
Out of scope for this spec. Confirm that's OK — providers will continue to email Curaway ops with readiness updates until a future Provider Portal spec.
11.6 Currency handling¶
Per-cell currency override (FacilityReadinessSheet) means Apollo's USD pricing and Bumrungrad's THB pricing coexist in provider_procedures. Matching engine + patient UI convert to patient's preferred currency via currency_service. This matches CLAUDE.md "All money in USD cents + ISO 4217" rule. Confirm.
11.7 Capabilities scope split¶
I've moved provider_capabilities table creation to PR-2 (separate from MVP PR-1). MVP can compute a placeholder readiness score from just provider_procedures fields without the capability layer. Acceptable, or do you want capabilities in PR-1?
11.8 Neo4j + Qdrant auto-sync — synchronous or async?¶
I've defaulted to synchronous during the admin write (single transaction guarantee). At catalog-size 50-200 it's fast. If you'd rather hit QStash for these side-effects (durability) and surface an "Indexing…" badge in the UI, that's a slightly different UX. Flag if you want async.
12. Definition of Done (per .claude/rules/definition-of-done.md)¶
Will be pasted into the implementation PR description. Highlights:
- Tier 3 feature checklist — all sections scanned, no N/A without justification
- Alembic migration uses
DATABASE_URL_ADMIN, reversible downgrade - Multi-tenancy:
procedure_requirementsjustified as global (notenant_id);provider_capabilitiesscoped via provider FK - Per-resource auth: N/A (no
{case_id}/{patient_id}in any path) - Feature flag:
procedure_admin_ui_enabled(default off; flip on per environment) - Audit log: every write emits an event (§3.3)
- Tests: 90%+ coverage on new service code; Vitest tests mirroring
Providers.test.tsx - Docs: this spec +
docs/api/error-codes.mdadditions +docs/reference/feature-flags.md+mkdocs.ymlnav entry under "Specs"
Last updated: 2026-05-12 — DRAFT awaiting SD review.