# Fable Ruling — م2 «الإعدادات والصلاحيات المؤسِّسة» (2026-07-02)

Binding sources: `clinic-execution-phases.md` L64-68 (م2 text = authority) · `clinic-service-settings.md` §مخطط الإعدادات (canonical schema; its internal م-tags are the OLD numbering — ignored as phase authority) · `clinic-principles.md` · scout maps A-D.

Governing reading of م2: this is a **settings + roles + FE-guard phase**. "Define now, effect later" applies ONLY to keys the binding م2 text names. Anything whose *value shape* can't be decided without building its consumer is deferred to the consuming phase. No behavioral wiring of modes in م2 (that is م4/م7/م8/م10).

---

## Ruling 1 — D1: Settings scope (the exact matrix)

### Canonical display groups (exactly these 6; regroup the existing 4 rows into them)
`clinic_financial` · `clinic_modes` · `clinic_operations` · `clinic_print` · `clinic_prescription` (placeholder card) · `clinic_clinical` (placeholder card).
Existing rows re-homed via seeder `updateOrCreate`: `ar_account_id`/`revenue_account_id` → `clinic_financial`; `ordering_mode` → `clinic_modes`; `allow_overbooking` → `clinic_operations`. (`clinic_accounting`/`clinic_general` retire.)

### 💰 clinic_financial — IN (validation is the deliverable)
| Key | Type | Default | م2 action |
|---|---|---|---|
| `clinic.ar_account_id` | integer (account) | null | exists — add fail-fast (rules in Ruling 3) |
| `clinic.revenue_account_id` | integer (account) | null | exists — add fail-fast |

**DEFERRED:** refund/credit-note account and cashier over/short account → **م8** (their posting logic lives there; an unconsumed nullable GL field on a screen whose test is "empty GL → reject" muddies the acceptance test). When م8 defines them they join the same required-GL validator set automatically (registry-driven).

### 🏗️ clinic_modes — ALL 4 modes IN (define + persist + validate; effects land later)
| Key | Type | Allowed | Default | Effect phase |
|---|---|---|---|---|
| `clinic.ordering_mode` | enum | `intent\|immediate` | `intent` | ✓ wired already |
| `clinic.lab_mode` 🆕 **«الإعداد الأساسي»** | enum | `integrated\|standalone` | `integrated` | م7 (+م4 partially) |
| `clinic.rad_mode` 🆕 | enum | `integrated\|standalone` | `integrated` | م7 |
| `clinic.pharmacy_source` 🆕 | enum | `products\|standalone_formulary` | `products` | م7 |
| `his.mode` — **promote** to a real definition 🆕 | enum | `single_clinic\|center` | `single_clinic` | already read (`DoctorScheduleSlotRequest:58`) |

Calls locked here:
- **Values are `integrated|standalone`** (per canonical §مخطط الإعدادات L160), NOT `lis_integrated` (the older his.md L4 wording) — `lis_integrated` is wrong for rad (our rad flow isn't LIS) and the two keys must share one vocabulary for م7's shared picker.
- **`his.mode` ≠ `scheduling_mode` — orthogonal axes.** `his.mode` = facility scale (single clinic vs multi-clinic center → room requirement); `scheduling_mode` = booking discipline (capacity slots vs free booking). Both exist as separate keys. **Keep the key string `his.mode`** (zero read-site churn; tests already write it) but register its `SettingDefinition` with `module='clinic'` so it renders on the clinic screen. Do NOT rename to `clinic.mode` in م2.
- Defaults = current behavior (`integrated`, `single_clinic`) — flipping a fresh definition must never change a live install's behavior.
- **م2's visible "effect" of lab_mode (owner test #1) is deliberately minimal:** (a) value persists + survives reload, (b) descriptive helper text on the setting itself, (c) **one informational mode badge on the م1 services screen** (standalone → emphasize the import-catalog CTA + «كتالوج التحاليل ملك العيادة»; integrated → hint «التحاليل تُطلب من كتالوج LIS مباشرة»). NOTHING else switches. Any order-routing/picker-source change = م7 scope violation.

### 📅 clinic_operations — IN
| Key | Type | Allowed/Range | Default | Effect phase |
|---|---|---|---|---|
| `clinic.allow_overbooking` | boolean | — | true | ✓ wired (add helper text: applies only in `slots` mode) |
| `clinic.scheduling_mode` 🆕 | enum | `slots\|open` | `slots` | م4 |
| `clinic.default_payment_method` 🆕 | enum | **exactly the existing receipt payment-method enum values** (implementer extracts from `ReceptionReceipt`/`CollectReceptionReceipt` — do NOT invent values) | `cash` | م4 |
| `clinic.allow_credit_balance` 🆕 | boolean | — | true (owner explicit) | م8 |
| `clinic.followup_window_days` 🆕 | integer | 0–365 | 14 (م10 test says «أسبوعين») | م10 (+م4 revisit price) |
| `clinic.dashboard_top_services_count` — **promote** 🆕 | integer | 1–50 | 5 | already read (`ClinicDashboardService:142`) |

**DEFERRED:** `clinic.visit_stations` → **م5** (binding م5 text explicitly owns it; its value shape should be decided with the station screens). Daily queue reset → **م5** (the "م2" tag in clinic-service-settings is the OLD numbering's Queue phase — confirmed false positive). Follow-up **price policy** → NOT a setting at all: the revisit price is the م1 `parent_service_id` variant's own price — a second policy key would create a duplicate source of truth. Only the window (days) is a setting.

### 🖨️ clinic_print — MINIMAL SHELL IN (the binding text says «طباعة (تعريفات)» — definitions, no consumer yet)
| Key | Type | Default |
|---|---|---|
| `clinic.print.header_text` 🆕 | string | '' |
| `clinic.print.footer_text` 🆕 | string | '' |
| `clinic.print.show_logo` 🆕 | boolean | true |
| `clinic.print.show_diagnosis` 🆕 | boolean | true |
| `clinic.print.paper_size` 🆕 | enum `a4\|a5` | `a4` |
Modeled on the LIS print precedent (`LabSettingDefinitionSeeder` report keys) so م10 consumes rather than redesigns. **EXCLUDED from م2:** margins/fonts detail (decided in م10 when real templates exist) and **per-doctor signature — that is per-doctor DATA (doctor file, م3/م10), not a company settings row. Do not model it as a setting.**

### 💊 clinic_prescription — PLACEHOLDER CARD ONLY
No keys in م2. Reconciliation ruling: the "drug source" item in the prescription group **IS** `clinic.pharmacy_source` — one key, and it lives in 🏗️ per the binding م2 text (no mirroring across groups). Everything else in this group (per-drug dose/frequency/duration defaults, per-doctor favorites) is **per-drug/per-doctor DATA (م7 formulary tables), not settings rows.** The card renders with a bilingual description + «تُستكمل مع مرحلة الروشتة (م7)».

### 🩺 clinic_clinical — PLACEHOLDER CARD + LINK ONLY (confirmed OUT)
Engine = م6 (binding). Card renders description + a link to the existing history-questions management screen (if the FE screen from P4 exists; otherwise description only). Zero engine work in م2.

**Net new definition rows: 12** (4 modes incl. his.mode promotion − ordering exists → 3 new + 1 promote; 4 operations new + 1 promote; 5 print) + 2 group re-homes. All six groups visibly organized on screen (4 populated + 2 placeholder cards) → the phase deliverable «مجموعات الإعدادات الست منظمة في الشاشة» is met honestly.

---

## Ruling 2 — D2: Storage & screen model

**(a) BE — YES: create `Modules/Clinic/database/seeders/ClinicSettingDefinitionSeeder.php`** (clone the LIS seeder's `run()`/`getDefinitions()` `updateOrCreate` shape), registered in `ClinicDatabaseSeeder`. **MOVE the existing 4 clinic definitions out of Core's `SettingDefinitionSeeder` into it** (idempotent — rows already exist, updateOrCreate re-homes their groups). `seedDefaultAccountSettings`'s GL auto-wire maps STAY in Core (value wiring, not definitions). From م2 onward, clinic settings have one module-owned home.

**(b) FE — HYBRID, not a hand-built rewrite.** The owner principle «عميل جديد = صف في seeder يظهر بلا كود FE» is load-bearing — a fully hand-built per-group form would break it. Rule:
- Keep the data-driven renderer as the base (definitions still auto-appear).
- ADD a structured **group registry** in the component: the 6 canonical groups with fixed order/icon/bilingual title/description + placeholder-card support for the 2 empty groups (fixes the latent SCSS group-key mismatch too).
- ADD a light **per-key decoration map**: helper text, `required` flag (GL accounts), control variant (the 4 mode enums render as radio-cards with descriptions instead of bare selects — this is the «الإعداد الأساسي» moment, make it read like one), numeric min/max.
- Unknown/new keys with no decoration fall through to today's generic rendering — principle preserved.
- Save path switches to the atomic bulk endpoint (Ruling 3) with inline per-field 422 errors. The per-key `forkJoin` path is REMOVED from the clinic screen (other modules' screens untouched — out of scope).

---

## Ruling 3 — D3: Validation / fail-fast

**Architecture: a Core-level validator registry + ONE new bulk endpoint. Not a clinic-private settings API.**
- New `Modules/Core/app/Support/SettingValidatorRegistry` (mirror of the existing `PermissionDependencyRegistry` precedent): modules register a validator class per key-prefix at boot. Clinic registers `Modules/Clinic/app/Support/ClinicSettingValidator` (covers `clinic.*` + `his.mode`).
- `SettingsService::set()` consults the registry → **single choke point**; even direct per-key PUTs get validated.
- New endpoint **`PUT /core/settings/bulk`** (`SettingController::updateBulk`, gate `core.settings.manage`): body `{settings:[{setting_key, value}...]}`. **Validate ALL first — collect the complete error map — write NOTHING on any failure; on success write all inside one `DB::transaction`**, flush the company cache once after commit. Errors return 422 keyed by `setting_key`.
- Fix the existing hole: `SettingsService::set()`'s `InvalidArgumentException` (enum guard) must surface as a 422 ValidationException, never a 500 — the owner test says "REJECT", a 500 fails the test.

**Exact rules in `ClinicSettingValidator`:**
1. **GL accounts** (`*_account_id` in the required set: ar, revenue — refund/variance join in م8): value REQUIRED non-empty, integer, must reference an existing non-deleted `accounts` row of the acting company with `account_type='detail'` (postable leaf). Empty/`0`/null/nonexistent/header → 422 «الحساب مطلوب — اختر حسابًا فرعيًا صالحًا». (Save-time layer; م0's C4 posting-time guard remains the second layer.)
2. **Enums**: must be in the definition's `allowed_values` (existing check, now 422).
3. **Integers**: strictly numeric + per-key min/max (`followup_window_days` 0–365, `dashboard_top_services_count` 1–50). **Global floor: no negative value on any clinic integer setting.** «نسبة سالبة ترفض» is satisfied structurally: no % keys exist in م2; when % keys arrive (م9 insurance) they inherit 0–100 bounds from this same registry — state this in the WP so the gate doesn't flag it as missing.
4. **Booleans**: strict cast (`true/false/1/0` only).
5. **Undefined keys**: the bulk endpoint REJECTS any key with no `SettingDefinition` (422 «إعداد غير معرّف») — closes the write-anything hole for the clinic path. (Global `set()` behavior for other modules unchanged — out of scope.)

---

## Ruling 4 — D4: Roles & presets

**(a) YES — clone LIS wholesale.** BE: `ClinicRoleController` mirroring `LabRoleController` (`index/catalog/store/update`) + `ClinicRoleAssignment::isAssignable` (clinic-confined role listing). Generalize `RoleSaveService`'s `lisOnly` into a prefix-confined mode (`restrictToPrefix: 'lis'|'clinic'`) **keeping `lisOnly` back-compat with regression tests** — clinic saves touch only `clinic.*` grants, other-module grants on the same role preserved. FE: `clinic-roles.component` cloning `lis-roles.component` (preset chips + dependency auto-tick from `catalog.dependencies`), route **`/his/roles`**, nav item under clinic settings area.
**Permission gate: `core.roles.view` / `core.roles.create` / `core.roles.update` — NO new permission.** (Consistent with م0's "settings on core.settings.*" precedent; the clinic roles screen is a friendlier veneer over the same capability, so same gate = zero privilege surface change. Do NOT mint `clinic.settings.*` or `clinic.roles.*`.)

**(b) Presets — 6 (the owner's 5 + keep the existing «طبيب العيادة»; retire `clinic_setup`, absorbed by manager).** Lists below are ANCHORS — `ClinicPermissionDependencies::expand()` auto-adds `.view` prerequisites. All in `ClinicPermissionDependencies::presets()`:

| Preset | Anchor permissions |
|---|---|
| **استقبال (reception)** | `clinic.visits.create` · `clinic.appointment.create` · `clinic.appointment.update` · `clinic.availability.view` · `clinic.queue.view` · `clinic.dashboard.view` · `clinic.intents.view` · `clinic.intents.decide` · `clinic.orders.create` · `clinic.orders.update` · `clinic.payments.create` («حصّل الآن») · `clinic.coverage.compute` (insured booking shows the share) · `clinic.results.external` (attach external results at execution) |
| **تمريض (nursing)** | `clinic.vitals.record` · `clinic.queue.view` (the arrivals worklist — per principles L70) — NOTHING else |
| **دكتور مساعد (assistant-doctor)** | `clinic.history.record` · `clinic.queue.view` — nothing else |
| **كاشير (cashier)** | `clinic.payments.create` · `clinic.payments.void` (refunds are the cashier's job per م8 workflow) · ALL `clinic.cashier.*` (session open/close/view — whatever exists in the catalog) · `clinic.orders.view` · `clinic.ledger.view` |
| **مدير (manager)** | ALL 85 `clinic.*` + `core.settings.view` (sees settings; does NOT get `core.settings.manage` nor `core.roles.*` — those stay owner/admin) |
| **طبيب العيادة (doctor — existing, kept)** | current anchors (`encounters.update/sign`, `vitals.record`, `history.record`, `diagnosis.record`, `intents.create/view`, `prescriptions.create`, `queue.view`) **+ `clinic.lab-orders.create` + `clinic.rad-orders.create`** (so the preset works under `ordering_mode=immediate` too) **+ `clinic.results.external`** (owner: doctor enters results in ALL modes) |

**Mandatory WP step:** E2E-verify each preset against the real screens it serves (create a user per preset on co4, walk its workflow) — if any screen needs a non-`clinic.*` permission (e.g., patient creation from create-visit), the implementer documents it and mirrors whatever mechanism LIS roles use for their base non-lis grants. Presets validated on paper only = gate failure.

**(c) TEMPLATES, not seeded roles.** Presets are UI templates (owner clicks preset → anchors tick → role is created tenant-scoped), exactly the LIS pattern. Do NOT seed 5 roles in `RolePermissionSeeder` (avoids global-role pollution, protected-roles friction, rename fights). The owner test itself exercises the template flow: creating the «تمريض» role from the preset IS test step 3's setup.

---

## Ruling 5 — D5: appCan sweep = FULL, in م2

The binding text says «على أزرار الكتابة كلها», and — decisive point — **owner test #3 is load-bearing on it**: a nursing user's "boundaries" are only visible if the other 27 templates' buttons hide; otherwise she sees every button and collects 403s = test fails. So: full sweep of all 33 clinic templates now.
Execution discipline (this is where the risk lives — a WRONG guard hides a button from a legitimate user):
1. First produce a **route→permission mapping table** from `Modules/Clinic/routes/api.php` (the 157 guarded routes are the single source of truth).
2. A button is swept iff its handler chain ends in a write API call; its guard = exactly that route's permission (arrays for any-of). Pure-UI handlers (tabs, filters, dialog-close, retry) are NOT swept.
3. Guard the terminal action too, not just the dialog trigger (fix the existing `cashier-collect` pattern: `collectAndPrint` `:364` and `submitRefund` `:532` get guarded, mirroring the already-guarded trigger).
4. Fix the known inconsistency in `clinic-service-list` (New/Edit/Delete get `clinic.service.manage` like their sibling screen).
5. Deliverable for the review gate: a coverage table (screen → button → permission → route line) — native+Codex review checks perm↔route match, not just presence.
6. Verify the clinic nav/menu items also hide per permission (route guards exist since م0; menu filtering must match — small check, fix if missing).
Later phases that rebuild screens (م4 booking, م8 cashier…) inherit guarded buttons and keep the policy for new ones — they do not re-do the sweep.

---

## Ruling 6 — D6: WP DAG (single writer per repo)

Contracts frozen by this ruling (keys matrix · bulk endpoint shape · catalog endpoint shape = LIS's `{groups, home_pages, data_scopes, dependencies, presets}`).

```
BE chain (one writer):            FE chain (one writer):
WP-A settings                     WP-E appCan sweep  (no BE dependency — starts immediately)
  seeder(12 new + move 4)             ↓
  + ValidatorRegistry             WP-B settings screen (needs WP-A gate: bulk endpoint + keys live)
  + ClinicSettingValidator            ↓
  + PUT /core/settings/bulk       WP-D roles screen (needs WP-C gate: catalog endpoint live)
  + Pest                              ↓
  ↓                               integration smoke
WP-C roles
  ClinicRoleController + catalog
  + presets(6) + RoleSaveService
  prefix generalization + Pest
```
- **Start in parallel: WP-A (BE) ∥ WP-E (FE).**
- WP-C follows WP-A (same BE writer). WP-B follows WP-E in the FE repo and WP-A's gate. WP-D follows WP-B (FE writer) and WP-C's gate.
- Every WP: implementer → independent reviewer → native+Codex gate, TDD inside (Pest for validator/bulk/roles; the sweep's "test" = the coverage table + build green + nursing-user smoke).
- Close with the **owner-test dry-run** on co4: flip `lab_mode` → badge appears on services screen; save empty `ar_account_id` → clean 422 inline error; create «تمريض» role from the preset → log in → sees queue + vitals only, everything else hidden/blocked. Then run `ClinicSettingDefinitionSeeder` on `moonui_dev_be`, deploy `/app`, CHANGELOG (bilingual, noting the seeder for client releases), push, update the progress log, hand to owner.

**Migrations: NONE.** Definitions are seeder rows (`php artisan db:seed --class=...ClinicSettingDefinitionSeeder --force` on moonui_dev_be; rides MoonStack update for clients like `LabSettingDefinitionSeeder` does). Roles/presets/appCan: no schema. م2 ships with zero migrations despite being a feature phase.

---

## Ruling 7 — D7: Owner escalations (ask at م2 start, lightweight — defaults apply if he just nods)

1. **Manager preset ceiling:** manager = all `clinic.*` + `core.settings.view`, but NOT `core.settings.manage`/`core.roles.*` (config & roles stay owner/admin). Default: as stated.
2. **Sixth preset:** keep «طبيب العيادة» alongside your named 5 (it's needed for م5/م6 testing anyway). Default: keep.
3. **Cashier refund power:** cashier preset holds `clinic.payments.void` directly (no manager approval step in م2). Default: yes.
Everything else (key names/types/defaults, values `integrated|standalone`, templates-not-seeded, full sweep) is decided by this ruling — no owner round-trip needed.

---

## Red flags (would pull م2 out of scope / repeat the م1-style traps)

- **⛔ Do not wire mode EFFECTS.** No picker-source switching, no order routing, no intents-whitelist change, no `EncounterResource` exposure of lab_mode. م2's effect = persist + services-screen badge, full stop. This is the #1 scope trap.
- **appCan mis-guard = functional regression** (button hidden from a legitimate user is worse than an unguarded one). The route-mapping table + perm↔route review check is mandatory, plus smoke as an all-perms user (nothing disappeared) AND as nursing (everything disappeared).
- **The 500 trap:** today an enum violation throws `InvalidArgumentException` from `SettingsService::set` — if the empty-GL rejection surfaces as a 500, owner test #2 fails on tone even though it "rejected". Must be a clean 422 with a readable Arabic message.
- **Cache:** bulk save must flush the 5-min company settings cache once post-commit, or the owner flips lab_mode and the badge doesn't move for 5 minutes → test #1 "fails".
- **`RoleSaveService` refactor risk:** the `lisOnly` generalization must not disturb LIS role saves — keep the old signature path + regression Pest on a lab role save.
- **Preset paper-validation trap:** a preset missing one non-clinic permission (e.g., patient create at reception) bricks the workflow — hence the mandatory per-preset E2E walk.
- **Numbering trap (again):** any "م5/م9/م2" tag inside `clinic-service-settings.md` is the OLD local numbering. Already caught twice this phase (queue reset, refund account). Implementers must not scope off those tags.
- **Do not hand-build the settings form** — the hybrid (Ruling 2b) is the line. A bespoke form per field breaks «عميل جديد بلا كود» and makes م8/م9/م10's future keys need FE work.
- **Don't touch other modules' settings screens** or the generic `/core/roles` behavior — clinic screen + new endpoints only.
