@open-mercato/core 0.7.1-develop.7170.1.d95074d7ba → 0.7.1-develop.7171.1.9b31dbba45
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/wms/commands/sales-order-assignment.js +12 -0
- package/dist/modules/wms/commands/sales-order-assignment.js.map +2 -2
- package/dist/modules/wms/data/enrichers.js +25 -0
- package/dist/modules/wms/data/enrichers.js.map +2 -2
- package/dist/modules/wms/lib/enricherCacheTags.js +9 -0
- package/dist/modules/wms/lib/enricherCacheTags.js.map +7 -0
- package/dist/modules/wms/lib/invalidateInventoryEnricherCache.js +37 -0
- package/dist/modules/wms/lib/invalidateInventoryEnricherCache.js.map +7 -0
- package/dist/modules/wms/lib/inventoryEnricherCacheSubscriber.js +14 -0
- package/dist/modules/wms/lib/inventoryEnricherCacheSubscriber.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-balance.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-balance.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-inventory.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-inventory.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-movement.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-movement.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-profile.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-profile.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-reservation.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-reservation.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-line.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-line.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-order.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-sales-order.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-variant.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-variant.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-warehouse.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-warehouse.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/wms/commands/sales-order-assignment.ts +20 -0
- package/src/modules/wms/data/enrichers.ts +25 -0
- package/src/modules/wms/lib/enricherCacheTags.ts +29 -0
- package/src/modules/wms/lib/invalidateInventoryEnricherCache.ts +71 -0
- package/src/modules/wms/lib/inventoryEnricherCacheSubscriber.ts +30 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-balance.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-inventory.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-movement.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-profile.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-reservation.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-sales-line.ts +14 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-sales-order.ts +18 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-variant.ts +9 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-warehouse.ts +9 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -6,7 +6,11 @@ import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
|
|
|
6
6
|
import { SalesOrderWarehouseAssignment, Warehouse } from "../data/entities.js";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { reserveInventoryForConfirmedOrder } from "../lib/salesOrderInventoryAutomation.js";
|
|
9
|
+
import { invalidateWmsInventoryEnricherCache } from "../lib/invalidateInventoryEnricherCache.js";
|
|
9
10
|
import { ensureOrganizationScope, ensureTenantScope } from "./shared.js";
|
|
11
|
+
async function invalidateAssignmentEnricherCache(ctx, tenantId) {
|
|
12
|
+
await invalidateWmsInventoryEnricherCache(ctx.container, tenantId, "warehouse");
|
|
13
|
+
}
|
|
10
14
|
function resolveScope(ctx, fallback) {
|
|
11
15
|
return {
|
|
12
16
|
tenantId: fallback?.tenantId ?? ctx.auth?.tenantId ?? null,
|
|
@@ -81,6 +85,7 @@ const assignWarehouseHandler = {
|
|
|
81
85
|
existing.assignedBy = ctx.auth?.sub ?? null;
|
|
82
86
|
existing.updatedAt = /* @__PURE__ */ new Date();
|
|
83
87
|
await em.flush();
|
|
88
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId);
|
|
84
89
|
return { assignmentId: existing.id, warehouseId: warehouse.id };
|
|
85
90
|
}
|
|
86
91
|
const assignment = em.create(SalesOrderWarehouseAssignment, {
|
|
@@ -93,6 +98,7 @@ const assignWarehouseHandler = {
|
|
|
93
98
|
});
|
|
94
99
|
em.persist(assignment);
|
|
95
100
|
await em.flush();
|
|
101
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId);
|
|
96
102
|
return { assignmentId: assignment.id, warehouseId: warehouse.id };
|
|
97
103
|
},
|
|
98
104
|
captureAfter: async (input, _result, ctx) => {
|
|
@@ -136,6 +142,7 @@ const assignWarehouseHandler = {
|
|
|
136
142
|
if (!before) {
|
|
137
143
|
assignment.deletedAt = /* @__PURE__ */ new Date();
|
|
138
144
|
await em.flush();
|
|
145
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId);
|
|
139
146
|
return;
|
|
140
147
|
}
|
|
141
148
|
const previousWarehouse = await findOneWithDecryption(
|
|
@@ -153,11 +160,13 @@ const assignWarehouseHandler = {
|
|
|
153
160
|
if (!previousWarehouse) {
|
|
154
161
|
assignment.deletedAt = /* @__PURE__ */ new Date();
|
|
155
162
|
await em.flush();
|
|
163
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId);
|
|
156
164
|
return;
|
|
157
165
|
}
|
|
158
166
|
assignment.warehouse = previousWarehouse;
|
|
159
167
|
assignment.notes = before.notes ?? null;
|
|
160
168
|
await em.flush();
|
|
169
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId);
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
};
|
|
@@ -179,6 +188,7 @@ const unassignWarehouseHandler = {
|
|
|
179
188
|
if (existing) {
|
|
180
189
|
existing.deletedAt = /* @__PURE__ */ new Date();
|
|
181
190
|
await em.flush();
|
|
191
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId);
|
|
182
192
|
}
|
|
183
193
|
return { ok: true };
|
|
184
194
|
},
|
|
@@ -212,6 +222,7 @@ const unassignWarehouseHandler = {
|
|
|
212
222
|
if (existingDeleted) {
|
|
213
223
|
existingDeleted.deletedAt = null;
|
|
214
224
|
await em.flush();
|
|
225
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId);
|
|
215
226
|
return;
|
|
216
227
|
}
|
|
217
228
|
const warehouse = await findOneWithDecryption(
|
|
@@ -236,6 +247,7 @@ const unassignWarehouseHandler = {
|
|
|
236
247
|
});
|
|
237
248
|
em.persist(restored);
|
|
238
249
|
await em.flush();
|
|
250
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId);
|
|
239
251
|
}
|
|
240
252
|
};
|
|
241
253
|
registerCommand(assignWarehouseHandler);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/wms/commands/sales-order-assignment.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { SalesOrderWarehouseAssignment, Warehouse } from '../data/entities'\nimport type { SalesOrderWarehouseAssignInput, SalesOrderWarehouseUnassignInput } from '../data/validators'\nimport { z } from 'zod'\nimport { reserveInventoryForConfirmedOrder } from '../lib/salesOrderInventoryAutomation'\nimport { ensureOrganizationScope, ensureTenantScope } from './shared'\n\ntype AssignmentSnapshot = {\n id: string\n salesOrderId: string\n warehouseId: string\n notes: string | null\n organizationId: string\n tenantId: string\n}\n\ntype AssignWarehouseUndoPayload = {\n before: AssignmentSnapshot | null\n after: AssignmentSnapshot | null\n}\n\ntype UnassignWarehouseUndoPayload = {\n before: AssignmentSnapshot | null\n}\n\nfunction resolveScope(\n ctx: CommandRuntimeContext,\n fallback?: { tenantId?: string | null; organizationId?: string | null },\n) {\n return {\n tenantId: fallback?.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: fallback?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n }\n}\n\nfunction resolveEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nasync function loadAssignment(\n em: EntityManager,\n salesOrderId: string,\n scope: ReturnType<typeof resolveScope>,\n): Promise<SalesOrderWarehouseAssignment | null> {\n if (!scope.organizationId || !scope.tenantId) return null\n return findOneWithDecryption(\n em,\n SalesOrderWarehouseAssignment,\n {\n salesOrderId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope as { organizationId: string; tenantId: string },\n )\n}\n\nfunction snapshotAssignment(\n assignment: SalesOrderWarehouseAssignment | null,\n): AssignmentSnapshot | null {\n if (!assignment) return null\n const warehouseRel = assignment.warehouse as { id?: string } | undefined\n const warehouseId = warehouseRel?.id ?? ''\n return {\n id: assignment.id,\n salesOrderId: assignment.salesOrderId,\n warehouseId,\n notes: assignment.notes ?? null,\n organizationId: assignment.organizationId,\n tenantId: assignment.tenantId,\n }\n}\n\nconst assignWarehouseHandler: CommandHandler<\n SalesOrderWarehouseAssignInput,\n { assignmentId: string; warehouseId: string }\n> = {\n id: 'wms.sales-order.assign-warehouse',\n\n prepare: async (input, ctx) => {\n const scope = resolveScope(ctx, input)\n const em = resolveEm(ctx)\n const existing = await loadAssignment(em, input.salesOrderId, scope)\n return { before: snapshotAssignment(existing) }\n },\n\n execute: async (input, ctx) => {\n const scope = resolveScope(ctx, input)\n if (!scope.tenantId || !scope.organizationId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n const em = resolveEm(ctx)\n\n const warehouse = await findOneWithDecryption(\n em,\n Warehouse,\n {\n id: input.warehouseId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope as { organizationId: string; tenantId: string },\n )\n if (!warehouse) throw new CrudHttpError(404, { error: 'Warehouse not found.' })\n ensureTenantScope(ctx, warehouse.tenantId)\n ensureOrganizationScope(ctx, warehouse.organizationId)\n if (!warehouse.isActive) throw new CrudHttpError(422, { error: 'Warehouse is inactive.' })\n\n const existing = await loadAssignment(em, input.salesOrderId, scope)\n\n if (existing) {\n existing.warehouse = warehouse\n existing.notes = input.notes ?? null\n existing.assignedBy = ctx.auth?.sub ?? null\n existing.updatedAt = new Date()\n await em.flush()\n return { assignmentId: existing.id, warehouseId: warehouse.id }\n }\n\n const assignment = em.create(SalesOrderWarehouseAssignment, {\n salesOrderId: input.salesOrderId,\n warehouse,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n assignedBy: ctx.auth?.sub ?? null,\n notes: input.notes ?? null,\n })\n em.persist(assignment)\n await em.flush()\n return { assignmentId: assignment.id, warehouseId: warehouse.id }\n },\n\n captureAfter: async (input, _result, ctx) => {\n const scope = resolveScope(ctx, input)\n const em = resolveEm(ctx)\n const updated = await loadAssignment(em, input.salesOrderId, scope)\n return { after: snapshotAssignment(updated) }\n },\n\n buildLog: async ({ input, result, ctx, snapshots }) => {\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('wms.audit.salesOrder.assignWarehouse', 'Assign warehouse to order'),\n resourceKind: 'wms.sales_order_warehouse_assignment',\n resourceId: result?.assignmentId ?? null,\n tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n payload: {\n undo: {\n before: (snapshots as { before?: AssignmentSnapshot | null })?.before ?? null,\n after: (snapshots as { after?: AssignmentSnapshot | null })?.after ?? null,\n } satisfies AssignWarehouseUndoPayload,\n },\n }\n },\n\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignWarehouseUndoPayload>(logEntry)\n if (!payload) return\n const em = resolveEm(ctx)\n\n const after = payload.after\n const before = payload.before\n\n if (after) {\n const scope = { tenantId: after.tenantId, organizationId: after.organizationId }\n const assignment = await findOneWithDecryption(\n em,\n SalesOrderWarehouseAssignment,\n { id: after.id, organizationId: after.organizationId, tenantId: after.tenantId },\n undefined,\n scope,\n )\n if (!assignment) return\n\n if (!before) {\n assignment.deletedAt = new Date()\n await em.flush()\n return\n }\n\n const previousWarehouse = await findOneWithDecryption(\n em,\n Warehouse,\n {\n id: before.warehouseId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!previousWarehouse) {\n assignment.deletedAt = new Date()\n await em.flush()\n return\n }\n\n assignment.warehouse = previousWarehouse\n assignment.notes = before.notes ?? null\n await em.flush()\n }\n },\n}\n\nconst unassignWarehouseHandler: CommandHandler<\n SalesOrderWarehouseUnassignInput,\n { ok: true }\n> = {\n id: 'wms.sales-order.unassign-warehouse',\n\n prepare: async (input, ctx) => {\n const scope = resolveScope(ctx, input)\n const em = resolveEm(ctx)\n const existing = await loadAssignment(em, input.salesOrderId, scope)\n return { before: snapshotAssignment(existing) }\n },\n\n execute: async (input, ctx) => {\n const scope = resolveScope(ctx, input)\n if (!scope.tenantId || !scope.organizationId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n const em = resolveEm(ctx)\n const existing = await loadAssignment(em, input.salesOrderId, scope)\n\n if (existing) {\n existing.deletedAt = new Date()\n await em.flush()\n }\n\n return { ok: true as const }\n },\n\n buildLog: async ({ input, ctx, snapshots }) => {\n const { translate } = await resolveTranslations()\n const before = (snapshots as { before?: AssignmentSnapshot | null })?.before ?? null\n return {\n actionLabel: translate('wms.audit.salesOrder.unassignWarehouse', 'Remove warehouse assignment from order'),\n resourceKind: 'wms.sales_order_warehouse_assignment',\n resourceId: before?.id ?? null,\n tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n payload: {\n undo: { before } satisfies UnassignWarehouseUndoPayload,\n },\n }\n },\n\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<UnassignWarehouseUndoPayload>(logEntry)\n const before = payload?.before\n if (!before?.salesOrderId || !before.warehouseId) return\n\n const scope = { tenantId: before.tenantId, organizationId: before.organizationId }\n const em = resolveEm(ctx)\n\n const existingDeleted = await findOneWithDecryption(\n em,\n SalesOrderWarehouseAssignment,\n { id: before.id, organizationId: before.organizationId, tenantId: before.tenantId },\n undefined,\n scope,\n )\n\n if (existingDeleted) {\n existingDeleted.deletedAt = null\n await em.flush()\n return\n }\n\n const warehouse = await findOneWithDecryption(\n em,\n Warehouse,\n {\n id: before.warehouseId,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!warehouse) return\n\n const restored = em.create(SalesOrderWarehouseAssignment, {\n salesOrderId: before.salesOrderId,\n warehouse,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n notes: before.notes ?? null,\n })\n em.persist(restored)\n await em.flush()\n },\n}\n\nregisterCommand(assignWarehouseHandler)\nregisterCommand(unassignWarehouseHandler)\n\nexport const reRunReservationInputSchema = z.object({\n salesOrderId: z.string().uuid(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n})\nexport type ReRunReservationInput = z.infer<typeof reRunReservationInputSchema>\n\nconst reRunReservationHandler: CommandHandler<ReRunReservationInput, { ok: boolean }> = {\n id: 'wms.sales-order.re-run-reservation',\n isUndoable: false,\n async execute(rawInput, ctx) {\n const input = reRunReservationInputSchema.parse(rawInput)\n const eventCtx = {\n resolve: <T>(name: string) => ctx.container.resolve<T>(name),\n }\n await reserveInventoryForConfirmedOrder(\n { orderId: input.salesOrderId, tenantId: input.tenantId, organizationId: input.organizationId },\n eventCtx,\n )\n return { ok: true }\n },\n async buildLog({ input, ctx }) {\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('wms.audit.salesOrder.reRunReservation', 'Re-run reservation for order'),\n resourceKind: 'wms.sales_order_reservation',\n resourceId: input?.salesOrderId ?? null,\n tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n }\n },\n}\n\nregisterCommand(reRunReservationHandler)\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,uBAAuB;AAGhC,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC,SAAS,6BAA6B;AACtC,SAAS,0BAA0B;AACnC,SAAS,+BAA+B,iBAAiB;AAEzD,SAAS,SAAS;AAClB,SAAS,yCAAyC;AAClD,SAAS,yBAAyB,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { SalesOrderWarehouseAssignment, Warehouse } from '../data/entities'\nimport type { SalesOrderWarehouseAssignInput, SalesOrderWarehouseUnassignInput } from '../data/validators'\nimport { z } from 'zod'\nimport { reserveInventoryForConfirmedOrder } from '../lib/salesOrderInventoryAutomation'\nimport { invalidateWmsInventoryEnricherCache } from '../lib/invalidateInventoryEnricherCache'\nimport { ensureOrganizationScope, ensureTenantScope } from './shared'\n\ntype AssignmentSnapshot = {\n id: string\n salesOrderId: string\n warehouseId: string\n notes: string | null\n organizationId: string\n tenantId: string\n}\n\ntype AssignWarehouseUndoPayload = {\n before: AssignmentSnapshot | null\n after: AssignmentSnapshot | null\n}\n\ntype UnassignWarehouseUndoPayload = {\n before: AssignmentSnapshot | null\n}\n\n// These commands emit no WMS event, so the enricher-cache subscribers never see\n// them. `wms.sales-order-inventory` surfaces the assigned warehouse, so every\n// path here that writes an assignment \u2014 including the undo handlers \u2014 drops the\n// warehouse tag itself.\nasync function invalidateAssignmentEnricherCache(\n ctx: CommandRuntimeContext,\n tenantId: string | null | undefined,\n): Promise<void> {\n await invalidateWmsInventoryEnricherCache(ctx.container, tenantId, 'warehouse')\n}\n\nfunction resolveScope(\n ctx: CommandRuntimeContext,\n fallback?: { tenantId?: string | null; organizationId?: string | null },\n) {\n return {\n tenantId: fallback?.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: fallback?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n }\n}\n\nfunction resolveEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\nasync function loadAssignment(\n em: EntityManager,\n salesOrderId: string,\n scope: ReturnType<typeof resolveScope>,\n): Promise<SalesOrderWarehouseAssignment | null> {\n if (!scope.organizationId || !scope.tenantId) return null\n return findOneWithDecryption(\n em,\n SalesOrderWarehouseAssignment,\n {\n salesOrderId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope as { organizationId: string; tenantId: string },\n )\n}\n\nfunction snapshotAssignment(\n assignment: SalesOrderWarehouseAssignment | null,\n): AssignmentSnapshot | null {\n if (!assignment) return null\n const warehouseRel = assignment.warehouse as { id?: string } | undefined\n const warehouseId = warehouseRel?.id ?? ''\n return {\n id: assignment.id,\n salesOrderId: assignment.salesOrderId,\n warehouseId,\n notes: assignment.notes ?? null,\n organizationId: assignment.organizationId,\n tenantId: assignment.tenantId,\n }\n}\n\nconst assignWarehouseHandler: CommandHandler<\n SalesOrderWarehouseAssignInput,\n { assignmentId: string; warehouseId: string }\n> = {\n id: 'wms.sales-order.assign-warehouse',\n\n prepare: async (input, ctx) => {\n const scope = resolveScope(ctx, input)\n const em = resolveEm(ctx)\n const existing = await loadAssignment(em, input.salesOrderId, scope)\n return { before: snapshotAssignment(existing) }\n },\n\n execute: async (input, ctx) => {\n const scope = resolveScope(ctx, input)\n if (!scope.tenantId || !scope.organizationId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n const em = resolveEm(ctx)\n\n const warehouse = await findOneWithDecryption(\n em,\n Warehouse,\n {\n id: input.warehouseId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope as { organizationId: string; tenantId: string },\n )\n if (!warehouse) throw new CrudHttpError(404, { error: 'Warehouse not found.' })\n ensureTenantScope(ctx, warehouse.tenantId)\n ensureOrganizationScope(ctx, warehouse.organizationId)\n if (!warehouse.isActive) throw new CrudHttpError(422, { error: 'Warehouse is inactive.' })\n\n const existing = await loadAssignment(em, input.salesOrderId, scope)\n\n if (existing) {\n existing.warehouse = warehouse\n existing.notes = input.notes ?? null\n existing.assignedBy = ctx.auth?.sub ?? null\n existing.updatedAt = new Date()\n await em.flush()\n await invalidateAssignmentEnricherCache(ctx, scope.tenantId)\n return { assignmentId: existing.id, warehouseId: warehouse.id }\n }\n\n const assignment = em.create(SalesOrderWarehouseAssignment, {\n salesOrderId: input.salesOrderId,\n warehouse,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n assignedBy: ctx.auth?.sub ?? null,\n notes: input.notes ?? null,\n })\n em.persist(assignment)\n await em.flush()\n await invalidateAssignmentEnricherCache(ctx, scope.tenantId)\n return { assignmentId: assignment.id, warehouseId: warehouse.id }\n },\n\n captureAfter: async (input, _result, ctx) => {\n const scope = resolveScope(ctx, input)\n const em = resolveEm(ctx)\n const updated = await loadAssignment(em, input.salesOrderId, scope)\n return { after: snapshotAssignment(updated) }\n },\n\n buildLog: async ({ input, result, ctx, snapshots }) => {\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('wms.audit.salesOrder.assignWarehouse', 'Assign warehouse to order'),\n resourceKind: 'wms.sales_order_warehouse_assignment',\n resourceId: result?.assignmentId ?? null,\n tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n payload: {\n undo: {\n before: (snapshots as { before?: AssignmentSnapshot | null })?.before ?? null,\n after: (snapshots as { after?: AssignmentSnapshot | null })?.after ?? null,\n } satisfies AssignWarehouseUndoPayload,\n },\n }\n },\n\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<AssignWarehouseUndoPayload>(logEntry)\n if (!payload) return\n const em = resolveEm(ctx)\n\n const after = payload.after\n const before = payload.before\n\n if (after) {\n const scope = { tenantId: after.tenantId, organizationId: after.organizationId }\n const assignment = await findOneWithDecryption(\n em,\n SalesOrderWarehouseAssignment,\n { id: after.id, organizationId: after.organizationId, tenantId: after.tenantId },\n undefined,\n scope,\n )\n if (!assignment) return\n\n if (!before) {\n assignment.deletedAt = new Date()\n await em.flush()\n await invalidateAssignmentEnricherCache(ctx, scope.tenantId)\n return\n }\n\n const previousWarehouse = await findOneWithDecryption(\n em,\n Warehouse,\n {\n id: before.warehouseId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!previousWarehouse) {\n assignment.deletedAt = new Date()\n await em.flush()\n await invalidateAssignmentEnricherCache(ctx, scope.tenantId)\n return\n }\n\n assignment.warehouse = previousWarehouse\n assignment.notes = before.notes ?? null\n await em.flush()\n await invalidateAssignmentEnricherCache(ctx, scope.tenantId)\n }\n },\n}\n\nconst unassignWarehouseHandler: CommandHandler<\n SalesOrderWarehouseUnassignInput,\n { ok: true }\n> = {\n id: 'wms.sales-order.unassign-warehouse',\n\n prepare: async (input, ctx) => {\n const scope = resolveScope(ctx, input)\n const em = resolveEm(ctx)\n const existing = await loadAssignment(em, input.salesOrderId, scope)\n return { before: snapshotAssignment(existing) }\n },\n\n execute: async (input, ctx) => {\n const scope = resolveScope(ctx, input)\n if (!scope.tenantId || !scope.organizationId) {\n throw new CrudHttpError(401, { error: 'Unauthorized' })\n }\n const em = resolveEm(ctx)\n const existing = await loadAssignment(em, input.salesOrderId, scope)\n\n if (existing) {\n existing.deletedAt = new Date()\n await em.flush()\n await invalidateAssignmentEnricherCache(ctx, scope.tenantId)\n }\n\n return { ok: true as const }\n },\n\n buildLog: async ({ input, ctx, snapshots }) => {\n const { translate } = await resolveTranslations()\n const before = (snapshots as { before?: AssignmentSnapshot | null })?.before ?? null\n return {\n actionLabel: translate('wms.audit.salesOrder.unassignWarehouse', 'Remove warehouse assignment from order'),\n resourceKind: 'wms.sales_order_warehouse_assignment',\n resourceId: before?.id ?? null,\n tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n payload: {\n undo: { before } satisfies UnassignWarehouseUndoPayload,\n },\n }\n },\n\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<UnassignWarehouseUndoPayload>(logEntry)\n const before = payload?.before\n if (!before?.salesOrderId || !before.warehouseId) return\n\n const scope = { tenantId: before.tenantId, organizationId: before.organizationId }\n const em = resolveEm(ctx)\n\n const existingDeleted = await findOneWithDecryption(\n em,\n SalesOrderWarehouseAssignment,\n { id: before.id, organizationId: before.organizationId, tenantId: before.tenantId },\n undefined,\n scope,\n )\n\n if (existingDeleted) {\n existingDeleted.deletedAt = null\n await em.flush()\n await invalidateAssignmentEnricherCache(ctx, scope.tenantId)\n return\n }\n\n const warehouse = await findOneWithDecryption(\n em,\n Warehouse,\n {\n id: before.warehouseId,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!warehouse) return\n\n const restored = em.create(SalesOrderWarehouseAssignment, {\n salesOrderId: before.salesOrderId,\n warehouse,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n notes: before.notes ?? null,\n })\n em.persist(restored)\n await em.flush()\n await invalidateAssignmentEnricherCache(ctx, scope.tenantId)\n },\n}\n\nregisterCommand(assignWarehouseHandler)\nregisterCommand(unassignWarehouseHandler)\n\nexport const reRunReservationInputSchema = z.object({\n salesOrderId: z.string().uuid(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n})\nexport type ReRunReservationInput = z.infer<typeof reRunReservationInputSchema>\n\nconst reRunReservationHandler: CommandHandler<ReRunReservationInput, { ok: boolean }> = {\n id: 'wms.sales-order.re-run-reservation',\n isUndoable: false,\n async execute(rawInput, ctx) {\n const input = reRunReservationInputSchema.parse(rawInput)\n const eventCtx = {\n resolve: <T>(name: string) => ctx.container.resolve<T>(name),\n }\n await reserveInventoryForConfirmedOrder(\n { orderId: input.salesOrderId, tenantId: input.tenantId, organizationId: input.organizationId },\n eventCtx,\n )\n return { ok: true }\n },\n async buildLog({ input, ctx }) {\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('wms.audit.salesOrder.reRunReservation', 'Re-run reservation for order'),\n resourceKind: 'wms.sales_order_reservation',\n resourceId: input?.salesOrderId ?? null,\n tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n }\n },\n}\n\nregisterCommand(reRunReservationHandler)\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,11 @@ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
|
2
2
|
import { E } from "../../../generated/entities.ids.generated.js";
|
|
3
3
|
import { InventoryBalance, InventoryReservation, ProductInventoryProfile, SalesOrderWarehouseAssignment, Warehouse } from "./entities.js";
|
|
4
4
|
import { resolvePrimaryWarehouseId } from "../lib/primaryWarehousePolicy.js";
|
|
5
|
+
import {
|
|
6
|
+
WMS_ENRICHER_CACHE_TTL_MS,
|
|
7
|
+
WMS_INVENTORY_CACHE_TAG,
|
|
8
|
+
WMS_WAREHOUSE_CACHE_TAG
|
|
9
|
+
} from "../lib/enricherCacheTags.js";
|
|
5
10
|
import { resolveWmsIntegrationToggleEnabled } from "../lib/wmsIntegrationToggles.js";
|
|
6
11
|
const EMPTY_ENRICHMENT = {
|
|
7
12
|
_wms: {
|
|
@@ -262,6 +267,13 @@ const salesOrderInventoryEnricher = {
|
|
|
262
267
|
priority: 40,
|
|
263
268
|
timeout: 2e3,
|
|
264
269
|
fallback: EMPTY_ENRICHMENT,
|
|
270
|
+
// Reads reservations, balances, the primary warehouse and the explicit
|
|
271
|
+
// assignment, so it depends on both the inventory and the warehouse tags.
|
|
272
|
+
cache: {
|
|
273
|
+
strategy: "read-through",
|
|
274
|
+
ttl: WMS_ENRICHER_CACHE_TTL_MS,
|
|
275
|
+
tags: [WMS_INVENTORY_CACHE_TAG, WMS_WAREHOUSE_CACHE_TAG]
|
|
276
|
+
},
|
|
265
277
|
async enrichOne(record, context) {
|
|
266
278
|
return (await this.enrichMany([record], context))[0];
|
|
267
279
|
},
|
|
@@ -393,6 +405,13 @@ const catalogProductInventoryEnricher = {
|
|
|
393
405
|
priority: 40,
|
|
394
406
|
timeout: 2e3,
|
|
395
407
|
fallback: EMPTY_CATALOG_ENRICHMENT,
|
|
408
|
+
// Reads inventory profiles and balances; also reads catalog variants, so the
|
|
409
|
+
// catalog variant events invalidate the inventory tag too.
|
|
410
|
+
cache: {
|
|
411
|
+
strategy: "read-through",
|
|
412
|
+
ttl: WMS_ENRICHER_CACHE_TTL_MS,
|
|
413
|
+
tags: [WMS_INVENTORY_CACHE_TAG]
|
|
414
|
+
},
|
|
396
415
|
async enrichOne(record, context) {
|
|
397
416
|
return (await this.enrichMany([record], context))[0];
|
|
398
417
|
},
|
|
@@ -453,6 +472,12 @@ const catalogVariantInventoryEnricher = {
|
|
|
453
472
|
priority: 40,
|
|
454
473
|
timeout: 2e3,
|
|
455
474
|
fallback: EMPTY_CATALOG_ENRICHMENT,
|
|
475
|
+
// Reads inventory profiles and balances for the variant only.
|
|
476
|
+
cache: {
|
|
477
|
+
strategy: "read-through",
|
|
478
|
+
ttl: WMS_ENRICHER_CACHE_TTL_MS,
|
|
479
|
+
tags: [WMS_INVENTORY_CACHE_TAG]
|
|
480
|
+
},
|
|
456
481
|
async enrichOne(record, context) {
|
|
457
482
|
return (await this.enrichMany([record], context))[0];
|
|
458
483
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/wms/data/enrichers.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { FeatureTogglesService } from '@open-mercato/core/modules/feature_toggles/lib/feature-flag-check'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { E } from '#generated/entities.ids.generated'\nimport { InventoryBalance, InventoryReservation, ProductInventoryProfile, SalesOrderWarehouseAssignment, Warehouse } from './entities'\nimport { resolvePrimaryWarehouseId } from '../lib/primaryWarehousePolicy'\n\ntype SalesOrderRecord = Record<string, unknown> & { id?: string }\ntype CatalogProductRecord = Record<string, unknown> & { id?: string }\ntype CatalogVariantRecord = Record<string, unknown> & { id?: string; product_id?: string; productId?: string }\n\ntype SalesOrderLineRow = {\n id?: string\n order_id?: string | null\n product_variant_id?: string | null\n quantity?: string | number | null\n line_number?: number | null\n status?: string | null\n}\n\ntype CatalogVariantRow = {\n id?: string\n product_id?: string | null\n}\ntype ReservationStatus = 'unreserved' | 'partially_reserved' | 'fully_reserved'\ntype ReorderState = 'no_profile' | 'healthy' | 'below_reorder_point' | 'below_safety_stock'\n\ntype InventoryProfileSummary = {\n profileId: string\n catalogProductId: string\n catalogVariantId: string | null\n defaultUom: string\n defaultStrategy: string\n trackLot: boolean\n trackSerial: boolean\n trackExpiration: boolean\n reorderPoint: string\n safetyStock: string\n}\n\ntype StockSummaryItem = {\n catalogVariantId: string\n onHand: string\n reserved: string\n allocated: string\n available: string\n}\n\ntype ReorderStatus = {\n state: ReorderState\n available: string\n reorderPoint: string\n safetyStock: string\n}\n\ntype SalesOrderWmsEnrichment = {\n _wms: {\n assignedWarehouseId: string | null\n assignedWarehouseName: string | null\n isExplicitlyAssigned: boolean\n stockSummary: Array<{\n catalogVariantId: string\n available: string\n reserved: string\n }>\n reservationSummary: {\n status: ReservationStatus\n reservationIds: string[]\n }\n }\n}\n\ntype CatalogWmsEnrichment = {\n _wms: {\n inventoryProfile: InventoryProfileSummary | null\n stockSummary: StockSummaryItem[]\n reorderStatus: ReorderStatus\n }\n}\n\ntype EnricherScope = EnricherContext & { em: EntityManager }\ntype Scope = { organizationId: string; tenantId: string }\ntype BalanceAggregate = { onHand: number; reserved: number; allocated: number; available: number }\n\nimport { resolveWmsIntegrationToggleEnabled } from '../lib/wmsIntegrationToggles'\nconst EMPTY_ENRICHMENT: SalesOrderWmsEnrichment = {\n _wms: {\n assignedWarehouseId: null,\n assignedWarehouseName: null,\n isExplicitlyAssigned: false,\n stockSummary: [],\n reservationSummary: {\n status: 'unreserved',\n reservationIds: [],\n },\n },\n}\n\nconst EMPTY_CATALOG_ENRICHMENT: CatalogWmsEnrichment = {\n _wms: {\n inventoryProfile: null,\n stockSummary: [],\n reorderStatus: {\n state: 'no_profile',\n available: '0',\n reorderPoint: '0',\n safetyStock: '0',\n },\n },\n}\n\nfunction extractRecordId(record: SalesOrderRecord): string | null {\n return typeof record.id === 'string' && record.id.trim().length > 0 ? record.id : null\n}\n\nfunction extractOrderIdFromLine(line: SalesOrderLineRow): string | null {\n return typeof line.order_id === 'string' && line.order_id.length > 0 ? line.order_id : null\n}\n\nfunction extractProductIdFromVariant(variant: CatalogVariantRow): string | null {\n return typeof variant.product_id === 'string' && variant.product_id.length > 0\n ? variant.product_id\n : null\n}\n\nfunction extractWarehouseId(reservation: InventoryReservation): string | null {\n const relation = reservation.warehouse as { id?: string } | undefined\n return typeof relation?.id === 'string' ? relation.id : null\n}\n\nfunction addToMap(map: Map<string, number>, key: string, value: string | number | null | undefined) {\n const numeric = typeof value === 'number' ? value : Number(value ?? 0)\n map.set(key, (map.get(key) ?? 0) + numeric)\n}\n\nfunction formatQuantity(value: number): string {\n if (!Number.isFinite(value) || value === 0) return '0'\n const normalized = value.toFixed(4).replace(/\\.?0+$/, '')\n return normalized.length > 0 ? normalized : '0'\n}\n\nfunction toInventoryProfileSummary(profile: ProductInventoryProfile): InventoryProfileSummary {\n return {\n profileId: profile.id,\n catalogProductId: profile.catalogProductId,\n catalogVariantId: profile.catalogVariantId ?? null,\n defaultUom: profile.defaultUom,\n defaultStrategy: profile.defaultStrategy,\n trackLot: profile.trackLot,\n trackSerial: profile.trackSerial,\n trackExpiration: profile.trackExpiration,\n reorderPoint: profile.reorderPoint,\n safetyStock: profile.safetyStock,\n }\n}\n\nfunction resolveReservationStatus(requiredQuantity: number, reservedQuantity: number): ReservationStatus {\n if (requiredQuantity <= 0 || reservedQuantity <= 0) return 'unreserved'\n if (reservedQuantity + 0.0001 < requiredQuantity) return 'partially_reserved'\n return 'fully_reserved'\n}\n\nfunction resolveReorderStatus(\n availableQuantity: number,\n profile: ProductInventoryProfile | null | undefined,\n): ReorderStatus {\n if (!profile) {\n return {\n state: 'no_profile',\n available: formatQuantity(availableQuantity),\n reorderPoint: '0',\n safetyStock: '0',\n }\n }\n\n const reorderPoint = Number(profile.reorderPoint)\n const safetyStock = Number(profile.safetyStock)\n let state: ReorderState = 'healthy'\n\n if (availableQuantity <= safetyStock) {\n state = 'below_safety_stock'\n } else if (availableQuantity <= reorderPoint) {\n state = 'below_reorder_point'\n }\n\n return {\n state,\n available: formatQuantity(availableQuantity),\n reorderPoint: profile.reorderPoint,\n safetyStock: profile.safetyStock,\n }\n}\n\nfunction buildBalanceSummaryByVariant(\n balances: InventoryBalance[],\n): Map<string, BalanceAggregate> {\n const byVariant = new Map<string, BalanceAggregate>()\n\n for (const balance of balances) {\n const current = byVariant.get(balance.catalogVariantId) ?? {\n onHand: 0,\n reserved: 0,\n allocated: 0,\n available: 0,\n }\n\n current.onHand += Number(balance.quantityOnHand)\n current.reserved += Number(balance.quantityReserved)\n current.allocated += Number(balance.quantityAllocated)\n current.available +=\n Number(balance.quantityOnHand) - Number(balance.quantityReserved) - Number(balance.quantityAllocated)\n\n byVariant.set(balance.catalogVariantId, current)\n }\n\n return byVariant\n}\n\nfunction buildStockSummaryItem(\n catalogVariantId: string,\n aggregate: BalanceAggregate | undefined,\n): StockSummaryItem {\n return {\n catalogVariantId,\n onHand: formatQuantity(aggregate?.onHand ?? 0),\n reserved: formatQuantity(aggregate?.reserved ?? 0),\n allocated: formatQuantity(aggregate?.allocated ?? 0),\n available: formatQuantity(aggregate?.available ?? 0),\n }\n}\n\nasync function isSalesOrderInventoryEnabled(context: EnricherContext): Promise<boolean> {\n const container = context.container as { resolve?: (name: string) => unknown } | undefined\n if (!container?.resolve) return true\n try {\n const featureTogglesService = container.resolve('featureTogglesService') as FeatureTogglesService | undefined\n const em = container.resolve('em') as EntityManager | undefined\n if (!featureTogglesService || !em) return true\n return resolveWmsIntegrationToggleEnabled(\n featureTogglesService,\n em,\n 'wms_integration_sales_order_inventory',\n context.tenantId,\n )\n } catch {\n return true\n }\n}\n\nasync function loadExplicitAssignments(\n em: EntityManager,\n orderIds: string[],\n scope: Scope,\n): Promise<SalesOrderWarehouseAssignment[]> {\n if (orderIds.length === 0) return []\n return findWithDecryption(\n em,\n SalesOrderWarehouseAssignment,\n {\n salesOrderId: { $in: orderIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nasync function loadWarehousesByIds(\n em: EntityManager,\n warehouseIds: string[],\n scope: Scope,\n): Promise<Warehouse[]> {\n if (warehouseIds.length === 0) return []\n return findWithDecryption(\n em,\n Warehouse,\n {\n id: { $in: warehouseIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nasync function loadOrderLines(\n context: EnricherContext,\n orderIds: string[],\n scope: Scope,\n): Promise<SalesOrderLineRow[]> {\n if (orderIds.length === 0) return []\n // Read sales order lines via QueryEngine instead of importing the sales ORM\n // entity. Keeps the WMS module decoupled from sales internals; queryEngine\n // resolves the base table via Mikro-ORM metadata.\n const container = context.container as { resolve: (name: string) => unknown }\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const result = await queryEngine.query<SalesOrderLineRow>(E.sales.sales_order_line, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n filters: { order_id: { $in: orderIds } },\n fields: ['id', 'order_id', 'product_variant_id', 'quantity', 'line_number', 'status'],\n sort: [{ field: 'line_number', dir: 'asc' as never }],\n page: { page: 1, pageSize: 5000 },\n })\n return result.items\n}\n\nasync function loadReservations(\n em: EntityManager,\n orderIds: string[],\n scope: Scope,\n): Promise<InventoryReservation[]> {\n if (orderIds.length === 0) return []\n return findWithDecryption(\n em,\n InventoryReservation,\n {\n sourceType: 'order',\n sourceId: { $in: orderIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { orderBy: { createdAt: 'asc' } },\n scope,\n )\n}\n\nasync function loadBalances(\n em: EntityManager,\n variantIds: string[],\n scope: Scope,\n): Promise<InventoryBalance[]> {\n if (variantIds.length === 0) return []\n return findWithDecryption(\n em,\n InventoryBalance,\n {\n catalogVariantId: { $in: variantIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nasync function loadCatalogVariants(\n context: EnricherContext,\n productIds: string[],\n scope: Scope,\n): Promise<CatalogVariantRow[]> {\n if (productIds.length === 0) return []\n const container = context.container as { resolve: (name: string) => unknown }\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const result = await queryEngine.query<CatalogVariantRow>(E.catalog.catalog_product_variant, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n filters: { product_id: { $in: productIds } },\n fields: ['id', 'product_id'],\n sort: [{ field: 'created_at', dir: 'asc' as never }],\n page: { page: 1, pageSize: 5000 },\n })\n return result.items\n}\n\nasync function loadProductInventoryProfiles(\n em: EntityManager,\n productIds: string[],\n scope: Scope,\n): Promise<ProductInventoryProfile[]> {\n if (productIds.length === 0) return []\n return findWithDecryption(\n em,\n ProductInventoryProfile,\n {\n catalogProductId: { $in: productIds },\n catalogVariantId: null,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nasync function loadVariantInventoryProfiles(\n em: EntityManager,\n variantIds: string[],\n scope: Scope,\n): Promise<ProductInventoryProfile[]> {\n if (variantIds.length === 0) return []\n return findWithDecryption(\n em,\n ProductInventoryProfile,\n {\n catalogVariantId: { $in: variantIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nconst salesOrderInventoryEnricher: ResponseEnricher<SalesOrderRecord, SalesOrderWmsEnrichment> = {\n id: 'wms.sales-order-inventory',\n targetEntity: E.sales.sales_order,\n features: ['wms.view'],\n priority: 40,\n timeout: 2000,\n fallback: EMPTY_ENRICHMENT,\n\n async enrichOne(record, context: EnricherScope) {\n return (await this.enrichMany!([record], context))[0]\n },\n\n async enrichMany(records, context: EnricherScope) {\n if (records.length === 0) return records as Array<SalesOrderRecord & SalesOrderWmsEnrichment>\n const enabled = await isSalesOrderInventoryEnabled(context)\n if (!enabled) return records as Array<SalesOrderRecord & SalesOrderWmsEnrichment>\n\n const orderIds = records\n .map(extractRecordId)\n .filter((value): value is string => Boolean(value))\n\n if (orderIds.length === 0) {\n return records.map((record) => ({ ...record, ...EMPTY_ENRICHMENT }))\n }\n\n const em = context.em.fork()\n const scope = { organizationId: context.organizationId, tenantId: context.tenantId }\n\n const lines = await loadOrderLines(context, orderIds, scope)\n\n const variantIds = Array.from(\n new Set(\n lines\n .map((line) => line.product_variant_id)\n .filter((value): value is string => typeof value === 'string' && value.length > 0),\n ),\n )\n\n const [reservations, balances, primaryWarehouseId, explicitAssignments] = await Promise.all([\n loadReservations(em, orderIds, scope),\n loadBalances(em, variantIds, scope),\n resolvePrimaryWarehouseId(em, scope),\n loadExplicitAssignments(em, orderIds, scope),\n ])\n\n const explicitWarehouseIdsByOrder = new Map<string, string>()\n for (const assignment of explicitAssignments) {\n const warehouseRel = assignment.warehouse as { id?: string } | undefined\n const warehouseId = warehouseRel?.id\n if (warehouseId) {\n explicitWarehouseIdsByOrder.set(assignment.salesOrderId, warehouseId)\n }\n }\n\n const reservationWarehouseIds = reservations\n .map(extractWarehouseId)\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n const allRelevantWarehouseIds = Array.from(\n new Set([\n ...Array.from(explicitWarehouseIdsByOrder.values()),\n ...reservationWarehouseIds,\n ...(primaryWarehouseId ? [primaryWarehouseId] : []),\n ]),\n )\n const warehouses = await loadWarehousesByIds(em, allRelevantWarehouseIds, scope)\n const warehouseNamesById = new Map(warehouses.map((w) => [w.id, w.name]))\n\n const variantAvailability = new Map<string, number>()\n const variantReserved = new Map<string, number>()\n for (const balance of balances) {\n addToMap(\n variantAvailability,\n balance.catalogVariantId,\n Number(balance.quantityOnHand) - Number(balance.quantityReserved) - Number(balance.quantityAllocated),\n )\n addToMap(variantReserved, balance.catalogVariantId, balance.quantityReserved)\n }\n\n const variantOrderBySalesOrder = new Map<string, string[]>()\n const requiredBySalesOrder = new Map<string, number>()\n\n for (const line of lines) {\n const orderId = extractOrderIdFromLine(line)\n const variantId = line.product_variant_id ?? null\n if (!orderId || !variantId) continue\n\n const variantOrder = variantOrderBySalesOrder.get(orderId) ?? []\n if (!variantOrder.includes(variantId)) {\n variantOrder.push(variantId)\n variantOrderBySalesOrder.set(orderId, variantOrder)\n }\n requiredBySalesOrder.set(orderId, (requiredBySalesOrder.get(orderId) ?? 0) + Number(line.quantity))\n }\n\n const activeReservationsBySalesOrder = new Map<string, InventoryReservation[]>()\n const reservedBySalesOrder = new Map<string, number>()\n\n for (const reservation of reservations) {\n if (reservation.status !== 'active') continue\n const orderId = reservation.sourceId\n const activeReservations = activeReservationsBySalesOrder.get(orderId) ?? []\n activeReservations.push(reservation)\n activeReservationsBySalesOrder.set(orderId, activeReservations)\n reservedBySalesOrder.set(orderId, (reservedBySalesOrder.get(orderId) ?? 0) + Number(reservation.quantity))\n }\n\n return records.map((record) => {\n const orderId = extractRecordId(record)\n if (!orderId) {\n return { ...record, ...EMPTY_ENRICHMENT }\n }\n\n const reservationsForOrder = activeReservationsBySalesOrder.get(orderId) ?? []\n const warehouseIds = Array.from(\n new Set(\n reservationsForOrder\n .map(extractWarehouseId)\n .filter((value): value is string => typeof value === 'string' && value.length > 0),\n ),\n )\n const variantIdsForOrder = variantOrderBySalesOrder.get(orderId) ?? []\n\n const explicitWarehouseId = explicitWarehouseIdsByOrder.get(orderId) ?? null\n const isExplicitlyAssigned = explicitWarehouseId !== null\n\n let assignedWarehouseId: string | null\n if (isExplicitlyAssigned) {\n assignedWarehouseId = explicitWarehouseId\n } else if (warehouseIds.length === 1) {\n assignedWarehouseId = warehouseIds[0]\n } else if (warehouseIds.length === 0 && primaryWarehouseId) {\n assignedWarehouseId = primaryWarehouseId\n } else {\n assignedWarehouseId = null\n }\n\n const assignedWarehouseName = assignedWarehouseId\n ? (warehouseNamesById.get(assignedWarehouseId) ?? null)\n : null\n\n return {\n ...record,\n _wms: {\n assignedWarehouseId,\n assignedWarehouseName,\n isExplicitlyAssigned,\n stockSummary: variantIdsForOrder.map((catalogVariantId) => ({\n catalogVariantId,\n available: formatQuantity(variantAvailability.get(catalogVariantId) ?? 0),\n reserved: formatQuantity(variantReserved.get(catalogVariantId) ?? 0),\n })),\n reservationSummary: {\n status: resolveReservationStatus(\n requiredBySalesOrder.get(orderId) ?? 0,\n reservedBySalesOrder.get(orderId) ?? 0,\n ),\n reservationIds: reservationsForOrder.map((reservation) => reservation.id),\n },\n },\n }\n })\n },\n}\n\nconst catalogProductInventoryEnricher: ResponseEnricher<CatalogProductRecord, CatalogWmsEnrichment> = {\n id: 'wms.catalog-product-inventory',\n targetEntity: E.catalog.catalog_product,\n features: ['wms.view'],\n priority: 40,\n timeout: 2000,\n fallback: EMPTY_CATALOG_ENRICHMENT,\n\n async enrichOne(record, context: EnricherScope) {\n return (await this.enrichMany!([record], context))[0]\n },\n\n async enrichMany(records, context: EnricherScope) {\n if (records.length === 0) return records as Array<CatalogProductRecord & CatalogWmsEnrichment>\n\n const productIds = records\n .map(extractRecordId)\n .filter((value): value is string => Boolean(value))\n\n if (productIds.length === 0) {\n return records.map((record) => ({ ...record, ...EMPTY_CATALOG_ENRICHMENT }))\n }\n\n const em = context.em.fork()\n const scope = { organizationId: context.organizationId, tenantId: context.tenantId }\n\n const variants = await loadCatalogVariants(context, productIds, scope)\n const variantIds = variants\n .map((variant) => variant.id)\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n const [productProfiles, balances] = await Promise.all([\n loadProductInventoryProfiles(em, productIds, scope),\n loadBalances(em, variantIds, scope),\n ])\n\n const profileByProductId = new Map(\n productProfiles.map((profile) => [profile.catalogProductId, profile] as const),\n )\n const balanceByVariantId = buildBalanceSummaryByVariant(balances)\n const variantIdsByProductId = new Map<string, string[]>()\n\n for (const variant of variants) {\n const productId = extractProductIdFromVariant(variant)\n if (!productId || typeof variant.id !== 'string') continue\n const entries = variantIdsByProductId.get(productId) ?? []\n entries.push(variant.id)\n variantIdsByProductId.set(productId, entries)\n }\n\n return records.map((record) => {\n const productId = extractRecordId(record)\n if (!productId) {\n return { ...record, ...EMPTY_CATALOG_ENRICHMENT }\n }\n\n const profile = profileByProductId.get(productId) ?? null\n const stockSummary = (variantIdsByProductId.get(productId) ?? []).map((variantId) =>\n buildStockSummaryItem(variantId, balanceByVariantId.get(variantId)),\n )\n const totalAvailable = stockSummary.reduce(\n (sum, item) => sum + Number(item.available),\n 0,\n )\n\n return {\n ...record,\n _wms: {\n inventoryProfile: profile ? toInventoryProfileSummary(profile) : null,\n stockSummary,\n reorderStatus: resolveReorderStatus(totalAvailable, profile),\n },\n }\n })\n },\n}\n\nconst catalogVariantInventoryEnricher: ResponseEnricher<CatalogVariantRecord, CatalogWmsEnrichment> = {\n id: 'wms.catalog-variant-inventory',\n targetEntity: E.catalog.catalog_product_variant,\n features: ['wms.view'],\n priority: 40,\n timeout: 2000,\n fallback: EMPTY_CATALOG_ENRICHMENT,\n\n async enrichOne(record, context: EnricherScope) {\n return (await this.enrichMany!([record], context))[0]\n },\n\n async enrichMany(records, context: EnricherScope) {\n if (records.length === 0) return records as Array<CatalogVariantRecord & CatalogWmsEnrichment>\n\n const variantIds = records\n .map(extractRecordId)\n .filter((value): value is string => Boolean(value))\n\n if (variantIds.length === 0) {\n return records.map((record) => ({ ...record, ...EMPTY_CATALOG_ENRICHMENT }))\n }\n\n const em = context.em.fork()\n const scope = { organizationId: context.organizationId, tenantId: context.tenantId }\n\n const [profiles, balances] = await Promise.all([\n loadVariantInventoryProfiles(em, variantIds, scope),\n loadBalances(em, variantIds, scope),\n ])\n\n const profileByVariantId = new Map(\n profiles\n .filter((profile): profile is ProductInventoryProfile & { catalogVariantId: string } => typeof profile.catalogVariantId === 'string')\n .map((profile) => [profile.catalogVariantId, profile] as const),\n )\n const balanceByVariantId = buildBalanceSummaryByVariant(balances)\n\n return records.map((record) => {\n const variantId = extractRecordId(record)\n if (!variantId) {\n return { ...record, ...EMPTY_CATALOG_ENRICHMENT }\n }\n\n const profile = profileByVariantId.get(variantId) ?? null\n const stockSummary = [buildStockSummaryItem(variantId, balanceByVariantId.get(variantId))]\n\n return {\n ...record,\n _wms: {\n inventoryProfile: profile ? toInventoryProfileSummary(profile) : null,\n stockSummary,\n reorderStatus: resolveReorderStatus(Number(stockSummary[0]?.available ?? 0), profile),\n },\n }\n })\n },\n}\n\nexport const enrichers: ResponseEnricher[] = [\n salesOrderInventoryEnricher,\n catalogProductInventoryEnricher,\n catalogVariantInventoryEnricher,\n]\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ResponseEnricher, EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { FeatureTogglesService } from '@open-mercato/core/modules/feature_toggles/lib/feature-flag-check'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { E } from '#generated/entities.ids.generated'\nimport { InventoryBalance, InventoryReservation, ProductInventoryProfile, SalesOrderWarehouseAssignment, Warehouse } from './entities'\nimport { resolvePrimaryWarehouseId } from '../lib/primaryWarehousePolicy'\nimport {\n WMS_ENRICHER_CACHE_TTL_MS,\n WMS_INVENTORY_CACHE_TAG,\n WMS_WAREHOUSE_CACHE_TAG,\n} from '../lib/enricherCacheTags'\n\ntype SalesOrderRecord = Record<string, unknown> & { id?: string }\ntype CatalogProductRecord = Record<string, unknown> & { id?: string }\ntype CatalogVariantRecord = Record<string, unknown> & { id?: string; product_id?: string; productId?: string }\n\ntype SalesOrderLineRow = {\n id?: string\n order_id?: string | null\n product_variant_id?: string | null\n quantity?: string | number | null\n line_number?: number | null\n status?: string | null\n}\n\ntype CatalogVariantRow = {\n id?: string\n product_id?: string | null\n}\ntype ReservationStatus = 'unreserved' | 'partially_reserved' | 'fully_reserved'\ntype ReorderState = 'no_profile' | 'healthy' | 'below_reorder_point' | 'below_safety_stock'\n\ntype InventoryProfileSummary = {\n profileId: string\n catalogProductId: string\n catalogVariantId: string | null\n defaultUom: string\n defaultStrategy: string\n trackLot: boolean\n trackSerial: boolean\n trackExpiration: boolean\n reorderPoint: string\n safetyStock: string\n}\n\ntype StockSummaryItem = {\n catalogVariantId: string\n onHand: string\n reserved: string\n allocated: string\n available: string\n}\n\ntype ReorderStatus = {\n state: ReorderState\n available: string\n reorderPoint: string\n safetyStock: string\n}\n\ntype SalesOrderWmsEnrichment = {\n _wms: {\n assignedWarehouseId: string | null\n assignedWarehouseName: string | null\n isExplicitlyAssigned: boolean\n stockSummary: Array<{\n catalogVariantId: string\n available: string\n reserved: string\n }>\n reservationSummary: {\n status: ReservationStatus\n reservationIds: string[]\n }\n }\n}\n\ntype CatalogWmsEnrichment = {\n _wms: {\n inventoryProfile: InventoryProfileSummary | null\n stockSummary: StockSummaryItem[]\n reorderStatus: ReorderStatus\n }\n}\n\ntype EnricherScope = EnricherContext & { em: EntityManager }\ntype Scope = { organizationId: string; tenantId: string }\ntype BalanceAggregate = { onHand: number; reserved: number; allocated: number; available: number }\n\nimport { resolveWmsIntegrationToggleEnabled } from '../lib/wmsIntegrationToggles'\nconst EMPTY_ENRICHMENT: SalesOrderWmsEnrichment = {\n _wms: {\n assignedWarehouseId: null,\n assignedWarehouseName: null,\n isExplicitlyAssigned: false,\n stockSummary: [],\n reservationSummary: {\n status: 'unreserved',\n reservationIds: [],\n },\n },\n}\n\nconst EMPTY_CATALOG_ENRICHMENT: CatalogWmsEnrichment = {\n _wms: {\n inventoryProfile: null,\n stockSummary: [],\n reorderStatus: {\n state: 'no_profile',\n available: '0',\n reorderPoint: '0',\n safetyStock: '0',\n },\n },\n}\n\nfunction extractRecordId(record: SalesOrderRecord): string | null {\n return typeof record.id === 'string' && record.id.trim().length > 0 ? record.id : null\n}\n\nfunction extractOrderIdFromLine(line: SalesOrderLineRow): string | null {\n return typeof line.order_id === 'string' && line.order_id.length > 0 ? line.order_id : null\n}\n\nfunction extractProductIdFromVariant(variant: CatalogVariantRow): string | null {\n return typeof variant.product_id === 'string' && variant.product_id.length > 0\n ? variant.product_id\n : null\n}\n\nfunction extractWarehouseId(reservation: InventoryReservation): string | null {\n const relation = reservation.warehouse as { id?: string } | undefined\n return typeof relation?.id === 'string' ? relation.id : null\n}\n\nfunction addToMap(map: Map<string, number>, key: string, value: string | number | null | undefined) {\n const numeric = typeof value === 'number' ? value : Number(value ?? 0)\n map.set(key, (map.get(key) ?? 0) + numeric)\n}\n\nfunction formatQuantity(value: number): string {\n if (!Number.isFinite(value) || value === 0) return '0'\n const normalized = value.toFixed(4).replace(/\\.?0+$/, '')\n return normalized.length > 0 ? normalized : '0'\n}\n\nfunction toInventoryProfileSummary(profile: ProductInventoryProfile): InventoryProfileSummary {\n return {\n profileId: profile.id,\n catalogProductId: profile.catalogProductId,\n catalogVariantId: profile.catalogVariantId ?? null,\n defaultUom: profile.defaultUom,\n defaultStrategy: profile.defaultStrategy,\n trackLot: profile.trackLot,\n trackSerial: profile.trackSerial,\n trackExpiration: profile.trackExpiration,\n reorderPoint: profile.reorderPoint,\n safetyStock: profile.safetyStock,\n }\n}\n\nfunction resolveReservationStatus(requiredQuantity: number, reservedQuantity: number): ReservationStatus {\n if (requiredQuantity <= 0 || reservedQuantity <= 0) return 'unreserved'\n if (reservedQuantity + 0.0001 < requiredQuantity) return 'partially_reserved'\n return 'fully_reserved'\n}\n\nfunction resolveReorderStatus(\n availableQuantity: number,\n profile: ProductInventoryProfile | null | undefined,\n): ReorderStatus {\n if (!profile) {\n return {\n state: 'no_profile',\n available: formatQuantity(availableQuantity),\n reorderPoint: '0',\n safetyStock: '0',\n }\n }\n\n const reorderPoint = Number(profile.reorderPoint)\n const safetyStock = Number(profile.safetyStock)\n let state: ReorderState = 'healthy'\n\n if (availableQuantity <= safetyStock) {\n state = 'below_safety_stock'\n } else if (availableQuantity <= reorderPoint) {\n state = 'below_reorder_point'\n }\n\n return {\n state,\n available: formatQuantity(availableQuantity),\n reorderPoint: profile.reorderPoint,\n safetyStock: profile.safetyStock,\n }\n}\n\nfunction buildBalanceSummaryByVariant(\n balances: InventoryBalance[],\n): Map<string, BalanceAggregate> {\n const byVariant = new Map<string, BalanceAggregate>()\n\n for (const balance of balances) {\n const current = byVariant.get(balance.catalogVariantId) ?? {\n onHand: 0,\n reserved: 0,\n allocated: 0,\n available: 0,\n }\n\n current.onHand += Number(balance.quantityOnHand)\n current.reserved += Number(balance.quantityReserved)\n current.allocated += Number(balance.quantityAllocated)\n current.available +=\n Number(balance.quantityOnHand) - Number(balance.quantityReserved) - Number(balance.quantityAllocated)\n\n byVariant.set(balance.catalogVariantId, current)\n }\n\n return byVariant\n}\n\nfunction buildStockSummaryItem(\n catalogVariantId: string,\n aggregate: BalanceAggregate | undefined,\n): StockSummaryItem {\n return {\n catalogVariantId,\n onHand: formatQuantity(aggregate?.onHand ?? 0),\n reserved: formatQuantity(aggregate?.reserved ?? 0),\n allocated: formatQuantity(aggregate?.allocated ?? 0),\n available: formatQuantity(aggregate?.available ?? 0),\n }\n}\n\nasync function isSalesOrderInventoryEnabled(context: EnricherContext): Promise<boolean> {\n const container = context.container as { resolve?: (name: string) => unknown } | undefined\n if (!container?.resolve) return true\n try {\n const featureTogglesService = container.resolve('featureTogglesService') as FeatureTogglesService | undefined\n const em = container.resolve('em') as EntityManager | undefined\n if (!featureTogglesService || !em) return true\n return resolveWmsIntegrationToggleEnabled(\n featureTogglesService,\n em,\n 'wms_integration_sales_order_inventory',\n context.tenantId,\n )\n } catch {\n return true\n }\n}\n\nasync function loadExplicitAssignments(\n em: EntityManager,\n orderIds: string[],\n scope: Scope,\n): Promise<SalesOrderWarehouseAssignment[]> {\n if (orderIds.length === 0) return []\n return findWithDecryption(\n em,\n SalesOrderWarehouseAssignment,\n {\n salesOrderId: { $in: orderIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nasync function loadWarehousesByIds(\n em: EntityManager,\n warehouseIds: string[],\n scope: Scope,\n): Promise<Warehouse[]> {\n if (warehouseIds.length === 0) return []\n return findWithDecryption(\n em,\n Warehouse,\n {\n id: { $in: warehouseIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nasync function loadOrderLines(\n context: EnricherContext,\n orderIds: string[],\n scope: Scope,\n): Promise<SalesOrderLineRow[]> {\n if (orderIds.length === 0) return []\n // Read sales order lines via QueryEngine instead of importing the sales ORM\n // entity. Keeps the WMS module decoupled from sales internals; queryEngine\n // resolves the base table via Mikro-ORM metadata.\n const container = context.container as { resolve: (name: string) => unknown }\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const result = await queryEngine.query<SalesOrderLineRow>(E.sales.sales_order_line, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n filters: { order_id: { $in: orderIds } },\n fields: ['id', 'order_id', 'product_variant_id', 'quantity', 'line_number', 'status'],\n sort: [{ field: 'line_number', dir: 'asc' as never }],\n page: { page: 1, pageSize: 5000 },\n })\n return result.items\n}\n\nasync function loadReservations(\n em: EntityManager,\n orderIds: string[],\n scope: Scope,\n): Promise<InventoryReservation[]> {\n if (orderIds.length === 0) return []\n return findWithDecryption(\n em,\n InventoryReservation,\n {\n sourceType: 'order',\n sourceId: { $in: orderIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n { orderBy: { createdAt: 'asc' } },\n scope,\n )\n}\n\nasync function loadBalances(\n em: EntityManager,\n variantIds: string[],\n scope: Scope,\n): Promise<InventoryBalance[]> {\n if (variantIds.length === 0) return []\n return findWithDecryption(\n em,\n InventoryBalance,\n {\n catalogVariantId: { $in: variantIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nasync function loadCatalogVariants(\n context: EnricherContext,\n productIds: string[],\n scope: Scope,\n): Promise<CatalogVariantRow[]> {\n if (productIds.length === 0) return []\n const container = context.container as { resolve: (name: string) => unknown }\n const queryEngine = container.resolve('queryEngine') as QueryEngine\n const result = await queryEngine.query<CatalogVariantRow>(E.catalog.catalog_product_variant, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n filters: { product_id: { $in: productIds } },\n fields: ['id', 'product_id'],\n sort: [{ field: 'created_at', dir: 'asc' as never }],\n page: { page: 1, pageSize: 5000 },\n })\n return result.items\n}\n\nasync function loadProductInventoryProfiles(\n em: EntityManager,\n productIds: string[],\n scope: Scope,\n): Promise<ProductInventoryProfile[]> {\n if (productIds.length === 0) return []\n return findWithDecryption(\n em,\n ProductInventoryProfile,\n {\n catalogProductId: { $in: productIds },\n catalogVariantId: null,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nasync function loadVariantInventoryProfiles(\n em: EntityManager,\n variantIds: string[],\n scope: Scope,\n): Promise<ProductInventoryProfile[]> {\n if (variantIds.length === 0) return []\n return findWithDecryption(\n em,\n ProductInventoryProfile,\n {\n catalogVariantId: { $in: variantIds },\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n}\n\nconst salesOrderInventoryEnricher: ResponseEnricher<SalesOrderRecord, SalesOrderWmsEnrichment> = {\n id: 'wms.sales-order-inventory',\n targetEntity: E.sales.sales_order,\n features: ['wms.view'],\n priority: 40,\n timeout: 2000,\n fallback: EMPTY_ENRICHMENT,\n // Reads reservations, balances, the primary warehouse and the explicit\n // assignment, so it depends on both the inventory and the warehouse tags.\n cache: {\n strategy: 'read-through',\n ttl: WMS_ENRICHER_CACHE_TTL_MS,\n tags: [WMS_INVENTORY_CACHE_TAG, WMS_WAREHOUSE_CACHE_TAG],\n },\n\n async enrichOne(record, context: EnricherScope) {\n return (await this.enrichMany!([record], context))[0]\n },\n\n async enrichMany(records, context: EnricherScope) {\n if (records.length === 0) return records as Array<SalesOrderRecord & SalesOrderWmsEnrichment>\n const enabled = await isSalesOrderInventoryEnabled(context)\n if (!enabled) return records as Array<SalesOrderRecord & SalesOrderWmsEnrichment>\n\n const orderIds = records\n .map(extractRecordId)\n .filter((value): value is string => Boolean(value))\n\n if (orderIds.length === 0) {\n return records.map((record) => ({ ...record, ...EMPTY_ENRICHMENT }))\n }\n\n const em = context.em.fork()\n const scope = { organizationId: context.organizationId, tenantId: context.tenantId }\n\n const lines = await loadOrderLines(context, orderIds, scope)\n\n const variantIds = Array.from(\n new Set(\n lines\n .map((line) => line.product_variant_id)\n .filter((value): value is string => typeof value === 'string' && value.length > 0),\n ),\n )\n\n const [reservations, balances, primaryWarehouseId, explicitAssignments] = await Promise.all([\n loadReservations(em, orderIds, scope),\n loadBalances(em, variantIds, scope),\n resolvePrimaryWarehouseId(em, scope),\n loadExplicitAssignments(em, orderIds, scope),\n ])\n\n const explicitWarehouseIdsByOrder = new Map<string, string>()\n for (const assignment of explicitAssignments) {\n const warehouseRel = assignment.warehouse as { id?: string } | undefined\n const warehouseId = warehouseRel?.id\n if (warehouseId) {\n explicitWarehouseIdsByOrder.set(assignment.salesOrderId, warehouseId)\n }\n }\n\n const reservationWarehouseIds = reservations\n .map(extractWarehouseId)\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n const allRelevantWarehouseIds = Array.from(\n new Set([\n ...Array.from(explicitWarehouseIdsByOrder.values()),\n ...reservationWarehouseIds,\n ...(primaryWarehouseId ? [primaryWarehouseId] : []),\n ]),\n )\n const warehouses = await loadWarehousesByIds(em, allRelevantWarehouseIds, scope)\n const warehouseNamesById = new Map(warehouses.map((w) => [w.id, w.name]))\n\n const variantAvailability = new Map<string, number>()\n const variantReserved = new Map<string, number>()\n for (const balance of balances) {\n addToMap(\n variantAvailability,\n balance.catalogVariantId,\n Number(balance.quantityOnHand) - Number(balance.quantityReserved) - Number(balance.quantityAllocated),\n )\n addToMap(variantReserved, balance.catalogVariantId, balance.quantityReserved)\n }\n\n const variantOrderBySalesOrder = new Map<string, string[]>()\n const requiredBySalesOrder = new Map<string, number>()\n\n for (const line of lines) {\n const orderId = extractOrderIdFromLine(line)\n const variantId = line.product_variant_id ?? null\n if (!orderId || !variantId) continue\n\n const variantOrder = variantOrderBySalesOrder.get(orderId) ?? []\n if (!variantOrder.includes(variantId)) {\n variantOrder.push(variantId)\n variantOrderBySalesOrder.set(orderId, variantOrder)\n }\n requiredBySalesOrder.set(orderId, (requiredBySalesOrder.get(orderId) ?? 0) + Number(line.quantity))\n }\n\n const activeReservationsBySalesOrder = new Map<string, InventoryReservation[]>()\n const reservedBySalesOrder = new Map<string, number>()\n\n for (const reservation of reservations) {\n if (reservation.status !== 'active') continue\n const orderId = reservation.sourceId\n const activeReservations = activeReservationsBySalesOrder.get(orderId) ?? []\n activeReservations.push(reservation)\n activeReservationsBySalesOrder.set(orderId, activeReservations)\n reservedBySalesOrder.set(orderId, (reservedBySalesOrder.get(orderId) ?? 0) + Number(reservation.quantity))\n }\n\n return records.map((record) => {\n const orderId = extractRecordId(record)\n if (!orderId) {\n return { ...record, ...EMPTY_ENRICHMENT }\n }\n\n const reservationsForOrder = activeReservationsBySalesOrder.get(orderId) ?? []\n const warehouseIds = Array.from(\n new Set(\n reservationsForOrder\n .map(extractWarehouseId)\n .filter((value): value is string => typeof value === 'string' && value.length > 0),\n ),\n )\n const variantIdsForOrder = variantOrderBySalesOrder.get(orderId) ?? []\n\n const explicitWarehouseId = explicitWarehouseIdsByOrder.get(orderId) ?? null\n const isExplicitlyAssigned = explicitWarehouseId !== null\n\n let assignedWarehouseId: string | null\n if (isExplicitlyAssigned) {\n assignedWarehouseId = explicitWarehouseId\n } else if (warehouseIds.length === 1) {\n assignedWarehouseId = warehouseIds[0]\n } else if (warehouseIds.length === 0 && primaryWarehouseId) {\n assignedWarehouseId = primaryWarehouseId\n } else {\n assignedWarehouseId = null\n }\n\n const assignedWarehouseName = assignedWarehouseId\n ? (warehouseNamesById.get(assignedWarehouseId) ?? null)\n : null\n\n return {\n ...record,\n _wms: {\n assignedWarehouseId,\n assignedWarehouseName,\n isExplicitlyAssigned,\n stockSummary: variantIdsForOrder.map((catalogVariantId) => ({\n catalogVariantId,\n available: formatQuantity(variantAvailability.get(catalogVariantId) ?? 0),\n reserved: formatQuantity(variantReserved.get(catalogVariantId) ?? 0),\n })),\n reservationSummary: {\n status: resolveReservationStatus(\n requiredBySalesOrder.get(orderId) ?? 0,\n reservedBySalesOrder.get(orderId) ?? 0,\n ),\n reservationIds: reservationsForOrder.map((reservation) => reservation.id),\n },\n },\n }\n })\n },\n}\n\nconst catalogProductInventoryEnricher: ResponseEnricher<CatalogProductRecord, CatalogWmsEnrichment> = {\n id: 'wms.catalog-product-inventory',\n targetEntity: E.catalog.catalog_product,\n features: ['wms.view'],\n priority: 40,\n timeout: 2000,\n fallback: EMPTY_CATALOG_ENRICHMENT,\n // Reads inventory profiles and balances; also reads catalog variants, so the\n // catalog variant events invalidate the inventory tag too.\n cache: {\n strategy: 'read-through',\n ttl: WMS_ENRICHER_CACHE_TTL_MS,\n tags: [WMS_INVENTORY_CACHE_TAG],\n },\n\n async enrichOne(record, context: EnricherScope) {\n return (await this.enrichMany!([record], context))[0]\n },\n\n async enrichMany(records, context: EnricherScope) {\n if (records.length === 0) return records as Array<CatalogProductRecord & CatalogWmsEnrichment>\n\n const productIds = records\n .map(extractRecordId)\n .filter((value): value is string => Boolean(value))\n\n if (productIds.length === 0) {\n return records.map((record) => ({ ...record, ...EMPTY_CATALOG_ENRICHMENT }))\n }\n\n const em = context.em.fork()\n const scope = { organizationId: context.organizationId, tenantId: context.tenantId }\n\n const variants = await loadCatalogVariants(context, productIds, scope)\n const variantIds = variants\n .map((variant) => variant.id)\n .filter((value): value is string => typeof value === 'string' && value.length > 0)\n const [productProfiles, balances] = await Promise.all([\n loadProductInventoryProfiles(em, productIds, scope),\n loadBalances(em, variantIds, scope),\n ])\n\n const profileByProductId = new Map(\n productProfiles.map((profile) => [profile.catalogProductId, profile] as const),\n )\n const balanceByVariantId = buildBalanceSummaryByVariant(balances)\n const variantIdsByProductId = new Map<string, string[]>()\n\n for (const variant of variants) {\n const productId = extractProductIdFromVariant(variant)\n if (!productId || typeof variant.id !== 'string') continue\n const entries = variantIdsByProductId.get(productId) ?? []\n entries.push(variant.id)\n variantIdsByProductId.set(productId, entries)\n }\n\n return records.map((record) => {\n const productId = extractRecordId(record)\n if (!productId) {\n return { ...record, ...EMPTY_CATALOG_ENRICHMENT }\n }\n\n const profile = profileByProductId.get(productId) ?? null\n const stockSummary = (variantIdsByProductId.get(productId) ?? []).map((variantId) =>\n buildStockSummaryItem(variantId, balanceByVariantId.get(variantId)),\n )\n const totalAvailable = stockSummary.reduce(\n (sum, item) => sum + Number(item.available),\n 0,\n )\n\n return {\n ...record,\n _wms: {\n inventoryProfile: profile ? toInventoryProfileSummary(profile) : null,\n stockSummary,\n reorderStatus: resolveReorderStatus(totalAvailable, profile),\n },\n }\n })\n },\n}\n\nconst catalogVariantInventoryEnricher: ResponseEnricher<CatalogVariantRecord, CatalogWmsEnrichment> = {\n id: 'wms.catalog-variant-inventory',\n targetEntity: E.catalog.catalog_product_variant,\n features: ['wms.view'],\n priority: 40,\n timeout: 2000,\n fallback: EMPTY_CATALOG_ENRICHMENT,\n // Reads inventory profiles and balances for the variant only.\n cache: {\n strategy: 'read-through',\n ttl: WMS_ENRICHER_CACHE_TTL_MS,\n tags: [WMS_INVENTORY_CACHE_TAG],\n },\n\n async enrichOne(record, context: EnricherScope) {\n return (await this.enrichMany!([record], context))[0]\n },\n\n async enrichMany(records, context: EnricherScope) {\n if (records.length === 0) return records as Array<CatalogVariantRecord & CatalogWmsEnrichment>\n\n const variantIds = records\n .map(extractRecordId)\n .filter((value): value is string => Boolean(value))\n\n if (variantIds.length === 0) {\n return records.map((record) => ({ ...record, ...EMPTY_CATALOG_ENRICHMENT }))\n }\n\n const em = context.em.fork()\n const scope = { organizationId: context.organizationId, tenantId: context.tenantId }\n\n const [profiles, balances] = await Promise.all([\n loadVariantInventoryProfiles(em, variantIds, scope),\n loadBalances(em, variantIds, scope),\n ])\n\n const profileByVariantId = new Map(\n profiles\n .filter((profile): profile is ProductInventoryProfile & { catalogVariantId: string } => typeof profile.catalogVariantId === 'string')\n .map((profile) => [profile.catalogVariantId, profile] as const),\n )\n const balanceByVariantId = buildBalanceSummaryByVariant(balances)\n\n return records.map((record) => {\n const variantId = extractRecordId(record)\n if (!variantId) {\n return { ...record, ...EMPTY_CATALOG_ENRICHMENT }\n }\n\n const profile = profileByVariantId.get(variantId) ?? null\n const stockSummary = [buildStockSummaryItem(variantId, balanceByVariantId.get(variantId))]\n\n return {\n ...record,\n _wms: {\n inventoryProfile: profile ? toInventoryProfileSummary(profile) : null,\n stockSummary,\n reorderStatus: resolveReorderStatus(Number(stockSummary[0]?.available ?? 0), profile),\n },\n }\n })\n },\n}\n\nexport const enrichers: ResponseEnricher[] = [\n salesOrderInventoryEnricher,\n catalogProductInventoryEnricher,\n catalogVariantInventoryEnricher,\n]\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const WMS_INVENTORY_CACHE_TAG = "wms:inventory";
|
|
2
|
+
const WMS_WAREHOUSE_CACHE_TAG = "wms:warehouse";
|
|
3
|
+
const WMS_ENRICHER_CACHE_TTL_MS = 3e4;
|
|
4
|
+
export {
|
|
5
|
+
WMS_ENRICHER_CACHE_TTL_MS,
|
|
6
|
+
WMS_INVENTORY_CACHE_TAG,
|
|
7
|
+
WMS_WAREHOUSE_CACHE_TAG
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=enricherCacheTags.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/lib/enricherCacheTags.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Cache tags shared by the WMS inventory enrichers and the write surfaces that\n * invalidate them. Read side and write side MUST use these constants so the two\n * cannot drift \u2014 a tag typo on either side is silent, and the only symptom is\n * inventory that stays stale until the TTL expires.\n *\n * The enricher runner adds `tenant:`, `organization:` and `enricher:` tags of\n * its own, and the cache service prefixes every tag with the current cache\n * tenant, so these are deliberately coarse collection tags rather than\n * per-warehouse ones: `ResponseEnricher.cache.tags` is a static array read from\n * the enricher definition and cannot vary per call. Collection tags\n * over-invalidate slightly; a per-warehouse scheme that the write side could\n * miss would under-invalidate, which is the failure that actually shows wrong\n * stock to a user.\n */\n\n/** Inventory quantities: balances, reservations, movements and profiles. */\nexport const WMS_INVENTORY_CACHE_TAG = 'wms:inventory'\n\n/** Warehouse identity and sales-order warehouse assignment. */\nexport const WMS_WAREHOUSE_CACHE_TAG = 'wms:warehouse'\n\n/**\n * Short by design. The tag invalidations below are the primary freshness\n * mechanism; this TTL is the unconditional backstop for anything they miss \u2014\n * a write that failed to resolve the cache service, a cross-process race, or a\n * write surface added later without wiring invalidation.\n */\nexport const WMS_ENRICHER_CACHE_TTL_MS = 30_000\n"],
|
|
5
|
+
"mappings": "AAiBO,MAAM,0BAA0B;AAGhC,MAAM,0BAA0B;AAQhC,MAAM,4BAA4B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runWithCacheTenant } from "@open-mercato/cache";
|
|
2
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
3
|
+
import {
|
|
4
|
+
WMS_INVENTORY_CACHE_TAG,
|
|
5
|
+
WMS_WAREHOUSE_CACHE_TAG
|
|
6
|
+
} from "./enricherCacheTags.js";
|
|
7
|
+
const logger = createLogger("wms").child({ component: "enricher-cache" });
|
|
8
|
+
function resolveCache(container) {
|
|
9
|
+
if (!container?.resolve) return null;
|
|
10
|
+
for (const name of ["cache", "cacheService"]) {
|
|
11
|
+
try {
|
|
12
|
+
const candidate = container.resolve(name);
|
|
13
|
+
if (candidate && typeof candidate.deleteByTags === "function") return candidate;
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function tagsForScope(scope) {
|
|
20
|
+
if (scope === "inventory") return [WMS_INVENTORY_CACHE_TAG];
|
|
21
|
+
if (scope === "warehouse") return [WMS_WAREHOUSE_CACHE_TAG];
|
|
22
|
+
return [WMS_INVENTORY_CACHE_TAG, WMS_WAREHOUSE_CACHE_TAG];
|
|
23
|
+
}
|
|
24
|
+
async function invalidateWmsInventoryEnricherCache(container, tenantId, scope = "all") {
|
|
25
|
+
if (!tenantId) return;
|
|
26
|
+
const cache = resolveCache(container);
|
|
27
|
+
if (!cache) return;
|
|
28
|
+
try {
|
|
29
|
+
await runWithCacheTenant(tenantId, () => cache.deleteByTags(tagsForScope(scope)));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
logger.warn("Failed to invalidate WMS inventory enricher cache", { tenantId, scope, err });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
invalidateWmsInventoryEnricherCache
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=invalidateInventoryEnricherCache.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/lib/invalidateInventoryEnricherCache.ts"],
|
|
4
|
+
"sourcesContent": ["// Drop the cached WMS inventory enrichment for a tenant after a write that\n// changed the data it was derived from.\n//\n// The three WMS inventory enrichers cache their cross-module reads read-through\n// with a short TTL (see `enricherCacheTags.ts`). Every write surface that moves\n// balances, reservations, movements, profiles, warehouses or the sales-order\n// warehouse assignment drops the matching tags here, so a user sees the new\n// stock on the next request rather than after the TTL.\n//\n// The cache service prefixes tags with the *current* cache tenant, which is an\n// AsyncLocalStorage value set on the request path. A subscriber or command side\n// effect running outside that scope would otherwise write its invalidation\n// against the `tenant:global:` prefix and silently miss every entry the request\n// path stored. `runWithCacheTenant` re-enters the right scope, mirroring\n// `directory/subscribers/invalidateOrgScopeCache.ts`.\n\nimport { runWithCacheTenant } from '@open-mercato/cache'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n WMS_INVENTORY_CACHE_TAG,\n WMS_WAREHOUSE_CACHE_TAG,\n} from './enricherCacheTags'\n\nconst logger = createLogger('wms').child({ component: 'enricher-cache' })\n\ntype CacheService = {\n deleteByTags(tags: string[]): Promise<number>\n}\n\nexport type EnricherCacheScope = 'inventory' | 'warehouse' | 'all'\n\ntype Resolver = { resolve: <T = unknown>(name: string) => T }\n\nfunction resolveCache(container: Resolver | null | undefined): CacheService | null {\n if (!container?.resolve) return null\n for (const name of ['cache', 'cacheService']) {\n try {\n const candidate = container.resolve<CacheService>(name)\n if (candidate && typeof candidate.deleteByTags === 'function') return candidate\n } catch {\n // try the next registration name\n }\n }\n return null\n}\n\nfunction tagsForScope(scope: EnricherCacheScope): string[] {\n if (scope === 'inventory') return [WMS_INVENTORY_CACHE_TAG]\n if (scope === 'warehouse') return [WMS_WAREHOUSE_CACHE_TAG]\n return [WMS_INVENTORY_CACHE_TAG, WMS_WAREHOUSE_CACHE_TAG]\n}\n\n/**\n * Best-effort tag invalidation. A failure is logged and swallowed: an\n * invalidation that throws must never fail the write that triggered it, and the\n * enricher TTL bounds how long a missed drop can serve stale data.\n */\nexport async function invalidateWmsInventoryEnricherCache(\n container: Resolver | null | undefined,\n tenantId: string | null | undefined,\n scope: EnricherCacheScope = 'all',\n): Promise<void> {\n if (!tenantId) return\n const cache = resolveCache(container)\n if (!cache) return\n try {\n await runWithCacheTenant(tenantId, () => cache.deleteByTags(tagsForScope(scope)))\n } catch (err) {\n logger.warn('Failed to invalidate WMS inventory enricher cache', { tenantId, scope, err })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAgBA,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,SAAS,aAAa,KAAK,EAAE,MAAM,EAAE,WAAW,iBAAiB,CAAC;AAUxE,SAAS,aAAa,WAA6D;AACjF,MAAI,CAAC,WAAW,QAAS,QAAO;AAChC,aAAW,QAAQ,CAAC,SAAS,cAAc,GAAG;AAC5C,QAAI;AACF,YAAM,YAAY,UAAU,QAAsB,IAAI;AACtD,UAAI,aAAa,OAAO,UAAU,iBAAiB,WAAY,QAAO;AAAA,IACxE,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,OAAqC;AACzD,MAAI,UAAU,YAAa,QAAO,CAAC,uBAAuB;AAC1D,MAAI,UAAU,YAAa,QAAO,CAAC,uBAAuB;AAC1D,SAAO,CAAC,yBAAyB,uBAAuB;AAC1D;AAOA,eAAsB,oCACpB,WACA,UACA,QAA4B,OACb;AACf,MAAI,CAAC,SAAU;AACf,QAAM,QAAQ,aAAa,SAAS;AACpC,MAAI,CAAC,MAAO;AACZ,MAAI;AACF,UAAM,mBAAmB,UAAU,MAAM,MAAM,aAAa,aAAa,KAAK,CAAC,CAAC;AAAA,EAClF,SAAS,KAAK;AACZ,WAAO,KAAK,qDAAqD,EAAE,UAAU,OAAO,IAAI,CAAC;AAAA,EAC3F;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
invalidateWmsInventoryEnricherCache
|
|
3
|
+
} from "./invalidateInventoryEnricherCache.js";
|
|
4
|
+
function createInventoryEnricherCacheHandler(scope) {
|
|
5
|
+
return async function handle(payload, ctx) {
|
|
6
|
+
const data = payload ?? {};
|
|
7
|
+
const tenantId = typeof data.tenantId === "string" && data.tenantId.length > 0 ? data.tenantId : ctx.tenantId ?? null;
|
|
8
|
+
await invalidateWmsInventoryEnricherCache(ctx, tenantId, scope);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
createInventoryEnricherCacheHandler
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=inventoryEnricherCacheSubscriber.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/lib/inventoryEnricherCacheSubscriber.ts"],
|
|
4
|
+
"sourcesContent": ["// Shared handler behind the WMS inventory enricher-cache invalidation\n// subscribers. Each subscriber file binds it to one event pattern, because\n// `metadata.event` takes a single pattern and the matcher is single-segment \u2014\n// `wms.*` does NOT match `wms.inventory_balance.created`, so every event family\n// the enrichers read from is registered explicitly.\n//\n// It lives here rather than under `subscribers/` because the module generator\n// scans that directory recursively and treats every file it finds as a\n// subscriber.\n\nimport {\n invalidateWmsInventoryEnricherCache,\n type EnricherCacheScope,\n} from './invalidateInventoryEnricherCache'\n\ntype SubscriberContext = {\n resolve: <T = unknown>(name: string) => T\n tenantId?: string | null\n}\n\nexport function createInventoryEnricherCacheHandler(scope: EnricherCacheScope) {\n return async function handle(payload: unknown, ctx: SubscriberContext): Promise<void> {\n const data = (payload ?? {}) as Record<string, unknown>\n const tenantId =\n typeof data.tenantId === 'string' && data.tenantId.length > 0\n ? data.tenantId\n : ctx.tenantId ?? null\n await invalidateWmsInventoryEnricherCache(ctx, tenantId, scope)\n }\n}\n"],
|
|
5
|
+
"mappings": "AAUA;AAAA,EACE;AAAA,OAEK;AAOA,SAAS,oCAAoC,OAA2B;AAC7E,SAAO,eAAe,OAAO,SAAkB,KAAuC;AACpF,UAAM,OAAQ,WAAW,CAAC;AAC1B,UAAM,WACJ,OAAO,KAAK,aAAa,YAAY,KAAK,SAAS,SAAS,IACxD,KAAK,WACL,IAAI,YAAY;AACtB,UAAM,oCAAoC,KAAK,UAAU,KAAK;AAAA,EAChE;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from "../lib/inventoryEnricherCacheSubscriber.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "wms.inventory_balance.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "wms:invalidate-enricher-cache-balance"
|
|
6
|
+
};
|
|
7
|
+
var invalidate_enricher_cache_balance_default = createInventoryEnricherCacheHandler("inventory");
|
|
8
|
+
export {
|
|
9
|
+
invalidate_enricher_cache_balance_default as default,
|
|
10
|
+
metadata
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invalidate-enricher-cache-balance.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/subscribers/invalidate-enricher-cache-balance.ts"],
|
|
4
|
+
"sourcesContent": ["import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'\n\nexport const metadata = {\n event: 'wms.inventory_balance.*',\n persistent: false,\n id: 'wms:invalidate-enricher-cache-balance',\n}\n\nexport default createInventoryEnricherCacheHandler('inventory')\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,IAAO,4CAAQ,oCAAoC,WAAW;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from "../lib/inventoryEnricherCacheSubscriber.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "wms.inventory.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "wms:invalidate-enricher-cache-inventory"
|
|
6
|
+
};
|
|
7
|
+
var invalidate_enricher_cache_inventory_default = createInventoryEnricherCacheHandler("inventory");
|
|
8
|
+
export {
|
|
9
|
+
invalidate_enricher_cache_inventory_default as default,
|
|
10
|
+
metadata
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invalidate-enricher-cache-inventory.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/subscribers/invalidate-enricher-cache-inventory.ts"],
|
|
4
|
+
"sourcesContent": ["import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'\n\nexport const metadata = {\n event: 'wms.inventory.*',\n persistent: false,\n id: 'wms:invalidate-enricher-cache-inventory',\n}\n\nexport default createInventoryEnricherCacheHandler('inventory')\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,IAAO,8CAAQ,oCAAoC,WAAW;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from "../lib/inventoryEnricherCacheSubscriber.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "wms.inventory_movement.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "wms:invalidate-enricher-cache-movement"
|
|
6
|
+
};
|
|
7
|
+
var invalidate_enricher_cache_movement_default = createInventoryEnricherCacheHandler("inventory");
|
|
8
|
+
export {
|
|
9
|
+
invalidate_enricher_cache_movement_default as default,
|
|
10
|
+
metadata
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invalidate-enricher-cache-movement.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/subscribers/invalidate-enricher-cache-movement.ts"],
|
|
4
|
+
"sourcesContent": ["import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'\n\nexport const metadata = {\n event: 'wms.inventory_movement.*',\n persistent: false,\n id: 'wms:invalidate-enricher-cache-movement',\n}\n\nexport default createInventoryEnricherCacheHandler('inventory')\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,IAAO,6CAAQ,oCAAoC,WAAW;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from "../lib/inventoryEnricherCacheSubscriber.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "wms.inventory_profile.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "wms:invalidate-enricher-cache-profile"
|
|
6
|
+
};
|
|
7
|
+
var invalidate_enricher_cache_profile_default = createInventoryEnricherCacheHandler("inventory");
|
|
8
|
+
export {
|
|
9
|
+
invalidate_enricher_cache_profile_default as default,
|
|
10
|
+
metadata
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invalidate-enricher-cache-profile.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/subscribers/invalidate-enricher-cache-profile.ts"],
|
|
4
|
+
"sourcesContent": ["import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'\n\nexport const metadata = {\n event: 'wms.inventory_profile.*',\n persistent: false,\n id: 'wms:invalidate-enricher-cache-profile',\n}\n\nexport default createInventoryEnricherCacheHandler('inventory')\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,IAAO,4CAAQ,oCAAoC,WAAW;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from "../lib/inventoryEnricherCacheSubscriber.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "wms.inventory_reservation.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "wms:invalidate-enricher-cache-reservation"
|
|
6
|
+
};
|
|
7
|
+
var invalidate_enricher_cache_reservation_default = createInventoryEnricherCacheHandler("inventory");
|
|
8
|
+
export {
|
|
9
|
+
invalidate_enricher_cache_reservation_default as default,
|
|
10
|
+
metadata
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invalidate-enricher-cache-reservation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/subscribers/invalidate-enricher-cache-reservation.ts"],
|
|
4
|
+
"sourcesContent": ["import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'\n\nexport const metadata = {\n event: 'wms.inventory_reservation.*',\n persistent: false,\n id: 'wms:invalidate-enricher-cache-reservation',\n}\n\nexport default createInventoryEnricherCacheHandler('inventory')\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,IAAO,gDAAQ,oCAAoC,WAAW;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from "../lib/inventoryEnricherCacheSubscriber.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "sales.line.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "wms:invalidate-enricher-cache-sales-line"
|
|
6
|
+
};
|
|
7
|
+
var invalidate_enricher_cache_sales_line_default = createInventoryEnricherCacheHandler("warehouse");
|
|
8
|
+
export {
|
|
9
|
+
invalidate_enricher_cache_sales_line_default as default,
|
|
10
|
+
metadata
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invalidate-enricher-cache-sales-line.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/subscribers/invalidate-enricher-cache-sales-line.ts"],
|
|
4
|
+
"sourcesContent": ["import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'\n\n// The sales module declares `sales.line.*` in its event registry and documents\n// it publicly, but no write surface emits it today \u2014 line writes ride the order\n// commands' `sales.order.*`. Registering the pattern anyway costs one ephemeral\n// subscription and means that wiring those events later cannot silently\n// reintroduce the staleness `invalidate-enricher-cache-sales-order.ts` closes.\nexport const metadata = {\n event: 'sales.line.*',\n persistent: false,\n id: 'wms:invalidate-enricher-cache-sales-line',\n}\n\nexport default createInventoryEnricherCacheHandler('warehouse')\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2CAA2C;AAO7C,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,IAAO,+CAAQ,oCAAoC,WAAW;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from "../lib/inventoryEnricherCacheSubscriber.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "sales.order.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "wms:invalidate-enricher-cache-sales-order"
|
|
6
|
+
};
|
|
7
|
+
var invalidate_enricher_cache_sales_order_default = createInventoryEnricherCacheHandler("warehouse");
|
|
8
|
+
export {
|
|
9
|
+
invalidate_enricher_cache_sales_order_default as default,
|
|
10
|
+
metadata
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invalidate-enricher-cache-sales-order.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/subscribers/invalidate-enricher-cache-sales-order.ts"],
|
|
4
|
+
"sourcesContent": ["import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'\n\n// `wms.sales-order-inventory` reads the order's lines, and both the required\n// quantity behind `reservationSummary.status` and the variant list behind\n// `stockSummary` come from them. Order lines are written inside the sales order\n// commands, which emit `sales.order.*` \u2014 not a WMS event \u2014 so without this the\n// enrichment would stay stale for the whole TTL after a line edit.\n//\n// Scope `warehouse` rather than `inventory`: `wms:warehouse` is declared by the\n// sales-order enricher alone, so this drops exactly its entries and leaves the\n// two catalog enrichers' caches intact.\nexport const metadata = {\n event: 'sales.order.*',\n persistent: false,\n id: 'wms:invalidate-enricher-cache-sales-order',\n}\n\nexport default createInventoryEnricherCacheHandler('warehouse')\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2CAA2C;AAW7C,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,IAAO,gDAAQ,oCAAoC,WAAW;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from "../lib/inventoryEnricherCacheSubscriber.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "catalog.variant.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "wms:invalidate-enricher-cache-variant"
|
|
6
|
+
};
|
|
7
|
+
var invalidate_enricher_cache_variant_default = createInventoryEnricherCacheHandler("inventory");
|
|
8
|
+
export {
|
|
9
|
+
invalidate_enricher_cache_variant_default as default,
|
|
10
|
+
metadata
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invalidate-enricher-cache-variant.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/subscribers/invalidate-enricher-cache-variant.ts"],
|
|
4
|
+
"sourcesContent": ["import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'\n\nexport const metadata = {\n event: 'catalog.variant.*',\n persistent: false,\n id: 'wms:invalidate-enricher-cache-variant',\n}\n\nexport default createInventoryEnricherCacheHandler('inventory')\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,IAAO,4CAAQ,oCAAoC,WAAW;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from "../lib/inventoryEnricherCacheSubscriber.js";
|
|
2
|
+
const metadata = {
|
|
3
|
+
event: "wms.warehouse.*",
|
|
4
|
+
persistent: false,
|
|
5
|
+
id: "wms:invalidate-enricher-cache-warehouse"
|
|
6
|
+
};
|
|
7
|
+
var invalidate_enricher_cache_warehouse_default = createInventoryEnricherCacheHandler("warehouse");
|
|
8
|
+
export {
|
|
9
|
+
invalidate_enricher_cache_warehouse_default as default,
|
|
10
|
+
metadata
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=invalidate-enricher-cache-warehouse.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/wms/subscribers/invalidate-enricher-cache-warehouse.ts"],
|
|
4
|
+
"sourcesContent": ["import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'\n\nexport const metadata = {\n event: 'wms.warehouse.*',\n persistent: false,\n id: 'wms:invalidate-enricher-cache-warehouse',\n}\n\nexport default createInventoryEnricherCacheHandler('warehouse')\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,2CAA2C;AAE7C,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAEA,IAAO,8CAAQ,oCAAoC,WAAW;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7171.1.9b31dbba45",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -252,16 +252,16 @@
|
|
|
252
252
|
"zod": "^4.4.3"
|
|
253
253
|
},
|
|
254
254
|
"peerDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
256
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
257
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7171.1.9b31dbba45",
|
|
256
|
+
"@open-mercato/shared": "0.7.1-develop.7171.1.9b31dbba45",
|
|
257
|
+
"@open-mercato/ui": "0.7.1-develop.7171.1.9b31dbba45",
|
|
258
258
|
"react": "^19.0.0",
|
|
259
259
|
"react-dom": "^19.0.0"
|
|
260
260
|
},
|
|
261
261
|
"devDependencies": {
|
|
262
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
263
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
264
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7171.1.9b31dbba45",
|
|
263
|
+
"@open-mercato/shared": "0.7.1-develop.7171.1.9b31dbba45",
|
|
264
|
+
"@open-mercato/ui": "0.7.1-develop.7171.1.9b31dbba45",
|
|
265
265
|
"@testing-library/dom": "^10.4.1",
|
|
266
266
|
"@testing-library/jest-dom": "^7.0.0",
|
|
267
267
|
"@testing-library/react": "^16.3.1",
|
|
@@ -10,6 +10,7 @@ import { SalesOrderWarehouseAssignment, Warehouse } from '../data/entities'
|
|
|
10
10
|
import type { SalesOrderWarehouseAssignInput, SalesOrderWarehouseUnassignInput } from '../data/validators'
|
|
11
11
|
import { z } from 'zod'
|
|
12
12
|
import { reserveInventoryForConfirmedOrder } from '../lib/salesOrderInventoryAutomation'
|
|
13
|
+
import { invalidateWmsInventoryEnricherCache } from '../lib/invalidateInventoryEnricherCache'
|
|
13
14
|
import { ensureOrganizationScope, ensureTenantScope } from './shared'
|
|
14
15
|
|
|
15
16
|
type AssignmentSnapshot = {
|
|
@@ -30,6 +31,17 @@ type UnassignWarehouseUndoPayload = {
|
|
|
30
31
|
before: AssignmentSnapshot | null
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
// These commands emit no WMS event, so the enricher-cache subscribers never see
|
|
35
|
+
// them. `wms.sales-order-inventory` surfaces the assigned warehouse, so every
|
|
36
|
+
// path here that writes an assignment — including the undo handlers — drops the
|
|
37
|
+
// warehouse tag itself.
|
|
38
|
+
async function invalidateAssignmentEnricherCache(
|
|
39
|
+
ctx: CommandRuntimeContext,
|
|
40
|
+
tenantId: string | null | undefined,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
await invalidateWmsInventoryEnricherCache(ctx.container, tenantId, 'warehouse')
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
function resolveScope(
|
|
34
46
|
ctx: CommandRuntimeContext,
|
|
35
47
|
fallback?: { tenantId?: string | null; organizationId?: string | null },
|
|
@@ -125,6 +137,7 @@ const assignWarehouseHandler: CommandHandler<
|
|
|
125
137
|
existing.assignedBy = ctx.auth?.sub ?? null
|
|
126
138
|
existing.updatedAt = new Date()
|
|
127
139
|
await em.flush()
|
|
140
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
128
141
|
return { assignmentId: existing.id, warehouseId: warehouse.id }
|
|
129
142
|
}
|
|
130
143
|
|
|
@@ -138,6 +151,7 @@ const assignWarehouseHandler: CommandHandler<
|
|
|
138
151
|
})
|
|
139
152
|
em.persist(assignment)
|
|
140
153
|
await em.flush()
|
|
154
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
141
155
|
return { assignmentId: assignment.id, warehouseId: warehouse.id }
|
|
142
156
|
},
|
|
143
157
|
|
|
@@ -187,6 +201,7 @@ const assignWarehouseHandler: CommandHandler<
|
|
|
187
201
|
if (!before) {
|
|
188
202
|
assignment.deletedAt = new Date()
|
|
189
203
|
await em.flush()
|
|
204
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
190
205
|
return
|
|
191
206
|
}
|
|
192
207
|
|
|
@@ -205,12 +220,14 @@ const assignWarehouseHandler: CommandHandler<
|
|
|
205
220
|
if (!previousWarehouse) {
|
|
206
221
|
assignment.deletedAt = new Date()
|
|
207
222
|
await em.flush()
|
|
223
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
208
224
|
return
|
|
209
225
|
}
|
|
210
226
|
|
|
211
227
|
assignment.warehouse = previousWarehouse
|
|
212
228
|
assignment.notes = before.notes ?? null
|
|
213
229
|
await em.flush()
|
|
230
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
214
231
|
}
|
|
215
232
|
},
|
|
216
233
|
}
|
|
@@ -239,6 +256,7 @@ const unassignWarehouseHandler: CommandHandler<
|
|
|
239
256
|
if (existing) {
|
|
240
257
|
existing.deletedAt = new Date()
|
|
241
258
|
await em.flush()
|
|
259
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
return { ok: true as const }
|
|
@@ -278,6 +296,7 @@ const unassignWarehouseHandler: CommandHandler<
|
|
|
278
296
|
if (existingDeleted) {
|
|
279
297
|
existingDeleted.deletedAt = null
|
|
280
298
|
await em.flush()
|
|
299
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
281
300
|
return
|
|
282
301
|
}
|
|
283
302
|
|
|
@@ -304,6 +323,7 @@ const unassignWarehouseHandler: CommandHandler<
|
|
|
304
323
|
})
|
|
305
324
|
em.persist(restored)
|
|
306
325
|
await em.flush()
|
|
326
|
+
await invalidateAssignmentEnricherCache(ctx, scope.tenantId)
|
|
307
327
|
},
|
|
308
328
|
}
|
|
309
329
|
|
|
@@ -6,6 +6,11 @@ import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
|
6
6
|
import { E } from '#generated/entities.ids.generated'
|
|
7
7
|
import { InventoryBalance, InventoryReservation, ProductInventoryProfile, SalesOrderWarehouseAssignment, Warehouse } from './entities'
|
|
8
8
|
import { resolvePrimaryWarehouseId } from '../lib/primaryWarehousePolicy'
|
|
9
|
+
import {
|
|
10
|
+
WMS_ENRICHER_CACHE_TTL_MS,
|
|
11
|
+
WMS_INVENTORY_CACHE_TAG,
|
|
12
|
+
WMS_WAREHOUSE_CACHE_TAG,
|
|
13
|
+
} from '../lib/enricherCacheTags'
|
|
9
14
|
|
|
10
15
|
type SalesOrderRecord = Record<string, unknown> & { id?: string }
|
|
11
16
|
type CatalogProductRecord = Record<string, unknown> & { id?: string }
|
|
@@ -419,6 +424,13 @@ const salesOrderInventoryEnricher: ResponseEnricher<SalesOrderRecord, SalesOrder
|
|
|
419
424
|
priority: 40,
|
|
420
425
|
timeout: 2000,
|
|
421
426
|
fallback: EMPTY_ENRICHMENT,
|
|
427
|
+
// Reads reservations, balances, the primary warehouse and the explicit
|
|
428
|
+
// assignment, so it depends on both the inventory and the warehouse tags.
|
|
429
|
+
cache: {
|
|
430
|
+
strategy: 'read-through',
|
|
431
|
+
ttl: WMS_ENRICHER_CACHE_TTL_MS,
|
|
432
|
+
tags: [WMS_INVENTORY_CACHE_TAG, WMS_WAREHOUSE_CACHE_TAG],
|
|
433
|
+
},
|
|
422
434
|
|
|
423
435
|
async enrichOne(record, context: EnricherScope) {
|
|
424
436
|
return (await this.enrichMany!([record], context))[0]
|
|
@@ -583,6 +595,13 @@ const catalogProductInventoryEnricher: ResponseEnricher<CatalogProductRecord, Ca
|
|
|
583
595
|
priority: 40,
|
|
584
596
|
timeout: 2000,
|
|
585
597
|
fallback: EMPTY_CATALOG_ENRICHMENT,
|
|
598
|
+
// Reads inventory profiles and balances; also reads catalog variants, so the
|
|
599
|
+
// catalog variant events invalidate the inventory tag too.
|
|
600
|
+
cache: {
|
|
601
|
+
strategy: 'read-through',
|
|
602
|
+
ttl: WMS_ENRICHER_CACHE_TTL_MS,
|
|
603
|
+
tags: [WMS_INVENTORY_CACHE_TAG],
|
|
604
|
+
},
|
|
586
605
|
|
|
587
606
|
async enrichOne(record, context: EnricherScope) {
|
|
588
607
|
return (await this.enrichMany!([record], context))[0]
|
|
@@ -659,6 +678,12 @@ const catalogVariantInventoryEnricher: ResponseEnricher<CatalogVariantRecord, Ca
|
|
|
659
678
|
priority: 40,
|
|
660
679
|
timeout: 2000,
|
|
661
680
|
fallback: EMPTY_CATALOG_ENRICHMENT,
|
|
681
|
+
// Reads inventory profiles and balances for the variant only.
|
|
682
|
+
cache: {
|
|
683
|
+
strategy: 'read-through',
|
|
684
|
+
ttl: WMS_ENRICHER_CACHE_TTL_MS,
|
|
685
|
+
tags: [WMS_INVENTORY_CACHE_TAG],
|
|
686
|
+
},
|
|
662
687
|
|
|
663
688
|
async enrichOne(record, context: EnricherScope) {
|
|
664
689
|
return (await this.enrichMany!([record], context))[0]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache tags shared by the WMS inventory enrichers and the write surfaces that
|
|
3
|
+
* invalidate them. Read side and write side MUST use these constants so the two
|
|
4
|
+
* cannot drift — a tag typo on either side is silent, and the only symptom is
|
|
5
|
+
* inventory that stays stale until the TTL expires.
|
|
6
|
+
*
|
|
7
|
+
* The enricher runner adds `tenant:`, `organization:` and `enricher:` tags of
|
|
8
|
+
* its own, and the cache service prefixes every tag with the current cache
|
|
9
|
+
* tenant, so these are deliberately coarse collection tags rather than
|
|
10
|
+
* per-warehouse ones: `ResponseEnricher.cache.tags` is a static array read from
|
|
11
|
+
* the enricher definition and cannot vary per call. Collection tags
|
|
12
|
+
* over-invalidate slightly; a per-warehouse scheme that the write side could
|
|
13
|
+
* miss would under-invalidate, which is the failure that actually shows wrong
|
|
14
|
+
* stock to a user.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Inventory quantities: balances, reservations, movements and profiles. */
|
|
18
|
+
export const WMS_INVENTORY_CACHE_TAG = 'wms:inventory'
|
|
19
|
+
|
|
20
|
+
/** Warehouse identity and sales-order warehouse assignment. */
|
|
21
|
+
export const WMS_WAREHOUSE_CACHE_TAG = 'wms:warehouse'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Short by design. The tag invalidations below are the primary freshness
|
|
25
|
+
* mechanism; this TTL is the unconditional backstop for anything they miss —
|
|
26
|
+
* a write that failed to resolve the cache service, a cross-process race, or a
|
|
27
|
+
* write surface added later without wiring invalidation.
|
|
28
|
+
*/
|
|
29
|
+
export const WMS_ENRICHER_CACHE_TTL_MS = 30_000
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Drop the cached WMS inventory enrichment for a tenant after a write that
|
|
2
|
+
// changed the data it was derived from.
|
|
3
|
+
//
|
|
4
|
+
// The three WMS inventory enrichers cache their cross-module reads read-through
|
|
5
|
+
// with a short TTL (see `enricherCacheTags.ts`). Every write surface that moves
|
|
6
|
+
// balances, reservations, movements, profiles, warehouses or the sales-order
|
|
7
|
+
// warehouse assignment drops the matching tags here, so a user sees the new
|
|
8
|
+
// stock on the next request rather than after the TTL.
|
|
9
|
+
//
|
|
10
|
+
// The cache service prefixes tags with the *current* cache tenant, which is an
|
|
11
|
+
// AsyncLocalStorage value set on the request path. A subscriber or command side
|
|
12
|
+
// effect running outside that scope would otherwise write its invalidation
|
|
13
|
+
// against the `tenant:global:` prefix and silently miss every entry the request
|
|
14
|
+
// path stored. `runWithCacheTenant` re-enters the right scope, mirroring
|
|
15
|
+
// `directory/subscribers/invalidateOrgScopeCache.ts`.
|
|
16
|
+
|
|
17
|
+
import { runWithCacheTenant } from '@open-mercato/cache'
|
|
18
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
19
|
+
import {
|
|
20
|
+
WMS_INVENTORY_CACHE_TAG,
|
|
21
|
+
WMS_WAREHOUSE_CACHE_TAG,
|
|
22
|
+
} from './enricherCacheTags'
|
|
23
|
+
|
|
24
|
+
const logger = createLogger('wms').child({ component: 'enricher-cache' })
|
|
25
|
+
|
|
26
|
+
type CacheService = {
|
|
27
|
+
deleteByTags(tags: string[]): Promise<number>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type EnricherCacheScope = 'inventory' | 'warehouse' | 'all'
|
|
31
|
+
|
|
32
|
+
type Resolver = { resolve: <T = unknown>(name: string) => T }
|
|
33
|
+
|
|
34
|
+
function resolveCache(container: Resolver | null | undefined): CacheService | null {
|
|
35
|
+
if (!container?.resolve) return null
|
|
36
|
+
for (const name of ['cache', 'cacheService']) {
|
|
37
|
+
try {
|
|
38
|
+
const candidate = container.resolve<CacheService>(name)
|
|
39
|
+
if (candidate && typeof candidate.deleteByTags === 'function') return candidate
|
|
40
|
+
} catch {
|
|
41
|
+
// try the next registration name
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function tagsForScope(scope: EnricherCacheScope): string[] {
|
|
48
|
+
if (scope === 'inventory') return [WMS_INVENTORY_CACHE_TAG]
|
|
49
|
+
if (scope === 'warehouse') return [WMS_WAREHOUSE_CACHE_TAG]
|
|
50
|
+
return [WMS_INVENTORY_CACHE_TAG, WMS_WAREHOUSE_CACHE_TAG]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Best-effort tag invalidation. A failure is logged and swallowed: an
|
|
55
|
+
* invalidation that throws must never fail the write that triggered it, and the
|
|
56
|
+
* enricher TTL bounds how long a missed drop can serve stale data.
|
|
57
|
+
*/
|
|
58
|
+
export async function invalidateWmsInventoryEnricherCache(
|
|
59
|
+
container: Resolver | null | undefined,
|
|
60
|
+
tenantId: string | null | undefined,
|
|
61
|
+
scope: EnricherCacheScope = 'all',
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
if (!tenantId) return
|
|
64
|
+
const cache = resolveCache(container)
|
|
65
|
+
if (!cache) return
|
|
66
|
+
try {
|
|
67
|
+
await runWithCacheTenant(tenantId, () => cache.deleteByTags(tagsForScope(scope)))
|
|
68
|
+
} catch (err) {
|
|
69
|
+
logger.warn('Failed to invalidate WMS inventory enricher cache', { tenantId, scope, err })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Shared handler behind the WMS inventory enricher-cache invalidation
|
|
2
|
+
// subscribers. Each subscriber file binds it to one event pattern, because
|
|
3
|
+
// `metadata.event` takes a single pattern and the matcher is single-segment —
|
|
4
|
+
// `wms.*` does NOT match `wms.inventory_balance.created`, so every event family
|
|
5
|
+
// the enrichers read from is registered explicitly.
|
|
6
|
+
//
|
|
7
|
+
// It lives here rather than under `subscribers/` because the module generator
|
|
8
|
+
// scans that directory recursively and treats every file it finds as a
|
|
9
|
+
// subscriber.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
invalidateWmsInventoryEnricherCache,
|
|
13
|
+
type EnricherCacheScope,
|
|
14
|
+
} from './invalidateInventoryEnricherCache'
|
|
15
|
+
|
|
16
|
+
type SubscriberContext = {
|
|
17
|
+
resolve: <T = unknown>(name: string) => T
|
|
18
|
+
tenantId?: string | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createInventoryEnricherCacheHandler(scope: EnricherCacheScope) {
|
|
22
|
+
return async function handle(payload: unknown, ctx: SubscriberContext): Promise<void> {
|
|
23
|
+
const data = (payload ?? {}) as Record<string, unknown>
|
|
24
|
+
const tenantId =
|
|
25
|
+
typeof data.tenantId === 'string' && data.tenantId.length > 0
|
|
26
|
+
? data.tenantId
|
|
27
|
+
: ctx.tenantId ?? null
|
|
28
|
+
await invalidateWmsInventoryEnricherCache(ctx, tenantId, scope)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
event: 'wms.inventory_balance.*',
|
|
5
|
+
persistent: false,
|
|
6
|
+
id: 'wms:invalidate-enricher-cache-balance',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default createInventoryEnricherCacheHandler('inventory')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
event: 'wms.inventory.*',
|
|
5
|
+
persistent: false,
|
|
6
|
+
id: 'wms:invalidate-enricher-cache-inventory',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default createInventoryEnricherCacheHandler('inventory')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
event: 'wms.inventory_movement.*',
|
|
5
|
+
persistent: false,
|
|
6
|
+
id: 'wms:invalidate-enricher-cache-movement',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default createInventoryEnricherCacheHandler('inventory')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
event: 'wms.inventory_profile.*',
|
|
5
|
+
persistent: false,
|
|
6
|
+
id: 'wms:invalidate-enricher-cache-profile',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default createInventoryEnricherCacheHandler('inventory')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
event: 'wms.inventory_reservation.*',
|
|
5
|
+
persistent: false,
|
|
6
|
+
id: 'wms:invalidate-enricher-cache-reservation',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default createInventoryEnricherCacheHandler('inventory')
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'
|
|
2
|
+
|
|
3
|
+
// The sales module declares `sales.line.*` in its event registry and documents
|
|
4
|
+
// it publicly, but no write surface emits it today — line writes ride the order
|
|
5
|
+
// commands' `sales.order.*`. Registering the pattern anyway costs one ephemeral
|
|
6
|
+
// subscription and means that wiring those events later cannot silently
|
|
7
|
+
// reintroduce the staleness `invalidate-enricher-cache-sales-order.ts` closes.
|
|
8
|
+
export const metadata = {
|
|
9
|
+
event: 'sales.line.*',
|
|
10
|
+
persistent: false,
|
|
11
|
+
id: 'wms:invalidate-enricher-cache-sales-line',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default createInventoryEnricherCacheHandler('warehouse')
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'
|
|
2
|
+
|
|
3
|
+
// `wms.sales-order-inventory` reads the order's lines, and both the required
|
|
4
|
+
// quantity behind `reservationSummary.status` and the variant list behind
|
|
5
|
+
// `stockSummary` come from them. Order lines are written inside the sales order
|
|
6
|
+
// commands, which emit `sales.order.*` — not a WMS event — so without this the
|
|
7
|
+
// enrichment would stay stale for the whole TTL after a line edit.
|
|
8
|
+
//
|
|
9
|
+
// Scope `warehouse` rather than `inventory`: `wms:warehouse` is declared by the
|
|
10
|
+
// sales-order enricher alone, so this drops exactly its entries and leaves the
|
|
11
|
+
// two catalog enrichers' caches intact.
|
|
12
|
+
export const metadata = {
|
|
13
|
+
event: 'sales.order.*',
|
|
14
|
+
persistent: false,
|
|
15
|
+
id: 'wms:invalidate-enricher-cache-sales-order',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default createInventoryEnricherCacheHandler('warehouse')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
event: 'catalog.variant.*',
|
|
5
|
+
persistent: false,
|
|
6
|
+
id: 'wms:invalidate-enricher-cache-variant',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default createInventoryEnricherCacheHandler('inventory')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createInventoryEnricherCacheHandler } from '../lib/inventoryEnricherCacheSubscriber'
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
event: 'wms.warehouse.*',
|
|
5
|
+
persistent: false,
|
|
6
|
+
id: 'wms:invalidate-enricher-cache-warehouse',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default createInventoryEnricherCacheHandler('warehouse')
|