Deploy Ladill Care / deploy (push) Successful in 1m26s
Add a template-driven assessment system with universal intake, clinical pathways, disease instruments (stroke MVP + extended, diabetes, and ten specialty packs), scoring, patient outcome trends, REST/API + FHIR export, and Enterprise org-level assessment analytics. Seed packs and design/licensing docs ship for deploy and pre-GA review.
1469 lines
72 KiB
Markdown
1469 lines
72 KiB
Markdown
# Layered Clinical Assessment Engine for Ladill Care
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| **Document title** | Layered Clinical Assessment Engine |
|
||
| **Author** | Ladill Care Engineering |
|
||
| **Date** | 2026-07-16 |
|
||
| **Status** | Approved for implementation |
|
||
| **Audience** | Senior engineers working in the Ladill Care Laravel codebase |
|
||
| **Related surfaces** | Consultations, patients, diagnoses, vitals, investigations, reports |
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
Ladill Care today supports patient registration, visits, consultations, free-text symptoms/notes, typed vital signs, free-text diagnoses, lab orders, prescriptions, and billing. What it does **not** yet support is structured clinical assessment across specialties: instruments such as NIHSS, mRS, NYHA, HbA1c foot exams, longitudinal outcome tracking, or automatic activation of disease-specific forms when a diagnosis is recorded.
|
||
|
||
This design introduces a **layered, metadata-driven clinical assessment engine**. Every patient completes a universal intake (Layer 1). When a clinician records or selects one or more diagnoses, matching **clinical pathways** activate (Layer 2) and load **disease-specific assessment templates** (Layer 3). Independent of disease, **generic outcome measures** (Layer 4) support longitudinal monitoring. The engine is hybrid: hot clinical data (demographics, allergies, vitals, diagnoses, labs) remains typed relational tables; specialty instruments and structured sections not yet modeled become template → question → answer instances.
|
||
|
||
The goal is to scale to many specialties and comorbidities without schema churn, without disease-specific patient columns, and without forcing disease modules at registration time.
|
||
|
||
---
|
||
|
||
## Background & Motivation
|
||
|
||
### Current state (codebase facts)
|
||
|
||
Multi-tenant healthcare app (`care.ladill.com`) with `owner_ref` + `organization_id` tenancy, UUIDs on public entities, soft deletes, Blade + Alpine UI, and service-layer business logic.
|
||
|
||
**Layer 1 is partially present as typed tables:**
|
||
|
||
| Concern | Table / model | Notes |
|
||
|---------|---------------|-------|
|
||
| Demographics | `care_patients` / `Patient` | name, DOB, gender, national_id, phone, email, address, city, region |
|
||
| Emergency contacts | `care_emergency_contacts` / `EmergencyContact` | |
|
||
| Insurance | `care_insurance_policies` / `InsurancePolicy` | |
|
||
| Allergies | `care_patient_allergies` / `PatientAllergy` | allergen, severity |
|
||
| Chronic conditions | `care_patient_conditions` / `PatientCondition` | free-text condition, not pathway-linked |
|
||
| Family history | `care_patient_family_history` / `PatientFamilyHistory` | |
|
||
| Vitals | `care_vital_signs` / `VitalSign` | BP, pulse, temp, weight, height, SpO₂, RR — **typed, consultation-scoped** |
|
||
| Diagnoses | `care_diagnoses` / `Diagnosis` | optional code + description, primary flag — **free text** |
|
||
| Labs | `care_investigation_*` | typed catalog + request + result; `care_investigation_result_values` uses a **single string** `value` column (EAV-like parameters, not typed columns) |
|
||
| Consultation narrative | `care_consultations.symptoms`, `.clinical_notes` | free text only |
|
||
|
||
**Clinical workflow already implemented:**
|
||
|
||
```
|
||
Appointment check-in → Visit → Consultation (draft)
|
||
→ Vitals (typed rows)
|
||
→ Symptoms / clinical notes (text)
|
||
→ Diagnoses (sync-delete-recreate in ConsultationService::syncDiagnoses)
|
||
→ Investigations / prescriptions / complete
|
||
```
|
||
|
||
Key entry points:
|
||
|
||
- UI: `resources/views/care/consultations/show.blade.php`
|
||
- Web: `App\Http\Controllers\Care\ConsultationController`
|
||
- API: `App\Http\Controllers\Api\ConsultationController`
|
||
- Domain: `App\Services\Care\ConsultationService`
|
||
- Auth: `CarePermissions` roles (`doctor`, `nurse`, …) + `ScopesToAccount` / `BelongsToOwner`
|
||
- Audit: `AuditLogger::record(...)` with actions listed in `config/care.php` → `audit_actions`
|
||
- Reports: `ReportService::clinicalReport` groups diagnosis descriptions only
|
||
- Seeders: `database/seeders/` currently has **no** assessment content-pack pattern (greenfield for this engine)
|
||
|
||
### Pain points
|
||
|
||
1. **Unstructured HPI** — symptoms are a textarea; no duration, pain score, or systematic functional status.
|
||
2. **No specialty instruments** — stroke/diabetes/COPD scoring cannot be captured, scored, or trended.
|
||
3. **Diagnosis is dead data for workflow** — recording “Ischaemic stroke” does not surface NIHSS or swallow assessment.
|
||
4. **Comorbidity gap** — `care_patient_conditions` is a static list; it does not load assessment packs.
|
||
5. **Longitudinal gap** — vitals append rows per consultation; there is no first-class QoL, falls, adherence, or readmission outcome series.
|
||
6. **Schema risk** — adding NIHSS as columns on patients/consultations would explode tables and break when guidelines change.
|
||
|
||
### Why now
|
||
|
||
Ladill Care is positioned for multi-specialty facilities (see `config/care.php` department types: maternity, dental, physiotherapy, etc.). Without a modular assessment engine, each specialty request becomes a one-off schema + UI project. A template-driven engine amortizes that cost.
|
||
|
||
---
|
||
|
||
## Goals & Non-Goals
|
||
|
||
### Goals
|
||
|
||
1. **Universal first** — register and consult any patient without selecting a specialty module.
|
||
2. **Diagnosis-activated pathways** — linking a diagnosis (or explicit clinician selection) activates one or more clinical pathways and their assessment templates.
|
||
3. **Comorbidity-safe** — N active pathways ⇒ N concurrent template sets on the same patient/consultation.
|
||
4. **Metadata-driven instruments** — new diseases and guideline updates ship as seed/content packs, not migrations of clinical columns.
|
||
5. **Hybrid storage** — keep typed tables for demographics, vitals, diagnoses, labs; use template answers for instruments and missing structured universal sections.
|
||
6. **Longitudinal by design** — every assessment instance is dated, patient-scoped, optionally consultation/visit-linked, with status lifecycle.
|
||
7. **Fit existing patterns** — `care_` tables, UUIDs, soft deletes, `owner_ref` on **runtime** data, organization/branch scope, `CarePermissions`, `AuditLogger`, service classes, Blade sections on consultation show.
|
||
8. **Incremental delivery** — ship engine + universal template first; pathways; scored stroke MVP (NIHSS + mRS); then more packs.
|
||
|
||
### Non-Goals
|
||
|
||
1. **Full ICD-10/SNOMED browser** in v1 — free-text + optional code remains; pathway matching uses configurable code/keyword rules, not a complete terminology server.
|
||
2. **Replacing typed vitals or lab catalog** with EAV — those stay relational.
|
||
3. **All twelve specialty modules in the first release** — Stroke MVP (NIHSS + mRS) then Diabetes; others follow as content packs.
|
||
4. **Treatment plan engine / care pathways as order sets** — assessment only; orders stay prescriptions/investigations.
|
||
5. **Offline mobile form runtime** — web + existing API patterns first.
|
||
6. **Clinical decision support alerts** (e.g. thrombolysis eligibility) beyond computed scores and basic validation.
|
||
7. **Patient self-assessment portal** in v1 (may reuse templates later).
|
||
8. **Migrating historical free-text symptoms into structured answers** automatically.
|
||
9. **Org-level pathway match_rules overrides** in v1 (platform rules only).
|
||
10. **Auto-copy between symptoms textarea and universal intake** in either direction.
|
||
|
||
---
|
||
|
||
## Key Decisions
|
||
|
||
| # | Decision | Rationale |
|
||
|---|----------|-----------|
|
||
| KD-1 | **Hybrid model**: typed clinical hot path + template-driven instruments | Matches agreed principles; vitals/labs/diagnoses already typed and report-friendly (`ReportService`, consultation UI). Avoids EAV for BP/weight used every visit. |
|
||
| KD-2 | **Assessment instances are first-class rows** (`care_assessments`), not JSON blobs on consultation | Enables longitudinal queries, multi-template per visit, draft/complete status, audit subject IDs. |
|
||
| KD-3 | **Answers store values in typed columns** (`value_text`, `value_number`, `value_boolean`, `value_date`, `value_json`) keyed by `question_id`; **exactly one authoritative column per answer_type**; unused columns **must be null** | Improves on string-only `care_investigation_result_values.value` for numeric scoring (NIHSS, CAT). Write/read invariants prevent dual-column drift. |
|
||
| KD-4 | **Pathways are overlays, not patient types** | Patient registration stays universal (`PatientService` / `_form.blade.php` unchanged for specialty). Pathways attach via `care_patient_pathways`. |
|
||
| KD-5 | **Pathway activation is explicit + rule-assisted** | Auto-suggest from diagnosis code/description match; clinician confirms. Avoids silent activation on ambiguous free-text diagnoses. |
|
||
| KD-6 | **Catalog is platform-global (no `owner_ref`); runtime always tenant-scoped** | Clinical instruments (NIHSS) are platform-standard. **Intentionally different** from org-scoped `care_investigation_types`. See [Catalog tenancy contract](#catalog-tenancy-contract). Org template forks are **out of v1**. |
|
||
| KD-7 | **Score materialization table ships with scoring PR (PR 4), not engine schema PR** | Keeps PR 1 small; scores needed before specialty instruments (KD-7 timing with specialty module). |
|
||
| KD-8 | **Link assessments to consultation when captured in-consult; always require `patient_id`** | Supports in-visit workflow and standalone follow-up assessments without open consultation. |
|
||
| KD-9 | **Capture gate = route ability + `assertCaptureAllowed` with admin bypass** | Flat abilities cannot express per-template rules alone. Nurses get `assessments.view` + `assessments.capture`; doctors get those + `assessments.manage` + `pathways.manage`. **Facility owners are `hospital_admin`** (`OrganizationResolver::ensureOwnerMember`), not `doctor` — admins must capture disease instruments. Rule: `hospital_admin` / `super_admin` always allowed; else member `role` must be in `template.meta.capture_roles`. See [Permissions](#permissions-carepermissions). |
|
||
| KD-10 | **Content packs as versioned JSON under `database/data/assessments/` + seeders** | Reproducible deploys; no admin form builder in v1. Canonical schema required (see [Seed JSON schema](#seedjson-content-pack-schema)). |
|
||
| KD-11 | **Do not expand `care_patients` with social/functional columns** | Put structured HPI / social history / functional status into universal assessment templates so they can evolve without migrations. |
|
||
| KD-12 | **Diagnoses remain on `care_diagnoses`** | Pathway engine reads diagnoses; does not replace them. Activation snapshots `activation_diagnosis_text` only — **never** diagnosis row FKs (`syncDiagnoses` deletes/recreates IDs every save). |
|
||
| KD-13 | **Branch visibility matches consultations** | List/show assessments apply `OrganizationResolver::branchScope` like `ConsultationController::authorizeConsultation`. Branch-scoped members only see assessments for their branch; admins without branch scope see org-wide. Write: `branch_id` from visit (preferred) else `patient.branch_id`. |
|
||
| KD-14 | **Completed assessments are immutable in v1** | `PUT` / complete on non-draft → **422**. No amend flow. Soft-delete assessment for administrative removal only (answers hard-owned by assessment; queries always scope non-deleted assessments). |
|
||
| KD-15 | **Consultation free-text remains narrative source of truth for HPI** | `care_consultations.symptoms` / `clinical_notes` stay primary narrative for discharge-style notes and existing UI. Universal intake is an **optional structured overlay**. Show both on chart; **no auto-copy either direction** in v1. |
|
||
| KD-16 | **Pathway bindings store `template_code` only** (no `template_id` on `care_pathway_templates`) | Resolve `is_current` at assessment start so version bumps do not break pathway rows. Historical assessments keep their own `template_id` FK. |
|
||
| KD-17 | **One active pathway per (patient, pathway); one draft per (patient, template, consultation)** | Enforced in transactions with `lockForUpdate`; see [Concurrency & uniqueness](#concurrency--uniqueness). |
|
||
| KD-18 | **Plan packaging: engine + disease capture free; advanced analytics Enterprise** | Core assessment engine, universal intake, pathway activation, and disease pack capture are available without Enterprise gate. Org-level outcome trends / advanced assessment analytics are Enterprise-packaged (see Rollout). |
|
||
| KD-19 | **Single Stroke pathway; subtype is assessment data (includes TIA)** | TIA, ischaemic, and haemorrhagic are not separate pathways. Capture subtype on stroke clinical instruments / match TIA keywords to the same `stroke` pathway. |
|
||
| KD-20 | **Pathways and `care_patient_conditions` stay independent** | Activating a pathway does **not** auto-add or sync chronic condition list rows. Clinicians maintain conditions separately. |
|
||
|
||
---
|
||
|
||
## Proposed Design
|
||
|
||
### Layer model
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph L1["Layer 1 — Universal"]
|
||
Demo[Demographics typed]
|
||
EC[Emergency / Insurance typed]
|
||
All[Allergies / Family / Conditions typed]
|
||
UnivTpl[Universal intake template]
|
||
Vitals[Vitals typed]
|
||
Narrative[Consultation symptoms/notes free text]
|
||
end
|
||
|
||
subgraph L2["Layer 2 — Pathway selection"]
|
||
Dx[Persisted diagnoses]
|
||
Match[Pathway matcher]
|
||
PP[Patient pathways active]
|
||
end
|
||
|
||
subgraph L3["Layer 3 — Disease modules"]
|
||
Stroke[Stroke templates]
|
||
DM[Diabetes templates]
|
||
Other[Other content packs...]
|
||
end
|
||
|
||
subgraph L4["Layer 4 — Outcomes"]
|
||
OutTpl[Generic outcome template]
|
||
Scores[Score / trend series]
|
||
end
|
||
|
||
Demo --> UnivTpl
|
||
Narrative -.->|no auto-copy v1| UnivTpl
|
||
UnivTpl --> Dx
|
||
Vitals --> Dx
|
||
Dx --> Match
|
||
Match --> PP
|
||
PP --> Stroke
|
||
PP --> DM
|
||
PP --> Other
|
||
Stroke --> OutTpl
|
||
DM --> OutTpl
|
||
UnivTpl --> OutTpl
|
||
OutTpl --> Scores
|
||
```
|
||
|
||
### Clinical workflow (target)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant R as Reception
|
||
participant N as Nurse
|
||
participant D as Doctor
|
||
participant Eng as AssessmentEngine
|
||
participant DB as DB
|
||
|
||
R->>DB: Register patient (typed demographics)
|
||
R->>DB: Check-in visit / appointment
|
||
N->>DB: Record vitals (care_vital_signs)
|
||
N->>Eng: Start universal intake (capture ability)
|
||
Eng->>DB: care_assessments + answers
|
||
D->>DB: Save consultation notes + diagnoses
|
||
Note over D,Eng: Suggestions use persisted diagnoses only
|
||
D->>Eng: GET pathway-suggestions
|
||
Eng-->>D: Stroke, Diabetes + match reasons
|
||
D->>Eng: Activate pathways (confirm)
|
||
Eng->>DB: care_patient_pathways + required drafts
|
||
D->>Eng: Complete NIHSS / mRS
|
||
Eng->>DB: Answers + scores
|
||
D->>DB: Plan (Rx / labs) + complete consultation
|
||
Note over Eng,DB: Follow-up: outcome template + disease follow-ups
|
||
```
|
||
|
||
### Architecture (application)
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
UI[Blade consultation / patient show]
|
||
API[API AssessmentController]
|
||
Ctl[Care\\AssessmentController<br/>PathwayController]
|
||
Svc[AssessmentService<br/>PathwayService<br/>ScoringService]
|
||
Feat[CareFeatures rollout flags]
|
||
Match[PathwayMatcher]
|
||
Models[(Catalog global<br/>Runtime tenant-scoped)]
|
||
Exist[ConsultationService<br/>Patient / Diagnosis / VitalSign]
|
||
|
||
UI --> Ctl
|
||
UI --> Feat
|
||
API --> Svc
|
||
Ctl --> Svc
|
||
Ctl --> Feat
|
||
Svc --> Models
|
||
Svc --> Match
|
||
Exist --> Match
|
||
Svc --> Exist
|
||
```
|
||
|
||
Place services under `app/Services/Care/` alongside `ConsultationService`, `InvestigationService`. Controllers under `app/Http/Controllers/Care/` and `Api/`. Follow `ScopesToAccount` + `authorizeAbility` patterns from `ConsultationController`.
|
||
|
||
### Domain concepts
|
||
|
||
| Concept | Definition |
|
||
|---------|------------|
|
||
| **Template** | Versioned form definition (e.g. `universal_intake`, `nihss`, `mrs`, `outcome_core`). |
|
||
| **Question** | Field within a template: code, label, answer type, options, validation, scoring weight, section. |
|
||
| **Assessment** | Completed or draft instance of a template for a patient at a point in time. |
|
||
| **Answer** | Value for one question on one assessment (single typed column authoritative). |
|
||
| **Pathway** | Clinical module registry entry (Stroke, Diabetes, …) binding diagnosis match rules to templates by **code**. |
|
||
| **Patient pathway** | Active association of a pathway to a patient (onset, status, activating diagnosis text snapshot). |
|
||
| **Score** | Materialized instrument total/subscores for an assessment. |
|
||
|
||
### Catalog tenancy contract
|
||
|
||
This engine **intentionally diverges** from `care_investigation_types` (which is org-scoped with `owner_ref` + `organization_id`).
|
||
|
||
| Layer | Tables | Tenancy | Authz |
|
||
|-------|--------|---------|-------|
|
||
| **Catalog** | `care_assessment_templates`, `care_assessment_questions`, `care_clinical_pathways`, `care_pathway_templates` | **No `owner_ref`**. Platform-global rows only in v1. `organization_id` is **null** and reserved for a future org-fork phase (not used in v1 writes). | Read: any authenticated Care member with `assessments.view` (or consultation context). Write: **not via product UI in v1** — only seeders/migrations/deploy. No `authorizeOwner` on catalog models. |
|
||
| **Runtime** | `care_assessments`, `care_assessment_answers`, `care_assessment_scores`, `care_patient_pathways` | Always `owner_ref` + `organization_id` copied from patient (and `branch_id` per KD-13). Models use `BelongsToOwner`. | `authorizeOwner` + org check + branchScope like consultations. |
|
||
|
||
**Models / traits:**
|
||
|
||
- Catalog models: **do not** use `BelongsToOwner`.
|
||
- Runtime models: **do** use `BelongsToOwner` + soft deletes where specified.
|
||
|
||
**Canonical catalog queries:**
|
||
|
||
```php
|
||
// Current system template by code (v1: all catalog rows are system)
|
||
AssessmentTemplate::query()
|
||
->whereNull('organization_id')
|
||
->where('code', $code)
|
||
->where('is_current', true)
|
||
->where('is_active', true)
|
||
->whereNull('deleted_at')
|
||
->firstOrFail();
|
||
|
||
// Active pathways for matching
|
||
ClinicalPathway::query()
|
||
->where('is_active', true)
|
||
->whereNull('deleted_at')
|
||
->orderBy('sort_order')
|
||
->get();
|
||
```
|
||
|
||
**Seeding across tenants:** one shared catalog in the app database; all `owner_ref` tenants read the same NIHSS definition. White-label multi-DB deploys each seed their own catalog (same JSON packs). There is no per-tenant catalog copy in v1.
|
||
|
||
**Why not `owner_ref` on catalog:** system NIHSS is not owned by any hospital account; putting a seeder `owner_ref` would break `authorizeOwner` for other tenants or force awkward sentinel values.
|
||
|
||
### Template categories
|
||
|
||
| `category` | Purpose | Default `meta.capture_roles` |
|
||
|------------|---------|------------------------------|
|
||
| `universal` | Layer 1 structured intake | `["doctor","nurse"]` |
|
||
| `disease` | Layer 3 instruments | `["doctor"]` |
|
||
| `outcome` | Layer 4 longitudinal | `["doctor","nurse"]` |
|
||
| `screening` | Optional later | `["doctor","nurse"]` |
|
||
|
||
### Answer types and write/read invariants
|
||
|
||
Supported `answer_type` values. **Exactly one storage column is authoritative**; on write, all other value columns **must be set to null**.
|
||
|
||
| Type | Authoritative column | Payload shape | UI control |
|
||
|------|----------------------|---------------|------------|
|
||
| `text` | `value_text` | string | input |
|
||
| `textarea` | `value_text` | string | textarea |
|
||
| `number` | `value_number` | numeric | number input |
|
||
| `integer` | `value_number` | integer (stored as decimal) | number input |
|
||
| `boolean` | `value_boolean` | bool | checkbox |
|
||
| `date` | `value_date` | `Y-m-d` | date |
|
||
| `datetime` | `value_date` | ISO datetime | datetime |
|
||
| `single_choice` | `value_text` | option `code` string | radio/select |
|
||
| `multi_choice` | `value_json` | array of option codes | checkboxes |
|
||
| `scale` | `value_number` | number within min/max | Likert / 0–10 |
|
||
| `score_item` | `value_number` | numeric score (from choice or direct) | scored choice (NIHSS item) |
|
||
| `calculated` | `value_number` | **not client-writable** in v1 | read-only display |
|
||
|
||
**Normalization (`AssessmentService::normalizeValue`):**
|
||
|
||
1. Validate raw payload against type + `validation` / `options`.
|
||
2. Build row with **only** the authoritative column set; explicitly null the others.
|
||
3. For `score_item` + `single_choice`-style choices: client may send option `code`; server maps `options.choices[].score` → `value_number`, and may also store code in `value_text` **only if** type were `single_choice`. For pure `score_item`, store score in `value_number` only (code optional in `value_json` as `{"code":"1"}` if audit of selection needed — v1: store score only in `value_number`).
|
||
4. For `multi_choice`: scores are **not** auto-summed unless `scoring_strategy` handles them (v1 disease instruments use `score_item`, not multi_choice, for scored totals).
|
||
5. **`calculated` fields (v1 deferred for formulas):** if present, ignore client input; recompute only if `options.formula` is absent — **v1 ships no calculated questions**. Reserve type for PR later; seed packs must not use `calculated` until formula support lands.
|
||
|
||
**Read path:** UI and scoring always read the authoritative column for the question’s `answer_type`; never fall back to another column.
|
||
|
||
Options JSON on questions:
|
||
|
||
```json
|
||
{
|
||
"choices": [
|
||
{"code": "0", "label": "No symptoms at all", "score": 0},
|
||
{"code": "1", "label": "No significant disability", "score": 1}
|
||
],
|
||
"min": 0,
|
||
"max": 6,
|
||
"unit": null
|
||
}
|
||
```
|
||
|
||
### Scoring contract
|
||
|
||
`ScoringService` runs on **complete** (and optionally `preview` on draft save for live totals — preview does not persist).
|
||
|
||
| `scoring_strategy` | Behavior |
|
||
|--------------------|----------|
|
||
| `null` / empty | No score row required (e.g. free-text heavy forms). Complete still validates required questions. |
|
||
| `sum_items` | Sum `value_number` for all questions with `answer_type = score_item` (and optional `score_key` buckets into `subscores`). `max_score` from sum of each item’s `options.max` or max choice score. |
|
||
| `single_value` | Exactly one scored question (usually `score_item` or `scale`); `total_score` = that `value_number`. Used for mRS. |
|
||
| `custom:{Handler}` | Map to `App\Services\Care\Scoring\{Handler}` implementing `ScoresAssessment` interface (`score(Assessment): array{total, max, subscores, severity_label}`). Register via strategy string only — no dynamic class from user input. |
|
||
|
||
**Complete-time validation:**
|
||
|
||
1. All `is_required` questions have non-null authoritative values.
|
||
2. If strategy is `sum_items` or `single_value`, all `score_item` questions required for the instrument must be present (treat missing as 422).
|
||
3. On success: upsert `care_assessment_scores`; audit `assessment.completed`.
|
||
4. **Idempotent complete:** if already `completed`, return 422 (no re-complete) per KD-14.
|
||
5. Scoring failure: `Log::warning` with `template_code`, `assessment_uuid`, exception message; do not complete; return 422.
|
||
|
||
### Pathway matching (detailed)
|
||
|
||
`PathwayMatcher` input: **persisted** diagnosis rows only (`code`, `description`) — see [Pathway suggestion UX](#pathway-suggestion-ux-timing).
|
||
|
||
Rules on `care_clinical_pathways.match_rules`:
|
||
|
||
```json
|
||
{
|
||
"icd_prefixes": ["I60", "I61", "I62", "I63", "I64", "G45"],
|
||
"keywords": ["stroke", "cva", "tia", "transient ischaemic attack", "transient ischemic attack", "cerebrovascular", "hemiplegia", "ischaemic stroke", "ischemic stroke"],
|
||
"exclude_keywords": ["family history"]
|
||
}
|
||
```
|
||
|
||
TIA matches the same `stroke` pathway (KD-19); subtype is not a separate pathway.
|
||
|
||
**Algorithm (v1, exact):**
|
||
|
||
```
|
||
normalize(s) = lowercase(trim(s)); collapse internal whitespace; ASCII-fold optional (v1: mb_strtolower only)
|
||
for each active pathway P:
|
||
best_rank = none
|
||
best_reason = null
|
||
for each diagnosis D:
|
||
code_n = normalize(D.code) without dots for prefix compare? → strip non-alnum for prefix: "I63.9" → "I639" then prefix match on stripped prefixes
|
||
Actually v1: strip dots from code: "I63.9" → "I639"; prefixes stored without dots "I63" matches startswith
|
||
desc_n = normalize(D.description)
|
||
if any exclude_keyword in desc_n as substring → skip this diagnosis for P
|
||
if any icd_prefix where stripped_code starts with stripped_prefix → rank=100, reason="icd_prefix:{prefix}"
|
||
else if any keyword where keyword is substring of desc_n → rank=50, reason="keyword:{keyword}"
|
||
keep max rank for P across diagnoses
|
||
if best_rank: include P once with rank + reason
|
||
sort by rank desc, then pathway.sort_order
|
||
return unique pathways (one entry per pathway_id)
|
||
```
|
||
|
||
**Examples:**
|
||
|
||
| Diagnosis | Expected |
|
||
|-----------|----------|
|
||
| code `I63.9`, desc empty | Match stroke (icd_prefix I63), rank 100 |
|
||
| code empty, desc `Ischaemic stroke` | Match stroke (keyword), rank 50 |
|
||
| desc `History unclear` | No match |
|
||
| desc `Family history of stroke` with exclude `family history` | No match if exclude_keywords configured |
|
||
|
||
**Org overrides:** out of scope v1 (Non-Goal 9). Platform `match_rules` only.
|
||
|
||
**API suggestion payload includes match reason** for UI chips (“Matched ICD I63”, “Matched keyword: stroke”).
|
||
|
||
### Pathway suggestion UX timing
|
||
|
||
Diagnoses exist in DB only after `ConsultationService::save` → `syncDiagnoses` (delete-recreate). Alpine form state is **not** the source for server suggestions.
|
||
|
||
**v1 rules:**
|
||
|
||
1. **Server suggestions (`GET .../pathway-suggestions`)** use **persisted** diagnoses on the consultation only. After Save, show section refreshes (or client refetches). Empty if no diagnoses saved yet.
|
||
2. **Optional UX:** button **“Save diagnoses & suggest pathways”** that POSTs consultation update then redirects/refetches suggestions (single CTA). Preferred in consultation UI copy.
|
||
3. **Optional client preview (non-authoritative):** embed active pathway keyword/prefix lists once on page load for live Alpine preview of unsaved diagnosis rows; label as “Preview — save to confirm”. Must not activate pathways from preview alone.
|
||
4. **Activation snapshot:** store `activation_diagnosis_text` (concat descriptions/codes at activate time). **Never** store diagnosis row IDs (IDs are unstable under `syncDiagnoses`).
|
||
|
||
### Consultation UI integration
|
||
|
||
Extend `resources/views/care/consultations/show.blade.php` with new sections (same card pattern as vitals/diagnoses), gated by `CareFeatures`:
|
||
|
||
1. **Universal assessment** — link/form for incomplete universal intake for this visit (`$canCaptureAssessment`).
|
||
2. **Active pathways** — chips; “Add clinical pathway” (`pathways.manage`).
|
||
3. **Suggested pathways** — from **saved** diagnoses + match reasons; empty state: “Save diagnoses to see suggestions”.
|
||
4. **Disease assessments** — drafts/completes for this consultation + patient; “Start NIHSS” if capture_roles allow.
|
||
5. **Outcomes** — optional follow-up CTA.
|
||
|
||
UI flags (mirror vitals split in `ConsultationController`):
|
||
|
||
```php
|
||
$canViewAssessments = permissions->can($member, 'assessments.view');
|
||
$canCaptureAssessment = permissions->can($member, 'assessments.capture')
|
||
|| permissions->can($member, 'assessments.manage');
|
||
$canManagePathways = permissions->can($member, 'pathways.manage');
|
||
// Per-template start/save: AssessmentService::assertCaptureAllowed (admin bypass + capture_roles)
|
||
```
|
||
|
||
Patient chart: timeline of assessments (branch-scoped per KD-13) + active pathways + latest scores. Free-text symptoms **and** latest universal intake shown as separate cards (KD-15).
|
||
|
||
**Routes (authoritative list — no DELETE for pathways; use deactivate):**
|
||
|
||
```
|
||
GET /patients/{patient}/assessments
|
||
GET /patients/{patient}/assessments/create
|
||
POST /patients/{patient}/assessments
|
||
POST /consultations/{consultation}/assessments
|
||
GET /assessments/{assessment}
|
||
PUT /assessments/{assessment}
|
||
POST /assessments/{assessment}/complete
|
||
POST /assessments/{assessment}/cancel
|
||
GET /patients/{patient}/pathways
|
||
POST /patients/{patient}/pathways
|
||
POST /patients/{patient}/pathways/{patientPathway}/deactivate
|
||
GET /consultations/{consultation}/pathway-suggestions
|
||
```
|
||
|
||
This mirrors lab requests (`care.lab.requests.store`) as a separate POST from consultation save.
|
||
|
||
### Hook points in existing services
|
||
|
||
| Location | Change |
|
||
|----------|--------|
|
||
| `ConsultationService::save` | No silent pathway activation. Optional: controller after save redirects with flash “diagnoses saved — review pathway suggestions”. |
|
||
| `ConsultationService::complete` | Soft warning only if flag `assessment_required_on_complete` (default false) — non-blocking in v1. |
|
||
| `Patient` model | Relations: `pathways()`, `assessments()`. |
|
||
| `Consultation` model | Relation: `assessments()`. |
|
||
| `CarePermissions` | Abilities: `assessments.view`, `assessments.capture`, `assessments.manage`, `pathways.manage`. |
|
||
| `config/care.php` | Status enums, audit actions, template categories. |
|
||
| `database/seeders/DatabaseSeeder.php` | Call assessment/pathway seeders in deploy path (PR 3+). |
|
||
|
||
### Dynamic form rendering
|
||
|
||
v1: Blade partial `resources/views/care/assessments/_form.blade.php` switches on `answer_type`. Alpine for multi-item instruments. No SPA.
|
||
|
||
```php
|
||
// Pseudocode — AssessmentService::saveAnswers
|
||
abort_unless($assessment->status === Assessment::STATUS_DRAFT, 422);
|
||
// hospital_admin/super_admin always allow; else role ∈ capture_roles
|
||
$this->assertCaptureAllowed($member, $assessment->template);
|
||
|
||
foreach ($template->questions as $question) {
|
||
if ($question->answer_type === 'calculated') {
|
||
continue; // v1 unused
|
||
}
|
||
$raw = $payload[$question->code] ?? null;
|
||
$this->validateAnswer($question, $raw);
|
||
AssessmentAnswer::updateOrCreate(
|
||
['assessment_id' => $assessment->id, 'question_id' => $question->id],
|
||
array_merge(
|
||
['owner_ref' => $ownerRef],
|
||
$this->normalizeValue($question, $raw), // sets one column, nulls others
|
||
),
|
||
);
|
||
}
|
||
```
|
||
|
||
### Versioning templates
|
||
|
||
When NIHSS items change:
|
||
|
||
1. Insert new `care_assessment_templates` row with same `code`, incremented `version`, `is_current = true`; mark previous `is_current = false`.
|
||
2. Historical assessments keep FK to the old template version (immutable definition).
|
||
3. New assessments resolve via `template_code` → `is_current` (pathway bindings never store versioned `template_id`).
|
||
4. Unit test required: bump version; pathway binding still resolves; new start uses new version; old assessment still loads old questions.
|
||
|
||
### Concurrency & uniqueness
|
||
|
||
**Active patient pathway**
|
||
|
||
- Service: `DB::transaction` + `PatientPathway::where(patient, pathway)->lockForUpdate()`.
|
||
- If row `status = active` exists → return existing (idempotent activate) or 422 “already active” (prefer **idempotent return** of existing active row).
|
||
- If `resolved` / `inactive` → create **new** row with `active` (history preserved) OR reactivate same row — v1: **create new active row** after setting old to stay resolved/inactive (audit trail). Enforce at most one `active` via transaction check (not partial unique index in MySQL without workarounds).
|
||
- Optional DB aid: generated column `active_pathway_key` = `patient_id` when status=active else NULL + unique `(active_pathway_key, pathway_id)` if MySQL version supports; otherwise service lock is sufficient for v1.
|
||
|
||
**Draft assessments**
|
||
|
||
- v1: **at most one draft** per `(patient_id, template_id, consultation_id)` when `consultation_id` present; when no consultation, at most one draft per `(patient_id, template_id)` with `consultation_id` null and `status=draft`.
|
||
- `start()`: if draft exists, **return existing draft** (idempotent) instead of creating duplicate.
|
||
- Multiple **completed** assessments of same template over time are allowed (longitudinal).
|
||
|
||
### Soft-delete / cancel lifecycle
|
||
|
||
| Status / action | Behavior |
|
||
|-----------------|----------|
|
||
| `draft` | Editable via PUT; completeable; **cancellable** via `POST .../cancel` → `status=cancelled` (not soft-deleted). |
|
||
| `completed` | Immutable; PUT/complete/cancel → **422**. Soft-delete only via admin tooling (out of product UI v1) if legally required. |
|
||
| `cancelled` | Terminal for that instance; excluded from “open assessments” lists; retained for audit. |
|
||
| Soft-delete assessment | Answers remain (FK cascade only on hard delete). Default global scope / queries: `Assessment::query()` excludes soft-deleted. **Never** list answers without joining non-deleted assessment. Hard delete only in tests. |
|
||
| Answers | No soft deletes; lifetime bound to assessment row. |
|
||
|
||
Amend of completed assessments: **out of v1** (Open Question remains for product later; KD-14 locks complete).
|
||
|
||
---
|
||
|
||
## API / Interface Changes
|
||
|
||
### Web routes (additions to `routes/web.php` inside `care.setup` group)
|
||
|
||
| Method | Route name | Ability |
|
||
|--------|------------|---------|
|
||
| GET `/patients/{patient}/assessments` | `care.assessments.index` | `assessments.view` |
|
||
| GET `/assessments/{assessment}` | `care.assessments.show` | `assessments.view` |
|
||
| GET `/patients/{patient}/assessments/create` | `care.assessments.create` | `assessments.capture` or `assessments.manage` |
|
||
| POST `/patients/{patient}/assessments` | `care.assessments.store` | `assessments.capture` or `assessments.manage` |
|
||
| POST `/consultations/{consultation}/assessments` | `care.consultations.assessments.store` | `assessments.capture` or `assessments.manage` |
|
||
| PUT `/assessments/{assessment}` | `care.assessments.update` | `assessments.capture` or `assessments.manage` |
|
||
| POST `/assessments/{assessment}/complete` | `care.assessments.complete` | `assessments.capture` or `assessments.manage` |
|
||
| POST `/assessments/{assessment}/cancel` | `care.assessments.cancel` | `assessments.capture` or `assessments.manage` |
|
||
| GET `/patients/{patient}/pathways` | `care.pathways.index` | `assessments.view` |
|
||
| POST `/patients/{patient}/pathways` | `care.pathways.store` | `pathways.manage` |
|
||
| POST `/patients/{patient}/pathways/{patientPathway}/deactivate` | `care.pathways.deactivate` | `pathways.manage` |
|
||
| GET `/consultations/{consultation}/pathway-suggestions` | `care.consultations.pathway-suggestions` | `consultations.view` |
|
||
|
||
Route model binding: assessments use `uuid` (`getRouteKeyName()`), consistent with `Consultation`, `Patient`, `Visit`.
|
||
|
||
### Request / response contracts
|
||
|
||
#### Public ID convention (web + API)
|
||
|
||
Care public routes bind consultations, visits, patients, and assessments by **UUID** (`getRouteKeyName()`). Request bodies that reference those entities **must accept UUID strings**, never internal bigint primary keys.
|
||
|
||
| Public field | Resolves to |
|
||
|--------------|-------------|
|
||
| `consultation` route param / `consultation_uuid` body | `care_consultations.id` FK |
|
||
| `visit_uuid` body (optional) | `care_visits.id` FK |
|
||
| `patient` route param | `care_patients.id` FK |
|
||
| `assessment` route param | `care_assessments.id` |
|
||
|
||
Controllers resolve UUID → model with tenant/branch checks, then pass integer FKs into services. Internal service methods may use integer IDs; **HTTP contracts never expose integer FKs as client-facing identifiers**.
|
||
|
||
#### `POST /patients/{patient}/assessments` (or consultation-scoped store)
|
||
|
||
```json
|
||
{
|
||
"template_code": "universal_intake",
|
||
"consultation_uuid": null,
|
||
"visit_uuid": null,
|
||
"patient_pathway_uuid": null
|
||
}
|
||
```
|
||
|
||
Consultation-scoped route `POST /consultations/{consultation}/assessments` takes the consultation from the path (UUID route binding); body needs only `template_code` (+ optional pathway uuid).
|
||
|
||
**Responses:**
|
||
|
||
| Code | When |
|
||
|------|------|
|
||
| 201 | Created (or 200 if idempotent existing draft returned — document as 200 with existing uuid) |
|
||
| 403 | Missing ability or `assertCaptureAllowed` denies member role |
|
||
| 404 | Patient/consultation wrong tenant/branch or unknown UUID |
|
||
| 422 | Template inactive / not current / unknown code |
|
||
|
||
#### `PUT /assessments/{assessment}`
|
||
|
||
```json
|
||
{
|
||
"answers": {
|
||
"chief_complaint": "Sudden right weakness",
|
||
"pain_score": 3,
|
||
"smoking_status": "former"
|
||
},
|
||
"notes": "Optional free text"
|
||
}
|
||
```
|
||
|
||
Answers keyed by **question code**. Partial updates allowed (only keys present are written). Omit key → leave previous answer unchanged; send `null` → clear if question not required (required null → 422 on complete, not necessarily on draft save).
|
||
|
||
**Responses:** 200 OK; **422** if not draft; **403** `assertCaptureAllowed`; **404** tenant.
|
||
|
||
#### `POST /assessments/{assessment}/complete`
|
||
|
||
Empty body or `{}`. Validates required + scoring; materializes score.
|
||
|
||
**Responses:** 200 with assessment + score; **422** validation/scoring/not draft/already completed; **403** `assertCaptureAllowed`.
|
||
|
||
#### `POST /assessments/{assessment}/cancel`
|
||
|
||
Draft only → `cancelled`. **422** if completed.
|
||
|
||
#### `GET /consultations/{consultation}/pathway-suggestions`
|
||
|
||
```json
|
||
{
|
||
"data": [
|
||
{
|
||
"pathway_code": "stroke",
|
||
"pathway_name": "Stroke",
|
||
"rank": 100,
|
||
"match_reason": "icd_prefix:I63",
|
||
"already_active": false
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### `POST /patients/{patient}/pathways`
|
||
|
||
```json
|
||
{
|
||
"pathway_code": "stroke",
|
||
"consultation_uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||
"activation_diagnosis_text": "I63.9 Ischaemic stroke"
|
||
}
|
||
```
|
||
|
||
`consultation_uuid` is optional; when present, resolve to consultation (owned + org/branch scoped) and store internal `activation_consultation_id` FK. Do **not** accept integer `consultation_id` from clients.
|
||
|
||
Creates patient pathway + required draft assessments for `is_required_on_activation` templates (resolve current template by code). Does **not** create or update `care_patient_conditions` (KD-20).
|
||
|
||
#### `listForPatient` filters
|
||
|
||
`template_code`, `status` (`draft|completed|cancelled`), `category`, `from`, `to` (assessed_at), `per_page` (default 20). Always branch-scoped per KD-13.
|
||
|
||
### Service interfaces
|
||
|
||
```php
|
||
// app/Services/Care/AssessmentService.php
|
||
class AssessmentService
|
||
{
|
||
public function start(
|
||
Patient $patient,
|
||
string $templateCode, // resolves is_current system template
|
||
string $ownerRef,
|
||
?Member $member,
|
||
array $context = [], // consultation?, visit?, patient_pathway? (models or internal ids after controller resolve), actor
|
||
): Assessment;
|
||
|
||
/** @param array<string, mixed> $answers keyed by question code */
|
||
public function saveAnswers(
|
||
Assessment $assessment,
|
||
string $ownerRef,
|
||
array $answers,
|
||
?Member $member = null,
|
||
?string $actorRef = null,
|
||
): Assessment;
|
||
|
||
public function complete(Assessment $assessment, string $ownerRef, ?Member $member = null, ?string $actorRef = null): Assessment;
|
||
|
||
public function cancel(Assessment $assessment, string $ownerRef, ?Member $member = null, ?string $actorRef = null): Assessment;
|
||
|
||
public function listForPatient(Patient $patient, string $ownerRef, array $filters = [], ?int $branchScope = null): LengthAwarePaginator;
|
||
}
|
||
|
||
// app/Services/Care/PathwayService.php
|
||
class PathwayService
|
||
{
|
||
/** @param iterable<Diagnosis|array{code?:string,description?:string}> $diagnoses */
|
||
public function suggest(iterable $diagnoses): Collection; // unique pathways + rank + reason
|
||
|
||
public function activate(
|
||
Patient $patient,
|
||
ClinicalPathway $pathway,
|
||
string $ownerRef,
|
||
array $context = [], // consultation? (model after UUID resolve), activation_diagnosis_text, actor
|
||
): PatientPathway;
|
||
|
||
public function deactivate(PatientPathway $patientPathway, string $ownerRef, ?string $actorRef = null): PatientPathway;
|
||
|
||
public function activeFor(Patient $patient): Collection;
|
||
}
|
||
|
||
// app/Services/Care/ScoringService.php
|
||
class ScoringService
|
||
{
|
||
public function materialize(Assessment $assessment): AssessmentScore;
|
||
/** @return array{total: ?float, max: ?float, subscores: array, severity_label: ?string} */
|
||
public function preview(Assessment $assessment): array;
|
||
}
|
||
|
||
// app/Services/Care/CareFeatures.php (rollout flags — NOT PlanService)
|
||
class CareFeatures
|
||
{
|
||
public function enabled(Organization $organization, string $flag): bool;
|
||
// keys under settings.rollout.*
|
||
}
|
||
```
|
||
|
||
### Permissions (`CarePermissions`)
|
||
|
||
| Ability | Roles (v1) | Purpose |
|
||
|---------|------------|---------|
|
||
| `assessments.view` | doctor, nurse, hospital_admin, super_admin | Read assessments/pathways |
|
||
| `assessments.capture` | doctor, nurse, hospital_admin, super_admin | Start/save/complete/cancel (subject to `assertCaptureAllowed`) |
|
||
| `assessments.manage` | doctor, hospital_admin, super_admin | Route-level capture access (same start/save/complete endpoints as capture) **plus** future admin ops; does **not** alone bypass template rules — service still runs `assertCaptureAllowed` |
|
||
| `pathways.manage` | doctor, hospital_admin, super_admin | Activate/deactivate pathways |
|
||
|
||
**Why admin bypass matters:** `OrganizationResolver::ensureOwnerMember` creates the facility owner as **`hospital_admin`**, not `doctor`. Small clinics often have a single clinician-owner on that role. Disease templates seed `capture_roles: ["doctor"]` for clinical scope — without an explicit admin exception, owners could activate stroke pathways but get **403** on NIHSS/mRS.
|
||
|
||
**Enforcement layers (both required):**
|
||
|
||
1. **Route/controller:** member must have `assessments.capture` **or** `assessments.manage` (or `assessments.view` for reads; `pathways.manage` for pathway mutations).
|
||
2. **Service — single rule for all start/save/complete/cancel:**
|
||
|
||
```
|
||
assertCaptureAllowed(Member $member, AssessmentTemplate $template):
|
||
if member is null → 403
|
||
if member.role in {hospital_admin, super_admin} → allow
|
||
roles = template.meta.capture_roles
|
||
?? default_capture_roles(template.category)
|
||
// universal/outcome/screening → ["doctor","nurse"]
|
||
// disease → ["doctor"]
|
||
if member.role ∈ roles → allow
|
||
else → 403
|
||
```
|
||
|
||
Notes:
|
||
|
||
- **`hospital_admin` / `super_admin` always pass** capture checks (owner/admin clinicians). They still need a route ability (`capture` or `manage`); both roles have `*` in `CarePermissions` today, so that is satisfied.
|
||
- **`assessments.manage` does not invent a second capture matrix** — it only unlocks the same endpoints as `capture` for roles that hold manage (doctor already holds both). Template scope is **only** decided by `assertCaptureAllowed` above.
|
||
- Disease seeds keep `capture_roles: ["doctor"]`; nurses remain **403** on NIHSS; doctors **201**; hospital_admin **201** via admin branch (not by expanding seed lists to every admin role).
|
||
|
||
**Feature tests required:**
|
||
|
||
| Actor role | Template | Expected |
|
||
|------------|----------|----------|
|
||
| nurse | `universal_intake` | 201 |
|
||
| nurse | `nihss` | 403 |
|
||
| doctor | `nihss` | 201 |
|
||
| hospital_admin | `nihss` | 201 |
|
||
| super_admin | `nihss` | 201 |
|
||
|
||
### Audit actions (add to `config/care.php`)
|
||
|
||
```
|
||
assessment.started
|
||
assessment.updated
|
||
assessment.completed
|
||
assessment.cancelled
|
||
pathway.activated
|
||
pathway.deactivated
|
||
```
|
||
|
||
Metadata: subject ids, `template_code`, `pathway_code` — **not** full answer payloads.
|
||
|
||
### JSON API (PR 9)
|
||
|
||
Mirror web under `routes/api.php` with `ScopesApiToAccount`. OpenAPI: `docs/openapi/care.yaml`. Tenant/branch tests parallel `CareLabTest` patterns.
|
||
|
||
### Consultation payload (unchanged)
|
||
|
||
`ConsultationController::validatedConsultationData` remains symptoms/notes/vitals/diagnoses/documents. Assessments are separate resources.
|
||
|
||
---
|
||
|
||
## Data Model Changes
|
||
|
||
### Entity relationship
|
||
|
||
```mermaid
|
||
erDiagram
|
||
care_patients ||--o{ care_assessments : has
|
||
care_consultations ||--o{ care_assessments : context
|
||
care_visits ||--o{ care_assessments : context
|
||
care_assessment_templates ||--o{ care_assessment_questions : defines
|
||
care_assessment_templates ||--o{ care_assessments : instances
|
||
care_assessments ||--o{ care_assessment_answers : has
|
||
care_assessment_questions ||--o{ care_assessment_answers : answers
|
||
care_assessments ||--o| care_assessment_scores : score
|
||
care_clinical_pathways ||--o{ care_pathway_templates : includes
|
||
care_patients ||--o{ care_patient_pathways : enrolled
|
||
care_clinical_pathways ||--o{ care_patient_pathways : activated
|
||
```
|
||
|
||
Note: **no FK** from `care_pathway_templates` to `care_assessment_templates` — linkage by `template_code` string (KD-16).
|
||
|
||
### New tables
|
||
|
||
#### Catalog tenancy (summary)
|
||
|
||
- Catalog: **no `owner_ref`**; platform-global; v1 no org clones.
|
||
- Runtime: always `owner_ref` + org (+ branch on assessments).
|
||
|
||
#### `care_assessment_templates`
|
||
|
||
| Column | Type | Notes |
|
||
|--------|------|-------|
|
||
| id | bigint PK | |
|
||
| uuid | uuid unique | public id |
|
||
| organization_id | FK nullable | **v1 always NULL** (system). Reserved for future forks. |
|
||
| code | string | e.g. `nihss`, `universal_intake` |
|
||
| name | string | |
|
||
| category | string | universal / disease / outcome / screening |
|
||
| description | text nullable | |
|
||
| version | unsigned int | default 1 |
|
||
| is_current | boolean | |
|
||
| scoring_strategy | string nullable | `sum_items`, `single_value`, `custom:HandlerName` |
|
||
| meta | json nullable | `capture_roles`, estimated_minutes, specialty, attribution |
|
||
| is_active | boolean | |
|
||
| timestamps | | |
|
||
| softDeletes | | |
|
||
|
||
**Uniqueness (v1):**
|
||
|
||
- Application invariant: for system rows (`organization_id IS NULL`), unique `(code, version)`.
|
||
- DB: unique index on `(code, version)` **valid in v1 because only system rows exist** and org clones are **out of scope**.
|
||
- **Future org forks (not v1):** use separate namespace — e.g. code prefix `org_{id}_nihss` **or** unique `(organization_id, code, version)` with sentinel `organization_id = 0` for system if MySQL NULL uniqueness is a problem. Documented so implementers do not invent conflicting clones mid-flight.
|
||
- Also enforce at most one row with `(code, is_current=true, organization_id null)` in service/seeder.
|
||
|
||
#### `care_assessment_questions`
|
||
|
||
| Column | Type | Notes |
|
||
|--------|------|-------|
|
||
| id | bigint PK | |
|
||
| template_id | FK cascade | |
|
||
| code | string | stable within template version |
|
||
| section | string nullable | |
|
||
| label | string | |
|
||
| help_text | text nullable | |
|
||
| answer_type | string | see answer types |
|
||
| options | json nullable | choices, min/max, unit |
|
||
| is_required | boolean | |
|
||
| sort_order | unsigned int | |
|
||
| score_key | string nullable | subscore bucket |
|
||
| validation | json nullable | min, max, regex |
|
||
| timestamps | | |
|
||
| unique | (template_id, code) | |
|
||
|
||
#### `care_assessments`
|
||
|
||
| Column | Type | Notes |
|
||
|--------|------|-------|
|
||
| id | bigint PK | |
|
||
| uuid | uuid unique | route key |
|
||
| owner_ref | string indexed | from patient |
|
||
| organization_id | FK | denormalized |
|
||
| branch_id | FK nullable | visit preferred else patient.branch_id |
|
||
| patient_id | FK cascade | |
|
||
| template_id | FK restrict | frozen version |
|
||
| consultation_id | FK nullOnDelete | |
|
||
| visit_id | FK nullOnDelete | |
|
||
| patient_pathway_id | FK nullOnDelete | |
|
||
| practitioner_id | FK nullable | |
|
||
| status | string | `draft`, `completed`, `cancelled` |
|
||
| assessed_at | timestamp nullable | clinical time (default now on complete) |
|
||
| completed_at | timestamp nullable | |
|
||
| completed_by | string nullable | |
|
||
| started_by | string nullable | |
|
||
| notes | text nullable | |
|
||
| timestamps | | |
|
||
| softDeletes | | |
|
||
| indexes | (patient_id, assessed_at), (consultation_id), (owner_ref, status), (template_id, status), (patient_id, template_id, status) | |
|
||
|
||
#### `care_assessment_answers`
|
||
|
||
| Column | Type | Notes |
|
||
|--------|------|-------|
|
||
| id | bigint PK | |
|
||
| owner_ref | string indexed | |
|
||
| assessment_id | FK cascade | |
|
||
| question_id | FK restrict | |
|
||
| value_text | text nullable | |
|
||
| value_number | decimal(12,4) nullable | |
|
||
| value_boolean | boolean nullable | |
|
||
| value_date | datetime nullable | |
|
||
| value_json | json nullable | |
|
||
| timestamps | | |
|
||
| unique | (assessment_id, question_id) | |
|
||
|
||
No soft deletes on answers.
|
||
|
||
#### `care_assessment_scores` (PR 4 migration)
|
||
|
||
| Column | Type | Notes |
|
||
|--------|------|-------|
|
||
| id | bigint PK | |
|
||
| owner_ref | string indexed | |
|
||
| assessment_id | FK cascade unique | |
|
||
| template_code | string indexed | |
|
||
| total_score | decimal(12,4) nullable | |
|
||
| max_score | decimal(12,4) nullable | |
|
||
| subscores | json nullable | |
|
||
| severity_label | string nullable | |
|
||
| computed_at | timestamp | |
|
||
| timestamps | | |
|
||
| index | (owner_ref, template_code, computed_at) | |
|
||
|
||
#### `care_clinical_pathways`
|
||
|
||
| Column | Type | Notes |
|
||
|--------|------|-------|
|
||
| id | bigint PK | |
|
||
| uuid | uuid unique | |
|
||
| code | string unique | `stroke`, `diabetes`, … |
|
||
| name | string | |
|
||
| description | text nullable | |
|
||
| match_rules | json | icd_prefixes, keywords, exclude_keywords |
|
||
| is_active | boolean | |
|
||
| sort_order | unsigned int | |
|
||
| meta | json nullable | icon, color |
|
||
| timestamps | | |
|
||
| softDeletes | | |
|
||
|
||
No `owner_ref` (catalog).
|
||
|
||
#### `care_pathway_templates`
|
||
|
||
| Column | Type | Notes |
|
||
|--------|------|-------|
|
||
| id | bigint PK | |
|
||
| pathway_id | FK cascade | |
|
||
| template_code | string | **only** — resolve `is_current` at start (KD-16) |
|
||
| is_required_on_activation | boolean | create draft when pathway activated |
|
||
| phase | string | `acute`, `follow_up`, `any` |
|
||
| sort_order | unsigned int | |
|
||
| unique | (pathway_id, template_code) | |
|
||
|
||
**No `template_id` column.**
|
||
|
||
#### `care_patient_pathways`
|
||
|
||
| Column | Type | Notes |
|
||
|--------|------|-------|
|
||
| id | bigint PK | |
|
||
| uuid | uuid unique | |
|
||
| owner_ref | string indexed | |
|
||
| organization_id | FK | |
|
||
| patient_id | FK cascade | |
|
||
| pathway_id | FK restrict | |
|
||
| status | string | `active`, `resolved`, `inactive` |
|
||
| activated_at | timestamp | |
|
||
| activated_by | string nullable | |
|
||
| activation_consultation_id | FK nullable | |
|
||
| activation_diagnosis_text | string nullable | snapshot — **not** diagnosis FK |
|
||
| resolved_at | timestamp nullable | |
|
||
| notes | text nullable | |
|
||
| timestamps | | |
|
||
| softDeletes | | |
|
||
| index | (patient_id, status), (patient_id, pathway_id, status) | |
|
||
|
||
### What stays typed (no change)
|
||
|
||
- `care_patients`, emergency contacts, insurance, allergies, family history, conditions
|
||
- `care_vital_signs`
|
||
- `care_diagnoses`
|
||
- investigation catalog/results (string `value` on result values unchanged)
|
||
- prescriptions, bills
|
||
|
||
### Migration strategy
|
||
|
||
1. PR 1: templates, questions, assessments, answers (no scores).
|
||
2. PR 4: `care_assessment_scores` + scoring service.
|
||
3. PR 5: pathway tables.
|
||
4. Seed via `database/seeders/*` + JSON packs; wire into `DatabaseSeeder` / deploy runbook.
|
||
5. No backfill of historical consultations.
|
||
6. Rollback: feature flag off; drop tables only if zero production runtime rows.
|
||
|
||
### Storage / load estimates
|
||
|
||
| Assumption | Value |
|
||
|------------|-------|
|
||
| Active patients per org | 5,000–50,000 |
|
||
| Assessments per patient-year | 4–20 |
|
||
| Questions per instrument | 5–20 (NIHSS ~15) |
|
||
| Answer rows per assessment | ≈ question count |
|
||
| Growth | ~1M answer rows / large org / year |
|
||
|
||
---
|
||
|
||
## Seed/JSON content pack schema
|
||
|
||
**Path convention:** `database/data/assessments/{code}.v{version}.json`
|
||
|
||
**Seeder:** `AssessmentTemplateSeeder` upserts by `(code, version)`; sets `is_current` per pack; loads questions replacing by template_id. `ClinicalPathwaySeeder` loads pathways + `pathway_templates` bindings.
|
||
|
||
**Root object:**
|
||
|
||
```json
|
||
{
|
||
"template": {
|
||
"code": "mrs",
|
||
"name": "Modified Rankin Scale",
|
||
"category": "disease",
|
||
"version": 1,
|
||
"is_current": true,
|
||
"is_active": true,
|
||
"scoring_strategy": "single_value",
|
||
"description": "Global disability scale after stroke (0–6).",
|
||
"meta": {
|
||
"capture_roles": ["doctor"],
|
||
"specialty": "neurology",
|
||
"estimated_minutes": 2,
|
||
"attribution": "Modified Rankin Scale — use under applicable clinical/educational license; verify commercial redistribution rights before packaging."
|
||
}
|
||
},
|
||
"questions": [
|
||
{
|
||
"code": "mrs_score",
|
||
"section": "scale",
|
||
"label": "Modified Rankin Scale score",
|
||
"help_text": "Select the grade that best describes the patient.",
|
||
"answer_type": "score_item",
|
||
"is_required": true,
|
||
"sort_order": 1,
|
||
"score_key": "total",
|
||
"options": {
|
||
"min": 0,
|
||
"max": 6,
|
||
"choices": [
|
||
{"code": "0", "label": "No symptoms at all", "score": 0},
|
||
{"code": "1", "label": "No significant disability despite symptoms", "score": 1},
|
||
{"code": "2", "label": "Slight disability", "score": 2},
|
||
{"code": "3", "label": "Moderate disability", "score": 3},
|
||
{"code": "4", "label": "Moderately severe disability", "score": 4},
|
||
{"code": "5", "label": "Severe disability", "score": 5},
|
||
{"code": "6", "label": "Dead", "score": 6}
|
||
]
|
||
},
|
||
"validation": {"min": 0, "max": 6}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Pathway pack** (`database/data/pathways/{code}.json`):
|
||
|
||
```json
|
||
{
|
||
"pathway": {
|
||
"code": "stroke",
|
||
"name": "Stroke",
|
||
"match_rules": {
|
||
"icd_prefixes": ["I60", "I61", "I62", "I63", "I64", "G45"],
|
||
"keywords": ["stroke", "cva", "tia", "transient ischaemic attack", "transient ischemic attack", "cerebrovascular", "hemiplegia", "ischaemic stroke", "ischemic stroke"],
|
||
"exclude_keywords": ["family history"]
|
||
},
|
||
"is_active": true,
|
||
"sort_order": 10
|
||
},
|
||
"templates": [
|
||
{"template_code": "nihss", "is_required_on_activation": true, "phase": "acute", "sort_order": 1},
|
||
{"template_code": "mrs", "is_required_on_activation": true, "phase": "acute", "sort_order": 2}
|
||
]
|
||
}
|
||
```
|
||
|
||
TIA diagnoses match this same `stroke` pathway (KD-19); subtype is recorded on assessment, not via a second pathway code.
|
||
|
||
**Content-pack PR requirements:**
|
||
|
||
1. JSON conforming to schema above.
|
||
2. Golden unit tests for scoring (e.g. mRS 3 → total 3; NIHSS fixture answers → known total).
|
||
3. Attribution/licensing note in `meta.attribution` (Open Question: commercial use of instrument IP — legal review before GA of each pack).
|
||
4. Seeder registration in `DatabaseSeeder` or documented `php artisan db:seed --class=...` in deploy runbook.
|
||
|
||
---
|
||
|
||
## Universal template content (Layer 1 structured)
|
||
|
||
Seed template `code = universal_intake` (illustrative sections):
|
||
|
||
**Presenting complaint:** `chief_complaint` (textarea, required), `hpi`, `duration_value` + `duration_unit`, `pain_score` (scale 0–10), `current_symptoms`.
|
||
|
||
**Social history:** `smoking_status`, `smoking_pack_years`, `alcohol_use`, `occupation`.
|
||
|
||
**Functional status:** `mobility`, `communication`, `vision`, `hearing`, `feeding`, `continence`.
|
||
|
||
**Baseline labs:** optional `baseline_labs_summary` only; prefer investigation results.
|
||
|
||
Demographics/allergies/vitals remain on existing typed UI.
|
||
|
||
**Dual narrative (KD-15):** consultation `symptoms` / `clinical_notes` remain the narrative source of truth for free-text clinical story. Universal intake does not replace or auto-sync them. Chart displays both when present.
|
||
|
||
---
|
||
|
||
## Disease module content packs (Layer 3)
|
||
|
||
### Stroke (`pathway.code = stroke`)
|
||
|
||
**MVP (PR 6)** — production vertical slice:
|
||
|
||
| Template code | Instruments | Required on activation |
|
||
|---------------|-------------|------------------------|
|
||
| `nihss` | NIH Stroke Scale items + total (`sum_items`) | yes |
|
||
| `mrs` | Modified Rankin Scale (`single_value`) | yes |
|
||
|
||
**Follow-on (PR 6b)** — same pathway bindings, not blocking MVP:
|
||
|
||
| Template code | Instruments |
|
||
|---------------|-------------|
|
||
| `barthel` | Barthel Index |
|
||
| `gcs` | Glasgow Coma Scale (E/V/M as score_items + sum or custom) |
|
||
| `stroke_swallow` | Swallow assessment |
|
||
| `stroke_clinical` | **Stroke subtype** (TIA / ischaemic / haemorrhagic / unspecified), TLKW, CT findings, thrombolysis, thrombectomy, limb strength, aphasia, dysphagia, spasticity, cognition, etc. |
|
||
|
||
**Subtype (KD-19):** One pathway `stroke` for stroke **and** TIA. Subtype is a `single_choice` (or equivalent) field on `stroke_clinical` (and may be surfaced early on a lightweight field in MVP if needed). Do **not** create a separate TIA pathway.
|
||
|
||
Match rules: ICD I60–I64 (and TIA codes such as G45.* as configured in seed); keywords include stroke, CVA, TIA, cerebrovascular, hemiplegia, ischaemic/ischemic stroke; `exclude_keywords` e.g. family history.
|
||
|
||
### Diabetes (`pathway.code = diabetes`) — after stroke E2E
|
||
|
||
Template `diabetes_core` (HbA1c entry optional, foot assessment, monofilament, eye exam, microalbuminuria, neuropathy score, hypoglycaemia episodes, diet adherence, insulin regimen). Optional read-only display of latest lab HbA1c (not hard dependency).
|
||
|
||
### Deferred packs
|
||
|
||
Heart Failure, CKD, Hypertension, Asthma, COPD, Dementia, Parkinson's, Cancer, Pregnancy, Orthopaedics — one PR per pack.
|
||
|
||
---
|
||
|
||
## Layer 4 — Generic outcome measures
|
||
|
||
Template `outcome_core`: quality_of_life, pain_score, functional_independence, medication_adherence, hospital_admissions_since_last, falls_count, readmissions_flag, patient_satisfaction. Prefer display of typed vitals for weight/BP rather than re-entry.
|
||
|
||
Cadence: manual at follow-up; no scheduler in v1.
|
||
|
||
---
|
||
|
||
## Alternatives Considered
|
||
|
||
### Alternative A — Wide disease columns on patients/consultations
|
||
|
||
**Rejected** — schema churn, sparse nulls, comorbidity nightmare.
|
||
|
||
### Alternative B — Pure JSON document per consultation
|
||
|
||
**Rejected** as primary store — weak validation/indexing/audit; JSON only for options, match rules, subscores.
|
||
|
||
### Alternative C — Full FHIR Questionnaire
|
||
|
||
**Deferred** — heavy for current monolith; export later if needed.
|
||
|
||
### Alternative D — Org-scoped template builder UI first
|
||
|
||
**Deferred** — seed system instruments first; org customization later with explicit uniqueness strategy (see templates uniqueness).
|
||
|
||
### Alternative E — Consultation `structured_findings` JSON + typed scores table only (no pathway engine)
|
||
|
||
Lighter intermediate: append parameter-style rows (similar spirit to investigation result values) and a scores table, without templates/pathways.
|
||
|
||
| Pros | Cons |
|
||
|------|------|
|
||
| Faster first instrument | No versioned instruments, no comorbidity pathway activation, no reusable content packs, still invents ad hoc keys |
|
||
|
||
**Rejected** — fails product goals for multi-specialty pathway overlays; full template engine is the target and not much more work once schema exists.
|
||
|
||
### Chosen approach — Hybrid typed + metadata-driven assessments
|
||
|
||
Balances hot-path query performance, instrument flexibility, and Care service/UI patterns.
|
||
|
||
---
|
||
|
||
## Security & Privacy Considerations
|
||
|
||
| Threat | Severity | Mitigation |
|
||
|--------|----------|------------|
|
||
| Cross-tenant assessment read/write | **Critical** | `owner_ref` on runtime; `authorizeOwner` + org check via patient/visit |
|
||
| Branch isolation bypass | **High** | KD-13: same `branchScope` as consultations; 404 cross-branch for branch-scoped members |
|
||
| Unauthorized specialty documentation | **High** | Route ability + `assertCaptureAllowed` (admin bypass; else `capture_roles`; nurses blocked on disease) |
|
||
| Catalog write abuse | **Medium** | No product write API for catalog in v1; seed/deploy only |
|
||
| PHI in audit logs | **Medium** | Action + subject ids + codes only; no full answers in metadata |
|
||
| Immutable clinical record tampering | **High** | KD-14: complete lock 422; soft-delete scoped queries |
|
||
| Template XSS | **Medium** | Seed labels; Blade escape |
|
||
| Diagnosis false pathway activation | **Medium** | Suggest-only; clinician confirms (KD-5) |
|
||
| Soft-deleted assessment answer leakage | **Low** | Always query answers through non-deleted assessment |
|
||
|
||
No new public unauthenticated endpoints.
|
||
|
||
---
|
||
|
||
## Observability
|
||
|
||
### v1 (required) — logs + audit only
|
||
|
||
Ladill Care has **no** StatsD/Prometheus metrics backend today. Do not block implementation on counters.
|
||
|
||
- **Audit:** start/save/complete/cancel/activate/deactivate via `AuditLogger` + `config/care.php` actions.
|
||
- **Structured logs:** `Log::warning` / `Log::error` on scoring failures and capture denials with fields: `template_code`, `assessment_uuid`, `owner_ref` (or org id), `reason`.
|
||
- **Optional reporting:** later extend `ReportService` with assessment completion counts from audit or scores table (PR 8 optional).
|
||
|
||
### Future (not v1)
|
||
|
||
| Metric | Purpose |
|
||
|--------|---------|
|
||
| `care.assessments.completed` | Adoption |
|
||
| `care.pathways.activated` | Pathway usage |
|
||
| `care.scoring.failures` | Content pack bugs |
|
||
|
||
Alerting: no dedicated channel in v1; scoring failures surface in application logs for on-call when log aggregation exists.
|
||
|
||
---
|
||
|
||
## Rollout Plan
|
||
|
||
### Feature flags (`CareFeatures`, not PlanService)
|
||
|
||
`PlanService::hasFeature` is for **plan entitlements** (lab, pharmacy, billing). Rollout flags are separate.
|
||
|
||
**Storage:** `care_organizations.settings` JSON:
|
||
|
||
```json
|
||
{
|
||
"plan": "pro",
|
||
"rollout": {
|
||
"assessments_engine": true,
|
||
"pathway_suggestions": true,
|
||
"assessment_required_on_complete": false
|
||
}
|
||
}
|
||
```
|
||
|
||
**Helper:** `App\Services\Care\CareFeatures::enabled(Organization $org, string $flag): bool` reading `settings.rollout.{flag}`, default **false** until explicitly enabled (internal tenants first).
|
||
|
||
**Ships in PR 2** so UI can gate. Do not nest under `settings.features` (collides conceptually with plan feature lists in `config/care.php`).
|
||
|
||
### Plan packaging (KD-18 — final)
|
||
|
||
| Capability | Plan gate |
|
||
|------------|-----------|
|
||
| Core assessment engine, universal intake, pathway activation, disease pack **capture** (Stroke, Diabetes, …) | **Free** (all plans) — not Enterprise-gated |
|
||
| Patient-level outcome history / simple per-patient score list (basic chart) | **Free** |
|
||
| **Advanced analytics** — org-level instrument trends, multi-patient assessment dashboards, advanced outcome analytics | **Enterprise** (`PlanService::hasFeature` / enterprise plan features; distinct from rollout flags) |
|
||
|
||
Rollout flags (`CareFeatures`) control gradual enablement; plan packaging controls Enterprise-only analytics surfaces. Disease content packs are **not** sold as Enterprise-only for capture.
|
||
|
||
### Stages
|
||
|
||
1. **Internal** — migrate + seed universal; `rollout.assessments_engine` for staging.
|
||
2. **Pilot** — pathways + stroke MVP (NIHSS/mRS).
|
||
3. **GA** — default rollout on for new orgs; content pack runbook.
|
||
4. **Outcomes / analytics** — basic free; advanced org trends Enterprise-gated.
|
||
|
||
### Rollback
|
||
|
||
- Disable rollout flag → hide UI; data retained.
|
||
- Reverse migration only if zero production assessments.
|
||
- Content pack errors: `is_active = false` on template/pathway.
|
||
|
||
### Performance safeguards
|
||
|
||
- Eager-load questions with template.
|
||
- Paginate assessment history.
|
||
- Use scores table for trends; no answer-table scans for lists.
|
||
|
||
---
|
||
|
||
## Risks
|
||
|
||
| Risk | Severity | Mitigation |
|
||
|------|----------|------------|
|
||
| Free-text diagnoses noisy matches | Medium | Ranked algorithm + exclude_keywords + explicit picker + match reasons; tests for false positives |
|
||
| Clinician form fatigue | Medium | Required templates only acute MVP (NIHSS/mRS); more instruments optional later |
|
||
| Scoring bugs | High | Golden tests per content pack; frozen template versions |
|
||
| Dual HPI narrative | Medium | KD-15 explicit dual display; no auto-copy |
|
||
| Instrument licensing | Medium | `meta.attribution`; legal review before GA per pack |
|
||
| Dual BP/weight sources | Low | Typed vitals preferred in UI |
|
||
| Race on pathway activate | Medium | Transaction + lockForUpdate; idempotent activate |
|
||
| Large stroke content PR | Medium | Split MVP (PR 6) vs 6b |
|
||
|
||
---
|
||
|
||
## Resolved decisions
|
||
|
||
Product decisions closed for implementation (do not re-litigate without a new design revision):
|
||
|
||
| Topic | Decision | Anchored in |
|
||
|-------|----------|-------------|
|
||
| **Plan gating** | Engine + universal intake + disease pack **capture** free on all plans. **Advanced analytics / org-level trends** → Enterprise. | KD-18, Rollout → Plan packaging |
|
||
| **TIA vs Stroke** | **One** `stroke` pathway. TIA / ischaemic / haemorrhagic are **subtype** fields on stroke assessments (e.g. `stroke_clinical`), not separate pathways. Match rules include TIA. | KD-19, Layer 3 Stroke |
|
||
| **Chronic conditions sync** | **No** auto-add. `care_patient_pathways` and `care_patient_conditions` remain independent lists. | KD-20 |
|
||
| **Multi-branch chart** | Match consultations / `branchScope`. | KD-13 |
|
||
| **Complete immutability (v1)** | Hard lock; no amend in v1. | KD-14 |
|
||
| **Dual HPI narrative** | Free-text symptoms primary; universal intake optional overlay; no auto-copy. | KD-15 |
|
||
| **Admin capture** | `hospital_admin` / `super_admin` always pass `assertCaptureAllowed`. | KD-9 |
|
||
|
||
---
|
||
|
||
## Open Questions
|
||
|
||
Still open (not blocking PR 1 engine work):
|
||
|
||
1. **Amend completed assessments (post-v1)** — versioned amendments with reason vs permanent lock after v1?
|
||
2. **Lab value pull-through** — how tightly to couple diabetes HbA1c display to `care_investigation_results` in the diabetes pack?
|
||
3. **Nurse NIHSS** — org-configurable `capture_roles` later? (v1: disease `["doctor"]`; hospital_admin/super_admin always allowed.)
|
||
4. **Internationalization** — English-only first packs?
|
||
5. **Clinical instrument licensing** — commercial redistribution rights for NIHSS/mRS/etc. before GA of each pack?
|
||
|
||
---
|
||
|
||
## References
|
||
|
||
### Internal code
|
||
|
||
| Path | Relevance |
|
||
|------|-----------|
|
||
| `database/migrations/2026_07_01_100000_create_care_patient_tables.php` | Patient, allergies, conditions, family, insurance |
|
||
| `database/migrations/2026_07_02_100000_create_care_clinical_tables.php` | Visits, consultations, vitals, diagnoses |
|
||
| `database/migrations/2026_07_03_100000_create_care_lab_and_prescription_tables.php` | Result values: **string** `value` only |
|
||
| `app/Models/Consultation.php` | Status draft/completed, relations |
|
||
| `app/Models/Diagnosis.php` | Pathway matching input |
|
||
| `app/Models/VitalSign.php` | Typed vitals retained |
|
||
| `app/Models/Concerns/BelongsToOwner.php` | Runtime tenant scope |
|
||
| `app/Services/Care/ConsultationService.php` | save/complete/syncDiagnoses |
|
||
| `app/Services/Care/PatientService.php` | Registration remains universal |
|
||
| `app/Services/Care/CarePermissions.php` | Role abilities (extend) |
|
||
| `app/Services/Care/PlanService.php` | Plan entitlements — **not** rollout flags |
|
||
| `app/Services/Care/AuditLogger.php` | Clinical audit |
|
||
| `app/Http/Controllers/Care/ConsultationController.php` | Web integration; vitals vs manage split pattern |
|
||
| `app/Http/Controllers/Care/Concerns/ScopesToAccount.php` | Authz helpers |
|
||
| `resources/views/care/consultations/show.blade.php` | Primary UI surface |
|
||
| `resources/views/care/patients/_form.blade.php` | Demographics (no specialty) |
|
||
| `config/care.php` | Enums, audit actions, plans |
|
||
| `routes/web.php` | Route registration |
|
||
| `tests/Feature/CarePatientTest.php`, `CareLabTest.php` | Test style to extend |
|
||
| `database/seeders/` | Currently empty of assessment packs — greenfield |
|
||
|
||
### External clinical instruments
|
||
|
||
- NIH Stroke Scale (NIHSS), Modified Rankin Scale (mRS), Barthel, GCS, NYHA, CAT, mMRC, Hoehn & Yahr, UPDRS, MMSE/MoCA — with licensing review before commercial packaging.
|
||
|
||
---
|
||
|
||
## PR Plan
|
||
|
||
Incremental, independently reviewable PRs. Merge order as listed.
|
||
|
||
### Milestones
|
||
|
||
| Milestone | After PRs | Demoable outcome |
|
||
|-----------|-----------|------------------|
|
||
| **M1 — Universal intake** | PR 1–3 | Register patient; complete structured universal intake on consultation; free-text symptoms still work |
|
||
| **M2 — Pathways + stroke E2E** | PR 4–6 | Save diagnoses → suggestions → activate stroke → complete NIHSS/mRS with scores |
|
||
| **M3 — Breadth** | PR 6b–8 | Extra stroke instruments, diabetes, outcomes trends |
|
||
| **M4 — API** | PR 9 | Mobile/API parity |
|
||
|
||
### PR 1 — Assessment engine schema + domain models
|
||
|
||
- **Title:** `feat(assessments): add template-driven assessment tables and models`
|
||
- **Depends on:** none
|
||
- **Files:** migration for templates, questions, assessments, answers (**no** scores table); catalog models without `BelongsToOwner`; runtime models with `BelongsToOwner`; minimal relation/uuid tests
|
||
- **Description:** Additive schema only. Establishes catalog tenancy contract and uniqueness `(code, version)` for system templates.
|
||
|
||
### PR 2 — AssessmentService + permissions + CareFeatures + web CRUD
|
||
|
||
- **Title:** `feat(assessments): service layer, permissions, rollout flags, and capture UI`
|
||
- **Depends on:** PR 1
|
||
- **Files:**
|
||
- `AssessmentService` (normalize invariants, capture_roles, draft idempotency, cancel, complete lock)
|
||
- `CareFeatures` + `settings.rollout.*`
|
||
- `CarePermissions`: `assessments.view`, `assessments.capture`, `assessments.manage`
|
||
- `AssessmentController`, routes, Blade forms
|
||
- `config/care.php` statuses + audit actions
|
||
- Feature tests: nurse universal yes / NIHSS no; doctor + hospital_admin NIHSS yes; tenant 404; public UUID body fields
|
||
- **Description:** Full capture lifecycle gated by rollout flag. No pathway tables yet.
|
||
|
||
### PR 3 — Universal intake seed + consultation entry
|
||
|
||
- **Title:** `feat(assessments): seed universal intake and link from consultation`
|
||
- **Depends on:** PR 2
|
||
- **Files:** `database/data/assessments/universal_intake.v1.json`, `AssessmentTemplateSeeder`, wire `DatabaseSeeder` / deploy note; consultation show section; patient timeline; dual narrative UI (symptoms + intake); tests
|
||
- **Description:** M1 complete. Layer 1 structured overlay without patient column churn.
|
||
|
||
### PR 4 — Scoring materialization
|
||
|
||
- **Title:** `feat(assessments): scoring service and care_assessment_scores`
|
||
- **Depends on:** PR 2
|
||
- **Files:** **only** migration for `care_assessment_scores` (not in PR 1); `AssessmentScore`; `ScoringService` + `sum_items` / `single_value`; unit tests; complete path integration
|
||
- **Description:** Materialize scores on complete. Required before specialty packs.
|
||
|
||
### PR 5 — Clinical pathway registry + patient activation
|
||
|
||
- **Title:** `feat(pathways): clinical pathway registry and patient activation`
|
||
- **Depends on:** PR 2 (PR 3 recommended for realistic consultation UX)
|
||
- **Files:** pathway migrations (`template_code` only on bindings); `PathwayService`, `PathwayMatcher` (full algorithm + tests: I63.9, ischaemic stroke, history unclear, exclude family history); `pathways.manage`; deactivate POST (not DELETE); suggestions endpoint on **persisted** diagnoses; consultation UI suggestions + “Save diagnoses & suggest”; activate creates required drafts; `lockForUpdate` uniqueness; `CarePathwayTest`
|
||
- **Description:** Layer 2. Explicit activation; comorbidity-safe.
|
||
|
||
### PR 6 — Stroke MVP content pack (NIHSS + mRS)
|
||
|
||
- **Title:** `feat(pathways): stroke MVP content pack (NIHSS, mRS)`
|
||
- **Depends on:** PR 4, PR 5 (PR 3 recommended)
|
||
- **Files:** `nihss.v1.json`, `mrs.v1.json`, stroke pathway JSON with required bindings; golden scoring tests; consultation instruments list when stroke active
|
||
- **Description:** **M2 vertical slice.** Do not include Barthel/GCS/swallow/clinical in this PR.
|
||
|
||
### PR 6b — Stroke extended instruments
|
||
|
||
- **Title:** `feat(pathways): stroke extended instruments (Barthel, GCS, swallow, clinical)`
|
||
- **Depends on:** PR 6
|
||
- **Files:** remaining stroke templates + pathway bindings (optional/required as product chooses); scoring tests
|
||
- **Description:** Completes full stroke module without blocking E2E milestone.
|
||
|
||
### PR 7 — Diabetes content pack
|
||
|
||
- **Title:** `feat(pathways): diabetes assessment content pack`
|
||
- **Depends on:** PR 6 (stroke E2E proven), PR 4, PR 5
|
||
- **Files:** `diabetes_core` seed + pathway; optional lab HbA1c display; comorbidity tests (stroke + diabetes)
|
||
- **Description:** Second module after stroke path is stable.
|
||
|
||
### PR 8 — Generic outcomes + patient trends (+ optional ReportService)
|
||
|
||
- **Title:** `feat(assessments): outcome measures template and patient trend view`
|
||
- **Depends on:** PR 4, PR 2
|
||
- **Files:** `outcome_core` seed; per-patient outcome history (free); optional org-level `ReportService` analytics gated Enterprise (KD-18)
|
||
- **Description:** Layer 4. Basic patient chart free; advanced multi-patient trends Enterprise.
|
||
|
||
### PR 9 — API parity + OpenAPI
|
||
|
||
- **Title:** `feat(api): assessment and pathway endpoints`
|
||
- **Depends on:** PR 5, PR 2
|
||
- **Files:** API controllers; `routes/api.php`; `docs/openapi/care.yaml`; feature tests paralleling `CareLabTest` tenant/branch isolation
|
||
- **Description:** Mobile/clients parity.
|
||
|
||
### PR 10 — Additional content packs (series)
|
||
|
||
- **Title:** `feat(pathways): {copd|heart_failure|dementia|…} content pack`
|
||
- **Depends on:** PR 5, PR 4
|
||
- **Description:** One PR per pack; schema-stable.
|
||
|
||
### Out of scope for this PR sequence
|
||
|
||
- Org template builder / org forks
|
||
- FHIR Questionnaire export
|
||
- Hard-block consultation complete on missing assessments
|
||
- Automated outcome reminders
|
||
- Full ICD-10 terminology service
|
||
- Metrics backend integration
|
||
- Org match_rules overrides
|
||
|
||
---
|
||
|
||
## Success criteria
|
||
|
||
1. New patient can be registered and consulted **without** selecting a specialty.
|
||
2. Clinician can complete a **universal intake** assessment linked to a consultation (M1).
|
||
3. After **saving** diagnoses, **pathway suggestions** appear with match reasons; activation supports **multiple** pathways (M2).
|
||
4. Stroke pathway exposes **NIHSS + mRS**; completion stores answers + **materialized score** (M2).
|
||
5. Adding a new instrument requires **seed JSON + seeder only**, no clinical column migration.
|
||
6. Assessment mutations appear in **audit log**; tenant + branch isolation enforced in tests.
|
||
7. Typed vitals/diagnoses/labs and free-text symptoms continue to work unchanged on `consultations/show`.
|
||
8. Nurse can capture universal intake; nurse **cannot** start NIHSS; doctor and **hospital_admin** can.
|
||
9. Completed assessment PUT returns **422**; cancel works on drafts only.
|
||
10. Web/API request bodies reference consultations/visits by **UUID**, resolved server-side to FKs.
|