@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/dist/index.d.ts
ADDED
|
@@ -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 { }
|