# 22 — Integration Contracts: Manufacturing (Modules/Production) ↔ Every Moon ERP Module

Execution-grade contract design. Pattern source: the proven LIS recipe — **Actions forward,
events backward** (`Modules/LIS/app/Actions/PostLabInvoice.php` calling
`Modules\Accounting\Actions\CreateJournalEntry`). Evidence: `md/04` (costing spec),
`md/06` (spec integration), `md/10`–`md/13` (ERP maps). Net-new items are marked **NET-NEW**.

> **Module identity (D-01, binding for every contract below):** the manufacturing layer lives
> INSIDE the extended `Modules/Production` — namespace `Modules\Production`, permission prefix
> `production.*` (registered contributor `production` in `PermissionDependencyRegistry`),
> sequence key `'production'` (already wired), settings namespace `production.*`, work centers =
> `production_centers` (EXTEND — there is NO `mfg_work_centers` table). "Manufacturing" below is
> shorthand for this extended Production module, never a separate `Modules\Manufacturing`.

---

## 0. The two prime rules

1. **Forward = synchronous Action call.** Manufacturing depends on (imports) Core, Inventory,
   Purchases, Sales, Accounting, HRM, QMS, CMMS. When Manufacturing needs something done in
   another module it calls `app(TargetModule\Actions\X::class)->execute(...)` inside the same
   DB transaction. Nothing else (no HTTP, no direct table writes into foreign modules except
   via their Actions).
2. **Backward = domain event.** No existing module ever imports `Modules\Production`.
   Cross-module *reactions* are implemented as **Manufacturing-side listeners** that consume
   Manufacturing's own events and then call the target module's Action — so the dependency
   arrow always points out of Manufacturing. The only inbound signals Manufacturing listens to
   are Core/Accounting events that already exist (`PeriodClosed`) or generic model events.

POS, WebStore and EInvoicing have **zero direct coupling** — they see Manufacturing only
through Inventory stock balances and normal Sales invoices.

---

## 1. Per-module interaction matrix

| Module | Manufacturing TAKES from it | Manufacturing GIVES to it | Mechanism |
|---|---|---|---|
| **Core** | Item master `products`/`product_variants` (BOM/order FKs); UoM `units` + `product_units` (conversion_factor); `SequenceService::generateNext(company,'production',entity)` (already wired); `SettingsService::get('production.*')`; `DataScope` branch scoping; `BaseModel`/TenantAware/Auditable; `Attachment`; `ApprovalWorkflowService` | Item manufacturing settings extension table `mfg_item_mrp_settings` (procurement_type, material_ownership, costing_method, lot rules, lead_time, safety_stock — AccBpExt pattern, no `products` rebuild); permission contributor registered to `PermissionDependencyRegistry` under the existing prefix `production`; **NET-NEW** `ApprovalModule::Production` enum case | FK (read) + Service calls forward; boot-time registry registration. No Core code calls Manufacturing |
| **Inventory** | `StockService::getIssueCost/getProductCost/getValuationMethod` (read-only costing); `ApproveIssue`/`ApproveReceipt` Actions — **the ONLY balance-mutation rail: `increaseStock`/`decreaseStock` are NEVER called directly** (every movement rides an `InventoryIssue`/`InventoryReceipt` document, "never touch balances directly"); `StockBalance::available_quantity`; `warehouses` (RM/WIP-staging/FG as warehouse rows); FIFO/WAC valuation | Reservations at Release (**NET-NEW** `StockService::reserve()/release()` writing `reserved_quantity`); `InventoryIssue` with `reference_type='production_order'` (RM→WIP); `InventoryReceipt` with rolled-up FG `unit_cost` (WIP→FG, copies `PurchaseGrnController::approve` lines 359–406); consignment bucket = dedicated `warehouses` rows flagged `is_consignment` for `material_ownership=Customer` (held, never valued, never purchasable — D-15) | Actions forward (`ApproveIssue`, `ApproveReceipt`); FK `reference_type/reference_id`; **NET-NEW** movement types `production_issue`, `production_receipt`, `production_scrap` + `ProductionOrder` case on the `ReceiptReferenceType`/`IssueReferenceType` enums |
| **Purchases** | Open `purchase_orders` (time-phased supply for MRP netting); GRN receipts restore availability; `purchases.inventory_account_id` fallback chain | MRP Planned Purchase Orders → `PurchaseRequest` + items (needed_by, priority, cost_center_id, source-demand tag) flowing through the normal `convertFromRequest` PO path | **NET-NEW** Action `Modules\Purchases\Actions\CreatePurchaseRequest` (extracted from `PurchaseRequestController::store`) called forward by the MRP run. Purchases never calls Manufacturing; next MRP run re-reads PO status |
| **Sales** | Confirmed `sales_orders`/`sales_order_items` = MPS demand (`confirmed_orders_qty`); MTO/Toll order triggers; delivery dates | CTP/ATP promised date write-back (**NET-NEW** column `sales_orders.promised_date` — single canonical name, D-16 — + thin Action `Modules\Sales\Actions\SetPromisedDate`); demand↔supply link via Manufacturing-owned `mfg_peggings (production_order_id, sales_order_id, allocated_quantity)`; FG availability on `GoodsReceiptPosted` | FK lives on Manufacturing side only (Pegging); Action forward for the promise write-back; Manufacturing listener on its own `GoodsReceiptPosted` updates pegged-order delivery readiness |
| **Accounting** | `CreateJournalEntry::execute($data,$lines)` — the ONLY GL rail; `cost_centers` + `journal_entry_lines.cost_center_id` dimension; `CostAllocationService`/`AllocationRule` (service→production CC, Direct method); `AutoAccountService::createChildAccount` (seed WIP/applied/variance accounts); open-period check; `PeriodClosed` event; `budgets`/`budget_lines` (`cost_center_id` + monthly amounts — EXISTS, feeds FOVV budgeted spend); `fixed_assets` register + `DepreciationEntry` (EXISTS — tool `fixed_asset_id` target) | 5 JE families per order (§4) + month-end OH absorption JE; every JE stamped `source_type='production_order'`, `source_id=order.id`, distinct `entry_type` per event; per-line `cost_center_id` from the work center | Action forward only. **NET-NEW**: `cost_centers.type` column (Production/Service/Auxiliary), `production_centers.cost_center_id` FK, settings keys `production.*_account_id` + `SettingDefinition` seeder, `DepreciationMethod::UnitsOfProduction` case for usage-based tool depreciation. Manufacturing variance run scheduled BEFORE `CloseFiscalPeriod` |
| **HRM** | Operator identity chain `users → employees.user_id` (unique); shifts/attendances context. Labor rate v1 = `production_centers.labor_cost_rate` (D-06 — HRM stores no hourly/piece rate today, only `basic_salary` derived in `PayrollService:48-49`); stored HRM rates + a `GetLaborRate` resolution Action are **DEFERRED post-v1** (optional employee override) | Piece-rate confirmed quantities per employee/period for payroll (`labor_calc=PieceRate`); actual labor hours per operation | Payroll feed = Manufacturing-side listener on `ConfirmationPosted` calling **NET-NEW** `Modules\HRM\Actions\RecordPieceworkEntry`, which writes the **HRM-owned** staging table `hrm_piecework_entries`; payroll reads its OWN table — HRM never imports Manufacturing and never reads an `mfg_*` table |
| **QMS** | Inspection results (`qms_inspections.result`) gate operation advance and GR `quality_status` {Released, OnHold, Rejected} | In-process inspection requests when `Routing_Operation.inspection_required` (tables already carry `production_order_id`); scrap-threshold NCRs; GR final-gate inspections | **NET-NEW** Actions `Modules\QMS\Actions\CreateInspection` / `CreateNonConformance` called forward from Manufacturing listeners on `ProductionOrderReleased`/`ConfirmationPosted`. **NET-NEW**: `operation_no` column on `qms_inspections`/`qms_inspection_plans` + real FK constraints on `production_order_id` once mfg tables exist |
| **CMMS** | Asset master for machines/molds (`cmms_assets`); PM schedules (`meter_field='cycles_used'`, `meter_threshold`); downtime windows for CRP capacity netting | Cumulative tool/mold cycle counts (cavity-adjusted) after each confirmation; PM trigger when shot-count threshold reached; downtime reports from SFC | Manufacturing holds the FK `mfg_tools.cmms_asset_id` (work-center↔asset link deferred). **NET-NEW**: `cmms_asset_meters` reading table + `Modules\CMMS\Actions\RecordMeterReading` + `CreateWorkOrder` + read-only query Action `Modules\CMMS\Actions\GetDowntimeWindows` (CRP consumes downtime via this Action — no raw `cmms_work_orders` table reads); Manufacturing listener on `MoldShotCountReached` calls them forward |
| **POS / WebStore** | — | FG availability only, **indirectly**: `GoodsReceiptPosted` → `ApproveReceipt` → `StockBalance` rises; POS/WebStore already read stock via Inventory | **Zero coupling.** No import either direction; Inventory is the mediator. Optional later: expose ATP endpoint for WebStore lead-time display |
| **EInvoicing** | — | Nothing direct. Toll-manufacturing conversion-fee revenue is billed as a normal Sales invoice (DR Customer / CR Toll-service revenue), which flows to EInvoicing through the existing Sales→EInvoicing pipe | **Zero coupling.** Relevance limited to ensuring the toll service item exists as a Core `Product` of type Service so the fee invoice e-invoices normally |

---

## 2. Named event contracts

All events live in `Modules\Production\Events` (canonical names per mapping/D-17), fired inside the emitting Action after commit
(or `afterCommit` listeners). Consumers are **Manufacturing-side listeners** that call foreign
Actions forward — preserving the dependency direction.

| Event | Emitter (Action) | Consumers (listeners → forward Action) | Payload fields |
|---|---|---|---|
| `ProductionOrderReleased` | `ReleaseProductionOrder` (Planned→Released; freezes BOM+Routing snapshot) | `ReserveOrderComponents` (→ `StockService::reserve`); `PrepareInProcessInspections` (→ `QMS\CreateInspection` plans for ops with `inspection_required`); SFC queue seeder (first op → Ready) | `order_id, order_no, company_id, branch_id, product_id, quantity, production_type, material_ownership, bom_snapshot_id, routing_snapshot_id, tool_id, warehouse_id, source_sales_order_ids[], released_by, released_at` |
| `MaterialIssued` | `PostMaterialIssue` (after `ApproveIssue` + JE) | WIP cost accumulator (order actual-cost ledger); reservation release; shortage/pegging refresh; SFC dashboard broadcast | `issue_id, issue_no, order_id, operation_no, issue_type, issue_level, warehouse_from_id, lines[{product_id, quantity, unit_id, unit_cost, total_cost, batch_no, ownership}], journal_entry_id, issued_by, issued_at` |
| `ConfirmationPosted` | `PostConfirmation` (posts labor + OH applied JEs; backflush issue if configured) | `AdvanceRouting` (next op → Ready or order → Completed; broadcast to next terminal — fires `OperationCompleted`); `AccumulateToolCycles` (cavity-adjusted; may fire `MoldShotCountReached`); `RecordPieceworkForPayroll` (PieceRate → `HRM\RecordPieceworkEntry` → HRM-owned `hrm_piecework_entries`); `EvaluateScrapForNcr` (reason_code threshold → `QMS\CreateNonConformance`); WIP accumulator | `confirmation_id, order_id, operation_no, work_center_id, tool_id, operator_user_id, employee_id, confirmation_type, yield[{grade_code, quantity, unit_sale_price}], scrap_quantity, rework_quantity, reason_code, setup_time_actual, run_time_actual, labor_hours, machine_hours, shift, journal_entry_ids[], confirmed_at` |
| `GoodsReceiptPosted` | `PostGoodsReceipt` (after `ApproveReceipt` + JE; NRV grade-cost allocation) | Pegging fulfillment (pegged sales orders → deliverable; promised-date status update via `Sales\SetPromisedDate` where needed); MPS/ATP refresh; QMS GR-gate follow-up for `OnHold` lines (default until Phase 6 QMS gates: `quality_status=Released` — D-21); order Completed check | `gr_id, gr_no, order_id, receipt_type, warehouse_to_id, lines[{item_id, grade_code, received_quantity, unit_cost, quality_status, batch_no}], journal_entry_id, received_by, posted_at` |
| `ProductionOrderClosed` | `CloseProductionOrder` (Completed→Closed; computes 7 variances; zeroes WIP) | Variance owner routing (notify Purchasing/Production/Management roles per variance type + tolerance band); Planning feedback queue (standard-cost revision candidates); Pareto report aggregator | `order_id, order_no, total_actual_cost, total_standard_cost, variances[{type∈{MPV,MUV,LRV,LEV,VOSV,VOEV,FOVV}, amount, percent, band∈{Normal,Monitor,Investigate}, owner_role}], journal_entry_id, settled_by, settled_at` |
| `MoldShotCountReached` | `AccumulateToolCycles` listener (when `cycles_used + Δ ≥ next meter_threshold`) | `RaiseToolMaintenance` (→ `CMMS\RecordMeterReading` + `CMMS\CreateWorkOrder` Preventive); tool status → Maintenance (blocks CRP allocation of that tool) | `tool_id, cmms_asset_id, work_center_id, cycles_used, life_cycles, threshold, last_order_id, reached_at` |
| `MrpRunCompleted` | `RunMrp` | `EmitPlannedPurchases` (→ `Purchases\CreatePurchaseRequest` per supplier/date bucket); `EmitPlannedProductionOrders` (create Planned `ProductionOrder` rows); exception-message notifier | `mrp_run_id, horizon_start, horizon_end, planned_purchase_count, planned_order_count, exceptions[{type, item_id, message, suggested_action}]` |
| *(inbound)* `PeriodClosed` (Accounting) | `CloseFiscalPeriod` | Manufacturing month-end guard: verifies OH-absorption + variance JEs were posted for the period; flags unsettled orders | per Accounting contract |

---

## 3. Named Action entry points Manufacturing calls (forward)

| # | Action / Service | Module | Status | Used for |
|---|---|---|---|---|
| 1 | `Modules\Accounting\Actions\CreateJournalEntry::execute($data,$lines)` | Accounting | **EXISTS** (proven by `PostLabInvoice`) | All 5 JE families + month-end absorption (§4). Guardrails: detail accounts only, open period required, may leave Draft |
| 2 | `Modules\Inventory\Actions\ApproveIssue::execute($issue,$userId)` | Inventory | **EXISTS** | RM→production issue at FIFO/WAC cost (`reference_type='production_order'`) |
| 3 | `Modules\Inventory\Actions\ApproveReceipt::execute($receipt,$userId)` | Inventory | **EXISTS** | FG receipt at rolled-up WIP unit cost (writes cost layer) |
| 4 | `Modules\Inventory\Services\StockService::getIssueCost / getProductCost / getValuationMethod` | Inventory | **EXISTS** | Cost preview for JE amounts; valuation method resolution |
| 5 | `Modules\Inventory\Services\StockService::reserve() / release()` | Inventory | **NET-NEW** (column `reserved_quantity` exists, no writer) | Reserve at Release, release at Issue/Cancel |
| 6 | `Modules\Purchases\Actions\CreatePurchaseRequest` | Purchases | **NET-NEW** (extract from `PurchaseRequestController::store`) | MRP planned purchases → requisition → existing `convertFromRequest` PO flow |
| 7 | `Modules\Sales\Actions\SetPromisedDate` | Sales | **NET-NEW** (thin; + `sales_orders.promised_date` column — D-16) | CTP write-back to sales orders |
| 8 | `Modules\QMS\Actions\CreateInspection` / `CreateNonConformance` | QMS | **NET-NEW** (tables ready: `qms_inspections.production_order_id`) | Operation gates, GR gate, scrap→NCR |
| 9 | `Modules\CMMS\Actions\RecordMeterReading` / `CreateWorkOrder` | CMMS | **NET-NEW** (`cmms_asset_meters` table missing; WO model exists) | Mold shot-count PM, downtime WOs |
| 10 | `Modules\CMMS\Actions\GetDowntimeWindows` | CMMS | **NET-NEW** (read-only query Action) | CRP subtracts maintenance downtime from available capacity — no raw `cmms_work_orders` reads |
| 11 | `Modules\HRM\Actions\RecordPieceworkEntry` | HRM | **NET-NEW** (writes HRM-owned `hrm_piecework_entries`) | Piece-rate payroll feed from confirmations; payroll reads its own table |
| 12 | `Modules\HRM\Actions\GetLaborRate(employee_id, labor_calc)` | HRM | **DEFERRED post-v1** (v1 rate = `production_centers.labor_cost_rate` — D-06) | Optional employee-rate override once HRM stores rates |
| 13 | `Modules\Accounting\Services\CostAllocationService::execute` | Accounting | **EXISTS** (Direct method) | Month-end service-CC → production-CC distribution before absorption variance |
| 14 | `Modules\Core\Services\SequenceService::generateNext(company,'production',entity)` | Core | **EXISTS** (key already wired) | order_no, issue_no, confirmation_no, gr_no, mrp_run_no |
| 15 | `Modules\Core\Services\SettingsService::get('production.*')` | Core | **EXISTS** (keys to seed) | Account mapping, tolerances, GRN-like mode flags |

---

## 4. Journal-posting map (costing cycle) — consistent with md/04 + md/12 §9

Every JE: `source_type='production_order'`, `source_id=order.id`, line `cost_center_id` = the
operation's work-center cost center (`production_centers.cost_center_id`, NET-NEW FK).
Accounts resolved via `SettingsService` keys (`production.wip_account_id`,
`production.raw_material_account_id`, `production.labor_applied_account_id`,
`production.moh_applied_account_id`, `production.fg_inventory_account_id`,
`production.scrap_expense_account_id`, `production.toll_revenue_account_id`,
7 × `production.*_variance_account_id`), seeded by `AutoAccountService` as **detail** accounts —
ONE namespace (`production.*`), identical to what Phase 0 seeds, so account resolution can never
split across two key spaces.

| Event | entry_type | DR | CR | Amount |
|---|---|---|---|---|
| Material issue | `production_material_issue` | WIP | Raw-Material Inventory | issued qty × FIFO/WAC cost (`getIssueCost`) |
| Labor applied (confirmation) | `production_labor` | WIP | Labor Applied (contra) | Hourly: hours × std rate · PieceRate: qty × piece_rate |
| Overhead applied (confirmation) | `production_overhead` | WIP | MOH Applied (contra) | cost_driver × overhead_rate — both per work center: `production_centers.cost_driver` (NET-NEW column) × `production_centers.overhead_rate` (exists) |
| Scrap (abnormal) | `production_scrap` | Scrap Expense (P&L) | WIP | scrap qty × accumulated unit WIP cost |
| FG receipt | `production_receipt` | Finished-Goods Inventory (per grade, NRV-allocated) | WIP | produced qty × std_total_cost (Standard) / allocated actual |
| Settlement / variances (Close) | `production_variance` | / CR variance accounts: MPV→Purchasing, MUV→Production, LRV+LEV→Production (n/a PieceRate), VOSV→Mgmt, VOEV, FOVV→Sales/Mgmt | WIP residual zeroed | residual WIP decomposed; adverse = DR variance, favourable = CR. Bands: <2% Normal, 2–5% Monitor, >5% Investigate |
| Month-end OH absorption | `production_oh_absorption` | Over/under-applied → P&L | MOH Applied cleared | applied (Σ event 3) vs actual CC expenses, after `CostAllocationService` distribution. MUST post before `CloseFiscalPeriod` |
| **Toll order** (material_ownership=Customer) | — | No material JE at issue (tracking-only consignment move); WIP carries conversion cost only (labor+OH); fee invoice via Sales: DR Customer / CR Toll-service Revenue → EInvoicing as normal | | |

---

## 5. Dependency rules (the law)

1. `Modules\Production` (manufacturing-extended) **imports forward**: Core, Inventory, Purchases,
   Sales, Accounting, HRM, QMS, CMMS — Actions/Services/Models read-only or via Actions.
2. **No module ever imports `Modules\Production`.** All reactions to manufacturing events are
   implemented as Production-owned listeners that call the target module's Action.
   New foreign Actions (#5–#11 above) are generic, Manufacturing-agnostic entry points added
   to their home modules. No foreign module ever reads an `mfg_*` table (the piecework payroll
   feed is pushed INTO HRM-owned `hrm_piecework_entries` via `RecordPieceworkEntry`).
3. Cross-module FKs live on the Manufacturing side only (`mfg_peggings.sales_order_id`,
   `mfg_tools.cmms_asset_id`/`fixed_asset_id`, `production_centers.cost_center_id`); foreign
   tables get at most nullable untyped reference columns (QMS already has `production_order_id`).
4. GL exclusively via `CreateJournalEntry`; stock exclusively via
   `StockService`/`ApproveIssue`/`ApproveReceipt`; numbering exclusively via `SequenceService`.
5. POS/WebStore/EInvoicing: zero direct contracts — Inventory and Sales mediate.
6. Every transactional flow: Action opens transaction → foreign Actions inside → events fired
   `afterCommit` → listeners idempotent (event payloads carry document numbers for dedup).
