# Scout D — "Service = backbone" wiring trace (Phase م1)

READ-ONLY scout. Traces one `clinic_service` end-to-end through
booking → price → order → invoice → coverage → revenue-split → report,
with `file:line` at every hop, then lists the BREAK POINTS and splits
them into "م1 must fix" vs "later phase".

Paths:
- BE: `/home/moonui/moon-erp-be/Modules/Clinic`
- FE: `/home/moonui/public_html/moon-erp/src/app/features/clinic`

Owner principle (binding): adding ONE service must cascade automatically —
appears in booking (by type + department) → priced (base/dept/doctor + contract)
→ orderable (intent|immediate) → invoiced → covered → revenue-split → reported.
Any break in that chain is a wiring bug م1 must address.

---

## (1) THE FULL CHAIN (with file:line at each hop)

### HOP 0 — Create a `clinic_service`
- `Http/Controllers/ClinicServiceController.php:57` `store()` → `Actions/UpsertClinicService.php:19` `create()`
  - allocates `code` via `SequenceService` (`UpsertClinicService.php:21`)
  - `ClinicService::create()` (`UpsertClinicService.php:27`)
  - **dispatches** `ClinicServiceCreated` (`UpsertClinicService.php:32`)
- Wiring: `Providers/EventServiceProvider.php:69-71` maps `ClinicServiceCreated → SeedBaseServicePrice`.
- `Listeners/SeedBaseServicePrice.php:15` `handle()`
  - **GUARD**: only upserts a `scope=base` row in `clinic_service_prices` **if `base_price > 0`** (`SeedBaseServicePrice.php:19`); otherwise returns with NO price row.
  - `ServicePrice::updateOrCreate(scope=base, department_id=null, doctor_id=null)` (`SeedBaseServicePrice.php:31`).
- Update path: `EventServiceProvider.php:72-74` `ClinicServiceUpdated → SyncBaseServicePrice`; `Listeners/SyncBaseServicePrice.php:21` re-upserts the base row (same `>0` guard; `base_price ≤ 0` DELETEs the base row, `:22-31`).
- Model: `Models/ClinicService.php` — bridge cols `lis_investigation_id` (`:34`), `rad_procedure_id` (`:35`); `service_type` enum cast (`:47`); `prices()` (`:80`), `contractPrices()` (`:88`), `splitScheme()` (`:121`).
- `Enums/ServiceType.php`: `consultation | clinic_service | lab | radiology`.

### HOP 1 — Appear in the booking / service picker (by type + department)
- `ClinicServiceController.php:22` `index()` filters: `service_type` (`:31`), `department_id` (`:35`), `is_active` (`:39`), `q` (`:43`). So the API CAN filter by type + department.
- FE picker: `services/clinic-service.service.ts` `list(...)` — called from `reception/reception-order.component.ts:329` with `{ q, service_type }` — **passes `service_type` only, NOT `department_id`** (see BREAK 6).

### HOP 2 — Booking / reception → `ServiceOrderLine`
Two entry points, and they behave differently:

**(a) Unified visit (integrated, correct path)** — `Actions/CreateVisit.php:45` `execute()`
  - books appointment (`:77`), creates `ServiceOrder` explicitly (`:81`)
  - **consultation lines** via `AddServiceOrderLine::execute()` (`:97-113`) — passes `clinic_service_id`, `doctor_id`, `doctor_grade_code`, `department_id`, `payer_contract_id`
  - **lab** via `OrderLabFromEncounter::handleForOrder()` (`:119`)
  - **radiology** via `CreateRadOrder::handleForOrder()` (`:127`)

**(b) Generic add-line (reception "add row" screen)** — `ServiceOrderController.php:150` `addLine()` → `AddServiceOrderLine::execute()` (`:158`)
  - spreads `$request->validated()` (`AddServiceOrderLineRequest.php:15-35` — DOES accept `department_id` `:29`, `catalog_ref_*`, `fulfillment_*`), plus order-level `payer_contract_id` (`ServiceOrderController.php:165`).
  - FE `reception-order.component.ts submitLines()` (`:431-437`) sends ONLY `{ line_type, clinic_service_id, doctor_id, quantity }` — no `department_id`, no catalog ref (see BREAK 6/7).

### HOP 3 — Price + coverage snapshot (per line, write-once)
- `Actions/AddServiceOrderLine.php:63` `execute()`
  - **price**: `PricingResolver::resolveWithAxis()` (`:85`) → `Services/PricingResolver.php:81` resolves **doctor(`:83`) → department(`:101`) → base(`:119`)**; returns `axis='none', price=0.0` when no row (`PricingResolver.php:132`). Runs ONLY when `clinic_service_id !== null` (`AddServiceOrderLine.php:84`) — else `unit_price` stays 0 (`:78`).
  - **coverage**: only when `payer_contract_id` AND `clinic_service_id` set (`:96`) → `CoverageService::resolveContractPrice()` (`:100`, `Services/CoverageService.php:26`); sets `unit_price = contract_price`, `price_source='contract'` (`:108-109`).
  - `patient_amount = unit_price*qty - discount - coverage` (`:114`).
  - opens/reuses the header `ServiceOrder` (`:129-169`).
  - persists line snapshot columns write-once (`:172-199`); `revenue_split_scheme_id = null` placeholder (`:193`).
  - **dispatches** `ServiceLineAdded` (`:202`).

### HOP 4 — ServiceLineAdded → snapshot split + recalc header
- `EventServiceProvider.php:76-80`: `ServiceLineAdded → SnapshotLinePricingAndSplit + SnapshotEncounterParties`.
- `Listeners/SnapshotLinePricingAndSplit.php:19` — does NOT re-resolve (immutability); fires `ServiceOrderRecalculated` (`:27`).
- `Listeners/SnapshotEncounterParties.php:19` — **returns early if `clinic_service_id === null`** (`:25`); else `RevenueSplitService::snapshot()` (`:29`).
- `Services/RevenueSplitService.php:85` `snapshot()` → `resolveScheme()` (`:33`) over **doctor+grade+contract** axes (`:45-59`); writes `EncounterServiceParty` rows; stamps `revenue_split_scheme_id` (`:107`). Passes `contractId: null` at snapshot (`:98` — see BREAK 5).
- `EventServiceProvider.php:81-83`: `ServiceOrderRecalculated → RecalculateServiceOrder` → `ServiceOrder::recalculateBillingStatus()` (`Listeners/RecalculateServiceOrder.php:19`).

### HOP 5 — Invoice / cashier (phase م8)
- `EventServiceProvider.php:92-96`: `ReceiptCollected → PostReceiptJournalEntry + UpdateCashierSessionTotals + WritePatientLedgerEntries`.
- Void/refund: `ReceiptVoided` (`:97-105`), `CreditNotePosted` (`:109-111`).

### HOP 6 — Coverage / insurance (phase م9)
- `EventServiceProvider.php:86-89`: `ServiceRendered → RecomputeCoverageOnLine`.
- Order-level split: `CoverageService::splitForOrder()` (`CoverageService.php:272`) with `applyVisitCap()` (`:191`) + annual cap (`:299`), writes `coverage_amount`/`patient_amount` back to each line (`:322-329`).

### HOP 7 — Revenue-split accrual (parties)
- `EventServiceProvider.php:86-88`: `ServiceRendered → AccrueLineParties` → `RevenueSplitService::accrue()` (`RevenueSplitService.php:147`) → delegates to `LIS\DoctorCommissionService::accrueParties()` using the immutable snapshot amounts (`:172-185`).

### HOP 8 — Reporting
- `Services/ClinicReportService.php:18` `revenueByDepartment()` — **INNER JOIN** `service_order_lines.clinic_service_id → clinic_services → departments` (`:30-31`).
- `ClinicReportService.php:179` `revenueByDoctor()` — reads `clinic_encounter_service_parties` (`:186`), joins `service_order_lines` (`:187`).

### The bridge (lab/rad-typed clinic_service → real LIS/RIS entity)
- `OrderLabFromEncounter.php:65-77` builds the LIS payload from raw `investigations:[{investigation_id}]` (the LIS investigation id passed by the caller) — NOT translated from any `clinic_services.lis_investigation_id`. Creates line with `clinic_service_id` **unset → null**, `fulfillment_type='lab_request'`, `price_source='lis'` (`:93-113`).
- `CreateRadOrder.php:59` loads `RadProcedure` from raw `procedure_id`; creates line with `clinic_service_id = null`, `catalog_ref_type='rad_procedure'`, `catalog_ref_id=procedure->id`, `price_source='catalog'` (`:102-126`).
- `grep lis_investigation_id / rad_procedure_id` across all BE modules → only: model `fillable`, `Store/UpdateClinicServiceRequest` validation guards, `ClinicServiceResource` output, factory, tests. **NEVER read by any ordering/pricing action.**

---

## (2) BREAK POINTS (where the cascade fails today)

**BREAK 1 — lab/rad-typed `clinic_service` is a DEAD-END catalog entry. (THE standalone-vs-integrated break.)**
A `service_type=lab` (with `lis_investigation_id`) or `radiology` (with `rad_procedure_id`) service can be created, priced (base row via SeedBaseServicePrice), and shown in the picker — but:
- Adding it via `AddServiceOrderLine` yields a generic priced line with `clinic_service_id` set and NO fulfillment → it never becomes a `LabRequest`/`RadStudy`. The bridge column is never consumed.
- Real lab/rad orders (`OrderLabFromEncounter`, `CreateRadOrder`) bypass the clinic catalog entirely, keying off raw LIS investigation / rad-procedure ids, and produce lines with `clinic_service_id = null`.
So the same clinical service lives in TWO disconnected catalogs; adding a lab service to the clinic catalog does NOT make it orderable-as-lab. Evidence: `OrderLabFromEncounter.php:65-113`, `CreateRadOrder.php:59-126`, and the grep showing the bridge is unread.

**BREAK 2 — reporting drops lab/rad lines (cascade of BREAK 1).**
`revenueByDepartment` INNER-JOINs on `clinic_service_id → clinic_services → departments` (`ClinicReportService.php:30-31`); lab/rad lines (`clinic_service_id = null`) fall out of department revenue. `revenueByDoctor` reads `EncounterServiceParty`, which `SnapshotEncounterParties` never writes for `clinic_service_id = null` lines (`SnapshotEncounterParties.php:25`). Net: the "reported" hop only reports consultation/clinic_service lines — lab/rad revenue and its split are invisible in the two headline reports.

**BREAK 3 — `SeedBaseServicePrice` only fires when `base_price > 0` → silent zero-pricing.**
A `clinic_service` created with `base_price = 0/null` gets NO `ServicePrice` row (`SeedBaseServicePrice.php:19`; mirror in `SyncBaseServicePrice.php:21`). `PricingResolver` then returns `axis='none', price=0.0` (`PricingResolver.php:132`) → `unit_price=0`, `patient_amount=0` with no "unpriced" signal to cashier/report. A service intended to be priced only per-dept/per-doctor is fine, but a service with zero base and no overrides prices silently at 0.

**BREAK 4 — doctor-GRADE pricing axis never wires into the price.**
The line carries `doctor_grade_code` (`AddServiceOrderLine.php:183`), but `ServicePrice`/`PricingResolver` support only base/department/doctor scopes (`PricingResolver.php:83-132`) — there is NO grade scope in pricing. Grade only influences revenue-split scheme resolution (`RevenueSplitService::resolveScheme` `:51-54`) and report display (`ClinicReportService.php:196`). So "priced by doctor grade" is not achievable via the pricing table.

**BREAK 5 — contract axis NOT passed at revenue-split snapshot.**
`RevenueSplitService::snapshot()` calls `resolveScheme(..., contractId: null)` (`RevenueSplitService.php:98`) even though the scheme table supports a `payer_contract_id` axis (`:55-58`). Contract-specific split schemes therefore never resolve on insured lines.

**BREAK 6 — department pricing axis not exercised from the reception add-line UI (FE).**
The resolver + `AddServiceOrderLineRequest` accept `department_id` (`AddServiceOrderLineRequest.php:29`), but `reception-order.component.ts submitLines()` (`:431-437`) never sends it, and the picker query omits `department_id` (`:329`). So per-department prices fire only from `CreateVisit` (which passes `$c['department_id']`, `CreateVisit.php:108`) — not from the generic reception add-line.

**BREAK 7 — reception "add lab/rad line" produces a zero-priced orphan (FE + generic action).**
`reception-order.component.ts rowReady()` allows lab/rad rows with no catalog ref (`:391` "BE resolves"), and `submitLines()` posts them to the generic `addLine` with `clinic_service_id = null` → `unit_price = 0`, no `LabRequest`/`RadOrder` created. Only `CreateVisit`'s unified flow routes lab/rad through `OrderLab`/`CreateRadOrder` correctly.

---

## (3) م1 MUST FIX  vs  LATER PHASES

Guidance from the task: ordering (intent|immediate), cashier, and insurance
already own phases م7 / م8 / م9. So م1 = the SERVICE-BACKBONE wiring itself
(catalog identity, pricing axes, and report/consistency keying), NOT the
ordering-mode decision or the money-collection mechanics.

**م1 must fix (backbone / "one service cascades"):**
- **BREAK 1 (catalog wiring half):** make a lab/rad-typed `clinic_service` the single source — either consume `lis_investigation_id`/`rad_procedure_id` so ordering resolves the real LIS/RIS entity FROM the clinic_service, and/or make lab/rad `ServiceOrderLine`s carry a stable `clinic_service_id` so price+report+split can key off it. (The intent|immediate *decision* is م7; but the fact that a service links to its fulfillment target + keeps its catalog identity on the line is م1 backbone.)
- **BREAK 2:** fix `ClinicReportService` so lab/rad lines are included (via the clinic_service link from BREAK 1, or widen to LEFT JOIN + `catalog_ref`). Report/consistency bug on the backbone.
- **BREAK 3:** decide + implement the base-price=0 behaviour — at minimum surface an "unpriced" state so a silently-zero service is visible in catalog/cashier/report.
- **BREAK 4:** if doctor-grade is a required pricing axis (owner's 3-axis+), add a `grade` scope to `ServicePrice`/`PricingResolver`; if grade is only a split axis, document that pricing is by-doctor, not by-grade. Either way this is a backbone gap to resolve in م1.
- **BREAK 6:** small FE/backbone consistency fix — have the reception picker/add-line pass `department_id` so the department pricing axis actually fires outside `CreateVisit`.

**Belongs to later phases:**
- **BREAK 5** (contract axis at split snapshot) → insurance / revenue-split refinement (م9 + revenue-split phase).
- **BREAK 7** (reception zero-priced lab/rad line) + the `ordering_mode = intent|immediate` decision → ordering phase م7.
- Cashier invoice/JE posting details (HOP 5) → م8.
- Coverage recompute / visit-&-annual caps (HOP 6) → م9.

---

### Key file:line index
- Create+seed price: `Actions/UpsertClinicService.php:19,32`; `Listeners/SeedBaseServicePrice.php:15,19,31`; `Listeners/SyncBaseServicePrice.php:21`.
- Event wiring: `Providers/EventServiceProvider.php:69-89`.
- Picker: `Http/Controllers/ClinicServiceController.php:22,31,35`; FE `reception/reception-order.component.ts:329,391,431-437`.
- Booking→line: `Actions/CreateVisit.php:45,97,108,119,127`; `Http/Controllers/ServiceOrderController.php:150,158,165`; `Http/Requests/AddServiceOrderLineRequest.php:29`.
- Price snapshot: `Actions/AddServiceOrderLine.php:84-114,172-202`; `Services/PricingResolver.php:81-132`.
- Coverage: `Services/CoverageService.php:26,142,272`.
- Split: `Listeners/SnapshotEncounterParties.php:25`; `Services/RevenueSplitService.php:33,85,98,147`.
- Reporting: `Services/ClinicReportService.php:18,30-31,179,186`.
- Bridge (unconsumed): `Actions/OrderLabFromEncounter.php:65-113`; `Actions/CreateRadOrder.php:59-126`; `Models/ClinicService.php:34-35`.
