# 02 — LIS Module Deep-Map: The Proven Clinical-Module Template Inside Moon ERP

Source tree: `/home/moonui/moon-erp-be/Modules/LIS` (Laravel 12, nwidart module). All statements below are traceable to files read on 2026-06-10/11.

## 1. Scale snapshot

| Artifact | Count | Location |
|---|---|---|
| Eloquent models | 66 | `Modules/LIS/app/Models/` |
| Migrations | 137 | `Modules/LIS/database/migrations/` |
| Domain events | 10 | `Modules/LIS/app/Events/` |
| Event listeners | 11 | `Modules/LIS/app/Listeners/` |
| Services | 26 | `Modules/LIS/app/Services/` |
| HTTP controllers | 66 | `Modules/LIS/app/Http/Controllers/` |
| Permission dependency edges | 36 explicit cross-resource edges | `Modules/LIS/app/Support/LisPermissionDependencies.php` |

## 2. Entity graph (read from `app/Models`)

### Clinical core
- **`LabPatient`** (`lab_patients`) — MRN, demographics (`name_ar`/`name_en`), `national_id`, `medical_history`, `insurance_info` (JSON). Cross-module links: `partner_id → Modules\Core\Models\BusinessPartner`, `insurance_contract_id → LabInsuranceContract`, `external_lab_id → LabExternalLab` (B2B-owned patients). Patient-portal: `portal_token` + permanent `portal_link_token` auto-generated in `booted()` (`Str::random(64)`) for QR/WhatsApp result links. Note: LIS owns its own patient table — there is **no central ERP patient registry yet**; `partner_id` is the bridge to Core.
- **`LabDoctor`** (`lab_doctors`) — referring doctor master. Links: `department_id → Modules\HRM\Models\Department`, `partner_id → BusinessPartner`, `price_list_id → LabPriceList` (doctor-preferred pricing), commission config (`commission_type/value`, `commission_account_id`/`commission_payable_account_id → Modules\Accounting\Models\Account`), `is_internal`, `specialty_id → LabDoctorSpecialty`. **NEW inert column `employee_id`** (see §7).
- **`LabRequest`** (`lab_requests`) — the order header. `request_number` (Core `SequenceService`), `patient_id`, `doctor_id`, `branch_id`, `priority` (`RequestPriority`), `status` (`RequestStatus`), `source` (`RequestSource`: `walk_in | in_patient | emergency | external` — `in_patient`/`emergency` already anticipate hospital ordering), money totals, insurance fields (`insurance_contract_id`, `coverage_percentage`, `patient_share_total`, `insurance_share_total`), `external_lab_id`, `price_list_id`, `ready_for_pickup_at`. **NEW inert column `encounter_id`** (see §7).
- **`LabRequestInvestigation`** — order lines (price, discount, net, section routing via `section_id`, `source_group`).
- **`LabSample`** (`lab_samples`) — specimen with full custody timeline columns (`picked_up_/collected_/received_/delivered_/rejected_/accepted_/processing_started_/processing_completed_/ready_for_result_ + _at/_by` pairs), parent/child split samples (`parent_id`), defer support, external-referral fields (`is_external`, `external_lab_id`, `external_referral_id`, `external_status`), `branch_id`. Authoritative chain-of-custody in `LabSampleCustodyLog`.
- **`LabResult`** (`lab_results`) — result row per investigation. Status machine (`ResultStatus`: `pending → entered → validated → approved → released`, plus `retracted | invalidated | entered_in_error`), abnormal/critical flags + ranges, delta check, three comment fields, `entry_source` (`workflow | validation | external_lab | machine`), machine metadata (`machine_id`, `raw_data`), full who/when stamps (`entered_/validated_/approved_/released_by/at`), amendment/retraction lineage (`retracted_from_id`, `retest_of`, `amended_from_value`). `booted()` auto-stamps `branch_id` from the parent request. Polymorphic `Attachment` (Core) for file results; `LabHistopathResult` 1:1 for histopathology.

### Billing
- **`LabInvoice`** (`lab_invoices`) — `invoice_type` enum (`standard | patient_invoice | insurance_invoice | external_lab_payable | external_lab_receivable`), `journal_entry_id`, `cogs_journal_entry_id`, `cancel_journal_entry_id`, `total_cost`, NPHIES claim fields (`nphies_claim_status`, `nphies_preauth_ref`, ...), posted/cancelled stamps. + `LabInvoiceItem`, `LabPayment`, `LabPaymentMethod`, `LisCashierSession` (cashier shift sessions).
- Insurance: `LabInsuranceContract` (+ per-investigation coverage `LabInsuranceContractInvestigation`), `LabMonthlyInsuranceInvoice` (monthly claims).
- Doctor commissions: `LabDoctorCommissionRule`, `LabDoctorCommission`, `LabCommissionSettlement`.

### External labs / B2B
- `LabExternalLab` (B2B client or reference lab; `pickup_config` JSON), `LabExternalLabPricing` + price lists, `LabExternalLabReferral`/`LabExternalLabReferralTest` (send-out workflow), `LabMonthlyExternalLabInvoice`, `LabExternalLabPayment`/`LabExternalLabPaymentAllocation`.
- **`LabExternalLabCourier`** (`lab_external_lab_couriers`) — courier eligibility pivot (which users may courier for which lab), created in migration `2026_06_09_150000_add_b2b_courier_pickup_foundation.php`. With pickup enabled, portal samples are born `at_external_lab` → courier `in_transit` → handover `collected`; reception/kanban unchanged (they only ever see samples once `collected`). Custody stays authoritative in `lab_sample_custody_logs`.

### Catalog / lab operations
- `LabInvestigation` (LOINC code fields, panels via `LabInvestigationPanelMember`, formulas with dependency columns, outsourced flags, gender applicability, NAFIS fields), `LabSection`, `LabSpecimenType`, `LabUnit`, `LabInvestigationNormalRange`, `LabPackage`, `LabPriceList`/`LabPriceListItem`.
- Machines (LIS interfacing): `LabMachine`, `LabDeviceModel`(+Test), `LabMachineTestMapping`, `LabMachineResult`, `LabMachineCommunicationLog`, `LabAutoVerifyRule` (+ `AutoVerificationService`).
- QC: `LabQcLot`, `LabQcResult`, `WestgardRuleService`. Inventory: reagents (`LabReagent*`), consumables. Compliance: `LabComplianceChecklist`, `LabRetentionPolicy`, `LabResultAuditLog`, `LabResultPublishLog`.

## 3. Request lifecycle (reception → release → invoice)

1. **Reception** — `Modules\LIS\Actions\CreateLabRequest::handle()` (docblock: *"HMS Phase-0 W1-3 — byte-identical extraction of LabRequestController::store so HIS can create lab requests through the same path. NO behavior change."*). Generates `request_number` via Core `SequenceService->generateNext($companyId,'lis','lab_request')`, stamps `branch_id` from `DataScope::operatingBranchId()`, resolves prices by priority *explicit → external-lab inbound pricing → doctor preferred price list → investigation default*. The reception wizard also bills and collects (creates invoice + payment) — encoded in `LisPermissionDependencies::EDGES['lis.requests.create']`.
2. **Sampling** — `LabSampleService`: collect dispatches `LabSampleCollected` (→ `UpdateRequestStatusOnSampleCollection`; request → `sample_collected`); receive dispatches `LabSampleReceived` (→ `GenerateResultsOnSampleReceived` creates pending `lab_results`, `CreateSectionProcessingOnSampleReceived` opens section worklist rows in `lab_sample_section_processing`, `CreateReferralForOutsourcedInvestigations` auto-creates send-out referrals).
3. **Worklists / processing** — section kanban (`KanbanRejectService`, `WorklistContextService`, `ReceptionWorklistService`, `SampleInvestigationService` + `lab_sample_investigations` state table), machine results inbox (`MachineResultMatchingService`, auto-verify rules).
4. **Result entry** — manual (`LabResultController` dispatches `LabResultEntered`, and `CriticalResultDetected` when critical → `CreateCriticalAlert`) or machine (`LabMachineResultController` same events). `LabResultEntered` also triggers `ConsumeReagentOnResultEntry` (inventory) and `UpdateRequestStatusOnResultEntry` (request → `in_progress`).
5. **Validate → approve → release** — `ResultStatus` ladder with separate permissions (`lis.results.validate/approve/release`, plus `release_unpaid` as a guarded override). Release dispatches **`LabResultReleased`** (from `LabResultController` and re-dispatched per promoted panel row by `RollUpPanelAndFormulaOnRelease`).
6. **On release** (ordered listeners in `app/Providers/EventServiceProvider.php`): `RollUpPanelAndFormulaOnRelease` → `AutoPublishOnResultRelease` (patient portal / report publication via `LabResultPublishService`, logged in `lab_result_publish_logs`) → `PostInvoiceItemOnResultRelease` (B2B per-item accounting posting, idempotent) → `UpdateRequestStatusOnResultRelease` flips the request to `completed` (or `partial_result`) and `LabWorkflowService` dispatches **`LabRequestCompleted`**.
7. **Post-release corrections** — first-class retract/correct/invalidate flow: events `LabResultRetracted`, `LabResultCorrected`, `LabRequestInvalidated`; lineage columns on `lab_results`; full audit in `lab_result_audit_logs`.

## 4. Events emitted (the integration surface HIS/OBGY can consume)

`Modules/LIS/app/Events/` — plain Laravel events carrying the full model:
`LabSampleCollected`, `LabSampleReceived`, `AllSamplesCollected`, `LabResultEntered`, `CriticalResultDetected`, `LabResultReleased(LabResult $result)`, `LabResultCorrected`, `LabResultRetracted`, `LabRequestCompleted(LabRequest $request)`, `LabRequestInvalidated`.

Dispatch points verified: `LabWorkflowService` (lines 40/75/120: `AllSamplesCollected`, `LabRequestCompleted` x2), `LabSampleService` (56/115), `LabResultController` (389/392/516/1058), `LabMachineResultController` (238/240), `RollUpPanelAndFormulaOnRelease` (188). Wiring in `EventServiceProvider::$listen` — listener order is deliberate and commented (roll-up before auto-publish; status update after roll-up).

## 5. Accounting integration (how LIS invoices post)

- `Modules\LIS\Actions\PostLabInvoice` — transactional. Calls `Modules\Accounting\Actions\CreateJournalEntry`; resolves accounts from Core `SettingsService` keys `lis.receivable_account_id`, `lis.revenue_account_id`, `lis.tax_payable_account_id`, preferring a **partner-specific AR account** via `Modules\Accounting\Models\AccBpExt`. Per-`invoice_type` JE builders: `createStandardJE`, `createInsuranceInvoiceJE` (contract-specific accounts), `createExternalLabOutboundJE`/`InboundJE`. Posts a **COGS journal entry** when `total_cost > 0` (`cogs_journal_entry_id`; costs from `LisCostCalculatorService`).
- Posting also auto-calculates **doctor commissions** (`DoctorCommissionService` → its own `CreateJournalEntry` calls + `CreatePartnerAccounts` listener reuse), skipped for insurance/external-lab invoice types to avoid double counting.
- Companion actions: `CancelLabInvoice` (reversal JE → `cancel_journal_entry_id`), `PostLabPayment` / `VoidLabPayment`, `PostLabInvoiceItem` (B2B per-item, used by the release listener), `RecordExternalLabPayment`.
- Pattern: **LIS keeps its own subledger tables and posts summarized JEs into the Accounting module — it never writes GL rows directly.**

## 6. Permissions and branch scoping

- **Permissions**: `lis.{resource}.{action}` strings enforced as controller middleware (`new Middleware('permission:lis.requests.create', only:['store'])` — `LabRequestController` lines 48-55). `Modules\LIS\Support\LisPermissionDependencies` encodes 36 explicit cross-resource prerequisite edges + a generic `{action}→view` rule, expanded as a transitive closure on role save and used to auto-select in the roles UI. **HMS Phase-0 W1-4**: `LISServiceProvider` registers this map into `Modules\Core\Support\PermissionDependencyRegistry::register('lis', ...)` — "fixes Core→LIS inversion", i.e., the registry is now a Core extension point any module (HIS, OBGY) can plug into.
- **Branch scoping**: `Modules\LIS\Support\LisDataScope` is now a deprecated shim extending `Modules\Core\Support\DataScope` (*"logic hoisted to Core (HMS Phase-0 W1-1); 14 existing call sites keep working"*). Core `DataScope` implements `own | branch | all` role-based data visibility (broadest-wins across roles), `?branch_id=`/`X-Branch-Id` header filtering, and `operatingBranchId()` (user's primary branch stamps every new record). Generic — ready for reuse by HIS/OBGY.

## 7. The inert HIS-readiness columns (HMS Phase-0)

- `2026_06_10_160000_add_encounter_id_to_lab_requests.php` — nullable unsigned `encounter_id` on `lab_requests`. Docblock: *"HIS encounters table does not exist yet — FK + consumers arrive at HIS kickoff (Phase-0 W1-3 inert column). Column is not read anywhere."* Confirmed inert: not in `LabRequest::$fillable`, zero references in `Modules/LIS/app/`.
- `2026_06_10_150000_add_employee_id_to_lab_doctors.php` — nullable `employee_id` + index on `lab_doctors`; FK to `employees` guarded by `Schema::hasTable('employees')` (HRM may not be migrated). Docblock: *"HMS Phase-0 W1-2 doctor→employee→user identity chain... No consumer yet."* Also confirmed absent from `LabDoctor::$fillable`.
- Together with W1-1 (DataScope hoist), W1-3 (CreateLabRequest action extraction) and W1-4 (PermissionDependencyRegistry), these form a deliberate, already-shipped **Phase-0 HIS preparation track inside LIS**.

## 8. How HIS / OBGY will order labs and receive results (the contract)

1. **Order**: call `Modules\LIS\Actions\CreateLabRequest` (extracted for exactly this purpose) with `source = in_patient` (enum value already exists) and set `lab_requests.encounter_id` once the HIS encounters table exists. Pricing, invoicing, insurance, sequencing, branch stamping all come for free.
2. **Identity**: patient bridged through `lab_patients.partner_id` (Core `BusinessPartner`) until/unless a central patient registry exists; ordering physician resolved through `lab_doctors.employee_id → employees → users`.
3. **Results back**: subscribe to `LabResultReleased` (per-test, includes critical flagging upstream via `CriticalResultDetected`) and `LabRequestCompleted` (order-level), plus `LabResultCorrected`/`LabResultRetracted` for amendments — the same in-process event bus LIS already uses internally for billing and publication side effects.
4. **Module shape to copy**: domain tables prefixed and self-contained; enums for every state machine; events + ordered listeners for side effects; Actions for cross-module entry points; posting to Accounting via `CreateJournalEntry` only; `lis.{res}.{action}` permissions with a dependency map registered to Core; Core `DataScope` for branch visibility.

## Key file citations
- Models: `/home/moonui/moon-erp-be/Modules/LIS/app/Models/{LabRequest,LabPatient,LabDoctor,LabSample,LabResult,LabInvoice,LabExternalLabCourier}.php`
- Events/wiring: `/home/moonui/moon-erp-be/Modules/LIS/app/Events/`, `/home/moonui/moon-erp-be/Modules/LIS/app/Providers/EventServiceProvider.php`
- Accounting: `/home/moonui/moon-erp-be/Modules/LIS/app/Actions/PostLabInvoice.php`, `.../Listeners/PostInvoiceItemOnResultRelease.php`, `.../Services/DoctorCommissionService.php`
- Permissions/scoping: `/home/moonui/moon-erp-be/Modules/LIS/app/Support/{LisPermissionDependencies,LisDataScope}.php`, `/home/moonui/moon-erp-be/Modules/Core/app/Support/DataScope.php`, `.../Providers/LISServiceProvider.php`
- HIS-readiness migrations: `.../database/migrations/2026_06_10_150000_add_employee_id_to_lab_doctors.php`, `2026_06_10_160000_add_encounter_id_to_lab_requests.php`; HIS entry point `.../app/Actions/CreateLabRequest.php`
