# Knowledge Base — INDEX

> The map of every topic. Read [`README.md`](README.md) first (how the KB works). كل موضوع وسطر وصف ولينك.
> ✅ = ملف مكتوب · 🟡 = موجود جزئيًا/يحتاج تكميل · ⬜ = مخطّط (نكتبه أول ما نشتغل عليه)

## 🏭 Production & Manufacturing
- 🏭🆕 [Production — shared product search + line-grid column control](plans/production-grid-and-search-unification.html) — **(2026-08-04) SHIPPED to `hazemdev2`, deployed to `/app`, awaiting owner test.** Owner asked for the invoice-style column reorder/resize in the factory screens (named example: `/app/factory/boms` components tab). **The column work was the small half.** The audit found the named screen broken underneath and Production users locked out of it entirely: `PUT /production/boms/{bom}` **silently discarded `components[]`/`operations[]`** (`UpdateBomRequest` declared neither; `update()` used `validated()`; the FE sent exactly those and `bom.service.ts` had no sync method — editing components reported success and changed nothing, with no test covering it) — and `show()` didn't load `components.substitutes`, so a round trip **wiped every substitute**. **No seeded Production role held `core.products.view`** → every picker 403'd silently; the sweep found `core.units.view`, `inventory.warehouses.view`, `core.partners.view` missing too, and since `unit_id` is required on the BOM header *and every line*, **a production user could not save a BOM at all**. Both grants needed **bridge migrations** (`ProductionRoleSeeder` is absent from `moonstack.updater.seeders`). **Tenant isolation:** BOM line `exists:` rules weren't company-scoped while the substitute rule beside them and `StoreBomRequest` were (red run: **every** cross-tenant write returned 200), plus 4 more in `ProductionOrderController` — one under a comment claiming a guarantee the code didn't provide; `grep exists:` over the module's controllers is now clean. **Shared layer (14 screens):** resize silently no-opped inside PrimeNG dialogs — i.e. on most of the 12 document screens — because it resolved the `<th>` by host query; ported `TxColumnLayout`'s `closest('th')`. Handles were gated on the **prefix** `core.settings` (which `…view` passes) while the write needs `core.settings.manage`, and `DocConfigService.persist()` swallowed the 403 — now exact-match, surfaced, and rolled back (needed a root `MessageService` + app-level `<p-toast>`; PrimeNG's has no `providedIn`, which is why a root service could never speak). **Delivered:** column reorder/resize/hide on 7 Production documents (BOM components + operations, MPS, consignment, routing, CoA, order-issue), company-wide persisted; R&D moved to the shared search. **Decision: `TxColumnLayout` everywhere, not `TransactionLineItemsComponent`** — `TxCellType` has **no `date` cell** and Production lines have no price/tax/total; identical user outcome without touching a template 12 live screens render. 11 WPs (3 added mid-run from findings), every one red-before-green where a guard was involved. Ledger: [`plans/production-grid-and-search/LEDGER.md`](plans/production-grid-and-search/LEDGER.md). **⏸️ Backlog PARKED 2026-08-06** (owner: other priorities first) — deferred, not dropped; canonical list with file/line + cause + fix + size in [`plans/production-grid-and-search/BACKLOG.md`](plans/production-grid-and-search/BACKLOG.md): 🔴 B1 `lines.*.product_variant_id` has **no** `exists` rule in `issueMaterials()` (needs a decision — scope to company, or force-match the resolved product) · B2 the FE swallows lookup 403s with no message (**the root reason all four permission defects stayed invisible**) · B3 consignment receipt Save button stays disabled while you fill lines (pre-existing; fix = the `patchLine()` shape proven in `433347abe`) · B4 add a `date` cell type to `TxCellType` (purely additive; unblocks date-bearing docs for the shared component) · B5 three lookup permissions held back on purpose, now pinned by a test (`core.branches.view` · `accounting.tax-rates.view` · `core.users.view` — orders still 403s silently on tax rates) · B6 `ProductionRoleSeeder` absent from `moonstack.updater.seeders` (two bridge migrations so far). Owner's browser pass on `/app` also still outstanding.
- 📦🆕 [Stock balances customer · stock-card source + print · products filters](plans/stock-balances-card-and-product-filters.html) — **(2026-08-06) SHIPPED to `hazemdev2`, deployed to `/app`, awaiting owner test.** Four owner asks, all delivered. **(1)** Stock balances now show/search/filter **«عميل التصنيع»** — kept deliberately apart from the screen's pre-existing **consignment custody** owner lens (`inventory_lot_balances.owner_partner_id` = who owns the *quantity*; `products.toll_customer_id` = who owns the *catalogue definition*; they can disagree per row, and the incumbent picker only lists partners with live consignment lots). Two columns, two filters, and the incumbent — which had **no visible caption at all** — was relabelled «المالك (أمانة)». **(2)** The stock card's movements now carry a **«المصدر المباشر»** column: proved from live data that product 17139's 7 issue movements are **all** recorded against production orders, i.e. the screen was showing 7 delivery notes where the truth was 7 production orders. Hard part: a movement's reference is a **convention, not a relation** — ~28 slugs, no enum, no FK, and `reference_id` means three different things (document id · **business-partner id** for consignment · **production-order id** for backflush/staging, 1 hop). So consignment renders as a **name with no link**, and a job tag (`inventory_issue_tags` — a plain operational label, **no GL, no WIP, never an `MfgMaterialIssue`**) renders with its own badge and its name, never as an order. Resolver is batched: **21 queries for a 21-row page and 21 for a 50-row page**, asserted equal. **(3)** The movements **print** — solving the trap that the on-screen table is client-paginated so only 25 `<tr>`s exist in the DOM and `window.print()` *cannot* reach the rest (a separate hidden sheet over the full loaded set), with the header stating the range because the card **opens on the current week**. **(4)** The products filters: the reported bug was 4 lines — the component set `toll_customer_id` and `ProductService.list()` never serialised it, so the request was **byte-identical to no-filter** — plus 9 further defects, chiefly a search firing one request per keystroke **with no ordering guarantee** and a dead parallel search path that would have silently discarded three filters. Ledger: [`plans/stock-card-source-and-filters/LEDGER.md`](plans/stock-card-source-and-filters/LEDGER.md) · [`RESUME.md`](plans/stock-card-source-and-filters/RESUME.md). **🔴 Biggest incidental find (D5): the app's print-isolation trick is DEAD** — `body * { visibility: hidden }` compiles to `body[_ngcontent-%COMP%] *` under emulated encapsulation and matches nothing (**126 bundle chunks** carry the selector), so the **consignment return slip and BMR printout have been printing the whole application page** behind the slip. Also open: a **dangling production-order reference** in live data (no FK prevents it), 3 list screens ignoring `?viewId=`, and `ProductService.search()` now dead code.
- 🔐🆕 [Data-level permissions — who sees whose data](plans/data-level-permissions-architecture.html) — **(2026-08-06) SHIPPED to `hazemdev2`, awaiting `/fullpush` + owner test.** Owner asked for keepers to see only their own warehouses, then widened it system-wide («الخزن نفس القصه»). **The audit found something worse than a missing feature: the appearance of protection.** A keeper saw **every stock issue/receipt/transfer/adjustment/count in the company** (the only SQL condition was `company_id`); a cashier saw **every cash box and its balance**; any POS user could list **everyone's shifts** (the endpoint filtered by a **client-supplied** `user_id`); `warehouses.manager_id` and `petty_cash.custodian_id` were settable from the UI and read by **zero queries**; and the only filtering that existed ran **in the browser after the rows were delivered**. Meanwhile `data_scope` existed end-to-end (role editor → DB → `/me` → cached user → a client computed) and was **consumed by nothing** outside LIS/Clinic. **Shipped:** `ResourceScope` — scopes by the **resource**, not the branch, because inventory documents have **no `branch_id` at all**; modes `all` (default = today) / `assigned` / `own_records` per resource type; assignment pivots on the `branch_user` pattern; wired end-to-end for **warehouses (12 controllers incl. the raw `DB::table` totals), cash boxes [FIN], and POS tills + shifts**. **It deliberately does not inherit the four fail-open behaviours of the old mechanism**: narrowest-wins replaces broadest-wins (a past migration set *every* role to `all`, so one extra role silently lifted any restriction), no null-resource escape, **empty assignment ⇒ ZERO rows**, and system contexts explicitly exempt so postings don't crash. **The part that outlives us:** with **zero global scopes** in the codebase and company scoping hand-written per method (8× in one controller), nothing makes a future controller get scoping for free — so a **router-enumerated invariant test** probes every route with a restricted user, asserts on **returned data** (not on a method call being present), requires a **written reason ≥60 chars** for each exemption, and **fails the build on a new unscoped route**. It landed **red on purpose** (70 probes, every failure a real leak) and WP3 turned it green. **Findings the plan didn't predict:** the analysis was **wrong** that `petty_cash_transactions` lacked a creator column (it has existed since February — Decision 5 void, no migration written); WP3 silently broke a WP4 precedent **inside the same feature**; the scope badge would never have rendered (its setting read needs a permission keepers don't hold); one constant was answering two different questions and was equal only **by coincidence**. Ledger: [`plans/data-level-permissions/LEDGER.md`](plans/data-level-permissions/LEDGER.md) · [`RESUME.md`](plans/data-level-permissions/RESUME.md). **Open [FIN]:** may a keeper *create* into an unassigned warehouse, and should money still **post** to an out-of-scope cash box — reading is settled, **writing and posting are not**; cost centres deliberately deferred (no scoping axis exists).
- ✅ [Production module — **FULL REFERENCE**](topics/production.md) — **the canonical, code-verified map of the whole module (read this, don't re-scan).** 47 BE models + 49 FE screens; cost-accounting megaproject **phases 0–7 + A + C shipped, B reverted (~88%)**; 3-leg standard cost + 7 variances + GL; 6 Cost-AI features ("AI computes nothing"); state machines; **unwired Core ApprovalWorkflow**; MTO net-new (intake `mfg_order_cases` + trial-as-PO Development BOM + 3 approval gates) + toll gaps. 🟢 Visual full map: [`mfg-module-overview.html`](plans/mfg-module-overview.html).
- 🆕📊 [Manufacturing — competitive gap analysis vs leading MFG software (BOARD-FACING)](plans/mfg-competitive-gap-analysis.html) — **(2026-07-07) first EXTERNAL benchmark** of the Production module vs 6 systems (SAP S/4HANA PP-PI · D365 SCM · Sage X3 · BatchMaster · Odoo · MRPeasy), built for a board go/no-go on continuing with Claude. Produced by an **18-agent Opus workflow** (code-verified baseline → 13 functional-area analysts → 3 adversarial auditors → synthesis), **directed by Fable 5**, **independently reviewed by Codex (non-Claude)** whose fixes were applied (change-log in §10). **Verdict: overall maturity 2.7/5** (weighted pharma-toll-CMO; equal-weight 2.6) — "upper-SMB with lower-mid-market spikes." Strengths (hardest-to-buy): A8 costing 7-variance+GL (3.4), A10 line-level toll ownership (3.0), A7 batch traceability (2.7), A12 deterministic-first AI (3.4), A13 Arabic-RTL+MoonStack (3.1). Troughs: **A3 finite scheduling 1.1**, A11 maintenance 1.5, A6 QMS 2.3. **5 P0 gaps:** e-signature+audit-trail layer, ApprovalWorkflow→ECO wiring, FEFO hard-block+GS1 labels, incoming-QC→GRN quarantine wiring, mandatory cleanout/cross-contamination op. **Recommendation: continue with Claude under conditions** (ship P0, keep independent-review gate, fund GxP CSV, second design partner). 🔑 **Audit correction that ran AGAINST Moon** (QMS is a real closed-loop NCR→CAPA engine in fat controllers, not an empty shell → 1.9→2.3) = the cleanest answer to "Claude assessing Claude." ⚠️ Codex flagged the **competitor benchmark column needs an edition-qualification re-audit** before any external/marketing use. Prior KB reports (490KB audit etc.) are internal build-plans — this is the only competitor benchmark. Baseline + per-area scorecards + audits at `scratchpad/mfg-gap/` (journal: `wf_5e4da26a-e73`).
- 🆕🔀 [Manufacturing — step-by-step operational flow (do-what-then-what)](plans/mfg-process-flow.html) — **(2026-07-07)** a visual "how the flow moves" guide of the Production module for operators/onboarding: the 5 phases (Setup → Plan → Execute → Quality → Close) as a numbered timeline, each step showing the **screen**, the **state transition** (order: planned→released→in_process→completed→closed with guards; operations: waiting→ready→in_progress→completed), the **GL posting** (issue Dr WIP/Cr Inventory · confirm Dr WIP/Cr labor+OH applied · receipt Dr FG/Cr WIP std · close Dr COGS/Cr WIP + 7 variances · scrap · utility clearing), and the **gate** (BOM active, CoA+BMR release, canRelease/canIssue/canClose). Includes the **toll variant** (customer-owned material = no GL on issue, settled via Toll Clearing at receipt) + a GL-summary table + screen map. Built directly from `production.md` (code-verified state machines + GL events) — the operational companion to the competitive gap analysis.
- 📦🆕 [Per-customer consignment stock ("أمانة بضاعة") in the warehouse — analysis + plan](plans/inventory-consignment-stock-analysis.html) — **(2026-07-08, code-verified + Fable 5).** Toll customer's ask: each of HIS customers has a per-customer consignment balance of the same product, segregated in the warehouse, issuable to production, with borrow → buy-from-customer OR return; must be an on/off **option** with zero disruption when off. **Finding: ~80% already ships as "Phase C" in the Production module (live, always-on):** `ConsignmentMaterialLedger` (per customer+product+warehouse balance, off-GL, declared cost) · `CreateConsignmentReceipt` (ledger + physical qty into StockBalance at declared cost, no GL) · **receipt/borrow REQUIRE an `is_consignment` warehouse** (segregation already enforced) · `RecordBorrow` (DR WIP / CR Materials-Due-to-Customer) · `SettleBorrow` buy (PurchaseBill, customer-as-supplier → DR Inventory/CR AP + clear liability) or return/replenish · FE `consignment` screen · perms `production.consignment.*`. 🔴 **LIVE BUG found:** `ConsignmentService::recordIssue` has ZERO callers → toll issues consume consigned stock but never decrement the customer ledger → **every toll issue since Phase C has overstated customer consignment balances**; also no balance guard (negative consignment possible). **The ask completes via 5 targeted moves, NO StockBalance schema surgery:** (1) wire recordIssue + balance guard [Phase 0 bug-fix], (2) exclude `is_consignment` warehouses from valuation/costing reports (sub-ledger>GL leak) + guard the perimeter (block GRN/transfer/adjustment on consignment warehouses), (3) `production.enable_consignment` toggle (default OFF, **backfill ON for tenants with existing consignment data**), (4) Inventory-side UX (receive/per-customer جرد/issue), (5) `borrow_target: stock` for warehouse-level borrow. **Segregation decision: warehouse-per-customer (incumbent, recommended, zero code, FEFO can't cross-pick) — REJECT `owner_partner_id` on StockBalance for v1** (changes the unique key + every lookup across 15 modules; NULL-in-unique trap). Accounting = SAP-style customer special stock (physical visible, value off-company-books). Fast-follows: VAT on buy-settle (currently forced tax=0, non-compliant), FIFO replenish GL/stock drift → settlement gain/loss account. Phases 0(fixes)→1(toggle+UX)→2(borrow completeness)→3(Arabic customer statement + reconciliation).

## 🔬 LIS (Laboratory)
- ✅ [Analyzer middleware](topics/middleware.md) — on-prem Python middleware (Maglumi/VITROS/Dymind/Udichem), source-of-truth/deploy loop, drivers, ASTM/HL7, SSH access, VITROS onboarding.
- ✅ [Client PC access — reverse SSH tunnel](topics/client-pc-tunnel.md) — **read when the tunnel drops.** How we reach the analyzer's Windows box (`ssh -4 -p 2222 hp@127.0.0.1`, reverse tunnel the client dials out), why it drops, and the fix (free port 2222 on our server: `kill -9` the zombie sshd → user re-runs the tunnel). scp/PowerShell/python toolkit + paths.
- ✅ [Sample generation & the barcode gate](topics/lis-sample-generation.md) — **🔴 sample tubes are NOT created by the BE at request creation**; the FE generates them in a separate fire-and-forget step (`onOrderSuccess` → `POST /lis/samples auto_split` → `aliquot`) after save → if it doesn't complete the request stays `pending` with 0 samples and barcode print warns "No collected samples". [Investigation report](plans/lab-barcode-no-samples-investigation.html). Fix = move generation to the BE (transactional).
- ✅ [Reference-range display (worklist/validation/print)](topics/lis-reference-range-display.md) — the range render path across the 3 surfaces; worklist + print ignored **text ranges** (`text_normal`) and print read the **wrong field names** → blank ranges (e.g. Vitamin B12). Fixed: BE emits one canonical `reference_range_text`, all surfaces consume it (validation = the gold-standard pattern). Data model: `lab_investigation_normal_ranges`.
- ⬜ LIS flow & rebuild — worklist-centric flow, kanban/validation, external labs, the rebuild plan. (content currently in MEMORY.md `lis-*` notes → consolidate here)
- ⬜ Lab accounting & invoicing — LIS GL settings, VAT, customer invoice printing, payments/cashier.

## 🚀 Distribution & Updates (MoonStack)
- ✅ [Dev & release branch workflow](topics/dev-workflow.md) — **read before cutting a release.** Branches (`hazemdev` work branch → `main` release source), the release page + CLI, **prerequisites + ALL env vars + gotchas so ANY dev env/user can release correctly.**
- ✅ [Parallel development (2nd Claude / fatamadev)](topics/parallel-dev.md) — running a second Claude instance in parallel on its own branch (`fatamadev`), each on a different module, merging to `main`. **Must be separate per instance: DB + URL/deploy + working dir** (🔴 never let the 2nd touch `moonui_dev_be` — not binlogged). Partition by module. [Visual guide](https://moonui.elbaset.com/parallel-claude-fatamadev.html).
- ✅ [MoonStack update & changelog](topics/moonstack-update.md) — fast/crash-safe self-host updates, the what's-new/changelog process, the **seeder gap** (updates don't seed new definitions), release flow, per-client gotchas, **🔴 docroot-ready packaging fix** ([plan](plans/moonstack-docroot-ready-plan.html)) — the zip extracts as raw Laravel (public/ subfolder → fresh-install 404 + insecure); fix = inject a root `.htaccess` rewrite→`public/`.
- ⬜ Self-hosted distribution plan — the separate WordPress-style installer direction (do NOT touch Ahmed's Moon Central). (in MEMORY.md `self-hosted-distribution-plan`)

## 🎤 Sales & Presentations
- ✅ [Sales presentation & deck visuals](topics/presentation.md) — the EN/AR lab decks (web root, 19 slides, design system), real-screenshot capture recipe, **AI image-gen playbook** (Gemini "Nano Banana" / OpenAI / WaveSpeed) + **🔐 secure key handling (keys never in repo/chat)**.

## 💼 Sales / Accounting
- ✅ [Partial goods-issue → GL vs stock divergence](plans/sales-partial-issue-accounting.html) — **ANALYSIS (2026-06-22, code-verified) → FIX IMPLEMENTED.** Sales invoice posts COGS `Dr COGS / Cr Inventory` immediately for the **FULL** invoice qty (100), but the auto-created GDN can be approved for only **part** (50) — and the GDN posts **no GL entry**. → GL Inventory ≠ physical stock (off by the unissued qty), COGS overstated, profit inflated, no invoice↔issue qty link / no `delivered_qty` / no back-order. Root causes: COGS tied to invoice not delivery; `ApproveIssue` never calls `CreateJournalEntry`; single `quantity` column; no GL-vs-stock tie-out check. Recommended fix: move COGS posting into `ApproveIssue` at the **actual issued qty** (skip invoice-time COGS when GDN active) + add tie-out integrity check. Refs in report. Related config: `sales.stock_deduction_point`, `sales.auto_create_stock_issue_on_invoice`, `auto_approve_stock_issue_on_invoice`. → **Permanent-fix implementation plan (chosen, long-term — rev2):** [`sales-partial-issue-fix-plan.html`](plans/sales-partial-issue-fix-plan.html) — invariant "COGS posts with the stock movement, for the issued qty". rev2 adds: explicit **partial-issue model** (`issued_quantity` vs `quantity` on issue line + new `PartiallyIssued` status), **role visibility** (accountant: invoice delivered/remaining cols + "invoiced-but-undelivered" report; warehouse: requested/issued/remaining on the issue + deliveries worklist), **over-delivery guard** + **auto back-order** so the remaining never gets lost/double-issued, and **new settings in Sales + Inventory** to pick the flow (`cogs_recognition_point`, `allow_partial_delivery`, `auto_create_backorder`, `block_over_delivery`, `inventory.allow_partial_issue`, …) with 3 ready presets (immediate / delivery-driven / auto-deliver). 10 TDD phases via `InventoryIssueApproved` event + Sales listener (revenue stays at invoice — policy A); cols `cogs_journal_entry_id`/`source_item_id`/`delivered_quantity`/`fulfillment_status`; cancel-reversal; **short-close credit note** (financial-only, no inventory leg, reuses SalesReturn with `affects_inventory=false`) to bill the customer only for what shipped when the rest will never be delivered → AR/revenue/VAT reversed for the undelivered qty, invoice `short_closed`; GL⇄stock tie-out check. **No backfill** (all data is test). → ✅ **IMPLEMENTED on `hazemdev2`** (11 phases, subagent-driven, per-phase code review + opus review of the core; gated on `cogs_recognition_point=delivery` so default/legacy behaviour is byte-for-byte unchanged): BE `b3287b5b3`→`2eb880ef6`, FE `310390383`+`27a2449d5` (built + deployed to `/app`); MoonStack changelog bullet added. ✅ **MERGED TO `main`** (2026-06-22): opus final whole-branch review = READY TO MERGE (its 2 Important edge cases — closed-period back-order JE date → now(), and overlapping draft back-orders — fixed in `f01830a31` before merge); integrated the parallel instance's Clinic-module work (BE main `ee8ecb038`, FE main `fb157b4b8` — only CHANGELOG conflicted, both bullets kept); 206-test feature suite green on the merged tree. Default (invoice) mode is byte-for-byte unchanged — feature is opt-in via `cogs_recognition_point=delivery`.
- 🧾🆕 [Purchase request → partial conversion to purchase orders](plans/purchase-request-partial-conversion.html) — **(2026-08-10) SHIPPED to `hazemdev2`, deployed to `/app`, awaiting `/fullpush` + owner test.** Owner: «عندي طلب شراء فيه ٢٠ صنف، عملت أمر شراء بـ١٠ — عايز العشرة الباقيين يفضلوا وأقدر أعمل بيهم تحويل تاني، وده يبقى إعداد». A request can now be converted **line by line and quantity by quantity**, more than once, staying `PartiallyConverted` until the last of it is ordered, at which point it closes itself; cancelling or deleting an order returns its quantities to the pool. Behind `purchases.allow_partial_request_conversion`, **default OFF** — off, behaviour is byte-for-byte today's. **The research found three live defects nobody knew about, and the review gates found two more the briefs hadn't predicted.** (1) `POST /purchases/orders` accepted a `purchase_request_id` and burned the whole request — **no line correlation, no `assertApprovedForPost`, and a non-company-scoped `exists:` so ANY tenant's request matched**; closed by refusing the linkage outright (breaking API change, chosen on evidence: the FE declares the field but never populates it and nothing else in `Modules app database` builds such a payload). (2) **`converted_at` never existed** — both call sites wrote it, the column was in no migration and the key not fillable, so Eloquent discarded it silently; there was no conversion timestamp anywhere in the system. (3) Cancelling an order **stranded its request** — a dangling `converted_to_order_id` on a request frozen at `Converted` that `canCancel()` and `isEditable()` both refuse to unstick: a dead record. (4) **The empty conversion** — a plan filtering to nothing fell through and minted a **lineless draft order with a burned sequence number** (the إذن إضافة empty-document family again), reachable by the loser of a race between two whole-request conversions. (5) **The edit-after-convert seam** — `update()` → `syncItems()` deletes and recreates lines from a payload that never carried `purchase_request_item_id`, so an ordinary edit wiped every back-link while `converted_quantity` stayed booked; the cancel reversal would then have returned **zero**, burning those quantities permanently, and the same edit could raise a line 500 → 5,000 since the cap only existed at convert time. **Invariants worth not breaking:** one writer for the status (`recalculateConversionStatus()`, which DERIVES and only ever rewrites within {Approved, PartiallyConverted, Converted}, so WP5 can move it backwards but can never resurrect a Cancelled request) · two writers for `converted_quantity`, both under the same lock · **lock order is one-directional — order row, then EVERY request line by `sort_order`** — which is what makes convert/cancel/edit queue instead of deadlock · **recompute the status only if a quantity actually moved**, or a pre-WP1 order (request link on the header, NULL on every line) demotes a legitimately `Converted` legacy request and re-opens lines a sibling order still covers · only conversion may create a back-link (written *after* the payload spread, so a caller can never supply one). 6 WPs, 65 new tests, full `Modules/Purchases` **11 failed / 531 passed** vs a baseline of 11/466 — same 11 failure names, verified individually, zero regressions. Ledger: [`plans/purchase-request-partial-conversion/LEDGER.md`](plans/purchase-request-partial-conversion/LEDGER.md) · [`RESUME.md`](plans/purchase-request-partial-conversion/RESUME.md). **Open for the owner:** how a buyer closes a request he has decided not to finish ordering (a partially-converted request whose orders are already received/billed has no exit — not a regression, today's `Converted` is equally frozen) · `scribe:generate` not re-run so the published docs may still advertise the refused param · legacy pre-WP1 orders have no line attribution so their dead records stay dead (needs a backfill migration) · a same-product edit can give a false 422 that fails safe.
- 🛒🆕 [Purchases + Inventory cycle — review & settings-based fix (the PURCHASES mirror of the sales COGS bug)](plans/purchases-inventory-cycle-analysis.html) — **(2026-07-08) code-verified, cross-confirmed by Codex (deep audit) + Fable 5.** Owner sensed the purchasing→inventory→accounting cycle is "not right." Intended flow: PR → PO → preliminary GRN on the PO → partial/full receiving → stock-add note (إذن إضافة) → invoice on actual received (3-way match + GR/IR). **Finding: the documents/states of a correct cycle already exist; the ACCOUNTING is that of a simple one.** `purchases.grn_mode = direct|grn|grn_quality` (default **direct**): in `direct` the **bill drives inventory** (auto-creates+approves an InventoryReceipt from bill lines — `PostPurchaseBill.php:185-190,233-261`); in `grn`/`grn_quality` the GRN approve adds stock (creates+approves InventoryReceipt = إذن إضافة, quality gate, accepted qty) but posts **NO GL at receipt**, and the bill **still** debits Inventory/CR AP directly (mode-agnostic JE `PostPurchaseBill.php:63-166`). 🔴 **No GR/IR (goods-received-not-invoiced) account exists anywhere** (exact search = 0) → received-not-invoiced liability invisible, GL≠stock timing — **exact mirror of the fixed sales `cogs_recognition_point` bug**. 🔴 **No 3-way match**: can over-bill, over-receive, bill unreceived goods, standalone bill (`PurchaseBillController.php:427-453,509-528`; `PurchaseGrnController.php:508-525`); bill-from-PO uses ordered-minus-billed, not received. 🐛 Two latent bugs: fallback mismatch (`getGrnMode()`→'grn' at :494 vs `PostPurchaseBill`→'direct' at :187 → **double stock** if no setting row) + loose `!=='direct'` enum check. **Fix = settings + targeted code, non-disruptive, `direct` stays default:** two orthogonal axes — `grn_mode` (physical) + **new `purchases.inventory_recognition_point` = bill|receipt** (mirror `sales.cogs_recognition_point`) + new `grni_account_id`/`price_variance_account_id` + PO-line 3-way guard + `billing_tolerance_percent`/`require_po_for_bill`. **Steps 1-5 reachable by CONFIG today** (`grn_mode=grn_quality` + enable_purchase_requests + approval + auto_approve=false); only the GR/IR accounting + guards need code. Migration is clean (direct mode ⇒ GR/IR opens at zero, no opening JE). Phases: 0 config (steps 1-5 live + monthly manual accrual) → 1 guards → 2 GR/IR accounting core → 3 GRNI-aging report. Recommended config table in §7. Codex report + Fable advisory at `scratchpad/purchasing/`. **✅ IMPLEMENTED — all phases 0-3 on `hazemdev2` (2026-07-09), Fable-designed + Codex-tested, ~69 new tests green, full Purchases suite 292 pass / 9 pre-existing fails (0 introduced), all opt-in (default byte-for-byte unchanged), migrated+seeded on moonui2, NOT pushed/merged.** P0 latent bugs: `GrnMode` enum resolves grn_mode consistently (closes double-stock/lost-stock). P1 guards: 5 settings (`enforce_three_way_match`+`billing_tolerance_percent`, `require_po_for_bill`, `enforce_receiving_limit`+`receiving_tolerance_percent`) + 3-way/2-way bill guard + GRN over-receive + quality accepted+rejected cap + cross-company fix. P2 GR/IR: `inventory_recognition_point`=bill|receipt + `grni_account_id`/`price_variance_account_id`; GRN approve DR Inventory/CR GR-IR, bill clears GR-IR + PPV (pro-rata + sweep, nets to zero), value accumulators on PO items + bill-line stamp, cancel reverses, config-state guards + flip-back guard, `GrnReceipt` JournalEntryType. P3: `GET /purchases/reports/grni-aging` (outstanding per PO line, supplier-grouped, aged). Progress/resume at `scratchpad/purchasing/PROGRESS.md`.
- 📘🆕 [Purchases + Inventory — USER MANUAL (with direct screen links)](plans/purchases-inventory-user-manual.html) — **(2026-07-09)** end-user guide to the purchasing + inventory cycle as it works in the app after the phase 0-3 changes: login + navigation (Purchases module · Inventory under `/core/`), the full PR→PO→GRN→stock-receipt→bill cycle step-by-step with states + the new controls at each step, the warehouse standalone operations (receipts/issues/transfers/counts/adjustments/balances/reorder), ALL settings + how to configure them (grn_mode, the new 3-way-match toggles, GR/IR recognition + accounts) with a recommended config, the 4 new features + how to enable each, a statuses reference, and a quick-links grid. **43 direct `moonui2.elbaset.com/app/...` links** (path-based routing: inventory at `/app/core/*`, purchases at `/app/purchases/*`, settings at `/app/core/settings`). Companion to the technical analysis; audience = users/operators.
- 👥🆕 [Purchases + Inventory — ROLES & USERS (who receives / buys / inspects / accounts)](plans/purchases-inventory-roles-and-users.html) — **(2026-07-09)** answers the owner's org question: "the one who receives + adds to stock is the WAREHOUSE MANAGER, not purchasing — how, when receiving is done against a PO?" **Viewpoint: the GRN screen (`/purchases/grns`) IS the warehouse-receiving screen** (select PO → quality gate → quantity-capped → approve adds stock); it's only labeled under Purchases, but *who* uses it is decided by the permission `purchases.grns.*`. So the fix is **organizational (permissions), not code**: give the warehouse-manager role `purchases.grns.*` + `inventory.*`. The owner's imagined flow (warehouse makes إذن إضافة → picks PO → forced quality + fixed qty) = exactly the GRN + `grn_mode=grn_quality` + `enforce_receiving_limit`. The generic Inventory إذن إضافة (`/core/stock-receipts`) is NOT PO-aware (don't duplicate). Contains: a swimlane (Purchasing → Warehouse → Quality → Accounting), the 4 role permission checklists (exact `purchases.*`/`inventory.*` grants + what each is denied), a who-can-do-what matrix, and **step-by-step user creation** at [`/core/roles`](https://moonui2.elbaset.com/app/core/roles) + [`/core/users`](https://moonui2.elbaset.com/app/core/users). ⚠️ Documents that **GRN quality-check + approve share one permission** (`purchases.grns.approve`) — a strict separate-QC role needs a small code split (`purchases.grns.quality`), offered as option (d).
- 🔄🆕 [Purchases — CONTROLLED FLOW redesign (desired-vs-current + one-button preset)](plans/purchases-controlled-flow-redesign.html) — **(2026-07-09) code-read (direct + 2 independent agents, file/line-cited) + Fable 5 design. (Codex verify pass could not run — host bwrap sandbox nesting limit; verification done by direct code reads instead.)** Owner described their REAL desired procurement flow (PR→PO→approve→preliminary GRN→**one active GRN per PO**→quality sets accepted-qty + **expiry + batch**→approve **locks qty**→auto-drafts إذن إضافة needing **warehouse-keeper approval**→bill by **actually-received** qty with variance settlement→**one bill per cycle, fully traceable**) and demanded ONE switch (not 20 settings). Maps desired-vs-current and pins **7 problems by root cause**: **(A)** empty إذن إضافة — `PurchaseGrnStatus::canApprove()` allows `Draft` in quality mode → `accepted_quantity` NULL → `if(stockQty<=0)continue` skips every line → empty receipt, no stock; **(B)** `qualityCheck()` doesn't capture batch/expiry (only qty); **(C)** bill prefills `ordered−billed` not received (`remainingBillQuantity`), no `createFromGrn`; **(D)** batch/expiry are a DEAD END — `StockService::increaseStock` drops them, no lot/batch stock model (only serial via ProductSerial); **(E)** bill links only to PO, no `grn_id`/status-history; **(F)** NO duplicate-doc guard (2nd GRN/2nd bill unblocked; caps default OFF) + standalone InventoryReceipt bypasses governance; **(G)** `approve()` auto-approves the receipt inline — no separate أمين-مخازن gate. **Solution (Fable):** one virtual setting `purchases.procurement_mode = simple|controlled` (default simple = byte-for-byte today) resolved by a new **`ProcurementPolicy`** service (option a, NOT a macro — drift-proof + MoonStack fleet auto-updates); `controlled` forces grn_quality + receipt-recognition + the 3 guards + one-active-cycle/one-bill-per-GRN/standalone-restriction as BEHAVIOR (no new setting rows); block the switch if GR/IR+PPV accounts missing; grandfather in-flight docs. **BUG A fix = both layers** (`canApprove(bool $qualityRequired)` → quality?QualityApproved only; + empty-receipt circuit-breaker abort). **Guards = one OPEN receiving cycle per PO** (not one-GRN-ever — partial deliveries are sequential cycles) enforced 3 rings (FormRequest / `lockForUpdate(PO)` in-tx = authoritative / generated-column unique backstop); `purchase_bills.purchase_grn_id` UNIQUE. **Perms = 2 new only**: `purchases.grns.quality_check` (QC, split from approve) + `inventory.receipts.create_manual` (inventory mgr); receipt-approve→أمين المخازن via existing `inventory.receipts.approve`. **Accounting:** qty variance needs no entry (GR/IR at accepted + capped bill + PO closes short), price variance→existing PPV (IAS 2), over-accept→3-way blocks→debit-note; + pre-post settlement panel. **Batch/expiry line drawn:** capture+trace now (P0+P3 movement-level), true FEFO/expiry-block = separate P4 initiative. **Phases P0** (BUG A + perm split + quality batch/expiry — quick wins) **→ P1** the switch **→ P2** guards+billing+traceability **→ P3** expiry-on-movements **→ P4** lot/FEFO (separate KB topic). 8-section HTML with 12-step flow, 7 root-cause cards, one-button visual, phase roadmap, permission matrix, accounting table, expiry-reality matrix. Design notes at `scratchpad/purchasing/FLOW-REDESIGN.md`. **Status: DESIGN — not yet implemented.** → Implementation plan written (below).
- 🧩🆕 [Purchases Controlled Flow — IMPLEMENTATION PLAN (TDD, task-by-task)](plans/purchases-controlled-flow-implementation-plan.md) — **(2026-07-09)** the executable plan for the controlled-flow redesign above (use `superpowers:subagent-driven-development` to run it). Markdown, writing-plans format. 5 phases / 18 tasks. **Phase 0 is code-complete + executable now** (4 tasks, 21 bite-sized TDD steps): 0.1 BUG A fix (`canApprove(bool $qualityRequired)` mode-aware + empty-receipt circuit-breaker `throw ValidationException` inside the approve tx — exact code given, call sites `PurchaseGrnStatus:29`/`PurchaseGrn:92`/`PurchaseGrnController:363,447-476`); 0.2 quality captures batch/expiry (validation + coalesced update); 0.3 split `purchases.grns.quality_check` permission (controller middleware `:48` + RolePermissionSeeder, non-breaking grant); 0.4 cleanup command for legacy empty receipts. **Phases 1-3 are task-level** (files/interfaces/test-cases/sequencing, code written at each phase start — honest fidelity boundary, not speculative code): P1 `procurement_mode` setting + `ProcurementPolicy` sole-reader service + read-site migration + drift arch-test + switch validation/UI-lock; P2 one-active-cycle guard (3 rings) + `purchase_bills.purchase_grn_id` unique + `createFromGrn`/bill-from-received + settlement panel + trail + separate إذن-إضافة keeper approval + `inventory.receipts.create_manual`; P3 batch/expiry on stock movements + expiry report. P4 (lot/FEFO) explicitly out-of-plan. Global constraints: default `simple` byte-identical, hazemdev2 only, tests are the gate, bilingual, MoonStack changelog per phase. **✅ FULLY IMPLEMENTED + PUSHED + DEPLOYED (2026-07-09).** All BE phases on `moon-erp-be` hazemdev2 (P0 ..2c3b94f89 · P1 ..5175396d7 · P2 ..3d453860f · P3 ..3b2aee23d) + FE on `moon-erp-angular` hazemdev2 (..78c960229) + deployed to `/app` on moonui2 (LIVE). Executed autonomously with Fable design consults + code-reviewer/Codex reviews per phase (2 CRITICALs caught+fixed pre-push: P1 controlled↔simple GL-corruption transition guards, P2 cancel-vs-keeper-approval race). Every phase: simple mode byte-identical, 0 new test failures (baselines: Purchases 9 pre-existing double-seed/return, Inventory 1 pre-existing OpeningBalance). Migrations applied + settings/permissions seeded on moonui2. Deferred (not done): 2.4 settlement-summary panel (BE+FE), P4 lot/FEFO stock (separate initiative), pre-existing-test fast-follows. Progress ledger: `scratchpad/purchasing/EXECUTION.md` + design `P2-DESIGN.md`/`FLOW-REDESIGN.md`.
- 🧪🆕 [Purchases Controlled Flow — E2E TEST SCENARIO](plans/purchases-controlled-flow-e2e-test.html) — **(2026-07-09)** step-by-step QA script to verify the live controlled flow on moonui2/app. Setup (GR/IR accounts → enable controlled → optional role users), the full happy path (PR→PO→GRN→quality[accepted qty+batch+expiry]→GRN approve=**pending_receipt, no stock yet**→warehouse-keeper approves إذن إضافة=**stock+GR/IR post now**→bill-by-received→trail), 5 guard/negative tests (2nd GRN blocked · 2nd bill blocked · createFromOrder blocked · manual receipt 403 · can't leave controlled while pending_receipt), + a pass/fail checklist incl. the reverse "back to simple = old behaviour" check. 12 steps, 10 direct app links.
- 🔀🆕 [Purchases + Inventory — FLEXIBLE FLOW (existing vs wanted + batches/serials/expiry)](plans/purchases-inventory-flexible-flow.html) — **(2026-07-09) code-read (2 investigation agents, file/line-cited) + Fable 5 design. FOR APPROVAL before implementing.** Owner, while testing the controlled flow, wants it relaxed from **hard-blocks → "flexible + accounting decides"**: bill from PO (cumulative across GRNs) **OR** from GRN (per-batch) both available; **standalone purchase bill (no PO)** → auto draft إذن إضافة → keeper approves → stock (invoice-first GR/IR); multiple GRNs per PO; 3-way match caps over-billing (verified: `alreadyBilled+billQty ≤ received×tol`). Maps the 9 controlled guards, the desired flow, and the gaps. **Batch/serial findings:** stock balance is **purely aggregate** (no per-lot, no FEFO — both flagged unbuilt in-code); `product_serials` = per-unit serial+batch+**expiry-END only (no production/start date anywhere)**; movements carry batch/expiry for **genealogy only**; **2 real blockers** — (a) serial-tracked product bought via PO→GRN is **impossible to receive** (no GRN/quality serial capture → `serial_count_mismatch` on keeper approve + GRN receipts edit-locked `receipt_locked_by_grn`), (b) per-serial expiry collected in the stock-receipts serial dialog is **dropped** on submit (one line-level expiry stamped on all). **Capture-point decision (Fable): unify at the keeper's receipt approval (C)** — partial-unlock GRN receipts (qty/items stay locked, batch/serial section opens) + **batches-first dialog** (batch rows: lot·prod·expiry·qty, sum=line qty; serials nest under each batch, inherit dates → makes bug (b) impossible) + quality pre-fills. Add `production_date` (informational). **Expiry visibility on stock-balances = derived "nearest expiry" column + badges + drill-down to the existing expiry report** (visibility NOT FEFO). **Phases: 1** flexible billing + direct bill + fix bug (b) · **2** unify capture (fixes blocker (a)) + production_date · **3** expiry visibility · **4 deferred** per-lot balances/FEFO. **§3b — NEW smart-sourcing module (RFQ), owner-requested:** greenfield (none exists; borrow SalesQuotation lifecycle + SupplierPriceList model + the **Patient-Portal** `portal_link_token` no-login pattern) — buyer sends a Purchase Request to N suppliers by email/WhatsApp via a unique per-supplier link → sealed-bid supplier pricing portal (no login) → **product×supplier comparison matrix** (green=lowest/red=highest per row, only over quoted cells) → select best-per-product → **one draft PO per winning supplier**. 4 tables (purchase_rfq + items-snapshot + suppliers/invitation-quote + quote_items), award tracked by columns (rfq_id on PO). Requires **request→multiple POs** first (today a hard 1→1 lock: `canConvert()`=Approved→Converted + single `converted_to_order_id` → switch to hasMany via `purchase_orders.purchase_request_id` + item-level partial conversion). Buyer-entered (phone) quotes mandatory or the matrix stays empty. Messaging: WhatsApp via client-side `wa.me`+template (ready); email needs a new Mailable + real SMTP (default mailer=log, zero Mailables today). RFQ phases: A request→multiple POs · B RFQ+portal+matrix+draft-POs (MLP) · C reminders/award emails/export/WA-API. §8 has 11 approval decisions. **Status: ✅ Phases 1–3 IMPLEMENTED + SHIPPED (2026-07-10) to `hazemdev2` + live on `/app`; Phase 4 (per-lot balances/FEFO) + the RFQ module (§3b) deferred.** See the **[controlled-flow topic](topics/purchases-controlled-flow.md)** (canonical state + backlog) + the per-phase trackers below. Also documents the 2 already-shipped test-phase fixes (PO bill button `fully_received` status, quick-receipt double-approve false error).
- 🧭🆕 [Purchases controlled→flexible flow — STATE + BACKLOG (topic)](topics/purchases-controlled-flow.md) — **(2026-07-10) canonical current-state record.** The `procurement_mode = simple|controlled` flow relaxed into "flexible + accounting decides". **Shipped Phases 1–3:** flexible billing + direct purchase bill (BE `f8d77d181`/FE `17371cb4d`); lot/batch/serial capture at receipt approval fixing the 2 blockers (BE `91cf7c634`/FE `7005989e9`); expiry visibility on stock-balances (BE `20e06ee5b`/FE `b8625169e`). **Deferred backlog:** (1) Phase 4 per-lot on-hand balances + FEFO; (2) RFQ/supplier-sourcing module + request→multiple-POs; (3) small review fast-follows (pendingReceipt N+1, dup-serial 500→422, ReceiptLotService tracking_type defense, expiry drill-down empty-list); (4) settlement/variance panel; (5) pre-existing baseline test failures (OpeningBalance + PurchasesSettingApiTest double-seed). Trackers: `plans/phase{1,2,3}-*.md`.
- 🧩🆕 Phase trackers (executable, task-by-task, with commit hashes): [Phase 1 — flexible billing + direct bill](plans/phase1-flexible-billing-plan.md) · [Phase 2 — lot capture at approval](plans/phase2-lot-capture-plan.md) · [Phase 3 — expiry visibility](plans/phase3-expiry-visibility-plan.md). All ☑ DONE + shipped 2026-07-10.
- ✅ [Sales bug/feature batch](topics/sales-bugfix-batch.md) — RESOLVED (2026-06-22). Fixed: commission-rules 404, auto stock-issue items, order→invoice warehouse, "draft only" error. Shipped BE 3afcbc03c / FE 57e2370a6. #1 (product price tiers) + #2 (default warehouse) already built — config-only. #7 commission = tracking-only (no GL) by choice.
- 🟡 [Sales cash quick-sale + line-entry UX](topics/sales-cash-quicksale-and-line-entry.md) — **built + deployed to `/app`, awaiting user test, NOT yet on `main` (2026-06-23).** Three FE features: **(A)** product-search shows **on-hand stock per warehouse** in the dropdown (opt-in `showStock`/`warehouseId`, cached per-warehouse via `WarehouseStockCacheService` + `shareReplay`; chosen over per-keystroke `search` to avoid false-0 + unbounded fetch); **(B)** **auto-append row + focus next product** on **last-row** pick across all 12 docs (`TxLineDescriptor.appendRow`; middle-row picks keep normal advance); **(C)** **cash quick-sale** — settings `sales.enable_cash_customer` + `sales.default_cash_customer_id` (BE `be629e391`: validation + `SalesSettingDefinitionSeeder`, partners=`business_partners`), new invoice pre-selects the cash customer + full **cash** payment to the user's **branch cashbox** (`PettyCash`, first or localStorage-preferred per branch; `receiving_account_id = cashbox.account_id`, GL-only — PettyCash balance not auto-synced), **Save&New** / **Print&Save&New** buttons. 🔴 review fix: cashbox destinations build even without GL defaults + `onSave` **aborts** (no silent payment skip) via `SALES.NO_CASHBOX`. Design note: cash mode posts immediately (bypasses approval) — cashier needs post/approve perm. FE `d79f0e3bf…154c19b4a`.
- ✅ [Inventory count: counted product "disappears" from Stock Balances](topics/inventory-count-zero-balance-fix.md) — **FIXED + MERGED to `main` (2026-06-27, BE `d247a63fa`).** Portal ticket **ISS-2026-0004** (مون). Counting a never-stocked product to **zero** → `difference=0` → `FinalizeCount` created **no adjustment and no `StockBalance` row** → product absent from "أرصدة المخزون" (nothing deletes/filters rows — it just never had one; client's clue: ADJ auto-made for the product that stayed, not the vanished one). Fix: `FinalizeCount::execute` now **`firstOrCreate`s** the balance row for every counted item (canonical `getOrCreateBalance` pattern) → counted product always shows (0-qty if never stocked); adjustment logic unchanged. +Pest test (18 green), changelog bullet. ⚠️ client `smart` needs the next MoonStack update to get it.
- 📋 [B2B partner packages at direct price — analysis + design (v2)](plans/lis-b2b-packages-analysis.html) — **ANALYSIS (2026-07-03, code-verified 3 investigations + Fable 5 advisory). Not built.** Feature: a **package (bundle of tests) with ONE direct flat price**, assignable to **multiple B2B partner labs** (inbound). 🔄 **v2 pivots from v1 after owner clarification: NO allocation/splitting** — the package bills as **ONE invoice line at the direct price** (member tests still execute in the lab at zero price; billing + revenue are package-level). Current state (unchanged facts): existing retail packages ALLOCATE `package_price` across members via `LabRequestService::recalculateTotals` (per-component invoice); B2B pricing/invoicing/JE is all **per-item**, no `package_id` anywhere, portal takes no packages. **v2 design:** add `billing_mode` (allocated=retail | **direct**=B2B) to `lab_packages` + new pivot **`lab_external_lab_packages`** (external_lab_id, package_id, optional per-partner price override, is_active) = "assign one package to many partners"; **explicit opt-in** (orderable only if assigned); new **single package-line** invoice path. **Accounting = ONE JE for the whole package at completion** (last member result released): Dr partner AR / Cr revenue (net) / Cr VAT — no allocation, **no money-invariant risk (that risk is gone with splitting)**. Net revenue (Fable 5/IFRS 15: direct price = transaction price; savings visible via report = Σ member list − direct, not GL). 🔴 biggest risk (changed) = **adapting the per-item invoice/JE machinery to a single package line + reliable idempotent completion trigger**. Owner decisions (§5): per-partner price override? revenue timing (completion vs invoice)? partial-result (cancelled member) policy? VAT-exclusive? Phases: BE core (billing_mode + pivot + package-line) → package JE at completion → FE assign tab → portal + claim/report.

## 🧩 Core / Platform
- 🛡️ [Regression-safety / test strategy — "be sure the old stuff still works before I change anything"](plans/regression-safety-plan.html) — **STRATEGY (2026-07-04, code-measured + Fable 5 advisory). Not yet implemented.** Owner's pain: editing/adding a feature silently breaks another module (shared Core services: JE/GL engine, stock, VAT). **Measured reality:** BE already has **478 test files / 4,672 tests** (LIS 83, Accounting 65, Production 59…), phpunit CI-ready (SQLite `:memory:`, BCRYPT=4) — but **~50-60 min serial, no ParaTest, and NO CI anywhere** → nothing runs them, nothing gates merge/ship. FE = **0 unit tests** but 2 seeded patterns (`scripts/report-contract.mjs` esbuild contract+snapshot harness for LIS report templates; 2 orphaned Playwright specs, no config/runner). **Core insight: the gap is a GATE, not tests.** Solo dev pushes direct to `hazemdev2` → a PR-merge gate never fires; the load-bearing pair is **`on:push` CI** + **`moonstack:ship` refusing to package unless the release SHA has a green check-run**. **Fast gate:** 4-6 balanced matrix shards + ParaTest → <10 min (SQLite `:memory:` is per-connection = ideal for parallel; real flake risk = DomPDF/`storage/` writes, use `Storage::fake()`). **SQLite≠MySQL → nightly full run on real MySQL (ships migrations to clients — non-negotiable).** **Cross-module:** suite is small — always run all (selection degenerates to full anyway) + add 10-20 golden money-path characterization tests early (JE debit/credit rows, stock balances, VAT, invoice totals). **FE priority d→b→a→c:** prod build+typecheck gate → Playwright 5-8 smokes (nightly first, promote only when non-flaky, cap ~10) → extend report-contract → NO broad unit. **Release gate:** #1 = **upgrade rehearsal** (prev version's post-migrate DB → migrate→seeders→health-check→smoke; catches the seeder gap + migration-on-real-data bugs the suite can't see; keep fixture fresh via saved per-release dump artifact) + fresh-install rehearsal + `settings:verify` (code-referenced setting keys vs seeded, in CI AND client health-check) + ship verifies green SHA/signature/monotonic version. **Anti-list (explicitly overrides the global 80%/TDD rule for this brownfield):** no coverage %, no full-page snapshots, no broad E2E, no mutation/TIA, no self-hosted runners on cPanel, no permanent quarantine (dated skip-list that must shrink). **4-week plan:** wk1 baseline triage + BE/FE CI on-push + ship-SHA gate; wk2-3 golden tests + Playwright + nightly MySQL; wk4 upgrade/install rehearsal + settings:verify. First step = run the full suite once to get the green baseline + serial-vs-parallel timing.
- ✅ [Line-items grid: unify + drag-reorder/resize columns (company-wide persist)](plans/line-items-unify-column-control-plan.html) — **SHIPPED TO `main` (2026-06-23).** 12 document screens (all sales + all purchases + 4 stock screens) migrated to ONE shared `TransactionLineItemsComponent` (descriptor-driven; reorder/resize/show-hide work everywhere at once). User with `core.settings` permission can **drag column headers to reorder** + **drag a column edge to resize**, persisted **company-wide** (not per-user) via `DocConfigService` (`core.document_settings` → `lineOrder`/`lineWidths` per doc, merged non-destructively with the existing field-visibility). All product pickers are type-to-search (incl. server-side supplier search). Built subagent-driven, per-screen + shared-component reviews + final approval; backward-compatible (no saved config → identical render). FE shipped `0f0e36f9a`; deployed to `/app`; changelog bullet added. **Deferred follow-up:** stock-issues + stock-receipts (need `dual-qty` + serial/expiry picker cell types), the linked-return read-only modes, and transfers receive dialog stay hand-written for now. Analysis that led here: [`line-items-grid-column-control.html`](plans/line-items-grid-column-control.html). Every document's product/qty/price table → one shared `TransactionLineItemsComponent` (today only Sales Orders uses it; `app-product-search` IS already shared in 19 screens — that's the shared bit, NOT the grid). Then drag-reorder + show/hide columns, persisted **company-wide** (owner's choice, not per-user) via `DocConfigService` (`core.document_settings` JSON, new `lineColumns` key). Analysis: [`line-items-grid-column-control.html`](plans/line-items-grid-column-control.html). Order: prep shared component → unify screens (invoice first) → column control → persist + settings UI.
- ✅ [Global Command Bar & deep links](plans/global-command-bar-plan.html) — ⌘K command bar that finds any sales invoice/order/customer from anywhere and opens it via a deep-link URL. **Phase 1 SHIPPED to `main` (2026-06-22)** — BE `GET /api/core/search` (company-scoped + permission-filtered, 15 Pest tests) `47a948af3`; FE command bar + `?viewId=` deep-links for orders/bills + invoice copy-link `e5e918e74`. Built subagent-driven (8 tasks, per-task review + opus final whole-branch review; 2 Important FE findings caught+fixed: customer deep-link wiring, recents/flatIndex desync). Plan: [`global-command-bar-impl-plan.md`](plans/global-command-bar-impl-plan.md). Deployed to moonui2 `/app`; changelog bullet in `[Unreleased]`. **Phase 2 SHIPPED (2026-06-22):** search expanded to purchase bills + sales quotations + products; ⭐ Pinned + Recent sections in the bar; **fast standalone invoice view `/sales/invoices/:id`** (loads only the one invoice — ~3 calls vs the list screen's 14+ — fixes the slow deep-link open); shareable-link `/app` base-href bug fixed. BE `ab8bf8d19` / FE `dc6302792`. **Phase 2b SHIPPED (2026-06-22):** the fast standalone view is now generalized to **sales orders (`/sales/orders/:id`), purchase bills (`/purchases/bills/:id`), sales quotations (`/sales/quotations/:id`)** — same pattern as invoices (loads only the one record, lazy print-logo). All command-bar types now open fast (invoices/orders/bills/quotations = dedicated pages; products = own detail route; customers = partner dialog). BE `47906c2d5` / FE `b13c83b86`. **Phase 2c SHIPPED (2026-06-22) — feature complete:** filter-chip **scopes**, `@`/`#`/`>` **shorthand**, permission-filtered **quick-actions** in the bar (BE `7fbf6d7a2` / FE `c43f7a4bd`); BE search **hardened** (LIKE-wildcard escaping via `ESCAPE` + company-scoped relation sub-queries, 28 Pest). Built subagent-driven across Phases 1→2c (implementer+review per task, opus final review, all fixes verified). **Possible future:** fuzzy-filter actions by text; auto-open create dialog on quick-action (currently navigates to the screen).
- ✅ [Default Accounts — master inventory & /setup plan](topics/default-accounts/INDEX.md) — **every GL-default-account setting across ALL modules** (de-duped ~52 keys), the 28 missing from the first-run `/setup` page (Lab 10 / Manufacturing 8 / HR 10), the **setup page writes 7 INERT legacy keys nothing reads**, the setting-definition schema, and the extension plan. Per-area topics in [`topics/default-accounts/`](topics/default-accounts/). ⚠️ AR-header trap.
- ⬜ API conventions — auth (`X-Authorization`), pagination (25 cap / `listAll`), dates/money/soft-deletes, envelope.
- ⬜ Deploy & environments — build/deploy to moonui `/app`, the dev-vs-prod BE targets, `.htaccess`, suexec/chown.

- 🖥️🆕 [POS settings infrastructure + Drug product](topics/pos-settings-and-drug-product.md) — **(2026-08-02)** the POS settings tab was the **only** hand-written settings screen left (3 fixed dropdowns, no explanation slot); it now renders from `setting_definitions` like the other 7 modules, so a new POS setting costs **one seeder entry + one reader** and shows its bilingual label *and description* automatically. Uncovered a family of four "stored, validated, rendered — never read" defects: all **7 terminal settings** had zero backend readers, `POSTerminal::getSettingAttribute()` returned only defaults (throwing away every terminal's real config), `POSSessionResource` never emitted `terminal.settings` (so the till's policy service was null and the cashier learned a rule only by being refused *after* pressing Pay), and `pos.default_receiving_account_type` still has no reader. Also split `pos.sales.create` off `sales.invoices.create` **with a bridge migration** (the updater runs `migrate`, not `RolePermissionSeeder`). Product side: **`is_drug` flag + `product_drug_details` 1:1 + `product_active_ingredient` pivot** — deliberately NOT a third `ProductType`, which would have silently removed every drug from POS index, POS search and Inventory Count (proved with live HTTP assertions); new `active_ingredients` + `dosage_forms` CRUD modules in **Core** (not WebStore — `brands`/`manufacturers` are the cautionary orphans), a conditional «دواء» tab, and an editable per-unit price/barcode grid with a pack-composition helper (flat factors kept — no `parent_unit_id`, `UnitConversionService` carries every stock/sale/purchase/valuation path). Gotchas worth stealing: a granted permission is invisible until **logout/login** (`cached_user`), `product_variants` has **no** `sale_price`, and the Pest one-process redeclare trap bit for the **4th** time. WP1–WP8 done + deployed to `/app`, uncommitted; **WP9 (whole 46-setting catalogue shown locked as «تحت التشغيل») queued**. Analysis: [catalogue + drug design](plans/pos-settings-catalogue-and-drug-product.html) · [pharmacy fitness](plans/pos-pharmacy-fitness-analysis.html) · authoritative execution record: [`plans/pos-settings-drug-product/LEDGER.md`](plans/pos-settings-drug-product/LEDGER.md).

---

### How to extend this index
Add your topic under the right section as `- ✅ [Title](topics/<slug>.md) — hook`. Keep it to one line. See [`README.md`](README.md) §3–4.
