# LEDGER — Approval Workflow Wiring (Sales + Purchases)

**Feature:** Wire the generic Core `ApprovalWorkflow` engine into the Sales & Purchases document lifecycle so it actually enforces multi-level, amount-threshold, assigned-approver approval — merged with (not parallel to) the existing status chain.
**Date:** 2026-07-12 · **Branch:** `hazemdev2` (both repos) · **Install:** moonui2 (`/home/moonui2`, DB `moonui2_dev_be`, chown `moonui2:moonui2`).
**Approved plan:** `knowledge-base/plans/approval-workflow-analysis.html`.

## Owner GOAL / acceptance test
"The approval-workflow system works correctly on **Sales and Purchases**." Concretely: define a workflow (e.g. sales_invoice, 1 level, role=manager, min_amount=10000) in `/sales/approval-workflows`; then a salesperson creating an invoice ≥10000 → it becomes **pending approval and CANNOT be posted** until the manager approves it from a **pending-approvals inbox**; an invoice <10000 (below threshold) or with **no workflow defined** posts exactly as today (NO regression). Purchases (bill/order) behaves the same. The rich config the screen collects (levels, thresholds, per-level approver) is now actually enforced by the backend.

## Decisions (from approved analysis + owner — do NOT re-litigate)
1. **Wire the generic engine** (`ApprovalWorkflowService::submitForApproval`) into Sales+Purchases post/approve paths. The engine already exists and is correct — this is WIRING, not rebuild.
2. **Merge, not parallel:** the existing hardcoded status chain (Draft→PendingApproval→Approved→Posted) becomes the surface; the engine decides whether approval is needed and tracks it via `approval_logs`. Post stays blocked until the engine says the doc is fully approved.
3. **NO-REGRESSION is the hard constraint:** if there is NO active workflow for the doc type (or all levels auto/below threshold), `submitForApproval` returns `auto_approved` → the document flows EXACTLY as today (byte-for-byte). Default state (no workflows defined) = current behavior.
4. **Scope = Sales + Purchases only** now. Production/others deferred.
5. `[FIN]` — touches document posting/lifecycle → Fable/advisor gate on the wiring WPs.
6. Amount source = the document total (`grand_total`/`total`), company-scoped, evaluated by the engine's existing min/max logic.

## Engine facts (confirmed at baseline)
- `ApprovalWorkflowService::submitForApproval(module, docType, id, amount, company, user)` → `'auto_approved'` (no workflow / all-auto / below all thresholds) or `'pending'` (creates ApprovalLog rows, notifies level 1). `approve()/reject()/canUserApprove()/getPendingForUser()` exist.
- `ApprovalModule` = sales, purchases, production(orphan). `ApprovalDocumentType` maps sales_* → Sales, purchase_* → Purchases. Logs table `approval_logs (document_type, document_id, level, status, approver_*)`.
- Real gate today: `PostSalesInvoice.php:38 canPost()===Approved`; `submitApproval`/`approve` controller actions set status. Purchases mirrors via `PurchaseBillStatus`.

## WP table (execution order)
| WP | Scope (one dense line) | Repo | Depends | Review | Migr? | Status | Commits |
|----|------------------------|------|---------|--------|-------|--------|---------|
| WP1 | BE: wire engine into **Sales** — submitApproval calls `submitForApproval` (pending⇒block, auto⇒advance as today); approve/post consult ApprovalLog state; post refuses while any log pending. Invoice first, then order/quotation/return. [FIN] | BE | — | Codex+Fable | maybe (indexes only) | ✅ done | BE e971a27a7 (4 wiring + 33 + 21, 0 new fails, no-regression proven; Fable→architect fallback: block-only-if-pending is structurally safe) |
| WP2 | BE: wire engine into **Purchases** — same pattern for bill/order/request/return. [FIN] | BE | WP1 (shared helper) | Codex+Fable | — | 🔄 verifying (4 wiring green, no-reg suites running) |
| WP3 | BE: expose a clean **document-approval state** on the doc API (is_pending_approval, current_level, my_can_approve) + a per-document approve/reject endpoint that drives the engine (so FE buttons act on the doc, not raw log ids). | BE | WP1 | Codex | — | ✅ done | BE c7b992ca5 (8 resources, no N+1, reject cancels logs, 5+4+4+21+33 green) |
| WP4 | FE: config screen supports **module selector + purchase doc types** (not sales-only) + render role-approver branch + auto_approve. | FE | — | Codex | — | ✅ done | FE 4588941a4 (both modules, role approver, auto_approve; build green) | |
| WP5 | FE: wire document screens (sales+purchases) — Submit-for-approval / pending / approve-reject / post reflect the engine state from WP3; hide the old auto-run chain when a workflow gates the doc. | FE | WP3,WP4 | Codex | — | ✅ done | FE 005e0205c (8 screens, build green, none-path unchanged) |
| WP6 | FE: unified **"My Approvals" inbox** (pending approvals across sales+purchases) with approve/reject, from the engine. | FE | WP3 | Codex | — | ✅ done | FE bde74ca3f (standalone inbox, endpoint fixed, perms verified, build green) |

## DAG / serialization
- BE WPs serialize: WP1 → WP2 → WP3 (one writer per BE repo). WP1 builds a shared wiring helper WP2 reuses.
- FE WPs: WP4 independent (config screen) can start alongside BE. WP5+WP6 depend on WP3's API. Serialize FE writers.
- Never two writers in the same repo at once.

## Baseline (Phase A)
- **BE:** `ApprovalWorkflowApiTest` = **21 passed**. Before WP1/WP2 also baseline the Sales + Purchases post/approve suites (record pre-existing fails). Gate = zero NEW failures.
- **FE:** `ng build` green (confirmed earlier this session).
- **NO-REGRESSION test (mandatory each BE WP):** with no workflow defined, an invoice/bill posts exactly as today.

## Deferrals
- Production + other modules (Inventory/HRM already have their own separate approval) — out of scope now.
- **[WP3/FE note]** Sales Order/Quotation have no dedicated PendingApproval status → wired as submit+block (confirm/accept 422 while pending), cleared via generic /core/approval-logs. Invoice+Return have full pending→approve→post. WP3 should expose per-doc approval state so FE surfaces this uniformly. `reject()` sends doc to Draft but doesn't cancel engine logs (WP3).
- **[WP6 note — confirmed]** The FE 'Pending Approvals' tab service points at `/core/approval-workflows/pending` but the real BE inbox endpoint is `GET /core/approval-logs` (pending-for-user). WP6 must repoint it. Also enum values CONFIRMED consistent end-to-end: case SalesInvoice='invoice', PurchaseBill='bill' — FE sends value, engine looks up by value, WP1/WP2 pass the case. No mismatch.
