Skip to content

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 (loader app/services/matching/registry.py, reference docs/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_code vocabulary 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 offeredis_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_CREATED
  • ADMIN_PROCEDURE_UPDATED
  • ADMIN_PROCEDURE_DEACTIVATED / ADMIN_PROCEDURE_REACTIVATED
  • ADMIN_PROVIDER_PROCEDURE_UPDATED (cell-level inline edit)
  • ADMIN_PROVIDER_CAPABILITY_UPDATED
  • ADMIN_PROCEDURE_BULK_IMPORT_RUN (with dry_run flag 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_404
  • PROCEDURE_CODE_DUPLICATE_409 (code must be globally unique)
  • PROCEDURE_HAS_ACTIVE_CASES_409 (deactivate guard; override with admin:force + ?force=true)
  • PROCEDURE_BULK_IMPORT_VALIDATION_422 (with per-row error list in details)
  • PROVIDER_PROCEDURE_NOT_FOUND_404
  • PROVIDER_PROCEDURE_INVALID_COST_RANGE_422 (min > avg, avg > max)
  • CAPABILITY_CODE_UNKNOWN_422 (not in config/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 PaginationControls component 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 (reuse ToastBanner).

5.5 Bulk Import modal

Modeled after the AddMappingForm / AddProviderModal pattern but full-screen:

  1. Drag-and-drop file box (YAML or CSV, ≤2MB)
  2. "Dry-run" toggle (default ON)
  3. [Validate] button → calls /bulk-import?dry_run=true
  4. Result panel: 3 lists (would_create, would_update, would_skip) + errors table
  5. 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), existing bg-status-warning-bg (amber), bg-status-error-bg (red), bg-muted / text-muted-foreground (inactive)
  • Brand color (Teal #008B8B) inherited via bg-primary Tailwind token, no override
  • Typography: existing Montserrat from apps/admin-app Tailwind config
  • Touch targets: min-h-[44px] per .claude/rules/definition-of-done.md FRONTEND section

6. Onboarding Flow (Operational Workflow)

6.1 New procedure (single)

  1. Admin clicks [+ Add Procedure] on list page → 4-step wizard
  2. Step 1 — Identity: code (CPT or custom), name, specialty, parent_procedure_code (optional)
  3. Step 2 — Clinical: ICD-10 + SNOMED chip-pickers (autocomplete from existing seed lists), keywords, required_documents, required_tests, comorbidity_screening, contraindications
  4. Step 3 — Logistics: typical_recovery_days, cost_range {low, mid, high, currency}, travel_considerations free text
  5. Step 4 — Review + submit: preview card, on submit:
  6. POST /api/v1/admin/procedures
  7. Backend creates row + emits ADMIN_PROCEDURE_CREATED event
  8. Backend triggers Neo4j auto-sync (POST /api/v1/admin/graph/procedure-sync — existing in admin_graph.py) — creates :Procedure node + REQUIRES_CAPABILITY edges from procedure_capability_requirements
  9. Backend triggers Qdrant auto-embed of name + description + keywords (existing pattern in seed_embeddings.py, wrapped into a service call)
  10. Backend triggers POST /api/v1/admin/system/bust-storefront-cache (existing #606 endpoint)
  11. Admin lands on new procedure's detail page with empty facilities grid → adds facilities

6.2 New facility readiness for an existing procedure

  1. Admin opens procedure detail page → [+ Add facility]
  2. Modal: provider picker (autocomplete across all 42+ providers), then form for cost / volume / success rate / lead_surgeon / technology / acceptance_criteria
  3. POST /api/v1/admin/procedures/{code}/facilities → row created, readiness score computed inline
  4. Cache bust + audit event emitted

6.3 Bulk import (50-200 procedures)

  1. Admin exports current config/procedures.yaml shape from production (out-of-band — included as a one-click "Download current catalog" link in the bulk-import modal for v2; v1 = upload only)
  2. Admin uploads YAML (preferred — matches existing seed shape) or CSV (column headers documented in tooltip)
  3. Dry-run shows full diff
  4. Admin applies → backend wraps in a single transaction:
  5. For each row: validate, upsert into procedure_requirements, emit ADMIN_PROCEDURE_CREATED or ADMIN_PROCEDURE_UPDATED per row
  6. At commit: single Neo4j sync batch + Qdrant batch embed + one cache bust
  7. Result page: summary counts + downloadable CSV of per-row outcomes
  8. If any row fails validation in non-dry-run, whole batch rolls back (atomic)

6.4 Deactivate procedure

  1. Admin clicks Deactivate → modal shows live_case_count from cases WHERE workflow_state->>'procedure_code' = :code AND state NOT IN ('case_complete','case_cancelled')
  2. If count > 0: require super_admin + ?force=true + reason field
  3. On confirm: PATCH is_active=false, set deactivated_at, emit ADMIN_PROCEDURE_DEACTIVATED
  4. Procedure stays in matching engine queries for existing cases (matching by procedure_code continues), but is hidden from new intake (intake_gates.py filters is_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 chars
  • specialty: required, must be one of the enum values seeded in config/specialties.yaml (orthopedics, cardiology, …)
  • typical_recovery_days: integer, 0-365
  • cost_range: if provided, low ≤ mid ≤ high, all > 0
  • icd10_primary[]: each entry matches [A-Z]\d{2}(\.\d{1,3})?
  • provider_procedure.{min,avg,max}_cost: integer cents, min ≤ avg ≤ max
  • success_rate_pct: 0-100 inclusive
  • complication_rate_pct: 0-100 inclusive; complication_rate + success_rate ≤ 100 warning (not hard fail)

7.2 Backend (authoritative)

All FE rules re-validated. In addition:

  • procedure_code global uniqueness — return PROCEDURE_CODE_DUPLICATE_409
  • (provider_id, procedure_code) uniqueness on provider_procedures (existing uq_provider_procedure constraint)
  • capability_code must exist in config/capabilities.yaml registry — CAPABILITY_CODE_UNKNOWN_422
  • currency_code: ISO 4217 (3 letters), must exist in currency_service rate 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_admin or platform_admin (cross-tenant scope justified — procedures are global)
  • provider_procedures writes verify providers.is_active = true for the target provider (cannot add readiness to a deactivated provider)
  • No raw tenant_id filtering on procedure_requirements (global catalog)
  • provider_capabilities reads/writes use BaseRepository._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_ratesuccess_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 / reactivate on /api/v1/admin/procedures
  • GET / PATCH / POST / DELETE on /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 to platform_admin); procedure:bulk_import (super_admin only, no-op in MVP)
  • Frontend:
  • ProceduresListPage + ProcedureDetailPage + FacilityReadinessSheet
  • EditProcedureDialog (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.py already 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_requirements justified as global (no tenant_id); provider_capabilities scoped 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.md additions + docs/reference/feature-flags.md + mkdocs.yml nav entry under "Specs"

Last updated: 2026-05-12 — DRAFT awaiting SD review.