@classytic/flow 0.1.4

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -0
  3. package/README.md +258 -0
  4. package/dist/allocation-policy-my_HfzdV.d.mts +23 -0
  5. package/dist/base-MWBqRFM2.mjs +16 -0
  6. package/dist/catalog-bridge-K8bdkncJ.d.mts +29 -0
  7. package/dist/cost-layer.port-iH9pvZqB.d.mts +30 -0
  8. package/dist/cost-layer.service-BQ1bs-XN.mjs +86 -0
  9. package/dist/cost-layer.service-DWmo9dQz.d.mts +53 -0
  10. package/dist/count.port-BRqwGbi3.d.mts +57 -0
  11. package/dist/counting/index.d.mts +2 -0
  12. package/dist/counting/index.mjs +2 -0
  13. package/dist/counting.service-BiQXqorv.mjs +232 -0
  14. package/dist/counting.service-CpAxU2G0.d.mts +74 -0
  15. package/dist/domain/contracts/index.d.mts +3 -0
  16. package/dist/domain/contracts/index.mjs +1 -0
  17. package/dist/domain/enums/index.d.mts +2 -0
  18. package/dist/domain/enums/index.mjs +4 -0
  19. package/dist/domain/index.d.mts +24 -0
  20. package/dist/domain/index.mjs +10 -0
  21. package/dist/domain/policies/index.d.mts +4 -0
  22. package/dist/domain/policies/index.mjs +1 -0
  23. package/dist/domain-D5cpMpR0.mjs +96 -0
  24. package/dist/domain-errors-D7S9ydNF.mjs +133 -0
  25. package/dist/enums-C3_z6aHC.mjs +82 -0
  26. package/dist/event-bus-BNmyoJb4.mjs +37 -0
  27. package/dist/event-bus-Um_xrcMY.d.mts +21 -0
  28. package/dist/event-emitter.port-BFh2pasY.d.mts +183 -0
  29. package/dist/event-types-BSqQOvXv.mjs +29 -0
  30. package/dist/events/index.d.mts +3 -0
  31. package/dist/events/index.mjs +3 -0
  32. package/dist/idempotency.port-CTC70JON.d.mts +55 -0
  33. package/dist/index-Bia4m8d2.d.mts +67 -0
  34. package/dist/index-BmNm3oNU2.d.mts +107 -0
  35. package/dist/index-C5PciI9P.d.mts +203 -0
  36. package/dist/index-CMTUKEK_.d.mts +308 -0
  37. package/dist/index-C_aEnozN.d.mts +220 -0
  38. package/dist/index-CulWO137.d.mts +107 -0
  39. package/dist/index-DFF0GJ4J.d.mts +36 -0
  40. package/dist/index-DsE7lZdO.d.mts +11 -0
  41. package/dist/index-DwO9IdNa.d.mts +1 -0
  42. package/dist/index-dtWUZr2a2.d.mts +350 -0
  43. package/dist/index.d.mts +128 -0
  44. package/dist/index.mjs +102 -0
  45. package/dist/insufficient-stock.error-Dyr4BYaV.mjs +15 -0
  46. package/dist/location.port-CValXIpb.d.mts +52 -0
  47. package/dist/lot.port-ChsmvZqs.d.mts +32 -0
  48. package/dist/models/index.d.mts +2 -0
  49. package/dist/models/index.mjs +2 -0
  50. package/dist/models-CHTMbp-G.mjs +1020 -0
  51. package/dist/move-group.port-DHGoQA3d.d.mts +56 -0
  52. package/dist/move-status-DkaFp2GD.mjs +38 -0
  53. package/dist/move.port-Qg1CYp7h.d.mts +89 -0
  54. package/dist/package.service-4tcAwBbr.mjs +95 -0
  55. package/dist/package.service-C605NaBQ.d.mts +42 -0
  56. package/dist/packaging/index.d.mts +2 -0
  57. package/dist/packaging/index.mjs +2 -0
  58. package/dist/procurement/index.d.mts +2 -0
  59. package/dist/procurement/index.mjs +2 -0
  60. package/dist/quant.port-BBa66PBT.d.mts +42 -0
  61. package/dist/removal-policy-BItBB8FD.d.mts +29 -0
  62. package/dist/replenishment-rule.port-DnEYtbyD.d.mts +78 -0
  63. package/dist/replenishment.service-BT9P-HKM.mjs +284 -0
  64. package/dist/replenishment.service-HO0sDhB_.d.mts +89 -0
  65. package/dist/reporting/index.d.mts +2 -0
  66. package/dist/reporting/index.mjs +2 -0
  67. package/dist/reporting-CL5ffrKM.mjs +243 -0
  68. package/dist/repositories/index.d.mts +2 -0
  69. package/dist/repositories/index.mjs +2 -0
  70. package/dist/repositories-nZXJKvLW.mjs +842 -0
  71. package/dist/reservation-status-ZfuTaWG0.mjs +22 -0
  72. package/dist/reservation.port-l9NFQ0si.d.mts +85 -0
  73. package/dist/reservations/index.d.mts +2 -0
  74. package/dist/reservations/index.mjs +2 -0
  75. package/dist/reservations-Cg4wN0QB.mjs +112 -0
  76. package/dist/routing/index.d.mts +362 -0
  77. package/dist/routing/index.mjs +582 -0
  78. package/dist/runtime-config-C0ggPkiK.mjs +40 -0
  79. package/dist/runtime-config-CQLtPPqY.d.mts +38 -0
  80. package/dist/scan-token-CNM9QVLY.d.mts +26 -0
  81. package/dist/scanning/index.d.mts +45 -0
  82. package/dist/scanning/index.mjs +228 -0
  83. package/dist/services/index.d.mts +8 -0
  84. package/dist/services/index.mjs +8 -0
  85. package/dist/services-_lLO4Xbl.mjs +1009 -0
  86. package/dist/stock-move-group-C0DqUfPY.mjs +88 -0
  87. package/dist/stock-package-BIarxbDS.d.mts +19 -0
  88. package/dist/stock-quant-CZhgvTu7.d.mts +41 -0
  89. package/dist/tenant-guard-6Ne-BILP.mjs +12 -0
  90. package/dist/tenant-isolation.error-D3OcKUdx.mjs +11 -0
  91. package/dist/trace.service-B9vAh-l-.d.mts +55 -0
  92. package/dist/trace.service-DE6Eh8_8.mjs +71 -0
  93. package/dist/traceability/index.d.mts +2 -0
  94. package/dist/traceability/index.mjs +2 -0
  95. package/dist/types/index.d.mts +2 -0
  96. package/dist/types/index.mjs +1 -0
  97. package/dist/unit-of-work.port-CWEkrDKu.d.mts +17 -0
  98. package/dist/valuation/index.d.mts +78 -0
  99. package/dist/valuation/index.mjs +103 -0
  100. package/dist/valuation-policy-Dco8c9Vw.d.mts +14 -0
  101. package/dist/virtual-locations-B9zXqPdi.d.mts +38 -0
  102. package/package.json +155 -0
@@ -0,0 +1,582 @@
1
+ //#region src/routing/cross-dock.engine.ts
2
+ /**
3
+ * Cross-Dock Engine
4
+ *
5
+ * Evaluates whether incoming stock should bypass storage and route directly
6
+ * to shipping destinations. Supports multi-destination routing:
7
+ *
8
+ * Normal: Receive → Storage → Pick → Pack → Ship
9
+ * Cross-dock: Receive → Ship-Dock-1 (15 units for Order A)
10
+ * → Ship-Dock-2 (10 units for Order B)
11
+ * → Storage (remaining 5 units)
12
+ */
13
+ var CrossDockEngine = class {
14
+ constructor(movePort, _quantPort) {
15
+ this.movePort = movePort;
16
+ }
17
+ async evaluate(skuRef, incomingQuantity, _nodeId, ctx) {
18
+ const allMoves = await this.movePort.findMany({
19
+ skuRef,
20
+ operationType: "shipment"
21
+ }, ctx);
22
+ const waitingStatuses = new Set([
23
+ "draft",
24
+ "planned",
25
+ "ready",
26
+ "waiting"
27
+ ]);
28
+ const waitingMoves = allMoves.filter((m) => waitingStatuses.has(m.status) && m.quantityPlanned > (m.quantityDone ?? 0)).sort((a, b) => new Date(a.createdAt ?? 0).getTime() - new Date(b.createdAt ?? 0).getTime());
29
+ if (waitingMoves.length === 0) return {
30
+ hasDemand: false,
31
+ demandQuantity: 0,
32
+ crossDockQuantity: 0,
33
+ remainingToStorage: incomingQuantity,
34
+ assignments: [],
35
+ crossDockDestination: null,
36
+ waitingMoveIds: []
37
+ };
38
+ const assignmentMap = /* @__PURE__ */ new Map();
39
+ let remaining = incomingQuantity;
40
+ const allWaitingIds = [];
41
+ for (const move of waitingMoves) {
42
+ if (remaining <= 0) break;
43
+ const needed = move.quantityPlanned - (move.quantityDone ?? 0);
44
+ const assigned = Math.min(needed, remaining);
45
+ remaining -= assigned;
46
+ allWaitingIds.push(move._id);
47
+ const dest = move.destinationLocationId;
48
+ const existing = assignmentMap.get(dest);
49
+ if (existing) {
50
+ existing.quantity += assigned;
51
+ existing.moveIds.push(move._id);
52
+ } else assignmentMap.set(dest, {
53
+ destinationLocationId: dest,
54
+ quantity: assigned,
55
+ moveIds: [move._id]
56
+ });
57
+ }
58
+ const assignments = [...assignmentMap.values()];
59
+ const crossDockQuantity = incomingQuantity - remaining;
60
+ return {
61
+ hasDemand: true,
62
+ demandQuantity: waitingMoves.reduce((sum, m) => sum + (m.quantityPlanned - (m.quantityDone ?? 0)), 0),
63
+ crossDockQuantity,
64
+ remainingToStorage: remaining,
65
+ assignments,
66
+ crossDockDestination: assignments[0]?.destinationLocationId ?? null,
67
+ waitingMoveIds: allWaitingIds
68
+ };
69
+ }
70
+ };
71
+ //#endregion
72
+ //#region src/routing/putaway.engine.ts
73
+ /**
74
+ * Putaway engine — resolves where received goods should be stored.
75
+ * Tries registered strategies in priority order, then falls back to default.
76
+ */
77
+ var PutawayEngine = class {
78
+ strategies = [];
79
+ constructor(_locationPort, defaultLocationResolver) {
80
+ this.defaultLocationResolver = defaultLocationResolver;
81
+ }
82
+ registerStrategy(strategy) {
83
+ this.strategies.push(strategy);
84
+ }
85
+ async resolve(skuRef, quantity, nodeId, context, ctx) {
86
+ for (const strategy of this.strategies) {
87
+ const result = await strategy.resolve(skuRef, quantity, nodeId, context);
88
+ if (result) return result;
89
+ }
90
+ const defaultLoc = await this.defaultLocationResolver(nodeId, ctx);
91
+ if (defaultLoc) return { locationId: defaultLoc };
92
+ throw new Error(`No putaway location resolved for ${skuRef} at node ${nodeId}`);
93
+ }
94
+ };
95
+ //#endregion
96
+ //#region src/routing/removal.engine.ts
97
+ /**
98
+ * Removal engine — delegates stock removal decisions to a RemovalPolicy.
99
+ */
100
+ var RemovalEngine = class {
101
+ constructor(defaultPolicy) {
102
+ this.defaultPolicy = defaultPolicy;
103
+ }
104
+ resolve(skuRef, quantity, candidates) {
105
+ return this.defaultPolicy.resolve(skuRef, quantity, candidates);
106
+ }
107
+ };
108
+ //#endregion
109
+ //#region src/routing/route-expander.ts
110
+ var RouteExpander = class {
111
+ customRoutes = [];
112
+ constructor(config = {
113
+ receptionSteps: 1,
114
+ deliverySteps: 1
115
+ }) {
116
+ this.config = config;
117
+ this.customRoutes = config.customRoutes ?? [];
118
+ }
119
+ /**
120
+ * Register a custom route template.
121
+ */
122
+ registerRoute(route) {
123
+ this.customRoutes.push(route);
124
+ }
125
+ /**
126
+ * Expand a high-level operation into planned move steps.
127
+ * Resolves location types to actual location IDs using the LocationResolver.
128
+ *
129
+ * Route matching priority (6-level resolution):
130
+ * 1. Demand-specific routes (options.routeId) — highest
131
+ * 2. Packaging/UoM routes
132
+ * 3. SKU-specific routes
133
+ * 4. SKU category routes
134
+ * 5. Node/warehouse routes
135
+ * 6. Operation type default route — lowest
136
+ */
137
+ async expand(operationType, nodeId, locations, options) {
138
+ if (options?.routeId) {
139
+ const custom = this.customRoutes.find((r) => r.name === options.routeId);
140
+ if (custom) return this.resolveSteps(custom.steps, nodeId, locations);
141
+ }
142
+ const template = this.getDefaultTemplate(operationType);
143
+ return this.resolveSteps(template, nodeId, locations);
144
+ }
145
+ /**
146
+ * Get the default route template based on operation type and configured step count.
147
+ *
148
+ * Complexity modes:
149
+ * - One-step: ship (small shop)
150
+ * - Two-step: pick → ship (standard warehouse)
151
+ * - Three-step: pick → pack → ship (e-commerce fulfillment)
152
+ *
153
+ * Cross-dock (transfer): receive → ship (bypass storage)
154
+ */
155
+ getDefaultTemplate(operationType) {
156
+ switch (operationType) {
157
+ case "receipt": return this.getReceptionTemplate();
158
+ case "shipment": return this.getDeliveryTemplate();
159
+ case "transfer": return [{
160
+ sourceLocationType: "storage",
161
+ destinationLocationType: "storage",
162
+ operationType: "transfer"
163
+ }];
164
+ default: return [{
165
+ sourceLocationType: "storage",
166
+ destinationLocationType: "storage",
167
+ operationType
168
+ }];
169
+ }
170
+ }
171
+ /**
172
+ * Auto-generated reception routes per warehouse:
173
+ * one_step: [vendor → stock]
174
+ * two_steps: [vendor → input, input → stock]
175
+ * three_steps: [vendor → input, input → qc, qc → stock]
176
+ */
177
+ getReceptionTemplate() {
178
+ switch (this.config.receptionSteps) {
179
+ case 1: return [{
180
+ sourceLocationType: "vendor",
181
+ destinationLocationType: "storage",
182
+ operationType: "receipt"
183
+ }];
184
+ case 2: return [{
185
+ sourceLocationType: "vendor",
186
+ destinationLocationType: "receiving",
187
+ operationType: "receipt"
188
+ }, {
189
+ sourceLocationType: "receiving",
190
+ destinationLocationType: "storage",
191
+ operationType: "transfer"
192
+ }];
193
+ case 3: return [
194
+ {
195
+ sourceLocationType: "vendor",
196
+ destinationLocationType: "receiving",
197
+ operationType: "receipt"
198
+ },
199
+ {
200
+ sourceLocationType: "receiving",
201
+ destinationLocationType: "quality_hold",
202
+ operationType: "transfer"
203
+ },
204
+ {
205
+ sourceLocationType: "quality_hold",
206
+ destinationLocationType: "storage",
207
+ operationType: "transfer"
208
+ }
209
+ ];
210
+ default: return [{
211
+ sourceLocationType: "vendor",
212
+ destinationLocationType: "storage",
213
+ operationType: "receipt"
214
+ }];
215
+ }
216
+ }
217
+ /**
218
+ * Auto-generated delivery routes per warehouse:
219
+ * one_step: [stock → customer]
220
+ * two_steps: [stock → shipping, shipping → customer]
221
+ * three_steps: [stock → packing, packing → shipping, shipping → customer]
222
+ */
223
+ getDeliveryTemplate() {
224
+ switch (this.config.deliverySteps) {
225
+ case 1: return [{
226
+ sourceLocationType: "storage",
227
+ destinationLocationType: "customer",
228
+ operationType: "shipment"
229
+ }];
230
+ case 2: return [{
231
+ sourceLocationType: "storage",
232
+ destinationLocationType: "shipping",
233
+ operationType: "shipment"
234
+ }, {
235
+ sourceLocationType: "shipping",
236
+ destinationLocationType: "customer",
237
+ operationType: "shipment"
238
+ }];
239
+ case 3: return [
240
+ {
241
+ sourceLocationType: "storage",
242
+ destinationLocationType: "packing",
243
+ operationType: "shipment"
244
+ },
245
+ {
246
+ sourceLocationType: "packing",
247
+ destinationLocationType: "shipping",
248
+ operationType: "shipment"
249
+ },
250
+ {
251
+ sourceLocationType: "shipping",
252
+ destinationLocationType: "customer",
253
+ operationType: "shipment"
254
+ }
255
+ ];
256
+ default: return [{
257
+ sourceLocationType: "storage",
258
+ destinationLocationType: "customer",
259
+ operationType: "shipment"
260
+ }];
261
+ }
262
+ }
263
+ /**
264
+ * Resolve location type references to actual location IDs.
265
+ */
266
+ async resolveSteps(steps, nodeId, resolver) {
267
+ const resolved = [];
268
+ for (let i = 0; i < steps.length; i++) {
269
+ const step = steps[i];
270
+ const sourceId = await resolver.resolveByType(nodeId, step.sourceLocationType);
271
+ const destId = await resolver.resolveByType(nodeId, step.destinationLocationType);
272
+ resolved.push({
273
+ sourceLocationId: sourceId,
274
+ destinationLocationId: destId,
275
+ operationType: step.operationType,
276
+ sequence: i + 1
277
+ });
278
+ }
279
+ return { steps: resolved };
280
+ }
281
+ };
282
+ //#endregion
283
+ //#region src/routing/strategies/abc-velocity.strategy.ts
284
+ /**
285
+ * ABC-velocity putaway strategy.
286
+ * Classifies SKUs by velocity (A/B/C) and routes to appropriate zones.
287
+ * A-items go near dock (low sortOrder), C-items go far back.
288
+ */
289
+ var AbcVelocityStrategy = class {
290
+ name = "abc_velocity";
291
+ constructor(velocityClassifier, zoneMappings) {
292
+ this.velocityClassifier = velocityClassifier;
293
+ this.zoneMappings = zoneMappings;
294
+ }
295
+ async resolve(skuRef, _quantity, _nodeId, _context) {
296
+ const velocityClass = this.velocityClassifier(skuRef);
297
+ const locationId = this.zoneMappings[velocityClass];
298
+ return locationId ? { locationId } : null;
299
+ }
300
+ };
301
+ //#endregion
302
+ //#region src/routing/strategies/category-zone.strategy.ts
303
+ /**
304
+ * Category-zone putaway strategy.
305
+ * Routes goods to zones based on their SKU category.
306
+ */
307
+ var CategoryZoneStrategy = class {
308
+ name = "category_zone";
309
+ constructor(categoryZoneMap) {
310
+ this.categoryZoneMap = categoryZoneMap;
311
+ }
312
+ async resolve(_skuRef, _quantity, _nodeId, context) {
313
+ if (!context.skuCategory) return null;
314
+ const locationId = this.categoryZoneMap.get(context.skuCategory);
315
+ return locationId ? { locationId } : null;
316
+ }
317
+ };
318
+ //#endregion
319
+ //#region src/routing/strategies/closest-to-pick.strategy.ts
320
+ /**
321
+ * Closest-to-pick putaway strategy.
322
+ * Selects the storage location with the lowest sortOrder (closest to pick face)
323
+ * that has remaining capacity.
324
+ */
325
+ var ClosestToPickStrategy = class {
326
+ name = "closest_to_pick";
327
+ constructor(locationPort, ctx) {
328
+ this.locationPort = locationPort;
329
+ this.ctx = ctx;
330
+ }
331
+ async resolve(_skuRef, quantity, nodeId, _context) {
332
+ const storageLocs = (await this.locationPort.findByNode(nodeId, this.ctx)).filter((loc) => loc.type === "storage" && loc.status === "active");
333
+ for (const loc of storageLocs) {
334
+ if (loc.capacity !== void 0 && quantity > loc.capacity) continue;
335
+ return { locationId: loc._id };
336
+ }
337
+ return null;
338
+ }
339
+ };
340
+ //#endregion
341
+ //#region src/routing/strategies/empty-bin.strategy.ts
342
+ /**
343
+ * Empty-bin putaway strategy.
344
+ * Finds the first storage location in the node that has zero quants.
345
+ */
346
+ var EmptyBinStrategy = class {
347
+ name = "empty_bin";
348
+ constructor(locationPort, quantPort, ctx) {
349
+ this.locationPort = locationPort;
350
+ this.quantPort = quantPort;
351
+ this.ctx = ctx;
352
+ }
353
+ async resolve(_skuRef, _quantity, nodeId, _context) {
354
+ const storageLocs = (await this.locationPort.findByNode(nodeId, this.ctx)).filter((loc) => loc.type === "storage" && loc.status === "active");
355
+ for (const loc of storageLocs) if ((await this.quantPort.getAvailability({ locationId: loc._id }, this.ctx)).quantityOnHand === 0) return { locationId: loc._id };
356
+ return null;
357
+ }
358
+ };
359
+ //#endregion
360
+ //#region src/routing/strategies/fixed-location.strategy.ts
361
+ /**
362
+ * Fixed-location putaway strategy.
363
+ * Maps specific SKUs to predetermined storage locations.
364
+ */
365
+ var FixedLocationStrategy = class {
366
+ name = "fixed_location";
367
+ constructor(skuLocationMap) {
368
+ this.skuLocationMap = skuLocationMap;
369
+ }
370
+ async resolve(skuRef, _quantity, _nodeId, _context) {
371
+ const locationId = this.skuLocationMap.get(skuRef);
372
+ return locationId ? { locationId } : null;
373
+ }
374
+ };
375
+ //#endregion
376
+ //#region src/routing/wave.engine.ts
377
+ const DEFAULT_ZONE = "ZZ";
378
+ const DEFAULT_BIN = "ZZ";
379
+ const DEFAULT_AISLE = 999;
380
+ const DEFAULT_BAY = 999;
381
+ const DEFAULT_LEVEL = 9;
382
+ const BAY_INVERSION_BASE = 1e3;
383
+ /**
384
+ * Wave Engine — production-grade pick-path optimization.
385
+ *
386
+ * Two sorting modes:
387
+ * 1. **sortOrder** (simple) — single integer, works for small warehouses
388
+ * 2. **coordinates** (3D) — zone/aisle/bay/level/bin tuple, serpentine path for large fulfillment centers
389
+ *
390
+ * Also supports:
391
+ * - Location consolidation (merge picks for same SKU at same bin)
392
+ * - Weight/volume/item batching (split into cart-sized sub-waves with pick splitting)
393
+ * - Wave summary for display
394
+ */
395
+ var WaveEngine = class {
396
+ /**
397
+ * Sort moves by location for optimal pick path.
398
+ * Uses 3D coordinates if available, falls back to sortOrder.
399
+ * Does not mutate input.
400
+ */
401
+ optimizePickPath(moves, locationMap) {
402
+ return [...moves].sort((a, b) => {
403
+ const sortKeyA = this.buildSortKey(a, locationMap);
404
+ const sortKeyB = this.buildSortKey(b, locationMap);
405
+ return sortKeyA.localeCompare(sortKeyB);
406
+ });
407
+ }
408
+ /**
409
+ * Consolidate moves for same SKU at same location into single pick lines.
410
+ */
411
+ consolidateByLocation(moves, locationMap) {
412
+ const picksByKey = /* @__PURE__ */ new Map();
413
+ for (const move of moves) {
414
+ const key = `${move.skuRef}::${move.sourceLocationId}`;
415
+ const existing = picksByKey.get(key);
416
+ if (existing) {
417
+ existing.totalQuantity += move.quantityPlanned;
418
+ existing.moveIds.push(move._id);
419
+ } else picksByKey.set(key, {
420
+ skuRef: move.skuRef,
421
+ locationId: move.sourceLocationId,
422
+ totalQuantity: move.quantityPlanned,
423
+ moveIds: [move._id],
424
+ sortKey: this.buildSortKey(move, locationMap)
425
+ });
426
+ }
427
+ return [...picksByKey.values()].sort((a, b) => a.sortKey.localeCompare(b.sortKey));
428
+ }
429
+ /**
430
+ * Split a consolidated pick list into cart-sized batches.
431
+ * Each batch respects weight, volume, and item count constraints.
432
+ * Oversized picks are split across multiple carts — no quantity is lost.
433
+ */
434
+ batchIntoCarts(picks, constraints, skuWeights, skuVolumes) {
435
+ const batches = [];
436
+ let currentBatch = this.createEmptyBatch();
437
+ for (const pick of picks) {
438
+ const unitWeight = skuWeights?.get(pick.skuRef) ?? 0;
439
+ const unitVolume = skuVolumes?.get(pick.skuRef) ?? 0;
440
+ let remainingQuantity = pick.totalQuantity;
441
+ while (remainingQuantity > 0) {
442
+ if (this.wouldExceedConstraints(remainingQuantity, unitWeight, unitVolume, currentBatch, constraints)) {
443
+ if (currentBatch.picks.length > 0) {
444
+ currentBatch = this.flushAndFillPartial(pick, remainingQuantity, unitWeight, unitVolume, currentBatch, constraints, batches);
445
+ remainingQuantity = this.recalculateRemaining(pick, batches, currentBatch);
446
+ continue;
447
+ }
448
+ }
449
+ const quantityForThisBatch = this.calculateFittingQuantity(remainingQuantity, unitWeight, unitVolume, currentBatch, constraints);
450
+ this.addPickToBatch(currentBatch, pick, quantityForThisBatch, unitWeight, unitVolume);
451
+ remainingQuantity -= quantityForThisBatch;
452
+ if (remainingQuantity > 0) {
453
+ batches.push(currentBatch);
454
+ currentBatch = this.createEmptyBatch();
455
+ }
456
+ }
457
+ }
458
+ if (currentBatch.picks.length > 0) batches.push(currentBatch);
459
+ return batches;
460
+ }
461
+ /**
462
+ * Create a summary of the wave.
463
+ */
464
+ createWaveSummary(moves) {
465
+ const locations = /* @__PURE__ */ new Set();
466
+ const skus = /* @__PURE__ */ new Set();
467
+ let totalQuantity = 0;
468
+ for (const move of moves) {
469
+ locations.add(move.sourceLocationId);
470
+ skus.add(move.skuRef);
471
+ totalQuantity += move.quantityPlanned;
472
+ }
473
+ return {
474
+ totalMoves: moves.length,
475
+ totalQuantity,
476
+ uniqueLocations: locations.size,
477
+ uniqueSkus: skus.size
478
+ };
479
+ }
480
+ /**
481
+ * Build a lexicographic sort key from location coordinates or metadata.
482
+ *
483
+ * Uses boustrophedon (serpentine/ox-plow) traversal for 3D coordinates:
484
+ * - Odd aisles: bays ascending (1→2→3→…)
485
+ * - Even aisles: bays descending (…→3→2→1)
486
+ * This eliminates dead-walking back to the front of each aisle.
487
+ */
488
+ buildSortKey(move, locationMap) {
489
+ if (locationMap) {
490
+ const location = locationMap.get(move.sourceLocationId);
491
+ if (location?.coordinates) return this.buildCoordinateSortKey(location.coordinates);
492
+ if (location?.sortOrder !== void 0) return this.padSortOrder(location.sortOrder);
493
+ }
494
+ return this.buildMetadataSortKey(move);
495
+ }
496
+ buildCoordinateSortKey(coords) {
497
+ const aisle = coords.aisle ?? DEFAULT_AISLE;
498
+ const bay = coords.bay ?? DEFAULT_BAY;
499
+ const effectiveBay = aisle % 2 === 0 ? BAY_INVERSION_BASE - bay : bay;
500
+ return [
501
+ coords.zone ?? DEFAULT_ZONE,
502
+ String(aisle).padStart(3, "0"),
503
+ String(effectiveBay).padStart(3, "0"),
504
+ String(coords.level ?? DEFAULT_LEVEL).padStart(1, "0"),
505
+ coords.bin ?? DEFAULT_BIN
506
+ ].join("-");
507
+ }
508
+ buildMetadataSortKey(move) {
509
+ const sortOrder = move.metadata?.locationSortOrder ?? Number.MAX_SAFE_INTEGER;
510
+ return this.padSortOrder(sortOrder);
511
+ }
512
+ padSortOrder(value) {
513
+ return String(value).padStart(10, "0");
514
+ }
515
+ createEmptyBatch() {
516
+ return {
517
+ picks: [],
518
+ totalWeight: 0,
519
+ totalVolume: 0,
520
+ totalItems: 0
521
+ };
522
+ }
523
+ addPickToBatch(batch, pick, quantity, unitWeight, unitVolume) {
524
+ batch.picks.push({
525
+ ...pick,
526
+ totalQuantity: quantity
527
+ });
528
+ batch.totalWeight += unitWeight * quantity;
529
+ batch.totalVolume += unitVolume * quantity;
530
+ batch.totalItems += 1;
531
+ }
532
+ wouldExceedConstraints(quantity, unitWeight, unitVolume, batch, constraints) {
533
+ const totalWeight = unitWeight * quantity;
534
+ const totalVolume = unitVolume * quantity;
535
+ return !!(constraints.maxWeight && batch.totalWeight + totalWeight > constraints.maxWeight || constraints.maxVolume && batch.totalVolume + totalVolume > constraints.maxVolume || constraints.maxItems && batch.totalItems + 1 > constraints.maxItems);
536
+ }
537
+ /**
538
+ * Flush the current batch, optionally fitting a partial quantity first.
539
+ * Item-count overflow skips partial fitting (the constraint is on lines, not units).
540
+ */
541
+ flushAndFillPartial(pick, remainingQuantity, unitWeight, unitVolume, currentBatch, constraints, batches) {
542
+ if (!!!(constraints.maxItems && currentBatch.totalItems + 1 > constraints.maxItems)) {
543
+ const partialQuantity = this.maxFittingUnits(remainingQuantity, unitWeight, unitVolume, currentBatch, constraints);
544
+ if (partialQuantity > 0) this.addPickToBatch(currentBatch, pick, partialQuantity, unitWeight, unitVolume);
545
+ }
546
+ batches.push(currentBatch);
547
+ return this.createEmptyBatch();
548
+ }
549
+ /**
550
+ * Recalculate remaining quantity for a pick after batches have been flushed.
551
+ */
552
+ recalculateRemaining(pick, batches, currentBatch) {
553
+ const alreadyBatched = [...batches, currentBatch].reduce((sum, batch) => sum + batch.picks.filter((p) => p.skuRef === pick.skuRef && p.locationId === pick.locationId).reduce((s, p) => s + p.totalQuantity, 0), 0);
554
+ return pick.totalQuantity - alreadyBatched;
555
+ }
556
+ /**
557
+ * Calculate how many units fit in the cart. Returns `wanted` if no
558
+ * weight/volume constraints apply (item-count-only or unconstrained).
559
+ */
560
+ calculateFittingQuantity(wanted, unitWeight, unitVolume, batch, constraints) {
561
+ const maxByPhysics = this.maxFittingUnits(wanted, unitWeight, unitVolume, batch, constraints);
562
+ return maxByPhysics > 0 ? maxByPhysics : wanted;
563
+ }
564
+ /**
565
+ * Maximum units that fit in the remaining cart capacity by weight/volume.
566
+ * Returns 0 if even 1 unit won't fit.
567
+ */
568
+ maxFittingUnits(wanted, unitWeight, unitVolume, batch, constraints) {
569
+ let max = wanted;
570
+ if (constraints.maxWeight && unitWeight > 0) {
571
+ const capacityLeft = constraints.maxWeight - batch.totalWeight;
572
+ max = Math.min(max, Math.floor(capacityLeft / unitWeight));
573
+ }
574
+ if (constraints.maxVolume && unitVolume > 0) {
575
+ const capacityLeft = constraints.maxVolume - batch.totalVolume;
576
+ max = Math.min(max, Math.floor(capacityLeft / unitVolume));
577
+ }
578
+ return Math.max(0, max);
579
+ }
580
+ };
581
+ //#endregion
582
+ export { AbcVelocityStrategy, CategoryZoneStrategy, ClosestToPickStrategy, CrossDockEngine, EmptyBinStrategy, FixedLocationStrategy, PutawayEngine, RemovalEngine, RouteExpander, WaveEngine };
@@ -0,0 +1,40 @@
1
+ //#region src/utils/document-number.ts
2
+ /**
3
+ * Format a document number from its constituent parts.
4
+ *
5
+ * @param prefix - Document type prefix (e.g. 'CHN', 'PO').
6
+ * @param sequence - Monotonically increasing sequence obtained from the counters collection.
7
+ * @param date - Optional date used for the YYYYMM segment (defaults to now).
8
+ * @returns A formatted document number, e.g. `PO-202603-0001`.
9
+ */
10
+ function formatDocumentNumber(prefix, sequence, date = /* @__PURE__ */ new Date()) {
11
+ return `${prefix}-${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, "0")}-${String(sequence).padStart(4, "0")}`;
12
+ }
13
+ //#endregion
14
+ //#region src/services/runtime-config.ts
15
+ /**
16
+ * Create a frozen RuntimeConfig instance.
17
+ * Used by createServices() to produce an instance-scoped config.
18
+ */
19
+ function createRuntimeConfig(mode, routing, valuation) {
20
+ return Object.freeze({
21
+ mode,
22
+ routing: Object.freeze({ ...routing }),
23
+ valuation: Object.freeze({ ...valuation })
24
+ });
25
+ }
26
+ /**
27
+ * Feature gates — pure functions that take a config instance.
28
+ * No global state; callers pass the config they own.
29
+ */
30
+ function requireStandardMode(config, feature) {
31
+ if (config.mode === "simple") throw new Error(`${feature} requires 'standard' or 'enterprise' mode (current: 'simple')`);
32
+ }
33
+ function requireEnterpriseMode(config, feature) {
34
+ if (config.mode !== "enterprise") throw new Error(`${feature} requires 'enterprise' mode (current: '${config.mode}')`);
35
+ }
36
+ function isRoutingEnabled(config, feature) {
37
+ return config.mode === "enterprise" && config.routing[feature];
38
+ }
39
+ //#endregion
40
+ export { formatDocumentNumber as a, requireStandardMode as i, isRoutingEnabled as n, requireEnterpriseMode as r, createRuntimeConfig as t };
@@ -0,0 +1,38 @@
1
+ import { n as FlowMode } from "./index-DFF0GJ4J.mjs";
2
+
3
+ //#region src/services/runtime-config.d.ts
4
+ /**
5
+ * Runtime configuration resolved from FlowConfig at engine creation time.
6
+ * Instance-scoped — each FlowEngine gets its own config object.
7
+ */
8
+ interface RuntimeConfig {
9
+ mode: FlowMode;
10
+ routing: {
11
+ putaway: boolean;
12
+ removal: boolean;
13
+ crossDock: boolean;
14
+ };
15
+ valuation: {
16
+ method: string;
17
+ };
18
+ }
19
+ /**
20
+ * Create a frozen RuntimeConfig instance.
21
+ * Used by createServices() to produce an instance-scoped config.
22
+ */
23
+ declare function createRuntimeConfig(mode: FlowMode, routing: {
24
+ putaway: boolean;
25
+ removal: boolean;
26
+ crossDock: boolean;
27
+ }, valuation: {
28
+ method: string;
29
+ }): RuntimeConfig;
30
+ /**
31
+ * Feature gates — pure functions that take a config instance.
32
+ * No global state; callers pass the config they own.
33
+ */
34
+ declare function requireStandardMode(config: RuntimeConfig, feature: string): void;
35
+ declare function requireEnterpriseMode(config: RuntimeConfig, feature: string): void;
36
+ declare function isRoutingEnabled(config: RuntimeConfig, feature: 'putaway' | 'removal' | 'crossDock'): boolean;
37
+ //#endregion
38
+ export { requireStandardMode as a, requireEnterpriseMode as i, createRuntimeConfig as n, isRoutingEnabled as r, RuntimeConfig as t };
@@ -0,0 +1,26 @@
1
+ //#region src/domain/value-objects/scan-token.d.ts
2
+ /**
3
+ * Resolved scan result — returned by the scan service after resolving a raw token.
4
+ */
5
+ interface ScanResolution {
6
+ token: string;
7
+ resolvedType: 'sku' | 'lot' | 'serial' | 'location' | 'package' | 'document' | 'unknown';
8
+ resolvedId: string | null;
9
+ resolvedEntity: Record<string, unknown> | null;
10
+ action?: 'receive' | 'pick' | 'move' | 'count' | 'verify';
11
+ }
12
+ /**
13
+ * GS1-128 decomposition — extracts multiple fields from one barcode.
14
+ */
15
+ interface GS1Resolution {
16
+ token: string;
17
+ components: Array<{
18
+ ai: string;
19
+ type: 'product' | 'lot' | 'serial' | 'expiry' | 'weight' | 'quantity';
20
+ value: string | number | Date;
21
+ }>;
22
+ resolvedSku: Record<string, unknown> | null;
23
+ resolvedLot: Record<string, unknown> | null;
24
+ }
25
+ //#endregion
26
+ export { ScanResolution as n, GS1Resolution as t };