# م8 (الكاشير والفلوس) — عقد التنفيذ + ليدجر (2026-07-08)

> العقد المُلزِم. الخريطة `SCOUT.md`. المرجع: `topics/clinic-tightening-plan.md` ملحق أ (تدقيق C1-M6) + §المحطة5. **اقرأ الليدجر أول حاجة.** ⚠️ **أخطر مرحلة ماليًا** — Fable معطّلة → architect يراجع العقد + **Codex كل WP + مراجعة مزدوجة لـWP المالي الأساسي (WP1/WP2)**.

## الهدف (المالك): درج صادق 100% وكل بند له طريق.
تست المالك: ابعت من الاستقبال → افتح كاشير تاني → لاقي البند وتممه واطبع · حصّل جزئي → الباقي آجل في رصيده · استرد بند → الشاشة قالت الصافي.

## القرارات (على أسئلة السكاوت الثمانية)
1. **performed lifecycle (a):** collect يقبل `ordered`+`performed` — ٣ فروع collect + JE listener + flip/ledger + FE `orderedLines`/`isLineSelectable`. (بند مكتمل قبل الدفع = شغل تمّ = لازم يتفوتر؛ استثناؤه من patient_due غلط.)
2. **Cashier queue:** **شاشة جديدة** + endpoint `GET /clinic/cashier/queue` (orders `payment_disposition=send_to_cashier` وغير مُسوّاة، company-scoped، بـpatient/due/sender/time).
3. **Receipt print:** **FE-HTML** (يعيد استخدام plumbing طباعة الروشتة/م7 slips، HTML-escaped) — ليترهيد + بنود + مدفوع/وسيلة + صافي. لا BE-PDF.
4. **M3 close:** order→`closed` لما fully billed **AND** outstanding≤0 (net). order→`cancelled` لما كل السطور cancelled. يتضبط في `recalculateBillingStatus` (أو finalize مخصّص).
5. **H5 money-truth:** `refunded_total`+`net` **محسوبان** (بلا عمود — net credit notes في recalc) على model+ServiceOrderResource+FE interface+header/rail/cashier + dashboard يخصم refunds من collected.
6. **blind-count:** close dialog ماتعبّيش counted_cash (blind، default فاضي/0) + ملخّص over/short صادق. **Z-report كامل بالفئات → مؤجّل م11.**
7. **M2 pending rescue:** **moot** (in-txn JE منع الجديد؛ م0 عالج القديم) — وثّق، متبنيش.
8. **credit balance / over-payment:** **مؤجّل** (over-payment يفضل مرفوض؛ مفيش allow_credit_balance).

## 📊 ليدجر
| WP | المحتوى | يعتمد | مراجعة | migration | الحالة |
|----|---------|-------|--------|-----------|--------|
| WP1 | **performed lifecycle + net helper + close/cancel BE (C1/M1/H5-core/M3) [CRITICAL]:** (أ) collect يقبل ordered+performed — **٣ بوابات collect + مُصنّف repayment** (R2) + **`WritePatientLedgerEntries` flip+charge** (R1، الحرج) + PostReceiptJournalEntry (صحيح أصلًا). (ب) **`refunded_total`+`net` helper في recalc** (R3، لازم قبل close) = paid_total gross − Σ credit-notes posted. (ج) close عند fully-billed+**net-outstanding≤0** + cancel عند `lines()->exists() && activeLines->isEmpty()`. Pest: **performed→billed + Charge row واحد + ليدجر nets 0.000 + collect تاني مرفوض** (R1) + repayment-misclassify + close/cancel + happy-path regression. | — | **native+Codex×2** | غالبًا لا | ✅ |
| WP2 | **money-truth surface BE (H5) [CRITICAL]:** عرض `refunded_total`+`net` (helper WP1) على `ServiceOrderResource:22-30` (derived، paid_total يفضل gross) + dashboard `resolveCollected`/`revenueLastDays` **يخصم credit-notes posted بالتاريخ** (DATE=today للمحصّل، مدى 7 أيام للإيراد، company+branch عبر DataScope) — **بلا استثناء receipts مسترّدة** (R5). Pest: fully-paid مايكدبش بعد refund، dashboard collected مايعدّش المسترد، لا double-net. | WP1 | **native+Codex×2** | لا | ✅ |
| WP3 | **Cashier queue endpoint BE:** `GET /clinic/cashier/queue` (send_to_cashier + غير مُسوّى) + Resource بـpatient/patient_due/net/sender/sent_at. Pest: يعرض غير المُسوّى فقط، company-scoped، يستثني المُسوّى/المدفوع. | WP1·WP2 | native+Codex | لا | ✅ |
| WP4 | **Cashier queue screen FE:** شاشة جديدة (list من WP3) — patient/due/sender/waited → «افتح» تروح cashier-collect بالـorder. الاستقبال «إرسال للكاشير» يظهر في الطابور. | WP3 | single | — | ✅ |
| WP5 | **money-truth display + performed-selectable + blind-count FE:** performed في `orderedLines`/`isLineSelectable` (قابل للتحصيل) + عرض «محصّل − مسترد = صافي» (header/rail/cashier، fullyPaid صادق) + close dialog blind counted_cash + ملخّص over/short. | WP1·WP2 | native+Codex | — | ✅ |
| WP6 | **Receipt print FE:** «Collect & print» يطبع إيصال FE-HTML حقيقي (ليترهيد + بنود + مدفوع/وسيلة/صافي/باقي) — يعيد استخدام plumbing الطباعة. | WP5 | single | — | ✅ |
| WP7 | **Polish+ship:** sweep + CHANGELOG + KB + نشر /app + push + السويت الكامل. | الكل | single | — | ✅ |

**DAG:** BE سلسلة {WP1→WP2→WP3} (كلهم يلمسوا ServiceOrder/routes — **يُسلسلوا**) · FE: WP4={WP3} · WP5={WP1,WP2} · WP6={WP5}. WP5/WP6 يلمسوا cashier-collect → **يُسلسلوا**. ⚠️ **migrations (لو لزمت) ≥ `2026_07_08_000030`.** ⚠️ **ماتختبرش على DB حقيقي؛ phpunit=sqlite.**

**ترتيب التنفيذ:** WP1(BE، مزدوج المراجعة) → WP2(BE، مزدوج) → WP3(BE) ‖ WP4(FE queue، بعد WP3) → WP5(FE) → WP6(FE) → WP7.

## ⚖️ تعديلات مراجعة architect (بديل Fable) — مُلزِمة قبل WP1
- **R1 [CRITICAL]:** flip السطر (ordered→billed) + قيد Charge للـ ledger **مش** في PostReceiptJournalEntry — هما في **`WritePatientLedgerEntries.php:29`** (مفلتر `where status=ordered` حتى مع receipt-lines؛ flip `:55`، charge `:41-52`). **بدونه:** performed محصّل يعترف بالإيراد + كاش بس مايقلبش ولا ياخد charge → **ليدجر سالب + الأوردر مايوصلش billed + double-collection** (الـflip = حارس الاعتراف-مرة). **الإصلاح:** `:29` → `whereIn([Ordered, Performed])`. Pest إلزامي: collect performed → billed + charge واحد + ليدجر nets 0.000 + collect تاني مرفوض.
- **R2:** ٣ بوابات collect: re-check `:163-170` + selection `:184` (+empty-guard `:197`) → `whereIn[ordered,performed]`؛ **ومُصنّف repayment `:98-104`** لازم «فيه سطر collectable (ordered أو performed)» وإلا performed يتصنّف repayment → **خسارة إيراد صامتة**. `performed→billed` terminal صحيح. legacy fallbacks تفضل ordered-only (كل collect جديد يكتب receipt-lines `:310-317`).
- **R3:** close (WP1) وnet (WP2) مترابطان — **ابنِ net أولًا** (منقول لـWP1). close يستخدم **net-outstanding = patient_due − (paid_total − refunded_total)**؛ على gross يقرا سالب بعد أي refund. `refunded_total = ClinicCreditNote::where(service_order_id)->where(status,'posted')->sum(amount)`. refund يخفّض patient_due وnet-paid **بنفس المقدار** → **مفيش re-open needed**. all-cancelled: احرس `lines()->exists() && activeLines->isEmpty()` (draft فاضي يفضل open).
- **R4:** «collectable» عقد مشترك **٤ مواقع** (collect WP1 · queue-unsettled WP3 · FE-selectability WP5): **collectable ⇔ status ∈ {ordered, performed}؛ owed ⇔ net-outstanding>0 أو فيه سطر collectable.** WP3 filter مايظهرش closed/net-zero ولا يخفي partially_paid المدين. **WP1 مش «done» إلا لما WritePatientLedgerEntries + تست ليدجر-nets-0 أخضر.** حافة WP1(كامل)→WP5 صلبة (WP5 يعرض performed قبل R1 = فساد صامت).
- **R5:** dashboard net يخصم credit-notes **بالتاريخ** (grain الـreceipt)، company+branch عبر DataScope؛ **ماتستثنيش receipts مسترّدة** (مايوجدش double-net اليوم لأن مفيش شيء بيعمل net). paid_total gross في كل مكان؛ refunded_total+net derived فقط.
- **مؤكّد (غير بلوكر):** M2-moot آمن (ReceiptCollected جوّه txn، listener sync يرمي→rollback، مفيش pending جديد) · over-payment مرفوض (مفيش credit-balance يتكوّن) · **cross-session refund Z-attribution (L1)**: الاسترداد يخصم من جلسة المسترِد لا الجامع الأصلي — **ثغرة معروفة مؤجّلة، وثّقها** · auto-flip آمن مع R1 · zero-price performed مايعملش JE وهمي (يتخطّى amount≤0 + يقلب billed).

## المؤجّلات (م8)
- Z-report كامل بالفئات/denominations → م11 (نهاية اليوم). التأمين (H1-H4/M5/M6) → م9. رصيد المريض الدائن/over-payment → لاحقًا. M2 pending rescue → moot. طباعة BE-PDF → لاحقًا (FE-HTML الآن). **L1 cross-session refund Z-attribution** (الاسترداد يخصم من جلسة المسترِد لا الجامع) → ثغرة معروفة مؤجّلة.
