Production Runs Module Documentation
Overview
The Production Runs module manages manufacturing and assembly operations where input materials (raw materials, components, packaging) are consumed to produce finished goods. It supports a full lifecycle from draft through completion, with automatic inventory adjustments, ledger audit trails, and PDF/print document generation.
Architecture
Hexagonal Structure
apps/backend/src/production-runs/
├── production-runs.module.ts # Module registration
├── domain/
│ └── production-runs-repository.domain.ts # Repository interface (port) + PRODUCTION_RUNS_REPOSITORY token
├── application/
│ ├── production-runs.service.ts # Business logic & use cases
│ ├── production-run.utils.ts # Shared utilities (inventory detail extraction, userId resolution)
│ ├── build-production-ledger-rows.ts # Pure function: transforms run into ledger rows
│ └── production-runs.service.spec.ts # Unit tests
├── infrastructure/
│ └── production-runs.repository.ts # Kysely implementation (adapter)
└── interfaces/
├── production-runs.controller.ts # REST endpoints (adapter)
├── dtos/
│ ├── create-production-run.dto.ts # Create validation + Swagger schema
│ └── update-production-run.dto.ts # Update validation (PartialType of Create)
└── query/
└── paginate-production-runs.query.ts # Pagination/filter/sort query
Dependency Injection
The module uses Symbol-based DI tokens for hexagonal boundaries:
PRODUCTION_RUNS_REPOSITORYSymbol defined in domain layer- Service injects via
@Inject(PRODUCTION_RUNS_REPOSITORY)withIProductionRunsRepositoryinterface type - Module registers:
{ provide: PRODUCTION_RUNS_REPOSITORY, useClass: ProductionRunsRepository }
Module Dependencies
| Import | Purpose |
|---|---|
InventoryLedgersModule | Create ledger audit rows on completion |
InventoryDetailsModule | Track serial/batch numbers during completion |
PdfModule | PDF generation and print preview |
ProductionFormulasModule | Expand formulas into run inputs/outputs |
Domain Concepts
Production Run
A production run represents a single manufacturing operation:
| Field | Description |
|---|---|
businessId | Owner business (multi-tenancy scope) |
locationId | Primary production location (used for outputs) |
documentNumber | Auto-generated sequential number |
productionRunType | Type: mixing, assembly, packing, etc. |
status | Lifecycle state (see Status Machine below) |
runDate | When the production occurred |
totalInputCost | Sum of (input quantity × unit cost) |
totalOutputValue | Sum of (output quantity × unit cost) |
productionFormulaId | Optional link to a production formula template |
plannedOutputQuantity | Target output quantity (for formula expansion) |
Input Lines
Materials consumed during production. Each input has:
productId— must be a raw_material, component, or packaging productlocationId— source inventory location (can differ per input)quantity,unitCost,uomIddetail— optional JSON withinventoryDetails[]for batch/serial tracking
Output Lines
Products created by the production run. Each output has:
productId— must be a finished_good productquantity,unitCost,uomId- Location is inherited from the run's
locationId detail— optional JSON withinventoryDetails[]for batch/serial tracking
Status Machine
┌──────────────────────────────────────┐
│ DRAFT (initial) │
│ (can add/edit/delete lines) │
└──────────────────────────────────────┘
↓ ↓ ↓
IN_PROGRESS COMPLETED CANCELLED
↓ │ │
COMPLETED (terminal) (terminal)
CANCELLED
│
(terminal)
- DRAFT → IN_PROGRESS, COMPLETED, CANCELLED
- IN_PROGRESS → COMPLETED, CANCELLED
- COMPLETED → (terminal, no further changes)
- CANCELLED → (terminal, no further changes)
Only DRAFT and CANCELLED runs can be deleted.
Main Use Cases
1. Create Production Run
Creates a run with inputs/outputs. Two modes:
- Manual: Client provides
inputs[]andoutputs[]directly - Formula expansion: Client provides
productionFormulaId+plannedOutputQuantity; the formula is expanded and scaled to generate concrete inputs/outputs
Validations:
- Initial status cannot be COMPLETED or CANCELLED
- At least one output line is required
- All locations must belong to the business
- All products must belong to the business
2. Update Production Run
Partial update with optional line replacement. If inputs/outputs arrays are provided, they replace all existing lines atomically (delete old, insert new).
Status transitions are validated against the allowed transitions map.
3. Complete Production Run
Triggered when status changes to COMPLETED during an update. This is the most complex operation:
- Validate inventory types — inputs must be raw_material/component/packaging; outputs must be finished_good
- Check inventory sufficiency — verifies enough stock exists at each location for all inputs
- Ensure output inventory records — creates inventory rows if they don't exist for output products
- Decrease input inventories — reduces stock for each input, grouped by location+product
- Increase output inventories — adds stock for each output at the run location
- Update inventory details — adjusts batch/serial tracking for inputs (decrease) and outputs (increase)
- Create ledger rows — generates idempotent audit trail rows with MD5-based deduplication keys
- Emit events — fires
OnInventoryDecreasedEventandOnInventoryIncreasedEventfor downstream listeners
4. Generate PDF / Print Preview
- PDF: Renders a production run as a downloadable PDF attachment with customizable page size, margins, and template
- Print Preview: Returns HTML for in-browser printing
Both enrich the run with product and location names before rendering.
5. Delete Production Run
Only allowed for DRAFT or CANCELLED runs. Cascade deletes input/output lines.
API Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /production-runs | Create a production run |
| GET | /production-runs | List production runs (paginated, filterable) |
| GET | /production-runs/search | Search production runs (alias for list) |
| GET | /production-runs/:id | Get a single production run with lines |
| PATCH | /production-runs/:id | Update a production run (partial) |
| DELETE | /production-runs/:id | Delete a draft/cancelled production run |
| GET | /production-runs/:id/pdf | Download production run PDF |
| GET | /production-runs/:id/print | Get HTML print preview |
Filters (GET /production-runs)
| Parameter | Type | Description |
|---|---|---|
businessId | UUID | Filter by business |
locationId | UUID | Filter by location |
status | enum | draft, in_progress, completed, cancelled |
productionRunType | enum | mixing, assembly, packing, etc. |
runDateFrom / runDateTo | ISO date-time | Run date range |
createdAtFrom / createdAtTo | ISO date-time | Creation date range |
search | string | Search document number or notes |
page / size | number | Pagination |
orderBy / order | string | Sort field and direction |
Design Decisions
-
Atomic line replacement: Updates always replace all lines of a given type (inputs or outputs) rather than individual line CRUD. This simplifies consistency and avoids orphan line management.
-
Idempotent ledger rows: Each ledger row gets an MD5-based
idempotencyKeyderived from run ID, line ID, direction, product, location, quantity, and cost. This prevents duplicate entries on retries. -
Formula expansion: Formulas act as templates. When expanding, input/output quantities are scaled proportionally to the
plannedOutputQuantityrelative to the formula's base output quantity. -
Output location inheritance: All output lines use the run-level
locationId, while input lines can each specify their own location. This models the common pattern where materials come from multiple warehouses but production happens at one location. -
Inventory type validation: Enforced at completion time (not creation) to allow draft runs to be planned before products are properly categorized.
Error Codes
| Code | HTTP Status | When |
|---|---|---|
INVALID_INITIAL_STATUS | 400 | Creating a run with COMPLETED or CANCELLED status |
OUTPUT_LINES_REQUIRED | 400 | No output lines provided |
INVALID_INPUT_LINE | 400 | Input line missing productId, locationId, or valid quantity |
INVALID_OUTPUT_LINE | 400 | Output line missing productId or valid quantity |
INVALID_RUN_LOCATION | 400 | Run location doesn't belong to the business |
INVALID_INPUT_LOCATION | 400 | Input location doesn't belong to the business |
INVALID_PRODUCT | 400 | Product doesn't belong to the business |
PRODUCTION_RUN_TERMINAL | 400 | Attempting to update a COMPLETED or CANCELLED run |
INVALID_STATUS_TRANSITION | 400 | Status change not allowed by the state machine |
INSUFFICIENT_INVENTORY | 400 | Not enough stock for one or more inputs (includes details) |
INVALID_PRODUCT_INVENTORY_TYPE | 400 | Input products are not raw_material/component/packaging, or output products are not finished_good |
PRODUCTION_RUN_DELETE_NOT_ALLOWED | 400 | Attempting to delete a non-draft/cancelled run |