@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.
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/allocation-policy-my_HfzdV.d.mts +23 -0
- package/dist/base-MWBqRFM2.mjs +16 -0
- package/dist/catalog-bridge-K8bdkncJ.d.mts +29 -0
- package/dist/cost-layer.port-iH9pvZqB.d.mts +30 -0
- package/dist/cost-layer.service-BQ1bs-XN.mjs +86 -0
- package/dist/cost-layer.service-DWmo9dQz.d.mts +53 -0
- package/dist/count.port-BRqwGbi3.d.mts +57 -0
- package/dist/counting/index.d.mts +2 -0
- package/dist/counting/index.mjs +2 -0
- package/dist/counting.service-BiQXqorv.mjs +232 -0
- package/dist/counting.service-CpAxU2G0.d.mts +74 -0
- package/dist/domain/contracts/index.d.mts +3 -0
- package/dist/domain/contracts/index.mjs +1 -0
- package/dist/domain/enums/index.d.mts +2 -0
- package/dist/domain/enums/index.mjs +4 -0
- package/dist/domain/index.d.mts +24 -0
- package/dist/domain/index.mjs +10 -0
- package/dist/domain/policies/index.d.mts +4 -0
- package/dist/domain/policies/index.mjs +1 -0
- package/dist/domain-D5cpMpR0.mjs +96 -0
- package/dist/domain-errors-D7S9ydNF.mjs +133 -0
- package/dist/enums-C3_z6aHC.mjs +82 -0
- package/dist/event-bus-BNmyoJb4.mjs +37 -0
- package/dist/event-bus-Um_xrcMY.d.mts +21 -0
- package/dist/event-emitter.port-BFh2pasY.d.mts +183 -0
- package/dist/event-types-BSqQOvXv.mjs +29 -0
- package/dist/events/index.d.mts +3 -0
- package/dist/events/index.mjs +3 -0
- package/dist/idempotency.port-CTC70JON.d.mts +55 -0
- package/dist/index-Bia4m8d2.d.mts +67 -0
- package/dist/index-BmNm3oNU2.d.mts +107 -0
- package/dist/index-C5PciI9P.d.mts +203 -0
- package/dist/index-CMTUKEK_.d.mts +308 -0
- package/dist/index-C_aEnozN.d.mts +220 -0
- package/dist/index-CulWO137.d.mts +107 -0
- package/dist/index-DFF0GJ4J.d.mts +36 -0
- package/dist/index-DsE7lZdO.d.mts +11 -0
- package/dist/index-DwO9IdNa.d.mts +1 -0
- package/dist/index-dtWUZr2a2.d.mts +350 -0
- package/dist/index.d.mts +128 -0
- package/dist/index.mjs +102 -0
- package/dist/insufficient-stock.error-Dyr4BYaV.mjs +15 -0
- package/dist/location.port-CValXIpb.d.mts +52 -0
- package/dist/lot.port-ChsmvZqs.d.mts +32 -0
- package/dist/models/index.d.mts +2 -0
- package/dist/models/index.mjs +2 -0
- package/dist/models-CHTMbp-G.mjs +1020 -0
- package/dist/move-group.port-DHGoQA3d.d.mts +56 -0
- package/dist/move-status-DkaFp2GD.mjs +38 -0
- package/dist/move.port-Qg1CYp7h.d.mts +89 -0
- package/dist/package.service-4tcAwBbr.mjs +95 -0
- package/dist/package.service-C605NaBQ.d.mts +42 -0
- package/dist/packaging/index.d.mts +2 -0
- package/dist/packaging/index.mjs +2 -0
- package/dist/procurement/index.d.mts +2 -0
- package/dist/procurement/index.mjs +2 -0
- package/dist/quant.port-BBa66PBT.d.mts +42 -0
- package/dist/removal-policy-BItBB8FD.d.mts +29 -0
- package/dist/replenishment-rule.port-DnEYtbyD.d.mts +78 -0
- package/dist/replenishment.service-BT9P-HKM.mjs +284 -0
- package/dist/replenishment.service-HO0sDhB_.d.mts +89 -0
- package/dist/reporting/index.d.mts +2 -0
- package/dist/reporting/index.mjs +2 -0
- package/dist/reporting-CL5ffrKM.mjs +243 -0
- package/dist/repositories/index.d.mts +2 -0
- package/dist/repositories/index.mjs +2 -0
- package/dist/repositories-nZXJKvLW.mjs +842 -0
- package/dist/reservation-status-ZfuTaWG0.mjs +22 -0
- package/dist/reservation.port-l9NFQ0si.d.mts +85 -0
- package/dist/reservations/index.d.mts +2 -0
- package/dist/reservations/index.mjs +2 -0
- package/dist/reservations-Cg4wN0QB.mjs +112 -0
- package/dist/routing/index.d.mts +362 -0
- package/dist/routing/index.mjs +582 -0
- package/dist/runtime-config-C0ggPkiK.mjs +40 -0
- package/dist/runtime-config-CQLtPPqY.d.mts +38 -0
- package/dist/scan-token-CNM9QVLY.d.mts +26 -0
- package/dist/scanning/index.d.mts +45 -0
- package/dist/scanning/index.mjs +228 -0
- package/dist/services/index.d.mts +8 -0
- package/dist/services/index.mjs +8 -0
- package/dist/services-_lLO4Xbl.mjs +1009 -0
- package/dist/stock-move-group-C0DqUfPY.mjs +88 -0
- package/dist/stock-package-BIarxbDS.d.mts +19 -0
- package/dist/stock-quant-CZhgvTu7.d.mts +41 -0
- package/dist/tenant-guard-6Ne-BILP.mjs +12 -0
- package/dist/tenant-isolation.error-D3OcKUdx.mjs +11 -0
- package/dist/trace.service-B9vAh-l-.d.mts +55 -0
- package/dist/trace.service-DE6Eh8_8.mjs +71 -0
- package/dist/traceability/index.d.mts +2 -0
- package/dist/traceability/index.mjs +2 -0
- package/dist/types/index.d.mts +2 -0
- package/dist/types/index.mjs +1 -0
- package/dist/unit-of-work.port-CWEkrDKu.d.mts +17 -0
- package/dist/valuation/index.d.mts +78 -0
- package/dist/valuation/index.mjs +103 -0
- package/dist/valuation-policy-Dco8c9Vw.d.mts +14 -0
- package/dist/virtual-locations-B9zXqPdi.d.mts +38 -0
- package/package.json +155 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { h as ValidationError, u as ProcurementNotApprovedError } from "./domain-errors-D7S9ydNF.mjs";
|
|
2
|
+
import { t as assertTenantContext } from "./tenant-guard-6Ne-BILP.mjs";
|
|
3
|
+
import { t as FlowEvents } from "./event-types-BSqQOvXv.mjs";
|
|
4
|
+
import { a as formatDocumentNumber, i as requireStandardMode } from "./runtime-config-C0ggPkiK.mjs";
|
|
5
|
+
import { n as MoveStatus } from "./move-status-DkaFp2GD.mjs";
|
|
6
|
+
//#region src/services/procurement.service.ts
|
|
7
|
+
var ProcurementService = class {
|
|
8
|
+
constructor(procurementPort, moveGroupPort, movePort, quantPort, unitOfWork, eventEmitter, idempotency, getNextSequence, virtualLocations) {
|
|
9
|
+
this.procurementPort = procurementPort;
|
|
10
|
+
this.moveGroupPort = moveGroupPort;
|
|
11
|
+
this.movePort = movePort;
|
|
12
|
+
this.quantPort = quantPort;
|
|
13
|
+
this.unitOfWork = unitOfWork;
|
|
14
|
+
this.eventEmitter = eventEmitter;
|
|
15
|
+
this.idempotency = idempotency;
|
|
16
|
+
this.getNextSequence = getNextSequence;
|
|
17
|
+
this.virtualLocations = virtualLocations;
|
|
18
|
+
}
|
|
19
|
+
async create(input, ctx) {
|
|
20
|
+
assertTenantContext(ctx);
|
|
21
|
+
if (!input.items || input.items.length === 0) throw new ValidationError("Procurement order must have at least one item");
|
|
22
|
+
const orderNumber = formatDocumentNumber("PO", await this.getNextSequence("PO", ctx.organizationId));
|
|
23
|
+
return this.procurementPort.create({
|
|
24
|
+
organizationId: ctx.organizationId,
|
|
25
|
+
orderNumber,
|
|
26
|
+
vendorRef: input.vendorRef,
|
|
27
|
+
destinationNodeId: input.destinationNodeId,
|
|
28
|
+
destinationLocationId: input.destinationLocationId,
|
|
29
|
+
status: "draft",
|
|
30
|
+
items: input.items.map((item) => ({
|
|
31
|
+
skuRef: item.skuRef,
|
|
32
|
+
quantity: item.quantity,
|
|
33
|
+
quantityReceived: 0,
|
|
34
|
+
unitCost: item.unitCost,
|
|
35
|
+
expectedAt: item.expectedAt
|
|
36
|
+
})),
|
|
37
|
+
expectedAt: input.items[0]?.expectedAt,
|
|
38
|
+
sourceDemandRefs: input.sourceDemandRefs,
|
|
39
|
+
createdBy: ctx.actorId,
|
|
40
|
+
metadata: input.metadata
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async getById(orderId, ctx) {
|
|
44
|
+
assertTenantContext(ctx);
|
|
45
|
+
return this.procurementPort.findById(orderId, ctx);
|
|
46
|
+
}
|
|
47
|
+
async list(query, ctx) {
|
|
48
|
+
assertTenantContext(ctx);
|
|
49
|
+
return this.procurementPort.list(query, ctx);
|
|
50
|
+
}
|
|
51
|
+
async approve(orderId, ctx) {
|
|
52
|
+
assertTenantContext(ctx);
|
|
53
|
+
const order = await this.requireOrder(orderId, ctx);
|
|
54
|
+
if (order.status !== "draft") throw new ValidationError(`Cannot approve order in status: ${order.status}`);
|
|
55
|
+
return this.procurementPort.updateStatus(orderId, "approved", {});
|
|
56
|
+
}
|
|
57
|
+
async receive(orderId, payload, ctx) {
|
|
58
|
+
assertTenantContext(ctx);
|
|
59
|
+
const scopedKey = ctx.idempotencyKey ? `${ctx.organizationId}:${ctx.idempotencyKey}` : null;
|
|
60
|
+
if (scopedKey && this.idempotency) {
|
|
61
|
+
if (this.idempotency.claim) {
|
|
62
|
+
const claimResult = await this.idempotency.claim(scopedKey);
|
|
63
|
+
if (claimResult.status === "hit") return claimResult.result;
|
|
64
|
+
if (claimResult.status === "busy") throw new Error(`Concurrent operation in progress for idempotency key: ${ctx.idempotencyKey}`);
|
|
65
|
+
try {
|
|
66
|
+
const result = await this.executeReceive(orderId, payload, ctx);
|
|
67
|
+
await this.idempotency?.complete?.(scopedKey, result);
|
|
68
|
+
return result;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
await this.idempotency?.release?.(scopedKey);
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const cached = await this.idempotency.check(scopedKey);
|
|
75
|
+
if (cached.hit) return cached.result;
|
|
76
|
+
const result = await this.executeReceive(orderId, payload, ctx);
|
|
77
|
+
await this.idempotency.save(scopedKey, result);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
return this.executeReceive(orderId, payload, ctx);
|
|
81
|
+
}
|
|
82
|
+
async executeReceive(orderId, payload, ctx) {
|
|
83
|
+
const order = await this.requireOrder(orderId, ctx);
|
|
84
|
+
if (![
|
|
85
|
+
"approved",
|
|
86
|
+
"ordered",
|
|
87
|
+
"partially_received"
|
|
88
|
+
].includes(order.status)) throw new ProcurementNotApprovedError(orderId);
|
|
89
|
+
for (const line of payload.lines) {
|
|
90
|
+
if (line.quantityReceived <= 0) throw new ValidationError(`quantityReceived must be positive for ${line.skuRef}`);
|
|
91
|
+
const orderItem = order.items.find((i) => i.skuRef === line.skuRef);
|
|
92
|
+
if (orderItem) {
|
|
93
|
+
if (orderItem.quantityReceived + line.quantityReceived > orderItem.quantity * 1.1) throw new ValidationError(`Over-receipt: ${line.skuRef} would exceed ordered quantity by more than 10%`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return await this.unitOfWork.withTransaction(async (session) => {
|
|
97
|
+
const receivedAt = payload.receivedAt ?? /* @__PURE__ */ new Date();
|
|
98
|
+
const docNumber = formatDocumentNumber("RCV", await this.getNextSequence("RCV", ctx.organizationId));
|
|
99
|
+
const moveGroup = await this.moveGroupPort.create({
|
|
100
|
+
organizationId: ctx.organizationId,
|
|
101
|
+
groupType: "receipt",
|
|
102
|
+
documentNumber: docNumber,
|
|
103
|
+
destinationNodeId: order.destinationNodeId,
|
|
104
|
+
counterparty: {
|
|
105
|
+
type: "vendor",
|
|
106
|
+
id: order.vendorRef
|
|
107
|
+
},
|
|
108
|
+
createdBy: ctx.actorId
|
|
109
|
+
}, session);
|
|
110
|
+
for (const line of payload.lines) {
|
|
111
|
+
const orderItem = order.items.find((i) => i.skuRef === line.skuRef);
|
|
112
|
+
if (!orderItem) continue;
|
|
113
|
+
const move = await this.movePort.create({
|
|
114
|
+
moveGroupId: moveGroup._id,
|
|
115
|
+
operationType: "receipt",
|
|
116
|
+
skuRef: line.skuRef,
|
|
117
|
+
sourceLocationId: this.virtualLocations.vendor,
|
|
118
|
+
destinationLocationId: order.destinationLocationId ?? order.destinationNodeId,
|
|
119
|
+
quantityPlanned: line.quantityReceived,
|
|
120
|
+
organizationId: ctx.organizationId,
|
|
121
|
+
createdBy: ctx.actorId,
|
|
122
|
+
metadata: {
|
|
123
|
+
procurementOrderId: orderId,
|
|
124
|
+
unitCost: line.unitCost ?? orderItem.unitCost
|
|
125
|
+
}
|
|
126
|
+
}, session);
|
|
127
|
+
await this.movePort.updateStatus(move._id, MoveStatus.done, {
|
|
128
|
+
quantityDone: line.quantityReceived,
|
|
129
|
+
executedAt: receivedAt
|
|
130
|
+
}, session);
|
|
131
|
+
await this.quantPort.upsert({
|
|
132
|
+
organizationId: ctx.organizationId,
|
|
133
|
+
skuRef: line.skuRef,
|
|
134
|
+
locationId: order.destinationLocationId ?? order.destinationNodeId,
|
|
135
|
+
quantityDelta: line.quantityReceived,
|
|
136
|
+
inDate: receivedAt,
|
|
137
|
+
unitCost: line.unitCost ?? orderItem.unitCost
|
|
138
|
+
}, session);
|
|
139
|
+
}
|
|
140
|
+
const updatedItems = order.items.map((item) => {
|
|
141
|
+
const receiveLine = payload.lines.find((l) => l.skuRef === item.skuRef);
|
|
142
|
+
return {
|
|
143
|
+
...item,
|
|
144
|
+
quantityReceived: item.quantityReceived + (receiveLine?.quantityReceived ?? 0)
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
const allReceived = updatedItems.every((item) => item.quantityReceived >= item.quantity);
|
|
148
|
+
const newStatus = allReceived ? "received" : "partially_received";
|
|
149
|
+
const updated = await this.procurementPort.updateStatus(orderId, newStatus, {
|
|
150
|
+
items: updatedItems,
|
|
151
|
+
receivedAt: allReceived ? receivedAt : void 0
|
|
152
|
+
}, session);
|
|
153
|
+
await this.eventEmitter.emit(FlowEvents.PROCUREMENT_RECEIVED, {
|
|
154
|
+
organizationId: ctx.organizationId,
|
|
155
|
+
orderId: updated._id,
|
|
156
|
+
orderNumber: updated.orderNumber,
|
|
157
|
+
vendorRef: updated.vendorRef,
|
|
158
|
+
destinationNodeId: updated.destinationNodeId,
|
|
159
|
+
itemCount: payload.lines.length
|
|
160
|
+
}, session);
|
|
161
|
+
return updated;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async cancel(orderId, ctx) {
|
|
165
|
+
assertTenantContext(ctx);
|
|
166
|
+
if ((await this.requireOrder(orderId, ctx)).status === "received") throw new ValidationError("Cannot cancel a fully received procurement order");
|
|
167
|
+
return this.procurementPort.updateStatus(orderId, "cancelled", {});
|
|
168
|
+
}
|
|
169
|
+
async requireOrder(orderId, ctx) {
|
|
170
|
+
const order = await this.procurementPort.findById(orderId, ctx);
|
|
171
|
+
if (!order) throw new ValidationError(`Procurement order ${orderId} not found`);
|
|
172
|
+
return order;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/services/replenishment.service.ts
|
|
177
|
+
var ReplenishmentService = class {
|
|
178
|
+
constructor(rulePort, quantPort, movePort, procurementPort, eventEmitter, getNextSequence, runtimeConfig) {
|
|
179
|
+
this.rulePort = rulePort;
|
|
180
|
+
this.quantPort = quantPort;
|
|
181
|
+
this.movePort = movePort;
|
|
182
|
+
this.procurementPort = procurementPort;
|
|
183
|
+
this.eventEmitter = eventEmitter;
|
|
184
|
+
this.getNextSequence = getNextSequence;
|
|
185
|
+
this.runtimeConfig = runtimeConfig;
|
|
186
|
+
}
|
|
187
|
+
async evaluateRules(scope, ctx) {
|
|
188
|
+
requireStandardMode(this.runtimeConfig, "Replenishment");
|
|
189
|
+
assertTenantContext(ctx);
|
|
190
|
+
let rules;
|
|
191
|
+
if (scope.skuRef) rules = await this.rulePort.findBySkuRef(scope.skuRef, ctx);
|
|
192
|
+
else if (scope.nodeId) rules = await this.rulePort.findByNode(scope.nodeId, ctx);
|
|
193
|
+
else rules = await this.rulePort.list(ctx);
|
|
194
|
+
const triggers = [];
|
|
195
|
+
for (const rule of rules) {
|
|
196
|
+
if (!rule.enabled) continue;
|
|
197
|
+
const availability = await this.quantPort.getAvailability({
|
|
198
|
+
skuRef: rule.skuRef,
|
|
199
|
+
locationId: rule.locationId
|
|
200
|
+
}, ctx);
|
|
201
|
+
const incoming = (await this.movePort.findMany({
|
|
202
|
+
organizationId: ctx.organizationId,
|
|
203
|
+
skuRef: rule.skuRef,
|
|
204
|
+
destinationLocationId: rule.locationId,
|
|
205
|
+
status: { $in: [
|
|
206
|
+
"draft",
|
|
207
|
+
"planned",
|
|
208
|
+
"waiting",
|
|
209
|
+
"ready"
|
|
210
|
+
] }
|
|
211
|
+
}, ctx)).reduce((sum, m) => sum + m.quantityPlanned, 0);
|
|
212
|
+
const outgoing = (await this.movePort.findMany({
|
|
213
|
+
organizationId: ctx.organizationId,
|
|
214
|
+
skuRef: rule.skuRef,
|
|
215
|
+
sourceLocationId: rule.locationId,
|
|
216
|
+
status: { $in: [
|
|
217
|
+
"draft",
|
|
218
|
+
"planned",
|
|
219
|
+
"waiting",
|
|
220
|
+
"ready"
|
|
221
|
+
] }
|
|
222
|
+
}, ctx)).reduce((sum, m) => sum + m.quantityPlanned, 0);
|
|
223
|
+
const projected = availability.quantityOnHand + incoming - outgoing;
|
|
224
|
+
if (projected <= rule.reorderPoint) {
|
|
225
|
+
let needed = rule.targetLevel - projected;
|
|
226
|
+
if (rule.minOrderQty && needed < rule.minOrderQty) needed = rule.minOrderQty;
|
|
227
|
+
if (rule.multipleOf && rule.multipleOf > 1) needed = Math.ceil(needed / rule.multipleOf) * rule.multipleOf;
|
|
228
|
+
if (rule.maxOrderQty && needed > rule.maxOrderQty) needed = rule.maxOrderQty;
|
|
229
|
+
triggers.push({
|
|
230
|
+
ruleId: rule._id,
|
|
231
|
+
skuRef: rule.skuRef,
|
|
232
|
+
locationId: rule.locationId ?? rule.scopeRef,
|
|
233
|
+
currentLevel: projected,
|
|
234
|
+
reorderPoint: rule.reorderPoint,
|
|
235
|
+
targetLevel: rule.targetLevel,
|
|
236
|
+
suggestedQty: needed,
|
|
237
|
+
preferredSourceType: rule.preferredSourceType,
|
|
238
|
+
preferredSourceRef: rule.preferredSourceRef
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { triggers };
|
|
243
|
+
}
|
|
244
|
+
async generateDemand(evaluation, ctx) {
|
|
245
|
+
assertTenantContext(ctx);
|
|
246
|
+
const orders = [];
|
|
247
|
+
const vendorGroups = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const trigger of evaluation.triggers) {
|
|
249
|
+
const key = trigger.preferredSourceRef ?? "default_vendor";
|
|
250
|
+
if (!vendorGroups.has(key)) vendorGroups.set(key, []);
|
|
251
|
+
vendorGroups.get(key)?.push(trigger);
|
|
252
|
+
}
|
|
253
|
+
for (const [vendorRef, triggers] of vendorGroups) {
|
|
254
|
+
const orderNumber = formatDocumentNumber("PO", await this.getNextSequence("PO", ctx.organizationId));
|
|
255
|
+
const order = await this.procurementPort.create({
|
|
256
|
+
organizationId: ctx.organizationId,
|
|
257
|
+
orderNumber,
|
|
258
|
+
vendorRef,
|
|
259
|
+
destinationNodeId: triggers[0].locationId,
|
|
260
|
+
status: "draft",
|
|
261
|
+
items: triggers.map((t) => ({
|
|
262
|
+
skuRef: t.skuRef,
|
|
263
|
+
quantity: t.suggestedQty,
|
|
264
|
+
quantityReceived: 0,
|
|
265
|
+
unitCost: 0
|
|
266
|
+
})),
|
|
267
|
+
sourceDemandRefs: triggers.map((t) => t.ruleId),
|
|
268
|
+
createdBy: ctx.actorId
|
|
269
|
+
});
|
|
270
|
+
orders.push(order);
|
|
271
|
+
for (const trigger of triggers) await this.eventEmitter.emit(FlowEvents.REPLENISHMENT_TRIGGERED, {
|
|
272
|
+
organizationId: ctx.organizationId,
|
|
273
|
+
ruleId: trigger.ruleId,
|
|
274
|
+
skuRef: trigger.skuRef,
|
|
275
|
+
currentLevel: trigger.currentLevel,
|
|
276
|
+
reorderPoint: trigger.reorderPoint,
|
|
277
|
+
suggestedQty: trigger.suggestedQty
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return orders;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
//#endregion
|
|
284
|
+
export { ProcurementService as n, ReplenishmentService as t };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { n as VirtualLocationMap } from "./virtual-locations-B9zXqPdi.mjs";
|
|
2
|
+
import { n as MovePort } from "./move.port-Qg1CYp7h.mjs";
|
|
3
|
+
import { i as PaginatedResult, r as ListQuery, t as FlowContext } from "./index-DFF0GJ4J.mjs";
|
|
4
|
+
import { a as ProcurementOrder, n as ProcurementPort, t as ReplenishmentRulePort } from "./replenishment-rule.port-DnEYtbyD.mjs";
|
|
5
|
+
import { n as MoveGroupPort } from "./move-group.port-DHGoQA3d.mjs";
|
|
6
|
+
import { n as UnitOfWork } from "./unit-of-work.port-CWEkrDKu.mjs";
|
|
7
|
+
import { t as EventEmitterPort } from "./event-emitter.port-BFh2pasY.mjs";
|
|
8
|
+
import { r as QuantPort } from "./quant.port-BBa66PBT.mjs";
|
|
9
|
+
import { t as RuntimeConfig } from "./runtime-config-CQLtPPqY.mjs";
|
|
10
|
+
import { t as IdempotencyPort } from "./idempotency.port-CTC70JON.mjs";
|
|
11
|
+
|
|
12
|
+
//#region src/services/procurement.service.d.ts
|
|
13
|
+
interface CreateProcurementInput {
|
|
14
|
+
vendorRef: string;
|
|
15
|
+
destinationNodeId: string;
|
|
16
|
+
destinationLocationId?: string;
|
|
17
|
+
items: Array<{
|
|
18
|
+
skuRef: string;
|
|
19
|
+
quantity: number;
|
|
20
|
+
unitCost: number;
|
|
21
|
+
expectedAt?: Date;
|
|
22
|
+
}>;
|
|
23
|
+
sourceDemandRefs?: string[];
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
interface ReceivePayload {
|
|
27
|
+
lines: Array<{
|
|
28
|
+
skuRef: string;
|
|
29
|
+
quantityReceived: number;
|
|
30
|
+
lotCode?: string;
|
|
31
|
+
unitCost?: number;
|
|
32
|
+
}>;
|
|
33
|
+
receivedAt?: Date;
|
|
34
|
+
}
|
|
35
|
+
declare class ProcurementService {
|
|
36
|
+
private procurementPort;
|
|
37
|
+
private moveGroupPort;
|
|
38
|
+
private movePort;
|
|
39
|
+
private quantPort;
|
|
40
|
+
private unitOfWork;
|
|
41
|
+
private eventEmitter;
|
|
42
|
+
private idempotency;
|
|
43
|
+
private getNextSequence;
|
|
44
|
+
private virtualLocations;
|
|
45
|
+
constructor(procurementPort: ProcurementPort, moveGroupPort: MoveGroupPort, movePort: MovePort, quantPort: QuantPort, unitOfWork: UnitOfWork, eventEmitter: EventEmitterPort, idempotency: IdempotencyPort | null, getNextSequence: (prefix: string, organizationId: string) => Promise<number>, virtualLocations: VirtualLocationMap);
|
|
46
|
+
create(input: CreateProcurementInput, ctx: FlowContext): Promise<ProcurementOrder>;
|
|
47
|
+
getById(orderId: string, ctx: FlowContext): Promise<ProcurementOrder | null>;
|
|
48
|
+
list(query: ListQuery, ctx: FlowContext): Promise<PaginatedResult<ProcurementOrder>>;
|
|
49
|
+
approve(orderId: string, ctx: FlowContext): Promise<ProcurementOrder>;
|
|
50
|
+
receive(orderId: string, payload: ReceivePayload, ctx: FlowContext): Promise<ProcurementOrder>;
|
|
51
|
+
private executeReceive;
|
|
52
|
+
cancel(orderId: string, ctx: FlowContext): Promise<ProcurementOrder>;
|
|
53
|
+
private requireOrder;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/services/replenishment.service.d.ts
|
|
57
|
+
interface ReplenishmentScope {
|
|
58
|
+
nodeId?: string;
|
|
59
|
+
skuRef?: string;
|
|
60
|
+
mode?: 'scheduled' | 'emergency';
|
|
61
|
+
}
|
|
62
|
+
interface ReplenishmentTrigger {
|
|
63
|
+
ruleId: string;
|
|
64
|
+
skuRef: string;
|
|
65
|
+
locationId: string;
|
|
66
|
+
currentLevel: number;
|
|
67
|
+
reorderPoint: number;
|
|
68
|
+
targetLevel: number;
|
|
69
|
+
suggestedQty: number;
|
|
70
|
+
preferredSourceType?: string;
|
|
71
|
+
preferredSourceRef?: string;
|
|
72
|
+
}
|
|
73
|
+
interface ReplenishmentEvaluation {
|
|
74
|
+
triggers: ReplenishmentTrigger[];
|
|
75
|
+
}
|
|
76
|
+
declare class ReplenishmentService {
|
|
77
|
+
private rulePort;
|
|
78
|
+
private quantPort;
|
|
79
|
+
private movePort;
|
|
80
|
+
private procurementPort;
|
|
81
|
+
private eventEmitter;
|
|
82
|
+
private getNextSequence;
|
|
83
|
+
private runtimeConfig;
|
|
84
|
+
constructor(rulePort: ReplenishmentRulePort, quantPort: QuantPort, movePort: MovePort, procurementPort: ProcurementPort, eventEmitter: EventEmitterPort, getNextSequence: (prefix: string, organizationId: string) => Promise<number>, runtimeConfig: RuntimeConfig);
|
|
85
|
+
evaluateRules(scope: ReplenishmentScope, ctx: FlowContext): Promise<ReplenishmentEvaluation>;
|
|
86
|
+
generateDemand(evaluation: ReplenishmentEvaluation, ctx: FlowContext): Promise<ProcurementOrder[]>;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { CreateProcurementInput as a, ReplenishmentTrigger as i, ReplenishmentScope as n, ProcurementService as o, ReplenishmentService as r, ReceivePayload as s, ReplenishmentEvaluation as t };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as StockAgingResult, c as AvailabilityMatrixQuery, i as StockAgingReport, l as AvailabilityMatrixResult, n as TurnoverResult, o as HealthMetricsReport, r as AgingBucket, s as StockHealthMetrics, t as TurnoverReport, u as AvailabilityReport } from "../index-BmNm3oNU2.mjs";
|
|
2
|
+
export { AgingBucket, AvailabilityMatrixQuery, AvailabilityMatrixResult, AvailabilityReport, HealthMetricsReport, StockAgingReport, StockAgingResult, StockHealthMetrics, TurnoverReport, TurnoverResult };
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { t as assertTenantContext } from "./tenant-guard-6Ne-BILP.mjs";
|
|
2
|
+
//#region src/reporting/availability.ts
|
|
3
|
+
var AvailabilityReport = class {
|
|
4
|
+
constructor(quantPort, locationPort) {
|
|
5
|
+
this.quantPort = quantPort;
|
|
6
|
+
this.locationPort = locationPort;
|
|
7
|
+
}
|
|
8
|
+
async getMatrix(query, ctx) {
|
|
9
|
+
assertTenantContext(ctx);
|
|
10
|
+
const matrix = [];
|
|
11
|
+
if (query.nodeIds && query.nodeIds.length > 0) for (const skuRef of query.skuRefs) {
|
|
12
|
+
const nodes = [];
|
|
13
|
+
let totalOnHand = 0;
|
|
14
|
+
let totalAvailable = 0;
|
|
15
|
+
for (const nodeId of query.nodeIds) {
|
|
16
|
+
const availability = await this.quantPort.getAvailability({
|
|
17
|
+
skuRef,
|
|
18
|
+
nodeId
|
|
19
|
+
}, ctx);
|
|
20
|
+
nodes.push({
|
|
21
|
+
nodeId,
|
|
22
|
+
onHand: availability.quantityOnHand,
|
|
23
|
+
reserved: availability.quantityReserved,
|
|
24
|
+
available: availability.quantityAvailable,
|
|
25
|
+
incoming: availability.quantityIncoming,
|
|
26
|
+
outgoing: availability.quantityOutgoing
|
|
27
|
+
});
|
|
28
|
+
totalOnHand += availability.quantityOnHand;
|
|
29
|
+
totalAvailable += availability.quantityAvailable;
|
|
30
|
+
}
|
|
31
|
+
matrix.push({
|
|
32
|
+
skuRef,
|
|
33
|
+
nodes,
|
|
34
|
+
totalOnHand,
|
|
35
|
+
totalAvailable
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else for (const skuRef of query.skuRefs) {
|
|
39
|
+
const availability = await this.quantPort.getAvailability({ skuRef }, ctx);
|
|
40
|
+
const nodeGroups = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const breakdown of availability.breakdowns) {
|
|
42
|
+
let nodeId = "unknown";
|
|
43
|
+
if (this.locationPort) {
|
|
44
|
+
const loc = await this.locationPort.findById(breakdown.locationId, ctx);
|
|
45
|
+
if (loc) nodeId = loc.nodeId;
|
|
46
|
+
}
|
|
47
|
+
const existing = nodeGroups.get(nodeId) ?? {
|
|
48
|
+
onHand: 0,
|
|
49
|
+
reserved: 0,
|
|
50
|
+
available: 0,
|
|
51
|
+
incoming: 0,
|
|
52
|
+
outgoing: 0
|
|
53
|
+
};
|
|
54
|
+
existing.onHand += breakdown.quantityOnHand;
|
|
55
|
+
existing.reserved += breakdown.quantityReserved;
|
|
56
|
+
existing.available += breakdown.quantityAvailable;
|
|
57
|
+
existing.incoming += breakdown.quantityIncoming ?? 0;
|
|
58
|
+
existing.outgoing += breakdown.quantityOutgoing ?? 0;
|
|
59
|
+
nodeGroups.set(nodeId, existing);
|
|
60
|
+
}
|
|
61
|
+
if (nodeGroups.size === 0) nodeGroups.set("all", {
|
|
62
|
+
onHand: availability.quantityOnHand,
|
|
63
|
+
reserved: availability.quantityReserved,
|
|
64
|
+
available: availability.quantityAvailable,
|
|
65
|
+
incoming: availability.quantityIncoming,
|
|
66
|
+
outgoing: availability.quantityOutgoing
|
|
67
|
+
});
|
|
68
|
+
const nodes = [...nodeGroups.entries()].map(([nodeId, data]) => ({
|
|
69
|
+
nodeId,
|
|
70
|
+
...data
|
|
71
|
+
}));
|
|
72
|
+
matrix.push({
|
|
73
|
+
skuRef,
|
|
74
|
+
nodes,
|
|
75
|
+
totalOnHand: availability.quantityOnHand,
|
|
76
|
+
totalAvailable: availability.quantityAvailable
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { matrix };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/reporting/health-metrics.ts
|
|
84
|
+
var HealthMetricsReport = class {
|
|
85
|
+
constructor(quantPort, _movePort) {
|
|
86
|
+
this.quantPort = quantPort;
|
|
87
|
+
}
|
|
88
|
+
async generate(ctx) {
|
|
89
|
+
assertTenantContext(ctx);
|
|
90
|
+
const withStock = (await this.quantPort.findMany({ organizationId: ctx.organizationId }, ctx)).filter((q) => q.quantityOnHand > 0);
|
|
91
|
+
const totalOnHand = withStock.reduce((s, q) => s + q.quantityOnHand, 0);
|
|
92
|
+
const totalValue = withStock.reduce((s, q) => s + q.quantityOnHand * (q.unitCost ?? 0), 0);
|
|
93
|
+
const uniqueSkus = new Set(withStock.map((q) => q.skuRef));
|
|
94
|
+
const now = /* @__PURE__ */ new Date();
|
|
95
|
+
const yearAgo = /* @__PURE__ */ new Date(now.getTime() - 365 * 864e5);
|
|
96
|
+
const deadStockValue = withStock.filter((q) => {
|
|
97
|
+
const lastMove = q.lastMovementAt ?? q.inDate;
|
|
98
|
+
return new Date(lastMove).getTime() < yearAgo.getTime();
|
|
99
|
+
}).reduce((s, q) => s + q.quantityOnHand * (q.unitCost ?? 0), 0);
|
|
100
|
+
const thirtyDaysOut = new Date(now.getTime() + 30 * 864e5);
|
|
101
|
+
const expiryRiskValue = withStock.filter((q) => q.lotExpiresAt && new Date(q.lotExpiresAt).getTime() < thirtyDaysOut.getTime()).reduce((s, q) => s + q.quantityOnHand * (q.unitCost ?? 0), 0);
|
|
102
|
+
return {
|
|
103
|
+
turnoverRate: 0,
|
|
104
|
+
daysOfInventory: 0,
|
|
105
|
+
stockoutRate: 0,
|
|
106
|
+
fillRate: 0,
|
|
107
|
+
deadStockPercentage: totalValue > 0 ? Math.round(deadStockValue / totalValue * 1e4) / 100 : 0,
|
|
108
|
+
expiryRiskValue: Math.round(expiryRiskValue * 100) / 100,
|
|
109
|
+
totalSkus: uniqueSkus.size,
|
|
110
|
+
totalOnHand,
|
|
111
|
+
totalValue: Math.round(totalValue * 100) / 100
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/reporting/stock-aging.ts
|
|
117
|
+
const BUCKET_RANGES = [
|
|
118
|
+
{
|
|
119
|
+
label: "0-30 days",
|
|
120
|
+
minDays: 0,
|
|
121
|
+
maxDays: 30
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: "31-60 days",
|
|
125
|
+
minDays: 31,
|
|
126
|
+
maxDays: 60
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: "61-90 days",
|
|
130
|
+
minDays: 61,
|
|
131
|
+
maxDays: 90
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
label: "91-180 days",
|
|
135
|
+
minDays: 91,
|
|
136
|
+
maxDays: 180
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
label: "181-365 days",
|
|
140
|
+
minDays: 181,
|
|
141
|
+
maxDays: 365
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
label: "365+ days",
|
|
145
|
+
minDays: 366,
|
|
146
|
+
maxDays: Infinity
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
var StockAgingReport = class {
|
|
150
|
+
constructor(quantPort) {
|
|
151
|
+
this.quantPort = quantPort;
|
|
152
|
+
}
|
|
153
|
+
async generate(ctx, nodeId) {
|
|
154
|
+
assertTenantContext(ctx);
|
|
155
|
+
const now = /* @__PURE__ */ new Date();
|
|
156
|
+
const quants = await this.quantPort.findMany({
|
|
157
|
+
organizationId: ctx.organizationId,
|
|
158
|
+
...nodeId ? { nodeId } : {},
|
|
159
|
+
quantityOnHand: { $gt: 0 }
|
|
160
|
+
}, ctx);
|
|
161
|
+
const buckets = BUCKET_RANGES.map((r) => ({
|
|
162
|
+
...r,
|
|
163
|
+
quantity: 0,
|
|
164
|
+
value: 0,
|
|
165
|
+
skuCount: 0
|
|
166
|
+
}));
|
|
167
|
+
const slowMoving = [];
|
|
168
|
+
const deadStock = [];
|
|
169
|
+
for (const q of quants) {
|
|
170
|
+
const lastMove = q.lastMovementAt ?? q.inDate;
|
|
171
|
+
const ageDays = Math.floor((now.getTime() - new Date(lastMove).getTime()) / 864e5);
|
|
172
|
+
const value = q.quantityOnHand * (q.unitCost ?? 0);
|
|
173
|
+
const bucket = buckets.find((b) => ageDays >= b.minDays && ageDays <= b.maxDays);
|
|
174
|
+
if (bucket) {
|
|
175
|
+
bucket.quantity += q.quantityOnHand;
|
|
176
|
+
bucket.value += value;
|
|
177
|
+
bucket.skuCount++;
|
|
178
|
+
}
|
|
179
|
+
if (ageDays > 90) slowMoving.push({
|
|
180
|
+
skuRef: q.skuRef,
|
|
181
|
+
locationId: q.locationId,
|
|
182
|
+
quantity: q.quantityOnHand,
|
|
183
|
+
ageDays
|
|
184
|
+
});
|
|
185
|
+
if (ageDays > 365) deadStock.push({
|
|
186
|
+
skuRef: q.skuRef,
|
|
187
|
+
locationId: q.locationId,
|
|
188
|
+
quantity: q.quantityOnHand,
|
|
189
|
+
ageDays
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
asOfDate: now,
|
|
194
|
+
buckets,
|
|
195
|
+
slowMoving,
|
|
196
|
+
deadStock
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/reporting/turnover.ts
|
|
202
|
+
var TurnoverReport = class {
|
|
203
|
+
constructor(quantPort, movePort) {
|
|
204
|
+
this.quantPort = quantPort;
|
|
205
|
+
this.movePort = movePort;
|
|
206
|
+
}
|
|
207
|
+
async generate(periodDays, ctx) {
|
|
208
|
+
assertTenantContext(ctx);
|
|
209
|
+
const end = /* @__PURE__ */ new Date();
|
|
210
|
+
const start = /* @__PURE__ */ new Date(end.getTime() - periodDays * 864e5);
|
|
211
|
+
const totalCOGS = (await this.movePort.findMany({
|
|
212
|
+
organizationId: ctx.organizationId,
|
|
213
|
+
operationType: "shipment",
|
|
214
|
+
status: "done",
|
|
215
|
+
executedAt: {
|
|
216
|
+
$gte: start,
|
|
217
|
+
$lte: end
|
|
218
|
+
}
|
|
219
|
+
}, ctx)).reduce((sum, m) => {
|
|
220
|
+
const cost = m.metadata?.unitCost ?? 0;
|
|
221
|
+
return sum + (m.quantityDone ?? 0) * cost;
|
|
222
|
+
}, 0);
|
|
223
|
+
const averageInventoryValue = (await this.quantPort.findMany({
|
|
224
|
+
organizationId: ctx.organizationId,
|
|
225
|
+
quantityOnHand: { $gt: 0 }
|
|
226
|
+
}, ctx)).reduce((sum, q) => sum + q.quantityOnHand * (q.unitCost ?? 0), 0);
|
|
227
|
+
const turnoverRate = averageInventoryValue > 0 ? totalCOGS / averageInventoryValue : 0;
|
|
228
|
+
const daysOfInventory = turnoverRate > 0 ? 365 / turnoverRate : Infinity;
|
|
229
|
+
return {
|
|
230
|
+
period: {
|
|
231
|
+
start,
|
|
232
|
+
end
|
|
233
|
+
},
|
|
234
|
+
totalCOGS,
|
|
235
|
+
averageInventoryValue,
|
|
236
|
+
turnoverRate: Math.round(turnoverRate * 100) / 100,
|
|
237
|
+
daysOfInventory: Math.round(daysOfInventory),
|
|
238
|
+
skuBreakdown: []
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
//#endregion
|
|
243
|
+
export { AvailabilityReport as i, StockAgingReport as n, HealthMetricsReport as r, TurnoverReport as t };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { _ as CountRepository, a as extractDocs, c as ReservationRepository, d as ProcurementRepository, f as NodeRepository, g as LocationRepository, h as LotRepository, i as asOffsetResult, l as ReplenishmentRuleRepository, m as MoveRepository, n as RepositoryPlugins, o as toOrgId, p as MoveGroupRepository, r as createRepositories, s as toSession, t as FlowRepositories, u as QuantRepository, v as CostLayerRepository } from "../index-C5PciI9P.mjs";
|
|
2
|
+
export { CostLayerRepository, CountRepository, FlowRepositories, LocationRepository, LotRepository, MoveGroupRepository, MoveRepository, NodeRepository, ProcurementRepository, QuantRepository, ReplenishmentRuleRepository, RepositoryPlugins, ReservationRepository, asOffsetResult, createRepositories, extractDocs, toOrgId, toSession };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as ProcurementRepository, c as MoveRepository, d as CountRepository, f as CostLayerRepository, g as toSession, h as toOrgId, i as QuantRepository, l as LotRepository, m as extractDocs, n as ReservationRepository, o as NodeRepository, p as asOffsetResult, r as ReplenishmentRuleRepository, s as MoveGroupRepository, t as createRepositories, u as LocationRepository } from "../repositories-nZXJKvLW.mjs";
|
|
2
|
+
export { CostLayerRepository, CountRepository, LocationRepository, LotRepository, MoveGroupRepository, MoveRepository, NodeRepository, ProcurementRepository, QuantRepository, ReplenishmentRuleRepository, ReservationRepository, asOffsetResult, createRepositories, extractDocs, toOrgId, toSession };
|