# م4 (الحجز والدفع) — عقد تنفيذ Fable المُلزِم + ليدجر التقدم الحي

## ✅ م4 مكتملة — منشور /app + hazemdev (2026-07-07، بانتظار تست المالك)
**كل الـ11 WP ✅** بمراجعة native+Codex (والمالي بـFable). نشر: /app `main-MBM2TDHN.js`؛ push hazemdev (BE `24f924b63`/FE `3404688`)؛ CHANGELOG م4 ثنائي؛ **الاختبار الشامل 555 نجاح / 1 فشل pre-existing موثّق (ClinicRoomTest HRM projection — مش من م4)**؛ صفر regression؛ إعدادات م4 مزروعة على moonui_dev_be (`allow_partial_payment` default=true). SHAs: WP1 `25fd9de50` · WP2 `a9cbe7798`/`9cb6836` · WP3 `2c5dba842` · WP4 `e193806e8` · WP5 `2f1e78f0a` · WP6 `7cabb6f78` · WP7 `e3a76f14e`/`c5f1058` · WP8 `665eab675`/`76b0b84` · WP9 `44e5be44a`/`d0004b7` · WP10 `99b06cd31`/`3404688`. **⛔ المالك: merge→main + ship + تست.** مؤجّلات مسجّلة أدناه (follow-ups + tech-debt).


> **نقطة الرجوع لتنفيذ م4.** المالك فوّض التنفيذ الذاتي الكامل (Opus ينفّذ، Fable يحسم، لا وقفة للمالك حتى تشتغل المرحلة). العقد أدناه بيناني على حقائق كود متحقَّقة. **اقرأ الليدجر أول حاجة عند العودة.**

## 📊 ليدجر التقدم (يتحدّث مع كل WP)
| WP | المحتوى | يعتمد | مراجعة | ⬜ |
|----|---------|-------|--------|--------|
| WP1 | `scheduling_mode` wiring (BookAppointment + open path + null-slot + slot-desync fix) | — | native+Codex | ✅ `25fd9de50` |
| WP2 | Grade price scope (migration + PricingResolver precedence doctor>grade>dept>base) | — | native+Codex | ✅ BE `a9cbe7798`/FE `9cb6836` |
| WP3 | `FollowupResolver` + enforcement في AddServiceOrderLine/CreateVisit | WP2 | native+Codex | ✅ `2c5dba842` |
| WP4 | Quote endpoint `POST /clinic/visits/quote` (grade+followup+CoverageService dry-run) | WP2+WP3 | native+Codex | ✅ (بعد كوميت) |
| WP5 | Partial payment core (guard relax + partially_paid + ledger remainder + repayment) | — | native+Codex (الأثقل) | ✅ `2f1e78f0a` |
| WP6 | `sent_to_cashier_at/by` + `payment_disposition` على CreateVisit | — | single | ✅ (بعد كوميت) |
| WP7 | create-visit payment UX (reseed subtotal + default_payment_method + two-path + partial confirm) | WP5+WP6 | single | ✅ BE `e3a76f14e`/FE (بعد) |
| WP8 | Dept-first doctor pickers (3 call sites → `/clinic/doctors?department_id=`) | — | single | ✅ BE `665eab675`/FE `76b0b84` |
| WP9 | Booking form fixes + scheduling_mode UX (open: free datetime؛ slots: capacity badge) | WP1 | single | ✅ BE `44e5be44a`/FE `d0004b7` |
| WP10 | Honest-price panel (quote endpoint: insured split + grade + «إعادة» badge/revert) | WP4 | single | ✅ BE `99b06cd31`/FE `3404688` |
| WP11 | Integration + deploy /app + push hazemdev + CHANGELOG + KB | الكل | self+smoke | ✅ |

**DAG:** ابدأ WP1 ‖ WP2 ‖ WP5 ‖ WP6 ‖ WP8 (مستقلين). WP3→WP4 بعد WP2؛ WP7 بعد WP5+WP6؛ WP9 بعد WP1؛ WP10 بعد WP4. WP11 آخرًا. المسار الحرج: WP2→WP3→WP4→WP10.

## 0. حسم توتر النطاق
`clinic-execution-phases.md` = الخطة المُلزِمة (KB بيقول كده صراحةً)؛ `clinic-service-settings` ترقيمه الداخلي أقدم — عند التعارض الخطة الـ12 تكسب. تست المالك لـم4 حرفيًا «ادفع أقل → أكّد الآجل → شوفه في رصيد المريض» → م4 لازم يعدّي الجزئي→دين. المحاسبة الحالية (Dr AR كامل / Cr إيراد كامل + Dr كاش مدفوع / Cr AR مدفوع) **أصلًا** بتسيب الباقي كـAR debit حي لو خفّفت الحارسين → فالفجوة 8 = **تخفيف حارس + UX تأكيد**، مش ميكانيكا ثقيلة. م8 يحتفظ بـ: شاشة الطابور، money-truth/refunds، performed lifecycle، session hardening، طباعة الإيصال، credit الفائض.

## 2. القرارات المحسومة (defaults — بلا سؤال المالك)
- **a. scheduling_mode = WIRE first-class** (لا تلغيه ولا تلغي allow_overbooking). `slots`=جدول مطلوب + reserveSeat زي دلوقتي + allow_overbooking = soft-cap modifier لوضع slots فقط. `open`=بلا جدول/سلوت/سعة: BookAppointment يقرا الإعداد مرة فوق وفي open **يتخطّى slot resolution + reserveSeat**، `schedule_slot_id=null` (nullable أصلًا — **صفر migration**). allow_overbooking يُتجاهل في open. AvailabilityService يفضل mode-ignorant داخليًا. + slot-desync fix في نفس الـWP. تأكّد MarkArrived/walk-in يتحمّلوا null slot.
- **b. Grade scope = BUILD.** migration يضيف `grade_id` (nullable FK لـ`clinic_doctor_grades`) على جدول service-prices؛ precedence **doctor > grade > dept > base**؛ grade من `doctor.grade_id` وقت الحل؛ يفضل يختم `doctor_grade_code` كـsnapshot؛ مدّد شاشة مصفوفة التسعير (م1) بصف grade (أعد استخدام نمط الصف، ما تعيدش تصميم).
- **c. Revisit auto-pricing = BUILD (server-authoritative، auto-substitution مرئي).** `FollowupResolver`: عند إضافة S، لو فيه R بـ`parent_service_id=S` والمريض عنده سطر سابق لـS أو R خلال `clinic.followup_window_days` → استبدل R تلقائيًا + badge «إعادة» + revert بضغطة. المطابقة: نفس المريض + نفس الخدمة، doctor-agnostic company-wide. الفرض server-side في quote + AddServiceOrderLine/CreateVisit (client يبعت `apply_followup:false` للتجاوز؛ السيرفر يعيد الحساب دائمًا). صفر أعمدة جديدة.
- **d. Insured live total = BE quote endpoint** (مش after-submit). `POST /clinic/visits/quote`: patient+lines → per-line resolved price (grade-aware) + followup subs + coverage split (CoverageService: coverage+visit-cap+annual-cap) + totals patient_due/insurer_share. FE debounced. ده عمود «السعر الصادق» لكل المرحلة، يعاد استخدامه م8/م9. additive (ما تعيدش هيكلة مسار الكاش).
- **Partial (gap 8) — العقد الدقيق:** setting جديد `clinic.allow_partial_payment` (bool، default **true**، عبر seeder). **لا** تحمّل `allow_credit_balance` (محجوز لـم8 overpayment). `CollectReceptionReceipt` يقبل `allow_partial:true` → الحارس يبقى `0 < tenderSum ≤ patientDue` (tender>due يفضل مرفوض)؛ بدون الفلاج = byte-identical للنهاردة. الإيصال يخزّن patient_due كامل + paid_total=المدفوع + status→`partially_paid`. `PostReceiptJournalEntry` يخفّف `billableTotal===paidTotal` لـ`paidTotal≤billableTotal` **فقط لو partial**؛ balanced-JE assertion (Σdebit===Σcredit) تفضل. Ledger: charge=full/payment=paid (المشتق يظهر الباقي — صفر migration). **Repayment**: receipt تاني على نفس الأوردر يقفلها — Pest lifecycle كامل. FE: تعديل المبلغ تحت due يفعّل dialog «الباقي X آجل على المريض؟» → التأكيد بس يبعت `allow_partial:true`. مفيش partial صامت.

## 4. Owner-escalation defaults (proceed)
1. partial default true، gate-able per client؛ أي user بصلاحية collect يقدر (بالتأكيد)؛ صلاحية مخصّصة = م8. 2. tender>due مرفوض دائمًا في م4. 3. zero-paid = مسار pay-later الموجود مش partial. 4. revisit match: نفس المريض+الخدمة، doctor-agnostic، company-wide، نافذة من آخر زيارة. 5. grade precedence: doctor>grade>dept>base. 6. open capacity: لا محدودة (تست المالك «عشرة في نفس المعاد»). 7. scheduling_mode default=slots fleet؛ اقلب co4=open يدويًا للتست. 8. allow_overbooking: يفضل، موثّق كـslots-only modifier.

**قيود قائمة:** كله additive (صفر تغيير سلوك بالفلاجات off — اكتب regression byte-identical)؛ مدّد CoverageService/PricingResolver/CollectReceptionReceipt، ما تعيدش بناء. deferred للتسجيل عند إغلاق المرحلة: queue UI، refunds/money-truth، performed lifecycle، printing، overpayment credit، debtors report.


## ⚠️ ملاحظات مصطادة أثناء التنفيذ (للـintegration/الترحيل)
- **WP6 tech-debt (بسيط):** تمييز «no open cashier session» بالـ`str_contains` على رسالة RuntimeException — نظّفه بـexception مخصّص `NoCashierSessionException` في CollectReceptionReceipt لاحقًا.
- **WP5 follow-ups (Codex MEDIUM، غير حاجب — لـWP11/م8):** (1) الليسنر `PostReceiptJournalEntry` بيعتمد على `billableTotal<=0.0` لتمييز repayment مع تعليق مضلّل «unreachable لغير-partial» — اجعل فرع repayment صريحًا. (2) إيصالات `partially_paid` غير قابلة للإلغاء (`VoidReceptionReceipt` يحرس `status==='paid'`) — اسمح بإلغائها (يحتاج تست عكس القيود).
- **WP1 side-note (سلوك قائم، مش من WP1):** عدّاد المقاعد `AvailabilityService::seatsTaken` بيعُدّ `whereNotIn('status',['cancelled','no_show','done'])` — الموعد بيعُدّ نفسه، فموعد وحيد على سلوت سعة-1 + overbooking off **مايقدرش يوصل** (409 'Slot full' لأن 1>=1)، وآخر مريض بيملأ السلوت لسعته مايقدرش يوصل. **يتراجع في م8/integration كـbug منفصل.**
