@open-mercato/core 0.10.1-develop.7709.1.046124599c → 0.10.1-develop.7714.1.9dbb8200d8
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 +2 -2
- package/dist/generated/entities/asn/index.js +19 -0
- package/dist/generated/entities/asn/index.js.map +7 -0
- package/dist/generated/entities/putaway_task/index.js +25 -0
- package/dist/generated/entities/putaway_task/index.js.map +7 -0
- package/dist/generated/entities/receiving_line/index.js +21 -0
- package/dist/generated/entities/receiving_line/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +4 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +34 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/communication_channels/lib/sanitize-channel-html.js +22 -3
- package/dist/modules/communication_channels/lib/sanitize-channel-html.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +40 -11
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +13 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +2 -2
- package/dist/modules/wms/acl.js +2 -0
- package/dist/modules/wms/acl.js.map +2 -2
- package/dist/modules/wms/api/asns/[id]/complete/route.js +61 -0
- package/dist/modules/wms/api/asns/[id]/complete/route.js.map +7 -0
- package/dist/modules/wms/api/asns/[id]/receive/route.js +68 -0
- package/dist/modules/wms/api/asns/[id]/receive/route.js.map +7 -0
- package/dist/modules/wms/api/asns/route.js +197 -0
- package/dist/modules/wms/api/asns/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/helpers.js +42 -34
- package/dist/modules/wms/api/inventory/helpers.js.map +2 -2
- package/dist/modules/wms/api/inventory/receive/route.js +1 -1
- package/dist/modules/wms/api/inventory/receive/route.js.map +2 -2
- package/dist/modules/wms/api/listEnrichers.js +23 -0
- package/dist/modules/wms/api/listEnrichers.js.map +2 -2
- package/dist/modules/wms/api/putaway-tasks/[id]/assign/route.js +56 -0
- package/dist/modules/wms/api/putaway-tasks/[id]/assign/route.js.map +7 -0
- package/dist/modules/wms/api/putaway-tasks/[id]/cancel/route.js +56 -0
- package/dist/modules/wms/api/putaway-tasks/[id]/cancel/route.js.map +7 -0
- package/dist/modules/wms/api/putaway-tasks/[id]/complete/route.js +65 -0
- package/dist/modules/wms/api/putaway-tasks/[id]/complete/route.js.map +7 -0
- package/dist/modules/wms/api/putaway-tasks/[id]/start/route.js +56 -0
- package/dist/modules/wms/api/putaway-tasks/[id]/start/route.js.map +7 -0
- package/dist/modules/wms/api/putaway-tasks/create-from-balance/route.js +51 -0
- package/dist/modules/wms/api/putaway-tasks/create-from-balance/route.js.map +7 -0
- package/dist/modules/wms/api/putaway-tasks/route.js +192 -0
- package/dist/modules/wms/api/putaway-tasks/route.js.map +7 -0
- package/dist/modules/wms/api/receiving-lines/route.js +180 -0
- package/dist/modules/wms/api/receiving-lines/route.js.map +7 -0
- package/dist/modules/wms/api/scan/putaway/route.js +104 -0
- package/dist/modules/wms/api/scan/putaway/route.js.map +7 -0
- package/dist/modules/wms/api/scan/receive/route.js +109 -0
- package/dist/modules/wms/api/scan/receive/route.js.map +7 -0
- package/dist/modules/wms/api/scan/resolve-location/route.js +89 -0
- package/dist/modules/wms/api/scan/resolve-location/route.js.map +7 -0
- package/dist/modules/wms/api/scan/resolve-lot/route.js +89 -0
- package/dist/modules/wms/api/scan/resolve-lot/route.js.map +7 -0
- package/dist/modules/wms/backend/wms/asns/[id]/page.js +9 -0
- package/dist/modules/wms/backend/wms/asns/[id]/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/asns/[id]/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/asns/[id]/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/asns/page.js +9 -0
- package/dist/modules/wms/backend/wms/asns/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/asns/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/asns/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/putaway/page.js +9 -0
- package/dist/modules/wms/backend/wms/putaway/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/putaway/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/putaway/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/receiving/page.js +8 -0
- package/dist/modules/wms/backend/wms/receiving/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/receiving/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/receiving/page.meta.js.map +7 -0
- package/dist/modules/wms/ce.js +27 -0
- package/dist/modules/wms/ce.js.map +2 -2
- package/dist/modules/wms/commands/asn.js +1746 -0
- package/dist/modules/wms/commands/asn.js.map +7 -0
- package/dist/modules/wms/commands/index.js +2 -0
- package/dist/modules/wms/commands/index.js.map +2 -2
- package/dist/modules/wms/commands/inventory-actions.js +347 -243
- package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
- package/dist/modules/wms/commands/putaway.js +945 -0
- package/dist/modules/wms/commands/putaway.js.map +7 -0
- package/dist/modules/wms/commands/shared.js +51 -0
- package/dist/modules/wms/commands/shared.js.map +2 -2
- package/dist/modules/wms/components/backend/AssignPutawayTaskDialog.js +278 -0
- package/dist/modules/wms/components/backend/AssignPutawayTaskDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/CompletePutawayTaskDialog.js +327 -0
- package/dist/modules/wms/components/backend/CompletePutawayTaskDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/CreateAsnDialog.js +404 -0
- package/dist/modules/wms/components/backend/CreateAsnDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/InventoryOperationsSection.js +16 -3
- package/dist/modules/wms/components/backend/InventoryOperationsSection.js.map +2 -2
- package/dist/modules/wms/components/backend/ReceiveAsnLineDialog.js +476 -0
- package/dist/modules/wms/components/backend/ReceiveAsnLineDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/ReceiveInventoryDialog.js +2 -2
- package/dist/modules/wms/components/backend/ReceiveInventoryDialog.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsAsnDetailPage.js +524 -0
- package/dist/modules/wms/components/backend/WmsAsnDetailPage.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsAsnsListPage.js +339 -0
- package/dist/modules/wms/components/backend/WmsAsnsListPage.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +5 -6
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js +5 -6
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js +14 -10
- package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsPutawayQueuePage.js +598 -0
- package/dist/modules/wms/components/backend/WmsPutawayQueuePage.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +5 -6
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/asnCompleteOptimisticLock.js +32 -0
- package/dist/modules/wms/components/backend/asnCompleteOptimisticLock.js.map +7 -0
- package/dist/modules/wms/components/backend/asnReceivingLinesLoader.js +36 -0
- package/dist/modules/wms/components/backend/asnReceivingLinesLoader.js.map +7 -0
- package/dist/modules/wms/components/backend/inboundStatusUi.js +106 -0
- package/dist/modules/wms/components/backend/inboundStatusUi.js.map +7 -0
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js +17 -0
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js.map +2 -2
- package/dist/modules/wms/components/backend/putawayQueueOptimisticLock.js +14 -0
- package/dist/modules/wms/components/backend/putawayQueueOptimisticLock.js.map +7 -0
- package/dist/modules/wms/components/backend/useWmsInventoryMutationAccess.js +4 -0
- package/dist/modules/wms/components/backend/useWmsInventoryMutationAccess.js.map +2 -2
- package/dist/modules/wms/data/enrichers.js +117 -12
- package/dist/modules/wms/data/enrichers.js.map +2 -2
- package/dist/modules/wms/data/entities.js +150 -1
- package/dist/modules/wms/data/entities.js.map +2 -2
- package/dist/modules/wms/data/validators.js +214 -0
- package/dist/modules/wms/data/validators.js.map +2 -2
- package/dist/modules/wms/events.js +12 -1
- package/dist/modules/wms/events.js.map +2 -2
- package/dist/modules/wms/extension-points.js +3 -1
- package/dist/modules/wms/extension-points.js.map +2 -2
- package/dist/modules/wms/lib/asnReceiving.js +124 -0
- package/dist/modules/wms/lib/asnReceiving.js.map +7 -0
- package/dist/modules/wms/lib/asnVendor.js +52 -0
- package/dist/modules/wms/lib/asnVendor.js.map +7 -0
- package/dist/modules/wms/lib/flashMutationError.js +4 -0
- package/dist/modules/wms/lib/flashMutationError.js.map +2 -2
- package/dist/modules/wms/lib/inventoryDisplayUi.js +24 -0
- package/dist/modules/wms/lib/inventoryDisplayUi.js.map +2 -2
- package/dist/modules/wms/lib/inventoryPolicy.js +17 -0
- package/dist/modules/wms/lib/inventoryPolicy.js.map +2 -2
- package/dist/modules/wms/lib/loadOperationalDashboard.js +67 -1
- package/dist/modules/wms/lib/loadOperationalDashboard.js.map +2 -2
- package/dist/modules/wms/lib/putaway.js +141 -0
- package/dist/modules/wms/lib/putaway.js.map +7 -0
- package/dist/modules/wms/lib/roleFeatures.js +3 -1
- package/dist/modules/wms/lib/roleFeatures.js.map +2 -2
- package/dist/modules/wms/lib/salesOrderInventoryAutomation.js +99 -1
- package/dist/modules/wms/lib/salesOrderInventoryAutomation.js.map +2 -2
- package/dist/modules/wms/lib/scanResolve.js +147 -0
- package/dist/modules/wms/lib/scanResolve.js.map +7 -0
- package/dist/modules/wms/lib/wmsIntegrationToggles.js +1 -1
- package/dist/modules/wms/lib/wmsIntegrationToggles.js.map +2 -2
- package/dist/modules/wms/migrations/Migration20260812120000.js +43 -0
- package/dist/modules/wms/migrations/Migration20260812120000.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20260812130000.js +30 -0
- package/dist/modules/wms/migrations/Migration20260812130000.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20260822120000.js +17 -0
- package/dist/modules/wms/migrations/Migration20260822120000.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20260822130000.js +17 -0
- package/dist/modules/wms/migrations/Migration20260822130000.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20261029120000.js +13 -0
- package/dist/modules/wms/migrations/Migration20261029120000.js.map +7 -0
- package/dist/modules/wms/search.js +85 -0
- package/dist/modules/wms/search.js.map +2 -2
- package/dist/modules/wms/subscribers/asn-line-received-reevaluate-reservations.js +14 -0
- package/dist/modules/wms/subscribers/asn-line-received-reevaluate-reservations.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-asn.js +12 -0
- package/dist/modules/wms/subscribers/invalidate-enricher-cache-asn.js.map +7 -0
- package/dist/modules/wms/subscribers/procurement-goods-receipt-created.js +163 -0
- package/dist/modules/wms/subscribers/procurement-goods-receipt-created.js.map +7 -0
- package/dist/modules/wms/subscribers/putaway-completed-reevaluate-reservations.js +14 -0
- package/dist/modules/wms/subscribers/putaway-completed-reevaluate-reservations.js.map +7 -0
- package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.client.js +37 -8
- package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.client.js.map +2 -2
- package/generated/entities/asn/index.ts +8 -0
- package/generated/entities/putaway_task/index.ts +11 -0
- package/generated/entities/receiving_line/index.ts +9 -0
- package/generated/entities.ids.generated.ts +4 -1
- package/generated/entity-fields-registry.ts +34 -0
- package/package.json +7 -7
- package/src/modules/auth/i18n/de.json +2 -0
- package/src/modules/auth/i18n/en.json +2 -0
- package/src/modules/auth/i18n/es.json +2 -0
- package/src/modules/auth/i18n/ko.json +2 -0
- package/src/modules/auth/i18n/pl.json +2 -0
- package/src/modules/communication_channels/i18n/de.json +2 -0
- package/src/modules/communication_channels/i18n/en.json +2 -0
- package/src/modules/communication_channels/i18n/es.json +2 -0
- package/src/modules/communication_channels/i18n/ko.json +2 -0
- package/src/modules/communication_channels/i18n/pl.json +2 -0
- package/src/modules/communication_channels/lib/sanitize-channel-html.ts +23 -3
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +44 -12
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +16 -2
- package/src/modules/wms/acl.ts +2 -0
- package/src/modules/wms/api/asns/[id]/complete/route.ts +62 -0
- package/src/modules/wms/api/asns/[id]/receive/route.ts +74 -0
- package/src/modules/wms/api/asns/route.ts +200 -0
- package/src/modules/wms/api/inventory/helpers.ts +57 -34
- package/src/modules/wms/api/inventory/receive/route.ts +2 -1
- package/src/modules/wms/api/listEnrichers.ts +38 -0
- package/src/modules/wms/api/putaway-tasks/[id]/assign/route.ts +56 -0
- package/src/modules/wms/api/putaway-tasks/[id]/cancel/route.ts +56 -0
- package/src/modules/wms/api/putaway-tasks/[id]/complete/route.ts +66 -0
- package/src/modules/wms/api/putaway-tasks/[id]/start/route.ts +56 -0
- package/src/modules/wms/api/putaway-tasks/create-from-balance/route.ts +50 -0
- package/src/modules/wms/api/putaway-tasks/route.ts +199 -0
- package/src/modules/wms/api/receiving-lines/route.ts +180 -0
- package/src/modules/wms/api/scan/putaway/route.ts +108 -0
- package/src/modules/wms/api/scan/receive/route.ts +118 -0
- package/src/modules/wms/api/scan/resolve-location/route.ts +92 -0
- package/src/modules/wms/api/scan/resolve-lot/route.ts +92 -0
- package/src/modules/wms/backend/wms/asns/[id]/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/asns/[id]/page.tsx +5 -0
- package/src/modules/wms/backend/wms/asns/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/asns/page.tsx +5 -0
- package/src/modules/wms/backend/wms/putaway/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/putaway/page.tsx +5 -0
- package/src/modules/wms/backend/wms/receiving/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/receiving/page.tsx +5 -0
- package/src/modules/wms/ce.ts +27 -0
- package/src/modules/wms/commands/asn.ts +2122 -0
- package/src/modules/wms/commands/index.ts +2 -0
- package/src/modules/wms/commands/inventory-actions.ts +413 -243
- package/src/modules/wms/commands/putaway.ts +1169 -0
- package/src/modules/wms/commands/shared.ts +51 -0
- package/src/modules/wms/components/backend/AssignPutawayTaskDialog.tsx +328 -0
- package/src/modules/wms/components/backend/CompletePutawayTaskDialog.tsx +366 -0
- package/src/modules/wms/components/backend/CreateAsnDialog.tsx +427 -0
- package/src/modules/wms/components/backend/InventoryOperationsSection.tsx +20 -2
- package/src/modules/wms/components/backend/ReceiveAsnLineDialog.tsx +535 -0
- package/src/modules/wms/components/backend/ReceiveInventoryDialog.tsx +2 -2
- package/src/modules/wms/components/backend/WmsAsnDetailPage.tsx +647 -0
- package/src/modules/wms/components/backend/WmsAsnsListPage.tsx +407 -0
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +4 -5
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +4 -5
- package/src/modules/wms/components/backend/WmsOperationalDashboardPage.tsx +12 -8
- package/src/modules/wms/components/backend/WmsPutawayQueuePage.tsx +677 -0
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +4 -5
- package/src/modules/wms/components/backend/asnCompleteOptimisticLock.ts +52 -0
- package/src/modules/wms/components/backend/asnReceivingLinesLoader.ts +83 -0
- package/src/modules/wms/components/backend/inboundStatusUi.ts +154 -0
- package/src/modules/wms/components/backend/inventoryMutationLoaders.ts +20 -0
- package/src/modules/wms/components/backend/putawayQueueOptimisticLock.ts +20 -0
- package/src/modules/wms/components/backend/useWmsInventoryMutationAccess.ts +4 -0
- package/src/modules/wms/data/enrichers.ts +175 -13
- package/src/modules/wms/data/entities.ts +155 -0
- package/src/modules/wms/data/validators.ts +256 -0
- package/src/modules/wms/events.ts +11 -0
- package/src/modules/wms/extension-points.ts +2 -0
- package/src/modules/wms/i18n/de.json +205 -3
- package/src/modules/wms/i18n/en.json +205 -3
- package/src/modules/wms/i18n/es.json +205 -3
- package/src/modules/wms/i18n/ko.json +205 -3
- package/src/modules/wms/i18n/pl.json +205 -3
- package/src/modules/wms/lib/asnReceiving.ts +226 -0
- package/src/modules/wms/lib/asnVendor.ts +80 -0
- package/src/modules/wms/lib/flashMutationError.ts +4 -0
- package/src/modules/wms/lib/inventoryDisplayUi.ts +46 -0
- package/src/modules/wms/lib/inventoryPolicy.ts +31 -1
- package/src/modules/wms/lib/loadOperationalDashboard.ts +89 -1
- package/src/modules/wms/lib/putaway.ts +210 -0
- package/src/modules/wms/lib/roleFeatures.ts +2 -0
- package/src/modules/wms/lib/salesOrderInventoryAutomation.ts +150 -1
- package/src/modules/wms/lib/scanResolve.ts +185 -0
- package/src/modules/wms/lib/wmsIntegrationToggles.ts +2 -1
- package/src/modules/wms/migrations/.snapshot-open-mercato.json +1000 -0
- package/src/modules/wms/migrations/Migration20260812120000.ts +43 -0
- package/src/modules/wms/migrations/Migration20260812130000.ts +28 -0
- package/src/modules/wms/migrations/Migration20260822120000.ts +17 -0
- package/src/modules/wms/migrations/Migration20260822130000.ts +15 -0
- package/src/modules/wms/migrations/Migration20261029120000.ts +11 -0
- package/src/modules/wms/search.ts +95 -0
- package/src/modules/wms/subscribers/asn-line-received-reevaluate-reservations.ts +21 -0
- package/src/modules/wms/subscribers/invalidate-enricher-cache-asn.ts +12 -0
- package/src/modules/wms/subscribers/procurement-goods-receipt-created.ts +238 -0
- package/src/modules/wms/subscribers/putaway-completed-reevaluate-reservations.ts +21 -0
- package/src/modules/wms/widgets/injection/sales-order-stock-context/widget.client.tsx +63 -13
|
@@ -1,10 +1,40 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
InventoryLotStatus,
|
|
3
|
+
InventoryStrategy,
|
|
4
|
+
WarehouseLocationType,
|
|
5
|
+
} from '../data/entities'
|
|
2
6
|
|
|
3
7
|
type LotEligibilityInput = {
|
|
4
8
|
status?: InventoryLotStatus | null
|
|
5
9
|
expiresAt?: Date | null
|
|
6
10
|
} | null | undefined
|
|
7
11
|
|
|
12
|
+
/** Staging/dock hold inbound stock; Phase 2 must not treat them as pick/reserve candidates. */
|
|
13
|
+
export const NON_RESERVABLE_LOCATION_TYPES: ReadonlySet<WarehouseLocationType> = new Set([
|
|
14
|
+
'staging',
|
|
15
|
+
'dock',
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
export function isReservableLocationType(type: string | null | undefined): boolean {
|
|
19
|
+
if (typeof type !== 'string' || type.length === 0) return true
|
|
20
|
+
return !NON_RESERVABLE_LOCATION_TYPES.has(type as WarehouseLocationType)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns false when the balance sits on staging/dock. Unpopulated location FKs
|
|
25
|
+
* (string id only) stay eligible so callers that omit `populate: ['location']`
|
|
26
|
+
* do not accidentally empty the candidate set.
|
|
27
|
+
*/
|
|
28
|
+
export function isBalanceLocationReservable(balance: { location?: unknown }): boolean {
|
|
29
|
+
const location = balance.location
|
|
30
|
+
if (!location || typeof location === 'string') return true
|
|
31
|
+
const type =
|
|
32
|
+
typeof (location as { type?: unknown }).type === 'string'
|
|
33
|
+
? (location as { type: string }).type
|
|
34
|
+
: null
|
|
35
|
+
return isReservableLocationType(type)
|
|
36
|
+
}
|
|
37
|
+
|
|
8
38
|
export function isLotEligible(lot: LotEligibilityInput, now: Date = new Date()): boolean {
|
|
9
39
|
if (!lot) return true
|
|
10
40
|
if (lot.status !== 'available') return false
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
3
3
|
import {
|
|
4
|
+
Asn,
|
|
4
5
|
InventoryBalance,
|
|
5
6
|
InventoryLot,
|
|
6
7
|
InventoryMovement,
|
|
@@ -16,6 +17,8 @@ const TREND_DAYS = 7
|
|
|
16
17
|
const MONTHLY_TREND_MONTHS = 6
|
|
17
18
|
const ACTIVITY_LIMIT = 10
|
|
18
19
|
const EXPIRY_CARD_LIMIT = 5
|
|
20
|
+
const UUID_PATTERN =
|
|
21
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
19
22
|
|
|
20
23
|
export type OperationalDashboardScope = {
|
|
21
24
|
organizationId: string
|
|
@@ -52,8 +55,12 @@ export type OperationalDashboardActivityRow = {
|
|
|
52
55
|
variantId: string
|
|
53
56
|
referenceType: string | null
|
|
54
57
|
referenceId: string | null
|
|
58
|
+
/** Human-readable reference (e.g. ASN reference number) — never the idempotency UUID. */
|
|
59
|
+
referenceLabel: string | null
|
|
55
60
|
reason: string | null
|
|
56
61
|
reasonCode: string | null
|
|
62
|
+
/** Movement metadata `source` (e.g. `asn_receive`) for activity label recovery. */
|
|
63
|
+
source: string | null
|
|
57
64
|
locationLabel: string
|
|
58
65
|
performedAt: string
|
|
59
66
|
}
|
|
@@ -340,6 +347,66 @@ async function loadVariantSkus(
|
|
|
340
347
|
return map
|
|
341
348
|
}
|
|
342
349
|
|
|
350
|
+
function readMovementMetadataRecord(
|
|
351
|
+
metadata: InventoryMovement['metadata'],
|
|
352
|
+
): Record<string, unknown> | null {
|
|
353
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) return null
|
|
354
|
+
return metadata as Record<string, unknown>
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function extractActivityMovementSource(
|
|
358
|
+
metadata: InventoryMovement['metadata'],
|
|
359
|
+
): string | null {
|
|
360
|
+
const record = readMovementMetadataRecord(metadata)
|
|
361
|
+
const source = record?.source
|
|
362
|
+
return typeof source === 'string' && source.trim() ? source.trim() : null
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function extractActivityAsnId(metadata: InventoryMovement['metadata']): string | null {
|
|
366
|
+
const record = readMovementMetadataRecord(metadata)
|
|
367
|
+
const asnId = record?.asnId
|
|
368
|
+
if (typeof asnId !== 'string') return null
|
|
369
|
+
const trimmed = asnId.trim()
|
|
370
|
+
return UUID_PATTERN.test(trimmed) ? trimmed : null
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function extractActivityAsnReferenceNumber(
|
|
374
|
+
metadata: InventoryMovement['metadata'],
|
|
375
|
+
): string | null {
|
|
376
|
+
const record = readMovementMetadataRecord(metadata)
|
|
377
|
+
const value = record?.asnReferenceNumber
|
|
378
|
+
if (typeof value !== 'string') return null
|
|
379
|
+
const trimmed = value.trim()
|
|
380
|
+
return trimmed.length > 0 ? trimmed : null
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function loadAsnReferenceNumbers(
|
|
384
|
+
em: EntityManager,
|
|
385
|
+
scope: OperationalDashboardScope,
|
|
386
|
+
asnIds: string[],
|
|
387
|
+
): Promise<Map<string, string>> {
|
|
388
|
+
if (asnIds.length === 0) return new Map()
|
|
389
|
+
const decryptionScope = resolveDecryptionScope(scope)
|
|
390
|
+
const asns = await findWithDecryption(
|
|
391
|
+
em,
|
|
392
|
+
Asn,
|
|
393
|
+
{
|
|
394
|
+
id: { $in: asnIds },
|
|
395
|
+
organizationId: scope.organizationId,
|
|
396
|
+
tenantId: scope.tenantId,
|
|
397
|
+
deletedAt: null,
|
|
398
|
+
},
|
|
399
|
+
undefined,
|
|
400
|
+
decryptionScope,
|
|
401
|
+
)
|
|
402
|
+
const map = new Map<string, string>()
|
|
403
|
+
for (const asn of asns) {
|
|
404
|
+
const label = asn.referenceNumber?.trim()
|
|
405
|
+
if (label) map.set(asn.id, label)
|
|
406
|
+
}
|
|
407
|
+
return map
|
|
408
|
+
}
|
|
409
|
+
|
|
343
410
|
async function loadVariantIdsForProductLevelProfiles(
|
|
344
411
|
em: EntityManager,
|
|
345
412
|
scope: OperationalDashboardScope,
|
|
@@ -785,8 +852,27 @@ export async function loadOperationalDashboard(
|
|
|
785
852
|
const variantIds = Array.from(new Set(activityMovements.map((movement) => movement.catalogVariantId)))
|
|
786
853
|
const variantSkus = await loadVariantSkus(em, scope, variantIds)
|
|
787
854
|
|
|
855
|
+
const asnIdsNeedingLookup = Array.from(
|
|
856
|
+
new Set(
|
|
857
|
+
activityMovements
|
|
858
|
+
.map((movement) => {
|
|
859
|
+
if (extractActivityAsnReferenceNumber(movement.metadata)) return null
|
|
860
|
+
return extractActivityAsnId(movement.metadata)
|
|
861
|
+
})
|
|
862
|
+
.filter((id): id is string => Boolean(id)),
|
|
863
|
+
),
|
|
864
|
+
)
|
|
865
|
+
const asnReferenceNumbers = await loadAsnReferenceNumbers(em, scope, asnIdsNeedingLookup)
|
|
866
|
+
|
|
788
867
|
const recentActivity: OperationalDashboardActivityRow[] = activityMovements.map((movement) => {
|
|
789
868
|
const warehouseLabel = resolveWarehouseLabel(movement.warehouse, movement.warehouse.id)
|
|
869
|
+
const source = extractActivityMovementSource(movement.metadata)
|
|
870
|
+
const metadataAsnReference = extractActivityAsnReferenceNumber(movement.metadata)
|
|
871
|
+
const asnId = extractActivityAsnId(movement.metadata)
|
|
872
|
+
const referenceLabel =
|
|
873
|
+
metadataAsnReference ?? (asnId ? asnReferenceNumbers.get(asnId) ?? null : null)
|
|
874
|
+
const reasonCode =
|
|
875
|
+
movement.reasonCode?.trim() || (source === 'asn_receive' ? 'asn_receive' : null)
|
|
790
876
|
return {
|
|
791
877
|
id: movement.id,
|
|
792
878
|
movementType: movement.type,
|
|
@@ -795,8 +881,10 @@ export async function loadOperationalDashboard(
|
|
|
795
881
|
variantId: movement.catalogVariantId,
|
|
796
882
|
referenceType: movement.referenceType ?? null,
|
|
797
883
|
referenceId: movement.referenceId ?? null,
|
|
884
|
+
referenceLabel,
|
|
798
885
|
reason: movement.reason ?? null,
|
|
799
|
-
reasonCode
|
|
886
|
+
reasonCode,
|
|
887
|
+
source,
|
|
800
888
|
locationLabel: resolveLocationLabel(
|
|
801
889
|
warehouseLabel,
|
|
802
890
|
movement.locationFrom?.code,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createHash } from 'crypto'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
4
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
|
+
import {
|
|
7
|
+
InventoryMovement,
|
|
8
|
+
type PutawayTaskStatus,
|
|
9
|
+
type WarehouseLocationType,
|
|
10
|
+
} from '../data/entities'
|
|
11
|
+
|
|
12
|
+
const TERMINAL: ReadonlySet<PutawayTaskStatus> = new Set(['done', 'cancelled'])
|
|
13
|
+
const FORBIDDEN_PUTAWAY_TARGET_TYPES: ReadonlySet<WarehouseLocationType> = new Set(['staging', 'dock'])
|
|
14
|
+
const PUTAWAY_TASK_STATUSES: ReadonlySet<PutawayTaskStatus> = new Set([
|
|
15
|
+
'open',
|
|
16
|
+
'in_progress',
|
|
17
|
+
'done',
|
|
18
|
+
'cancelled',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse list `status` query: single value or comma-separated (e.g. `open,in_progress`).
|
|
23
|
+
* Returns `$eq` / `$in` filter fragment, or undefined when empty/invalid.
|
|
24
|
+
*/
|
|
25
|
+
export function buildPutawayTaskStatusFilter(
|
|
26
|
+
statusQuery: string | undefined,
|
|
27
|
+
): { $eq: PutawayTaskStatus } | { $in: PutawayTaskStatus[] } | undefined {
|
|
28
|
+
if (typeof statusQuery !== 'string' || statusQuery.trim().length === 0) return undefined
|
|
29
|
+
const statuses = [
|
|
30
|
+
...new Set(
|
|
31
|
+
statusQuery
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((value) => value.trim())
|
|
34
|
+
.filter((value): value is PutawayTaskStatus =>
|
|
35
|
+
PUTAWAY_TASK_STATUSES.has(value as PutawayTaskStatus),
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
]
|
|
39
|
+
if (statuses.length === 0) return undefined
|
|
40
|
+
if (statuses.length === 1) return { $eq: statuses[0]! }
|
|
41
|
+
return { $in: statuses }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function assertPutawayAssignable(status: PutawayTaskStatus): void {
|
|
45
|
+
if (TERMINAL.has(status)) {
|
|
46
|
+
throw new CrudHttpError(409, { error: 'invalid_putaway_state' })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function assertPutawayStartable(status: PutawayTaskStatus): void {
|
|
51
|
+
if (status !== 'open' && status !== 'in_progress') {
|
|
52
|
+
throw new CrudHttpError(409, { error: 'invalid_putaway_state' })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function assertPutawayCompletable(status: PutawayTaskStatus): void {
|
|
57
|
+
if (status !== 'open' && status !== 'in_progress') {
|
|
58
|
+
throw new CrudHttpError(409, { error: 'invalid_putaway_state' })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assertPutawayCancellable(status: PutawayTaskStatus): void {
|
|
63
|
+
if (TERMINAL.has(status)) {
|
|
64
|
+
throw new CrudHttpError(409, { error: 'invalid_putaway_state' })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Soft-delete is archival cleanup for cancelled tasks only.
|
|
70
|
+
* Done is permanent history: soft-deleting it would drop the row from the
|
|
71
|
+
* partial unique index (`deleted_at IS NULL`) while leaving `putaway_key`,
|
|
72
|
+
* letting idempotent ASN receive recreate an open putaway for already-moved stock.
|
|
73
|
+
* Cancel first (clears the key); refuse open/in_progress/done delete.
|
|
74
|
+
*/
|
|
75
|
+
export function assertPutawayDeletable(status: PutawayTaskStatus): void {
|
|
76
|
+
if (status === 'cancelled') return
|
|
77
|
+
if (status === 'done') {
|
|
78
|
+
throw new CrudHttpError(409, { error: 'putaway_delete_done_forbidden' })
|
|
79
|
+
}
|
|
80
|
+
throw new CrudHttpError(409, { error: 'putaway_delete_requires_terminal_status' })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Reject lifecycle fields on putaway-task CRUD before zod strips them. */
|
|
84
|
+
export function assertPutawayLifecycleFieldsForbidden(rawInput: unknown): void {
|
|
85
|
+
if (!rawInput || typeof rawInput !== 'object') return
|
|
86
|
+
const payload = rawInput as Record<string, unknown>
|
|
87
|
+
// Status and assignee changes go through dedicated lifecycle endpoints.
|
|
88
|
+
if ('status' in payload || 'assignedTo' in payload || 'assigned_to' in payload) {
|
|
89
|
+
throw new CrudHttpError(422, { error: 'lifecycle_field_forbidden' })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Complete authorization: managers may complete any task; operators with
|
|
95
|
+
* `wms.adjust_inventory` may complete only when the task is assigned to them.
|
|
96
|
+
*/
|
|
97
|
+
export function assertPutawayCompleteAuthorized(input: {
|
|
98
|
+
canManagePutaway: boolean
|
|
99
|
+
canAdjustInventory: boolean
|
|
100
|
+
actorUserId: string | null | undefined
|
|
101
|
+
assignedTo: string | null | undefined
|
|
102
|
+
}): void {
|
|
103
|
+
if (input.canManagePutaway) return
|
|
104
|
+
if (
|
|
105
|
+
input.canAdjustInventory
|
|
106
|
+
&& typeof input.actorUserId === 'string'
|
|
107
|
+
&& input.actorUserId.length > 0
|
|
108
|
+
&& input.assignedTo === input.actorUserId
|
|
109
|
+
) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
throw new CrudHttpError(403, { error: 'Forbidden' })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Reject completing with more than the open task quantity. */
|
|
116
|
+
export function assertPutawayConfirmedQuantity(
|
|
117
|
+
taskQuantity: number,
|
|
118
|
+
confirmedQuantity: number,
|
|
119
|
+
): void {
|
|
120
|
+
if (!(confirmedQuantity > 0) || !Number.isFinite(confirmedQuantity)) {
|
|
121
|
+
throw new CrudHttpError(422, { error: 'invalid_putaway_quantity' })
|
|
122
|
+
}
|
|
123
|
+
if (confirmedQuantity > taskQuantity + 0.000001) {
|
|
124
|
+
throw new CrudHttpError(422, { error: 'putaway_quantity_exceeds_task' })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function putawayResidualQuantity(taskQuantity: number, confirmedQuantity: number): number {
|
|
129
|
+
const residual = taskQuantity - confirmedQuantity
|
|
130
|
+
return residual > 0.000001 ? residual : 0
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Staging floor for new putaway: on-hand minus reserved/allocated minus open
|
|
135
|
+
* and in-progress putaway commitments (optionally excluding one task being updated).
|
|
136
|
+
*/
|
|
137
|
+
export function computeUncommittedPutawaySourceQuantity(input: {
|
|
138
|
+
quantityOnHand: number
|
|
139
|
+
quantityReserved: number
|
|
140
|
+
quantityAllocated: number
|
|
141
|
+
openPutawayQuantities: number[]
|
|
142
|
+
}): number {
|
|
143
|
+
const available =
|
|
144
|
+
input.quantityOnHand - input.quantityReserved - input.quantityAllocated
|
|
145
|
+
const committed = input.openPutawayQuantities.reduce((sum, qty) => sum + qty, 0)
|
|
146
|
+
return available - committed
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** True when requested qty fits the uncommitted staging floor (epsilon-tolerant). */
|
|
150
|
+
export function hasUncommittedPutawaySourceQuantity(
|
|
151
|
+
remainingAvailable: number,
|
|
152
|
+
requestedQuantity: number,
|
|
153
|
+
): boolean {
|
|
154
|
+
return requestedQuantity <= remainingAvailable + 0.000001
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Prefer an open/in_progress peer whose quantity covers the receipt when
|
|
159
|
+
* recreate would oversubscribe staging stock already queued elsewhere.
|
|
160
|
+
*/
|
|
161
|
+
export function selectCoveringOpenPutawayTask<T extends { quantity: number }>(
|
|
162
|
+
tasks: T[],
|
|
163
|
+
requestedQuantity: number,
|
|
164
|
+
): T | null {
|
|
165
|
+
if (!(requestedQuantity > 0.000001) || tasks.length === 0) return null
|
|
166
|
+
let best: T | null = null
|
|
167
|
+
for (const task of tasks) {
|
|
168
|
+
if (task.quantity + 0.000001 < requestedQuantity) continue
|
|
169
|
+
if (!best || task.quantity < best.quantity) best = task
|
|
170
|
+
}
|
|
171
|
+
return best
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Stable movement reference for putaway complete — task id only (not confirmed qty).
|
|
176
|
+
* Retries with the same or different confirmedQuantity must not open a second
|
|
177
|
+
* stock move under a different idempotency signature; quantity conflicts are
|
|
178
|
+
* detected by looking up this reference before moving.
|
|
179
|
+
*/
|
|
180
|
+
export function buildPutawayCompleteReferenceId(taskId: string): string {
|
|
181
|
+
const hex = createHash('sha256').update(`wms:putaway-complete:${taskId}`).digest('hex')
|
|
182
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function assertPutawayTargetLocationType(type: WarehouseLocationType): void {
|
|
186
|
+
if (FORBIDDEN_PUTAWAY_TARGET_TYPES.has(type)) {
|
|
187
|
+
throw new CrudHttpError(422, { error: 'invalid_putaway_target_location' })
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function resolveLotIdFromMovement(
|
|
192
|
+
em: EntityManager,
|
|
193
|
+
ctx: CommandRuntimeContext,
|
|
194
|
+
movementId: string,
|
|
195
|
+
): Promise<string | null> {
|
|
196
|
+
const tenantId = ctx.auth?.tenantId ?? null
|
|
197
|
+
const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
198
|
+
if (!tenantId || !organizationId) return null
|
|
199
|
+
const movement = await findOneWithDecryption(
|
|
200
|
+
em,
|
|
201
|
+
InventoryMovement,
|
|
202
|
+
{ id: movementId, deletedAt: null },
|
|
203
|
+
undefined,
|
|
204
|
+
{ tenantId, organizationId },
|
|
205
|
+
)
|
|
206
|
+
if (!movement) return null
|
|
207
|
+
const lotRaw = movement.lot ?? null
|
|
208
|
+
if (!lotRaw) return null
|
|
209
|
+
return typeof lotRaw === 'string' ? lotRaw : lotRaw.id
|
|
210
|
+
}
|
|
@@ -3,11 +3,13 @@ import type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib
|
|
|
3
3
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
4
4
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
5
|
import type { FeatureTogglesService } from '@open-mercato/core/modules/feature_toggles/lib/feature-flag-check'
|
|
6
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
6
7
|
import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
|
|
7
8
|
import { E } from '#generated/entities.ids.generated'
|
|
8
9
|
import { InventoryBalance, InventoryReservation } from '../data/entities'
|
|
9
10
|
import { emitWmsEvent } from '../events'
|
|
10
11
|
import { loadExplicitWarehouseIdForOrder } from './salesOrderWarehouseAssignment'
|
|
12
|
+
import { isBalanceLocationReservable } from './inventoryPolicy'
|
|
11
13
|
import {
|
|
12
14
|
resolvePrimaryWarehouseId,
|
|
13
15
|
sortWarehouseAvailabilityForReservation,
|
|
@@ -15,6 +17,8 @@ import {
|
|
|
15
17
|
} from './primaryWarehousePolicy'
|
|
16
18
|
import { resolveWmsIntegrationToggleEnabled } from './wmsIntegrationToggles'
|
|
17
19
|
|
|
20
|
+
const logger = createLogger('wms')
|
|
21
|
+
|
|
18
22
|
type EventContext = {
|
|
19
23
|
resolve: <T = unknown>(name: string) => T
|
|
20
24
|
}
|
|
@@ -49,10 +53,29 @@ type SalesOrderRow = {
|
|
|
49
53
|
id?: string
|
|
50
54
|
order_number?: string | null
|
|
51
55
|
status?: string | null
|
|
56
|
+
fulfillment_status?: string | null
|
|
52
57
|
tenant_id?: string | null
|
|
53
58
|
organization_id?: string | null
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/** Only confirmed (non-fulfilled / non-cancelled) orders may receive re-reservations. */
|
|
62
|
+
export function isReservableOrderStatus(
|
|
63
|
+
status?: string | null,
|
|
64
|
+
fulfillmentStatus?: string | null,
|
|
65
|
+
): boolean {
|
|
66
|
+
const normalized = (status ?? '').trim().toLowerCase()
|
|
67
|
+
if (normalized !== 'confirmed') return false
|
|
68
|
+
const fulfillment = (fulfillmentStatus ?? '').trim().toLowerCase()
|
|
69
|
+
if (
|
|
70
|
+
fulfillment === 'fulfilled' ||
|
|
71
|
+
fulfillment === 'cancelled' ||
|
|
72
|
+
fulfillment === 'canceled'
|
|
73
|
+
) {
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
type SalesOrderLineRow = {
|
|
57
80
|
id?: string
|
|
58
81
|
kind?: string | null
|
|
@@ -197,7 +220,7 @@ async function loadBalances(
|
|
|
197
220
|
organizationId: scope.organizationId,
|
|
198
221
|
deletedAt: null,
|
|
199
222
|
},
|
|
200
|
-
|
|
223
|
+
{ populate: ['location'] },
|
|
201
224
|
scope,
|
|
202
225
|
)
|
|
203
226
|
}
|
|
@@ -232,6 +255,10 @@ function buildWarehouseAvailability(
|
|
|
232
255
|
): Map<string, WarehouseAvailability[]> {
|
|
233
256
|
const byVariant = new Map<string, Map<string, number>>()
|
|
234
257
|
for (const balance of balances) {
|
|
258
|
+
// Staging/dock are inbound holding locations — availability for sales
|
|
259
|
+
// reservation must ignore them so ASN receive re-eval does not claim
|
|
260
|
+
// stock that putaway still needs to move into storage.
|
|
261
|
+
if (!isBalanceLocationReservable(balance)) continue
|
|
235
262
|
const warehouseId = getWarehouseId(balance)
|
|
236
263
|
if (!warehouseId) continue
|
|
237
264
|
const available =
|
|
@@ -364,6 +391,128 @@ export async function reserveInventoryForConfirmedOrder(
|
|
|
364
391
|
}
|
|
365
392
|
}
|
|
366
393
|
|
|
394
|
+
type StockIncreasePayload = {
|
|
395
|
+
catalogVariantId?: string | null
|
|
396
|
+
tenantId?: string | null
|
|
397
|
+
organizationId?: string | null
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Re-runs reservation automation for sales orders that include the received
|
|
402
|
+
* catalog variant. Idempotent: reserveInventoryForConfirmedOrder only fills
|
|
403
|
+
* remaining shortfall. No-ops when the sales integration toggle is off or
|
|
404
|
+
* sales query peers are unavailable.
|
|
405
|
+
*
|
|
406
|
+
* Candidate discovery starts from currently reservable (confirmed,
|
|
407
|
+
* non-terminal fulfillment) sales orders — not lifetime `sales_order_line`
|
|
408
|
+
* history for the variant — then keeps only orders that still reference the
|
|
409
|
+
* received catalog variant. That bounds work to the open order set.
|
|
410
|
+
*/
|
|
411
|
+
const REEVAL_ORDER_PAGE_SIZE = 500
|
|
412
|
+
|
|
413
|
+
export async function reevaluateReservationsAfterStockIncrease(
|
|
414
|
+
payload: StockIncreasePayload,
|
|
415
|
+
ctx: EventContext,
|
|
416
|
+
): Promise<void> {
|
|
417
|
+
if (!payload.catalogVariantId || !payload.tenantId || !payload.organizationId) return
|
|
418
|
+
if (!(await isInventoryAutomationEnabled(ctx, payload.tenantId))) return
|
|
419
|
+
|
|
420
|
+
const scope: Scope = {
|
|
421
|
+
tenantId: payload.tenantId,
|
|
422
|
+
organizationId: payload.organizationId,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let queryEngine: QueryEngine
|
|
426
|
+
try {
|
|
427
|
+
queryEngine = ctx.resolve<QueryEngine>('queryEngine')
|
|
428
|
+
} catch {
|
|
429
|
+
// Sales module / query engine absent or unavailable — degrade gracefully.
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Page reservable orders, then intersect with lines for this variant.
|
|
434
|
+
// Process each page immediately (bounded memory); reserveInventoryForConfirmedOrder is idempotent.
|
|
435
|
+
let page = 1
|
|
436
|
+
|
|
437
|
+
for (;;) {
|
|
438
|
+
let orderItems: SalesOrderRow[]
|
|
439
|
+
try {
|
|
440
|
+
const result = await queryEngine.query<SalesOrderRow>(E.sales.sales_order, {
|
|
441
|
+
tenantId: scope.tenantId,
|
|
442
|
+
organizationId: scope.organizationId,
|
|
443
|
+
filters: { status: { $eq: 'confirmed' } },
|
|
444
|
+
fields: ['id', 'status', 'fulfillment_status'],
|
|
445
|
+
page: { page, pageSize: REEVAL_ORDER_PAGE_SIZE },
|
|
446
|
+
})
|
|
447
|
+
orderItems = result.items
|
|
448
|
+
} catch {
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (orderItems.length === 0) break
|
|
453
|
+
|
|
454
|
+
const reservableOrderIds = orderItems
|
|
455
|
+
.filter((order) => isReservableOrderStatus(order.status, order.fulfillment_status))
|
|
456
|
+
.map((order) => order.id)
|
|
457
|
+
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
458
|
+
|
|
459
|
+
if (reservableOrderIds.length > 0) {
|
|
460
|
+
const matchingOrderIds = new Set<string>()
|
|
461
|
+
let linePage = 1
|
|
462
|
+
try {
|
|
463
|
+
for (;;) {
|
|
464
|
+
const lines = await queryEngine.query<SalesOrderLineRow & { order_id?: string | null }>(
|
|
465
|
+
E.sales.sales_order_line,
|
|
466
|
+
{
|
|
467
|
+
tenantId: scope.tenantId,
|
|
468
|
+
organizationId: scope.organizationId,
|
|
469
|
+
filters: {
|
|
470
|
+
product_variant_id: { $eq: payload.catalogVariantId },
|
|
471
|
+
order_id: { $in: reservableOrderIds },
|
|
472
|
+
},
|
|
473
|
+
fields: ['id', 'order_id', 'product_variant_id'],
|
|
474
|
+
page: { page: linePage, pageSize: REEVAL_ORDER_PAGE_SIZE },
|
|
475
|
+
},
|
|
476
|
+
)
|
|
477
|
+
for (const line of lines.items) {
|
|
478
|
+
if (typeof line.order_id === 'string' && line.order_id.length > 0) {
|
|
479
|
+
matchingOrderIds.add(line.order_id)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (lines.items.length < REEVAL_ORDER_PAGE_SIZE) break
|
|
483
|
+
linePage += 1
|
|
484
|
+
}
|
|
485
|
+
} catch (error) {
|
|
486
|
+
// Transient peer failure on one page must not abort later order pages.
|
|
487
|
+
logger.warn('Reservation re-eval line lookup failed; continuing next page', {
|
|
488
|
+
catalogVariantId: payload.catalogVariantId,
|
|
489
|
+
page,
|
|
490
|
+
orderCount: reservableOrderIds.length,
|
|
491
|
+
error: error instanceof Error ? error.message : String(error),
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for (const orderId of matchingOrderIds) {
|
|
496
|
+
try {
|
|
497
|
+
await reserveInventoryForConfirmedOrder(
|
|
498
|
+
{
|
|
499
|
+
orderId,
|
|
500
|
+
tenantId: scope.tenantId,
|
|
501
|
+
organizationId: scope.organizationId,
|
|
502
|
+
},
|
|
503
|
+
ctx,
|
|
504
|
+
)
|
|
505
|
+
} catch {
|
|
506
|
+
// Keep processing remaining orders; a single peer failure must not abort the batch.
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (orderItems.length < REEVAL_ORDER_PAGE_SIZE) break
|
|
512
|
+
page += 1
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
367
516
|
export async function releaseInventoryForCancelledOrder(
|
|
368
517
|
payload: SalesOrderLifecyclePayload,
|
|
369
518
|
ctx: EventContext,
|