# Task 3 (BE) — م0: `clinic:remediate-stranded-receipts` — REPORT

## Status: DONE_WITH_CONCERNS (one flagged edge; see Concerns)

## Deliverables (files)
- **Command:** `/home/moonui/moon-erp-be/Modules/Clinic/app/Console/RemediateStrandedReceipts.php`
- **Tests:** `/home/moonui/moon-erp-be/Modules/Clinic/tests/Feature/RemediateStrandedReceiptsTest.php`
- **Registration:** `Modules/Clinic/app/Providers/ClinicServiceProvider.php` — added the command to `registerCommands()` (was `$this->commands([])`).

Branch `hazemdev`, base `22d0b4270`. NO migration. Command + tests only.

## Command behaviour
Signature: `clinic:remediate-stranded-receipts {--force} {--company=}` — mirrors the
dry-run/`--force`/table pattern of `lis:prune-machine-heartbeats`.

### Scan (default, read-only — also the any-install audit AND the post-run "verify")
Prints three detector tables + a summary, writes NOTHING:
- **(a)** Stranded pending receipts: `status='pending' AND paid_total > 0 AND journal_entry_id IS NULL`.
  Columns: id, receipt_number, company_id, paid_total, `ar/revenue set?`, `payments have account?`, `remediable?`.
- **(b)** Orphan credit notes: posted `clinic_credit_notes` whose paying receipt has `journal_entry_id IS NULL`.
- **(c)** M4 retro-detector: soft-deleted `service_orders` (`withTrashed`) that still carry a receipt or CN.

### `--force` (remediate detector (a) only; (b)/(c) are report-only)
Per remediable receipt, in its OWN `DB::transaction` (all-or-nothing):
1. Skip+report if `clinic.ar_account_id`/`clinic.revenue_account_id` unset (never guess).
2. Payment side from `reception_payments`: `DR payment.account_id` + `CR ar` per payment; if ANY payment `account_id IS NULL` → skip the whole receipt.
3. Revenue side: if `reception_receipt_lines` exist → per line `DR ar (amount)` + `CR revenue`; else reconstruct from `PatientLedgerEntry` Charge rows (`source_type='service_order_line'`, `reference=receipt_number`, company-scoped).
4. Balance asserts: `round(Σdr,3)===round(Σcr,3)` AND payment-side Σ === `paid_total` AND revenue-side Σ === `paid_total`; else throw → roll back that receipt, report needs-manual.
5. Post via `CreateJournalEntry` — `entry_type=ClinicReceipt`, `date=now()` (never backdate), `reference=receipt_number`, description references original receipt# + original created_at date, `source_type='reception_receipt'`, `source_id=receipt.id`, `created_by=receipt.created_by`, **`idempotency_key='clinic_receipt_'.$receipt->id`**.
6. Flip receipt `journal_entry_id=$je->id`, `status='paid'`.
7. After the loop: for each orphan CN whose receipt now has a JE → report "CN <n> linkage healed" (CN never modified).

The JE is built EXPLICITLY (NOT via `PostReceiptJournalEntry`, whose `status='ordered'`
legacy fallback would build an empty/unbalanced revenue side for these billed/cancelled lines).

### Idempotency verified
Re-running `--force` is a clean no-op: the idempotency key blocks a duplicate JE, and a
remediated receipt is no longer `pending` so it drops out of detector (a). Test 3 asserts
exactly ONE JE after two runs.

## Tests — RED → GREEN (7 tests, 39 assertions)
Written first (RED — command absent, plus a CN NOT-NULL seed fix for `payment_method`),
then GREEN after implementing:
1. scan is read-only — receipt reported, still pending, no JE. ✓
2. `--force` posts a balanced JE (Σdr==Σcr, source_id=receipt, status Posted) + flips paid; patient ledger untouched. ✓
3. idempotent — two `--force` runs → exactly one JE. ✓
4. GL unset → skipped needs-manual, no JE, still pending. ✓
5. null payment account_id → whole receipt skipped, no JE. ✓
6. reconstruct-from-ledger — no receipt-lines, Charge rows → balanced JE. ✓
7. CN linkage heal — posted CN against a stranded receipt reported "healed", CN untouched. ✓

`Tests: 7 passed (39 assertions)`.

## Regression
- The 4 required L2-regression tests + the new file run together: **49 passed (278 assertions)**
  (`PartialCollectTest`, `RefundLineTest`, `MoneyPathRegressionTest`, `ReceptionCashierTest`, `RemediateStrandedReceiptsTest`).
- No Pest global-helper name collisions (all helpers prefixed `rem*`; verified unique).
- Full `Modules/Clinic/tests/Feature` suite: the ONLY failure is the pre-existing
  `ClinicRoomTest › it returns HRM departments via the clinic projection` (unrelated to this
  change, flagged known by the coordinator). All other files — including the 4 L2 tests and
  the new `RemediateStrandedReceiptsTest` — pass.

## Concerns
1. **Draft JE when `created_by` is NULL or `accounting.auto_post_entries=false`.** `CreateJournalEntry`
   auto-posts only when it can resolve a real user (CLI has no `auth()->user()`, so it falls back to
   `entry->created_by`) and the company opts in; otherwise it deliberately leaves a safe **Draft** JE.
   The receipt is still flipped to `paid` with `journal_entry_id` set. This exactly mirrors the live
   `PostReceiptJournalEntry` listener, so it is not a new behaviour — but it means a stranded receipt
   whose `created_by` is null (or whose company uses manual posting) would end with a Draft JE that
   posted-only reports don't show. The 4 real stranded receipts were collected by a cashier, so
   `created_by` is expected to be set; the orchestrator should confirm the posted JE status after the
   live run (the scan re-run "verify" shows the receipt healed regardless of Draft/Posted).
2. **`--filter=` crashes Pest here** (exit 255, no output) — a local harness quirk unrelated to this
   change; run tests by FILE PATH (`vendor/bin/pest <path>`), which works. Confirmed `--filter` also
   fails on unrelated files.

## Commit
`29c72a4362d5cc9219cad9b97ba963858bece4f7` (branch `hazemdev`) — 3 files: the command,
the test, and the `ClinicServiceProvider` registration. Nothing else staged.

---

# REVIEW-GATE FIXES (2nd commit)

Commit `14b8f378fed49fbffd868294a86673af3b635410` (branch `hazemdev`) — 2 files:
`RemediateStrandedReceipts.php` + `RemediateStrandedReceiptsTest.php`. Message:
`fix(clinic): م0 review — surface JE posted/draft status + tighten reconstruction + positive-payment guard`.

All four review findings applied, each TDD-covered. `RemediateStrandedReceiptsTest`: **11 passed (61 assertions)**.

### Fix 1 (HIGHEST VALUE) — surface JE Posted/Draft status
- `remediateReceipt` now captures the JE from `CreateJournalEntry->execute()` and returns
  `['status'=>'remediated','je_id'=>…,'je_status'=>$je->status->value]`.
- Per-receipt output: `JE #{id} POSTED, receipt marked paid.` vs (warn)
  `JE #{id} left as DRAFT (NOT posted) — post it manually; receipt marked paid.` (status upper-cased).
- Final summary table gained a **"of which JE left Draft"** column + a warn banner when > 0.
- New **scan detector (d)** — `receiptsWithDraftJe()` — surfaces any receipt whose linked JE is
  NON-POSTED (status != posted, receipt not voided). So the VERIFY re-scan does NOT report a
  Draft-JE receipt as fully healed; it lists it with a "post those manually" warning. We do NOT
  force a post (that would override a company's deliberate manual-posting policy and diverge from
  live `PostReceiptJournalEntry`).
- Covering tests: `--force posts a balanced JE …` now also asserts the `POSTED` label is surfaced;
  NEW `--force surfaces a Draft JE when auto-post is off and the verify scan flags it` (auto-post
  OFF → JE Draft → command prints DRAFT, receipt still paid, `$je->status===Draft`, and the verify
  scan output contains the receipt number + `Draft`).

### Fix 2 — tighten revenue reconstruction (Codex)
- The pre-L2 Charge-ledger reconstruction now also constrains `patient_id = $receipt->patient_id`
  AND `source_id IN (the receipt's service order's line ids)`, in addition to
  company/entry_type/source_type/reference. Balance assert retained (a genuine mismatch still rolls
  back → needs-manual).
- Covering test: NEW `--force reconstruction excludes a decoy Charge with a different patient/order
  but the same reference` — a 500 decoy sharing the receipt-number reference (different patient +
  different order line) is excluded; remediation SUCCEEDS at the correct 100 (JE 200/200 balanced).
  Had the decoy leaked in, the revenue side would be 600 and the receipt would be skipped as
  unbalanced — so a green "paid" proves exclusion.

### Fix 3 — positive-payment guard (Codex)
- Before building payment JE lines, if ANY `reception_payments.amount <= 0` → skip the whole
  receipt + report needs-manual (never post a negative debit/credit line even if payments net to
  paid_total).
- Covering test: NEW `--force skips a receipt that has a non-positive payment line` — payments
  150 + (−50) net to paid_total 100, but the −50 line → receipt skipped, no JE, still pending.

### Fix 4 — `--company=0` (Minor)
- All detector scoping changed from `if ($companyId)` → `if ($companyId !== null)` so an explicit
  `--company=0` scopes to company 0 (empty) instead of silently "all".
- Covering test: NEW `--company=0 does not silently scope to all companies` — a real stranded
  receipt in another company is left untouched under `--force --company=0`.

### Command run (tests only — NEVER against a real DB)
`vendor/bin/pest Modules/Clinic/tests/Feature/RemediateStrandedReceiptsTest.php` →
`Tests: 11 passed (61 assertions)`. The 4 L2-regression tests (run by file path together):
`Tests: 42 passed (239 assertions)`. (`--filter` still crashes the local harness — use file paths.)
Post-fix full `Modules/Clinic/tests/Feature` suite: **1 failed, 355 passed (1239 assertions)** —
the ONLY failure is the known pre-existing `ClinicRoomTest › it returns HRM departments via the
clinic projection` (unrelated). My `RemediateStrandedReceiptsTest` and all 4 L2 tests PASS. No new regression.

### Updated concern
Concern #1 above is now directly SURFACED by the command itself (per-receipt DRAFT line + summary
count + verify detector (d)), instead of being silent. The orchestrator's live `--force` output and
the verify re-scan will explicitly list any receipt whose JE ended Draft (auto-post off / null
created_by) so they can be posted manually.
