# 🧊 م1 (Service = backbone) — FREEZE / RESUME

**STATUS: PAUSED 2026-07-02** — الكلينك متوقف مؤقتًا للتحويل لشغل **elmadina (LIS)**. نرجع نكمّل م1 من هنا.

## أين وصلنا
- ✅ **م0 DONE + owner-APPROVED** (money guards C4/C3/C2/M4 + 20 route guards + settings→core.settings.view + remediation: 4 stranded receipts → JE#590-593 Posted + CN#1 healed + re-scan zero). Deployed /app + pushed. BE `6b7febb87` / FE `f0b2d08` (hazemdev). Backup `/home/moonui/pre-m0-remediation-moonui_dev_be-20260702.sql.gz`.
- 🔄 **م1 planning was in progress:** 4 read-only scouts DONE (maps in THIS dir: `scout-{a,b,c,d}.md`). Fable م1 advisory consult was SENT (scope + 6 decisions) — **its plan may still be pending capture; if not appended below, re-consult Fable with the questions in §Pending-Decisions.**

## كيف تستأنف م1 (cold-start)
1. اقرأ هذا الملف + `scout-{a,b,c,d}.md` (هنا) + KB `topics/clinic-execution-phases.md` (م1) + `topics/clinic-service-settings.md` (جدول F1-F10 — canonical).
2. لو خطة Fable مش ملقوطة تحت → أعد إرسال أسئلة §Pending-Decisions لـ Fable (advisor via architect-model agent — `advisor()` tool مقفول).
3. اكتب بريفات SDD → منفّذون Opus بالتوازي + بوابة native+Codex لكل task (Codex عبر stdin-diff: shell جواه مقفول، `max_user_namespaces=15000`).
4. **م1 = feature phase → migrations مسموحة** (عكس م0). migrate على `moonui_dev_be` بإذن (dev DB مش binlogged).

## نطاق م1 (المؤكد من الـ scouts)
**الهدف:** الخدمة تبقى العمود الفقري كامل: CRUD (add/edit/disable/delete موجود) + 4 أنواع (موجود) + 3 أوضاع إسناد للكشف + فئات + استيراد كتالوج lab/rad بنقرة (standalone) + خدمة إعادة (`parent_service_id`) + إصلاح مصفوفة التسعير + إصلاح كسور الـ wiring.
**IN scope:** BREAK 1 (جسر lab/rad) · BREAK 2 (التقارير) · BREAK 3 (base_price=0) · BREAK 4 (grade axis) · BREAK 6 (department_id في reception). 
**DEFERRED:** BREAK 5 (contract split axis) → م9 · BREAK 7 + ordering_mode → م7 · cashier → م8 · insurance caps → م9.

## الـ4 أعمدة migration على `clinic_services` (كلها additive nullable — آمنة)
1. `doctor_id` FK→`lab_doctors` nullOnDelete — لوضع الإسناد (أ) دكتور بعينه؛ **الساعات تُعاد استخدامها من `doctor_schedule_slots` الموجود** (لا تخزّن ساعات على الخدمة).
2. `assignment_mode` enum `specific_doctor|department_choose_at_booking|department_pool` — meaningful لـ consultation؛ (ب)+(ج) شغّالين اليوم على مستوى appointment/DoctorAssignmentService، (أ) مفقود.
3. `category` — **PENDING DECISION: flat string vs جدول `clinic_service_categories` مُدار (bilingual/orderable).** الفئات لاحقًا تسوق pick-grids.
4. `parent_service_id` self-FK nullOnDelete — خدمة الإعادة كـ variant مرتبط (لا صف مكرر مسطّح).

## كسور الـ wiring (Scout D — بالـ file:line في scout-d)
- **BREAK 1 (قلب standalone):** جسر `clinic_services.lis_investigation_id/rad_procedure_id` **غير مقروء أبدًا** — طلبات lab/rad الحقيقية تتخطى الكتالوج وتُنتج سطور `clinic_service_id=null` → تسقط من التقارير. المقاربة **PENDING DECISION** (A: استهلاك الجسر / B: السطر يحمل clinic_service_id ثابت / both؛ وحدّ م1-vs-م7).
- **BREAK 2:** `ClinicReportService::revenueByDepartment` INNER JOIN على `clinic_service_id` → يسقط سطور lab/rad (`ClinicReportService.php:30-31`).
- **BREAK 3:** `SeedBaseServicePrice` يتخطى `base_price=0` بصمت → سعر صفر بلا إشارة (`SeedBaseServicePrice.php:19`).
- **BREAK 4:** السطر يحمل `doctor_grade_code` لكن `PricingResolver` مفيهوش grade scope (grade يؤثر على split+report فقط). **PENDING: نضيف grade pricing scope ولا نوثّق «التسعير بالدكتور مش الدرجة»** (مبدأ المالك = 3 محاور base→dept→doctor، فالميل للتوثيق).
- **BREAK 6:** `reception-order.component submitLines()` ما بيبعتش `department_id` → محور سعر القسم ما بيشتغلش خارج CreateVisit.

## مصفوفة التسعير (Scout B — file:line في scout-b)
- **شيل `buildPlaceholderSplit`** (fake 62.5/20/10%، `pricing-matrix.component.ts:62-73`) — بيغطي محرك RevenueSplitService حقيقي **مفصول**. **PENDING: نوصّل preview حقيقي دلوقتي (endpoint جديد فوق `RevenueSplitService::resolveScheme`+`computePartyAmounts` [نعمّمها public]) ولا نشيل الوهمي بس ونأجّل الـ preview لـ م9.**
- **أعمدة العقود «—» دائمًا:** `axisToColKey` مفيهوش contract branch + `loadPriceAxes` ما بيجيبش أسعار العقود. BE جاهز: `GET payer-contracts/{id}/service-prices` (bulk) + `PUT` (upsert) — غير موصولين FE. (لاحظ: body = `approved_price` مش `price`.)
- **bug الـ 3-axis:** (1) حفظ dept ما بيبعتش `department_id` (`colKeyToUpsertRow` ts:357) → يتصادم مع base. (2) `ServicePriceChanged` dispatched بلا listener → تعديل Base في المصفوفة ما بيرجعش لـ `clinic_services.base_price` (drift). 
- **doctor picker:** `DoctorController::index` مفيهوش `search`/`department_id` params + FE بيعمل `listAll` لكل الأطباء → **يكسر قاعدة الـ1000 طبيب.** م1 يضيف الـ params + FE يوقف listAll.

## Pending Fable decisions (أسئلة الاستشارة — أعد إرسالها لو خطة Fable مش ملقوطة)
1. مجموعة الـ migration (الـ4 أعمدة) — موافقة/حذف/إضافة؟
2. **نموذج الفئة:** flat string vs جدول مُدار؟ (وهل قرار مالك؟)
3. **مقاربة BREAK 1:** A (استهلاك الجسر) / B (clinic_service_id على السطر) / both؟ وحدّ م1-vs-م7؟
4. **BREAK 4:** grade pricing scope ولا document-only؟
5. **مصفوفة التسعير:** preview حقيقي دلوقتي ولا شيل الوهمي + تأجيل الـ preview لـ م9؟
6. **Task DAG:** T1 BE schema → parallel T2 BE wiring + T3 BE import + T4 FE services screen + T5 FE pricing matrix.

## ⚠️ قواعد ثابتة عند الاستئناف
- Opus ينفّذ · Fable advisor (agent بموديل fable) · SDD منفّذ→مراجع→بوابة native+Codex · **skills frontend-design لكل شاشة**.
- نشر: moonui `/app` فقط · ⛔ لا cp لعملاء/staging · migrations بإذن على moonui_dev_be · CHANGELOG ثنائي · **سجل التقدم في KB clinic-execution-phases.md + HTML يتحدث مع كل خطوة**.

---
# ✅ خطة Fable لـ م1 (محسومة 2026-07-02) — جاهزة للتنفيذ عند الاستئناف

**المبدأ الحاكم:** plan line 54 هو عقد النطاق. م1 = **الكتالوج كامل، قابل للتهيئة، ومتّسق داخليًا (والتقارير ما تسقّطش حاجة)**. م1 ≠ كيفية تنفيذ طلب lab/rad (ده معمار النية/وضع البيع = م4/م7). مبدأ المالك «الطبيب بيطلب، الشراء/التنفيذ مختلف» يؤكد إن fulfillment مش شغل الأرضية.

## النطاق المؤكد
**IN م1:** الـ4 قدرات الناقصة (إسناد أ · فئات · parent_service_id · استيراد) + BREAK 2 (تقارير LEFT JOIN) + BREAK 3 (إشارة unpriced) + BREAK 6 (محور القسم من reception) + مصفوفة التسعير (شيل الوهمي + أعمدة عقود + bug الـ3-axis الاتنين + doctor picker مفلتر) + back-sync لـ `ServicePriceChanged→base_price`.
**DEFERRED:** BREAK 1 fulfillment→م4 · BREAK 4 grade→document-only · BREAK 5→م9 · BREAK 7+ordering→م7 · live split preview + scheme UI→م9.

## Ruling 1 — Migration set (كلها additive nullable، آمنة على moonui_dev_be)
1. `doctor_id` FK→`lab_doctors` nullOnDelete (مود أ؛ الساعات من `doctor_schedule_slots`).
2. `assignment_mode` enum `specific_doctor|department_choose_at_booking|department_pool` (لـ consultation؛ `requires_doctor` منفصل orthogonal — سيبهم الاتنين).
3. `parent_service_id` self-FK nullOnDelete.
4. `category_id` FK → **جدول جديد `clinic_service_categories`** (مُدار، مش flat string).
**ماتضيفش:** عمود `is_unpriced` (computed مش stored).

## Ruling 2 — الفئات = **جدول مُدار** (مش string). سبب حاسم: الاستيراد بنقرة بيحل الفئات بالكود عبر `firstOrCreate` → محتاج جدول بأكواد. clone `LisInvestigationCategory` **بس صحّح `unique(company_id,code)`** (مش global-unique زي غلطة LIS). ⚠️ escalate سطر واحد للمالك (بيضيف شاشة فئات صغيرة).

## Ruling 3 — BREAK 1: مرونة طبقة التقارير IN، fulfillment OUT→م4
- **IN (report/consistency):** `ClinicReportService::revenueByDepartment` من INNER JOIN → **LEFT JOIN + catalog_ref union** علشان سطور lab/rad (`clinic_service_id=null`) ما تسقطش. (`ClinicReportService.php:30-31`.)
- **OUT→م4:** ماتعدّلش `OrderLabFromEncounter`/`CreateRadOrder` تحط `clinic_service_id` — ده بيجرّ أسئلة م4 (خدمتين→تحليل واحد؟ standalone بلا investigation_id؟).
- **متطلب forward-compat على م1:** الсхема + الاستيراد **يحافظوا ويملّوا `lis_investigation_id`/`rad_procedure_id`** لما معروفة (علشان م4 يستهلكها). ماتشيلهاش.
- ⚠️ escalate توقّع للمالك: م1 = lab/rad service تبقى catalog+priced+**reported**؛ التنفيذ الكامل م4/م7 (عشان تست م1 ما يتعايرش غلط).

## Ruling 4 — BREAK 4: **document-only** (grade مش محور تسعير؛ التسعير 3 محاور base→dept→doctor؛ grade يبقى محور split/report). ماتضيفش grade scope. ⚠️ escalate تأكيد خفيف.

## Ruling 5 — مصفوفة التسعير
- **شيل `buildPlaceholderSplit` + call + P1 notice — إلزامي** (الـ`✓ ties out` وهمي رياضيًا). استبدل الـ rail بـ **empty state صادق**: «لا preview — هيّئ revenue-split schemes (م9)».
- **أجّل live preview + scheme UI → م9.**
- **أعمدة العقود IN** (BE جاهز: `PayerContractController::servicePrices/upsertServicePrices`). القرار: **overrides صريحة فقط** (dash=بلا override؛ click→الـ rail يعرض coverage عبر `coverage()` الموجود). body=`approved_price` مش `price`.
- **bug الـ3-axis IN:** (a) حفظ dept يبعت `department_id` (plumb في الـ debounce payload); (b) وصّل **listener `ServicePriceChanged→base_price` back-sync** (base scope، idempotent — فيه قرّاء مباشرين لـ base_price زي FHIR `unitPrice()`).
- **doctor picker IN:** أضف `search`+`department_id` لـ `DoctorController::index` + FE يوقف `listAll`.
- **BREAK 3:** `is_unpriced` boolean computed على الـ resource (لا base row AND base_price≤0 AND لا override).

## Ruling 6 — Task DAG (معدّل: كاتب واحد لكل repo)
**Step 0 — اقفل API contracts** (جزء من مخرجات T1؛ ده اللي بيخلي FE+BE بالتوازي).
- **BE track (منفّذ واحد، تسلسلي):** T1 schema+model (3 أعمدة + جدول categories + category_id + relations + validation [self-parent + same-service_type parent guards] + resources) → T2 wiring (BREAK 2 LEFT JOIN، BREAK 3 computed unpriced، `ServicePriceChanged` back-sync، dept `department_id`، `DoctorController` search+department_id) → T3 import (port `LisCatalogService`→`ClinicCatalogService`، JSON كتالوج مشتق + قائمة rad منسّقة [مفيش وحدة موجودة]، `firstOrCreate` categories بـ (company_id,code)، احترم `guardBridgeIds`، **ملّي bridge ids**، clone password-gate + QueryException→422). بوابة review+Codex بعد كل واحدة.
- **FE track (منفّذ واحد، تسلسلي، بالتوازي مع BE):** T4 services screen (assignment-mode select + mode-أ doctor picker server-search dept-filtered + category picker/filter/column + category CRUD screen [clone lis-investigation-categories ~219 سطر] + parent picker same-type exclude-self + **debounce على البحث** + import trigger) → T5 pricing matrix (شيل fake + empty rail صادق + أعمدة عقود [bulk GET / PUT approved_price] + dept يبعت department_id + doctor picker مفلتر).
- **مراجع واحد بعدسة money/pricing عبر التراكين.** Integration gate: full Clinic Pest + FE build → **تست المالك:** add خدمة لكل نوع → edit → disable → delete → استيراد بنقرة → صنّف → أكّد 3-axis + أسعار العقود تظهر + الـ split rail فاضي بصدق (بلا fake).

## ⚠️ 4 Escalations للمالك (كلها خفيفة — تُسأل عند استئناف م1)
1. **الفئات = جدول مُدار** (recommend yes؛ لازم للاستيراد المحمول).
2. **حدّ cascade الـ lab/rad في م1** = catalog+priced+reported؛ التنفيذ م4/م7 (معايرة التست).
3. **grade مش محور تسعير** (تأكيد مطابقة الـ3 محاور).
4. **الـ split rail empty state صادق في م1** (الـ preview الحي م9).

الـ3 migrations + الجدول الجديد ما يحتاجوش بوابة مالك خاصة (additive/nullable/no-backfill — نفس فئة أمان م0). القرار المالك الوحيد في الсхема = جدول الفئات (#1).

## 🚩 Red flags (Fable ماسك الخط عليها)
- ماتعدّلش order actions للـ bridge في م1 (بيجرّ م4). · ماتضيفش grade pricing scope. · ماتبنيش split-preview/scheme UI (شغل م9). · ماتسيبش الـ fake split «disabled» بس — **احذفه** (الكذبة أسوأ من empty state). · clone `LisInvestigationCategory` بس صحّح global-unique→`(company_id,code)`.

**الانطلاق عند العودة:** اقفل contracts → BE T1→T2→T3 + FE T4→T5 بالتوازي، بوابة بعد كل واحدة، integration gate + تست المالك يقفل م1.
