# Phase 1 — Flexible Billing + Direct Purchase Bill — Implementation Plan & Tracker

> **Owner-approved (2026-07-10).** Executes Phase 1 of [purchases-inventory-flexible-flow.html](purchases-inventory-flexible-flow.html): **flexible billing** (bill from PO cumulatively OR from GRN per-batch, both available) + **direct purchase bill** (bill with no PO → draft receipt → keeper approves → stock, invoice-first GR/IR). Autonomous execution via `superpowers:subagent-driven-development` — Fable design consults, Opus for complex, Sonnet for mechanical, Codex/code-reviewer per task. Owner tests + collects notes at the end.

**Repos:** BE `/home/moonui2/moon-erp-be` · FE `/home/moonui2/public_html/moon-erp` · branch `hazemdev2`.
**Test company:** company 4 (controlled mode). **Gate:** BE `php artisan test` (Pest), FE `ng build`.

## Global constraints
- **Simple mode stays byte-for-byte unchanged** — all changes gated behind controlled mode / no-PO detection.
- **No over-billing ever:** the 3-way match at post caps cumulative billed at received qty — this is the safety net that replaces the removed hard blocks.
- Bilingual messages (en + ar). MoonStack CHANGELOG bullet per task. Fetch-first before push. Never print remote tokens. `chown moonui2:moonui2` after root edits. Do NOT merge to main.

---

## Task 1.1 — Flexible billing: cumulative bill-from-PO + drop the one-bill-per-GRN hard blocks (BE)
**Model:** Sonnet implementer · **Review:** code-reviewer/Codex
**Files:** `Modules/Purchases/app/Http/Controllers/PurchaseBillController.php` (createFromOrder), `Modules/Purchases/app/Actions/PostPurchaseBill.php` (assertBillQuantitiesWithinLimits), `Modules/Purchases/tests/Feature/OneBillPerGrnTest.php`, `CreateBillFromGrnTest.php`.

**Changes:**
1. `createFromOrder`: **remove the Option-B GRN-routing branch** (added earlier) so createFromOrder runs its original **cumulative** logic in ALL modes — prefills `received_quantity − billed_quantity` per PO line (across all GRNs), `purchase_grn_id = null`.
2. `assertBillQuantitiesWithinLimits`: **remove the `oneBillPerGrn && !purchase_grn_id → bill_requires_grn` guard** so a PO-linked bill without a specific GRN can post. The 3-way match block below it still caps `alreadyBilled + billQty ≤ received × tolerance`.
3. `createFromGrn` stays untouched (per-batch billing still works). Both paths coexist.
4. Tests: update OneBillPerGrnTest (createFromOrder no longer routes/blocks — it bills cumulatively; the bill_requires_grn manual-bill test now expects success under 3-way cap) + keep CreateBillFromGrnTest green.

**Done when:** controlled PO fully received → createFromOrder makes a Draft bill for received-not-billed, posts fine (3-way capped); createFromGrn still works; over-bill still blocked by 3-way; full Purchases suite green (no new failures).

**Status:** ☑ CODE DONE (2026-07-10). Removed the createFromOrder GRN-routing branch + the PostPurchaseBill `bill_requires_grn` post guard; rewrote 3 obsolete OneBillPerGrnTest tests → 2 flexible tests. **33/33 pass** (OneBillPerGrn + CreateBillFromGrn + PurchaseMatchGuards). Full-suite gate + review pending.

---

## Task 1.2 — Direct purchase bill (no PO) → draft receipt → invoice-first GR/IR (BE)
**Model:** Opus implementer (accounting-sensitive) · **Design:** Fable consult on the exact JE before coding · **Review:** Codex + code-reviewer
**Files:** `PostPurchaseBill.php` (createJournalEntry/buildGrniClearingLegs + new draft-receipt-at-post path + requirePo relax), a new listener `Modules/Purchases/app/Listeners/RecognizeInventoryOnDirectBillReceipt.php` + register in `Modules/Purchases/app/Providers/EventServiceProvider.php`, `PurchaseBillResource.php` (is_direct_purchase badge), lang files, tests.

**Behaviour (two-step, invoice-first):**
1. A bill with **no `purchase_order_id`** on any line in controlled mode = **direct purchase bill** (relax `requirePoForBill` for it).
2. **At bill post:** tracked lines DR **GR/IR** (not Inventory) / CR AP + tax/expense as usual; **create a DRAFT `InventoryReceipt`** from the tracked lines (`reference_type=Purchase`, `reference_id=bill`), **NOT auto-approved**. Skip the `bill_requires_grn`/3-way guards for a direct bill.
3. **At keeper receipt approval:** `ApproveReceipt` adds stock (existing) + a **new listener** posts **DR Inventory / CR GR/IR** at received cost. GR/IR nets to zero once both legs run.
4. `is_direct_purchase` (derived: PO id null && GRN id null) on the bill resource → FE badge.

**Core double-entry (Fable-validated GO):** post → `DR GR-IR / CR AP`; keeper approval → `DR Inventory / CR GR-IR`. Same leg directions as the GRN flow, reversed in time. GR-IR nets zero. A GR-IR debit open between post and approval is correct (invoiced-not-received); AP is recognized at post (deferring it would misstate payables).

**Fable's 6 MANDATORY changes (2 are books-corrupting):**
1. **Value-basis (BOOKS-CORRUPTING):** post debit uses `line_total` (net of line discount) but approval credit + stock use `qty×unit_cost` → residual for discounted lines. **Fix:** value receipt lines at effective net cost (`line_total/qty`, last line takes rounding residual — like `stampGrniCleared`) so both legs == `line_total` exactly. Never post the two legs from different bases.
2. **Listener discriminator (BOOKS-CORRUPTING):** `reference_type=Purchase` receipts share an id-space between GRN receipts (`reference_id=grn.id`) and bill receipts — reference_id alone can't distinguish (see `FinalizeGrnOnReceiptApproval` docblock). **Fix:** new `ReceiptReferenceType::PurchaseBill` value; receipt.reference_type=PurchaseBill, reference_id=bill.id; gate the listener strictly on that. Gate on document state, not the current setting.
3. **Lock the draft receipt:** `InventoryReceiptController::update/destroy` let any Draft receipt change qty/cost or be deleted → residual / hanging debit. **Fix:** lock qty/cost/product/warehouse on PurchaseBill-receipts + block destroy — **but keep serial/batch/expiry editable** (keeper must enter serials before approving a serialized product).
4. **Cancellation both branches:** (a) cancelled while receipt Draft → `CancelPurchaseBill` must also void/delete the linked draft receipt(s) in-tx (else keeper approves a receipt for a dead bill). (b) cancelled after receipt approved → **block** the cancel, require purchase-return/debit-note (mirror `CancelReceipt`). (c) extend `CancelReceipt`'s GR-IR guard (checks only `PurchaseGrn.journal_entry_id`) to direct-bill receipts, else it reverses stock without reversing the DR Inventory/CR GR-IR.
5. **Direct = strictly fully-no-PO:** every tracked line lacks `purchase_order_item_id` AND header `purchase_order_id`/`purchase_grn_id` null. Mixed → keep throwing `bill_requires_po_line`. **Persist an explicit `is_direct` flag at creation** (all-or-nothing validation) — gives the FE badge for free.
6. **Guards + idempotency:** copy `handleDirectModeStock`'s warehouse guard (tracked line needs warehouse_id); approval JE idempotency key `direct_bill_receipt:{receipt_id}`; **permission-gate the requirePo bypass with a new `purchases.bills.create_direct`** (mirror `inventory.receipts.create_manual`) — don't silently gut require-PO.
- Extract the per-warehouse receipt-builder from `handleDirectModeStock` into a shared method parameterized on (auto-approve, reference type, cost basis). One bill → possibly MULTIPLE receipts (per warehouse) → GR-IR clears incrementally per approval; key each JE per receipt. Service-only direct bill (no tracked lines) → no receipt (`hasInventoryItems` skip). Bill derives a `pending_receipt` state until all linked receipts approved.

**Files:** migration (ReceiptReferenceType::PurchaseBill + purchase_bills.is_direct + permission seed), StorePurchaseBillRequest/PurchaseBillController::store (set is_direct all-or-nothing + permission), PostPurchaseBill.php (direct branch + shared receipt-builder + skip requirePo/3-way for is_direct), new Listeners/RecognizeInventoryOnDirectBillReceipt.php + EventServiceProvider, InventoryReceiptController::update/destroy (lock), CancelPurchaseBill.php + CancelReceipt.php (cancel branches + guard), PurchaseBillResource.php (is_direct + pending_receipt), lang en/ar, tests.

**Done when:** direct bill post → GR-IR debit(net) + AP credit + Draft receipt, NO stock; keeper approves → DR Inventory/CR GR-IR(net) + stock; GR-IR nets zero even with line discounts; mixed bill rejected; cancel-before-approve voids receipt + reverses; cancel-after-approve blocked; edit-lock holds; permission enforced; tests cover all branches + full suite green.

**Status:** ☑ CODE DONE (2026-07-10, Opus implementer). All 6 Fable changes implemented: net-cost basis (`PostDirectBillReceiptRecognition`), `ReceiptReferenceType::PurchaseBill` discriminator + strictly-gated listener `RecognizeInventoryOnDirectBillReceipt`, receipt edit-lock (InventoryReceiptController + UpdateInventoryReceiptRequest), both cancel branches (CancelPurchaseBill + CancelReceipt guard extension), `is_direct` all-or-nothing flag (migration ran [222]), `purchases.bills.create_direct` permission + warehouse guard + idempotency + shared receipt-builder (handleDirectModeStock refactor byte-for-byte). **`DirectPurchaseBillTest` 11/11 + OneBillPerGrnTest 5/5 = 16 pass (166 assertions)** incl. GR-IR-nets-zero-with-discount, per-warehouse receipts, mixed-bill reject, cancel branches, edit-lock, permission. Field names `is_direct`/`pending_receipt` MATCH the FE (Task 1.3). Full suite gate + Codex review running.

---

## Task 1.3 — FE: direct-bill creation + cumulative PO billing + "direct purchase" badge
**Model:** Sonnet (frontend) · **Design:** Fable UI pass on the badge/no-PO create · **Review:** code-reviewer
**Files:** `features/purchases/bills/bills.component.{ts,html}`, `core/models/purchase-bill.model.ts`, `core/services/purchase-bill.service.ts`, i18n `ar.json`/`en.json`.

**Changes:**
1. Allow creating a bill **without selecting a PO** (direct) — verify/adjust the bills create form (investigate current no-PO support first).
2. Show a **"شراء مباشر / Direct"** badge on bills where `is_direct_purchase`.
3. Ensure the PO create-bill dialog reflects cumulative received-not-billed (BE `remaining_bill_qty`).
4. i18n keys.

**Done when:** you can create a direct bill from the bills screen; it shows the badge; PO cumulative billing works; `ng build` clean.

**Status:** ☑ CODE DONE (2026-07-10) — added `is_direct` + `pending_receipt` to the `PurchaseBill` FE model; a **"Direct/مباشر"** badge + a **"Awaiting Receipt/بانتظار الاستلام"** badge next to the status tag (`bills.component.html:42`); 4 i18n keys (en+ar). `ng build` deferred to Task 1.4 (bundled). Verify `is_direct`/`pending_receipt` field names match the BE resource from Task 1.2's report. **FE investigation earlier:** the bills create form has **no PO selector at all**, so every bill made there is already `purchase_order_id=null` (de-facto direct) and auto-posts when `approval_workflow='none'`. So Task 1.3 shrinks to: (1) add a **"Direct/مباشر" badge** next to the status `p-tag` (`bills.component.html:42-44`, condition `!b.purchase_order_id` — `purchase_order_id` already on the model) + i18n. The real blocker was the BE rejecting the post (Task 1.2). Cumulative PO billing is on the ORDERS screen (Task 1.1), not here.

---

## Task 1.4 — Deep test + review + deploy + push + record
- Run full Purchases + Inventory Pest suites; assert 0 new failures vs baseline (Purchases 9 pre-existing, Inventory 1 pre-existing).
- Codex/code-reviewer whole-diff review; fix Critical/Important.
- `bash local-deploy.sh` (as moonui2), `ng build` + deploy to `/app`, `chown`.
- Fetch-first, commit + push BE + FE to hazemdev2. CHANGELOG bilingual bullets.
- Update this tracker + the flexible-flow doc status.

**Status:** ☑ DONE (2026-07-10). Verified all bill/GRNI/direct-bill test files green (83 tests: DirectPurchaseBill 12, OneBillPerGrn 5, CreateBillFromGrn 5, MatchGuards 23, BillApi 23, Grni 15). Full Purchases+Inventory suite: **10 failed = the exact pre-existing baseline** (Purchases double-seed/return + Inventory OpeningBalance) — **0 new failures**. Deployed: BE `local-deploy` + FE built → `/app`. Pushed: BE `f8d77d181` (tahadeveloper/moon-erp) + FE `17371cb4d` (moon-erp-angular), fetch-first. Bilingual MoonStack CHANGELOG bullets added + chowned.

## ✅ PHASE 1 COMPLETE (2026-07-10) — flexible billing + direct purchase bill, live on /app + pushed to hazemdev2. Next: Phase 2 (unify batch/serial capture at keeper receipt approval).

---

## Progress ledger (append-only)
- 2026-07-10 — Plan written. Starting Task 1.1.
- 2026-07-10 — Task 1.1 code done: removed createFromOrder GRN-routing branch + PostPurchaseBill bill_requires_grn guard; rewrote OneBillPerGrnTest (2 flexible tests). Clean run of the 3 billing test files = **33/33 pass**. Full `php artisan test Modules/Purchases` exit 0 (ran partly concurrent with the Task 1.2 implementer — authoritative clean gate deferred to Task 1.4).
- 2026-07-10 — Task 1.2 dispatched to an Opus implementer with Fable's 6 mandatory changes as the brief. In progress (adding `is_direct`, `handleDirectBillReceipt`, `buildDirectBillGrniLegs`, ReceiptReferenceType::PurchaseBill, the approval listener, cancel branches). Awaiting its report + Codex review.
- 2026-07-10 — Task 1.2 implementer done (18 files, 14 mod + 4 new). Verified directly: **DirectPurchaseBillTest 11/11 + OneBillPerGrnTest 5/5** clean. Field names `is_direct`/`pending_receipt` match the FE. (Had to TaskStop the implementer — it left a Monitor running a concurrent full-suite that was corrupting the gate suite's shared test DB.)
- 2026-07-10 — Review: **Codex env-blocked** on this host (bwrap ENOSPC), substituted a **code-reviewer** (Claude) pass. Verdict BLOCK: **1 CRITICAL** (`is_direct` never recomputed on `update()` → a direct bill edited to add a PO link stays flagged direct → double stock receipt + stranded GR-IR + defeats the create_direct permission) + **1 IMPORTANT** (position-fallback line matching in `updatePurchaseBillReceipt` could swap batch/serial between same product/qty/cost lines). Reviewer CONFIRMED all core accounting correct (GR-IR nets zero on net basis, strict listener gating, idempotent JE, cancel branches, edit-lock, byte-for-byte refactor). **Both fixed:** update() recomputes is_direct + re-gates permission (+ allows nulling the PO); receipt-line update requires `id` (422 `receipt_line_id_required`). Added a regression test. **DirectPurchaseBillTest now 12/12.** MEDIUM (pendingReceipt N+1 on list) + 2 minors noted as fast-follows. Final clean full suite running → then deploy + push.
