# 35 — Gated MTO Production Cycle Design (Cycle 2, end-to-end)

> **Design addendum.** Turns the client requirements (`md/30` R-05..R-18), the coverage audit
> (`md/31` C-05/C-06/C-08), the ERP evidence (`md/32`) and the pharma defaults (`md/33` P2/P3)
> into ONE executable design for the make-to-order customer case: state machine, entity decision,
> stage screens, events, postings, permissions. Everything stays inside the ratified rails:
> `Modules/Production` extension, `mfg_*` prefix, `production.*` permissions, Actions-forward /
> events-backward (`md/22` §0/§5), `SequenceService` numbering. Nothing here re-opens D-01..D-22 —
> with TWO explicitly ratified amendments carried as decision rows in the authoritative register
> (`md/36` §4): **D-32** (Trial draft-BOM snapshot carve-out, amends `md/20` row 2) and
> **LA-1/D-33** (advance cash-in rides the `ReceiptVoucher` rail, amends `md/22` §5.4).
> Cycle-1 naming follows the canonical `md/34` pair (`mfg_cost_estimates` /
> `mfg_cost_estimate_versions` — the provisional `mfg_quotations` names are retired).

---

## 0. THE ENTITY DECISION — thin umbrella `mfg_order_cases` (RECOMMENDED), not columns

**Question:** model the customer production case as columns on `production_orders`
(case linkage + `order_type` + `sales_order_id` + deposit fields), or as a thin umbrella entity?

**Decision: `mfg_order_cases` umbrella entity + minimal columns on `production_orders`.**

| # | Reason |
|---|---|
| 1 | **1 case ↔ N production orders.** A case owns the trial order(s) (`order_type=Trial`, re-trials loop) *and* the main order (and possible split lots). Columns on one order row cannot hold a lifecycle that spans several orders. |
| 2 | **7 of 14 case stages exist when NO released production order exists** (SalesOrder, TrialRequested, TrialApproved, BomFinalized, PlanningChecked, DepositGate, and Cancelled-before-release). Columns would have no row to live on. |
| 3 | **Two machines, one spine.** `ProductionOrderStatus` (Planned→Released→InProcess→Completed→Closed, `md/20` row 13) remains the untouched financial/execution machine; the case machine is the *commercial orchestration* layer above it. Widening the order enum to 14 states would corrupt the Phase-1 state machine already ratified. |
| 4 | **Deposit is case-level, not order-level.** One advance covers the whole engagement (trial + main); `mfg_customer_advances` pegs naturally to the case. |
| 5 | **The stage board (R-17) operates on cases.** Kanban cards are cases; shop-floor terminals stay operation-level inside one stage (see §4). |

**Rejected:** columns-only on `production_orders` — duplicates deposit fields across trial+main
rows, has no home for pre-order stages, and forces the execution enum to absorb commercial states.

### 0.1 Schema

```text
mfg_order_cases                              -- NEW (BaseModel: TenantAware, Auditable)
  id, company_id, branch_id
  case_no            SequenceService::generateNext($company,'production','case')
  partner_id         customer (Core partner)
  sales_order_id     FK sales_orders        -- demand anchor (R-08); FK lives mfg-side only (law §5.3)
  cost_estimate_id   FK mfg_cost_estimates NULL -- Cycle-1 lineage (md/34 canonical naming)
  product_id, quantity, unit_id
  stage              enum {SalesOrder, TrialRequested, TrialInProduction, TrialApproved,
                           BomFinalized, PlanningChecked, DepositGate, MaterialsReserved,
                           InProduction, QcRelease, InFgWarehouse, Delivered, Closed, Cancelled}
  trial_cost_policy  enum {Bill, Absorb, CreditOnWin}  -- default settings 'production.trial_cost_policy'
  deposit_percent    decimal NULL           -- override of production.default_advance_percent (OQ-2)
  deposit_basis      enum {OwnMaterialCost, OrderValue}
  deposit_required, deposit_received        -- denormalized from mfg_customer_advances
  deposit_status     enum {NotRequired, Due, Received, Waived}
  consignment_warehouse_id FK warehouses NULL  -- customer free-issue bucket (is_consignment, D-15)
  active_bom_id      FK bill_of_materials NULL -- stamped at BomFinalized
  planning_verdict   json NULL              -- shortages snapshot at PlanningChecked
  closed_at, cancelled_at
```

**EXTEND `production_orders`:** `case_id` FK NULL (Standard MTS orders carry none) ·
`order_type` enum gains `Trial` (→ `{Standard, Rework, Repair, Trial}`, amends `md/20` row 13) ·
`sales_order_id` FK NULL (direct spine column per `md/32` §7; `mfg_peggings` stays canonical for
MRP allocation quantities). **No deposit columns on orders.**

**Other tables (already in `md/30` §4, amended here):** `mfg_trial_batches` gains `case_id`;
`mfg_customer_advances` keys on `case_id` (+ `receipt_voucher_id`); **`mfg_order_stage_gates` is
re-keyed to `case_id`** (`case_id, from_stage, to_stage, actor_user_id, gate_snapshot json, at`)
— it is the audit log of every board move; **human sign-off gates (BOM finalize, trial decision,
advance waive, QC release) ride the Core `ApprovalWorkflow` engine (`md/32` §5) as transition
preconditions — the gate table is the audit log, never a second approval mechanism** (split rule,
`md/36` M-10). Net table delta vs the published 24: **+6 → 30 total**
(`mfg_cost_estimates`, `mfg_cost_estimate_versions`, `mfg_trial_batches`, `mfg_customer_advances`,
`mfg_order_stage_gates`, `mfg_order_cases`) — ratified by `md/36` §1.

---

## 1. The gated state machine

One transition = one permission-guarded Action that (a) validates the gate, (b) mutates `stage`,
(c) appends `mfg_order_stage_gates`, (d) fires the event `afterCommit`. Screens call the same
Actions the API exposes — no bypass (R-17). `Cancelled` is reachable from every stage before
`InProduction` via `CancelProductionCase` (reservations released, advance refunded/forfeited per
OQ-6 policy).

| # | Transition | Gate (precondition) | Actor (perm) | Action → Event |
|---|---|---|---|---|
| 0 | — → `SalesOrder` | Confirmed `SalesOrder` exists (via the forward Action `Sales\ConvertQuotationToOrder` — the `md/36` #19 extraction of `convertFromQuotation`, `md/32` §1) | Sales (`production.case.open`) | `OpenProductionCase` → `ProductionCaseOpened` |
| 1 | `SalesOrder` → `TrialRequested` | Trial qty + `trial_cost_policy` captured | Sales (`production.case.trial_request`) | `RequestCaseTrial` → `CaseTrialRequested` |
| 1b | `SalesOrder` → `BomFinalized` *(skip-trial path)* | Repeat product with existing `Active` BOM AND customer waiver recorded | Supervisor (`production.case.trial_skip`) | `SkipCaseTrial` → `CaseTrialSkipped` |
| 2 | `TrialRequested` → `TrialInProduction` | A BOM exists. **Carve-out (ratified as D-32, an explicit amendment to `md/20` row 2):** `order_type=Trial` orders MAY snapshot an `Engineering/Draft` BOM (guarded by setting `production.trial_allow_draft_bom`); Standard orders never | R&D (`production.case.trial_create`) | `CreateTrialOrder` — real production order, `order_type=Trial`, small qty, `case_id` set; runs the FULL Release→Issue→Confirm→Receive→Close rail, fully costed; **trial output is received into the trial/evaluation bucket (warehouse-as-state, non-sellable FG)** → `TrialOrderCreated` |
| 3 | `TrialInProduction` → `TrialApproved` | Trial order `Closed` + **trial-sample hand-over document exists** (tracking-only issue out of the evaluation bucket, stamped on `mfg_trial_batches.sample_delivery_doc_id`/`sample_delivered_at` — the sample never reaches the customer undocumented, U-3) + customer sign-off attachment + `mfg_trial_batches.result=Passed` | Sales (`production.case.trial_decide`) | `RecordTrialDecision` → `CaseTrialDecided` |
| 3b | `TrialInProduction` → `TrialRequested` *(re-trial)* / → `Cancelled` | `result=Failed`; cost ownership inherits `trial_cost_policy` (OQ-1/OQ-3) | Sales | same Action, `Failed` branch |
| 4 | `TrialApproved` → `BomFinalized` | R&D promotes Draft → `bom_type=Production`,`status=Active`; one Active version per effectivity window (`md/20` row 2); `active_bom_id` stamped | R&D (`production.case.bom_finalize`) | `PromoteCaseBom` → `CaseBomFinalized` |
| 5 | `BomFinalized` → `PlanningChecked` | **Single-order MRP run**: `RunMrp` scoped to this case's pegged demand — gross→net (on-hand − reserved + open POs), explode `active_bom_id` × qty (+scrap%), **split Own vs Customer-supplied lines** (R-10); own shortages → **DRAFT purchase requests prepared but NOT submitted** (the deposit funds this buying — see row 7); verdict persisted in `planning_verdict` | Planning (`production.case.plan`) | `RunCasePlanning` → `CasePlanningChecked` |
| 6 | `PlanningChecked` → `DepositGate` | Auto: compute `deposit_required = deposit_percent × basis`; if zero own-procurement and policy allows → `deposit_status=NotRequired`, auto-pass | system | auto → — |
| 7 | `DepositGate` → `MaterialsReserved` | **HARD GATE:** Σ approved `ReceiptVoucher`s with `reference_type='mfg_order_case'`, `reference_id=case` ≥ `deposit_required` (`md/32` §2; the voucher's own JE is the advance posting — LA-1/D-33), OR `Waived` by `production.case.deposit_waive`. **On `CaseDepositSatisfied` the row-5 draft PRs are NOW submitted via `Purchases\CreatePurchaseRequest` (`md/22` #6) tagged billable-to-customer — purchasing-on-behalf NEVER precedes the deposit (the client's stated causal rule, U-6; fixes the earlier row-5 placement).** Then `ReleaseProductionOrder` runs for the main order — the advance gate is its new precondition (per `md/30` R-11) — freezing the snapshot and reserving **as its ratified side-effect** (`md/20` row 13): `StockService::reserve()` per component pegged to the order (A5); own components from own stock, customer components from the consignment warehouse; tool reserved | Accounting records (`production.case.deposit_record`); release by Planning/PM (`production.case.release`) | `AllocateCaseAdvance` / `WaiveCaseDeposit` → `CaseDepositSatisfied`; then `ReleaseProductionOrder` → `ProductionOrderReleased` (reused) + `CaseMaterialsReserved` |
| 8 | `MaterialsReserved` → `InProduction` | **Auto** on first `MaterialIssued` event of the main order (case-side listener). Customer-supplied materials: received beforehand into the consignment warehouse as **free-issue via a real Inventory receipt document** (`ReceiveCustomerMaterial` → tracking-only `ApproveReceipt` branch for `is_consignment` warehouses — the U-2 intake document; never a direct balance write, `md/22` §1) — tracking-only movements, **never costed into our COGS** (no RM credit, no WIP material debit — `md/22` §4 toll row), but their **lot numbers ARE written into batch genealogy** (md/33 P3). **Unused customer material at case close returns via the mirror tracking-only issue document (`ReturnCustomerMaterial`) + reconciliation: received − issued − consignment scrap (D-30) = returned** | Stores issue; floor executes | existing rail: `PostMaterialIssue`/`PostConfirmation`/`PostGoodsReceipt`; new `CaseConsignmentReceived`/`CaseConsignmentReturned` |
| 9 | `InProduction` → `QcRelease` | Auto on `GoodsReceiptPosted` covering order qty; GR lines land `quality_status=OnHold` (pharma default flip, `production.gr_default_quality_status=OnHold`, md/33 P2) — FG sits in quarantine, not sellable | system | listener → `CaseAwaitingQc` |
| 10 | `QcRelease` → `InFgWarehouse` | ALL `qms_inspections` for the GR = `Released` (QMS hold; rejected lines → NCR loop via `QMS\CreateNonConformance`) | QC (`production.case.qc_release`) | `ReleaseCaseQc` → `CaseQcReleased` |
| 11 | `InFgWarehouse` → `Delivered` | Delivery note confirmed + **final invoice posted — created forward via the net-new thin Action `Modules\Sales\Actions\CreateServiceInvoice` (`md/36` #20; the U-4 compliant rail — never a controller call or direct Sales writes)**: toll branch = conversion-fee line **+ material pass-through line(s) at cost (+ contractual handling %) for factory-bought-on-behalf RM (U-1, §3)**, FG is customer property; own branch: product invoice + **deposit settled** against it (§3) | Sales/Accounting (`production.case.deliver`) | `InvoiceAndSettleCase` → `CaseDelivered` |
| 12 | `Delivered` → `Closed` | Main order `Closed` (7 variances posted, WIP zeroed — `ProductionOrderClosed`) AND invoice settlement complete | system/Accounting | auto → `CaseClosed` |

### 1.1 Trial cost policy (configurable, OQ-1/OQ-3 resolution)

`trial_cost_policy` per case, default `production.trial_cost_policy`:

- **Bill** — trial billed at cost + agreed markup as a Sales service invoice on trial decision
  (win or lose).
- **Absorb** — at trial-order close: DR R&D Expense / CR WIP (`entry_type=production_trial_absorb`).
- **CreditOnWin** — at trial-order close: DR Deferred Trial Cost (asset) / CR WIP
  (`production_trial_defer`); on win, drawn into the engagement (commercial credit line on the final
  invoice + DR order cost / CR Deferred — `production_trial_apply`); on loss/abandon, written off:
  DR R&D Expense / CR Deferred (`production_trial_writeoff`).

Trial orders are excluded from standard-cost baselining and reported separately (filter
`order_type=Trial` — `md/30` R-05).

---

## 2. Events (new + reused)

New, in `Modules\Production\Events`, fired `afterCommit`, idempotent listeners (`md/22` §2 pattern):
`ProductionCaseOpened`, `CaseTrialRequested`, `TrialOrderCreated`, `CaseTrialDecided`,
`CaseBomFinalized`, `CasePlanningChecked`, `CaseDepositSatisfied`, `CaseMaterialsReserved`,
`CaseQcReleased`, `CaseDelivered`, `CaseClosed`, `CaseCancelled` (+ `CaseConsignmentReceived`,
`CaseConsignmentReturned` for the U-2 free-issue intake/return documents). **This vocabulary —
together with `md/34`'s five estimate events — is the single canonical event set, ratified by
`md/36` §2.1.** Payload core:
`case_id, case_no, company_id, branch_id, sales_order_id, stage_from, stage_to, actor_user_id, at`
(+ stage-specific fields, e.g. `deposit_required/received` on `CaseDepositSatisfied`).

Reused unchanged: `ProductionOrderReleased`, `MaterialIssued`, `ConfirmationPosted`,
`GoodsReceiptPosted`, `ProductionOrderClosed` — **case-side listeners** on these auto-advance the
case (rows 8/9/12 above). Dependency law intact: no foreign module imports Production; the case
listeners call foreign Actions forward only.

---

## 3. Accounting postings (deposit liability → settlement)

Accounts via `SettingsService` keys seeded by `AutoAccountService` (detail accounts):
`production.customer_advance_account_id` (new `2105 Customer Advances / دفعات عملاء تحت الحساب`
under `2104 Unearned Revenue` — `md/32` §2), `production.deferred_trial_cost_account_id`,
`production.rnd_expense_account_id`. Cash-in rides `ReceiptVoucher` + `ApproveReceiptVoucher`
(account-agnostic line posting — EXISTS, `md/32` §2) — **this is the explicitly ratified law
amendment LA-1/D-33 to `md/22` §5.4: the voucher engine's own JE IS the advance posting (single
posting, no parallel `CreateJournalEntry` call)**; everything else rides `CreateJournalEntry`
with `source_type='production_order'`/`'mfg_order_case'`.

| Event | entry_type | DR | CR | Amount |
|---|---|---|---|---|
| Advance received (DepositGate) | `production_customer_advance` | Cash/Bank | **2105 Customer Advances (liability)** | receipt amount; voucher `reference_type='mfg_order_case'`; posted ONCE by the voucher engine (LA-1/D-33) |
| Trial — Bill | (normal Sales invoice) | AR | Trial-service revenue | trial cost + markup |
| Trial — Absorb | `production_trial_absorb` | R&D Expense | WIP | trial WIP residual |
| Trial — CreditOnWin (defer) | `production_trial_defer` | Deferred Trial Cost | WIP | trial WIP residual |
| Trial — applied on win / written off on loss | `production_trial_apply` / `production_trial_writeoff` | Order cost / R&D Expense | Deferred Trial Cost | deferred balance |
| Production run | — | *the existing 5 JE families unchanged* (`md/22` §4: issue, labor, OH, scrap, FG receipt + variances) | | |
| Customer-supplied (free-issue) | — | **no JE ever** — tracking-only consignment movement; lots recorded in genealogy | | |
| **On-behalf material relief at toll FG receipt (U-1)** | `production_toll_material_cogs` | Toll material COGS (recharged) | WIP | material portion of WIP for factory-bought-on-behalf components |
| Final invoice (toll, via `Sales\CreateServiceInvoice` — U-4) | (Sales invoice → EInvoicing) | AR | Toll-service revenue + **Material pass-through revenue (U-1)** | conversion fee + on-behalf material at cost (+ contractual handling %) — the invoice now CONTAINS the material the advance funded; the cash math closes |
| **Deposit settlement** | `production_advance_settlement` *(single canonical name — `md/36`'s earlier `production_advance_application` alias retired)* | **2105 Customer Advances** | **AR** | `min(advance balance, invoice total)` |
| Advance refund (cancel, per OQ-6) | `production_advance_refund` | 2105 Customer Advances | Cash/Bank | residual after incurred cost |

---

## 4. The stage screens (R-17): board + per-stage workspaces

**Pattern:** one order-centric **kanban stage board** (`production/cases/board` — columns = case
stages, cards = cases, drag disabled: moves happen ONLY through the stage action) + a **dedicated
workspace per stage** showing that stage's queue, its gate checklist (live evaluation of the
precondition), and the single primary action that advances the case. Every action button calls the
same API Action as §1; every move is audited in `mfg_order_stage_gates`.

| Screen (route under `production/cases/`) | Owner dept | Queue shows | Primary action |
|---|---|---|---|
| `intake` — Case desk | Sales | confirmed sales orders without a case / stage `SalesOrder` | Open case · Request trial · Skip trial |
| `trial-bench` | R&D | `TrialRequested` + `TrialInProduction` monitor (links into the trial order detail) | Create trial order |
| `trial-review` | Sales | trials awaiting customer verdict | Record sample hand-over (delivery document out of the evaluation bucket — required first) · Record decision (Passed/Failed + sign-off attachment) |
| `bom-bench` | R&D | `TrialApproved` | Promote BOM Draft→Active |
| `planning-bench` | Planning | `BomFinalized` | Run case planning (MRP single-order) · view shortages / Own-vs-Customer split · raise PRs |
| `deposit-desk` | Accounting | `DepositGate` with required/received/remaining | Allocate receipt voucher · Waive (privileged) |
| `reserve-desk` | Stores + Planning | deposit-satisfied cases | Receive customer free-issue into consignment (**backed by the `ReceiveCustomerMaterial` tracking-only receipt document — the U-2 rail, not a bare button**) · Release order (snapshot + reserve as Release side-effect) · Return unused customer material at close (`ReturnCustomerMaterial` + reconciliation) |
| `production-monitor` | Production | `InProduction` — order/operation progress, issues, confirmations | (monitoring; advance is automatic) |
| `qc-desk` | QC | `QcRelease` — GR lines OnHold + inspection results | Release / Reject (NCR) |
| `delivery-desk` | Sales/Accounting | `InFgWarehouse` | Delivery note + final invoice via `CreateServiceInvoice` (conversion fee + material pass-through lines) + deposit settlement |

**Relation to shop-floor terminals (Phase 5, `md/20` §5):** terminals remain **operation-level
inside the `InProduction` stage** — operators Start/Done operations and post confirmations; they
**never move the case**. The case auto-advances off the events those confirmations already fire
(`MaterialIssued` → InProduction; `GoodsReceiptPosted` → QcRelease). Stage screens are back-office
workbenches; terminals are floor devices. Two layers, one state machine each, joined by events.

**Interpretation caveat (OQ-9, must be answered at the Phase-0 gate):** this whole section reads
the client's «واجهات لكل مرحلة إنتاجية» as *commercial case stages*. If he means
per-MANUFACTURING-stage screens (mixing/granulation/compression/packaging — pharma-plausible),
those are per-operation workbenches over routing operations: served by the Phase 5 SFC terminals,
with an early read-only per-operation `production-monitor` view shippable in Phase 2.5. Logged as
**OQ-9** in `md/30` §5 / `md/36` §4. Phasing of THESE workbenches: board MVP Phase 2,
gate-critical workbenches Phase 2.5, completion Phase 5 (`md/36` §3 — D-28).

---

## 5. Permissions per stage/department

Slice under the existing `production` contributor in `PermissionDependencyRegistry`; supervisor
role spans all (OQ-7 default — folded into D-28). **This `production.case.*` namespace (with
`md/34`'s `production.estimates.*` for Cycle 1) is the single canonical permission taxonomy,
ratified by `md/36` M-19.** Human sign-offs ride Core `ApprovalWorkflow` once
`ApprovalModule::Production` lands (`md/32` §5) — as transition *preconditions*, per the §0.1
split rule (the gate table stays the audit log).

| Permission | Department | Guards |
|---|---|---|
| `production.case.view` | all stage owners | board + read |
| `production.case.open` / `.trial_request` / `.trial_decide` / `.deliver` | Sales | rows 0,1,3,11 |
| `production.case.trial_create` / `.bom_finalize` | R&D | rows 2,4 |
| `production.case.plan` | Planning | row 5 |
| `production.case.deposit_record` / `.deposit_waive` | Accounting (waive = elevated) | row 7 gate |
| `production.case.release` | Planning / Production manager | row 7 release |
| `production.case.consignment_receive` + existing issue perms | Stores | free-issue + issues |
| `production.case.qc_release` | QC | row 10 |
| `production.case.trial_skip` / `.cancel` | Supervisor | 1b, Cancelled |

---

## 6. Settings keys (seed via `SettingDefinition`)

`production.default_advance_percent` (OQ-2; v1 basis = own-procured material cost) ·
`production.advance_basis` {OwnMaterialCost, OrderValue} · `production.trial_cost_policy`
{Bill, Absorb, CreditOnWin} · `production.trial_allow_draft_bom` (bool) ·
`production.customer_advance_account_id` · `production.deferred_trial_cost_account_id` ·
`production.rnd_expense_account_id` · `production.gr_default_quality_status` (pharma: `OnHold`).

## 7. Roadmap & published-doc amendments

- Lands per the **`md/36` §3 phasing** (which supersedes the provisional "Phase 1.5" slot of
  `md/30` §6 — correctly placed AFTER Phase 2, since labor/OH work-center rates + `cost_driver`
  land in Phase 2, `md/23` L89): case core + deposit gate + stage-gate table + board MVP in
  **Phase 2**; estimates/trial flow + gate-critical workbenches in **Phase 2.5 "Customer Front"**;
  no planning engine needed except the scoped single-order MRP call, which can ship as a
  case-local explosion before full Phase 4 MRP.
- Amends `md/30` §4: `mfg_order_stage_gates` re-keyed to `case_id`; adds `mfg_order_cases`
  (table count 24 → 30 total new `mfg_*` tables; Cycle-1 pair under `md/34`'s canonical names).
- Amends `md/20` row 13: `order_type` += `Trial`; adds `case_id`, `sales_order_id` columns; and
  row 2 gains the **D-32** Trial draft-BOM carve-out (explicit ratified amendment).
- Amends `md/22`: `ReleaseProductionOrder` gains the advance-gate precondition (which also holds
  back on-behalf PR submission); **LA-1/D-33** amends §5.4 for the voucher-ridden advance; new
  `entry_type` values listed in §3; 12+2 new case events appended to §2; `CreateServiceInvoice`,
  `ReceiveCustomerMaterial`, `ReturnCustomerMaterial` registered as forward entry points.
- Decisions consumed: D-15 consignment, A5 reservation, D-21→OnHold flip for pharma (md/33 P2),
  OQ-1/2/3/6/7 given concrete configurable defaults pending board ratification via the
  authoritative register **`md/36` §4 (D-23..D-36 + OQ-9)**.
