Skip to main content

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_REPOSITORY Symbol defined in domain layer
  • Service injects via @Inject(PRODUCTION_RUNS_REPOSITORY) with IProductionRunsRepository interface type
  • Module registers: { provide: PRODUCTION_RUNS_REPOSITORY, useClass: ProductionRunsRepository }

Module Dependencies

ImportPurpose
InventoryLedgersModuleCreate ledger audit rows on completion
InventoryDetailsModuleTrack serial/batch numbers during completion
PdfModulePDF generation and print preview
ProductionFormulasModuleExpand formulas into run inputs/outputs

Domain Concepts

Production Run

A production run represents a single manufacturing operation:

FieldDescription
businessIdOwner business (multi-tenancy scope)
locationIdPrimary production location (used for outputs)
documentNumberAuto-generated sequential number
productionRunTypeType: mixing, assembly, packing, etc.
statusLifecycle state (see Status Machine below)
runDateWhen the production occurred
totalInputCostSum of (input quantity × unit cost)
totalOutputValueSum of (output quantity × unit cost)
productionFormulaIdOptional link to a production formula template
plannedOutputQuantityTarget output quantity (for formula expansion)

Input Lines

Materials consumed during production. Each input has:

  • productId — must be a raw_material, component, or packaging product
  • locationId — source inventory location (can differ per input)
  • quantity, unitCost, uomId
  • detail — optional JSON with inventoryDetails[] for batch/serial tracking

Output Lines

Products created by the production run. Each output has:

  • productId — must be a finished_good product
  • quantity, unitCost, uomId
  • Location is inherited from the run's locationId
  • detail — optional JSON with inventoryDetails[] 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[] and outputs[] 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:

  1. Validate inventory types — inputs must be raw_material/component/packaging; outputs must be finished_good
  2. Check inventory sufficiency — verifies enough stock exists at each location for all inputs
  3. Ensure output inventory records — creates inventory rows if they don't exist for output products
  4. Decrease input inventories — reduces stock for each input, grouped by location+product
  5. Increase output inventories — adds stock for each output at the run location
  6. Update inventory details — adjusts batch/serial tracking for inputs (decrease) and outputs (increase)
  7. Create ledger rows — generates idempotent audit trail rows with MD5-based deduplication keys
  8. Emit events — fires OnInventoryDecreasedEvent and OnInventoryIncreasedEvent for 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

MethodPathDescription
POST/production-runsCreate a production run
GET/production-runsList production runs (paginated, filterable)
GET/production-runs/searchSearch production runs (alias for list)
GET/production-runs/:idGet a single production run with lines
PATCH/production-runs/:idUpdate a production run (partial)
DELETE/production-runs/:idDelete a draft/cancelled production run
GET/production-runs/:id/pdfDownload production run PDF
GET/production-runs/:id/printGet HTML print preview

Filters (GET /production-runs)

ParameterTypeDescription
businessIdUUIDFilter by business
locationIdUUIDFilter by location
statusenumdraft, in_progress, completed, cancelled
productionRunTypeenummixing, assembly, packing, etc.
runDateFrom / runDateToISO date-timeRun date range
createdAtFrom / createdAtToISO date-timeCreation date range
searchstringSearch document number or notes
page / sizenumberPagination
orderBy / orderstringSort field and direction

Design Decisions

  1. 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.

  2. Idempotent ledger rows: Each ledger row gets an MD5-based idempotencyKey derived from run ID, line ID, direction, product, location, quantity, and cost. This prevents duplicate entries on retries.

  3. Formula expansion: Formulas act as templates. When expanding, input/output quantities are scaled proportionally to the plannedOutputQuantity relative to the formula's base output quantity.

  4. 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.

  5. Inventory type validation: Enforced at completion time (not creation) to allow draft runs to be planned before products are properly categorized.

Error Codes

CodeHTTP StatusWhen
INVALID_INITIAL_STATUS400Creating a run with COMPLETED or CANCELLED status
OUTPUT_LINES_REQUIRED400No output lines provided
INVALID_INPUT_LINE400Input line missing productId, locationId, or valid quantity
INVALID_OUTPUT_LINE400Output line missing productId or valid quantity
INVALID_RUN_LOCATION400Run location doesn't belong to the business
INVALID_INPUT_LOCATION400Input location doesn't belong to the business
INVALID_PRODUCT400Product doesn't belong to the business
PRODUCTION_RUN_TERMINAL400Attempting to update a COMPLETED or CANCELLED run
INVALID_STATUS_TRANSITION400Status change not allowed by the state machine
INSUFFICIENT_INVENTORY400Not enough stock for one or more inputs (includes details)
INVALID_PRODUCT_INVENTORY_TYPE400Input products are not raw_material/component/packaging, or output products are not finished_good
PRODUCTION_RUN_DELETE_NOT_ALLOWED400Attempting to delete a non-draft/cancelled run