@happyvertical/smrt-manufacturing 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/__smrt-register__.ts","../src/models/BillOfMaterials.ts","../src/collections/BillOfMaterialsCollection.ts","../src/models/BomLine.ts","../src/collections/BomLineCollection.ts","../src/services/BomService.ts","../src/services/ProductionService.ts","../src/services/dispatch-handlers.ts"],"sourcesContent":["/**\n * Self-registers this package's build-time manifest before any @smrt() decorator\n * in the package fires. Fixes issue #1132: in consumer runtimes (tsx, SvelteKit\n * SSR, plain `vite dev`) the decorator's synchronous manifest lookup previously\n * missed because no step populated the global manifest cache — classes got\n * registered with zero fields and `save()` / `toJSON()` silently dropped every\n * declared property.\n *\n * Import this module as the first statement in `src/index.ts` (and from any\n * other entry that loads class modules — e.g. `src/models/index.ts`,\n * `src/collections/index.ts`) so its top-level side effect runs ahead of any\n * class module's @smrt() decorator.\n *\n * Silent no-op in dev/test, where the vitest plugin already populates manifests\n * via a different path. Only needs to succeed in the published dist output.\n *\n * @see https://github.com/happyvertical/smrt/issues/1132\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\n// `new URL('./manifest.json', import.meta.url)` resolves at runtime to the\n// manifest sitting next to this module's compiled output. Vite warns at build\n// time that it cannot pre-resolve the URL; that is the intended behavior —\n// the URL must resolve to dist/manifest.json at runtime, not be inlined.\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\n","/**\n * BillOfMaterials — recipe describing how to produce a finished product.\n *\n * A BOM enumerates the component SKUs (raw materials, trims, sub-assemblies)\n * that must be consumed to produce one unit of a given parent product. The\n * same product may have many BOMs — alternate revisions, alternate recipes,\n * region-specific variations — distinguished by `version` and lifecycle\n * `status`.\n *\n * Strictly industry-neutral: the same model serves apparel, furniture,\n * automotive, CPG, electronics, food production, custom hardware, and any\n * other vertical that builds finished goods from components.\n *\n * Cross-package references (`productId`) are plain string ids — never\n * `@foreignKey()` — so this package can be consumed without pulling in the\n * upstream catalog package.\n *\n * @packageDocumentation\n */\n\nimport {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { BomStatus } from '../types.js';\n\n/**\n * Options accepted by the {@link BillOfMaterials} constructor.\n */\nexport interface BillOfMaterialsOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n productId?: string;\n version?: number;\n effectiveDate?: Date | string | null;\n status?: BomStatus;\n notes?: string;\n currency?: string;\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'manufacturing_boms',\n // Natural key: one BOM per (product, version) per tenant. Re-saving the\n // same combination is an upsert rather than a UNIQUE violation.\n conflictColumns: ['product_id', 'version', 'tenant_id'],\n api: { include: ['list', 'get', 'create', 'update'] },\n mcp: { include: ['list', 'get'] },\n cli: true,\n})\nexport class BillOfMaterials extends SmrtObject {\n /** Tenant scope. `null` means the BOM is a global record. */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * Plain string reference to the upstream product this BOM produces.\n * Typically a `Product.id` (or any STI subtype thereof) from\n * `@happyvertical/smrt-products`. Cross-package id, intentionally not\n * a `@foreignKey()`, so this package does not pull in `smrt-products`.\n */\n @field({ required: true })\n productId: string = '';\n\n /**\n * Monotonically increasing revision number scoped per `productId`. Together\n * with `tenantId` and `productId` this forms the natural key — so\n * `version` is an integer and `0` is reserved by convention for unsaved\n * scratch rows.\n */\n @field({ required: true })\n version: number = 1;\n\n /**\n * Date from which this BOM is intended to take effect for new production\n * runs. Optional — older BOMs imported from legacy systems may have no\n * effective date on file.\n */\n effectiveDate: Date | null = null;\n\n /**\n * Lifecycle state — `draft`, `active`, or `superseded`. See\n * {@link BomStatus} for the semantics.\n *\n * Most queries that pick \"the BOM to use right now\" should filter for\n * `status: 'active'`.\n */\n @field({ required: true })\n status: BomStatus = 'draft';\n\n /**\n * Optional free-form notes shown in admin UIs (revision reason, source\n * document reference, sign-off chain, etc.).\n */\n notes: string = '';\n\n /**\n * ISO 4217 currency code for cost rollups. Mostly a hint for the rollup\n * service — defaults to `'USD'` so older imports without a currency\n * still produce sensible numbers.\n */\n currency: string = 'USD';\n\n constructor(options: BillOfMaterialsOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.productId !== undefined) this.productId = options.productId;\n if (options.version !== undefined) this.version = options.version;\n if (options.effectiveDate !== undefined) {\n this.effectiveDate =\n options.effectiveDate === null\n ? null\n : options.effectiveDate instanceof Date\n ? options.effectiveDate\n : new Date(options.effectiveDate);\n }\n if (options.status !== undefined) this.status = options.status;\n if (options.notes !== undefined) this.notes = options.notes;\n if (options.currency !== undefined) this.currency = options.currency;\n }\n}\n","/**\n * BillOfMaterialsCollection — query helpers for {@link BillOfMaterials} rows.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { BillOfMaterials } from '../models/BillOfMaterials.js';\nimport type { BomStatus } from '../types.js';\n\nexport class BillOfMaterialsCollection extends SmrtCollection<BillOfMaterials> {\n static readonly _itemClass = BillOfMaterials;\n\n /**\n * Return every BOM for the given upstream product id, newest version\n * first. Useful for \"show me all revisions of this product's recipe\"\n * admin screens.\n */\n async findByProduct(productId: string): Promise<BillOfMaterials[]> {\n return this.list({ where: { productId }, orderBy: 'version DESC' });\n }\n\n /**\n * Return the currently `active` BOM for the given product, or `null` if\n * none has been activated yet. When multiple `active` rows exist (which\n * should not happen but can arise during partial migrations), the\n * highest-version row wins.\n */\n async findActiveForProduct(\n productId: string,\n ): Promise<BillOfMaterials | null> {\n const matches = await this.list({\n where: { productId, status: 'active' },\n orderBy: 'version DESC',\n limit: 1,\n });\n return matches[0] ?? null;\n }\n\n /**\n * Return every BOM with the given lifecycle status across all products.\n * Mostly useful for admin dashboards (e.g. \"show every draft BOM\").\n */\n async findByStatus(status: BomStatus): Promise<BillOfMaterials[]> {\n return this.list({ where: { status }, orderBy: 'productId ASC' });\n }\n}\n","/**\n * BomLine — one component on a {@link BillOfMaterials}.\n *\n * Each line declares the component SKU required to produce one unit of the\n * parent product, the quantity-per-unit, the unit of measure (open-ended\n * string — `yards`, `each`, `grams`, `kg`, `m`, anything else the\n * application needs), and an optional `wastePercent` so cost rollups and\n * requirements explosions can account for offcuts, setup losses, and other\n * material consumed but not incorporated into a finished unit.\n *\n * Cross-package references (`componentSkuId`) are plain string ids — never\n * `@foreignKey()` — so this package can ship without a hard runtime\n * dependency on `@happyvertical/smrt-products` (the `Sku` model lives\n * there) or `@happyvertical/smrt-inventory` (which holds the stock\n * levels / movements addressed by the id) even though all three almost\n * always travel together.\n *\n * @packageDocumentation\n */\n\nimport {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n\n/**\n * Options accepted by the {@link BomLine} constructor.\n */\nexport interface BomLineOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n bomId?: string;\n componentSkuId?: string;\n qtyPerUnit?: number;\n uom?: string;\n wastePercent?: number;\n notes?: string;\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n tableName: 'manufacturing_bom_lines',\n // Natural key: one row per `(bom, component)` per tenant. Re-saving the\n // same combination updates the row in place rather than violating the\n // UNIQUE constraint. Callers that want to declare two distinct lines for\n // the same component (e.g. \"main\" vs \"trim\" passes) should use distinct\n // component SKUs or model the variants in inventory.\n conflictColumns: ['bom_id', 'component_sku_id', 'tenant_id'],\n api: { include: ['list', 'get', 'create', 'update'] },\n mcp: { include: ['list', 'get'] },\n cli: true,\n})\nexport class BomLine extends SmrtObject {\n /** Tenant scope. `null` means the line is a global record. */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * Plain string reference to the parent {@link BillOfMaterials}. Within\n * this package this is the only \"structural\" foreign key — but it is\n * still stored as a plain string to keep with the package's\n * cross-reference convention and to avoid coupling the schema generator\n * to a particular load order.\n */\n @field({ required: true })\n bomId: string = '';\n\n /**\n * Plain string reference to the component SKU id (raw material, trim,\n * sub-assembly). The `Sku` model itself lives in\n * `@happyvertical/smrt-products`; `@happyvertical/smrt-inventory` uses\n * this id to track stock levels and movements. Cross-package id;\n * never `@foreignKey()`.\n */\n @field({ required: true })\n componentSkuId: string = '';\n\n /**\n * Quantity of the component required per unit of finished product, in\n * the unit declared by {@link uom}. Decimal so fractional usage like\n * `1.75 yards` or `12.5 grams` round-trips correctly.\n */\n @field({ type: 'decimal' })\n qtyPerUnit: number = 0.0;\n\n /**\n * Unit of measure for {@link qtyPerUnit}. Open-ended string so the\n * framework does not constrain the vocabulary — `'yards'`, `'each'`,\n * `'grams'`, `'kg'`, `'m'`, `'litres'`, `'sq_ft'`, anything.\n *\n * Conventions follow the upstream `Material.uom` value when applicable.\n */\n uom: string = 'each';\n\n /**\n * Expected percentage waste on this line. Adds to the effective quantity\n * consumed during cost rollups and requirements explosions so a cutting\n * yield of 10% surfaces in the total cost without callers having to\n * pre-inflate `qtyPerUnit`. Stored as a percent (`10` means 10%), not a\n * fraction (`0.10`).\n */\n @field({ type: 'decimal' })\n wastePercent: number = 0.0;\n\n /**\n * Optional free-form notes — e.g. \"primary structural component\",\n * \"60-second cure\", or \"matched batch only\".\n */\n notes: string = '';\n\n constructor(options: BomLineOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.bomId !== undefined) this.bomId = options.bomId;\n if (options.componentSkuId !== undefined)\n this.componentSkuId = options.componentSkuId;\n if (options.qtyPerUnit !== undefined) this.qtyPerUnit = options.qtyPerUnit;\n if (options.uom !== undefined) this.uom = options.uom;\n if (options.wastePercent !== undefined)\n this.wastePercent = options.wastePercent;\n if (options.notes !== undefined) this.notes = options.notes;\n }\n\n /**\n * Effective quantity consumed per produced unit, including waste. Used\n * by {@link BomService} for cost rollups and requirements explosions.\n *\n * `effectiveQty = qtyPerUnit * (1 + wastePercent / 100)`\n */\n effectiveQtyPerUnit(): number {\n const qty = Number(this.qtyPerUnit ?? 0);\n const waste = Number(this.wastePercent ?? 0);\n return qty * (1 + waste / 100);\n }\n}\n","/**\n * BomLineCollection — query helpers for {@link BomLine} rows.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { BomLine } from '../models/BomLine.js';\n\nexport class BomLineCollection extends SmrtCollection<BomLine> {\n static readonly _itemClass = BomLine;\n\n /**\n * Return every line that belongs to the given BOM. Caller-stable order\n * helps make cost rollups and requirements explosions deterministic.\n */\n async findByBom(bomId: string): Promise<BomLine[]> {\n return this.list({ where: { bomId }, orderBy: 'componentSkuId ASC' });\n }\n\n /**\n * Return every line that references the given component SKU across all\n * BOMs. Useful for \"where is this material used?\" admin screens and for\n * cost-change impact analysis (a price hike on a raw material affects\n * every BOM in the result).\n */\n async findByComponent(componentSkuId: string): Promise<BomLine[]> {\n return this.list({ where: { componentSkuId }, orderBy: 'bomId ASC' });\n }\n}\n","/**\n * BomService — cost rollup, requirements explosion, and \"can we make this?\"\n * stock-availability checks for a {@link BillOfMaterials}.\n *\n * The service is the sanctioned planning surface for BOM-driven workflows.\n * It never mutates stock — it answers questions:\n *\n * - \"How much does it cost to make one unit?\" via {@link computeMaterialCost}\n * - \"What do I need to buy / pull to make N units?\" via {@link explodeRequirements}\n * - \"Do I have enough on hand?\" via {@link canProduce}\n *\n * Construct via {@link BomService.create} — the static factory wires up\n * the BOM, BOM-line, and inventory collections so a single `db` (or a\n * pre-built {@link StockService}) drives everything.\n *\n * Material cost resolution is pluggable. Pass a `costResolver` callback to\n * supply unit costs from anywhere — `@happyvertical/smrt-products`\n * (`Material.costPerUnit`), a purchase-order rolling average, a vendor\n * price book, anything. When no resolver is supplied, costs default to\n * `0` and the per-line `costUnavailable` flag is set so UIs can surface\n * \"missing cost data\" rather than silently rolling up a $0 BOM.\n *\n * @packageDocumentation\n */\n\nimport type { DatabaseConfig } from '@happyvertical/smrt-core';\nimport {\n createStockService,\n type StockService,\n} from '@happyvertical/smrt-inventory';\nimport { BillOfMaterialsCollection } from '../collections/BillOfMaterialsCollection.js';\nimport { BomLineCollection } from '../collections/BomLineCollection.js';\nimport type { BillOfMaterials } from '../models/BillOfMaterials.js';\nimport type { BomLine } from '../models/BomLine.js';\nimport {\n type BomCostRollup,\n type BomLineCost,\n BomNotFoundError,\n type CanProduceResult,\n type MaterialRequirement,\n type MaterialShortage,\n} from '../types.js';\n\n/**\n * Callback signature for resolving the unit cost of a component SKU.\n *\n * Implementations should return the latest known cost in the BOM's\n * currency, or `null` (or `undefined`) when no cost is available. The\n * service treats unresolved costs as `0` and marks the affected line so\n * callers can warn the user.\n *\n * @example\n * ```typescript\n * import { MaterialCollection } from '@happyvertical/smrt-products/models';\n *\n * const materials = await MaterialCollection.create({ db });\n *\n * const bom = await BomService.create({\n * db,\n * costResolver: async (componentSkuId) => {\n * const sku = await skus.get(componentSkuId);\n * if (!sku?.productId) return null;\n * const material = await materials.get(sku.productId);\n * return material?.costPerUnit ?? null;\n * },\n * });\n * ```\n */\nexport type ComponentCostResolver = (\n componentSkuId: string,\n) => Promise<number | null | undefined> | number | null | undefined;\n\n/**\n * Options accepted by {@link BomService.create}.\n *\n * Either provide a `db` for the service to construct its own internal\n * collections + {@link StockService}, or pass a pre-built `stockService`\n * to share one across subsystems.\n */\nexport type BomServiceOptions = {\n /**\n * Optional resolver that returns the latest known unit cost for a\n * component SKU. See {@link ComponentCostResolver}. When omitted, all\n * costs default to `0` and per-line `costUnavailable` is set.\n */\n costResolver?: ComponentCostResolver;\n} & (\n | { db: DatabaseConfig; stockService?: StockService }\n | { stockService: StockService; db?: DatabaseConfig }\n);\n\n/**\n * BOM-driven planning service. See module documentation for the role\n * this plays in the manufacturing pipeline.\n *\n * @example\n * ```typescript\n * const bomService = await BomService.create({ db });\n * const rollup = await bomService.computeMaterialCost(bom.id!);\n * const requirements = await bomService.explodeRequirements(bom.id!, 100);\n * const check = await bomService.canProduce(bom.id!, 100);\n * ```\n */\nexport class BomService {\n private constructor(\n public readonly boms: BillOfMaterialsCollection,\n public readonly lines: BomLineCollection,\n public readonly stockService: StockService,\n private readonly costResolver: ComponentCostResolver | undefined,\n ) {}\n\n /** Factory — prefer {@link createBomService}. */\n static async create(options: BomServiceOptions): Promise<BomService> {\n const stockService =\n options.stockService ?? (await buildStockService(options));\n // Share the same db that the StockService uses so reads always hit\n // one connection / pool. StockService exposes its `db` as a public\n // field specifically so downstream services can compose against it\n // without reaching into Collection internals.\n const sharedDb = options.db ?? stockService.db;\n const [boms, lines] = await Promise.all([\n BillOfMaterialsCollection.create({ db: sharedDb }),\n BomLineCollection.create({ db: sharedDb }),\n ]);\n return new BomService(boms, lines, stockService, options.costResolver);\n }\n\n /**\n * Walk every {@link BomLine} for the given BOM, resolve each line's\n * component cost, apply waste, and return the rolled-up material cost\n * per produced unit along with a per-line breakdown.\n *\n * Throws {@link BomNotFoundError} when the BOM does not exist.\n *\n * Lines whose cost cannot be resolved contribute `0` to the total and\n * set `costUnavailable: true` on their breakdown row; the aggregate\n * `hasMissingCosts` flag mirrors this so callers can surface a UI\n * warning.\n */\n async computeMaterialCost(bomId: string): Promise<BomCostRollup> {\n const bom = await this.requireBom(bomId);\n const lines = await this.lines.findByBom(bomId);\n const lineBreakdown: BomLineCost[] = [];\n let totalCost = 0;\n let hasMissingCosts = false;\n\n for (const line of lines) {\n const effectiveQty = line.effectiveQtyPerUnit();\n const resolved = await this.resolveCost(line.componentSkuId);\n const unitCost = resolved ?? 0;\n const costUnavailable = resolved === null || resolved === undefined;\n const lineCost = unitCost * effectiveQty;\n if (costUnavailable) {\n hasMissingCosts = true;\n } else {\n totalCost += lineCost;\n }\n\n lineBreakdown.push({\n componentSkuId: line.componentSkuId,\n qtyPerUnit: Number(line.qtyPerUnit ?? 0),\n wastePercent: Number(line.wastePercent ?? 0),\n effectiveQty,\n unitCost,\n lineCost,\n uom: line.uom,\n costUnavailable,\n });\n }\n\n return {\n bomId,\n totalCost,\n currency: bom.currency || 'USD',\n lineBreakdown,\n hasMissingCosts,\n };\n }\n\n /**\n * Return a \"shopping list\" of materials needed to produce `qty` units of\n * the parent product against the given BOM. Lines that reference the\n * same component SKU are summed so each `componentSkuId` appears once\n * in the result.\n *\n * Does NOT mutate stock — purely a planning helper. Use\n * {@link ProductionService.consumeMaterials} when you're ready to write\n * stock movements.\n *\n * Throws {@link BomNotFoundError} when the BOM does not exist; throws a\n * plain `Error` when `qty` is not a positive finite number.\n */\n async explodeRequirements(\n bomId: string,\n qty: number,\n ): Promise<MaterialRequirement[]> {\n assertPositiveQty(qty, 'explodeRequirements');\n await this.requireBom(bomId);\n const lines = await this.lines.findByBom(bomId);\n\n // Sum across duplicate component SKUs. Keep the first-seen `uom` per\n // SKU; if two lines disagree on `uom`, we surface the first one and\n // assume the caller noticed during BOM authoring.\n const aggregate = new Map<string, MaterialRequirement>();\n for (const line of lines) {\n const effectiveQty = line.effectiveQtyPerUnit() * qty;\n const existing = aggregate.get(line.componentSkuId);\n if (existing) {\n existing.totalQty += effectiveQty;\n } else {\n aggregate.set(line.componentSkuId, {\n componentSkuId: line.componentSkuId,\n totalQty: effectiveQty,\n uom: line.uom,\n });\n }\n }\n\n return Array.from(aggregate.values());\n }\n\n /**\n * Check whether the requirements for producing `qty` units against the\n * given BOM are currently satisfied by available stock. Returns\n * `{ ok: true, shortages: [] }` when every component has enough\n * `available` stock across all locations; `{ ok: false, shortages: [...] }`\n * with one entry per insufficient component otherwise.\n *\n * Available stock is summed across every location (the planning\n * question is \"do we have it at all?\"; the operational question of\n * \"where do we pull from?\" is left to the caller of\n * {@link ProductionService.consumeMaterials}).\n *\n * Throws {@link BomNotFoundError} when the BOM does not exist.\n */\n async canProduce(bomId: string, qty: number): Promise<CanProduceResult> {\n const requirements = await this.explodeRequirements(bomId, qty);\n\n // Fan the per-component availability queries out in parallel so a\n // BOM with N components costs O(1) round-trips of latency instead\n // of O(N). `totalForSku` is a single aggregate read per component\n // — independent across SKUs — so there's no ordering or\n // consistency concern from running them concurrently.\n const availabilities = await Promise.all(\n requirements.map(async (requirement) => ({\n requirement,\n available: await this.stockService.levels.totalForSku(\n requirement.componentSkuId,\n 'available',\n ),\n })),\n );\n\n const shortages: MaterialShortage[] = [];\n for (const { requirement, available } of availabilities) {\n if (available < requirement.totalQty) {\n shortages.push({\n componentSkuId: requirement.componentSkuId,\n requested: requirement.totalQty,\n available,\n });\n }\n }\n\n if (shortages.length === 0) {\n return { ok: true, shortages: [] };\n }\n return { ok: false, shortages };\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Internal helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Fetch a BOM by id or throw {@link BomNotFoundError}. Centralises the\n * \"missing BOM\" error path so callers don't have to repeat the check.\n */\n private async requireBom(bomId: string): Promise<BillOfMaterials> {\n if (!bomId) throw new BomNotFoundError(bomId);\n const bom = await this.boms.get(bomId);\n if (!bom) throw new BomNotFoundError(bomId);\n return bom;\n }\n\n /**\n * Run the supplied {@link ComponentCostResolver} for the given component\n * SKU. Returns `null` when no resolver is registered or when the\n * resolver returns `null` / `undefined`.\n */\n private async resolveCost(componentSkuId: string): Promise<number | null> {\n if (!this.costResolver) return null;\n const value = await this.costResolver(componentSkuId);\n if (value === null || value === undefined) return null;\n if (!Number.isFinite(value)) return null;\n return Number(value);\n }\n}\n\n/**\n * Reject zero / negative / non-finite quantities up front for service\n * methods that accept production-run quantities.\n */\nfunction assertPositiveQty(qty: number, op: string): void {\n if (!Number.isFinite(qty) || qty <= 0) {\n throw new Error(`${op}: qty must be a positive finite number (got ${qty})`);\n }\n}\n\n/**\n * Build a `StockService` from a `db` config in the options. Called only\n * when the caller did not pass a pre-built `stockService` — the option\n * union guarantees `db` is present in that case, but TypeScript can't\n * narrow the discriminated union from a `??` value check, so this helper\n * validates at runtime with a clear message.\n */\nasync function buildStockService(\n options: BomServiceOptions,\n): Promise<StockService> {\n if (!options.db) {\n throw new Error(\n 'BomService.create: either `db` or `stockService` is required',\n );\n }\n return createStockService({ db: options.db });\n}\n\n/**\n * Convenience factory. Returns a fully-initialized {@link BomService}.\n */\nexport async function createBomService(\n options: BomServiceOptions,\n): Promise<BomService> {\n return BomService.create(options);\n}\n\n// Re-export the BomLine type alias to ease typing in adjacent code that\n// wires `BomLine[]` payloads through this service.\nexport type { BomLine };\n","/**\n * ProductionService — the operational bridge from a production order to\n * actual stock-mutating consume/produce events.\n *\n * Production orders themselves live in `@happyvertical/smrt-commerce` as a\n * `Contract` STI subtype. This service does NOT modify those rows; it\n * simply reads the order's `productId`, looks up the active BOM, and\n * writes the right stock movements through {@link StockService}.\n *\n * Three methods:\n *\n * - {@link consumeMaterials} — for each BOM line, deduct\n * `qtyPerUnit * (1 + wastePercent / 100) * runQty` from `available` at\n * the production location. Stamps every movement with\n * `sourceType: 'ProductionOrder'` + the production order id so audit\n * queries can roll them up later. Atomic across BOM lines (one tx).\n * - {@link produceFinishedGoods} — receive `runQty` of the finished\n * product's SKU into `available` at the production location. The caller\n * supplies the SKU id (the production order references a *product*, not\n * a *SKU*; in practice the production-order-level \"what we are making\"\n * gets resolved to a specific SKU per variant on the line items).\n * - {@link runProduction} — consume materials AND receive finished\n * goods in one transaction. Use this when consume + produce should be\n * jointly atomic (e.g. a make-to-stock workflow where the factory step\n * is invisible). Calling `consumeMaterials` then `produceFinishedGoods`\n * separately is NOT jointly atomic — each opens its own transaction\n * and a failure between them can leave materials deducted with no\n * finished SKU receipt to balance it.\n *\n * ## Location convention (explicit-arg design)\n *\n * The original design considered carrying `originLocationId` on a per-\n * package mixin of `ProductionOrder`, but that adds a meta field to a\n * model owned by a sibling package. The cleaner design — and the one\n * shipped here — keeps the location explicit on the call:\n *\n * ```typescript\n * await production.consumeMaterials(order, { locationId, qty });\n * await production.produceFinishedGoods(order, { locationId, qty, finishedSkuId });\n * ```\n *\n * Callers that want a \"default factory\" convention can stash the\n * `locationId` on their own production-order helper or wrap this service\n * with a thin domain-level façade.\n *\n * @packageDocumentation\n */\n\nimport type { DatabaseConfig } from '@happyvertical/smrt-core';\nimport type {\n StockMovementReason,\n StockMutationOptions,\n StockService,\n} from '@happyvertical/smrt-inventory';\nimport { createStockService } from '@happyvertical/smrt-inventory';\nimport { BillOfMaterialsCollection } from '../collections/BillOfMaterialsCollection.js';\nimport { BomLineCollection } from '../collections/BomLineCollection.js';\nimport type { BillOfMaterials } from '../models/BillOfMaterials.js';\nimport type { BomLine } from '../models/BomLine.js';\nimport { BomNotFoundError, NoActiveBomForProductError } from '../types.js';\n\n/**\n * Minimal shape of a production order this service needs. We deliberately\n * accept this structural type rather than importing the `ProductionOrder`\n * class from `@happyvertical/smrt-commerce` — that would either pull a\n * dep on commerce into manufacturing or force commerce to extend a\n * manufacturing interface. Plain duck typing keeps the dependency arrow\n * pointing the way the architecture asks for (manufacturing builds on\n * inventory; commerce stays orthogonal).\n */\nexport interface ProductionOrderRef {\n /** Production order row id. Used for `sourceId` on every movement. */\n id?: string | null;\n /**\n * Plain string reference to the upstream product the order is asked to\n * produce. Used to look up the active BOM for the consume step.\n */\n productId?: string;\n /**\n * Optional explicit BOM id. When set, {@link consumeMaterials} uses this\n * row even if the product has a different `active` BOM — useful when an\n * order was kicked off against a specific revision.\n */\n bomId?: string;\n}\n\n/**\n * Per-call options for {@link ProductionService.consumeMaterials}.\n */\nexport interface ConsumeMaterialsOptions {\n /**\n * Inventory location id (warehouse / factory) where the materials are\n * pulled from. Required because the production order itself does not\n * carry a stocking location — see \"Location convention\" in the module\n * doc.\n */\n locationId: string;\n /**\n * Run quantity — how many finished units the BOM is being multiplied\n * by. Each BOM line's `effectiveQtyPerUnit()` is multiplied by this\n * number before being deducted from `available` stock.\n */\n qty: number;\n /**\n * Optional override of the reason code stamped on each\n * {@link StockMovement}. Defaults to `'production_consume'`.\n */\n reasonCode?: StockMovementReason;\n /**\n * Optional free-form note attached to every emitted movement.\n */\n note?: string;\n}\n\n/**\n * Per-call options for {@link ProductionService.produceFinishedGoods}.\n */\nexport interface ProduceFinishedGoodsOptions {\n /**\n * Inventory location id (warehouse / factory) where the finished goods\n * land. Required for the same reason as {@link ConsumeMaterialsOptions.locationId}.\n */\n locationId: string;\n /**\n * Quantity of finished goods produced. Always positive.\n */\n qty: number;\n /**\n * The SKU id of the finished product to receive into `available`. The\n * production order references a `productId` (a `Product` from\n * `@happyvertical/smrt-products`), but a product typically has multiple\n * SKUs (one per variant). The caller decides which concrete SKU each\n * line of the run produces.\n */\n finishedSkuId: string;\n /**\n * Optional override of the reason code stamped on the\n * {@link StockMovement}. Defaults to `'production_produce'`.\n */\n reasonCode?: StockMovementReason;\n /**\n * Optional free-form note attached to the emitted movement.\n */\n note?: string;\n}\n\n/**\n * Options accepted by {@link ProductionService.create}.\n *\n * Either pass a `db` for the service to construct its own\n * collections + {@link StockService}, or share a pre-built `stockService`\n * across subsystems.\n */\nexport type ProductionServiceOptions =\n | { db: DatabaseConfig; stockService?: StockService }\n | { stockService: StockService; db?: DatabaseConfig };\n\n/**\n * A single emitted stock movement, returned by\n * {@link ProductionService.consumeMaterials} so callers can audit / log\n * the impact of a production run without re-querying.\n */\nexport interface ConsumeResult {\n /** Component SKU id whose stock was deducted. */\n componentSkuId: string;\n /** Quantity deducted (already inflated for waste). */\n qty: number;\n /** Location the stock was deducted from. */\n locationId: string;\n}\n\n/**\n * Result of {@link ProductionService.produceFinishedGoods}.\n */\nexport interface ProduceResult {\n /** Finished SKU id whose stock increased. */\n finishedSkuId: string;\n /** Quantity received. */\n qty: number;\n /** Location the stock was received into. */\n locationId: string;\n}\n\n/**\n * Result of {@link ProductionService.runProduction} — the consume + produce\n * pair, run inside one transaction.\n */\nexport interface RunProductionResult {\n consumed: ConsumeResult[];\n produced: ProduceResult;\n}\n\n/**\n * Operational bridge between a production order and the inventory ledger.\n */\nexport class ProductionService {\n private constructor(\n /**\n * BOM collection bound to the OUTER (non-tx) database. Used for\n * pre-flight reads in `prepareConsume()` — those run before the\n * stock-service transaction opens, by design (read-then-tx keeps\n * the transaction window narrow and rollback only covers actual\n * mutations). DO NOT add BOM writes that depend on the stock-tx\n * rolling them back together — this collection is not tx-scoped\n * and writes here would commit independently. If joint BOM-plus-\n * stock mutations are ever needed, build tx-scoped BOM collections\n * inside the `stockService.withTransaction` callback (mirror the\n * pattern in StockService.withTransaction itself).\n */\n public readonly boms: BillOfMaterialsCollection,\n /**\n * BomLine collection bound to the OUTER database. Same caveat as\n * {@link boms} — pre-flight reads only.\n */\n public readonly lines: BomLineCollection,\n public readonly stockService: StockService,\n ) {}\n\n /** Factory — prefer {@link createProductionService}. */\n static async create(\n options: ProductionServiceOptions,\n ): Promise<ProductionService> {\n const stockService =\n options.stockService ?? (await buildStockService(options));\n // Share the StockService's db so production + stock writes ride one\n // connection/pool. StockService.db is exposed publicly for this case.\n const sharedDb = options.db ?? stockService.db;\n const [boms, lines] = await Promise.all([\n BillOfMaterialsCollection.create({ db: sharedDb }),\n BomLineCollection.create({ db: sharedDb }),\n ]);\n return new ProductionService(boms, lines, stockService);\n }\n\n /**\n * Walk every BOM line for the production order's BOM and deduct\n * `effectiveQtyPerUnit() * qty` from `available` at `locationId`. Each\n * deduction goes through {@link StockService.adjust} with `sourceType:\n * 'ProductionOrder'` and `sourceId: order.id`. Returns the list of\n * movements emitted so callers can log / surface them.\n *\n * **Atomic across lines**: all per-line deductions and their audit\n * movements run inside a single `stockService.withTransaction(...)`\n * scope. An `InsufficientStockError` on line N+1 rolls back lines 1..N\n * so production-order posting never leaves materials half-consumed.\n * Pre-flight with {@link BomService.canProduce} when you'd rather know\n * upfront than discover the shortfall mid-run.\n *\n * Throws {@link NoActiveBomForProductError} if no BOM id is supplied\n * on the order *and* no active BOM exists for the order's `productId`.\n * Throws {@link BomNotFoundError} if `order.bomId` is supplied but\n * doesn't resolve. Throws plain `Error` on missing `locationId`,\n * non-positive `qty`, or when both `order.bomId` and `order.productId`\n * are absent (a programmer error — neither input identifies a BOM).\n * Re-throws `InsufficientStockError` from `StockService.adjust` if a\n * line would drive `available` below zero.\n */\n async consumeMaterials(\n order: ProductionOrderRef,\n options: ConsumeMaterialsOptions,\n ): Promise<ConsumeResult[]> {\n const ctx = await this.prepareConsume(order, options);\n return this.stockService.withTransaction(async (tx) =>\n this.consumeMaterialsWith(tx, ctx),\n );\n }\n\n /**\n * Receive `qty` of the finished SKU into `available` at `locationId`.\n * Goes through {@link StockService.receive} with `sourceType:\n * 'ProductionOrder'` and `sourceId: order.id`. Returns a single\n * {@link ProduceResult} describing what landed.\n *\n * Throws plain `Error` on missing required arguments or non-positive\n * `qty`.\n */\n async produceFinishedGoods(\n order: ProductionOrderRef,\n options: ProduceFinishedGoodsOptions,\n ): Promise<ProduceResult> {\n const ctx = prepareProduce(order, options);\n return this.stockService.withTransaction(async (tx) =>\n this.produceFinishedGoodsWith(tx, ctx),\n );\n }\n\n /**\n * Atomically consume materials AND receive finished goods for one\n * production run.\n *\n * Both calls share a single `stockService.withTransaction(...)` scope:\n *\n * - Materials are deducted line-by-line via {@link StockService.adjust}.\n * - Finished goods are received via {@link StockService.receive}.\n * - If ANY step throws — a BOM-line shortage, the produce-leg adapter\n * failing, a tenancy mismatch surfaced by an interceptor — every\n * write in the transaction rolls back. The materials are restored\n * and the finished SKU's `available` row is unchanged. Callers\n * never observe \"materials deducted but finished goods missing\".\n *\n * Use this when consume + produce should be one indivisible event —\n * e.g. a make-to-stock workflow posting `production_order:completed`\n * where the shop floor step is invisible to the ledger.\n *\n * The two-call form (call {@link consumeMaterials} then {@link\n * produceFinishedGoods} separately) is still appropriate when the\n * factory step is a real wall-clock gap that other observers need to\n * see — work-in-progress dashboards, partial-run reporting, etc.\n *\n * Throws the same errors as the individual methods: missing args\n * (plain `Error`), unresolvable BOM ({@link BomNotFoundError} /\n * {@link NoActiveBomForProductError}), or `InsufficientStockError`\n * if a line would drive `available` below zero.\n */\n async runProduction(\n order: ProductionOrderRef,\n options: {\n consume: ConsumeMaterialsOptions;\n produce: ProduceFinishedGoodsOptions;\n },\n ): Promise<RunProductionResult> {\n const consumeCtx = await this.prepareConsume(order, options.consume);\n const produceCtx = prepareProduce(order, options.produce);\n return this.stockService.withTransaction(async (tx) => {\n const consumed = await this.consumeMaterialsWith(tx, consumeCtx);\n const produced = await this.produceFinishedGoodsWith(tx, produceCtx);\n return { consumed, produced };\n });\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Internal helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Validate consume args, resolve the BOM + lines, and pre-compute the\n * mutation attribution. Anything that can throw without a tx open\n * happens here so the transaction we open later doesn't have to roll\n * back over a validation miss.\n */\n private async prepareConsume(\n order: ProductionOrderRef,\n options: ConsumeMaterialsOptions,\n ): Promise<ConsumeContext> {\n assertLocationId(options.locationId, 'consumeMaterials');\n assertPositiveQty(options.qty, 'consumeMaterials');\n const orderId = order.id ?? '';\n if (!orderId) {\n throw new Error(\n 'consumeMaterials: order.id is required for source attribution',\n );\n }\n const bom = await this.resolveBom(order);\n const lines = await this.lines.findByBom(bom.id!);\n return {\n orderId,\n lines,\n runQty: options.qty,\n locationId: options.locationId,\n mutationOptions: {\n sourceType: 'ProductionOrder',\n sourceId: orderId,\n reasonCode: options.reasonCode ?? 'production_consume',\n note: options.note,\n },\n };\n }\n\n /**\n * Execute the consume leg against a caller-supplied tx-bound\n * {@link StockService}. Public entry points (`consumeMaterials`,\n * `runProduction`) open the tx and call through here. Each per-line\n * `tx.adjust(...)` shares the same transaction so a shortfall on\n * line N+1 rolls back lines 1..N.\n */\n private async consumeMaterialsWith(\n tx: StockService,\n ctx: ConsumeContext,\n ): Promise<ConsumeResult[]> {\n const emitted: ConsumeResult[] = [];\n for (const line of ctx.lines) {\n const qty = line.effectiveQtyPerUnit() * ctx.runQty;\n if (qty <= 0) continue;\n await tx.adjust(\n line.componentSkuId,\n ctx.locationId,\n -qty,\n ctx.mutationOptions,\n );\n emitted.push({\n componentSkuId: line.componentSkuId,\n qty,\n locationId: ctx.locationId,\n });\n }\n return emitted;\n }\n\n /**\n * Execute the produce leg against a caller-supplied tx-bound\n * {@link StockService}. Public entry points (`produceFinishedGoods`,\n * `runProduction`) open the tx and call through here.\n */\n private async produceFinishedGoodsWith(\n tx: StockService,\n ctx: ProduceContext,\n ): Promise<ProduceResult> {\n await tx.receive(\n ctx.finishedSkuId,\n ctx.locationId,\n ctx.qty,\n ctx.mutationOptions,\n );\n return {\n finishedSkuId: ctx.finishedSkuId,\n qty: ctx.qty,\n locationId: ctx.locationId,\n };\n }\n\n /**\n * Resolve the BOM for a production order. Order of preference:\n *\n * 1. Explicit `order.bomId` if supplied — the caller pinned a specific\n * revision; we honor that pin or fail. We deliberately do NOT fall\n * back to the active BOM when `order.bomId` is stale or mistyped,\n * because silently switching recipes would have the production run\n * consume materials against a different BOM than the order locked.\n * 2. The active BOM for `order.productId`.\n *\n * Throws {@link BomNotFoundError} when an explicit `order.bomId` is\n * supplied but doesn't resolve to a row.\n *\n * Throws {@link NoActiveBomForProductError} when no `bomId` was supplied\n * and the product has no active BOM.\n *\n * Throws plain `Error` when neither `order.bomId` nor `order.productId`\n * is supplied — there's nothing to resolve against, so we surface that\n * as a programmer error with a distinct message rather than overload\n * `NoActiveBomForProductError` with an empty product id.\n */\n private async resolveBom(\n order: ProductionOrderRef,\n ): Promise<BillOfMaterials> {\n if (order.bomId) {\n const bom = await this.boms.get(order.bomId);\n if (!bom) throw new BomNotFoundError(order.bomId);\n return bom;\n }\n const productId = order.productId ?? '';\n if (!productId) {\n // The order carries neither an explicit `bomId` nor a `productId`,\n // so there is nothing for us to resolve against. Surface that as a\n // distinct, actionable error rather than constructing\n // `NoActiveBomForProductError('')` — the empty-string message\n // (\"No active Bill of Materials found for product: ...\") buries\n // the actual misuse (missing input field) under a SQL-flavoured\n // \"no row found\" error that doesn't reflect what went wrong.\n throw new Error(\n 'consumeMaterials: order.productId is required when order.bomId is not supplied',\n );\n }\n const active = await this.boms.findActiveForProduct(productId);\n if (!active) throw new NoActiveBomForProductError(productId);\n return active;\n }\n}\n\n/**\n * Pre-computed consume work that an open transaction can apply without\n * any further validation. Built by {@link ProductionService.prepareConsume}.\n */\ninterface ConsumeContext {\n orderId: string;\n lines: BomLine[];\n runQty: number;\n locationId: string;\n mutationOptions: StockMutationOptions;\n}\n\n/**\n * Pre-computed produce work that an open transaction can apply without\n * any further validation. Built by {@link prepareProduce}.\n */\ninterface ProduceContext {\n orderId: string;\n finishedSkuId: string;\n qty: number;\n locationId: string;\n mutationOptions: StockMutationOptions;\n}\n\n/**\n * Validate produce args and pre-compute the mutation attribution.\n * Mirrors {@link ProductionService.prepareConsume}; it's a free function\n * because the produce leg doesn't need access to any collection state.\n */\nfunction prepareProduce(\n order: ProductionOrderRef,\n options: ProduceFinishedGoodsOptions,\n): ProduceContext {\n assertLocationId(options.locationId, 'produceFinishedGoods');\n assertPositiveQty(options.qty, 'produceFinishedGoods');\n if (!options.finishedSkuId) {\n throw new Error(\n 'produceFinishedGoods: finishedSkuId is required (the production order references a productId; the caller picks which SKU is being produced)',\n );\n }\n const orderId = order.id ?? '';\n if (!orderId) {\n throw new Error(\n 'produceFinishedGoods: order.id is required for source attribution',\n );\n }\n return {\n orderId,\n finishedSkuId: options.finishedSkuId,\n qty: options.qty,\n locationId: options.locationId,\n mutationOptions: {\n sourceType: 'ProductionOrder',\n sourceId: orderId,\n reasonCode: options.reasonCode ?? 'production_produce',\n note: options.note,\n },\n };\n}\n\nfunction assertPositiveQty(qty: number, op: string): void {\n if (!Number.isFinite(qty) || qty <= 0) {\n throw new Error(`${op}: qty must be a positive finite number (got ${qty})`);\n }\n}\n\nfunction assertLocationId(locationId: string, op: string): void {\n if (!locationId || typeof locationId !== 'string') {\n throw new Error(`${op}: locationId is required`);\n }\n}\n\n/**\n * Build a `StockService` from a `db` config in the options. Called only\n * when the caller did not pass a pre-built `stockService` — the option\n * union guarantees `db` is present in that case, but TypeScript can't\n * narrow the discriminated union from a `??` value check, so this helper\n * validates at runtime with a clear message.\n */\nasync function buildStockService(\n options: ProductionServiceOptions,\n): Promise<StockService> {\n if (!options.db) {\n throw new Error(\n 'ProductionService.create: either `db` or `stockService` is required',\n );\n }\n return createStockService({ db: options.db });\n}\n\n/**\n * Convenience factory. Returns a fully-initialized {@link ProductionService}.\n */\nexport async function createProductionService(\n options: ProductionServiceOptions,\n): Promise<ProductionService> {\n return ProductionService.create(options);\n}\n\n// Re-export the BomLine type so callers don't have to reach into the\n// models barrel just to type service results.\nexport type { BomLine };\n","/**\n * Opt-in DispatchBus subscribers that bridge upstream production-order\n * signals to {@link ProductionService}.\n *\n * **Off by default.** The package never auto-subscribes — consumer apps\n * call {@link installManufacturingDispatchHandlers} from their `smrt.ts`\n * wiring once they have decided they want a `production_order:posted`\n * emit to consume materials and produce finished goods automatically.\n *\n * Two paired toggles let consumers wire only the legs they want:\n *\n * - `installProductionPosted` (default `true`) — on `production_order:posted`,\n * call {@link ProductionService.consumeMaterials} once.\n * - `installProductionCompleted` (default `false`) — on `production_order:completed`,\n * call {@link ProductionService.produceFinishedGoods} once. Off by default\n * because not every shop emits a separate \"completed\" event; lots of\n * workflows expect consume + produce to happen atomically when the\n * order is posted. Set both flags to `true` to get the split lifecycle.\n *\n * @packageDocumentation\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type {\n DatabaseConfig,\n DispatchBus,\n DispatchHandler,\n DispatchMetadata,\n} from '@happyvertical/smrt-core';\nimport type { StockService } from '@happyvertical/smrt-inventory';\nimport {\n createProductionService,\n type ProductionService,\n} from './ProductionService.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/**\n * Shape of a `production_order:posted` payload this handler expects.\n *\n * The producer (typically the application's smrt.ts wiring once a\n * `ProductionOrder` row transitions to the posted state) packs one\n * payload per posted order. The handler consumes materials for every\n * unit declared by `qty`, attributed to `('ProductionOrder', productionOrderId)`\n * on each emitted {@link StockMovement}.\n */\nexport interface ProductionOrderPostedPayload {\n /** Production order row id. Used for `sourceId` on every movement. */\n productionOrderId: string;\n /**\n * Plain string reference to the upstream product the order is asked to\n * produce. Used to resolve the active BOM at consume time.\n */\n productId: string;\n /**\n * Inventory location id (factory / warehouse) where materials are\n * pulled from.\n */\n locationId: string;\n /**\n * Run quantity. Each BOM line's effective qty is multiplied by this\n * before being deducted from `available`.\n */\n qty: number;\n /**\n * Optional explicit BOM id, e.g. when the order locked itself to a\n * specific BOM revision when it was created. Falls back to the active\n * BOM for `productId` if omitted.\n */\n bomId?: string;\n /**\n * Optional finished SKU id. When provided AND the\n * `installProductionPosted` handler is wired to also produce in one\n * shot (which only happens when `installProductionCompleted` is false\n * and `producedOnPosted` is true), this is the SKU to receive into\n * `available`.\n */\n finishedSkuId?: string;\n}\n\n/**\n * Shape of a `production_order:completed` payload this handler expects.\n *\n * Typically emitted when the factory signs off the run and finished\n * goods are physically available to ship. Only used when callers opt in\n * via `installProductionCompleted`.\n */\nexport interface ProductionOrderCompletedPayload {\n /** Production order row id. Used for `sourceId` on the receipt movement. */\n productionOrderId: string;\n /**\n * SKU id of the finished product to receive into `available`. Required.\n */\n finishedSkuId: string;\n /** Inventory location id where the finished goods land. */\n locationId: string;\n /** Quantity received. */\n qty: number;\n}\n\n/**\n * Options accepted by {@link installManufacturingDispatchHandlers}.\n *\n * Provide either a pre-built {@link ProductionService} (when sharing\n * across subsystems) or a `db` / `stockService` for the helper to\n * construct one on first use.\n */\nexport type InstallManufacturingDispatchHandlersOptions = {\n /** Bus to subscribe on. Typically the app-wide DispatchBus. */\n dispatchBus: DispatchBus;\n /**\n * When `true` (default), install the `production_order:posted` handler.\n */\n installProductionPosted?: boolean;\n /**\n * When `true` (default `false`), install the\n * `production_order:completed` handler. Off by default because many\n * workflows want consume+produce to happen on `posted`; opt in to the\n * split lifecycle when your shop emits a separate completion event.\n */\n installProductionCompleted?: boolean;\n /**\n * When `true` (default `false`) AND `installProductionCompleted` is\n * false, the `production_order:posted` handler also calls\n * {@link ProductionService.produceFinishedGoods} after consume —\n * handy for \"make-to-stock instantly\" workflows where the factory\n * step is invisible.\n *\n * Ignored when `installProductionCompleted` is `true` (in that case\n * production is the completed-handler's job).\n */\n producedOnPosted?: boolean;\n} & (\n | {\n productionService: ProductionService;\n db?: DatabaseConfig;\n stockService?: StockService;\n }\n | {\n stockService: StockService;\n db?: DatabaseConfig;\n productionService?: undefined;\n }\n | {\n db: DatabaseConfig;\n stockService?: undefined;\n productionService?: undefined;\n }\n);\n\n/**\n * Result handle returned by {@link installManufacturingDispatchHandlers}.\n * Call `dispose()` to detach the subscribers (mainly useful in tests and\n * on graceful shutdown).\n */\nexport interface InstalledManufacturingDispatchHandlers {\n /** Resolved ProductionService — useful for follow-up writes in the same scope. */\n productionService: ProductionService;\n /** Detach every installed subscriber. Idempotent. */\n dispose(): void;\n}\n\n/**\n * Subscribe production-order handlers to the given DispatchBus.\n *\n * @example\n * ```typescript\n * import { createDispatchBus } from '@happyvertical/smrt-core';\n * import { installManufacturingDispatchHandlers } from '@happyvertical/smrt-manufacturing';\n *\n * const bus = await createDispatchBus({ db });\n * const handlers = await installManufacturingDispatchHandlers({\n * dispatchBus: bus,\n * db,\n * producedOnPosted: true, // consume + produce in one shot\n * });\n * ```\n */\nexport async function installManufacturingDispatchHandlers(\n options: InstallManufacturingDispatchHandlersOptions,\n): Promise<InstalledManufacturingDispatchHandlers> {\n const {\n dispatchBus,\n installProductionPosted = true,\n installProductionCompleted = false,\n producedOnPosted = false,\n } = options;\n\n const productionService =\n options.productionService ?? (await buildProductionService(options));\n\n const installed: Array<{ pattern: string; handler: DispatchHandler }> = [];\n\n if (installProductionPosted) {\n const shouldProduceOnPosted =\n producedOnPosted && !installProductionCompleted;\n const handler: DispatchHandler = async (payload, metadata) => {\n await handleProductionPosted(\n productionService,\n payload as ProductionOrderPostedPayload,\n metadata,\n shouldProduceOnPosted,\n );\n };\n dispatchBus.on('production_order:posted', handler);\n installed.push({ pattern: 'production_order:posted', handler });\n }\n\n if (installProductionCompleted) {\n const handler: DispatchHandler = async (payload, metadata) => {\n await handleProductionCompleted(\n productionService,\n payload as ProductionOrderCompletedPayload,\n metadata,\n );\n };\n dispatchBus.on('production_order:completed', handler);\n installed.push({ pattern: 'production_order:completed', handler });\n }\n\n return {\n productionService,\n dispose() {\n for (const entry of installed) {\n dispatchBus.off(entry.pattern, entry.handler);\n }\n installed.length = 0;\n },\n };\n}\n\nasync function handleProductionPosted(\n productionService: ProductionService,\n payload: ProductionOrderPostedPayload | null | undefined,\n metadata: DispatchMetadata,\n shouldProduce: boolean,\n): Promise<void> {\n // Mirror the inventory-handler observability + strictness pattern:\n // producer contract drift goes silent without this warn (the\n // dispatch bus would just see a quick return and never surface\n // anything), and a non-string `productionOrderId` (e.g. a numeric\n // primary key coming from a Postgres source) would otherwise land\n // as a non-string `sourceId` on every emitted StockMovement —\n // breaking the audit-trail-by-source-id pattern.\n const postedReason = malformedProductionPayloadReason(payload);\n if (postedReason || !payload) {\n warnMalformedPayload(\n 'production_order:posted',\n payload,\n metadata,\n postedReason ?? 'payload is null/undefined',\n );\n return;\n }\n\n // If `producedOnPosted` is enabled, the produce leg is required — not\n // optional. Validate `finishedSkuId` before consuming materials so we\n // don't leave inventory depleted with no corresponding receipt. (Without\n // this guard the consume leg would land, the produce leg would silently\n // skip due to the falsy `finishedSkuId`, and the audit log would carry\n // production_consume movements but no matching production_produce.)\n if (shouldProduce && !payload.finishedSkuId) {\n throw new Error(\n `production_order:posted handler: producedOnPosted is enabled but ` +\n `payload.finishedSkuId is missing (productionOrderId=${payload.productionOrderId}). ` +\n `Either supply finishedSkuId on the event, or disable producedOnPosted ` +\n `and emit production_order:completed separately.`,\n );\n }\n\n const order = {\n id: payload.productionOrderId,\n productId: payload.productId,\n bomId: payload.bomId,\n };\n const consumeNote = metadata.source\n ? `auto-consume via ${metadata.source}`\n : undefined;\n\n // When producedOnPosted is on, route through runProduction so consume\n // and produce share a single transaction. The two calls below would\n // otherwise open separate transactions, and a process crash or adapter\n // error between them would leave materials deducted with no\n // finished-goods receipt to balance the audit ledger.\n if (shouldProduce && payload.finishedSkuId) {\n const produceNote = metadata.source\n ? `auto-produce via ${metadata.source}`\n : undefined;\n await productionService.runProduction(order, {\n consume: {\n locationId: payload.locationId,\n qty: payload.qty,\n note: consumeNote,\n },\n produce: {\n locationId: payload.locationId,\n qty: payload.qty,\n finishedSkuId: payload.finishedSkuId,\n note: produceNote,\n },\n });\n return;\n }\n\n // Consume-only path — the produce leg lives on the separate\n // `production_order:completed` event (or is not modeled at all in\n // this workflow). consumeMaterials is itself atomic across BOM lines.\n await productionService.consumeMaterials(order, {\n locationId: payload.locationId,\n qty: payload.qty,\n note: consumeNote,\n });\n}\n\nasync function handleProductionCompleted(\n productionService: ProductionService,\n payload: ProductionOrderCompletedPayload | null | undefined,\n metadata: DispatchMetadata,\n): Promise<void> {\n const completedReason = malformedProductionPayloadReason(payload);\n if (completedReason || !payload) {\n warnMalformedPayload(\n 'production_order:completed',\n payload,\n metadata,\n completedReason ?? 'payload is null/undefined',\n );\n return;\n }\n await productionService.produceFinishedGoods(\n { id: payload.productionOrderId },\n {\n locationId: payload.locationId,\n qty: payload.qty,\n finishedSkuId: payload.finishedSkuId,\n note: metadata.source ? `auto-produce via ${metadata.source}` : undefined,\n },\n );\n}\n\n/**\n * Determine whether a production-order payload (either\n * `:posted` or `:completed`) is structurally unusable and return a\n * concrete reason naming the specific field that failed validation.\n * Returns `null` when the top-level shape is acceptable. Mirrors the\n * inventory package's per-signal helpers so the log message names the\n * actual failure instead of a hardcoded generic.\n */\nfunction malformedProductionPayloadReason(\n payload: { productionOrderId?: unknown } | null | undefined,\n): string | null {\n if (!payload || typeof payload !== 'object') {\n return 'payload is null/undefined or not an object';\n }\n if (\n !payload.productionOrderId ||\n typeof payload.productionOrderId !== 'string'\n ) {\n return 'missing or non-string `productionOrderId` (required for audit-trail source attribution)';\n }\n return null;\n}\n\n/**\n * Surface dispatch payloads that don't match the documented shape.\n * Mirrors the inventory package's `warnMalformedPayload` so a\n * misshapen `production_order:*` event leaves a trace (signal name,\n * source attribution, specific reason, payload-key summary) instead\n * of vanishing into the dispatch bus's silent-success path. Without\n * this, producer drift only surfaces as \"stock didn't move\" /\n * \"audit log empty\" bugs a long way from the handler.\n */\nfunction warnMalformedPayload(\n signal: string,\n payload: unknown,\n metadata: DispatchMetadata,\n reason: string,\n): void {\n logger.warn(\n `[@happyvertical/smrt-manufacturing] dispatch handler ignored a ${signal} ` +\n `event with a malformed payload (${reason}). ` +\n `Source: ${metadata.source ?? '<unknown>'}; payload keys: ` +\n `${payload && typeof payload === 'object' ? Object.keys(payload).join(',') : typeof payload}`,\n );\n}\n\n/**\n * Build a `ProductionService` for the dispatch installer when no\n * pre-built service was supplied. The discriminated option union\n * guarantees one of `stockService` / `db` is set in that branch, but\n * TypeScript can't narrow from a `??` value check on a sibling property,\n * so we validate at runtime with a clear error message.\n */\nasync function buildProductionService(\n options: InstallManufacturingDispatchHandlersOptions,\n): Promise<ProductionService> {\n if (options.stockService) {\n // Pass `db` through when supplied alongside `stockService` so the\n // resulting ProductionService points its BOM collections at the\n // caller-supplied manufacturing database. Dropping `db` here would\n // make the BOM collections fall back to `stockService.db`, which\n // is the *inventory* connection in deployments where the two\n // packages target different databases. ProductionService.create\n // already accepts both fields together; the discriminated option\n // union just doesn't make the combined shape obvious.\n return createProductionService({\n stockService: options.stockService,\n ...(options.db ? { db: options.db } : {}),\n });\n }\n if (!options.db) {\n throw new Error(\n 'installManufacturingDispatchHandlers: one of `productionService`, `stockService`, or `db` is required',\n );\n }\n return createProductionService({ db: options.db });\n}\n"],"names":["__decorateClass","buildStockService","assertPositiveQty"],"mappings":"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"}