@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,517 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "generatedAt": "2026-06-23T01:11:25.105Z",
4
+ "packageName": "@happyvertical/smrt-manufacturing",
5
+ "packageVersion": "0.30.0",
6
+ "sourceManifestPath": "dist/manifest.json",
7
+ "agentDocPath": "AGENTS.md",
8
+ "sourceHashes": {
9
+ "manifest": "c7139a33f23d351732a117c0a6fc3a8d0f49bc78a7833b795c1275a7d5f4b79f",
10
+ "packageJson": "ae801e6712daa107ee016fe85490a34323749ecf35794a8cc4f59ce25e050ccc",
11
+ "agents": "94d10408c859743730368f932f77e5602ebefb8fe9a8ac3178fa4b211e95ad29"
12
+ },
13
+ "exports": [
14
+ ".",
15
+ "./manifest",
16
+ "./manifest.json"
17
+ ],
18
+ "dependencies": {
19
+ "@happyvertical/logger": "catalog:",
20
+ "@happyvertical/smrt-core": "workspace:*",
21
+ "@happyvertical/smrt-inventory": "workspace:*",
22
+ "@happyvertical/smrt-tenancy": "workspace:*",
23
+ "@happyvertical/sql": "catalog:",
24
+ "@happyvertical/smrt-vitest": "workspace:*",
25
+ "@types/node": "25.0.9",
26
+ "typescript": "^5.9.3",
27
+ "vite": "^7.3.1",
28
+ "vitest": "^4.0.17"
29
+ },
30
+ "smrtDependencies": [
31
+ "@happyvertical/smrt-core",
32
+ "@happyvertical/smrt-inventory",
33
+ "@happyvertical/smrt-tenancy",
34
+ "@happyvertical/smrt-vitest"
35
+ ],
36
+ "sdkDependencies": [
37
+ "@happyvertical/logger",
38
+ "@happyvertical/sql"
39
+ ],
40
+ "tags": [],
41
+ "risks": [],
42
+ "objects": [
43
+ {
44
+ "name": "BillOfMaterialsCollection",
45
+ "qualifiedName": "@happyvertical/smrt-manufacturing:BillOfMaterialsCollection",
46
+ "collection": "billofmaterialses",
47
+ "tableName": "manufacturing_boms",
48
+ "packageName": "@happyvertical/smrt-manufacturing",
49
+ "extends": "SmrtCollection",
50
+ "fields": [],
51
+ "relationships": [],
52
+ "methods": [
53
+ "findActiveForProduct",
54
+ "findByProduct",
55
+ "findByStatus"
56
+ ],
57
+ "surfaces": [],
58
+ "relationshipFeatures": [
59
+ "uuidColumns"
60
+ ],
61
+ "tags": [],
62
+ "risks": []
63
+ },
64
+ {
65
+ "name": "BomLineCollection",
66
+ "qualifiedName": "@happyvertical/smrt-manufacturing:BomLineCollection",
67
+ "collection": "bomlines",
68
+ "tableName": "manufacturing_bom_lines",
69
+ "packageName": "@happyvertical/smrt-manufacturing",
70
+ "extends": "SmrtCollection",
71
+ "fields": [],
72
+ "relationships": [],
73
+ "methods": [
74
+ "findByBom",
75
+ "findByComponent"
76
+ ],
77
+ "surfaces": [],
78
+ "relationshipFeatures": [
79
+ "uuidColumns"
80
+ ],
81
+ "tags": [],
82
+ "risks": []
83
+ },
84
+ {
85
+ "name": "BillOfMaterials",
86
+ "qualifiedName": "@happyvertical/smrt-manufacturing:BillOfMaterials",
87
+ "collection": "billofmaterialses",
88
+ "tableName": "manufacturing_boms",
89
+ "packageName": "@happyvertical/smrt-manufacturing",
90
+ "extends": "SmrtObject",
91
+ "fields": [
92
+ {
93
+ "name": "tenantId",
94
+ "type": "text",
95
+ "required": false,
96
+ "columnType": "UUID"
97
+ },
98
+ {
99
+ "name": "productId",
100
+ "type": "text",
101
+ "required": true,
102
+ "columnType": "TEXT"
103
+ },
104
+ {
105
+ "name": "version",
106
+ "type": "integer",
107
+ "required": true,
108
+ "columnType": "INTEGER"
109
+ },
110
+ {
111
+ "name": "effectiveDate",
112
+ "type": "datetime",
113
+ "required": false,
114
+ "columnType": "TIMESTAMP"
115
+ },
116
+ {
117
+ "name": "status",
118
+ "type": "text",
119
+ "required": true,
120
+ "columnType": "TEXT"
121
+ },
122
+ {
123
+ "name": "notes",
124
+ "type": "text",
125
+ "required": false,
126
+ "columnType": "TEXT"
127
+ },
128
+ {
129
+ "name": "currency",
130
+ "type": "text",
131
+ "required": false,
132
+ "columnType": "TEXT"
133
+ }
134
+ ],
135
+ "relationships": [],
136
+ "methods": [],
137
+ "surfaces": [
138
+ {
139
+ "kind": "api",
140
+ "name": "billofmaterialses.list",
141
+ "operation": "list",
142
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials",
143
+ "path": "/billofmaterialses",
144
+ "method": "GET"
145
+ },
146
+ {
147
+ "kind": "api",
148
+ "name": "billofmaterialses.get",
149
+ "operation": "get",
150
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials",
151
+ "path": "/billofmaterialses/[id]",
152
+ "method": "GET"
153
+ },
154
+ {
155
+ "kind": "api",
156
+ "name": "billofmaterialses.create",
157
+ "operation": "create",
158
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials",
159
+ "path": "/billofmaterialses",
160
+ "method": "POST"
161
+ },
162
+ {
163
+ "kind": "api",
164
+ "name": "billofmaterialses.update",
165
+ "operation": "update",
166
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials",
167
+ "path": "/billofmaterialses/[id]",
168
+ "method": "PATCH"
169
+ },
170
+ {
171
+ "kind": "cli",
172
+ "name": "billofmaterials_list",
173
+ "operation": "list",
174
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
175
+ },
176
+ {
177
+ "kind": "cli",
178
+ "name": "billofmaterials_get",
179
+ "operation": "get",
180
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
181
+ },
182
+ {
183
+ "kind": "cli",
184
+ "name": "billofmaterials_create",
185
+ "operation": "create",
186
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
187
+ },
188
+ {
189
+ "kind": "cli",
190
+ "name": "billofmaterials_update",
191
+ "operation": "update",
192
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
193
+ },
194
+ {
195
+ "kind": "cli",
196
+ "name": "billofmaterials_delete",
197
+ "operation": "delete",
198
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
199
+ },
200
+ {
201
+ "kind": "mcp",
202
+ "name": "billofmaterials_list",
203
+ "operation": "list",
204
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
205
+ },
206
+ {
207
+ "kind": "mcp",
208
+ "name": "billofmaterials_get",
209
+ "operation": "get",
210
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
211
+ }
212
+ ],
213
+ "relationshipFeatures": [
214
+ "uuidColumns"
215
+ ],
216
+ "tags": [],
217
+ "risks": []
218
+ },
219
+ {
220
+ "name": "BomLine",
221
+ "qualifiedName": "@happyvertical/smrt-manufacturing:BomLine",
222
+ "collection": "bomlines",
223
+ "tableName": "manufacturing_bom_lines",
224
+ "packageName": "@happyvertical/smrt-manufacturing",
225
+ "extends": "SmrtObject",
226
+ "fields": [
227
+ {
228
+ "name": "tenantId",
229
+ "type": "text",
230
+ "required": false,
231
+ "columnType": "UUID"
232
+ },
233
+ {
234
+ "name": "bomId",
235
+ "type": "text",
236
+ "required": true,
237
+ "columnType": "TEXT"
238
+ },
239
+ {
240
+ "name": "componentSkuId",
241
+ "type": "text",
242
+ "required": true,
243
+ "columnType": "TEXT"
244
+ },
245
+ {
246
+ "name": "qtyPerUnit",
247
+ "type": "decimal",
248
+ "required": false,
249
+ "columnType": "REAL"
250
+ },
251
+ {
252
+ "name": "uom",
253
+ "type": "text",
254
+ "required": false,
255
+ "columnType": "TEXT"
256
+ },
257
+ {
258
+ "name": "wastePercent",
259
+ "type": "decimal",
260
+ "required": false,
261
+ "columnType": "REAL"
262
+ },
263
+ {
264
+ "name": "notes",
265
+ "type": "text",
266
+ "required": false,
267
+ "columnType": "TEXT"
268
+ }
269
+ ],
270
+ "relationships": [],
271
+ "methods": [
272
+ "effectiveQtyPerUnit"
273
+ ],
274
+ "surfaces": [
275
+ {
276
+ "kind": "api",
277
+ "name": "bomlines.list",
278
+ "operation": "list",
279
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine",
280
+ "path": "/bomlines",
281
+ "method": "GET"
282
+ },
283
+ {
284
+ "kind": "api",
285
+ "name": "bomlines.get",
286
+ "operation": "get",
287
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine",
288
+ "path": "/bomlines/[id]",
289
+ "method": "GET"
290
+ },
291
+ {
292
+ "kind": "api",
293
+ "name": "bomlines.create",
294
+ "operation": "create",
295
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine",
296
+ "path": "/bomlines",
297
+ "method": "POST"
298
+ },
299
+ {
300
+ "kind": "api",
301
+ "name": "bomlines.update",
302
+ "operation": "update",
303
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine",
304
+ "path": "/bomlines/[id]",
305
+ "method": "PATCH"
306
+ },
307
+ {
308
+ "kind": "cli",
309
+ "name": "bomline_list",
310
+ "operation": "list",
311
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
312
+ },
313
+ {
314
+ "kind": "cli",
315
+ "name": "bomline_get",
316
+ "operation": "get",
317
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
318
+ },
319
+ {
320
+ "kind": "cli",
321
+ "name": "bomline_create",
322
+ "operation": "create",
323
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
324
+ },
325
+ {
326
+ "kind": "cli",
327
+ "name": "bomline_update",
328
+ "operation": "update",
329
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
330
+ },
331
+ {
332
+ "kind": "cli",
333
+ "name": "bomline_delete",
334
+ "operation": "delete",
335
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
336
+ },
337
+ {
338
+ "kind": "mcp",
339
+ "name": "bomline_list",
340
+ "operation": "list",
341
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
342
+ },
343
+ {
344
+ "kind": "mcp",
345
+ "name": "bomline_get",
346
+ "operation": "get",
347
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
348
+ }
349
+ ],
350
+ "relationshipFeatures": [
351
+ "uuidColumns"
352
+ ],
353
+ "tags": [],
354
+ "risks": []
355
+ }
356
+ ],
357
+ "surfaces": [
358
+ {
359
+ "kind": "api",
360
+ "name": "billofmaterialses.list",
361
+ "operation": "list",
362
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials",
363
+ "path": "/billofmaterialses",
364
+ "method": "GET"
365
+ },
366
+ {
367
+ "kind": "api",
368
+ "name": "billofmaterialses.get",
369
+ "operation": "get",
370
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials",
371
+ "path": "/billofmaterialses/[id]",
372
+ "method": "GET"
373
+ },
374
+ {
375
+ "kind": "api",
376
+ "name": "billofmaterialses.create",
377
+ "operation": "create",
378
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials",
379
+ "path": "/billofmaterialses",
380
+ "method": "POST"
381
+ },
382
+ {
383
+ "kind": "api",
384
+ "name": "billofmaterialses.update",
385
+ "operation": "update",
386
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials",
387
+ "path": "/billofmaterialses/[id]",
388
+ "method": "PATCH"
389
+ },
390
+ {
391
+ "kind": "cli",
392
+ "name": "billofmaterials_list",
393
+ "operation": "list",
394
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
395
+ },
396
+ {
397
+ "kind": "cli",
398
+ "name": "billofmaterials_get",
399
+ "operation": "get",
400
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
401
+ },
402
+ {
403
+ "kind": "cli",
404
+ "name": "billofmaterials_create",
405
+ "operation": "create",
406
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
407
+ },
408
+ {
409
+ "kind": "cli",
410
+ "name": "billofmaterials_update",
411
+ "operation": "update",
412
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
413
+ },
414
+ {
415
+ "kind": "cli",
416
+ "name": "billofmaterials_delete",
417
+ "operation": "delete",
418
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
419
+ },
420
+ {
421
+ "kind": "mcp",
422
+ "name": "billofmaterials_list",
423
+ "operation": "list",
424
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
425
+ },
426
+ {
427
+ "kind": "mcp",
428
+ "name": "billofmaterials_get",
429
+ "operation": "get",
430
+ "objectName": "@happyvertical/smrt-manufacturing:BillOfMaterials"
431
+ },
432
+ {
433
+ "kind": "api",
434
+ "name": "bomlines.list",
435
+ "operation": "list",
436
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine",
437
+ "path": "/bomlines",
438
+ "method": "GET"
439
+ },
440
+ {
441
+ "kind": "api",
442
+ "name": "bomlines.get",
443
+ "operation": "get",
444
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine",
445
+ "path": "/bomlines/[id]",
446
+ "method": "GET"
447
+ },
448
+ {
449
+ "kind": "api",
450
+ "name": "bomlines.create",
451
+ "operation": "create",
452
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine",
453
+ "path": "/bomlines",
454
+ "method": "POST"
455
+ },
456
+ {
457
+ "kind": "api",
458
+ "name": "bomlines.update",
459
+ "operation": "update",
460
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine",
461
+ "path": "/bomlines/[id]",
462
+ "method": "PATCH"
463
+ },
464
+ {
465
+ "kind": "cli",
466
+ "name": "bomline_list",
467
+ "operation": "list",
468
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
469
+ },
470
+ {
471
+ "kind": "cli",
472
+ "name": "bomline_get",
473
+ "operation": "get",
474
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
475
+ },
476
+ {
477
+ "kind": "cli",
478
+ "name": "bomline_create",
479
+ "operation": "create",
480
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
481
+ },
482
+ {
483
+ "kind": "cli",
484
+ "name": "bomline_update",
485
+ "operation": "update",
486
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
487
+ },
488
+ {
489
+ "kind": "cli",
490
+ "name": "bomline_delete",
491
+ "operation": "delete",
492
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
493
+ },
494
+ {
495
+ "kind": "mcp",
496
+ "name": "bomline_list",
497
+ "operation": "list",
498
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
499
+ },
500
+ {
501
+ "kind": "mcp",
502
+ "name": "bomline_get",
503
+ "operation": "get",
504
+ "objectName": "@happyvertical/smrt-manufacturing:BomLine"
505
+ }
506
+ ],
507
+ "prompts": [],
508
+ "relationshipsV2": {
509
+ "foreignKeyFields": 0,
510
+ "crossPackageRefFields": 0,
511
+ "junctionCollections": 0,
512
+ "hierarchicalObjects": 0,
513
+ "polymorphicAssociations": 0,
514
+ "uuidColumns": 6
515
+ },
516
+ "agentDoc": "# @happyvertical/smrt-manufacturing\n\nBills of materials, cost rollup, and production-order operations. Strictly industry-neutral — the same primitives serve apparel, furniture, automotive, CPG, electronics, food production, custom hardware, and any vertical that builds finished goods from a recipe.\n\nSits on top of `@happyvertical/smrt-inventory` (stock) and works alongside the `ProductionOrder` Contract STI subtype already shipped in `@happyvertical/smrt-commerce`.\n\n## Models\n\n| Model | Purpose |\n|---|---|\n| `BillOfMaterials` | Recipe for a finished product. `productId` (plain string) references the upstream `Product` or any STI subtype. Multiple revisions per product via `version` + `status` (`draft` / `active` / `superseded`). `conflictColumns: ['product_id', 'version', 'tenant_id']`. |\n| `BomLine` | One component on a BOM. `bomId` (FK), `componentSkuId` (plain string ref — the `Sku` model lives in `@happyvertical/smrt-products`; inventory tracks stock motion against the id), `qtyPerUnit`, `uom` (open-ended — `yards`, `each`, `grams`, `kg`, ...), `wastePercent`, `notes`. `conflictColumns: ['bom_id', 'component_sku_id', 'tenant_id']`. |\n\nBoth models are `@TenantScoped({ mode: 'optional' })` with a nullable `tenantId` so they can be used either tenant-scoped or globally.\n\n`RoutingStep` (labor cost) is intentionally out of scope for v1 — see issue [#1245](https://github.com/happyvertical/smrt/issues/1245) for the planned follow-up.\n\n## BomService — planning helpers\n\n```typescript\nimport { BomService } from '@happyvertical/smrt-manufacturing';\n\nconst bom = await BomService.create({\n db,\n // Optional. Resolve unit cost for a component SKU.\n // Without this, every line rolls up to $0 and `costUnavailable` is set.\n costResolver: async (componentSkuId) => fetchLatestCost(componentSkuId),\n});\n\nconst rollup = await bom.computeMaterialCost(bomId);\n// { totalCost, currency, lineBreakdown, hasMissingCosts }\n\nconst requirements = await bom.explodeRequirements(bomId, 100);\n// [{ componentSkuId, totalQty, uom }, ...]\n\nconst check = await bom.canProduce(bomId, 100);\n// { ok: true } | { ok: false, shortages: [...] }\n```\n\n| Method | Behavior |\n|---|---|\n| `computeMaterialCost(bomId)` | Walks every BomLine, applies waste (`qtyPerUnit * (1 + wastePercent / 100)`), resolves unit costs via the optional `costResolver`, returns per-line breakdown plus rolled-up total. Lines with no cost set `costUnavailable: true` and contribute `0`. |\n| `explodeRequirements(bomId, qty)` | Returns a \"shopping list\" of materials needed for `qty` units. Duplicates across lines are summed. Does NOT mutate stock. |\n| `canProduce(bomId, qty)` | Calls `explodeRequirements`, then sums `available` stock across every location per component, returns `{ ok: true }` if everything's covered, else `{ ok: false, shortages: [...] }`. |\n\n## ProductionService — consume / produce\n\n```typescript\nimport { ProductionService } from '@happyvertical/smrt-manufacturing';\n\nconst production = await ProductionService.create({ db });\n\n// Drain materials at the factory.\nconst consumed = await production.consumeMaterials(\n { id: order.id, productId: order.productId, bomId: order.bomId },\n { locationId: factory.id, qty: runQty },\n);\n\n// Receive finished goods.\nconst produced = await production.produceFinishedGoods(\n { id: order.id, productId: order.productId },\n { locationId: factory.id, qty: runQty, finishedSkuId: variant.id },\n);\n\n// Or run both in one transaction — see \"Joint atomicity\" below.\nconst { consumed, produced } = await production.runProduction(\n { id: order.id, productId: order.productId, bomId: order.bomId },\n {\n consume: { locationId: factory.id, qty: runQty },\n produce: { locationId: factory.id, qty: runQty, finishedSkuId: variant.id },\n },\n);\n```\n\nAll three methods write through `StockService` and stamp every emitted `StockMovement` with `sourceType: 'ProductionOrder'` + the production order id so audit queries can roll them up later.\n\n### Joint atomicity — `runProduction` vs the two-call form\n\n`consumeMaterials` and `produceFinishedGoods` are each individually atomic, but calling them as two separate awaits is NOT jointly atomic — each opens its own `stockService.withTransaction(...)` scope. If something goes wrong between the two calls (process crash, transient adapter failure on the produce leg), you can land in a state where materials are deducted but no finished SKU receipt balances them. The audit ledger stays consistent within each call; what's missing is the cross-call invariant.\n\nWhen you need that invariant — typically make-to-stock flows where the factory step is invisible to the ledger — use `runProduction(order, { consume, produce })`. Both legs run inside one transaction; any failure (BOM shortage, adapter error, interceptor reject) rolls back both legs together.\n\nWhen NOT to use it: workflows where consume and produce represent a real wall-clock gap that downstream observers need to see (WIP dashboards, partial-run reporting, separate \"materials posted\" and \"production completed\" events on the dispatch bus). There, the two-call form is the right shape — each call is its own ledger event.\n\n### Location convention — explicit-arg design\n\nThe location where materials are consumed and finished goods are received is passed explicitly to `consumeMaterials` / `produceFinishedGoods`, not carried on the production order itself. Rationale:\n\n- The commerce `ProductionOrder` is a `Contract` STI subtype owned by `@happyvertical/smrt-commerce`. Adding an `originLocationId` field there would either need a schema change in commerce (cross-cutting) or a meta field that only manufacturing knows about (leaky).\n- Real shops often pick a location at run time (factory A is congested, route the run through factory B), so even if the order carried a default, the explicit-arg signature is the more flexible canonical form.\n- Callers that want a default can stash a `locationId` on their own production-order helper and pass it through.\n\n### Finished-SKU convention\n\nA `ProductionOrder` references a `productId`, but a `Product` typically has multiple SKUs (one per variant). The caller of `produceFinishedGoods` picks the concrete `finishedSkuId` because the multi-SKU mapping is application-specific (size run, finish mix, kit variant).\n\n## Opt-in DispatchBus hooks\n\nOff by default. Wire them up explicitly in the application's `smrt.ts`:\n\n```typescript\nimport { installManufacturingDispatchHandlers } from '@happyvertical/smrt-manufacturing';\n\nconst handlers = await installManufacturingDispatchHandlers({\n dispatchBus: bus,\n db,\n // Default: subscribe to production_order:posted, call consumeMaterials.\n installProductionPosted: true,\n // Default: don't auto-produce. Set to true if your shop emits a\n // separate production_order:completed event.\n installProductionCompleted: false,\n // Default: don't combine consume + produce on `posted`. Set to true\n // for make-to-stock-instantly workflows where the factory step is\n // invisible. Ignored when installProductionCompleted is true.\n producedOnPosted: false,\n});\n```\n\nThis subscribes to:\n\n- `production_order:posted` → `production.consumeMaterials(...)`; when `producedOnPosted: true` the handler instead calls `production.runProduction(...)` so consume + produce share one transaction. Process crashes or adapter errors between the two legs can never leave materials deducted with no finished-goods receipt.\n- `production_order:completed` → `production.produceFinishedGoods(...)` (opt-in)\n\nThe companion handlers for `contract:created` (reserve) and `fulfillment:shipped` (fulfil) live in `@happyvertical/smrt-inventory` — wire both packages' installers from `smrt.ts` to get the full lifecycle.\n\n## Gotchas\n\n- **`computeMaterialCost` without a resolver returns `$0`.** That is deliberate — manufacturing does not assume any particular cost source. A real wiring will plug in `@happyvertical/smrt-products` `Material.costPerUnit`, or a rolling average from purchase-order history, or a vendor price book. The `costUnavailable` flag tells UIs to surface \"unknown cost\" rather than silently rolling up zeros.\n- **`explodeRequirements` does not call any stock APIs.** It is a planning helper. To check whether the materials are actually on hand, use `canProduce`. To actually deduct them, use `ProductionService.consumeMaterials`.\n- **`canProduce` sums available stock across every location.** The planning question is \"do we have it at all?\". The operational question of \"which warehouse do we pull from?\" is left to the caller of `consumeMaterials`, which targets a single `locationId` per call.\n- **`consumeMaterials` propagates `InsufficientStockError`.** If a line would drive `available` below zero, the underlying `StockService.adjust` throws. Pre-flight with `canProduce` before posting if you want to avoid partial-failure mid-run.\n- **`consumeMaterials` is atomic across BOM lines.** All per-line deductions and their audit rows run inside a single `stockService.withTransaction(...)` scope (powered by `@happyvertical/sql >= 0.74.0`'s native `db.transaction()`). An `InsufficientStockError` on line N+1 rolls back lines 1..N so production-order posting never leaves materials half-consumed. The recommended pre-flight (`BomService.canProduce(orderId, qty)`) is still useful when you'd rather know upfront than discover the shortfall mid-run, but a missed pre-flight no longer corrupts state.\n- **`consumeMaterials` + `produceFinishedGoods` are NOT jointly atomic.** Each opens its own transaction. A failure on the produce leg leaves materials deducted with no finished SKU receipt to balance it. Use `runProduction(order, { consume, produce })` when you need both legs to commit or roll back together.\n- **Cross-package references are plain strings.** `productId`, `componentSkuId`, `bomId` (within this package) — all plain string ids, never `@foreignKey()`. Keeps the dependency graph DAG-shaped and lets each upstream package evolve independently.\n- **`conflictColumns` include `tenant_id`** on both models. NULL-matching semantics are handled by `@happyvertical/sql >= 0.74.0`; two saves with the same `(product_id, version, NULL)` tuple merge in place.\n- **Lazy table creation.** Like everything else in SMRT, the `manufacturing_boms` and `manufacturing_bom_lines` tables are created on first DB op via `syncSchema`. Safe for SSR.\n- **Cross-industry constraint.** This package's vocabulary stays generic. Apparel-specific concepts (`Style`, `Makeup`, `Colorway`, `tech-pack`, fashion `Season`) and their analogues in furniture / automotive / CPG live in the relevant template package, never here. PRs that introduce industry vocabulary should be rejected.\n\n## Source attribution\n\nEvery emitted `StockMovement` carries `sourceType: 'ProductionOrder'` plus `sourceId: order.id` so downstream queries can reconstruct \"what caused this movement\". Reason codes used:\n\n| reasonCode | Emitter | Note |\n|---|---|---|\n| `production_consume` | `ProductionService.consumeMaterials` | One per BOM line per consume call |\n| `production_produce` | `ProductionService.produceFinishedGoods` | One per produce call |\n\nThese join cleanly with the standard inventory reason codes (`receipt`, `reservation`, `release`, `fulfillment`, `transfer_out`, `transfer_in`, `adjustment`) defined in `@happyvertical/smrt-inventory`.\n\n## Dependencies\n\n| Package | Purpose |\n|---|---|\n| `@happyvertical/smrt-core` | SmrtObject / SmrtCollection / DispatchBus |\n| `@happyvertical/smrt-inventory` | StockService, stock levels, movements (the `Sku` model itself lives in `@happyvertical/smrt-products`; inventory tracks stock motion against the id) |\n| `@happyvertical/smrt-tenancy` | Optional tenant scoping |\n| `@happyvertical/sql` | Database adapter |\n"
517
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Shared types and error classes for `@happyvertical/smrt-manufacturing`.
3
+ *
4
+ * Strictly industry-neutral. The same vocabulary serves apparel, furniture,
5
+ * automotive, CPG, electronics, food production, custom hardware, and any
6
+ * other vertical that builds finished goods from raw inputs by recipe.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ /**
12
+ * Aggregate cost rollup returned by {@link BomService.computeMaterialCost}.
13
+ */
14
+ export declare interface BomCostRollup {
15
+ /** BOM that was rolled up. */
16
+ bomId: string;
17
+ /** Total material cost per produced unit. */
18
+ totalCost: number;
19
+ /** ISO 4217 currency code. Defaults to `'USD'`. */
20
+ currency: string;
21
+ /** Per-line breakdown. Empty array when the BOM has no lines. */
22
+ lineBreakdown: BomLineCost[];
23
+ /**
24
+ * `true` when at least one line's unit cost could not be resolved.
25
+ * Callers should treat `totalCost` as a lower bound in that case.
26
+ */
27
+ hasMissingCosts: boolean;
28
+ }
29
+
30
+ /**
31
+ * Per-line cost breakdown returned by {@link BomService.computeMaterialCost}.
32
+ * Lets callers surface "here's where the $42 came from" UIs without
33
+ * recomputing on the client.
34
+ */
35
+ export declare interface BomLineCost {
36
+ /**
37
+ * Plain string reference to a component SKU id. The `Sku` model
38
+ * itself lives in `@happyvertical/smrt-products`; inventory only
39
+ * tracks stock motion against that id.
40
+ */
41
+ componentSkuId: string;
42
+ /** Quantity per produced unit (pre-waste). */
43
+ qtyPerUnit: number;
44
+ /** Waste percent applied to this line (`0` if none). */
45
+ wastePercent: number;
46
+ /** Effective quantity used per produced unit, including waste. */
47
+ effectiveQty: number;
48
+ /** Latest known unit cost, or `0` if unavailable. */
49
+ unitCost: number;
50
+ /** Effective unit-cost contribution to the rolled-up total. */
51
+ lineCost: number;
52
+ /** Unit of measure as declared on the line (`'yards'`, `'each'`, ...). */
53
+ uom: string;
54
+ /**
55
+ * `true` when the unit cost could not be resolved from upstream
56
+ * `smrt-products`. The line still contributes `0` to the total; the
57
+ * flag lets UIs warn the user that the cost rollup is incomplete.
58
+ */
59
+ costUnavailable: boolean;
60
+ }
61
+
62
+ /**
63
+ * Thrown when a service operation needs to resolve a BOM by id but the row
64
+ * does not exist. Carries the requested id so callers can surface a
65
+ * meaningful message.
66
+ */
67
+ export declare class BomNotFoundError extends Error {
68
+ readonly bomId: string;
69
+ name: string;
70
+ constructor(bomId: string);
71
+ }
72
+
73
+ /**
74
+ * Lifecycle state of a {@link BillOfMaterials}.
75
+ *
76
+ * - `draft` — under construction; not yet released for production. Cost
77
+ * rollups can still be computed, but the BOM should not yet be referenced
78
+ * by a production order.
79
+ * - `active` — currently in use. New production orders should reference the
80
+ * active BOM for a given product.
81
+ * - `superseded` — an older revision that has been replaced by a newer
82
+ * `active` BOM. Historical production orders may still reference the
83
+ * superseded row for audit, but no new orders should pick it up.
84
+ */
85
+ export declare type BomStatus = 'draft' | 'active' | 'superseded';
86
+
87
+ /**
88
+ * Result returned by {@link BomService.canProduce}.
89
+ *
90
+ * `ok: true` means every material requirement is currently covered by
91
+ * available stock; `ok: false` means at least one component is short.
92
+ */
93
+ export declare type CanProduceResult = {
94
+ ok: true;
95
+ shortages: [];
96
+ } | {
97
+ ok: false;
98
+ shortages: MaterialShortage[];
99
+ };
100
+
101
+ /**
102
+ * A single entry on a requirements explosion. Multiple BOM lines pointing
103
+ * at the same component SKU are summed.
104
+ */
105
+ export declare interface MaterialRequirement {
106
+ /** Plain string reference to the required component {@link Sku}. */
107
+ componentSkuId: string;
108
+ /** Total quantity needed across the full production run (waste included). */
109
+ totalQty: number;
110
+ /** Unit of measure carried over from the originating BOM line(s). */
111
+ uom: string;
112
+ }
113
+
114
+ /**
115
+ * A single shortage discovered by {@link BomService.canProduce}.
116
+ */
117
+ export declare interface MaterialShortage {
118
+ /** Plain string reference to the missing component {@link Sku}. */
119
+ componentSkuId: string;
120
+ /** Total quantity required for the requested production run. */
121
+ requested: number;
122
+ /** Available stock across all locations (`available` state). */
123
+ available: number;
124
+ }
125
+
126
+ /**
127
+ * Thrown when a production-order operation needs a BOM to plan against but
128
+ * no active BOM is currently registered for the production order's
129
+ * `productId`. The caller should either link a BOM by passing one in
130
+ * explicitly or activate one for the given product.
131
+ */
132
+ export declare class NoActiveBomForProductError extends Error {
133
+ readonly productId: string;
134
+ name: string;
135
+ constructor(productId: string);
136
+ }
137
+
138
+ export { }