# LIS Deep Analysis -- Final Research Before Backend Tasks

> Date: 2026-03-28
> Scope: Every edge case, data model gap, settings, notifications, and missing API specifications

---

## PART 1: Every "What If" Scenario

### 1. Two Requests for the Same Patient on the Same Day

**Current behavior:** Each request gets a unique `request_number` and unique `lab_request_id`. Samples generated from each request get unique barcodes. There is NO cross-request deduplication.

**Real-world handling:**
- Each request is independent. Same patient can have Request-001 (CBC, Glucose) at 8am and Request-002 (Lipid Panel) at 2pm.
- Each request generates its own set of samples with unique barcodes.
- Results are isolated by `lab_request_id` -- no accidental mixing.
- The Results page already filters by `lab_request_id`, so the technician sees one request at a time.

**Edge case -- same test in two requests same day:**
- This IS allowed. Patient might need CBC at 8am and CBC at 4pm (monitoring).
- Each produces a separate result row with separate `lab_request_investigation_id`.
- Delta check already handles this -- it compares with the previous result from ANY request for that patient+investigation.

**What is MISSING:**
- **Add-on test to existing request** (see item 2 below) -- currently no endpoint for this.
- **Request linking/grouping** -- no concept of a "visit" that groups multiple requests. The `visit_id` field exists on `LabRequest` but visits are standalone and not enforced.
- **Duplicate test warning** -- When creating a new request, no check warns "this patient already has a CBC ordered today." This should be a soft warning, not a block.

**Backend task needed:**
- Add a `GET /lis/patients/{id}/recent-investigations?hours=24` endpoint that returns recent investigations for duplicate-detection warnings in the wizard.

---

### 2. Test Added AFTER Samples Are Already Collected (Add-On Test)

**Current behavior:** Once a request is created and samples collected, there is NO way to add a new investigation to the existing request through the API. The only option is to create a new request entirely.

**Real-world handling:**
- Doctor calls after seeing initial results: "Please add TSH to this patient's request."
- If the required specimen type is ALREADY collected (e.g., serum) and the tube is still in the lab, the add-on should use the existing sample.
- If a DIFFERENT specimen type is needed (e.g., urine when only blood was collected), a new sample must be collected.

**What needs to happen:**
1. `POST /lis/requests/{id}/add-investigation` endpoint
2. Business logic:
   - Check if request is in a state that allows add-ons (pending, sample_collected, in_progress, partial_result -- NOT completed, NOT cancelled)
   - Add the investigation to `lab_request_investigations`
   - Check if a sample of the required specimen type already exists for this request
     - YES and sample is still usable (not rejected): Link investigation to existing sample, create result row, send to kanban
     - NO: Create a new sample in `pending` status requiring collection
   - Recalculate pricing: update `total_amount`, `net_amount`, insurance splits
   - If invoice exists, create an adjustment invoice item or a supplementary invoice
3. Audit log the add-on with reason

**Backend task needed:**
- New endpoint `POST /lis/requests/{id}/add-investigation`
- New endpoint `POST /lis/requests/{id}/remove-investigation` (only if status is pending/not yet collected)
- Invoice adjustment logic when adding tests post-invoicing

---

### 3. Analyzer Result Differs from Manual Entry

**Current behavior:** The `LabMachineResult` model stores raw machine results separately. The `MachineResultMatchingService` matches machine results to `LabResult` rows. Machine results have status: pending, matched, approved, rejected.

**Real-world handling:**
- Machine sends result via interface -> stored in `lab_machine_results` as `pending`
- Matching service tries to link it to a `LabResult` row by barcode + test mapping
- If matched, the machine result status becomes `matched` but the `LabResult.result_value` is NOT automatically overwritten
- Technician reviews: can approve (copies machine value to result) or reject (enters manual value)

**What is MISSING:**
- **Conflict detection:** When a `LabResult` already has a manually entered value and a machine result arrives (or vice versa), there is no explicit conflict resolution UI or logic.
- **Priority rule setting:** Lab should be able to configure: "Machine results always override manual" OR "Manual always takes priority" OR "Flag for review"
- **Machine result history on the result:** The `LabResult` model does not track which machine produced the value or the original raw value if it was transformed.

**Backend task needed:**
- Add `machine_result_id` FK to `lab_results` table (nullable, references the accepted machine result)
- Add `result_source` enum field to `lab_results`: `manual`, `machine`, `formula`, `external` (default: `manual`)
- Add setting `lis.machine_result_priority` with values: `machine_first`, `manual_first`, `flag_for_review`
- When conflict detected, if `flag_for_review`, set result status to a special `pending_review` or add a flag

---

### 4. Power Outage During Result Entry (Autosave)

**Current behavior:** Results are saved ONLY when the technician explicitly clicks Submit/Enter (POST `/lis/results/{id}/enter`). If the browser crashes or power goes out mid-entry, all unsaved work is lost.

**Real-world handling:**
- Modern LIS systems have autosave/draft functionality.
- Results should be saved as drafts periodically without triggering status transitions.
- The "Save" action preserves work; the "Submit" action triggers workflow.

**What is MISSING:**
- **Draft save endpoint:** A way to save a partial result value without changing status from `pending` to `entered`.
- **Bulk draft save:** Save multiple results at once (worksheet-style entry).
- **Client-side autosave:** Frontend should periodically POST draft values.

**Backend task needed:**
- New endpoint `POST /lis/results/bulk-draft` -- saves result values without status transition
  - Request: `{ results: [ { id: 1, result_value: "14.5", comment: "..." }, ... ] }`
  - Only works for results in `pending` status
  - Stores value but keeps status as `pending`
  - Sets `entered_by` and `entered_at` as draft markers
  - Audit log entry with action `draft_save`
- New endpoint `POST /lis/results/bulk-enter` -- batch submit with status transition
  - Request: `{ result_ids: [1, 2, 3] }` (values already saved as draft)
  - Transitions all from `pending` to `entered`
  - Triggers auto-verification, formula calculations, critical alerts for each

---

### 5. Same Test Ordered Twice in the Same Request

**Current behavior:** The `lab_request_investigations` table has NO unique constraint on `(lab_request_id, investigation_id)`. The backend does not prevent duplicates.

**Real-world handling:**
- Duplicate tests in the same request are almost always an error.
- Exception: Some tests are intentionally ordered as "serial" (e.g., serial glucose at 0min, 30min, 60min for glucose tolerance test). These are typically handled as a PANEL, not duplicate individual tests.
- The system should BLOCK exact duplicate test orders in the same request.

**Backend task needed:**
- Add validation in `LabRequestService` when creating/updating requests: reject if same `investigation_id` appears twice in the `investigations[]` array.
- Add a unique index: `UNIQUE(lab_request_id, investigation_id)` on `lab_request_investigations` table (with deleted_at for soft-delete safety if applicable).
- For glucose tolerance or serial tests, these should be modeled as panels.

---

### 6. Panel Member Re-test Without Whole Panel

**Current behavior:** Re-test from kanban creates a new sample and sends ALL investigations back to collection. There is no way to re-test a single member of a panel.

**Real-world handling:**
- Hemoglobin flagged on CBC -- only hemoglobin needs re-run, not WBC, platelets, etc.
- The re-test should create a new result row ONLY for the specific member, not the entire panel.
- The original panel result should link to the re-tested member's new result for comparison.

**What is MISSING:**
- Granular re-test at the investigation level (not sample level).
- The kanban re-test action currently operates at the sample level.

**Backend task needed:**
- New endpoint `POST /lis/results/{id}/retest` -- creates a re-test for a single result
  - Creates a new `LabResult` row for the same investigation, linked to the same request
  - Sets `previous_result` to the old value
  - Sets a `retest_of` FK pointing to the original result ID
  - Original result is marked with `retested = true` flag
  - The new result enters the workflow at `pending` (may need new sample or use existing)
- Add to `lab_results` table:
  - `retest_of` int nullable FK -> lab_results.id
  - `is_retested` boolean default false
  - `retest_reason` text nullable

---

### 7. Insurance Applied After Results Are Already Entered

**Current behavior:** Insurance contract is selected during request creation (Step 3 of wizard). If not selected then, there is no mechanism to apply it later.

**Real-world handling:**
- Patient arrives without insurance card, pays cash. Next day brings card.
- Lab needs to: apply insurance retroactively, recalculate patient/insurance shares, adjust the invoice, potentially refund the patient's overpayment.

**What is MISSING:**
- Endpoint to retroactively apply/change insurance on an existing request.
- Invoice adjustment/credit note generation.
- Payment reconciliation (refund patient, bill insurance).

**Backend task needed:**
- New endpoint `PUT /lis/requests/{id}/insurance` to apply/update insurance contract retroactively
  - Request: `{ insurance_contract_id, insurance_approval_number, coverage_percentage }`
  - Recalculates per-investigation pricing from contract
  - Updates `lab_request_investigations` rows with insurance prices
  - Updates request totals
  - If invoice exists and is posted: creates a credit note + new adjusted invoice
  - If invoice exists but draft: updates in place
  - If payment exists: calculates refund amount, creates refund payment record

---

### 8. Patient Demographics Change (Gender/Age Correction) -- Reference Range Recalculation

**Current behavior:** Reference ranges are looked up at the time of result entry via `findApplicableRange()` which queries by patient age and gender. If demographics change AFTER results are entered, the stored `normal_min`/`normal_max` on the result are NOT recalculated.

**Real-world handling:**
- Patient gender was entered wrong (male instead of female).
- Patient age was wrong (birthdate typo).
- Reference ranges are gender/age-specific. Wrong demographics = wrong reference ranges = wrong flags.

**What is MISSING:**
- A trigger/hook when patient demographics change to recalculate reference ranges for recent results.
- An endpoint to force recalculation.

**Backend task needed:**
- Add a listener on patient update events that checks for recent results (last 30 days, not yet released) and recalculates:
  - `normal_min`, `normal_max`, `critical_low`, `critical_high` from the updated age/gender
  - `abnormal_flag` recalculated
  - Audit log entry recording the demographic change and recalculation
- New endpoint `POST /lis/results/recalculate-ranges` with body `{ lab_request_id }` for manual trigger

---

### 9. Released Result for WRONG PATIENT (Specimen Swap) -- CRITICAL SAFETY

**Current behavior:** There is NO retract/invalidate mechanism. Once a result is `released`, the only action is `returnResult` which steps it back one status (to `approved`). There is no concept of marking an entire request as "entered-in-error" or a result as invalid.

**Real-world handling (from HL7 FHIR standards and SENAITE):**
- **Retract:** Pull back a released result. Original result preserved with new status `retracted`. Notification sent to anyone who received the result (doctor, patient portal).
- **Invalidate:** Mark an entire request/sample as invalid (wrong patient, contaminated sample). All results under it become invalid.
- **Corrected report:** Issue a new report marked as "Corrected" with reference to the original. Original marked as superseded.
- **Amended report:** Non-result changes (demographics, doctor name).

**What is MISSING (CRITICAL):**
- `retracted` status on `ResultStatus` enum
- `invalidated` status on `ResultStatus` enum
- `entered_in_error` status on `ResultStatus` enum
- Retract endpoint with mandatory reason, notification cascade, and audit
- Invalidate endpoint at the request level
- Corrected report generation

**Backend task needed -- HIGH PRIORITY:**

**A) Result Retraction:**
- New endpoint `POST /lis/results/{id}/retract`
  - Permission: `lis.results.retract` (restricted to lab manager / pathologist)
  - Request: `{ reason: string, notify: boolean }`
  - Sets status to `retracted`, preserves all previous data
  - Creates audit log entry
  - Fires `LabResultRetracted` event
  - If `notify=true`, sends notifications to: ordering doctor, patient (if portal result published)
  - Does NOT delete data -- result row stays with `retracted` status

**B) Request Invalidation:**
- New endpoint `POST /lis/requests/{id}/invalidate`
  - Permission: `lis.requests.invalidate`
  - Request: `{ reason: string, notify: boolean }`
  - Sets all results under the request to `invalidated`
  - Sets request status to `cancelled` with `cancellation_reason`
  - Reverses any published reports (marks publish logs as retracted)
  - If invoice posted: creates credit note
  - Fires `LabRequestInvalidated` event

**C) Corrected Result:**
- New endpoint `POST /lis/results/{id}/correct`
  - Permission: `lis.results.correct`
  - Request: `{ corrected_value: string, reason: string }`
  - Creates a new result row as the "corrected" version
  - Links to original via `corrects_result_id` FK
  - Original result gets `is_corrected = true` flag
  - New result enters at `entered` status (needs re-verification)
  - Audit log + notification

---

### 10. Lab Runs Out of Reagent Mid-Batch

**Current behavior:** Reagent consumption is tracked via `ConsumeReagentOnResultEntry` listener. It deducts from `LabReagent.current_stock` when a result is entered. There is NO pre-check before processing and NO batch-level tracking.

**Real-world handling:**
- Before starting a batch run, lab should check if enough reagent is available for the batch size.
- If reagent runs out mid-run, affected samples need to be marked as "incomplete" and queued for re-run.
- Low stock alerts should fire BEFORE stockout.

**What is MISSING:**
- Pre-batch reagent sufficiency check
- Mid-batch failure handling (mark specific results as `pending` again)
- Real-time stock alert when stock falls below minimum

**Backend task needed:**
- Add `POST /lis/reagents/{id}/check-sufficiency` endpoint: `{ test_count: int }` -> returns `{ sufficient: bool, available_tests: int }`
- Add reagent stock alert event: when `current_stock` falls below `minimum_stock` after consumption, fire `ReagentLowStock` event
- Enhance the existing `minimum_stock` field to trigger notifications

---

## PART 2: Data Model Requirements -- Exact Schema Changes

### 2.1 Result Retraction / Correction / Invalidation

**New columns on `lab_results` table:**

```sql
ALTER TABLE lab_results ADD COLUMN retracted_by INT UNSIGNED NULL AFTER released_at;
ALTER TABLE lab_results ADD COLUMN retracted_at TIMESTAMP NULL AFTER retracted_by;
ALTER TABLE lab_results ADD COLUMN retraction_reason TEXT NULL AFTER retracted_at;
ALTER TABLE lab_results ADD COLUMN corrects_result_id INT UNSIGNED NULL AFTER retraction_reason;
ALTER TABLE lab_results ADD COLUMN is_corrected BOOLEAN DEFAULT FALSE AFTER corrects_result_id;
ALTER TABLE lab_results ADD COLUMN retest_of INT UNSIGNED NULL AFTER is_corrected;
ALTER TABLE lab_results ADD COLUMN is_retested BOOLEAN DEFAULT FALSE AFTER retest_of;
ALTER TABLE lab_results ADD COLUMN retest_reason TEXT NULL AFTER is_retested;
ALTER TABLE lab_results ADD COLUMN result_source ENUM('manual','machine','formula','external') DEFAULT 'manual' AFTER retest_reason;
ALTER TABLE lab_results ADD COLUMN machine_result_id INT UNSIGNED NULL AFTER result_source;

-- Indexes
ALTER TABLE lab_results ADD INDEX idx_corrects_result (corrects_result_id);
ALTER TABLE lab_results ADD INDEX idx_retest_of (retest_of);
ALTER TABLE lab_results ADD INDEX idx_machine_result (machine_result_id);

-- Foreign keys
ALTER TABLE lab_results ADD CONSTRAINT fk_results_corrects FOREIGN KEY (corrects_result_id) REFERENCES lab_results(id) ON DELETE SET NULL;
ALTER TABLE lab_results ADD CONSTRAINT fk_results_retest FOREIGN KEY (retest_of) REFERENCES lab_results(id) ON DELETE SET NULL;
ALTER TABLE lab_results ADD CONSTRAINT fk_results_retracted_by FOREIGN KEY (retracted_by) REFERENCES users(id) ON DELETE SET NULL;
```

**New `ResultStatus` enum values:**

```php
case Retracted = 'retracted';
case Invalidated = 'invalidated';
case EnteredInError = 'entered_in_error';
```

### 2.2 Re-test Linking

The `retest_of` column above handles this. When a re-test is created:
- New `LabResult` row: `retest_of = original_result_id`
- Original row: `is_retested = true`, `retest_reason` set
- The query for the results page should include `retest_of` to show comparison

### 2.3 Reject Result (as distinct from sample rejection)

**Current state:** Rejection happens at the **investigation status** level (`InvestigationStatus::Rejected`) or at the **sample** level. There is no result-level rejection.

**What is needed:** A result-level rejection for when a result value is clearly wrong (instrument malfunction, QC failure).

This is handled by the existing `returnResult` + `reset` flow:
- `returnResult` steps back one status
- `reset` clears value and goes back to `pending`

No new table needed. The existing flow covers this. BUT we should add:

```sql
ALTER TABLE lab_results ADD COLUMN rejection_reason TEXT NULL AFTER retest_reason;
ALTER TABLE lab_results ADD COLUMN rejected_by INT UNSIGNED NULL AFTER rejection_reason;
ALTER TABLE lab_results ADD COLUMN rejected_at TIMESTAMP NULL AFTER rejected_by;
```

### 2.4 Invalidation Impact on Invoice/Payment

When a request is invalidated:
1. If `lab_invoices` row exists with status `posted`:
   - Create a new `lab_invoices` row with `invoice_type = 'credit_note'`
   - `total` = negative of original
   - `journal_entry_id` for the reversal posting
2. If `lab_payments` exist:
   - Create refund payment rows (`amount` negative, or new `is_refund` boolean)
   - Link to original payment

**New columns on `lab_invoices`:**

```sql
ALTER TABLE lab_invoices ADD COLUMN credit_note_of INT UNSIGNED NULL;
ALTER TABLE lab_invoices ADD COLUMN invalidation_reason TEXT NULL;
ALTER TABLE lab_invoices ADD CONSTRAINT fk_invoice_credit_of FOREIGN KEY (credit_note_of) REFERENCES lab_invoices(id) ON DELETE SET NULL;
```

### 2.5 Critical Alerts -- Notification Infrastructure

**New table `lab_critical_alerts`:**

```sql
CREATE TABLE lab_critical_alerts (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    lab_result_id INT UNSIGNED NOT NULL,
    lab_request_id INT UNSIGNED NOT NULL,
    patient_id INT UNSIGNED NULL,
    investigation_id INT UNSIGNED NOT NULL,
    abnormal_flag VARCHAR(20) NOT NULL, -- critical_low, critical_high
    result_value VARCHAR(100) NOT NULL,
    reference_range VARCHAR(100) NULL, -- "normal: 4.0-11.0, critical: <2.0 or >30.0"
    acknowledged_by INT UNSIGNED NULL,
    acknowledged_at TIMESTAMP NULL,
    acknowledgment_note TEXT NULL,
    notified_users JSON NULL, -- [{ user_id, channel, sent_at, delivered }]
    escalated BOOLEAN DEFAULT FALSE,
    escalated_at TIMESTAMP NULL,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,

    INDEX idx_company_unack (company_id, acknowledged_at),
    INDEX idx_result (lab_result_id),
    FOREIGN KEY (lab_result_id) REFERENCES lab_results(id),
    FOREIGN KEY (lab_request_id) REFERENCES lab_requests(id),
    FOREIGN KEY (investigation_id) REFERENCES lab_investigations(id),
    FOREIGN KEY (acknowledged_by) REFERENCES users(id)
);
```

### 2.6 Audit Trail Assessment

**Current `lab_result_audit_logs` table fields:**
- `lab_result_id`, `action`, `previous_value`, `new_value`, `previous_status`, `new_status`, `changed_by`, `changed_at`, `ip_address`, `user_agent`, `metadata` (JSON)

**Assessment:** This is quite good. The `metadata` JSON field can store additional context. What is MISSING:

```sql
ALTER TABLE lab_result_audit_logs ADD COLUMN reason TEXT NULL AFTER metadata;
ALTER TABLE lab_result_audit_logs ADD COLUMN event_type VARCHAR(50) NULL AFTER action;
-- event_type: result_entry, status_change, value_correction, retraction, invalidation, draft_save, range_recalculation, demographic_change
```

**New audit event types to track:**
- `draft_save` -- autosave without status change
- `retraction` -- result pulled back after release
- `correction` -- value changed with corrected report
- `invalidation` -- entire request marked invalid
- `range_recalculation` -- reference ranges changed due to demographics update
- `insurance_change` -- insurance applied/changed retroactively
- `add_on_test` -- investigation added to existing request

---

## PART 3: Settings/Configuration Needed

Based on SENAITE, OpenELIS, CrelioHealth, and LigoLab analysis, here are ALL settings a lab administrator should be able to configure. Settings marked with [EXISTS] are already in the system.

### 3.1 General Lab Settings

| Setting Key | Type | Default | Description |
|---|---|---|---|
| `lis.lab_name` | string | - | [EXISTS] Lab name |
| `lis.lab_name_ar` | string | - | [EXISTS] Lab name (Arabic) |
| `lis.lab_license_number` | string | - | License / registration number |
| `lis.lab_phone` | string | - | Lab phone number |
| `lis.lab_email` | string | - | Lab email |
| `lis.lab_address` | string | - | Lab address |
| `lis.lab_logo_url` | string | - | URL to lab logo |

### 3.2 Workflow Settings

| Setting Key | Type | Default | Description |
|---|---|---|---|
| `lis.verification_levels` | enum | `two_level` | `one_level` (enter->release), `two_level` (enter->approve->release), `three_level` (enter->validate->approve->release) |
| `lis.allow_self_verification` | boolean | `false` | Can the person who entered the result also verify/approve it? (CAP/CLIA typically requires NO) |
| `lis.auto_publish_on_release` | boolean | `true` | [EXISTS] Auto-publish when all results released |
| `lis.auto_verify_enabled` | boolean | `false` | Enable auto-verification engine |
| `lis.require_all_results_for_release` | boolean | `true` | Can individual results be released, or must ALL results in a request be entered first? |
| `lis.allow_partial_release` | boolean | `false` | Allow releasing some results while others are still pending |
| `lis.scan_to_receive_mode` | boolean | `false` | Enable barcode scan-to-receive on samples page |

### 3.3 Barcode Settings

| Setting Key | Type | Default | Description |
|---|---|---|---|
| `lis.barcode_format` | enum | `CODE128` | Barcode symbology: `CODE128`, `CODE39`, `QR`, `DATAMATRIX` |
| `lis.barcode_prefix` | string | `''` | Prefix for generated barcodes (e.g., `LAB-`) |
| `lis.barcode_length` | integer | `10` | Total barcode length (including prefix) |
| `lis.barcode_include_patient_name` | boolean | `true` | Show patient name on barcode label |
| `lis.barcode_include_dob` | boolean | `false` | Show date of birth on label |
| `lis.barcode_copies` | integer | `1` | Default number of label copies to print |
| `lis.label_width_mm` | integer | `50` | Label width in millimeters |
| `lis.label_height_mm` | integer | `25` | Label height in millimeters |

### 3.4 Report Settings

| Setting Key | Type | Default | Description |
|---|---|---|---|
| `lis.report_header_text` | string | - | Custom header text for reports |
| `lis.report_footer_text` | string | - | Custom footer / disclaimer text |
| `lis.report_show_qr` | boolean | `true` | Show QR code on reports |
| `lis.report_show_previous_result` | boolean | `true` | Show previous result comparison |
| `lis.report_show_signature` | boolean | `true` | Show doctor/pathologist signature |
| `lis.report_paper_size` | enum | `A4` | `A4`, `Letter`, `A5` |
| `lis.report_template` | enum | `default` | Report template ID/name |
| `lis.report_group_by_section` | boolean | `true` | Group results by lab section in report |

### 3.5 TAT (Turnaround Time) Settings

| Setting Key | Type | Default | Description |
|---|---|---|---|
| `lis.sla_routine_hours` | integer | `24` | [EXISTS?] Default TAT for routine priority |
| `lis.sla_urgent_hours` | integer | `4` | Default TAT for urgent priority |
| `lis.sla_stat_hours` | integer | `1` | Default TAT for STAT priority |
| `lis.tat_alert_enabled` | boolean | `true` | Enable TAT exceeded notifications |
| `lis.tat_alert_threshold_percent` | integer | `80` | Alert when TAT reaches this % of target (early warning) |

Note: Per-investigation TAT is already on `lab_investigations.tat_hours` and `tat_priority_hours`.

### 3.6 Critical Value Settings

| Setting Key | Type | Default | Description |
|---|---|---|---|
| `lis.critical_alert_enabled` | boolean | `true` | Enable critical value alerts |
| `lis.critical_alert_require_acknowledgment` | boolean | `true` | Require explicit acknowledgment of critical alerts |
| `lis.critical_alert_escalation_minutes` | integer | `30` | Minutes before unacknowledged alert escalates |
| `lis.critical_alert_channels` | json | `["in_app"]` | Notification channels: `in_app`, `email`, `sms` |
| `lis.critical_notify_supervisor` | boolean | `true` | Always notify section supervisor on critical |
| `lis.critical_notify_ordering_doctor` | boolean | `true` | Notify the ordering doctor |

### 3.7 Rejection Reasons (Configurable List)

**New table `lab_rejection_reasons`:**

```sql
CREATE TABLE lab_rejection_reasons (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    code VARCHAR(50) NOT NULL,
    name_ar VARCHAR(255) NOT NULL,
    name_en VARCHAR(255) NOT NULL,
    category ENUM('sample','result','request') NOT NULL DEFAULT 'sample',
    is_active BOOLEAN DEFAULT TRUE,
    sort_order INT DEFAULT 0,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    deleted_at TIMESTAMP NULL,

    UNIQUE INDEX idx_company_code (company_id, code),
    INDEX idx_category (company_id, category, is_active)
);
```

**Seed data for common rejection reasons:**

| Code | Category | Name (EN) | Name (AR) |
|---|---|---|---|
| `hemolyzed` | sample | Hemolyzed specimen | عينة متحللة |
| `clotted` | sample | Clotted specimen | عينة متجلطة |
| `insufficient` | sample | Insufficient volume | حجم غير كاف |
| `wrong_tube` | sample | Wrong collection tube | أنبوب جمع خاطئ |
| `mislabeled` | sample | Mislabeled specimen | عينة خاطئة التوسيم |
| `expired` | sample | Specimen expired / too old | عينة منتهية الصلاحية |
| `lipemic` | sample | Lipemic specimen | عينة دهنية |
| `contaminated` | sample | Contaminated specimen | عينة ملوثة |
| `wrong_temp` | sample | Incorrect storage temperature | درجة حرارة تخزين خاطئة |
| `transport_damage` | sample | Damaged during transport | تالفة أثناء النقل |
| `qc_failure` | result | QC failure | فشل مراقبة الجودة |
| `instrument_error` | result | Instrument malfunction | خلل في الجهاز |
| `duplicate_order` | request | Duplicate order | طلب مكرر |
| `wrong_patient` | request | Wrong patient | مريض خاطئ |
| `cancelled_by_doctor` | request | Cancelled by ordering physician | ملغى من الطبيب |

### 3.8 Email Settings

| Setting Key | Type | Default | Description |
|---|---|---|---|
| `lis.email_enabled` | boolean | `false` | [EXISTS] Enable email delivery of reports |
| `lis.sms_enabled` | boolean | `false` | [EXISTS] Enable SMS notifications |
| `lis.email_subject_template` | string | `Lab Results - {patient_name}` | Email subject template |
| `lis.email_body_template` | text | default | Email body HTML template |

---

## PART 4: Notification System

### 4.1 Notification Events and Recipients

| Event | Recipients | Channel | Priority |
|---|---|---|---|
| **Critical result detected** | Section supervisor, Pathologist, Ordering doctor | In-app + Email + SMS | URGENT |
| **Critical alert unacknowledged (30min)** | Lab manager (escalation) | In-app + Email | URGENT |
| **Sample rejected** | Reception, Phlebotomist who collected | In-app | HIGH |
| **Result retracted** | Analyst who entered, Ordering doctor, Patient (if published) | In-app + Email | HIGH |
| **Request invalidated** | All involved staff, Ordering doctor | In-app + Email | HIGH |
| **Report published** | Patient (if portal), Ordering doctor (if email) | Email / Portal | NORMAL |
| **TAT exceeded** | Section head, Lab manager | In-app | MEDIUM |
| **TAT at 80%** | Assigned technician | In-app | LOW |
| **QC failure** | Section head, Lab manager | In-app | HIGH |
| **Reagent low stock** | Lab manager, Section head | In-app | MEDIUM |
| **Reagent stockout** | Lab manager | In-app + Email | HIGH |
| **Add-on test added** | Section where test belongs | In-app | NORMAL |
| **Insurance applied retroactively** | Billing staff | In-app | NORMAL |
| **Machine interface error** | Section head, IT | In-app | HIGH |
| **Auto-verify failed** | Assigned technician, Section supervisor | In-app | MEDIUM |

### 4.2 Notification Infrastructure

**New table `lab_notifications`:**

```sql
CREATE TABLE lab_notifications (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    company_id INT UNSIGNED NOT NULL,
    user_id INT UNSIGNED NOT NULL, -- recipient
    type VARCHAR(100) NOT NULL, -- e.g., 'critical_result', 'tat_exceeded', 'sample_rejected'
    title VARCHAR(255) NOT NULL,
    body TEXT NULL,
    data JSON NULL, -- { result_id, request_id, patient_name, etc. }
    channel ENUM('in_app','email','sms') NOT NULL DEFAULT 'in_app',
    priority ENUM('low','normal','high','urgent') NOT NULL DEFAULT 'normal',
    read_at TIMESTAMP NULL,
    acknowledged_at TIMESTAMP NULL,
    created_at TIMESTAMP NULL,

    INDEX idx_user_unread (user_id, read_at),
    INDEX idx_company_type (company_id, type),
    INDEX idx_priority (company_id, priority, read_at)
);
```

Note: This may already exist in the core module's notification system. If so, the LIS module should use the core notification infrastructure and just define LIS-specific notification types and their routing rules.

### 4.3 Critical Value Notification Workflow (Detailed)

1. Result entered with critical flag (critical_low or critical_high)
2. `CriticalResultDetected` event fires (ALREADY EXISTS)
3. Listener creates `lab_critical_alerts` row
4. Listener sends notifications to configured recipients
5. Dashboard shows unacknowledged critical alerts count (real-time)
6. User acknowledges with a note
7. If not acknowledged within `critical_alert_escalation_minutes`, escalation notification fires
8. All steps logged in audit

---

## PART 5: Complete API Specifications for Missing Endpoints

### 5.1 Add Investigation to Existing Request

```
POST /api/lis/requests/{id}/add-investigation
Permission: lis.requests.update
```

**Request Body:**
```json
{
    "investigation_id": 15,
    "price": 50.000,       // optional, defaults to investigation.price
    "discount": 0.000,     // optional
    "notes": "Doctor requested add-on after seeing initial results"
}
```

**Validation Rules:**
- `investigation_id`: required, integer, exists:lab_investigations,id
- `price`: nullable, numeric, min:0
- `discount`: nullable, numeric, min:0
- `notes`: nullable, string, max:500
- Request must NOT be in `completed` or `cancelled` status
- Investigation must NOT already exist in this request (duplicate check)
- Investigation must be `is_active = true`

**Response (200):**
```json
{
    "data": {
        "id": 42,
        "lab_request_id": 10,
        "investigation_id": 15,
        "investigation": { "id": 15, "name_ar": "...", "name_en": "...", "code": "TSH" },
        "status": "pending",
        "price": 50.000,
        "discount": 0.000,
        "net_price": 50.000,
        "new_sample_required": false,
        "linked_sample_id": 23
    },
    "message": "Investigation added successfully",
    "request_totals": {
        "total_amount": 250.000,
        "discount_amount": 0.000,
        "net_amount": 250.000
    }
}
```

**Side Effects:**
1. Creates `lab_request_investigations` row
2. Checks if existing sample of correct specimen type exists:
   - YES: Creates `lab_results` row linked to existing sample, creates kanban entry
   - NO: Returns `new_sample_required: true`, sample must be collected separately
3. Recalculates request totals
4. If insurance contract exists, recalculates insurance splits
5. Fires `InvestigationAddedToRequest` event
6. Creates audit log entry

**Error Responses:**
- 404: Request not found
- 422: `{ "message": "Cannot add investigation to completed request", "errors": { "status": [...] } }`
- 422: `{ "message": "Investigation already exists in this request", "errors": { "investigation_id": [...] } }`

---

### 5.2 Result Retraction

```
POST /api/lis/results/{id}/retract
Permission: lis.results.retract
```

**Request Body:**
```json
{
    "reason": "Specimen swap confirmed - wrong patient sample was tested",
    "notify": true,
    "notify_doctor": true,
    "notify_patient": false
}
```

**Validation Rules:**
- `reason`: required, string, min:10, max:1000
- `notify`: nullable, boolean (default: true)
- `notify_doctor`: nullable, boolean (default: true)
- `notify_patient`: nullable, boolean (default: false)
- Result must be in `released` or `approved` status (you can only retract what has been at least approved)
- User must NOT be the same person who entered the result (safety guard)

**Response (200):**
```json
{
    "data": {
        "id": 55,
        "status": { "value": "retracted", "label": "Retracted" },
        "retracted_by": 3,
        "retracted_at": "2026-03-28T14:30:00.000000Z",
        "retraction_reason": "Specimen swap confirmed...",
        "result_value": "14.5",
        "previous_status": "released"
    },
    "message": "Result retracted successfully"
}
```

**Side Effects:**
1. Sets result status to `retracted`
2. Sets `retracted_by`, `retracted_at`, `retraction_reason`
3. Updates `lab_request_investigations` status back (e.g., to `in_progress`)
4. Updates request status if it was `completed` -> `partial_result` or `in_progress`
5. Marks any `lab_result_publish_logs` for this request as `retracted`
6. If `notify_doctor`, creates notification for ordering doctor
7. If `notify_patient` and patient portal published, creates portal alert
8. Creates audit log with `event_type: retraction`
9. Fires `LabResultRetracted` event

---

### 5.3 Request Invalidation

```
POST /api/lis/requests/{id}/invalidate
Permission: lis.requests.invalidate
```

**Request Body:**
```json
{
    "reason": "Wrong patient - specimen labels were swapped between patient A and patient B",
    "void_invoice": true,
    "notify": true
}
```

**Validation Rules:**
- `reason`: required, string, min:10, max:2000
- `void_invoice`: nullable, boolean (default: true)
- `notify`: nullable, boolean (default: true)
- Request must NOT already be `cancelled`

**Response (200):**
```json
{
    "data": {
        "id": 10,
        "request_number": "REQ-2026-0042",
        "status": { "value": "cancelled", "label": "Cancelled" },
        "cancellation_reason": "INVALIDATED: Wrong patient...",
        "invalidated_results_count": 5,
        "voided_invoice_id": 18,
        "credit_note_id": 25
    },
    "message": "Request invalidated successfully"
}
```

**Side Effects:**
1. Sets ALL results under the request to `invalidated` status
2. Sets request status to `cancelled`, `cancellation_reason` = "INVALIDATED: " + reason
3. If `void_invoice` and invoice exists:
   - If invoice is `draft`: delete it
   - If invoice is `posted`: create credit note, reverse journal entry
4. Marks all `lab_result_publish_logs` as retracted
5. Notifications to all involved staff
6. Audit log for each result + the request
7. Fires `LabRequestInvalidated` event

---

### 5.4 Result Correction (Amended Value)

```
POST /api/lis/results/{id}/correct
Permission: lis.results.correct
```

**Request Body:**
```json
{
    "corrected_value": "12.8",
    "reason": "Transcription error - original value was from wrong column on worksheet",
    "comment": "Corrected from 14.5 to 12.8"
}
```

**Validation Rules:**
- `corrected_value`: required, string, max:500
- `reason`: required, string, min:10, max:1000
- `comment`: nullable, string, max:500
- Result must be in `entered`, `validated`, `approved`, or `released` status
- New value must be different from current value

**Response (200):**
```json
{
    "data": {
        "id": 102,
        "status": { "value": "entered", "label": "Entered" },
        "result_value": "12.8",
        "corrects_result_id": 55,
        "abnormal_flag": { "value": "normal", "label": "Normal" },
        "original_result": {
            "id": 55,
            "result_value": "14.5",
            "is_corrected": true
        }
    },
    "message": "Corrected result created. Re-verification required."
}
```

**Side Effects:**
1. Original result: `is_corrected = true`
2. New result row created: same request/investigation/sample, `corrects_result_id` = original ID
3. New result enters at `entered` status (must go through verification again)
4. Abnormal flag recalculated for new value
5. Delta check performed against new value
6. Audit log on both original and new result
7. Fires `LabResultCorrected` event

---

### 5.5 Bulk Draft Save (Autosave)

```
POST /api/lis/results/bulk-draft
Permission: lis.results.enter
```

**Request Body:**
```json
{
    "results": [
        { "id": 1, "result_value": "14.5", "comment": "slightly elevated" },
        { "id": 2, "result_value": "Negative" },
        { "id": 3, "result_value": "5.2" }
    ]
}
```

**Validation Rules:**
- `results`: required, array, min:1, max:100
- `results.*.id`: required, integer, exists:lab_results,id
- `results.*.result_value`: required, string, max:500
- `results.*.comment`: nullable, string, max:1000
- Each result must be in `pending` status
- Each result must belong to the user's company

**Response (200):**
```json
{
    "data": {
        "saved_count": 3,
        "results": [
            { "id": 1, "result_value": "14.5", "status": "pending", "abnormal_flag": "high" },
            { "id": 2, "result_value": "Negative", "status": "pending", "abnormal_flag": null },
            { "id": 3, "result_value": "5.2", "status": "pending", "abnormal_flag": "normal" }
        ]
    },
    "message": "Draft saved successfully"
}
```

**Side Effects:**
1. Updates `result_value` on each result WITHOUT changing status
2. Calculates `abnormal_flag` for each (for preview purposes)
3. Sets `entered_by` and `entered_at` (draft marker)
4. Audit log with `event_type: draft_save`
5. Does NOT fire result entered events
6. Does NOT trigger formula calculations
7. Does NOT trigger auto-verification

---

### 5.6 Bulk Enter (Submit Drafted Results)

```
POST /api/lis/results/bulk-enter
Permission: lis.results.enter
```

**Request Body:**
```json
{
    "result_ids": [1, 2, 3]
}
```

**Validation Rules:**
- `result_ids`: required, array, min:1, max:100
- `result_ids.*`: required, integer, exists:lab_results,id
- Each result must be in `pending` status
- Each result must have a non-null `result_value` (previously saved as draft)
- Each result must belong to the user's company

**Response (200):**
```json
{
    "data": {
        "entered_count": 3,
        "auto_verified_count": 1,
        "critical_count": 0,
        "results": [
            { "id": 1, "status": "entered", "abnormal_flag": "high" },
            { "id": 2, "status": "entered", "abnormal_flag": null },
            { "id": 3, "status": "validated", "abnormal_flag": "normal", "auto_verified": true }
        ]
    },
    "message": "Results entered successfully"
}
```

**Side Effects (per result):**
1. Status transitions from `pending` to `entered`
2. Abnormal flag finalized
3. Delta check performed
4. Formula calculations triggered
5. Auto-verification rules evaluated (if enabled)
6. If auto-verify passes, status advances to `validated` automatically
7. Critical alert fired if critical flag
8. `LabResultEntered` event dispatched
9. Reagent consumption tracked
10. Request status updated

---

### 5.7 Acknowledge Critical Alert

```
POST /api/lis/critical-alerts/{id}/acknowledge
Permission: lis.critical-alerts.acknowledge
```

**Request Body:**
```json
{
    "note": "Dr. Ahmed notified by phone at 14:35. He ordered additional monitoring.",
    "doctor_notified": true,
    "doctor_notified_at": "2026-03-28T14:35:00"
}
```

**Validation Rules:**
- `note`: required, string, min:5, max:1000
- `doctor_notified`: nullable, boolean
- `doctor_notified_at`: nullable, date_format:Y-m-d\TH:i:s
- Alert must not already be acknowledged

**Response (200):**
```json
{
    "data": {
        "id": 7,
        "acknowledged_by": 3,
        "acknowledged_at": "2026-03-28T14:36:00.000000Z",
        "acknowledgment_note": "Dr. Ahmed notified...",
        "escalated": false
    },
    "message": "Critical alert acknowledged"
}
```

---

### 5.8 Apply Insurance Retroactively

```
PUT /api/lis/requests/{id}/insurance
Permission: lis.requests.update
```

**Request Body:**
```json
{
    "insurance_contract_id": 5,
    "insurance_approval_number": "INS-2026-0042",
    "coverage_percentage": 80.00
}
```

**Validation Rules:**
- `insurance_contract_id`: required, integer, exists:lab_insurance_contracts,id
- `insurance_approval_number`: nullable, string, max:100
- `coverage_percentage`: nullable, numeric, min:0, max:100
- Request must not be `cancelled`

**Response (200):**
```json
{
    "data": {
        "id": 10,
        "insurance_contract_id": 5,
        "insurance_status": "approved",
        "coverage_percentage": 80.00,
        "patient_share_total": 50.000,
        "insurance_share_total": 200.000,
        "total_amount": 250.000,
        "invoice_adjusted": true,
        "refund_amount": 200.000
    },
    "message": "Insurance applied. Invoice adjusted. Patient refund of 200.000 calculated."
}
```

**Side Effects:**
1. Updates request insurance fields
2. Recalculates per-investigation pricing using `InsurancePricingService`
3. Updates `lab_request_investigations` with `insurance_price`, `patient_share`, `insurance_share`
4. If invoice exists:
   - Draft: update in place
   - Posted: create credit note + new adjusted invoice
5. Calculates patient refund if already paid full amount
6. Audit log
7. Fires `InsuranceAppliedRetroactively` event

---

### 5.9 List Critical Alerts

```
GET /api/lis/critical-alerts
Permission: lis.critical-alerts.view
```

**Query Parameters:**
- `status`: `unacknowledged` | `acknowledged` | `all` (default: `unacknowledged`)
- `date_from`: YYYY-MM-DD
- `date_to`: YYYY-MM-DD
- `section_id`: integer
- `page`: integer
- `per_page`: integer

**Response (200):**
```json
{
    "data": [
        {
            "id": 7,
            "result": { "id": 55, "result_value": "1.2", "investigation": { "name_en": "Hemoglobin", "code": "HGB" } },
            "patient": { "id": 10, "name": "Ahmed Ali", "mrn": "MRN-001" },
            "abnormal_flag": "critical_low",
            "reference_range": "Normal: 12.0-17.0, Critical: <7.0",
            "acknowledged_by": null,
            "acknowledged_at": null,
            "escalated": false,
            "created_at": "2026-03-28T14:00:00.000000Z"
        }
    ],
    "meta": { "total": 3, "current_page": 1, "last_page": 1 }
}
```

---

### 5.10 Configurable Rejection Reasons CRUD

```
GET    /api/lis/rejection-reasons                    -- List all (filterable by category)
POST   /api/lis/rejection-reasons                    -- Create
PUT    /api/lis/rejection-reasons/{id}               -- Update
DELETE /api/lis/rejection-reasons/{id}               -- Soft delete
Permission: lis.settings.manage (for CUD), lis.settings.view (for list)
```

**Create/Update Request Body:**
```json
{
    "code": "hemolyzed",
    "name_ar": "عينة متحللة",
    "name_en": "Hemolyzed specimen",
    "category": "sample",
    "is_active": true,
    "sort_order": 1
}
```

---

### 5.11 Recalculate Reference Ranges

```
POST /api/lis/results/recalculate-ranges
Permission: lis.results.enter
```

**Request Body:**
```json
{
    "lab_request_id": 10
}
```

**Response (200):**
```json
{
    "data": {
        "recalculated_count": 5,
        "flag_changes": [
            { "result_id": 55, "old_flag": "normal", "new_flag": "high", "investigation": "Hemoglobin" }
        ]
    },
    "message": "Reference ranges recalculated for 5 results"
}
```

---

### 5.12 Result Retest (Single Investigation)

```
POST /api/lis/results/{id}/retest
Permission: lis.results.enter
```

**Request Body:**
```json
{
    "reason": "QC control out of range, hemoglobin value questionable",
    "reuse_sample": true
}
```

**Validation Rules:**
- `reason`: required, string, min:5, max:500
- `reuse_sample`: nullable, boolean (default: true -- use same sample if possible)
- Result must be in `entered`, `validated`, `approved`, `released`, or `completed` status

**Response (200):**
```json
{
    "data": {
        "original_result_id": 55,
        "new_result": {
            "id": 103,
            "status": "pending",
            "retest_of": 55,
            "sample_id": 23,
            "new_sample_required": false
        }
    },
    "message": "Retest created. Original result preserved for comparison."
}
```

**Side Effects:**
1. Original result: `is_retested = true`, `retest_reason` set
2. New `LabResult` row: `retest_of = original_id`, status `pending`
3. If `reuse_sample` and existing sample is valid: link to same sample
4. If not reusable: create new sample in `pending` status
5. New result appears in kanban/worklist
6. Investigation status set back to `in_progress`
7. Audit log on both results

---

### 5.13 Recent Patient Investigations (Duplicate Detection)

```
GET /api/lis/patients/{id}/recent-investigations
Permission: lis.patients.view
```

**Query Parameters:**
- `hours`: integer (default: 24, max: 168/7 days)

**Response (200):**
```json
{
    "data": [
        {
            "investigation_id": 15,
            "investigation": { "name_en": "CBC", "code": "CBC" },
            "request_id": 10,
            "request_number": "REQ-2026-0042",
            "ordered_at": "2026-03-28T08:00:00.000000Z",
            "status": "in_progress"
        }
    ]
}
```

---

## PART 6: Summary of All Backend Tasks

### CRITICAL PRIORITY (Safety)

| # | Task | Description |
|---|---|---|
| 1 | **Result Retraction** | `POST /results/{id}/retract` with status, audit, notifications |
| 2 | **Request Invalidation** | `POST /requests/{id}/invalidate` with cascade, invoice void, notifications |
| 3 | **Result Correction** | `POST /results/{id}/correct` with new result, re-verification |
| 4 | **Self-Verification Guard** | Block same user from entering AND approving/verifying the same result |
| 5 | **Critical Alert System** | `lab_critical_alerts` table, acknowledge endpoint, escalation |

### HIGH PRIORITY (Core Workflow)

| # | Task | Description |
|---|---|---|
| 6 | **Add-On Test** | `POST /requests/{id}/add-investigation` with sample reuse logic |
| 7 | **Bulk Draft Save** | `POST /results/bulk-draft` for autosave without status change |
| 8 | **Bulk Enter** | `POST /results/bulk-enter` for batch submission |
| 9 | **Single Result Retest** | `POST /results/{id}/retest` without whole-panel retest |
| 10 | **Configurable Rejection Reasons** | CRUD for `lab_rejection_reasons` table |
| 11 | **Verification Level Config** | Setting to control 1/2/3 level verification workflow |
| 12 | **Retroactive Insurance** | `PUT /requests/{id}/insurance` with invoice adjustment |

### MEDIUM PRIORITY (Quality & Operations)

| # | Task | Description |
|---|---|---|
| 13 | **TAT Monitoring** | Alert system when investigations exceed TAT targets |
| 14 | **Reagent Sufficiency Check** | Pre-batch check endpoint |
| 15 | **Reference Range Recalculation** | Trigger on demographics change + manual endpoint |
| 16 | **Duplicate Test Warning** | `GET /patients/{id}/recent-investigations` for wizard |
| 17 | **Result Source Tracking** | `result_source` field on results (manual/machine/formula/external) |
| 18 | **Enhanced Audit Trail** | `event_type` and `reason` fields on audit log |

### LOW PRIORITY (Settings & Configuration)

| # | Task | Description |
|---|---|---|
| 19 | **Barcode Settings** | Format, prefix, label size configuration |
| 20 | **Report Settings** | Template, paper size, header/footer configuration |
| 21 | **Notification Settings** | Channel preferences, escalation timing |
| 22 | **Reagent Low Stock Alerts** | Notification on minimum stock breach |

---

## PART 7: New ResultStatus Enum (Complete)

```php
enum ResultStatus: string
{
    // Current
    case Pending = 'pending';
    case Entered = 'entered';
    case Validated = 'validated';
    case Approved = 'approved';
    case Released = 'released';

    // NEW
    case Retracted = 'retracted';        // Pulled back after release (safety)
    case Invalidated = 'invalidated';    // Part of request invalidation
    case EnteredInError = 'entered_in_error'; // Wrong patient / specimen swap
}
```

## PART 8: New InvestigationStatus Transitions (Updated)

```
Current: Pending -> Collected -> Received -> InProgress -> WorkCompleted -> ResultsAdded -> Reviewed -> Approved -> Published

Add: Published -> Retracted (new)
Add: Any -> Invalidated (new, only via request invalidation)
Add: ResultsAdded -> ResultsAdded (re-entry after correction)
```

## PART 9: New Events

| Event | Trigger | Data |
|---|---|---|
| `LabResultRetracted` | Result retraction | result, reason, retracted_by |
| `LabResultCorrected` | Result correction | original_result, new_result, reason |
| `LabRequestInvalidated` | Request invalidation | request, reason, affected_results[] |
| `CriticalAlertEscalated` | Unacknowledged critical after timeout | alert, elapsed_minutes |
| `InvestigationAddedToRequest` | Add-on test | request, investigation, new_sample_required |
| `InsuranceAppliedRetroactively` | Insurance change on existing request | request, old_totals, new_totals |
| `ReagentLowStock` | Stock below minimum after consumption | reagent, current_stock, minimum_stock |
| `TATExceeded` | Investigation TAT target exceeded | request_investigation, target_hours, actual_hours |
| `ReferenceRangesRecalculated` | Demographics change triggers recalc | patient, affected_results[], changes[] |

---

## Sources

Research was conducted across the following sources:

- [SENAITE LIMS Documentation](https://www.senaite.com/docs/)
- [SENAITE Core GitHub - Workflow Issues](https://github.com/senaite/senaite.core/issues/1283)
- [Bika LIMS - Verifying Results](https://www.bikalims.org/manual/workflow/verifying-results)
- [HL7 FHIR Laboratory Report Status Management](https://build.fhir.org/ig/HL7/uv-lab-rep-ig/status-mgmt.html)
- [HL7 Europe Lab Report Statuses](https://www.hl7.eu/fhir/laboratory/status-mgmt.html)
- [FHIR DiagnosticReport Resource](http://www.hl7.org/fhir/diagnosticreport.html)
- [PMC - Auto-verification in Clinical Labs](https://pmc.ncbi.nlm.nih.gov/articles/PMC8170738/)
- [PMC - Autoverification in Core Clinical Chemistry](https://pmc.ncbi.nlm.nih.gov/articles/PMC4023033/)
- [PMC - Critical Value Notification](https://pmc.ncbi.nlm.nih.gov/articles/PMC7065418/)
- [PMC - Critical Value Communication Guidelines](https://pmc.ncbi.nlm.nih.gov/articles/PMC5107409/)
- [PMC - Specimen Rejection in Laboratory Medicine](https://pmc.ncbi.nlm.nih.gov/articles/PMC4622196/)
- [PMC - Specimen Mislabeling](https://pmc.ncbi.nlm.nih.gov/articles/PMC5701285/)
- [PMC - Turnaround Time Efficacy](https://pmc.ncbi.nlm.nih.gov/articles/PMC9535613/)
- [UCSC - Amended Laboratory Report Policy](http://shs-manual.ucsc.edu/policy/amended-laboratory-report-policy)
- [CrelioHealth - TAT Configuration](https://blog.creliohealth.com/how-to-set-the-tat-for-your-laboratory-tests/)
- [LigoLab - LIS Collection Model](https://www.ligolab.com/post/ligolabs-collection-model-combines-lis-rcm-workflows-to-maximize-clean-claims-and-laboratory-payments)
- [SpeedsPath - Barcode Labeling in LIS](https://blog.speedspath.com/lis-barcode-integration/)
- [Prolisphere - Instrument Integration vs Manual Entry](https://www.prolisphere.com/instrument-integration-vs-manual-data-entry-in-lab/)
- [Orchard Software - LIS Error Reduction](https://www.orchardsoft.com/blog/using-your-lis-to-reduce-laboratory-errors/)
- [IHE Wiki - Laboratory Testing Workflow](https://wiki.ihe.net/index.php/Laboratory_Testing_Workflow)
- [AHRQ PSNet - Right Patient Wrong Sample](https://psnet.ahrq.gov/web-mm/right-patient-wrong-sample)
- [UToledo - Correction of Records and Results](https://www.utoledo.edu/policies/utmc/laboratory/pdfs/3364-107-106.pdf)
- [Rochester URMC - Critical Value Notification Policy](https://www.urmc.rochester.edu/medialibraries/URMCMedia/urmc-labs/clinical/documents/VI-014-Clinical-Laboratory-Critical-Value-Immediate-Notification-Policy.pdf)
