@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.
- package/AGENTS.md +157 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +224 -0
- package/dist/index.d.ts +846 -0
- package/dist/index.js +810 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +623 -0
- package/dist/smrt-knowledge.json +517 -0
- package/dist/types.d.ts +138 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# @happyvertical/smrt-manufacturing
|
|
2
|
+
|
|
3
|
+
Bills 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.
|
|
4
|
+
|
|
5
|
+
Sits on top of `@happyvertical/smrt-inventory` (stock) and works alongside the `ProductionOrder` Contract STI subtype already shipped in `@happyvertical/smrt-commerce`.
|
|
6
|
+
|
|
7
|
+
## Models
|
|
8
|
+
|
|
9
|
+
| Model | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `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']`. |
|
|
12
|
+
| `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']`. |
|
|
13
|
+
|
|
14
|
+
Both models are `@TenantScoped({ mode: 'optional' })` with a nullable `tenantId` so they can be used either tenant-scoped or globally.
|
|
15
|
+
|
|
16
|
+
`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.
|
|
17
|
+
|
|
18
|
+
## BomService — planning helpers
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { BomService } from '@happyvertical/smrt-manufacturing';
|
|
22
|
+
|
|
23
|
+
const bom = await BomService.create({
|
|
24
|
+
db,
|
|
25
|
+
// Optional. Resolve unit cost for a component SKU.
|
|
26
|
+
// Without this, every line rolls up to $0 and `costUnavailable` is set.
|
|
27
|
+
costResolver: async (componentSkuId) => fetchLatestCost(componentSkuId),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const rollup = await bom.computeMaterialCost(bomId);
|
|
31
|
+
// { totalCost, currency, lineBreakdown, hasMissingCosts }
|
|
32
|
+
|
|
33
|
+
const requirements = await bom.explodeRequirements(bomId, 100);
|
|
34
|
+
// [{ componentSkuId, totalQty, uom }, ...]
|
|
35
|
+
|
|
36
|
+
const check = await bom.canProduce(bomId, 100);
|
|
37
|
+
// { ok: true } | { ok: false, shortages: [...] }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Method | Behavior |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `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`. |
|
|
43
|
+
| `explodeRequirements(bomId, qty)` | Returns a "shopping list" of materials needed for `qty` units. Duplicates across lines are summed. Does NOT mutate stock. |
|
|
44
|
+
| `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: [...] }`. |
|
|
45
|
+
|
|
46
|
+
## ProductionService — consume / produce
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { ProductionService } from '@happyvertical/smrt-manufacturing';
|
|
50
|
+
|
|
51
|
+
const production = await ProductionService.create({ db });
|
|
52
|
+
|
|
53
|
+
// Drain materials at the factory.
|
|
54
|
+
const consumed = await production.consumeMaterials(
|
|
55
|
+
{ id: order.id, productId: order.productId, bomId: order.bomId },
|
|
56
|
+
{ locationId: factory.id, qty: runQty },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Receive finished goods.
|
|
60
|
+
const produced = await production.produceFinishedGoods(
|
|
61
|
+
{ id: order.id, productId: order.productId },
|
|
62
|
+
{ locationId: factory.id, qty: runQty, finishedSkuId: variant.id },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Or run both in one transaction — see "Joint atomicity" below.
|
|
66
|
+
const { consumed, produced } = await production.runProduction(
|
|
67
|
+
{ id: order.id, productId: order.productId, bomId: order.bomId },
|
|
68
|
+
{
|
|
69
|
+
consume: { locationId: factory.id, qty: runQty },
|
|
70
|
+
produce: { locationId: factory.id, qty: runQty, finishedSkuId: variant.id },
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
All 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.
|
|
76
|
+
|
|
77
|
+
### Joint atomicity — `runProduction` vs the two-call form
|
|
78
|
+
|
|
79
|
+
`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.
|
|
80
|
+
|
|
81
|
+
When 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.
|
|
82
|
+
|
|
83
|
+
When 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.
|
|
84
|
+
|
|
85
|
+
### Location convention — explicit-arg design
|
|
86
|
+
|
|
87
|
+
The location where materials are consumed and finished goods are received is passed explicitly to `consumeMaterials` / `produceFinishedGoods`, not carried on the production order itself. Rationale:
|
|
88
|
+
|
|
89
|
+
- 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).
|
|
90
|
+
- 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.
|
|
91
|
+
- Callers that want a default can stash a `locationId` on their own production-order helper and pass it through.
|
|
92
|
+
|
|
93
|
+
### Finished-SKU convention
|
|
94
|
+
|
|
95
|
+
A `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).
|
|
96
|
+
|
|
97
|
+
## Opt-in DispatchBus hooks
|
|
98
|
+
|
|
99
|
+
Off by default. Wire them up explicitly in the application's `smrt.ts`:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { installManufacturingDispatchHandlers } from '@happyvertical/smrt-manufacturing';
|
|
103
|
+
|
|
104
|
+
const handlers = await installManufacturingDispatchHandlers({
|
|
105
|
+
dispatchBus: bus,
|
|
106
|
+
db,
|
|
107
|
+
// Default: subscribe to production_order:posted, call consumeMaterials.
|
|
108
|
+
installProductionPosted: true,
|
|
109
|
+
// Default: don't auto-produce. Set to true if your shop emits a
|
|
110
|
+
// separate production_order:completed event.
|
|
111
|
+
installProductionCompleted: false,
|
|
112
|
+
// Default: don't combine consume + produce on `posted`. Set to true
|
|
113
|
+
// for make-to-stock-instantly workflows where the factory step is
|
|
114
|
+
// invisible. Ignored when installProductionCompleted is true.
|
|
115
|
+
producedOnPosted: false,
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This subscribes to:
|
|
120
|
+
|
|
121
|
+
- `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.
|
|
122
|
+
- `production_order:completed` → `production.produceFinishedGoods(...)` (opt-in)
|
|
123
|
+
|
|
124
|
+
The 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.
|
|
125
|
+
|
|
126
|
+
## Gotchas
|
|
127
|
+
|
|
128
|
+
- **`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.
|
|
129
|
+
- **`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`.
|
|
130
|
+
- **`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.
|
|
131
|
+
- **`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.
|
|
132
|
+
- **`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.
|
|
133
|
+
- **`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.
|
|
134
|
+
- **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.
|
|
135
|
+
- **`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.
|
|
136
|
+
- **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.
|
|
137
|
+
- **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.
|
|
138
|
+
|
|
139
|
+
## Source attribution
|
|
140
|
+
|
|
141
|
+
Every emitted `StockMovement` carries `sourceType: 'ProductionOrder'` plus `sourceId: order.id` so downstream queries can reconstruct "what caused this movement". Reason codes used:
|
|
142
|
+
|
|
143
|
+
| reasonCode | Emitter | Note |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `production_consume` | `ProductionService.consumeMaterials` | One per BOM line per consume call |
|
|
146
|
+
| `production_produce` | `ProductionService.produceFinishedGoods` | One per produce call |
|
|
147
|
+
|
|
148
|
+
These join cleanly with the standard inventory reason codes (`receipt`, `reservation`, `release`, `fulfillment`, `transfer_out`, `transfer_in`, `adjustment`) defined in `@happyvertical/smrt-inventory`.
|
|
149
|
+
|
|
150
|
+
## Dependencies
|
|
151
|
+
|
|
152
|
+
| Package | Purpose |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `@happyvertical/smrt-core` | SmrtObject / SmrtCollection / DispatchBus |
|
|
155
|
+
| `@happyvertical/smrt-inventory` | StockService, stock levels, movements (the `Sku` model itself lives in `@happyvertical/smrt-products`; inventory tracks stock motion against the id) |
|
|
156
|
+
| `@happyvertical/smrt-tenancy` | Optional tenant scoping |
|
|
157
|
+
| `@happyvertical/sql` | Database adapter |
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# @happyvertical/smrt-manufacturing
|
|
2
|
+
|
|
3
|
+
Bills of materials, cost rollup, and production-order operations for the SMRT framework. Strictly industry-neutral — the same primitives serve apparel, furniture, automotive, CPG, electronics, food production, custom hardware, and any other vertical that builds finished goods from a recipe.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @happyvertical/smrt-manufacturing
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This package depends on `@happyvertical/smrt-inventory` (peer-installed via your workspace) for stock operations.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Define a BOM with components
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import {
|
|
19
|
+
BillOfMaterialsCollection,
|
|
20
|
+
BomLineCollection,
|
|
21
|
+
} from '@happyvertical/smrt-manufacturing';
|
|
22
|
+
|
|
23
|
+
const db = { type: 'sqlite', url: 'app.db' };
|
|
24
|
+
const boms = await BillOfMaterialsCollection.create({ db });
|
|
25
|
+
const lines = await BomLineCollection.create({ db });
|
|
26
|
+
|
|
27
|
+
const bom = await boms.create({
|
|
28
|
+
productId: shirt.id, // upstream Product or any STI subtype
|
|
29
|
+
version: 1,
|
|
30
|
+
status: 'active',
|
|
31
|
+
currency: 'USD',
|
|
32
|
+
notes: 'Initial revision',
|
|
33
|
+
});
|
|
34
|
+
await bom.save();
|
|
35
|
+
|
|
36
|
+
const fabric = await lines.create({
|
|
37
|
+
bomId: bom.id!,
|
|
38
|
+
componentSkuId: fabricSku.id!,
|
|
39
|
+
qtyPerUnit: 2.0,
|
|
40
|
+
uom: 'yards',
|
|
41
|
+
wastePercent: 10, // 10% cutting waste
|
|
42
|
+
});
|
|
43
|
+
await fabric.save();
|
|
44
|
+
|
|
45
|
+
const buttons = await lines.create({
|
|
46
|
+
bomId: bom.id!,
|
|
47
|
+
componentSkuId: buttonSku.id!,
|
|
48
|
+
qtyPerUnit: 4,
|
|
49
|
+
uom: 'each',
|
|
50
|
+
});
|
|
51
|
+
await buttons.save();
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Roll up material cost with waste
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { BomService } from '@happyvertical/smrt-manufacturing';
|
|
58
|
+
|
|
59
|
+
const service = await BomService.create({
|
|
60
|
+
db,
|
|
61
|
+
// Plug in any cost source: smrt-products Material.costPerUnit,
|
|
62
|
+
// a purchase-order rolling average, a vendor price book, anything.
|
|
63
|
+
costResolver: async (componentSkuId) => {
|
|
64
|
+
const sku = await skus.get(componentSkuId);
|
|
65
|
+
return sku?.attributes ? Number(JSON.parse(sku.attributes).cost ?? 0) : null;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const rollup = await service.computeMaterialCost(bom.id!);
|
|
70
|
+
console.log(rollup.totalCost, rollup.currency);
|
|
71
|
+
// Walks lines, applies waste, surfaces a per-line breakdown.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Plan a production run
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const requirements = await service.explodeRequirements(bom.id!, 100);
|
|
78
|
+
// [{ componentSkuId: fabricSku.id, totalQty: 220, uom: 'yards' },
|
|
79
|
+
// { componentSkuId: buttonSku.id, totalQty: 400, uom: 'each' }]
|
|
80
|
+
|
|
81
|
+
const check = await service.canProduce(bom.id!, 100);
|
|
82
|
+
if (!check.ok) {
|
|
83
|
+
for (const shortage of check.shortages) {
|
|
84
|
+
console.log(
|
|
85
|
+
`Need ${shortage.requested} of ${shortage.componentSkuId}, have ${shortage.available}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Execute consume / produce against a production order
|
|
92
|
+
|
|
93
|
+
The `ProductionOrder` row itself lives in `@happyvertical/smrt-commerce` as a `Contract` STI subtype. This package mutates the inventory ledger on its behalf.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { ProductionService } from '@happyvertical/smrt-manufacturing';
|
|
97
|
+
|
|
98
|
+
const production = await ProductionService.create({ db });
|
|
99
|
+
|
|
100
|
+
// Pull materials from the factory.
|
|
101
|
+
const consumed = await production.consumeMaterials(
|
|
102
|
+
{
|
|
103
|
+
id: order.id, // ProductionOrder.id
|
|
104
|
+
productId: order.productId,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
locationId: factory.id, // explicit — not stored on the order
|
|
108
|
+
qty: 100,
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Receive finished goods.
|
|
113
|
+
const produced = await production.produceFinishedGoods(
|
|
114
|
+
{ id: order.id, productId: order.productId },
|
|
115
|
+
{
|
|
116
|
+
locationId: factory.id,
|
|
117
|
+
qty: 100,
|
|
118
|
+
finishedSkuId: finishedVariant.id, // explicit — one productId can have many SKUs
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Every emitted `StockMovement` is stamped with `sourceType: 'ProductionOrder'` plus `sourceId: order.id` so audit queries can reconstruct what happened later via `StockMovementCollection.findBySource('ProductionOrder', order.id)`.
|
|
124
|
+
|
|
125
|
+
### Multi-tenancy
|
|
126
|
+
|
|
127
|
+
Both `BillOfMaterials` and `BomLine` use `@TenantScoped({ mode: 'optional' })` with a nullable `tenantId`. Wrap mutations in `withTenant()` from `@happyvertical/smrt-tenancy` to scope queries automatically.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { withTenant } from '@happyvertical/smrt-tenancy';
|
|
131
|
+
|
|
132
|
+
await withTenant({ tenantId: 'tenant-a' }, async () => {
|
|
133
|
+
const requirements = await service.explodeRequirements(bom.id!, 50);
|
|
134
|
+
// Reads are auto-filtered by tenant_id = 'tenant-a'.
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Opt-in DispatchBus wiring
|
|
139
|
+
|
|
140
|
+
The package ships handlers that bridge production-order lifecycle events to the consume / produce flow. Off by default; install them explicitly in your `smrt.ts`:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { createDispatchBus } from '@happyvertical/smrt-core';
|
|
144
|
+
import { installInventoryDispatchHandlers } from '@happyvertical/smrt-inventory';
|
|
145
|
+
import { installManufacturingDispatchHandlers } from '@happyvertical/smrt-manufacturing';
|
|
146
|
+
|
|
147
|
+
const bus = await createDispatchBus({ db });
|
|
148
|
+
|
|
149
|
+
// Inventory handlers bridge contract:created and fulfillment:shipped.
|
|
150
|
+
await installInventoryDispatchHandlers({ dispatchBus: bus, db });
|
|
151
|
+
|
|
152
|
+
// Manufacturing handlers bridge production_order:posted (and optionally
|
|
153
|
+
// production_order:completed) to consume / produce.
|
|
154
|
+
await installManufacturingDispatchHandlers({
|
|
155
|
+
dispatchBus: bus,
|
|
156
|
+
db,
|
|
157
|
+
// Consume and produce in one shot when posted (make-to-stock).
|
|
158
|
+
producedOnPosted: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Later, when a production order is posted:
|
|
162
|
+
await bus.emit('production_order:posted', {
|
|
163
|
+
productionOrderId: order.id,
|
|
164
|
+
productId: order.productId,
|
|
165
|
+
locationId: factory.id,
|
|
166
|
+
qty: 100,
|
|
167
|
+
finishedSkuId: finishedVariant.id, // only needed when producedOnPosted: true
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Per-handler toggles (`installProductionPosted`, `installProductionCompleted`) let consumers pick exactly the legs they want. The companion `contract:created` and `fulfillment:shipped` handlers live in `@happyvertical/smrt-inventory`.
|
|
172
|
+
|
|
173
|
+
## API
|
|
174
|
+
|
|
175
|
+
### Models
|
|
176
|
+
|
|
177
|
+
| Export | Description |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `BillOfMaterials` | Recipe for one finished product. Versioned with a `draft` / `active` / `superseded` lifecycle. |
|
|
180
|
+
| `BomLine` | One component on a BOM. `effectiveQtyPerUnit()` returns the qty including waste. |
|
|
181
|
+
|
|
182
|
+
### Collections
|
|
183
|
+
|
|
184
|
+
| Export | Description |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `BillOfMaterialsCollection` | `findByProduct`, `findActiveForProduct`, `findByStatus` |
|
|
187
|
+
| `BomLineCollection` | `findByBom`, `findByComponent` |
|
|
188
|
+
|
|
189
|
+
### Services
|
|
190
|
+
|
|
191
|
+
| Export | Description |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `BomService` | Cost rollup, requirements explosion, can-produce check. |
|
|
194
|
+
| `createBomService({ db, costResolver? })` | Convenience factory. |
|
|
195
|
+
| `ProductionService` | Operational consume / produce against a production order. |
|
|
196
|
+
| `createProductionService({ db })` | Convenience factory. |
|
|
197
|
+
| `installManufacturingDispatchHandlers({ dispatchBus, db })` | Opt-in bus wiring. |
|
|
198
|
+
| `BomNotFoundError` | Thrown when a BOM id cannot be resolved. |
|
|
199
|
+
| `NoActiveBomForProductError` | Thrown by `ProductionService` when neither an explicit `bomId` nor an active BOM is available for a production order. |
|
|
200
|
+
|
|
201
|
+
### Types
|
|
202
|
+
|
|
203
|
+
| Export | Description |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `BomStatus` | `'draft' \| 'active' \| 'superseded'` |
|
|
206
|
+
| `BomCostRollup` | Return shape of `computeMaterialCost`. |
|
|
207
|
+
| `BomLineCost` | Per-line entry inside a `BomCostRollup`. |
|
|
208
|
+
| `MaterialRequirement` | Entry returned by `explodeRequirements`. |
|
|
209
|
+
| `MaterialShortage` | Entry returned by `canProduce` when stock is insufficient. |
|
|
210
|
+
| `CanProduceResult` | `{ ok: true; shortages: [] } \| { ok: false; shortages: [...] }` |
|
|
211
|
+
| `ComponentCostResolver` | Async (or sync) callback returning unit cost or `null`. |
|
|
212
|
+
|
|
213
|
+
## Dependencies
|
|
214
|
+
|
|
215
|
+
| Package | Purpose |
|
|
216
|
+
|---|---|
|
|
217
|
+
| `@happyvertical/smrt-core` | SmrtObject / SmrtCollection / DispatchBus |
|
|
218
|
+
| `@happyvertical/smrt-inventory` | StockService (consume / produce target) |
|
|
219
|
+
| `@happyvertical/smrt-tenancy` | Optional tenant scoping |
|
|
220
|
+
| `@happyvertical/sql` | Database adapter |
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT
|