@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,846 @@
1
+ import { DatabaseConfig } from '@happyvertical/smrt-core';
2
+ import { DispatchBus } from '@happyvertical/smrt-core';
3
+ import { SmrtCollection } from '@happyvertical/smrt-core';
4
+ import { SmrtObject } from '@happyvertical/smrt-core';
5
+ import { SmrtObjectOptions } from '@happyvertical/smrt-core';
6
+ import { StockMovementReason } from '@happyvertical/smrt-inventory';
7
+ import { StockService } from '@happyvertical/smrt-inventory';
8
+
9
+ export declare class BillOfMaterials extends SmrtObject {
10
+ /** Tenant scope. `null` means the BOM is a global record. */
11
+ tenantId: string | null;
12
+ /**
13
+ * Plain string reference to the upstream product this BOM produces.
14
+ * Typically a `Product.id` (or any STI subtype thereof) from
15
+ * `@happyvertical/smrt-products`. Cross-package id, intentionally not
16
+ * a `@foreignKey()`, so this package does not pull in `smrt-products`.
17
+ */
18
+ productId: string;
19
+ /**
20
+ * Monotonically increasing revision number scoped per `productId`. Together
21
+ * with `tenantId` and `productId` this forms the natural key — so
22
+ * `version` is an integer and `0` is reserved by convention for unsaved
23
+ * scratch rows.
24
+ */
25
+ version: number;
26
+ /**
27
+ * Date from which this BOM is intended to take effect for new production
28
+ * runs. Optional — older BOMs imported from legacy systems may have no
29
+ * effective date on file.
30
+ */
31
+ effectiveDate: Date | null;
32
+ /**
33
+ * Lifecycle state — `draft`, `active`, or `superseded`. See
34
+ * {@link BomStatus} for the semantics.
35
+ *
36
+ * Most queries that pick "the BOM to use right now" should filter for
37
+ * `status: 'active'`.
38
+ */
39
+ status: BomStatus;
40
+ /**
41
+ * Optional free-form notes shown in admin UIs (revision reason, source
42
+ * document reference, sign-off chain, etc.).
43
+ */
44
+ notes: string;
45
+ /**
46
+ * ISO 4217 currency code for cost rollups. Mostly a hint for the rollup
47
+ * service — defaults to `'USD'` so older imports without a currency
48
+ * still produce sensible numbers.
49
+ */
50
+ currency: string;
51
+ constructor(options?: BillOfMaterialsOptions);
52
+ }
53
+
54
+ export declare class BillOfMaterialsCollection extends SmrtCollection<BillOfMaterials> {
55
+ static readonly _itemClass: typeof BillOfMaterials;
56
+ /**
57
+ * Return every BOM for the given upstream product id, newest version
58
+ * first. Useful for "show me all revisions of this product's recipe"
59
+ * admin screens.
60
+ */
61
+ findByProduct(productId: string): Promise<BillOfMaterials[]>;
62
+ /**
63
+ * Return the currently `active` BOM for the given product, or `null` if
64
+ * none has been activated yet. When multiple `active` rows exist (which
65
+ * should not happen but can arise during partial migrations), the
66
+ * highest-version row wins.
67
+ */
68
+ findActiveForProduct(productId: string): Promise<BillOfMaterials | null>;
69
+ /**
70
+ * Return every BOM with the given lifecycle status across all products.
71
+ * Mostly useful for admin dashboards (e.g. "show every draft BOM").
72
+ */
73
+ findByStatus(status: BomStatus): Promise<BillOfMaterials[]>;
74
+ }
75
+
76
+ /**
77
+ * Options accepted by the {@link BillOfMaterials} constructor.
78
+ */
79
+ export declare interface BillOfMaterialsOptions extends SmrtObjectOptions {
80
+ tenantId?: string | null;
81
+ productId?: string;
82
+ version?: number;
83
+ effectiveDate?: Date | string | null;
84
+ status?: BomStatus;
85
+ notes?: string;
86
+ currency?: string;
87
+ }
88
+
89
+ /**
90
+ * Aggregate cost rollup returned by {@link BomService.computeMaterialCost}.
91
+ */
92
+ export declare interface BomCostRollup {
93
+ /** BOM that was rolled up. */
94
+ bomId: string;
95
+ /** Total material cost per produced unit. */
96
+ totalCost: number;
97
+ /** ISO 4217 currency code. Defaults to `'USD'`. */
98
+ currency: string;
99
+ /** Per-line breakdown. Empty array when the BOM has no lines. */
100
+ lineBreakdown: BomLineCost[];
101
+ /**
102
+ * `true` when at least one line's unit cost could not be resolved.
103
+ * Callers should treat `totalCost` as a lower bound in that case.
104
+ */
105
+ hasMissingCosts: boolean;
106
+ }
107
+
108
+ export declare class BomLine extends SmrtObject {
109
+ /** Tenant scope. `null` means the line is a global record. */
110
+ tenantId: string | null;
111
+ /**
112
+ * Plain string reference to the parent {@link BillOfMaterials}. Within
113
+ * this package this is the only "structural" foreign key — but it is
114
+ * still stored as a plain string to keep with the package's
115
+ * cross-reference convention and to avoid coupling the schema generator
116
+ * to a particular load order.
117
+ */
118
+ bomId: string;
119
+ /**
120
+ * Plain string reference to the component SKU id (raw material, trim,
121
+ * sub-assembly). The `Sku` model itself lives in
122
+ * `@happyvertical/smrt-products`; `@happyvertical/smrt-inventory` uses
123
+ * this id to track stock levels and movements. Cross-package id;
124
+ * never `@foreignKey()`.
125
+ */
126
+ componentSkuId: string;
127
+ /**
128
+ * Quantity of the component required per unit of finished product, in
129
+ * the unit declared by {@link uom}. Decimal so fractional usage like
130
+ * `1.75 yards` or `12.5 grams` round-trips correctly.
131
+ */
132
+ qtyPerUnit: number;
133
+ /**
134
+ * Unit of measure for {@link qtyPerUnit}. Open-ended string so the
135
+ * framework does not constrain the vocabulary — `'yards'`, `'each'`,
136
+ * `'grams'`, `'kg'`, `'m'`, `'litres'`, `'sq_ft'`, anything.
137
+ *
138
+ * Conventions follow the upstream `Material.uom` value when applicable.
139
+ */
140
+ uom: string;
141
+ /**
142
+ * Expected percentage waste on this line. Adds to the effective quantity
143
+ * consumed during cost rollups and requirements explosions so a cutting
144
+ * yield of 10% surfaces in the total cost without callers having to
145
+ * pre-inflate `qtyPerUnit`. Stored as a percent (`10` means 10%), not a
146
+ * fraction (`0.10`).
147
+ */
148
+ wastePercent: number;
149
+ /**
150
+ * Optional free-form notes — e.g. "primary structural component",
151
+ * "60-second cure", or "matched batch only".
152
+ */
153
+ notes: string;
154
+ constructor(options?: BomLineOptions);
155
+ /**
156
+ * Effective quantity consumed per produced unit, including waste. Used
157
+ * by {@link BomService} for cost rollups and requirements explosions.
158
+ *
159
+ * `effectiveQty = qtyPerUnit * (1 + wastePercent / 100)`
160
+ */
161
+ effectiveQtyPerUnit(): number;
162
+ }
163
+
164
+ export declare class BomLineCollection extends SmrtCollection<BomLine> {
165
+ static readonly _itemClass: typeof BomLine;
166
+ /**
167
+ * Return every line that belongs to the given BOM. Caller-stable order
168
+ * helps make cost rollups and requirements explosions deterministic.
169
+ */
170
+ findByBom(bomId: string): Promise<BomLine[]>;
171
+ /**
172
+ * Return every line that references the given component SKU across all
173
+ * BOMs. Useful for "where is this material used?" admin screens and for
174
+ * cost-change impact analysis (a price hike on a raw material affects
175
+ * every BOM in the result).
176
+ */
177
+ findByComponent(componentSkuId: string): Promise<BomLine[]>;
178
+ }
179
+
180
+ /**
181
+ * Per-line cost breakdown returned by {@link BomService.computeMaterialCost}.
182
+ * Lets callers surface "here's where the $42 came from" UIs without
183
+ * recomputing on the client.
184
+ */
185
+ export declare interface BomLineCost {
186
+ /**
187
+ * Plain string reference to a component SKU id. The `Sku` model
188
+ * itself lives in `@happyvertical/smrt-products`; inventory only
189
+ * tracks stock motion against that id.
190
+ */
191
+ componentSkuId: string;
192
+ /** Quantity per produced unit (pre-waste). */
193
+ qtyPerUnit: number;
194
+ /** Waste percent applied to this line (`0` if none). */
195
+ wastePercent: number;
196
+ /** Effective quantity used per produced unit, including waste. */
197
+ effectiveQty: number;
198
+ /** Latest known unit cost, or `0` if unavailable. */
199
+ unitCost: number;
200
+ /** Effective unit-cost contribution to the rolled-up total. */
201
+ lineCost: number;
202
+ /** Unit of measure as declared on the line (`'yards'`, `'each'`, ...). */
203
+ uom: string;
204
+ /**
205
+ * `true` when the unit cost could not be resolved from upstream
206
+ * `smrt-products`. The line still contributes `0` to the total; the
207
+ * flag lets UIs warn the user that the cost rollup is incomplete.
208
+ */
209
+ costUnavailable: boolean;
210
+ }
211
+
212
+ /**
213
+ * Options accepted by the {@link BomLine} constructor.
214
+ */
215
+ export declare interface BomLineOptions extends SmrtObjectOptions {
216
+ tenantId?: string | null;
217
+ bomId?: string;
218
+ componentSkuId?: string;
219
+ qtyPerUnit?: number;
220
+ uom?: string;
221
+ wastePercent?: number;
222
+ notes?: string;
223
+ }
224
+
225
+ /**
226
+ * Thrown when a service operation needs to resolve a BOM by id but the row
227
+ * does not exist. Carries the requested id so callers can surface a
228
+ * meaningful message.
229
+ */
230
+ export declare class BomNotFoundError extends Error {
231
+ readonly bomId: string;
232
+ name: string;
233
+ constructor(bomId: string);
234
+ }
235
+
236
+ /**
237
+ * BOM-driven planning service. See module documentation for the role
238
+ * this plays in the manufacturing pipeline.
239
+ *
240
+ * @example
241
+ * ```typescript
242
+ * const bomService = await BomService.create({ db });
243
+ * const rollup = await bomService.computeMaterialCost(bom.id!);
244
+ * const requirements = await bomService.explodeRequirements(bom.id!, 100);
245
+ * const check = await bomService.canProduce(bom.id!, 100);
246
+ * ```
247
+ */
248
+ export declare class BomService {
249
+ readonly boms: BillOfMaterialsCollection;
250
+ readonly lines: BomLineCollection;
251
+ readonly stockService: StockService;
252
+ private readonly costResolver;
253
+ private constructor();
254
+ /** Factory — prefer {@link createBomService}. */
255
+ static create(options: BomServiceOptions): Promise<BomService>;
256
+ /**
257
+ * Walk every {@link BomLine} for the given BOM, resolve each line's
258
+ * component cost, apply waste, and return the rolled-up material cost
259
+ * per produced unit along with a per-line breakdown.
260
+ *
261
+ * Throws {@link BomNotFoundError} when the BOM does not exist.
262
+ *
263
+ * Lines whose cost cannot be resolved contribute `0` to the total and
264
+ * set `costUnavailable: true` on their breakdown row; the aggregate
265
+ * `hasMissingCosts` flag mirrors this so callers can surface a UI
266
+ * warning.
267
+ */
268
+ computeMaterialCost(bomId: string): Promise<BomCostRollup>;
269
+ /**
270
+ * Return a "shopping list" of materials needed to produce `qty` units of
271
+ * the parent product against the given BOM. Lines that reference the
272
+ * same component SKU are summed so each `componentSkuId` appears once
273
+ * in the result.
274
+ *
275
+ * Does NOT mutate stock — purely a planning helper. Use
276
+ * {@link ProductionService.consumeMaterials} when you're ready to write
277
+ * stock movements.
278
+ *
279
+ * Throws {@link BomNotFoundError} when the BOM does not exist; throws a
280
+ * plain `Error` when `qty` is not a positive finite number.
281
+ */
282
+ explodeRequirements(bomId: string, qty: number): Promise<MaterialRequirement[]>;
283
+ /**
284
+ * Check whether the requirements for producing `qty` units against the
285
+ * given BOM are currently satisfied by available stock. Returns
286
+ * `{ ok: true, shortages: [] }` when every component has enough
287
+ * `available` stock across all locations; `{ ok: false, shortages: [...] }`
288
+ * with one entry per insufficient component otherwise.
289
+ *
290
+ * Available stock is summed across every location (the planning
291
+ * question is "do we have it at all?"; the operational question of
292
+ * "where do we pull from?" is left to the caller of
293
+ * {@link ProductionService.consumeMaterials}).
294
+ *
295
+ * Throws {@link BomNotFoundError} when the BOM does not exist.
296
+ */
297
+ canProduce(bomId: string, qty: number): Promise<CanProduceResult>;
298
+ /**
299
+ * Fetch a BOM by id or throw {@link BomNotFoundError}. Centralises the
300
+ * "missing BOM" error path so callers don't have to repeat the check.
301
+ */
302
+ private requireBom;
303
+ /**
304
+ * Run the supplied {@link ComponentCostResolver} for the given component
305
+ * SKU. Returns `null` when no resolver is registered or when the
306
+ * resolver returns `null` / `undefined`.
307
+ */
308
+ private resolveCost;
309
+ }
310
+
311
+ /**
312
+ * Options accepted by {@link BomService.create}.
313
+ *
314
+ * Either provide a `db` for the service to construct its own internal
315
+ * collections + {@link StockService}, or pass a pre-built `stockService`
316
+ * to share one across subsystems.
317
+ */
318
+ export declare type BomServiceOptions = {
319
+ /**
320
+ * Optional resolver that returns the latest known unit cost for a
321
+ * component SKU. See {@link ComponentCostResolver}. When omitted, all
322
+ * costs default to `0` and per-line `costUnavailable` is set.
323
+ */
324
+ costResolver?: ComponentCostResolver;
325
+ } & ({
326
+ db: DatabaseConfig;
327
+ stockService?: StockService;
328
+ } | {
329
+ stockService: StockService;
330
+ db?: DatabaseConfig;
331
+ });
332
+
333
+ /**
334
+ * Lifecycle state of a {@link BillOfMaterials}.
335
+ *
336
+ * - `draft` — under construction; not yet released for production. Cost
337
+ * rollups can still be computed, but the BOM should not yet be referenced
338
+ * by a production order.
339
+ * - `active` — currently in use. New production orders should reference the
340
+ * active BOM for a given product.
341
+ * - `superseded` — an older revision that has been replaced by a newer
342
+ * `active` BOM. Historical production orders may still reference the
343
+ * superseded row for audit, but no new orders should pick it up.
344
+ */
345
+ export declare type BomStatus = 'draft' | 'active' | 'superseded';
346
+
347
+ /**
348
+ * Result returned by {@link BomService.canProduce}.
349
+ *
350
+ * `ok: true` means every material requirement is currently covered by
351
+ * available stock; `ok: false` means at least one component is short.
352
+ */
353
+ export declare type CanProduceResult = {
354
+ ok: true;
355
+ shortages: [];
356
+ } | {
357
+ ok: false;
358
+ shortages: MaterialShortage[];
359
+ };
360
+
361
+ /**
362
+ * Callback signature for resolving the unit cost of a component SKU.
363
+ *
364
+ * Implementations should return the latest known cost in the BOM's
365
+ * currency, or `null` (or `undefined`) when no cost is available. The
366
+ * service treats unresolved costs as `0` and marks the affected line so
367
+ * callers can warn the user.
368
+ *
369
+ * @example
370
+ * ```typescript
371
+ * import { MaterialCollection } from '@happyvertical/smrt-products/models';
372
+ *
373
+ * const materials = await MaterialCollection.create({ db });
374
+ *
375
+ * const bom = await BomService.create({
376
+ * db,
377
+ * costResolver: async (componentSkuId) => {
378
+ * const sku = await skus.get(componentSkuId);
379
+ * if (!sku?.productId) return null;
380
+ * const material = await materials.get(sku.productId);
381
+ * return material?.costPerUnit ?? null;
382
+ * },
383
+ * });
384
+ * ```
385
+ */
386
+ export declare type ComponentCostResolver = (componentSkuId: string) => Promise<number | null | undefined> | number | null | undefined;
387
+
388
+ /**
389
+ * Per-call options for {@link ProductionService.consumeMaterials}.
390
+ */
391
+ export declare interface ConsumeMaterialsOptions {
392
+ /**
393
+ * Inventory location id (warehouse / factory) where the materials are
394
+ * pulled from. Required because the production order itself does not
395
+ * carry a stocking location — see "Location convention" in the module
396
+ * doc.
397
+ */
398
+ locationId: string;
399
+ /**
400
+ * Run quantity — how many finished units the BOM is being multiplied
401
+ * by. Each BOM line's `effectiveQtyPerUnit()` is multiplied by this
402
+ * number before being deducted from `available` stock.
403
+ */
404
+ qty: number;
405
+ /**
406
+ * Optional override of the reason code stamped on each
407
+ * {@link StockMovement}. Defaults to `'production_consume'`.
408
+ */
409
+ reasonCode?: StockMovementReason;
410
+ /**
411
+ * Optional free-form note attached to every emitted movement.
412
+ */
413
+ note?: string;
414
+ }
415
+
416
+ /**
417
+ * A single emitted stock movement, returned by
418
+ * {@link ProductionService.consumeMaterials} so callers can audit / log
419
+ * the impact of a production run without re-querying.
420
+ */
421
+ export declare interface ConsumeResult {
422
+ /** Component SKU id whose stock was deducted. */
423
+ componentSkuId: string;
424
+ /** Quantity deducted (already inflated for waste). */
425
+ qty: number;
426
+ /** Location the stock was deducted from. */
427
+ locationId: string;
428
+ }
429
+
430
+ /**
431
+ * Convenience factory. Returns a fully-initialized {@link BomService}.
432
+ */
433
+ export declare function createBomService(options: BomServiceOptions): Promise<BomService>;
434
+
435
+ /**
436
+ * Convenience factory. Returns a fully-initialized {@link ProductionService}.
437
+ */
438
+ export declare function createProductionService(options: ProductionServiceOptions): Promise<ProductionService>;
439
+
440
+ /**
441
+ * Result handle returned by {@link installManufacturingDispatchHandlers}.
442
+ * Call `dispose()` to detach the subscribers (mainly useful in tests and
443
+ * on graceful shutdown).
444
+ */
445
+ export declare interface InstalledManufacturingDispatchHandlers {
446
+ /** Resolved ProductionService — useful for follow-up writes in the same scope. */
447
+ productionService: ProductionService;
448
+ /** Detach every installed subscriber. Idempotent. */
449
+ dispose(): void;
450
+ }
451
+
452
+ /**
453
+ * Subscribe production-order handlers to the given DispatchBus.
454
+ *
455
+ * @example
456
+ * ```typescript
457
+ * import { createDispatchBus } from '@happyvertical/smrt-core';
458
+ * import { installManufacturingDispatchHandlers } from '@happyvertical/smrt-manufacturing';
459
+ *
460
+ * const bus = await createDispatchBus({ db });
461
+ * const handlers = await installManufacturingDispatchHandlers({
462
+ * dispatchBus: bus,
463
+ * db,
464
+ * producedOnPosted: true, // consume + produce in one shot
465
+ * });
466
+ * ```
467
+ */
468
+ export declare function installManufacturingDispatchHandlers(options: InstallManufacturingDispatchHandlersOptions): Promise<InstalledManufacturingDispatchHandlers>;
469
+
470
+ /**
471
+ * Options accepted by {@link installManufacturingDispatchHandlers}.
472
+ *
473
+ * Provide either a pre-built {@link ProductionService} (when sharing
474
+ * across subsystems) or a `db` / `stockService` for the helper to
475
+ * construct one on first use.
476
+ */
477
+ export declare type InstallManufacturingDispatchHandlersOptions = {
478
+ /** Bus to subscribe on. Typically the app-wide DispatchBus. */
479
+ dispatchBus: DispatchBus;
480
+ /**
481
+ * When `true` (default), install the `production_order:posted` handler.
482
+ */
483
+ installProductionPosted?: boolean;
484
+ /**
485
+ * When `true` (default `false`), install the
486
+ * `production_order:completed` handler. Off by default because many
487
+ * workflows want consume+produce to happen on `posted`; opt in to the
488
+ * split lifecycle when your shop emits a separate completion event.
489
+ */
490
+ installProductionCompleted?: boolean;
491
+ /**
492
+ * When `true` (default `false`) AND `installProductionCompleted` is
493
+ * false, the `production_order:posted` handler also calls
494
+ * {@link ProductionService.produceFinishedGoods} after consume —
495
+ * handy for "make-to-stock instantly" workflows where the factory
496
+ * step is invisible.
497
+ *
498
+ * Ignored when `installProductionCompleted` is `true` (in that case
499
+ * production is the completed-handler's job).
500
+ */
501
+ producedOnPosted?: boolean;
502
+ } & ({
503
+ productionService: ProductionService;
504
+ db?: DatabaseConfig;
505
+ stockService?: StockService;
506
+ } | {
507
+ stockService: StockService;
508
+ db?: DatabaseConfig;
509
+ productionService?: undefined;
510
+ } | {
511
+ db: DatabaseConfig;
512
+ stockService?: undefined;
513
+ productionService?: undefined;
514
+ });
515
+
516
+ /**
517
+ * A single entry on a requirements explosion. Multiple BOM lines pointing
518
+ * at the same component SKU are summed.
519
+ */
520
+ export declare interface MaterialRequirement {
521
+ /** Plain string reference to the required component {@link Sku}. */
522
+ componentSkuId: string;
523
+ /** Total quantity needed across the full production run (waste included). */
524
+ totalQty: number;
525
+ /** Unit of measure carried over from the originating BOM line(s). */
526
+ uom: string;
527
+ }
528
+
529
+ /**
530
+ * A single shortage discovered by {@link BomService.canProduce}.
531
+ */
532
+ export declare interface MaterialShortage {
533
+ /** Plain string reference to the missing component {@link Sku}. */
534
+ componentSkuId: string;
535
+ /** Total quantity required for the requested production run. */
536
+ requested: number;
537
+ /** Available stock across all locations (`available` state). */
538
+ available: number;
539
+ }
540
+
541
+ /**
542
+ * Thrown when a production-order operation needs a BOM to plan against but
543
+ * no active BOM is currently registered for the production order's
544
+ * `productId`. The caller should either link a BOM by passing one in
545
+ * explicitly or activate one for the given product.
546
+ */
547
+ export declare class NoActiveBomForProductError extends Error {
548
+ readonly productId: string;
549
+ name: string;
550
+ constructor(productId: string);
551
+ }
552
+
553
+ /**
554
+ * Per-call options for {@link ProductionService.produceFinishedGoods}.
555
+ */
556
+ export declare interface ProduceFinishedGoodsOptions {
557
+ /**
558
+ * Inventory location id (warehouse / factory) where the finished goods
559
+ * land. Required for the same reason as {@link ConsumeMaterialsOptions.locationId}.
560
+ */
561
+ locationId: string;
562
+ /**
563
+ * Quantity of finished goods produced. Always positive.
564
+ */
565
+ qty: number;
566
+ /**
567
+ * The SKU id of the finished product to receive into `available`. The
568
+ * production order references a `productId` (a `Product` from
569
+ * `@happyvertical/smrt-products`), but a product typically has multiple
570
+ * SKUs (one per variant). The caller decides which concrete SKU each
571
+ * line of the run produces.
572
+ */
573
+ finishedSkuId: string;
574
+ /**
575
+ * Optional override of the reason code stamped on the
576
+ * {@link StockMovement}. Defaults to `'production_produce'`.
577
+ */
578
+ reasonCode?: StockMovementReason;
579
+ /**
580
+ * Optional free-form note attached to the emitted movement.
581
+ */
582
+ note?: string;
583
+ }
584
+
585
+ /**
586
+ * Result of {@link ProductionService.produceFinishedGoods}.
587
+ */
588
+ export declare interface ProduceResult {
589
+ /** Finished SKU id whose stock increased. */
590
+ finishedSkuId: string;
591
+ /** Quantity received. */
592
+ qty: number;
593
+ /** Location the stock was received into. */
594
+ locationId: string;
595
+ }
596
+
597
+ /**
598
+ * Shape of a `production_order:completed` payload this handler expects.
599
+ *
600
+ * Typically emitted when the factory signs off the run and finished
601
+ * goods are physically available to ship. Only used when callers opt in
602
+ * via `installProductionCompleted`.
603
+ */
604
+ export declare interface ProductionOrderCompletedPayload {
605
+ /** Production order row id. Used for `sourceId` on the receipt movement. */
606
+ productionOrderId: string;
607
+ /**
608
+ * SKU id of the finished product to receive into `available`. Required.
609
+ */
610
+ finishedSkuId: string;
611
+ /** Inventory location id where the finished goods land. */
612
+ locationId: string;
613
+ /** Quantity received. */
614
+ qty: number;
615
+ }
616
+
617
+ /**
618
+ * Shape of a `production_order:posted` payload this handler expects.
619
+ *
620
+ * The producer (typically the application's smrt.ts wiring once a
621
+ * `ProductionOrder` row transitions to the posted state) packs one
622
+ * payload per posted order. The handler consumes materials for every
623
+ * unit declared by `qty`, attributed to `('ProductionOrder', productionOrderId)`
624
+ * on each emitted {@link StockMovement}.
625
+ */
626
+ export declare interface ProductionOrderPostedPayload {
627
+ /** Production order row id. Used for `sourceId` on every movement. */
628
+ productionOrderId: string;
629
+ /**
630
+ * Plain string reference to the upstream product the order is asked to
631
+ * produce. Used to resolve the active BOM at consume time.
632
+ */
633
+ productId: string;
634
+ /**
635
+ * Inventory location id (factory / warehouse) where materials are
636
+ * pulled from.
637
+ */
638
+ locationId: string;
639
+ /**
640
+ * Run quantity. Each BOM line's effective qty is multiplied by this
641
+ * before being deducted from `available`.
642
+ */
643
+ qty: number;
644
+ /**
645
+ * Optional explicit BOM id, e.g. when the order locked itself to a
646
+ * specific BOM revision when it was created. Falls back to the active
647
+ * BOM for `productId` if omitted.
648
+ */
649
+ bomId?: string;
650
+ /**
651
+ * Optional finished SKU id. When provided AND the
652
+ * `installProductionPosted` handler is wired to also produce in one
653
+ * shot (which only happens when `installProductionCompleted` is false
654
+ * and `producedOnPosted` is true), this is the SKU to receive into
655
+ * `available`.
656
+ */
657
+ finishedSkuId?: string;
658
+ }
659
+
660
+ /**
661
+ * Minimal shape of a production order this service needs. We deliberately
662
+ * accept this structural type rather than importing the `ProductionOrder`
663
+ * class from `@happyvertical/smrt-commerce` — that would either pull a
664
+ * dep on commerce into manufacturing or force commerce to extend a
665
+ * manufacturing interface. Plain duck typing keeps the dependency arrow
666
+ * pointing the way the architecture asks for (manufacturing builds on
667
+ * inventory; commerce stays orthogonal).
668
+ */
669
+ export declare interface ProductionOrderRef {
670
+ /** Production order row id. Used for `sourceId` on every movement. */
671
+ id?: string | null;
672
+ /**
673
+ * Plain string reference to the upstream product the order is asked to
674
+ * produce. Used to look up the active BOM for the consume step.
675
+ */
676
+ productId?: string;
677
+ /**
678
+ * Optional explicit BOM id. When set, {@link consumeMaterials} uses this
679
+ * row even if the product has a different `active` BOM — useful when an
680
+ * order was kicked off against a specific revision.
681
+ */
682
+ bomId?: string;
683
+ }
684
+
685
+ /**
686
+ * Operational bridge between a production order and the inventory ledger.
687
+ */
688
+ export declare class ProductionService {
689
+ /**
690
+ * BOM collection bound to the OUTER (non-tx) database. Used for
691
+ * pre-flight reads in `prepareConsume()` — those run before the
692
+ * stock-service transaction opens, by design (read-then-tx keeps
693
+ * the transaction window narrow and rollback only covers actual
694
+ * mutations). DO NOT add BOM writes that depend on the stock-tx
695
+ * rolling them back together — this collection is not tx-scoped
696
+ * and writes here would commit independently. If joint BOM-plus-
697
+ * stock mutations are ever needed, build tx-scoped BOM collections
698
+ * inside the `stockService.withTransaction` callback (mirror the
699
+ * pattern in StockService.withTransaction itself).
700
+ */
701
+ readonly boms: BillOfMaterialsCollection;
702
+ /**
703
+ * BomLine collection bound to the OUTER database. Same caveat as
704
+ * {@link boms} — pre-flight reads only.
705
+ */
706
+ readonly lines: BomLineCollection;
707
+ readonly stockService: StockService;
708
+ private constructor();
709
+ /** Factory — prefer {@link createProductionService}. */
710
+ static create(options: ProductionServiceOptions): Promise<ProductionService>;
711
+ /**
712
+ * Walk every BOM line for the production order's BOM and deduct
713
+ * `effectiveQtyPerUnit() * qty` from `available` at `locationId`. Each
714
+ * deduction goes through {@link StockService.adjust} with `sourceType:
715
+ * 'ProductionOrder'` and `sourceId: order.id`. Returns the list of
716
+ * movements emitted so callers can log / surface them.
717
+ *
718
+ * **Atomic across lines**: all per-line deductions and their audit
719
+ * movements run inside a single `stockService.withTransaction(...)`
720
+ * scope. An `InsufficientStockError` on line N+1 rolls back lines 1..N
721
+ * so production-order posting never leaves materials half-consumed.
722
+ * Pre-flight with {@link BomService.canProduce} when you'd rather know
723
+ * upfront than discover the shortfall mid-run.
724
+ *
725
+ * Throws {@link NoActiveBomForProductError} if no BOM id is supplied
726
+ * on the order *and* no active BOM exists for the order's `productId`.
727
+ * Throws {@link BomNotFoundError} if `order.bomId` is supplied but
728
+ * doesn't resolve. Throws plain `Error` on missing `locationId`,
729
+ * non-positive `qty`, or when both `order.bomId` and `order.productId`
730
+ * are absent (a programmer error — neither input identifies a BOM).
731
+ * Re-throws `InsufficientStockError` from `StockService.adjust` if a
732
+ * line would drive `available` below zero.
733
+ */
734
+ consumeMaterials(order: ProductionOrderRef, options: ConsumeMaterialsOptions): Promise<ConsumeResult[]>;
735
+ /**
736
+ * Receive `qty` of the finished SKU into `available` at `locationId`.
737
+ * Goes through {@link StockService.receive} with `sourceType:
738
+ * 'ProductionOrder'` and `sourceId: order.id`. Returns a single
739
+ * {@link ProduceResult} describing what landed.
740
+ *
741
+ * Throws plain `Error` on missing required arguments or non-positive
742
+ * `qty`.
743
+ */
744
+ produceFinishedGoods(order: ProductionOrderRef, options: ProduceFinishedGoodsOptions): Promise<ProduceResult>;
745
+ /**
746
+ * Atomically consume materials AND receive finished goods for one
747
+ * production run.
748
+ *
749
+ * Both calls share a single `stockService.withTransaction(...)` scope:
750
+ *
751
+ * - Materials are deducted line-by-line via {@link StockService.adjust}.
752
+ * - Finished goods are received via {@link StockService.receive}.
753
+ * - If ANY step throws — a BOM-line shortage, the produce-leg adapter
754
+ * failing, a tenancy mismatch surfaced by an interceptor — every
755
+ * write in the transaction rolls back. The materials are restored
756
+ * and the finished SKU's `available` row is unchanged. Callers
757
+ * never observe "materials deducted but finished goods missing".
758
+ *
759
+ * Use this when consume + produce should be one indivisible event —
760
+ * e.g. a make-to-stock workflow posting `production_order:completed`
761
+ * where the shop floor step is invisible to the ledger.
762
+ *
763
+ * The two-call form (call {@link consumeMaterials} then {@link
764
+ * produceFinishedGoods} separately) is still appropriate when the
765
+ * factory step is a real wall-clock gap that other observers need to
766
+ * see — work-in-progress dashboards, partial-run reporting, etc.
767
+ *
768
+ * Throws the same errors as the individual methods: missing args
769
+ * (plain `Error`), unresolvable BOM ({@link BomNotFoundError} /
770
+ * {@link NoActiveBomForProductError}), or `InsufficientStockError`
771
+ * if a line would drive `available` below zero.
772
+ */
773
+ runProduction(order: ProductionOrderRef, options: {
774
+ consume: ConsumeMaterialsOptions;
775
+ produce: ProduceFinishedGoodsOptions;
776
+ }): Promise<RunProductionResult>;
777
+ /**
778
+ * Validate consume args, resolve the BOM + lines, and pre-compute the
779
+ * mutation attribution. Anything that can throw without a tx open
780
+ * happens here so the transaction we open later doesn't have to roll
781
+ * back over a validation miss.
782
+ */
783
+ private prepareConsume;
784
+ /**
785
+ * Execute the consume leg against a caller-supplied tx-bound
786
+ * {@link StockService}. Public entry points (`consumeMaterials`,
787
+ * `runProduction`) open the tx and call through here. Each per-line
788
+ * `tx.adjust(...)` shares the same transaction so a shortfall on
789
+ * line N+1 rolls back lines 1..N.
790
+ */
791
+ private consumeMaterialsWith;
792
+ /**
793
+ * Execute the produce leg against a caller-supplied tx-bound
794
+ * {@link StockService}. Public entry points (`produceFinishedGoods`,
795
+ * `runProduction`) open the tx and call through here.
796
+ */
797
+ private produceFinishedGoodsWith;
798
+ /**
799
+ * Resolve the BOM for a production order. Order of preference:
800
+ *
801
+ * 1. Explicit `order.bomId` if supplied — the caller pinned a specific
802
+ * revision; we honor that pin or fail. We deliberately do NOT fall
803
+ * back to the active BOM when `order.bomId` is stale or mistyped,
804
+ * because silently switching recipes would have the production run
805
+ * consume materials against a different BOM than the order locked.
806
+ * 2. The active BOM for `order.productId`.
807
+ *
808
+ * Throws {@link BomNotFoundError} when an explicit `order.bomId` is
809
+ * supplied but doesn't resolve to a row.
810
+ *
811
+ * Throws {@link NoActiveBomForProductError} when no `bomId` was supplied
812
+ * and the product has no active BOM.
813
+ *
814
+ * Throws plain `Error` when neither `order.bomId` nor `order.productId`
815
+ * is supplied — there's nothing to resolve against, so we surface that
816
+ * as a programmer error with a distinct message rather than overload
817
+ * `NoActiveBomForProductError` with an empty product id.
818
+ */
819
+ private resolveBom;
820
+ }
821
+
822
+ /**
823
+ * Options accepted by {@link ProductionService.create}.
824
+ *
825
+ * Either pass a `db` for the service to construct its own
826
+ * collections + {@link StockService}, or share a pre-built `stockService`
827
+ * across subsystems.
828
+ */
829
+ export declare type ProductionServiceOptions = {
830
+ db: DatabaseConfig;
831
+ stockService?: StockService;
832
+ } | {
833
+ stockService: StockService;
834
+ db?: DatabaseConfig;
835
+ };
836
+
837
+ /**
838
+ * Result of {@link ProductionService.runProduction} — the consume + produce
839
+ * pair, run inside one transaction.
840
+ */
841
+ declare interface RunProductionResult {
842
+ consumed: ConsumeResult[];
843
+ produced: ProduceResult;
844
+ }
845
+
846
+ export { }