Saltar al contenido principal

Production Runs

Overview

The production-runs module manages manufacturing operations that transform input materials (raw materials, components, packaging) into finished goods. It supports a status-based lifecycle, formula-driven expansion, inventory synchronization, audit ledger creation, and PDF/print generation.

Architecture

Follows hexagonal architecture with strict layer separation:

production-runs/
├── domain/
│ └── production-runs-repository.domain.ts # Port (interface) + types
├── application/
│ ├── production-runs.service.ts # Use cases / business logic
│ ├── production-run.utils.ts # Shared utilities
│ ├── build-production-ledger-rows.ts # Ledger row builder (pure function)
│ └── production-runs.service.spec.ts # Unit tests
├── infrastructure/
│ └── production-runs.repository.ts # Kysely adapter (implements port)
├── interfaces/
│ ├── production-runs.controller.ts # HTTP controller
│ ├── dtos/
│ │ ├── create-production-run.dto.ts # Create request validation
│ │ └── update-production-run.dto.ts # Update request validation
│ └── query/
│ └── paginate-production-runs.query.ts # List/search query params
└── production-runs.module.ts # NestJS module definition

Dependency injection: The repository is injected via a PRODUCTION_RUNS_REPOSITORY symbol token, allowing the service to depend on the interface (port) rather than the concrete class.

Domain Concepts

Production Run

A production run is a manufacturing operation that:

  • Consumes inputs: raw materials, components, or packaging (sourced from specific locations)
  • Produces outputs: finished goods (at the run's primary location)
  • Has a document number (auto-generated)
  • Belongs to a business and a primary location
  • Has a production run type: assembly, mixing, packing, etc.

Status Lifecycle

DRAFT ──────┬──> IN_PROGRESS ──┬──> COMPLETED (terminal)
│ └──> CANCELLED (terminal)
├──> COMPLETED (terminal)
└──> CANCELLED (terminal)
  • DRAFT: Run can be freely edited. Lines can be added/removed.
  • IN_PROGRESS: Run is being executed. Lines can still be updated.
  • COMPLETED: Terminal. Inventory is adjusted (inputs decreased, outputs increased). Cannot be modified.
  • CANCELLED: Terminal. No inventory changes. Cannot be modified.

Only DRAFT and CANCELLED runs can be deleted.

Formula Expansion

When creating a run with productionFormulaId + plannedOutputQuantity (and no explicit inputs/outputs), the system:

  1. Fetches the production formula
  2. Scales inputs/outputs to match the planned quantity
  3. Maps the scaled data into concrete run lines

Completion Flow

When status transitions to COMPLETED:

  1. Validate product inventory types (inputs must be raw_material/component/packaging, outputs must be finished_good)
  2. Check inventory sufficiency for all inputs
  3. Ensure inventory records exist for output products
  4. Decrease input quantities in inventory
  5. Increase output quantities and costs in inventory
  6. Update inventory details (batch/serial numbers)
  7. Create ledger rows (with idempotency keys for deduplication)
  8. Emit OnInventoryDecreasedEvent and OnInventoryIncreasedEvent

API Endpoints

All endpoints require authentication via Bearer token (Firebase ID token).

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 production run by ID (with lines)
PATCH/production-runs/:idUpdate production run (partial)
DELETE/production-runs/:idDelete production run (DRAFT/CANCELLED only)
GET/production-runs/:id/pdfGenerate PDF document
GET/production-runs/:id/printGet HTML print preview

Filters (GET /production-runs)

ParameterTypeDescription
businessIdUUIDFilter by business
locationIdUUIDFilter by location
statusenumdraft, in_progress, completed, cancelled
productionRunTypeenumFilter by type
runDateFrom / runDateToISO dateDate range for run date
createdAtFrom / createdAtToISO dateDate range for creation date
searchstringSearch in document number and notes
orderByenumrunDate, createdAt, documentNumber, status
orderenumasc, desc
page / sizenumberPagination

Error Codes

CodeHTTP StatusWhen
INVALID_INITIAL_STATUS400Creating with COMPLETED/CANCELLED status
OUTPUT_LINES_REQUIRED400No output lines provided
INVALID_INPUT_LINE400Input line missing productId/locationId or invalid quantity
INVALID_OUTPUT_LINE400Output line missing productId or invalid quantity
INVALID_RUN_LOCATION400Run location not in business
INVALID_INPUT_LOCATION400Input line location not in business
INVALID_PRODUCT400Product not in business
PRODUCTION_RUN_TERMINAL400Updating a COMPLETED/CANCELLED run
INVALID_STATUS_TRANSITION400Invalid status change (e.g., IN_PROGRESS → DRAFT)
INSUFFICIENT_INVENTORY400Not enough stock for inputs on completion
INVALID_PRODUCT_INVENTORY_TYPE400Wrong product type (e.g., finished_good as input)
PRODUCTION_RUN_DELETE_NOT_ALLOWED400Deleting non-DRAFT/non-CANCELLED run

Create Example

POST /production-runs
{
"businessId": "{{BUSINESS_ID}}",
"locationId": "{{LOCATION_ID}}",
"createdBy": "{{USER_ID}}",
"productionRunType": "mixing",
"runDate": "2026-03-25T10:00:00.000Z",
"status": "draft",
"notes": "Morning batch",
"inputs": [
{
"productId": "{{RAW_MATERIAL_ID}}",
"locationId": "{{LOCATION_ID}}",
"quantity": 10,
"unitCost": 5.50,
"sortOrder": 0
}
],
"outputs": [
{
"productId": "{{FINISHED_GOOD_ID}}",
"quantity": 8,
"unitCost": 7.00,
"sortOrder": 0
}
]
}

Create from Formula Example

POST /production-runs
{
"businessId": "{{BUSINESS_ID}}",
"locationId": "{{LOCATION_ID}}",
"createdBy": "{{USER_ID}}",
"productionRunType": "assembly",
"runDate": "2026-03-25T10:00:00.000Z",
"status": "draft",
"productionFormulaId": "{{FORMULA_ID}}",
"plannedOutputQuantity": 50
}

Complete Example

PATCH /production-runs/{{RUN_ID}}
{
"updatedBy": "{{USER_ID}}",
"status": "completed"
}

Key Design Decisions

  1. Atomic line replacement: On update, if lines are provided, all existing lines are deleted and new ones inserted in a single transaction. This avoids complex diffing logic.

  2. Idempotent ledger rows: Each ledger row has an MD5-based idempotency key derived from run ID, line ID, direction, product, location, and quantities. This prevents duplicate ledger entries.

  3. Inventory type enforcement: At completion time, inputs must be raw_material, component, or packaging. Outputs must be finished_good. This is validated against the product table.

  4. Event-driven side effects: Inventory changes emit events (OnInventoryDecreasedEvent, OnInventoryIncreasedEvent) for downstream listeners (e.g., low-stock alerts, cost recalculation).

  5. PDF enrichment on demand: Product and location names are fetched at render time rather than stored on the run. This keeps the data normalized and avoids stale names.