@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
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
} from '../data/validators'
|
|
64
64
|
import {
|
|
65
65
|
evaluateLowStock,
|
|
66
|
+
isBalanceLocationReservable,
|
|
66
67
|
isLotEligible,
|
|
67
68
|
resolveReservationStrategyFromProfile,
|
|
68
69
|
sortBucketsForStrategy,
|
|
@@ -522,11 +523,16 @@ async function listCandidateBalances(
|
|
|
522
523
|
},
|
|
523
524
|
scope,
|
|
524
525
|
)
|
|
526
|
+
const reservable: InventoryBalance[] = []
|
|
525
527
|
for (const balance of balances) {
|
|
526
528
|
ensureTenantScope(ctx, balance.tenantId)
|
|
527
529
|
ensureOrganizationScope(ctx, balance.organizationId)
|
|
530
|
+
// Staging/dock are inbound holding locations — not pick/reserve candidates
|
|
531
|
+
// (Phase 2: ASN receive must not reserve staging stock and block putaway).
|
|
532
|
+
if (!isBalanceLocationReservable(balance)) continue
|
|
533
|
+
reservable.push(balance)
|
|
528
534
|
}
|
|
529
|
-
return
|
|
535
|
+
return reservable
|
|
530
536
|
}
|
|
531
537
|
|
|
532
538
|
async function listBalancesForVariant(
|
|
@@ -1411,12 +1417,28 @@ const adjustInventoryCommand: CommandHandler<InventoryAdjustInput, { movementId:
|
|
|
1411
1417
|
if (delta < 0 && getAvailableQuantity(balance) < Math.abs(delta) - 0.000001) {
|
|
1412
1418
|
throw new CrudHttpError(409, { error: 'insufficient_stock' })
|
|
1413
1419
|
}
|
|
1420
|
+
// Persist movement before bumping balance so a unique-constraint replay cannot
|
|
1421
|
+
// leave a skewed on-hand and still report idempotentReplay: false.
|
|
1422
|
+
const { movement, idempotentReplay } = await persistMovementWithIdempotency(trx, scope, movementInput)
|
|
1423
|
+
if (idempotentReplay) {
|
|
1424
|
+
return {
|
|
1425
|
+
movementId: movement.id,
|
|
1426
|
+
warehouseId: input.warehouseId,
|
|
1427
|
+
catalogVariantId: input.catalogVariantId,
|
|
1428
|
+
quantity: delta,
|
|
1429
|
+
tenantId: scope.tenantId,
|
|
1430
|
+
organizationId: scope.organizationId,
|
|
1431
|
+
movementEntity: movement,
|
|
1432
|
+
balanceEntity: balance,
|
|
1433
|
+
balanceAction: ('updated' as const),
|
|
1434
|
+
idempotentReplay: true,
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1414
1437
|
setNumeric(
|
|
1415
1438
|
balance as unknown as Record<string, unknown>,
|
|
1416
1439
|
'quantityOnHand',
|
|
1417
1440
|
toNumber(balance.quantityOnHand) + delta,
|
|
1418
1441
|
)
|
|
1419
|
-
const { movement } = await persistMovementWithIdempotency(trx, scope, movementInput)
|
|
1420
1442
|
await trx.flush()
|
|
1421
1443
|
return {
|
|
1422
1444
|
movementId: movement.id,
|
|
@@ -1470,6 +1492,171 @@ const adjustInventoryCommand: CommandHandler<InventoryAdjustInput, { movementId:
|
|
|
1470
1492
|
}),
|
|
1471
1493
|
}
|
|
1472
1494
|
|
|
1495
|
+
/**
|
|
1496
|
+
* Core receipt mutation for an already-open transaction.
|
|
1497
|
+
* Used by `wms.inventory.receive` and by ASN QC-pass receive so stock + line
|
|
1498
|
+
* qty + putaway can share one transactional boundary (nested commandBus
|
|
1499
|
+
* receives cannot participate in the ASN row lock).
|
|
1500
|
+
*/
|
|
1501
|
+
export type InventoryReceiveMutationResult = {
|
|
1502
|
+
movementId: string
|
|
1503
|
+
warehouseId: string
|
|
1504
|
+
locationId: string
|
|
1505
|
+
catalogVariantId: string
|
|
1506
|
+
quantity: number
|
|
1507
|
+
lotId: string | null
|
|
1508
|
+
tenantId: string
|
|
1509
|
+
organizationId: string
|
|
1510
|
+
movementEntity: InventoryMovement
|
|
1511
|
+
balanceEntity: InventoryBalance
|
|
1512
|
+
balanceAction: 'created' | 'updated'
|
|
1513
|
+
idempotentReplay: boolean
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
export async function applyInventoryReceiveInTransaction(
|
|
1517
|
+
trx: EntityManager,
|
|
1518
|
+
ctx: CommandRuntimeContext,
|
|
1519
|
+
input: InventoryReceiveInput,
|
|
1520
|
+
): Promise<InventoryReceiveMutationResult> {
|
|
1521
|
+
const scope = resolveScope(ctx, input)
|
|
1522
|
+
await requireWarehouse(trx, ctx, input.warehouseId, scope)
|
|
1523
|
+
const location = await requireLocation(trx, ctx, input.locationId, scope)
|
|
1524
|
+
const locationWarehouseId = typeof location.warehouse === 'string' ? location.warehouse : location.warehouse.id
|
|
1525
|
+
if (locationWarehouseId !== input.warehouseId) {
|
|
1526
|
+
throw new CrudHttpError(422, { error: 'invalid_location' })
|
|
1527
|
+
}
|
|
1528
|
+
const profile = await loadProfileForVariant(trx, ctx, input.catalogVariantId, scope)
|
|
1529
|
+
const resolvedLotId = await resolveReceiveLotId(trx, ctx, scope, input)
|
|
1530
|
+
enforceInventoryTrackingRequirements(profile, {
|
|
1531
|
+
lotId: resolvedLotId ?? null,
|
|
1532
|
+
serialNumber: input.serialNumber ?? null,
|
|
1533
|
+
})
|
|
1534
|
+
const { balance, created: balanceWasNew } = await upsertBalanceBucket(trx, scope, {
|
|
1535
|
+
warehouseId: input.warehouseId,
|
|
1536
|
+
locationId: input.locationId,
|
|
1537
|
+
catalogVariantId: input.catalogVariantId,
|
|
1538
|
+
lotId: resolvedLotId,
|
|
1539
|
+
serialNumber: input.serialNumber,
|
|
1540
|
+
})
|
|
1541
|
+
const receivedAt = input.receivedAt ?? input.performedAt ?? new Date()
|
|
1542
|
+
const performedAt = input.performedAt ?? receivedAt
|
|
1543
|
+
const movementInput: MovementMutationInput = {
|
|
1544
|
+
warehouseId: input.warehouseId,
|
|
1545
|
+
locationToId: input.locationId,
|
|
1546
|
+
catalogVariantId: input.catalogVariantId,
|
|
1547
|
+
lotId: resolvedLotId ?? null,
|
|
1548
|
+
serialNumber: input.serialNumber ?? null,
|
|
1549
|
+
quantity: input.quantity,
|
|
1550
|
+
type: 'receipt',
|
|
1551
|
+
referenceType: input.referenceType,
|
|
1552
|
+
referenceId: input.referenceId,
|
|
1553
|
+
performedBy: input.performedBy,
|
|
1554
|
+
performedAt,
|
|
1555
|
+
receivedAt,
|
|
1556
|
+
reason: input.reason,
|
|
1557
|
+
reasonCode: input.reasonCode ?? null,
|
|
1558
|
+
metadata: input.metadata ?? null,
|
|
1559
|
+
}
|
|
1560
|
+
const idempotencyKey = buildMovementIdempotencyKey({
|
|
1561
|
+
referenceType: movementInput.referenceType,
|
|
1562
|
+
referenceId: movementInput.referenceId,
|
|
1563
|
+
type: movementInput.type,
|
|
1564
|
+
warehouseId: movementInput.warehouseId,
|
|
1565
|
+
locationFromId: movementInput.locationFromId,
|
|
1566
|
+
locationToId: movementInput.locationToId,
|
|
1567
|
+
catalogVariantId: movementInput.catalogVariantId,
|
|
1568
|
+
lotId: movementInput.lotId,
|
|
1569
|
+
serialNumber: movementInput.serialNumber,
|
|
1570
|
+
quantity: movementInput.quantity,
|
|
1571
|
+
})
|
|
1572
|
+
const existingMovement = await findExistingMovementByIdempotencyKey(trx, scope, idempotencyKey)
|
|
1573
|
+
if (existingMovement) {
|
|
1574
|
+
const existingLotRaw = existingMovement.lot ?? null
|
|
1575
|
+
const existingLotId =
|
|
1576
|
+
typeof existingLotRaw === 'string' ? existingLotRaw : existingLotRaw?.id ?? resolvedLotId ?? null
|
|
1577
|
+
return {
|
|
1578
|
+
movementId: existingMovement.id,
|
|
1579
|
+
warehouseId: input.warehouseId,
|
|
1580
|
+
locationId: input.locationId,
|
|
1581
|
+
catalogVariantId: input.catalogVariantId,
|
|
1582
|
+
quantity: input.quantity,
|
|
1583
|
+
lotId: existingLotId,
|
|
1584
|
+
tenantId: scope.tenantId,
|
|
1585
|
+
organizationId: scope.organizationId,
|
|
1586
|
+
movementEntity: existingMovement,
|
|
1587
|
+
balanceEntity: balance,
|
|
1588
|
+
balanceAction: 'updated' as const,
|
|
1589
|
+
idempotentReplay: true,
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
// Persist movement before bumping balance so a unique-constraint replay cannot
|
|
1593
|
+
// leave an inflated on-hand and still report idempotentReplay: false.
|
|
1594
|
+
const { movement, idempotentReplay } = await persistMovementWithIdempotency(trx, scope, movementInput)
|
|
1595
|
+
if (idempotentReplay) {
|
|
1596
|
+
return {
|
|
1597
|
+
movementId: movement.id,
|
|
1598
|
+
warehouseId: input.warehouseId,
|
|
1599
|
+
locationId: input.locationId,
|
|
1600
|
+
catalogVariantId: input.catalogVariantId,
|
|
1601
|
+
quantity: input.quantity,
|
|
1602
|
+
lotId: resolvedLotId ?? null,
|
|
1603
|
+
tenantId: scope.tenantId,
|
|
1604
|
+
organizationId: scope.organizationId,
|
|
1605
|
+
movementEntity: movement,
|
|
1606
|
+
balanceEntity: balance,
|
|
1607
|
+
balanceAction: 'updated' as const,
|
|
1608
|
+
idempotentReplay: true,
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
setNumeric(
|
|
1612
|
+
balance as unknown as Record<string, unknown>,
|
|
1613
|
+
'quantityOnHand',
|
|
1614
|
+
toNumber(balance.quantityOnHand) + input.quantity,
|
|
1615
|
+
)
|
|
1616
|
+
await trx.flush()
|
|
1617
|
+
return {
|
|
1618
|
+
movementId: movement.id,
|
|
1619
|
+
warehouseId: input.warehouseId,
|
|
1620
|
+
locationId: input.locationId,
|
|
1621
|
+
catalogVariantId: input.catalogVariantId,
|
|
1622
|
+
quantity: input.quantity,
|
|
1623
|
+
lotId: resolvedLotId ?? null,
|
|
1624
|
+
tenantId: scope.tenantId,
|
|
1625
|
+
organizationId: scope.organizationId,
|
|
1626
|
+
movementEntity: movement,
|
|
1627
|
+
balanceEntity: balance,
|
|
1628
|
+
balanceAction: balanceWasNew ? ('created' as const) : ('updated' as const),
|
|
1629
|
+
idempotentReplay: false,
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
export async function emitInventoryReceiveSideEffects(
|
|
1634
|
+
ctx: CommandRuntimeContext,
|
|
1635
|
+
result: InventoryReceiveMutationResult,
|
|
1636
|
+
): Promise<void> {
|
|
1637
|
+
if (result.idempotentReplay) return
|
|
1638
|
+
await emitInventorySideEffects(ctx, {
|
|
1639
|
+
movements: [{ entity: result.movementEntity, action: 'created' }],
|
|
1640
|
+
balances: [{ entity: result.balanceEntity, action: result.balanceAction }],
|
|
1641
|
+
})
|
|
1642
|
+
void emitWmsEvent('wms.inventory.received', {
|
|
1643
|
+
id: result.movementId,
|
|
1644
|
+
movementId: result.movementId,
|
|
1645
|
+
warehouseId: result.warehouseId,
|
|
1646
|
+
locationId: result.locationId,
|
|
1647
|
+
catalogVariantId: result.catalogVariantId,
|
|
1648
|
+
quantity: toNumericString(result.quantity),
|
|
1649
|
+
tenantId: result.tenantId,
|
|
1650
|
+
organizationId: result.organizationId,
|
|
1651
|
+
}).catch(() => undefined)
|
|
1652
|
+
void emitLowStockEventIfNeeded(
|
|
1653
|
+
resolveEm(ctx),
|
|
1654
|
+
ctx,
|
|
1655
|
+
{ tenantId: result.tenantId, organizationId: result.organizationId },
|
|
1656
|
+
result.catalogVariantId,
|
|
1657
|
+
).catch(() => undefined)
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1473
1660
|
const receiveInventoryCommand: CommandHandler<InventoryReceiveInput, { movementId: string }> = {
|
|
1474
1661
|
id: 'wms.inventory.receive',
|
|
1475
1662
|
// See "WMS Inventory Mutation Commands — Undo Policy" at top of file.
|
|
@@ -1479,116 +1666,10 @@ const receiveInventoryCommand: CommandHandler<InventoryReceiveInput, { movementI
|
|
|
1479
1666
|
ensureTenantScope(ctx, input.tenantId)
|
|
1480
1667
|
ensureOrganizationScope(ctx, input.organizationId)
|
|
1481
1668
|
const em = resolveEm(ctx)
|
|
1482
|
-
const result = await runInTransaction(em, async (trx) =>
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
const locationWarehouseId = typeof location.warehouse === 'string' ? location.warehouse : location.warehouse.id
|
|
1487
|
-
if (locationWarehouseId !== input.warehouseId) {
|
|
1488
|
-
throw new CrudHttpError(422, { error: 'invalid_location' })
|
|
1489
|
-
}
|
|
1490
|
-
const profile = await loadProfileForVariant(trx, ctx, input.catalogVariantId, scope)
|
|
1491
|
-
const resolvedLotId = await resolveReceiveLotId(trx, ctx, scope, input)
|
|
1492
|
-
enforceInventoryTrackingRequirements(profile, {
|
|
1493
|
-
lotId: resolvedLotId ?? null,
|
|
1494
|
-
serialNumber: input.serialNumber ?? null,
|
|
1495
|
-
})
|
|
1496
|
-
const { balance, created: balanceWasNew } = await upsertBalanceBucket(trx, scope, {
|
|
1497
|
-
warehouseId: input.warehouseId,
|
|
1498
|
-
locationId: input.locationId,
|
|
1499
|
-
catalogVariantId: input.catalogVariantId,
|
|
1500
|
-
lotId: resolvedLotId,
|
|
1501
|
-
serialNumber: input.serialNumber,
|
|
1502
|
-
})
|
|
1503
|
-
const receivedAt = input.receivedAt ?? input.performedAt ?? new Date()
|
|
1504
|
-
const performedAt = input.performedAt ?? receivedAt
|
|
1505
|
-
const movementInput: MovementMutationInput = {
|
|
1506
|
-
warehouseId: input.warehouseId,
|
|
1507
|
-
locationToId: input.locationId,
|
|
1508
|
-
catalogVariantId: input.catalogVariantId,
|
|
1509
|
-
lotId: resolvedLotId ?? null,
|
|
1510
|
-
serialNumber: input.serialNumber ?? null,
|
|
1511
|
-
quantity: input.quantity,
|
|
1512
|
-
type: 'receipt',
|
|
1513
|
-
referenceType: input.referenceType,
|
|
1514
|
-
referenceId: input.referenceId,
|
|
1515
|
-
performedBy: input.performedBy,
|
|
1516
|
-
performedAt,
|
|
1517
|
-
receivedAt,
|
|
1518
|
-
reason: input.reason,
|
|
1519
|
-
metadata: input.metadata ?? null,
|
|
1520
|
-
}
|
|
1521
|
-
const idempotencyKey = buildMovementIdempotencyKey({
|
|
1522
|
-
referenceType: movementInput.referenceType,
|
|
1523
|
-
referenceId: movementInput.referenceId,
|
|
1524
|
-
type: movementInput.type,
|
|
1525
|
-
warehouseId: movementInput.warehouseId,
|
|
1526
|
-
locationFromId: movementInput.locationFromId,
|
|
1527
|
-
locationToId: movementInput.locationToId,
|
|
1528
|
-
catalogVariantId: movementInput.catalogVariantId,
|
|
1529
|
-
lotId: movementInput.lotId,
|
|
1530
|
-
serialNumber: movementInput.serialNumber,
|
|
1531
|
-
quantity: movementInput.quantity,
|
|
1532
|
-
})
|
|
1533
|
-
const existingMovement = await findExistingMovementByIdempotencyKey(trx, scope, idempotencyKey)
|
|
1534
|
-
if (existingMovement) {
|
|
1535
|
-
return {
|
|
1536
|
-
movementId: existingMovement.id,
|
|
1537
|
-
warehouseId: input.warehouseId,
|
|
1538
|
-
locationId: input.locationId,
|
|
1539
|
-
catalogVariantId: input.catalogVariantId,
|
|
1540
|
-
quantity: input.quantity,
|
|
1541
|
-
tenantId: scope.tenantId,
|
|
1542
|
-
organizationId: scope.organizationId,
|
|
1543
|
-
movementEntity: existingMovement,
|
|
1544
|
-
balanceEntity: balance,
|
|
1545
|
-
balanceAction: ('updated' as const),
|
|
1546
|
-
idempotentReplay: true,
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
setNumeric(
|
|
1550
|
-
balance as unknown as Record<string, unknown>,
|
|
1551
|
-
'quantityOnHand',
|
|
1552
|
-
toNumber(balance.quantityOnHand) + input.quantity,
|
|
1553
|
-
)
|
|
1554
|
-
const { movement } = await persistMovementWithIdempotency(trx, scope, movementInput)
|
|
1555
|
-
await trx.flush()
|
|
1556
|
-
return {
|
|
1557
|
-
movementId: movement.id,
|
|
1558
|
-
warehouseId: input.warehouseId,
|
|
1559
|
-
locationId: input.locationId,
|
|
1560
|
-
catalogVariantId: input.catalogVariantId,
|
|
1561
|
-
quantity: input.quantity,
|
|
1562
|
-
tenantId: scope.tenantId,
|
|
1563
|
-
organizationId: scope.organizationId,
|
|
1564
|
-
movementEntity: movement,
|
|
1565
|
-
balanceEntity: balance,
|
|
1566
|
-
balanceAction: balanceWasNew ? ('created' as const) : ('updated' as const),
|
|
1567
|
-
idempotentReplay: false,
|
|
1568
|
-
}
|
|
1569
|
-
})
|
|
1570
|
-
if (!result.idempotentReplay) {
|
|
1571
|
-
await emitInventorySideEffects(ctx, {
|
|
1572
|
-
movements: [{ entity: result.movementEntity, action: 'created' }],
|
|
1573
|
-
balances: [{ entity: result.balanceEntity, action: result.balanceAction }],
|
|
1574
|
-
})
|
|
1575
|
-
void emitWmsEvent('wms.inventory.received', {
|
|
1576
|
-
id: result.movementId,
|
|
1577
|
-
movementId: result.movementId,
|
|
1578
|
-
warehouseId: result.warehouseId,
|
|
1579
|
-
locationId: result.locationId,
|
|
1580
|
-
catalogVariantId: result.catalogVariantId,
|
|
1581
|
-
quantity: toNumericString(result.quantity),
|
|
1582
|
-
tenantId: result.tenantId,
|
|
1583
|
-
organizationId: result.organizationId,
|
|
1584
|
-
}).catch(() => undefined)
|
|
1585
|
-
void emitLowStockEventIfNeeded(
|
|
1586
|
-
resolveEm(ctx),
|
|
1587
|
-
ctx,
|
|
1588
|
-
{ tenantId: result.tenantId, organizationId: result.organizationId },
|
|
1589
|
-
result.catalogVariantId,
|
|
1590
|
-
).catch(() => undefined)
|
|
1591
|
-
}
|
|
1669
|
+
const result = await runInTransaction(em, async (trx) =>
|
|
1670
|
+
applyInventoryReceiveInTransaction(trx, ctx, input),
|
|
1671
|
+
)
|
|
1672
|
+
await emitInventoryReceiveSideEffects(ctx, result)
|
|
1592
1673
|
return { movementId: result.movementId }
|
|
1593
1674
|
},
|
|
1594
1675
|
buildLog: async ({ input, result, ctx }) =>
|
|
@@ -1607,6 +1688,205 @@ const receiveInventoryCommand: CommandHandler<InventoryReceiveInput, { movementI
|
|
|
1607
1688
|
}),
|
|
1608
1689
|
}
|
|
1609
1690
|
|
|
1691
|
+
/**
|
|
1692
|
+
* Core move mutation for an already-open transaction.
|
|
1693
|
+
* Used by `wms.inventory.move` and by putaway complete so stock move + task
|
|
1694
|
+
* status share one transactional boundary (nested commandBus moves cannot).
|
|
1695
|
+
*/
|
|
1696
|
+
export type InventoryMoveMutationResult = {
|
|
1697
|
+
movementId: string
|
|
1698
|
+
warehouseId: string
|
|
1699
|
+
catalogVariantId: string
|
|
1700
|
+
quantity: number
|
|
1701
|
+
tenantId: string
|
|
1702
|
+
organizationId: string
|
|
1703
|
+
movementEntity: InventoryMovement
|
|
1704
|
+
balances: AffectedBalance[]
|
|
1705
|
+
idempotentReplay: boolean
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
export async function applyInventoryMoveInTransaction(
|
|
1709
|
+
trx: EntityManager,
|
|
1710
|
+
ctx: CommandRuntimeContext,
|
|
1711
|
+
input: InventoryMoveInput,
|
|
1712
|
+
): Promise<InventoryMoveMutationResult> {
|
|
1713
|
+
const scope = resolveScope(ctx, input)
|
|
1714
|
+
await requireWarehouse(trx, ctx, input.warehouseId, scope)
|
|
1715
|
+
const sourceLocation = await requireLocation(trx, ctx, input.fromLocationId, scope)
|
|
1716
|
+
const targetLocation = await requireLocation(trx, ctx, input.toLocationId, scope)
|
|
1717
|
+
const sourceWarehouseId =
|
|
1718
|
+
typeof sourceLocation.warehouse === 'string' ? sourceLocation.warehouse : sourceLocation.warehouse.id
|
|
1719
|
+
const targetWarehouseId =
|
|
1720
|
+
typeof targetLocation.warehouse === 'string' ? targetLocation.warehouse : targetLocation.warehouse.id
|
|
1721
|
+
if (sourceWarehouseId !== input.warehouseId || targetWarehouseId !== input.warehouseId) {
|
|
1722
|
+
throw new CrudHttpError(422, { error: 'invalid_location' })
|
|
1723
|
+
}
|
|
1724
|
+
const sourceBalance = await findExactBalanceForUpdate(trx, scope, {
|
|
1725
|
+
warehouseId: input.warehouseId,
|
|
1726
|
+
locationId: input.fromLocationId,
|
|
1727
|
+
catalogVariantId: input.catalogVariantId,
|
|
1728
|
+
lotId: input.lotId,
|
|
1729
|
+
serialNumber: input.serialNumber,
|
|
1730
|
+
})
|
|
1731
|
+
if (!sourceBalance) {
|
|
1732
|
+
throw new CrudHttpError(409, { error: 'insufficient_stock' })
|
|
1733
|
+
}
|
|
1734
|
+
const sourceBalanceAction = 'updated' as const
|
|
1735
|
+
const targetResult = await upsertBalanceBucket(trx, scope, {
|
|
1736
|
+
warehouseId: input.warehouseId,
|
|
1737
|
+
locationId: input.toLocationId,
|
|
1738
|
+
catalogVariantId: input.catalogVariantId,
|
|
1739
|
+
lotId: input.lotId,
|
|
1740
|
+
serialNumber: input.serialNumber,
|
|
1741
|
+
})
|
|
1742
|
+
const targetBalance = targetResult.balance
|
|
1743
|
+
const targetBalanceAction = targetResult.created ? ('created' as const) : ('updated' as const)
|
|
1744
|
+
const performedAt = input.performedAt ?? new Date()
|
|
1745
|
+
const receivedAt = await resolveReceivedAtForBalance(trx, sourceBalance, scope, performedAt)
|
|
1746
|
+
const movementInput: MovementMutationInput = {
|
|
1747
|
+
warehouseId: input.warehouseId,
|
|
1748
|
+
locationFromId: input.fromLocationId,
|
|
1749
|
+
locationToId: input.toLocationId,
|
|
1750
|
+
catalogVariantId: input.catalogVariantId,
|
|
1751
|
+
lotId: input.lotId ?? null,
|
|
1752
|
+
serialNumber: input.serialNumber ?? null,
|
|
1753
|
+
quantity: input.quantity,
|
|
1754
|
+
type: input.type ?? 'transfer',
|
|
1755
|
+
referenceType: input.referenceType,
|
|
1756
|
+
referenceId: input.referenceId,
|
|
1757
|
+
performedBy: input.performedBy,
|
|
1758
|
+
performedAt,
|
|
1759
|
+
receivedAt,
|
|
1760
|
+
reason: input.reason,
|
|
1761
|
+
reasonCode: input.reasonCode ?? null,
|
|
1762
|
+
metadata: input.metadata ?? null,
|
|
1763
|
+
}
|
|
1764
|
+
const idempotencyKey = buildMovementIdempotencyKey({
|
|
1765
|
+
referenceType: movementInput.referenceType,
|
|
1766
|
+
referenceId: movementInput.referenceId,
|
|
1767
|
+
type: movementInput.type,
|
|
1768
|
+
warehouseId: movementInput.warehouseId,
|
|
1769
|
+
locationFromId: movementInput.locationFromId,
|
|
1770
|
+
locationToId: movementInput.locationToId,
|
|
1771
|
+
catalogVariantId: movementInput.catalogVariantId,
|
|
1772
|
+
lotId: movementInput.lotId,
|
|
1773
|
+
serialNumber: movementInput.serialNumber,
|
|
1774
|
+
quantity: movementInput.quantity,
|
|
1775
|
+
})
|
|
1776
|
+
const existingMovement = await findExistingMovementByIdempotencyKey(trx, scope, idempotencyKey)
|
|
1777
|
+
if (existingMovement) {
|
|
1778
|
+
return {
|
|
1779
|
+
movementId: existingMovement.id,
|
|
1780
|
+
warehouseId: input.warehouseId,
|
|
1781
|
+
catalogVariantId: input.catalogVariantId,
|
|
1782
|
+
quantity: input.quantity,
|
|
1783
|
+
tenantId: scope.tenantId,
|
|
1784
|
+
organizationId: scope.organizationId,
|
|
1785
|
+
movementEntity: existingMovement,
|
|
1786
|
+
balances: [
|
|
1787
|
+
{ entity: sourceBalance, action: sourceBalanceAction },
|
|
1788
|
+
{ entity: targetBalance, action: targetBalanceAction },
|
|
1789
|
+
],
|
|
1790
|
+
idempotentReplay: true,
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
// Persist movement before mutating balances so unique-constraint replay cannot
|
|
1794
|
+
// leave skewed source/target on-hand and still report a fresh write.
|
|
1795
|
+
const { movement, idempotentReplay } = await persistMovementWithIdempotency(trx, scope, movementInput)
|
|
1796
|
+
if (idempotentReplay) {
|
|
1797
|
+
return {
|
|
1798
|
+
movementId: movement.id,
|
|
1799
|
+
warehouseId: input.warehouseId,
|
|
1800
|
+
catalogVariantId: input.catalogVariantId,
|
|
1801
|
+
quantity: input.quantity,
|
|
1802
|
+
tenantId: scope.tenantId,
|
|
1803
|
+
organizationId: scope.organizationId,
|
|
1804
|
+
movementEntity: movement,
|
|
1805
|
+
balances: [
|
|
1806
|
+
{ entity: sourceBalance, action: sourceBalanceAction },
|
|
1807
|
+
{ entity: targetBalance, action: targetBalanceAction },
|
|
1808
|
+
],
|
|
1809
|
+
idempotentReplay: true,
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
if (getAvailableQuantity(sourceBalance) < input.quantity - 0.000001) {
|
|
1813
|
+
throw new CrudHttpError(409, { error: 'insufficient_stock' })
|
|
1814
|
+
}
|
|
1815
|
+
setNumeric(
|
|
1816
|
+
sourceBalance as unknown as Record<string, unknown>,
|
|
1817
|
+
'quantityOnHand',
|
|
1818
|
+
toNumber(sourceBalance.quantityOnHand) - input.quantity,
|
|
1819
|
+
)
|
|
1820
|
+
setNumeric(
|
|
1821
|
+
targetBalance as unknown as Record<string, unknown>,
|
|
1822
|
+
'quantityOnHand',
|
|
1823
|
+
toNumber(targetBalance.quantityOnHand) + input.quantity,
|
|
1824
|
+
)
|
|
1825
|
+
await trx.flush()
|
|
1826
|
+
return {
|
|
1827
|
+
movementId: movement.id,
|
|
1828
|
+
warehouseId: input.warehouseId,
|
|
1829
|
+
catalogVariantId: input.catalogVariantId,
|
|
1830
|
+
quantity: input.quantity,
|
|
1831
|
+
tenantId: scope.tenantId,
|
|
1832
|
+
organizationId: scope.organizationId,
|
|
1833
|
+
movementEntity: movement,
|
|
1834
|
+
balances: [
|
|
1835
|
+
{ entity: sourceBalance, action: sourceBalanceAction },
|
|
1836
|
+
{ entity: targetBalance, action: targetBalanceAction },
|
|
1837
|
+
],
|
|
1838
|
+
idempotentReplay: false,
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
export async function emitInventoryMoveSideEffects(
|
|
1843
|
+
ctx: CommandRuntimeContext,
|
|
1844
|
+
result: InventoryMoveMutationResult,
|
|
1845
|
+
): Promise<void> {
|
|
1846
|
+
if (result.idempotentReplay) return
|
|
1847
|
+
await emitInventorySideEffects(ctx, {
|
|
1848
|
+
movements: [{ entity: result.movementEntity, action: 'created' }],
|
|
1849
|
+
balances: result.balances,
|
|
1850
|
+
})
|
|
1851
|
+
void emitWmsEvent('wms.inventory.moved', {
|
|
1852
|
+
id: result.movementId,
|
|
1853
|
+
movementId: result.movementId,
|
|
1854
|
+
warehouseId: result.warehouseId,
|
|
1855
|
+
catalogVariantId: result.catalogVariantId,
|
|
1856
|
+
quantity: toNumericString(result.quantity),
|
|
1857
|
+
tenantId: result.tenantId,
|
|
1858
|
+
organizationId: result.organizationId,
|
|
1859
|
+
}).catch(() => undefined)
|
|
1860
|
+
void emitLowStockEventIfNeeded(
|
|
1861
|
+
resolveEm(ctx),
|
|
1862
|
+
ctx,
|
|
1863
|
+
{ tenantId: result.tenantId, organizationId: result.organizationId },
|
|
1864
|
+
result.catalogVariantId,
|
|
1865
|
+
).catch(() => undefined)
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/** Find a putaway-complete movement by stable task reference (qty-independent). */
|
|
1869
|
+
export async function findPutawayCompleteMovementByReference(
|
|
1870
|
+
em: EntityManager,
|
|
1871
|
+
scope: { tenantId: string; organizationId: string },
|
|
1872
|
+
referenceId: string,
|
|
1873
|
+
): Promise<InventoryMovement | null> {
|
|
1874
|
+
return findOneWithDecryption(
|
|
1875
|
+
em,
|
|
1876
|
+
InventoryMovement,
|
|
1877
|
+
{
|
|
1878
|
+
organizationId: scope.organizationId,
|
|
1879
|
+
tenantId: scope.tenantId,
|
|
1880
|
+
referenceType: 'manual',
|
|
1881
|
+
referenceId,
|
|
1882
|
+
type: 'putaway',
|
|
1883
|
+
deletedAt: null,
|
|
1884
|
+
},
|
|
1885
|
+
undefined,
|
|
1886
|
+
scope,
|
|
1887
|
+
)
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1610
1890
|
const moveInventoryCommand: CommandHandler<InventoryMoveInput, { movementId: string }> = {
|
|
1611
1891
|
id: 'wms.inventory.move',
|
|
1612
1892
|
// See "WMS Inventory Mutation Commands — Undo Policy" at top of file.
|
|
@@ -1616,136 +1896,10 @@ const moveInventoryCommand: CommandHandler<InventoryMoveInput, { movementId: str
|
|
|
1616
1896
|
ensureTenantScope(ctx, input.tenantId)
|
|
1617
1897
|
ensureOrganizationScope(ctx, input.organizationId)
|
|
1618
1898
|
const em = resolveEm(ctx)
|
|
1619
|
-
const result = await runInTransaction(em, async (trx) =>
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
const targetLocation = await requireLocation(trx, ctx, input.toLocationId, scope)
|
|
1624
|
-
const sourceWarehouseId = typeof sourceLocation.warehouse === 'string' ? sourceLocation.warehouse : sourceLocation.warehouse.id
|
|
1625
|
-
const targetWarehouseId = typeof targetLocation.warehouse === 'string' ? targetLocation.warehouse : targetLocation.warehouse.id
|
|
1626
|
-
if (sourceWarehouseId !== input.warehouseId || targetWarehouseId !== input.warehouseId) {
|
|
1627
|
-
throw new CrudHttpError(422, { error: 'invalid_location' })
|
|
1628
|
-
}
|
|
1629
|
-
const sourceBalance = await findExactBalanceForUpdate(trx, scope, {
|
|
1630
|
-
warehouseId: input.warehouseId,
|
|
1631
|
-
locationId: input.fromLocationId,
|
|
1632
|
-
catalogVariantId: input.catalogVariantId,
|
|
1633
|
-
lotId: input.lotId,
|
|
1634
|
-
serialNumber: input.serialNumber,
|
|
1635
|
-
})
|
|
1636
|
-
if (!sourceBalance) {
|
|
1637
|
-
throw new CrudHttpError(409, { error: 'insufficient_stock' })
|
|
1638
|
-
}
|
|
1639
|
-
const sourceBalanceAction = 'updated' as const
|
|
1640
|
-
const targetResult = await upsertBalanceBucket(trx, scope, {
|
|
1641
|
-
warehouseId: input.warehouseId,
|
|
1642
|
-
locationId: input.toLocationId,
|
|
1643
|
-
catalogVariantId: input.catalogVariantId,
|
|
1644
|
-
lotId: input.lotId,
|
|
1645
|
-
serialNumber: input.serialNumber,
|
|
1646
|
-
})
|
|
1647
|
-
const targetBalance = targetResult.balance
|
|
1648
|
-
const targetBalanceAction = targetResult.created ? ('created' as const) : ('updated' as const)
|
|
1649
|
-
const performedAt = input.performedAt ?? new Date()
|
|
1650
|
-
const receivedAt = await resolveReceivedAtForBalance(trx, sourceBalance, scope, performedAt)
|
|
1651
|
-
const movementInput: MovementMutationInput = {
|
|
1652
|
-
warehouseId: input.warehouseId,
|
|
1653
|
-
locationFromId: input.fromLocationId,
|
|
1654
|
-
locationToId: input.toLocationId,
|
|
1655
|
-
catalogVariantId: input.catalogVariantId,
|
|
1656
|
-
lotId: input.lotId ?? null,
|
|
1657
|
-
serialNumber: input.serialNumber ?? null,
|
|
1658
|
-
quantity: input.quantity,
|
|
1659
|
-
type: input.type ?? 'transfer',
|
|
1660
|
-
referenceType: input.referenceType,
|
|
1661
|
-
referenceId: input.referenceId,
|
|
1662
|
-
performedBy: input.performedBy,
|
|
1663
|
-
performedAt,
|
|
1664
|
-
receivedAt,
|
|
1665
|
-
reason: input.reason,
|
|
1666
|
-
reasonCode: input.reasonCode ?? null,
|
|
1667
|
-
metadata: input.metadata ?? null,
|
|
1668
|
-
}
|
|
1669
|
-
const idempotencyKey = buildMovementIdempotencyKey({
|
|
1670
|
-
referenceType: movementInput.referenceType,
|
|
1671
|
-
referenceId: movementInput.referenceId,
|
|
1672
|
-
type: movementInput.type,
|
|
1673
|
-
warehouseId: movementInput.warehouseId,
|
|
1674
|
-
locationFromId: movementInput.locationFromId,
|
|
1675
|
-
locationToId: movementInput.locationToId,
|
|
1676
|
-
catalogVariantId: movementInput.catalogVariantId,
|
|
1677
|
-
lotId: movementInput.lotId,
|
|
1678
|
-
serialNumber: movementInput.serialNumber,
|
|
1679
|
-
quantity: movementInput.quantity,
|
|
1680
|
-
})
|
|
1681
|
-
const existingMovement = await findExistingMovementByIdempotencyKey(trx, scope, idempotencyKey)
|
|
1682
|
-
if (existingMovement) {
|
|
1683
|
-
return {
|
|
1684
|
-
movementId: existingMovement.id,
|
|
1685
|
-
warehouseId: input.warehouseId,
|
|
1686
|
-
catalogVariantId: input.catalogVariantId,
|
|
1687
|
-
quantity: input.quantity,
|
|
1688
|
-
tenantId: scope.tenantId,
|
|
1689
|
-
organizationId: scope.organizationId,
|
|
1690
|
-
movementEntity: existingMovement,
|
|
1691
|
-
balances: [
|
|
1692
|
-
{ entity: sourceBalance, action: sourceBalanceAction },
|
|
1693
|
-
{ entity: targetBalance, action: targetBalanceAction },
|
|
1694
|
-
] as AffectedBalance[],
|
|
1695
|
-
idempotentReplay: true,
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
if (getAvailableQuantity(sourceBalance) < input.quantity - 0.000001) {
|
|
1699
|
-
throw new CrudHttpError(409, { error: 'insufficient_stock' })
|
|
1700
|
-
}
|
|
1701
|
-
setNumeric(
|
|
1702
|
-
sourceBalance as unknown as Record<string, unknown>,
|
|
1703
|
-
'quantityOnHand',
|
|
1704
|
-
toNumber(sourceBalance.quantityOnHand) - input.quantity,
|
|
1705
|
-
)
|
|
1706
|
-
setNumeric(
|
|
1707
|
-
targetBalance as unknown as Record<string, unknown>,
|
|
1708
|
-
'quantityOnHand',
|
|
1709
|
-
toNumber(targetBalance.quantityOnHand) + input.quantity,
|
|
1710
|
-
)
|
|
1711
|
-
const { movement } = await persistMovementWithIdempotency(trx, scope, movementInput)
|
|
1712
|
-
await trx.flush()
|
|
1713
|
-
return {
|
|
1714
|
-
movementId: movement.id,
|
|
1715
|
-
warehouseId: input.warehouseId,
|
|
1716
|
-
catalogVariantId: input.catalogVariantId,
|
|
1717
|
-
quantity: input.quantity,
|
|
1718
|
-
tenantId: scope.tenantId,
|
|
1719
|
-
organizationId: scope.organizationId,
|
|
1720
|
-
movementEntity: movement,
|
|
1721
|
-
balances: [
|
|
1722
|
-
{ entity: sourceBalance, action: sourceBalanceAction },
|
|
1723
|
-
{ entity: targetBalance, action: targetBalanceAction },
|
|
1724
|
-
] as AffectedBalance[],
|
|
1725
|
-
idempotentReplay: false,
|
|
1726
|
-
}
|
|
1727
|
-
})
|
|
1728
|
-
if (!result.idempotentReplay) {
|
|
1729
|
-
await emitInventorySideEffects(ctx, {
|
|
1730
|
-
movements: [{ entity: result.movementEntity, action: 'created' }],
|
|
1731
|
-
balances: result.balances,
|
|
1732
|
-
})
|
|
1733
|
-
void emitWmsEvent('wms.inventory.moved', {
|
|
1734
|
-
id: result.movementId,
|
|
1735
|
-
movementId: result.movementId,
|
|
1736
|
-
warehouseId: result.warehouseId,
|
|
1737
|
-
catalogVariantId: result.catalogVariantId,
|
|
1738
|
-
quantity: toNumericString(result.quantity),
|
|
1739
|
-
tenantId: result.tenantId,
|
|
1740
|
-
organizationId: result.organizationId,
|
|
1741
|
-
}).catch(() => undefined)
|
|
1742
|
-
void emitLowStockEventIfNeeded(
|
|
1743
|
-
resolveEm(ctx),
|
|
1744
|
-
ctx,
|
|
1745
|
-
{ tenantId: result.tenantId, organizationId: result.organizationId },
|
|
1746
|
-
result.catalogVariantId,
|
|
1747
|
-
).catch(() => undefined)
|
|
1748
|
-
}
|
|
1899
|
+
const result = await runInTransaction(em, async (trx) =>
|
|
1900
|
+
applyInventoryMoveInTransaction(trx, ctx, input),
|
|
1901
|
+
)
|
|
1902
|
+
await emitInventoryMoveSideEffects(ctx, result)
|
|
1749
1903
|
return { movementId: result.movementId }
|
|
1750
1904
|
},
|
|
1751
1905
|
buildLog: async ({ input, result, ctx }) =>
|
|
@@ -1860,12 +2014,28 @@ const cycleCountInventoryCommand: CommandHandler<InventoryCycleCountInput, { adj
|
|
|
1860
2014
|
),
|
|
1861
2015
|
})
|
|
1862
2016
|
}
|
|
2017
|
+
// Persist movement before setting on-hand so a unique-constraint replay cannot
|
|
2018
|
+
// skew the balance and still report idempotentReplay: false.
|
|
2019
|
+
const { movement, idempotentReplay } = await persistMovementWithIdempotency(trx, scope, movementInput)
|
|
2020
|
+
if (idempotentReplay) {
|
|
2021
|
+
return {
|
|
2022
|
+
adjustmentDelta: toNumericString(delta),
|
|
2023
|
+
movementId: movement.id,
|
|
2024
|
+
warehouseId: input.warehouseId,
|
|
2025
|
+
catalogVariantId: input.catalogVariantId,
|
|
2026
|
+
tenantId: scope.tenantId,
|
|
2027
|
+
organizationId: scope.organizationId,
|
|
2028
|
+
movementEntity: movement,
|
|
2029
|
+
balanceEntity: balance,
|
|
2030
|
+
balanceAction: ('updated' as const),
|
|
2031
|
+
idempotentReplay: true,
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
1863
2034
|
setNumeric(
|
|
1864
2035
|
balance as unknown as Record<string, unknown>,
|
|
1865
2036
|
'quantityOnHand',
|
|
1866
2037
|
input.countedQuantity,
|
|
1867
2038
|
)
|
|
1868
|
-
const { movement } = await persistMovementWithIdempotency(trx, scope, movementInput)
|
|
1869
2039
|
await trx.flush()
|
|
1870
2040
|
return {
|
|
1871
2041
|
adjustmentDelta: toNumericString(delta),
|