@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
|
@@ -0,0 +1,1746 @@
|
|
|
1
|
+
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
|
+
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
3
|
+
import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
|
|
4
|
+
import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
|
|
5
|
+
import { LockMode } from "@mikro-orm/core";
|
|
6
|
+
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
7
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
8
|
+
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
9
|
+
import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
10
|
+
import {
|
|
11
|
+
Asn,
|
|
12
|
+
InventoryBalance,
|
|
13
|
+
PutawayTask,
|
|
14
|
+
ReceivingLine,
|
|
15
|
+
Warehouse,
|
|
16
|
+
WarehouseLocation
|
|
17
|
+
} from "../data/entities.js";
|
|
18
|
+
import { requireVendorIfPresent as requireVendorIfPresentLookup } from "../lib/asnVendor.js";
|
|
19
|
+
import {
|
|
20
|
+
asnCloseSchema,
|
|
21
|
+
asnCreateSchema,
|
|
22
|
+
asnReceiveLineSchema,
|
|
23
|
+
asnUpdateSchema,
|
|
24
|
+
receivingLineCreateSchema,
|
|
25
|
+
receivingLineUpdateSchema
|
|
26
|
+
} from "../data/validators.js";
|
|
27
|
+
import { emitWmsEvent } from "../events.js";
|
|
28
|
+
import {
|
|
29
|
+
assertAsnQcTransition,
|
|
30
|
+
assertReceivingLineLifecycleFieldsForbidden,
|
|
31
|
+
buildAsnReceivePutawayKey,
|
|
32
|
+
buildAsnReceiveReferenceId,
|
|
33
|
+
hasAsnDeleteBlockingLineActivity,
|
|
34
|
+
isAsnCloseable,
|
|
35
|
+
resolveAsnReceiveAttempt,
|
|
36
|
+
resolvePutawayQuantityForAlreadyAtTargetRetry,
|
|
37
|
+
shouldEnsurePutawayOnAlreadyAtTarget,
|
|
38
|
+
shouldRecreatePutawayOnAlreadyAtTarget,
|
|
39
|
+
shouldWriteStockOnQcPass
|
|
40
|
+
} from "../lib/asnReceiving.js";
|
|
41
|
+
import {
|
|
42
|
+
computeUncommittedPutawaySourceQuantity,
|
|
43
|
+
selectCoveringOpenPutawayTask
|
|
44
|
+
} from "../lib/putaway.js";
|
|
45
|
+
import {
|
|
46
|
+
applyInventoryReceiveInTransaction,
|
|
47
|
+
emitInventoryReceiveSideEffects
|
|
48
|
+
} from "./inventory-actions.js";
|
|
49
|
+
import {
|
|
50
|
+
asnCrudEvents,
|
|
51
|
+
asnCrudIndexer,
|
|
52
|
+
ensureOrganizationScope,
|
|
53
|
+
ensureTenantScope,
|
|
54
|
+
normalizeOptionalString,
|
|
55
|
+
putawayTaskCrudEvents,
|
|
56
|
+
putawayTaskCrudIndexer,
|
|
57
|
+
receivingLineCrudEvents,
|
|
58
|
+
receivingLineCrudIndexer,
|
|
59
|
+
requireId,
|
|
60
|
+
toNumericString,
|
|
61
|
+
WMS_ASN_RESOURCE,
|
|
62
|
+
WMS_INVENTORY_BALANCE_RESOURCE,
|
|
63
|
+
WMS_RECEIVING_LINE_RESOURCE
|
|
64
|
+
} from "./shared.js";
|
|
65
|
+
function resolveScope(ctx, fallback) {
|
|
66
|
+
const tenantId = fallback?.tenantId ?? ctx.auth?.tenantId ?? null;
|
|
67
|
+
const organizationId = fallback?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
|
|
68
|
+
if (!tenantId || !organizationId) {
|
|
69
|
+
throw new CrudHttpError(400, { error: "Tenant and organization scope are required." });
|
|
70
|
+
}
|
|
71
|
+
return { tenantId, organizationId };
|
|
72
|
+
}
|
|
73
|
+
function resolveEm(ctx) {
|
|
74
|
+
return ctx.container.resolve("em").fork();
|
|
75
|
+
}
|
|
76
|
+
function toNumber(value) {
|
|
77
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
78
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
79
|
+
const parsed = Number(value);
|
|
80
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
function toJsonValue(value) {
|
|
85
|
+
if (value === void 0) return void 0;
|
|
86
|
+
return value ?? null;
|
|
87
|
+
}
|
|
88
|
+
function iso(value) {
|
|
89
|
+
if (value instanceof Date) return value.toISOString();
|
|
90
|
+
return new Date(value).toISOString();
|
|
91
|
+
}
|
|
92
|
+
function snapshotAsn(record) {
|
|
93
|
+
const warehouseId = typeof record.warehouse === "string" ? record.warehouse : record.warehouse.id;
|
|
94
|
+
return {
|
|
95
|
+
id: record.id,
|
|
96
|
+
organizationId: record.organizationId,
|
|
97
|
+
tenantId: record.tenantId,
|
|
98
|
+
warehouseId,
|
|
99
|
+
vendorId: record.vendorId ?? null,
|
|
100
|
+
status: record.status,
|
|
101
|
+
expectedAt: iso(record.expectedAt),
|
|
102
|
+
referenceNumber: record.referenceNumber ?? null,
|
|
103
|
+
notes: record.notes ?? null,
|
|
104
|
+
metadata: record.metadata ?? null,
|
|
105
|
+
createdAt: iso(record.createdAt),
|
|
106
|
+
updatedAt: iso(record.updatedAt)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function snapshotReceivingLine(record) {
|
|
110
|
+
const asnId = typeof record.asn === "string" ? record.asn : record.asn.id;
|
|
111
|
+
return {
|
|
112
|
+
id: record.id,
|
|
113
|
+
organizationId: record.organizationId,
|
|
114
|
+
tenantId: record.tenantId,
|
|
115
|
+
asnId,
|
|
116
|
+
catalogVariantId: record.catalogVariantId,
|
|
117
|
+
expectedQty: record.expectedQty,
|
|
118
|
+
receivedQty: record.receivedQty,
|
|
119
|
+
lotNumber: record.lotNumber ?? null,
|
|
120
|
+
serialNumbers: Array.isArray(record.serialNumbers) ? [...record.serialNumbers] : null,
|
|
121
|
+
qcStatus: record.qcStatus,
|
|
122
|
+
targetStagingLocationId: record.targetStagingLocationId ?? null,
|
|
123
|
+
rejectionReason: record.rejectionReason ?? null,
|
|
124
|
+
metadata: record.metadata ?? null,
|
|
125
|
+
createdAt: iso(record.createdAt),
|
|
126
|
+
updatedAt: iso(record.updatedAt)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function loadAsn(em, ctx, id) {
|
|
130
|
+
const asn = await findOneWithDecryption(em, Asn, { id, deletedAt: null }, void 0, resolveScope(ctx));
|
|
131
|
+
if (!asn) throw new CrudHttpError(404, { error: "ASN not found." });
|
|
132
|
+
ensureTenantScope(ctx, asn.tenantId);
|
|
133
|
+
ensureOrganizationScope(ctx, asn.organizationId);
|
|
134
|
+
return asn;
|
|
135
|
+
}
|
|
136
|
+
async function withLockedAsn(em, ctx, id, scope, fn) {
|
|
137
|
+
return em.transactional(async (trx) => {
|
|
138
|
+
const asn = await findOneWithDecryption(
|
|
139
|
+
trx,
|
|
140
|
+
Asn,
|
|
141
|
+
{ id, deletedAt: null },
|
|
142
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
143
|
+
scope
|
|
144
|
+
);
|
|
145
|
+
if (!asn) throw new CrudHttpError(404, { error: "ASN not found." });
|
|
146
|
+
ensureTenantScope(ctx, asn.tenantId);
|
|
147
|
+
ensureOrganizationScope(ctx, asn.organizationId);
|
|
148
|
+
return fn(asn, trx);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function withLockedAsnReceivingLine(em, ctx, lineId, scope, options, fn) {
|
|
152
|
+
return em.transactional(async (trx) => {
|
|
153
|
+
const unlocked = await findOneWithDecryption(trx, ReceivingLine, { id: lineId }, void 0, scope);
|
|
154
|
+
if (!unlocked) return null;
|
|
155
|
+
if (options.requireActiveLine && unlocked.deletedAt) return null;
|
|
156
|
+
ensureTenantScope(ctx, unlocked.tenantId);
|
|
157
|
+
ensureOrganizationScope(ctx, unlocked.organizationId);
|
|
158
|
+
const asnId = typeof unlocked.asn === "string" ? unlocked.asn : unlocked.asn.id;
|
|
159
|
+
const asn = await findOneWithDecryption(
|
|
160
|
+
trx,
|
|
161
|
+
Asn,
|
|
162
|
+
{ id: asnId, deletedAt: null },
|
|
163
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
164
|
+
scope
|
|
165
|
+
);
|
|
166
|
+
if (!asn) throw new CrudHttpError(404, { error: "ASN not found." });
|
|
167
|
+
ensureTenantScope(ctx, asn.tenantId);
|
|
168
|
+
ensureOrganizationScope(ctx, asn.organizationId);
|
|
169
|
+
const locked = await findOneWithDecryption(
|
|
170
|
+
trx,
|
|
171
|
+
ReceivingLine,
|
|
172
|
+
{ id: lineId },
|
|
173
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
174
|
+
scope
|
|
175
|
+
);
|
|
176
|
+
if (!locked) return null;
|
|
177
|
+
if (options.requireActiveLine && locked.deletedAt) return null;
|
|
178
|
+
ensureTenantScope(ctx, locked.tenantId);
|
|
179
|
+
ensureOrganizationScope(ctx, locked.organizationId);
|
|
180
|
+
return fn({ asn, line: locked, trx });
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async function loadReceivingLine(em, ctx, id) {
|
|
184
|
+
const line = await findOneWithDecryption(
|
|
185
|
+
em,
|
|
186
|
+
ReceivingLine,
|
|
187
|
+
{ id, deletedAt: null },
|
|
188
|
+
void 0,
|
|
189
|
+
resolveScope(ctx)
|
|
190
|
+
);
|
|
191
|
+
if (!line) throw new CrudHttpError(404, { error: "Receiving line not found." });
|
|
192
|
+
ensureTenantScope(ctx, line.tenantId);
|
|
193
|
+
ensureOrganizationScope(ctx, line.organizationId);
|
|
194
|
+
return line;
|
|
195
|
+
}
|
|
196
|
+
async function requireWarehouse(em, ctx, warehouseId, scope) {
|
|
197
|
+
const warehouse = await findOneWithDecryption(
|
|
198
|
+
em,
|
|
199
|
+
Warehouse,
|
|
200
|
+
{ id: warehouseId, deletedAt: null },
|
|
201
|
+
void 0,
|
|
202
|
+
scope
|
|
203
|
+
);
|
|
204
|
+
if (!warehouse) throw new CrudHttpError(404, { error: "Warehouse not found." });
|
|
205
|
+
ensureTenantScope(ctx, warehouse.tenantId);
|
|
206
|
+
ensureOrganizationScope(ctx, warehouse.organizationId);
|
|
207
|
+
return warehouse;
|
|
208
|
+
}
|
|
209
|
+
async function requireStagingLocation(em, ctx, locationId, warehouseId, scope) {
|
|
210
|
+
const location = await findOneWithDecryption(
|
|
211
|
+
em,
|
|
212
|
+
WarehouseLocation,
|
|
213
|
+
{ id: locationId, deletedAt: null },
|
|
214
|
+
void 0,
|
|
215
|
+
scope
|
|
216
|
+
);
|
|
217
|
+
if (!location) throw new CrudHttpError(404, { error: "Warehouse location not found." });
|
|
218
|
+
ensureTenantScope(ctx, location.tenantId);
|
|
219
|
+
ensureOrganizationScope(ctx, location.organizationId);
|
|
220
|
+
const locationWarehouseId = typeof location.warehouse === "string" ? location.warehouse : location.warehouse.id;
|
|
221
|
+
if (locationWarehouseId !== warehouseId) {
|
|
222
|
+
throw new CrudHttpError(422, { error: "invalid_location" });
|
|
223
|
+
}
|
|
224
|
+
if (location.type !== "staging" && location.type !== "dock") {
|
|
225
|
+
throw new CrudHttpError(422, { error: "invalid_staging_location" });
|
|
226
|
+
}
|
|
227
|
+
if (!location.isActive) {
|
|
228
|
+
throw new CrudHttpError(422, { error: "inactive_location" });
|
|
229
|
+
}
|
|
230
|
+
return location;
|
|
231
|
+
}
|
|
232
|
+
async function requireVendorIfPresent(ctx, vendorId, scope) {
|
|
233
|
+
await requireVendorIfPresentLookup(ctx.container, vendorId, scope);
|
|
234
|
+
}
|
|
235
|
+
function isUniqueConstraintError(error) {
|
|
236
|
+
if (!error || typeof error !== "object") return false;
|
|
237
|
+
const code = error.code;
|
|
238
|
+
if (code === "23505") return true;
|
|
239
|
+
const name = error.name;
|
|
240
|
+
return name === "UniqueConstraintViolationException";
|
|
241
|
+
}
|
|
242
|
+
async function findExistingPutawayForReceipt(em, scope, putawayKey) {
|
|
243
|
+
return findOneWithDecryption(
|
|
244
|
+
em,
|
|
245
|
+
PutawayTask,
|
|
246
|
+
{
|
|
247
|
+
organizationId: scope.organizationId,
|
|
248
|
+
tenantId: scope.tenantId,
|
|
249
|
+
putawayKey,
|
|
250
|
+
deletedAt: null,
|
|
251
|
+
status: { $in: ["open", "in_progress", "done"] }
|
|
252
|
+
},
|
|
253
|
+
void 0,
|
|
254
|
+
scope
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
async function findCancelledPutawayForReceipt(em, scope, putawayKey) {
|
|
258
|
+
const rows = await em.getConnection().execute(
|
|
259
|
+
`select id
|
|
260
|
+
from wms_putaway_tasks
|
|
261
|
+
where organization_id = ?
|
|
262
|
+
and tenant_id = ?
|
|
263
|
+
and deleted_at is null
|
|
264
|
+
and status = 'cancelled'
|
|
265
|
+
and metadata->>'putawayKey' = ?
|
|
266
|
+
order by updated_at desc
|
|
267
|
+
limit 1`,
|
|
268
|
+
[scope.organizationId, scope.tenantId, putawayKey]
|
|
269
|
+
);
|
|
270
|
+
const id = rows[0]?.id;
|
|
271
|
+
if (!id) return null;
|
|
272
|
+
return findOneWithDecryption(
|
|
273
|
+
em,
|
|
274
|
+
PutawayTask,
|
|
275
|
+
{ id, deletedAt: null, status: "cancelled" },
|
|
276
|
+
void 0,
|
|
277
|
+
scope
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
async function resolveUncommittedStagingPutawayQuantity(em, scope, input, options) {
|
|
281
|
+
const serialNumber = input.serialNumber?.trim() ? input.serialNumber.trim() : null;
|
|
282
|
+
const balance = await findOneWithDecryption(
|
|
283
|
+
em,
|
|
284
|
+
InventoryBalance,
|
|
285
|
+
{
|
|
286
|
+
warehouse: input.warehouseId,
|
|
287
|
+
location: input.sourceLocationId,
|
|
288
|
+
catalogVariantId: input.catalogVariantId,
|
|
289
|
+
lot: input.lotId ?? null,
|
|
290
|
+
serialNumber,
|
|
291
|
+
deletedAt: null
|
|
292
|
+
},
|
|
293
|
+
options?.lock ? { lockMode: LockMode.PESSIMISTIC_WRITE } : void 0,
|
|
294
|
+
scope
|
|
295
|
+
);
|
|
296
|
+
if (!balance) return null;
|
|
297
|
+
const openTasks = await findWithDecryption(
|
|
298
|
+
em,
|
|
299
|
+
PutawayTask,
|
|
300
|
+
{
|
|
301
|
+
warehouse: input.warehouseId,
|
|
302
|
+
sourceLocationId: input.sourceLocationId,
|
|
303
|
+
catalogVariantId: input.catalogVariantId,
|
|
304
|
+
lotId: input.lotId ?? null,
|
|
305
|
+
serialNumber,
|
|
306
|
+
status: { $in: ["open", "in_progress"] },
|
|
307
|
+
deletedAt: null
|
|
308
|
+
},
|
|
309
|
+
void 0,
|
|
310
|
+
scope
|
|
311
|
+
);
|
|
312
|
+
return {
|
|
313
|
+
remaining: computeUncommittedPutawaySourceQuantity({
|
|
314
|
+
quantityOnHand: toNumber(balance.quantityOnHand),
|
|
315
|
+
quantityReserved: toNumber(balance.quantityReserved),
|
|
316
|
+
quantityAllocated: toNumber(balance.quantityAllocated),
|
|
317
|
+
openPutawayQuantities: openTasks.map((task) => toNumber(task.quantity))
|
|
318
|
+
}),
|
|
319
|
+
openTasks
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
async function findOrCreatePutawayForReceipt(em, scope, putawayKey, input) {
|
|
323
|
+
const existing = await findExistingPutawayForReceipt(em, scope, putawayKey);
|
|
324
|
+
if (existing) return { task: existing, created: false };
|
|
325
|
+
const task = em.create(PutawayTask, {
|
|
326
|
+
organizationId: scope.organizationId,
|
|
327
|
+
tenantId: scope.tenantId,
|
|
328
|
+
warehouse: em.getReference(Warehouse, input.warehouseId),
|
|
329
|
+
sourceLocationId: input.sourceLocationId,
|
|
330
|
+
targetLocationId: null,
|
|
331
|
+
catalogVariantId: input.catalogVariantId,
|
|
332
|
+
lotId: input.lotId,
|
|
333
|
+
serialNumber: input.serialNumber?.trim() ? input.serialNumber.trim() : null,
|
|
334
|
+
quantity: toNumericString(input.quantity),
|
|
335
|
+
status: "open",
|
|
336
|
+
assignedTo: null,
|
|
337
|
+
priority: 5,
|
|
338
|
+
putawayKey,
|
|
339
|
+
metadata: {
|
|
340
|
+
...input.metadata ?? {},
|
|
341
|
+
asnId: input.asnId,
|
|
342
|
+
receivingLineId: input.lineId,
|
|
343
|
+
putawayKey,
|
|
344
|
+
source: "asn_receive",
|
|
345
|
+
...input.serialNumber?.trim() ? { serialNumber: input.serialNumber.trim() } : {}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
try {
|
|
349
|
+
await em.persist(task).flush();
|
|
350
|
+
return { task, created: true };
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (!isUniqueConstraintError(error)) throw error;
|
|
353
|
+
const raced = await findExistingPutawayForReceipt(em, scope, putawayKey);
|
|
354
|
+
if (raced) return { task: raced, created: false };
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async function emitAsnReceivePutawayCreatedSideEffects(ctx, scope, putaway, warehouseId) {
|
|
359
|
+
let de = null;
|
|
360
|
+
try {
|
|
361
|
+
de = ctx.container.resolve("dataEngine");
|
|
362
|
+
} catch {
|
|
363
|
+
de = null;
|
|
364
|
+
}
|
|
365
|
+
if (de) {
|
|
366
|
+
await emitCrudSideEffects({
|
|
367
|
+
dataEngine: de,
|
|
368
|
+
action: "created",
|
|
369
|
+
entity: putaway,
|
|
370
|
+
identifiers: {
|
|
371
|
+
id: putaway.id,
|
|
372
|
+
organizationId: putaway.organizationId,
|
|
373
|
+
tenantId: putaway.tenantId
|
|
374
|
+
},
|
|
375
|
+
indexer: putawayTaskCrudIndexer,
|
|
376
|
+
events: putawayTaskCrudEvents
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
void emitWmsEvent("wms.putaway.created", {
|
|
380
|
+
id: putaway.id,
|
|
381
|
+
warehouseId,
|
|
382
|
+
sourceLocationId: putaway.sourceLocationId,
|
|
383
|
+
targetLocationId: putaway.targetLocationId ?? null,
|
|
384
|
+
catalogVariantId: putaway.catalogVariantId,
|
|
385
|
+
quantity: putaway.quantity,
|
|
386
|
+
tenantId: scope.tenantId,
|
|
387
|
+
organizationId: scope.organizationId
|
|
388
|
+
}).catch(() => void 0);
|
|
389
|
+
}
|
|
390
|
+
async function buildCrudLog(ctx, input, resultId, actionKey, fallbackLabel, resourceKind) {
|
|
391
|
+
const { translate } = await resolveTranslations();
|
|
392
|
+
return {
|
|
393
|
+
actionLabel: translate(actionKey, fallbackLabel),
|
|
394
|
+
resourceKind,
|
|
395
|
+
resourceId: resultId ?? input?.id ?? null,
|
|
396
|
+
tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
|
|
397
|
+
organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function assertAsnMutable(asn) {
|
|
401
|
+
if (asn.status === "closed" || asn.status === "received") {
|
|
402
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function hasOpenPutawayForAsn(em, scope, asnId) {
|
|
406
|
+
const rows = await em.getConnection().execute(
|
|
407
|
+
`select 1 as hit
|
|
408
|
+
from wms_putaway_tasks
|
|
409
|
+
where organization_id = ?
|
|
410
|
+
and tenant_id = ?
|
|
411
|
+
and deleted_at is null
|
|
412
|
+
and status in ('open', 'in_progress')
|
|
413
|
+
and metadata->>'asnId' = ?
|
|
414
|
+
limit 1`,
|
|
415
|
+
[scope.organizationId, scope.tenantId, asnId]
|
|
416
|
+
);
|
|
417
|
+
return rows.length > 0;
|
|
418
|
+
}
|
|
419
|
+
function resolveSerialNumbers(input) {
|
|
420
|
+
if (!input.serialNumbers?.length) return [];
|
|
421
|
+
return input.serialNumbers.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
422
|
+
}
|
|
423
|
+
function normalizeStoredSerials(values) {
|
|
424
|
+
if (!values?.length) return [];
|
|
425
|
+
return values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
426
|
+
}
|
|
427
|
+
function assertUniqueSerialNumbers(serialNumbers) {
|
|
428
|
+
const seen = /* @__PURE__ */ new Set();
|
|
429
|
+
for (const serialNumber of serialNumbers) {
|
|
430
|
+
if (seen.has(serialNumber)) {
|
|
431
|
+
throw new CrudHttpError(422, { error: "duplicate_serial" });
|
|
432
|
+
}
|
|
433
|
+
seen.add(serialNumber);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function assertIncomingSerialsAreNew(incoming, stored) {
|
|
437
|
+
const known = new Set(normalizeStoredSerials(stored));
|
|
438
|
+
for (const serialNumber of incoming) {
|
|
439
|
+
if (known.has(serialNumber)) {
|
|
440
|
+
throw new CrudHttpError(422, { error: "duplicate_serial" });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function mergeReceivedSerials(stored, incoming) {
|
|
445
|
+
const next = normalizeStoredSerials(stored);
|
|
446
|
+
const seen = new Set(next);
|
|
447
|
+
for (const serialNumber of incoming) {
|
|
448
|
+
if (seen.has(serialNumber)) continue;
|
|
449
|
+
seen.add(serialNumber);
|
|
450
|
+
next.push(serialNumber);
|
|
451
|
+
}
|
|
452
|
+
return next;
|
|
453
|
+
}
|
|
454
|
+
function recordReceivedSerials(line, incoming, priorReceivedQty) {
|
|
455
|
+
if (incoming.length === 0) return;
|
|
456
|
+
line.serialNumbers = priorReceivedQty > 1e-6 ? mergeReceivedSerials(line.serialNumbers, incoming) : incoming;
|
|
457
|
+
}
|
|
458
|
+
async function emitAsnSideEffects(ctx, affected) {
|
|
459
|
+
let de = null;
|
|
460
|
+
try {
|
|
461
|
+
de = ctx.container.resolve("dataEngine");
|
|
462
|
+
} catch {
|
|
463
|
+
de = null;
|
|
464
|
+
}
|
|
465
|
+
if (!de) return;
|
|
466
|
+
for (const { entity, action } of affected.asns ?? []) {
|
|
467
|
+
await emitCrudSideEffects({
|
|
468
|
+
dataEngine: de,
|
|
469
|
+
action,
|
|
470
|
+
entity,
|
|
471
|
+
identifiers: {
|
|
472
|
+
id: entity.id,
|
|
473
|
+
organizationId: entity.organizationId,
|
|
474
|
+
tenantId: entity.tenantId
|
|
475
|
+
},
|
|
476
|
+
indexer: asnCrudIndexer,
|
|
477
|
+
events: asnCrudEvents
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
for (const { entity, action } of affected.lines ?? []) {
|
|
481
|
+
await emitCrudSideEffects({
|
|
482
|
+
dataEngine: de,
|
|
483
|
+
action,
|
|
484
|
+
entity,
|
|
485
|
+
identifiers: {
|
|
486
|
+
id: entity.id,
|
|
487
|
+
organizationId: entity.organizationId,
|
|
488
|
+
tenantId: entity.tenantId
|
|
489
|
+
},
|
|
490
|
+
indexer: receivingLineCrudIndexer,
|
|
491
|
+
events: receivingLineCrudEvents
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const createAsnCommand = {
|
|
496
|
+
id: "wms.asns.create",
|
|
497
|
+
async execute(rawInput, ctx) {
|
|
498
|
+
const input = asnCreateSchema.parse(rawInput ?? {});
|
|
499
|
+
ensureTenantScope(ctx, input.tenantId);
|
|
500
|
+
ensureOrganizationScope(ctx, input.organizationId);
|
|
501
|
+
const em = resolveEm(ctx);
|
|
502
|
+
const scope = resolveScope(ctx, input);
|
|
503
|
+
await requireWarehouse(em, ctx, input.warehouseId, scope);
|
|
504
|
+
await requireVendorIfPresent(ctx, input.vendorId, scope);
|
|
505
|
+
const status = input.status ?? "draft";
|
|
506
|
+
const createdLines = [];
|
|
507
|
+
let asn;
|
|
508
|
+
await withAtomicFlush(
|
|
509
|
+
em,
|
|
510
|
+
[
|
|
511
|
+
() => {
|
|
512
|
+
asn = em.create(Asn, {
|
|
513
|
+
organizationId: scope.organizationId,
|
|
514
|
+
tenantId: scope.tenantId,
|
|
515
|
+
warehouse: em.getReference(Warehouse, input.warehouseId),
|
|
516
|
+
vendorId: input.vendorId ?? null,
|
|
517
|
+
status,
|
|
518
|
+
expectedAt: input.expectedAt,
|
|
519
|
+
referenceNumber: normalizeOptionalString(input.referenceNumber ?? void 0),
|
|
520
|
+
// sourceKey is server/system-only (auth null). Authenticated HTTP creates ignore it.
|
|
521
|
+
sourceKey: ctx.auth == null ? normalizeOptionalString(input.sourceKey ?? void 0) : null,
|
|
522
|
+
notes: normalizeOptionalString(input.notes ?? void 0),
|
|
523
|
+
metadata: toJsonValue(input.metadata ?? null) ?? null
|
|
524
|
+
});
|
|
525
|
+
em.persist(asn);
|
|
526
|
+
},
|
|
527
|
+
() => {
|
|
528
|
+
for (const lineInput of input.lines ?? []) {
|
|
529
|
+
const line = em.create(ReceivingLine, {
|
|
530
|
+
organizationId: scope.organizationId,
|
|
531
|
+
tenantId: scope.tenantId,
|
|
532
|
+
asn,
|
|
533
|
+
catalogVariantId: lineInput.catalogVariantId,
|
|
534
|
+
expectedQty: toNumericString(lineInput.expectedQty),
|
|
535
|
+
receivedQty: "0",
|
|
536
|
+
lotNumber: normalizeOptionalString(lineInput.lotNumber ?? void 0),
|
|
537
|
+
serialNumbers: lineInput.serialNumbers ?? null,
|
|
538
|
+
qcStatus: "pending",
|
|
539
|
+
targetStagingLocationId: lineInput.targetStagingLocationId ?? null,
|
|
540
|
+
metadata: toJsonValue(lineInput.metadata ?? null) ?? null
|
|
541
|
+
});
|
|
542
|
+
em.persist(line);
|
|
543
|
+
createdLines.push(line);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
],
|
|
547
|
+
{ transaction: true, label: "wms.asns.create" }
|
|
548
|
+
);
|
|
549
|
+
await emitAsnSideEffects(ctx, {
|
|
550
|
+
asns: [{ entity: asn, action: "created" }],
|
|
551
|
+
lines: createdLines.map((entity) => ({ entity, action: "created" }))
|
|
552
|
+
});
|
|
553
|
+
void emitWmsEvent("wms.asn.created", {
|
|
554
|
+
id: asn.id,
|
|
555
|
+
warehouseId: input.warehouseId,
|
|
556
|
+
vendorId: input.vendorId ?? null,
|
|
557
|
+
status: asn.status,
|
|
558
|
+
tenantId: scope.tenantId,
|
|
559
|
+
organizationId: scope.organizationId
|
|
560
|
+
}).catch(() => void 0);
|
|
561
|
+
return { asnId: asn.id, lineIds: createdLines.map((line) => line.id) };
|
|
562
|
+
},
|
|
563
|
+
captureAfter: async (_input, result, ctx) => {
|
|
564
|
+
if (!result?.asnId) return null;
|
|
565
|
+
const em = resolveEm(ctx);
|
|
566
|
+
const asn = await loadAsn(em, ctx, result.asnId);
|
|
567
|
+
return snapshotAsn(asn);
|
|
568
|
+
},
|
|
569
|
+
undo: async ({ logEntry, ctx }) => {
|
|
570
|
+
const payload = extractUndoPayload(logEntry);
|
|
571
|
+
const after = payload?.after;
|
|
572
|
+
if (!after?.id) return;
|
|
573
|
+
const em = resolveEm(ctx);
|
|
574
|
+
const scope = { tenantId: after.tenantId, organizationId: after.organizationId };
|
|
575
|
+
const existing = await findOneWithDecryption(em, Asn, { id: after.id }, void 0, scope);
|
|
576
|
+
if (!existing || existing.deletedAt) return;
|
|
577
|
+
const locked = await withLockedAsn(em, ctx, after.id, scope, async (asn, trx) => {
|
|
578
|
+
assertAsnMutable(asn);
|
|
579
|
+
const lines = await findWithDecryption(
|
|
580
|
+
trx,
|
|
581
|
+
ReceivingLine,
|
|
582
|
+
{ asn: asn.id, deletedAt: null },
|
|
583
|
+
void 0,
|
|
584
|
+
scope
|
|
585
|
+
);
|
|
586
|
+
if (hasAsnDeleteBlockingLineActivity(lines)) {
|
|
587
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
588
|
+
}
|
|
589
|
+
const deletedAt = /* @__PURE__ */ new Date();
|
|
590
|
+
asn.deletedAt = deletedAt;
|
|
591
|
+
for (const line of lines) {
|
|
592
|
+
line.deletedAt = deletedAt;
|
|
593
|
+
}
|
|
594
|
+
await trx.flush();
|
|
595
|
+
return { asn, lines };
|
|
596
|
+
});
|
|
597
|
+
await emitAsnSideEffects(ctx, {
|
|
598
|
+
asns: [{ entity: locked.asn, action: "deleted" }],
|
|
599
|
+
lines: locked.lines.map((entity) => ({ entity, action: "deleted" }))
|
|
600
|
+
});
|
|
601
|
+
},
|
|
602
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
603
|
+
const base = await buildCrudLog(
|
|
604
|
+
ctx,
|
|
605
|
+
input,
|
|
606
|
+
result?.asnId ?? null,
|
|
607
|
+
"wms.audit.asn.create",
|
|
608
|
+
"Create ASN",
|
|
609
|
+
WMS_ASN_RESOURCE
|
|
610
|
+
);
|
|
611
|
+
const after = snapshots?.after;
|
|
612
|
+
return {
|
|
613
|
+
...base,
|
|
614
|
+
snapshotAfter: after,
|
|
615
|
+
payload: { undo: { after, lineIds: result?.lineIds ?? [] } }
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const updateAsnCommand = {
|
|
620
|
+
id: "wms.asns.update",
|
|
621
|
+
async prepare(rawInput, ctx) {
|
|
622
|
+
const id = requireId(rawInput?.id, "ASN");
|
|
623
|
+
const em = resolveEm(ctx);
|
|
624
|
+
const asn = await loadAsn(em, ctx, id);
|
|
625
|
+
assertAsnMutable(asn);
|
|
626
|
+
return { before: snapshotAsn(asn) };
|
|
627
|
+
},
|
|
628
|
+
async execute(rawInput, ctx) {
|
|
629
|
+
const input = asnUpdateSchema.parse(rawInput ?? {});
|
|
630
|
+
ensureTenantScope(ctx, input.tenantId ?? ctx.auth?.tenantId ?? "");
|
|
631
|
+
if (input.organizationId) ensureOrganizationScope(ctx, input.organizationId);
|
|
632
|
+
const em = resolveEm(ctx);
|
|
633
|
+
const asn = await withLockedAsn(em, ctx, input.id, resolveScope(ctx), async (asn2, trx) => {
|
|
634
|
+
assertAsnMutable(asn2);
|
|
635
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
636
|
+
resourceKind: WMS_ASN_RESOURCE,
|
|
637
|
+
resourceId: asn2.id,
|
|
638
|
+
current: asn2.updatedAt,
|
|
639
|
+
request: ctx.request ?? null
|
|
640
|
+
});
|
|
641
|
+
const scope = resolveScope(ctx, {
|
|
642
|
+
tenantId: asn2.tenantId,
|
|
643
|
+
organizationId: asn2.organizationId
|
|
644
|
+
});
|
|
645
|
+
const lines = await findWithDecryption(
|
|
646
|
+
trx,
|
|
647
|
+
ReceivingLine,
|
|
648
|
+
{ asn: asn2.id, deletedAt: null },
|
|
649
|
+
void 0,
|
|
650
|
+
scope
|
|
651
|
+
);
|
|
652
|
+
if (hasAsnDeleteBlockingLineActivity(lines)) {
|
|
653
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
654
|
+
}
|
|
655
|
+
if (input.warehouseId) {
|
|
656
|
+
await requireWarehouse(trx, ctx, input.warehouseId, scope);
|
|
657
|
+
asn2.warehouse = trx.getReference(Warehouse, input.warehouseId);
|
|
658
|
+
}
|
|
659
|
+
if (input.vendorId !== void 0) {
|
|
660
|
+
await requireVendorIfPresent(ctx, input.vendorId, scope);
|
|
661
|
+
asn2.vendorId = input.vendorId;
|
|
662
|
+
}
|
|
663
|
+
if (input.status !== void 0) {
|
|
664
|
+
asn2.status = input.status;
|
|
665
|
+
}
|
|
666
|
+
if (input.expectedAt !== void 0) asn2.expectedAt = input.expectedAt;
|
|
667
|
+
if (input.referenceNumber !== void 0) {
|
|
668
|
+
asn2.referenceNumber = normalizeOptionalString(input.referenceNumber ?? void 0);
|
|
669
|
+
}
|
|
670
|
+
if (input.notes !== void 0) asn2.notes = normalizeOptionalString(input.notes ?? void 0);
|
|
671
|
+
if (input.metadata !== void 0) asn2.metadata = toJsonValue(input.metadata) ?? null;
|
|
672
|
+
await trx.flush();
|
|
673
|
+
return asn2;
|
|
674
|
+
});
|
|
675
|
+
await emitAsnSideEffects(ctx, { asns: [{ entity: asn, action: "updated" }] });
|
|
676
|
+
void emitWmsEvent("wms.asn.updated", {
|
|
677
|
+
id: asn.id,
|
|
678
|
+
status: asn.status,
|
|
679
|
+
tenantId: asn.tenantId,
|
|
680
|
+
organizationId: asn.organizationId
|
|
681
|
+
}).catch(() => void 0);
|
|
682
|
+
return { asnId: asn.id };
|
|
683
|
+
},
|
|
684
|
+
captureAfter: async (_input, result, ctx) => {
|
|
685
|
+
if (!result?.asnId) return null;
|
|
686
|
+
const em = resolveEm(ctx);
|
|
687
|
+
const asn = await loadAsn(em, ctx, result.asnId);
|
|
688
|
+
return snapshotAsn(asn);
|
|
689
|
+
},
|
|
690
|
+
undo: async ({ logEntry, ctx }) => {
|
|
691
|
+
const payload = extractUndoPayload(logEntry);
|
|
692
|
+
const before = payload?.before;
|
|
693
|
+
if (!before?.id) return;
|
|
694
|
+
const em = resolveEm(ctx);
|
|
695
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId };
|
|
696
|
+
const asn = await withLockedAsn(em, ctx, before.id, scope, async (asn2, trx) => {
|
|
697
|
+
assertAsnMutable(asn2);
|
|
698
|
+
const lines = await findWithDecryption(
|
|
699
|
+
trx,
|
|
700
|
+
ReceivingLine,
|
|
701
|
+
{ asn: asn2.id, deletedAt: null },
|
|
702
|
+
void 0,
|
|
703
|
+
scope
|
|
704
|
+
);
|
|
705
|
+
if (hasAsnDeleteBlockingLineActivity(lines)) {
|
|
706
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
707
|
+
}
|
|
708
|
+
asn2.warehouse = trx.getReference(Warehouse, before.warehouseId);
|
|
709
|
+
asn2.vendorId = before.vendorId;
|
|
710
|
+
asn2.status = before.status;
|
|
711
|
+
asn2.expectedAt = new Date(before.expectedAt);
|
|
712
|
+
asn2.referenceNumber = before.referenceNumber;
|
|
713
|
+
asn2.notes = before.notes;
|
|
714
|
+
asn2.metadata = before.metadata;
|
|
715
|
+
asn2.deletedAt = null;
|
|
716
|
+
await trx.flush();
|
|
717
|
+
return asn2;
|
|
718
|
+
});
|
|
719
|
+
await emitAsnSideEffects(ctx, { asns: [{ entity: asn, action: "updated" }] });
|
|
720
|
+
},
|
|
721
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
722
|
+
const base = await buildCrudLog(
|
|
723
|
+
ctx,
|
|
724
|
+
input,
|
|
725
|
+
result?.asnId ?? null,
|
|
726
|
+
"wms.audit.asn.update",
|
|
727
|
+
"Update ASN",
|
|
728
|
+
WMS_ASN_RESOURCE
|
|
729
|
+
);
|
|
730
|
+
const before = snapshots?.before;
|
|
731
|
+
const after = snapshots?.after;
|
|
732
|
+
return {
|
|
733
|
+
...base,
|
|
734
|
+
snapshotBefore: before,
|
|
735
|
+
snapshotAfter: after,
|
|
736
|
+
payload: { undo: { before, after } }
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
const deleteAsnCommand = {
|
|
741
|
+
id: "wms.asns.delete",
|
|
742
|
+
async prepare(rawInput, ctx) {
|
|
743
|
+
const id = requireId(rawInput?.id, "ASN");
|
|
744
|
+
const em = resolveEm(ctx);
|
|
745
|
+
const asn = await loadAsn(em, ctx, id);
|
|
746
|
+
const lines = await findWithDecryption(
|
|
747
|
+
em,
|
|
748
|
+
ReceivingLine,
|
|
749
|
+
{ asn: asn.id, deletedAt: null },
|
|
750
|
+
void 0,
|
|
751
|
+
{ tenantId: asn.tenantId, organizationId: asn.organizationId }
|
|
752
|
+
);
|
|
753
|
+
return {
|
|
754
|
+
before: snapshotAsn(asn),
|
|
755
|
+
lineIds: lines.map((line) => line.id)
|
|
756
|
+
};
|
|
757
|
+
},
|
|
758
|
+
async execute(rawInput, ctx) {
|
|
759
|
+
const id = requireId(rawInput?.id, "ASN");
|
|
760
|
+
const em = resolveEm(ctx);
|
|
761
|
+
const locked = await withLockedAsn(em, ctx, id, resolveScope(ctx), async (asn, trx) => {
|
|
762
|
+
assertAsnMutable(asn);
|
|
763
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
764
|
+
resourceKind: WMS_ASN_RESOURCE,
|
|
765
|
+
resourceId: asn.id,
|
|
766
|
+
current: asn.updatedAt,
|
|
767
|
+
request: ctx.request ?? null
|
|
768
|
+
});
|
|
769
|
+
const scope = { tenantId: asn.tenantId, organizationId: asn.organizationId };
|
|
770
|
+
const lines = await findWithDecryption(
|
|
771
|
+
trx,
|
|
772
|
+
ReceivingLine,
|
|
773
|
+
{ asn: asn.id, deletedAt: null },
|
|
774
|
+
void 0,
|
|
775
|
+
scope
|
|
776
|
+
);
|
|
777
|
+
if (hasAsnDeleteBlockingLineActivity(lines)) {
|
|
778
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
779
|
+
}
|
|
780
|
+
if (await hasOpenPutawayForAsn(trx, scope, asn.id)) {
|
|
781
|
+
throw new CrudHttpError(409, { error: "asn_has_open_putaway" });
|
|
782
|
+
}
|
|
783
|
+
const deletedAt = /* @__PURE__ */ new Date();
|
|
784
|
+
asn.deletedAt = deletedAt;
|
|
785
|
+
for (const line of lines) {
|
|
786
|
+
line.deletedAt = deletedAt;
|
|
787
|
+
}
|
|
788
|
+
await trx.flush();
|
|
789
|
+
return { asn, lines };
|
|
790
|
+
});
|
|
791
|
+
await emitAsnSideEffects(ctx, {
|
|
792
|
+
asns: [{ entity: locked.asn, action: "deleted" }],
|
|
793
|
+
lines: locked.lines.map((entity) => ({ entity, action: "deleted" }))
|
|
794
|
+
});
|
|
795
|
+
return { asnId: locked.asn.id, lineIds: locked.lines.map((line) => line.id) };
|
|
796
|
+
},
|
|
797
|
+
undo: async ({ logEntry, ctx }) => {
|
|
798
|
+
const payload = extractUndoPayload(logEntry);
|
|
799
|
+
const before = payload?.before;
|
|
800
|
+
if (!before?.id) return;
|
|
801
|
+
const em = resolveEm(ctx);
|
|
802
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId };
|
|
803
|
+
const asn = await findOneWithDecryption(em, Asn, { id: before.id }, void 0, scope);
|
|
804
|
+
if (!asn) return;
|
|
805
|
+
const sourceKey = normalizeOptionalString(asn.sourceKey ?? void 0);
|
|
806
|
+
if (sourceKey) {
|
|
807
|
+
const clash = await findOneWithDecryption(
|
|
808
|
+
em,
|
|
809
|
+
Asn,
|
|
810
|
+
{
|
|
811
|
+
organizationId: scope.organizationId,
|
|
812
|
+
tenantId: scope.tenantId,
|
|
813
|
+
sourceKey,
|
|
814
|
+
deletedAt: null,
|
|
815
|
+
id: { $ne: asn.id }
|
|
816
|
+
},
|
|
817
|
+
void 0,
|
|
818
|
+
scope
|
|
819
|
+
);
|
|
820
|
+
if (clash) {
|
|
821
|
+
throw new CrudHttpError(409, { error: "asn_source_key_conflict" });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
asn.deletedAt = null;
|
|
825
|
+
const lineIds = Array.isArray(payload?.lineIds) ? payload.lineIds : [];
|
|
826
|
+
const restoredLines = [];
|
|
827
|
+
if (lineIds.length > 0) {
|
|
828
|
+
const lines = await findWithDecryption(
|
|
829
|
+
em,
|
|
830
|
+
ReceivingLine,
|
|
831
|
+
{ id: { $in: lineIds }, asn: asn.id },
|
|
832
|
+
void 0,
|
|
833
|
+
scope
|
|
834
|
+
);
|
|
835
|
+
for (const line of lines) {
|
|
836
|
+
line.deletedAt = null;
|
|
837
|
+
restoredLines.push(line);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
await em.flush();
|
|
842
|
+
} catch (error) {
|
|
843
|
+
if (sourceKey && isUniqueConstraintError(error)) {
|
|
844
|
+
throw new CrudHttpError(409, { error: "asn_source_key_conflict" });
|
|
845
|
+
}
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
await emitAsnSideEffects(ctx, {
|
|
849
|
+
asns: [{ entity: asn, action: "updated" }],
|
|
850
|
+
lines: restoredLines.map((entity) => ({ entity, action: "updated" }))
|
|
851
|
+
});
|
|
852
|
+
},
|
|
853
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
854
|
+
const base = await buildCrudLog(
|
|
855
|
+
ctx,
|
|
856
|
+
input,
|
|
857
|
+
result?.asnId ?? null,
|
|
858
|
+
"wms.audit.asn.delete",
|
|
859
|
+
"Delete ASN",
|
|
860
|
+
WMS_ASN_RESOURCE
|
|
861
|
+
);
|
|
862
|
+
const before = snapshots?.before;
|
|
863
|
+
const lineIds = result?.lineIds ?? [];
|
|
864
|
+
return {
|
|
865
|
+
...base,
|
|
866
|
+
snapshotBefore: before,
|
|
867
|
+
payload: { undo: { before, lineIds } }
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
const createReceivingLineCommand = {
|
|
872
|
+
id: "wms.receiving-lines.create",
|
|
873
|
+
async execute(rawInput, ctx) {
|
|
874
|
+
const input = receivingLineCreateSchema.parse(rawInput ?? {});
|
|
875
|
+
ensureTenantScope(ctx, input.tenantId);
|
|
876
|
+
ensureOrganizationScope(ctx, input.organizationId);
|
|
877
|
+
const em = resolveEm(ctx);
|
|
878
|
+
const line = await withLockedAsn(em, ctx, input.asnId, resolveScope(ctx, input), async (asn, trx) => {
|
|
879
|
+
assertAsnMutable(asn);
|
|
880
|
+
const created = trx.create(ReceivingLine, {
|
|
881
|
+
organizationId: asn.organizationId,
|
|
882
|
+
tenantId: asn.tenantId,
|
|
883
|
+
asn,
|
|
884
|
+
catalogVariantId: input.catalogVariantId,
|
|
885
|
+
expectedQty: toNumericString(input.expectedQty),
|
|
886
|
+
receivedQty: "0",
|
|
887
|
+
lotNumber: normalizeOptionalString(input.lotNumber ?? void 0),
|
|
888
|
+
serialNumbers: input.serialNumbers ?? null,
|
|
889
|
+
qcStatus: "pending",
|
|
890
|
+
targetStagingLocationId: input.targetStagingLocationId ?? null,
|
|
891
|
+
metadata: toJsonValue(input.metadata ?? null) ?? null
|
|
892
|
+
});
|
|
893
|
+
trx.persist(created);
|
|
894
|
+
await trx.flush();
|
|
895
|
+
return created;
|
|
896
|
+
});
|
|
897
|
+
await emitAsnSideEffects(ctx, { lines: [{ entity: line, action: "created" }] });
|
|
898
|
+
return { lineId: line.id };
|
|
899
|
+
},
|
|
900
|
+
captureAfter: async (_input, result, ctx) => {
|
|
901
|
+
if (!result?.lineId) return null;
|
|
902
|
+
const em = resolveEm(ctx);
|
|
903
|
+
const line = await loadReceivingLine(em, ctx, result.lineId);
|
|
904
|
+
return snapshotReceivingLine(line);
|
|
905
|
+
},
|
|
906
|
+
undo: async ({ logEntry, ctx }) => {
|
|
907
|
+
const payload = extractUndoPayload(logEntry);
|
|
908
|
+
const after = payload?.after;
|
|
909
|
+
if (!after?.id) return;
|
|
910
|
+
const em = resolveEm(ctx);
|
|
911
|
+
const scope = { tenantId: after.tenantId, organizationId: after.organizationId };
|
|
912
|
+
const line = await withLockedAsnReceivingLine(
|
|
913
|
+
em,
|
|
914
|
+
ctx,
|
|
915
|
+
after.id,
|
|
916
|
+
scope,
|
|
917
|
+
{ requireActiveLine: true },
|
|
918
|
+
async ({ asn, line: line2, trx }) => {
|
|
919
|
+
assertAsnMutable(asn);
|
|
920
|
+
if (hasAsnDeleteBlockingLineActivity([line2])) {
|
|
921
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
922
|
+
}
|
|
923
|
+
line2.deletedAt = /* @__PURE__ */ new Date();
|
|
924
|
+
await trx.flush();
|
|
925
|
+
return line2;
|
|
926
|
+
}
|
|
927
|
+
);
|
|
928
|
+
if (!line) return;
|
|
929
|
+
await emitAsnSideEffects(ctx, { lines: [{ entity: line, action: "deleted" }] });
|
|
930
|
+
},
|
|
931
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
932
|
+
const base = await buildCrudLog(
|
|
933
|
+
ctx,
|
|
934
|
+
input,
|
|
935
|
+
result?.lineId ?? null,
|
|
936
|
+
"wms.audit.receivingLine.create",
|
|
937
|
+
"Create receiving line",
|
|
938
|
+
WMS_RECEIVING_LINE_RESOURCE
|
|
939
|
+
);
|
|
940
|
+
const after = snapshots?.after;
|
|
941
|
+
return { ...base, snapshotAfter: after, payload: { undo: { after } } };
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
const updateReceivingLineCommand = {
|
|
945
|
+
id: "wms.receiving-lines.update",
|
|
946
|
+
async prepare(rawInput, ctx) {
|
|
947
|
+
const id = requireId(rawInput?.id, "Receiving line");
|
|
948
|
+
const em = resolveEm(ctx);
|
|
949
|
+
const line = await loadReceivingLine(em, ctx, id);
|
|
950
|
+
return { before: snapshotReceivingLine(line) };
|
|
951
|
+
},
|
|
952
|
+
async execute(rawInput, ctx) {
|
|
953
|
+
assertReceivingLineLifecycleFieldsForbidden(rawInput);
|
|
954
|
+
const input = receivingLineUpdateSchema.parse(rawInput ?? {});
|
|
955
|
+
const rootEm = resolveEm(ctx);
|
|
956
|
+
const line = await rootEm.transactional(async (trx) => {
|
|
957
|
+
const unlocked = await findOneWithDecryption(
|
|
958
|
+
trx,
|
|
959
|
+
ReceivingLine,
|
|
960
|
+
{ id: input.id, deletedAt: null },
|
|
961
|
+
void 0,
|
|
962
|
+
resolveScope(ctx)
|
|
963
|
+
);
|
|
964
|
+
if (!unlocked) throw new CrudHttpError(404, { error: "Receiving line not found." });
|
|
965
|
+
ensureTenantScope(ctx, unlocked.tenantId);
|
|
966
|
+
ensureOrganizationScope(ctx, unlocked.organizationId);
|
|
967
|
+
const asnId = typeof unlocked.asn === "string" ? unlocked.asn : unlocked.asn.id;
|
|
968
|
+
const scope = { tenantId: unlocked.tenantId, organizationId: unlocked.organizationId };
|
|
969
|
+
const asn = await findOneWithDecryption(
|
|
970
|
+
trx,
|
|
971
|
+
Asn,
|
|
972
|
+
{ id: asnId, deletedAt: null },
|
|
973
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
974
|
+
scope
|
|
975
|
+
);
|
|
976
|
+
if (!asn) throw new CrudHttpError(404, { error: "ASN not found." });
|
|
977
|
+
ensureTenantScope(ctx, asn.tenantId);
|
|
978
|
+
ensureOrganizationScope(ctx, asn.organizationId);
|
|
979
|
+
assertAsnMutable(asn);
|
|
980
|
+
const locked = await findOneWithDecryption(
|
|
981
|
+
trx,
|
|
982
|
+
ReceivingLine,
|
|
983
|
+
{ id: input.id, deletedAt: null },
|
|
984
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
985
|
+
scope
|
|
986
|
+
);
|
|
987
|
+
if (!locked) throw new CrudHttpError(404, { error: "Receiving line not found." });
|
|
988
|
+
ensureTenantScope(ctx, locked.tenantId);
|
|
989
|
+
ensureOrganizationScope(ctx, locked.organizationId);
|
|
990
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
991
|
+
resourceKind: WMS_RECEIVING_LINE_RESOURCE,
|
|
992
|
+
resourceId: locked.id,
|
|
993
|
+
current: locked.updatedAt,
|
|
994
|
+
request: ctx.request ?? null
|
|
995
|
+
});
|
|
996
|
+
if (hasAsnDeleteBlockingLineActivity([locked])) {
|
|
997
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
998
|
+
}
|
|
999
|
+
if (input.catalogVariantId !== void 0) locked.catalogVariantId = input.catalogVariantId;
|
|
1000
|
+
if (input.expectedQty !== void 0) locked.expectedQty = toNumericString(input.expectedQty);
|
|
1001
|
+
if (input.lotNumber !== void 0) {
|
|
1002
|
+
locked.lotNumber = normalizeOptionalString(input.lotNumber ?? void 0);
|
|
1003
|
+
}
|
|
1004
|
+
if (input.serialNumbers !== void 0) locked.serialNumbers = input.serialNumbers;
|
|
1005
|
+
if (input.targetStagingLocationId !== void 0) {
|
|
1006
|
+
locked.targetStagingLocationId = input.targetStagingLocationId;
|
|
1007
|
+
}
|
|
1008
|
+
if (input.metadata !== void 0) locked.metadata = toJsonValue(input.metadata) ?? null;
|
|
1009
|
+
await trx.flush();
|
|
1010
|
+
return locked;
|
|
1011
|
+
});
|
|
1012
|
+
await emitAsnSideEffects(ctx, { lines: [{ entity: line, action: "updated" }] });
|
|
1013
|
+
return { lineId: line.id };
|
|
1014
|
+
},
|
|
1015
|
+
captureAfter: async (_input, result, ctx) => {
|
|
1016
|
+
if (!result?.lineId) return null;
|
|
1017
|
+
const em = resolveEm(ctx);
|
|
1018
|
+
const line = await loadReceivingLine(em, ctx, result.lineId);
|
|
1019
|
+
return snapshotReceivingLine(line);
|
|
1020
|
+
},
|
|
1021
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1022
|
+
const payload = extractUndoPayload(logEntry);
|
|
1023
|
+
const before = payload?.before;
|
|
1024
|
+
if (!before?.id) return;
|
|
1025
|
+
const em = resolveEm(ctx);
|
|
1026
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId };
|
|
1027
|
+
const line = await withLockedAsnReceivingLine(
|
|
1028
|
+
em,
|
|
1029
|
+
ctx,
|
|
1030
|
+
before.id,
|
|
1031
|
+
scope,
|
|
1032
|
+
{ requireActiveLine: false },
|
|
1033
|
+
async ({ asn, line: line2, trx }) => {
|
|
1034
|
+
assertAsnMutable(asn);
|
|
1035
|
+
if (hasAsnDeleteBlockingLineActivity([line2])) {
|
|
1036
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
1037
|
+
}
|
|
1038
|
+
if (hasAsnDeleteBlockingLineActivity([before])) {
|
|
1039
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
1040
|
+
}
|
|
1041
|
+
line2.catalogVariantId = before.catalogVariantId;
|
|
1042
|
+
line2.expectedQty = before.expectedQty;
|
|
1043
|
+
line2.receivedQty = before.receivedQty;
|
|
1044
|
+
line2.lotNumber = before.lotNumber;
|
|
1045
|
+
line2.serialNumbers = before.serialNumbers;
|
|
1046
|
+
line2.qcStatus = before.qcStatus;
|
|
1047
|
+
line2.targetStagingLocationId = before.targetStagingLocationId;
|
|
1048
|
+
line2.rejectionReason = before.rejectionReason;
|
|
1049
|
+
line2.metadata = before.metadata;
|
|
1050
|
+
line2.deletedAt = null;
|
|
1051
|
+
await trx.flush();
|
|
1052
|
+
return line2;
|
|
1053
|
+
}
|
|
1054
|
+
);
|
|
1055
|
+
if (!line) return;
|
|
1056
|
+
await emitAsnSideEffects(ctx, { lines: [{ entity: line, action: "updated" }] });
|
|
1057
|
+
},
|
|
1058
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1059
|
+
const base = await buildCrudLog(
|
|
1060
|
+
ctx,
|
|
1061
|
+
input,
|
|
1062
|
+
result?.lineId ?? null,
|
|
1063
|
+
"wms.audit.receivingLine.update",
|
|
1064
|
+
"Update receiving line",
|
|
1065
|
+
WMS_RECEIVING_LINE_RESOURCE
|
|
1066
|
+
);
|
|
1067
|
+
const before = snapshots?.before;
|
|
1068
|
+
const after = snapshots?.after;
|
|
1069
|
+
return {
|
|
1070
|
+
...base,
|
|
1071
|
+
snapshotBefore: before,
|
|
1072
|
+
snapshotAfter: after,
|
|
1073
|
+
payload: { undo: { before, after } }
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
const deleteReceivingLineCommand = {
|
|
1078
|
+
id: "wms.receiving-lines.delete",
|
|
1079
|
+
async prepare(rawInput, ctx) {
|
|
1080
|
+
const id = requireId(rawInput?.id, "Receiving line");
|
|
1081
|
+
const em = resolveEm(ctx);
|
|
1082
|
+
const line = await loadReceivingLine(em, ctx, id);
|
|
1083
|
+
return { before: snapshotReceivingLine(line) };
|
|
1084
|
+
},
|
|
1085
|
+
async execute(rawInput, ctx) {
|
|
1086
|
+
const id = requireId(rawInput?.id, "Receiving line");
|
|
1087
|
+
const rootEm = resolveEm(ctx);
|
|
1088
|
+
const line = await rootEm.transactional(async (trx) => {
|
|
1089
|
+
const unlocked = await findOneWithDecryption(
|
|
1090
|
+
trx,
|
|
1091
|
+
ReceivingLine,
|
|
1092
|
+
{ id, deletedAt: null },
|
|
1093
|
+
void 0,
|
|
1094
|
+
resolveScope(ctx)
|
|
1095
|
+
);
|
|
1096
|
+
if (!unlocked) throw new CrudHttpError(404, { error: "Receiving line not found." });
|
|
1097
|
+
ensureTenantScope(ctx, unlocked.tenantId);
|
|
1098
|
+
ensureOrganizationScope(ctx, unlocked.organizationId);
|
|
1099
|
+
const asnId = typeof unlocked.asn === "string" ? unlocked.asn : unlocked.asn.id;
|
|
1100
|
+
const scope = { tenantId: unlocked.tenantId, organizationId: unlocked.organizationId };
|
|
1101
|
+
const asn = await findOneWithDecryption(
|
|
1102
|
+
trx,
|
|
1103
|
+
Asn,
|
|
1104
|
+
{ id: asnId, deletedAt: null },
|
|
1105
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1106
|
+
scope
|
|
1107
|
+
);
|
|
1108
|
+
if (!asn) throw new CrudHttpError(404, { error: "ASN not found." });
|
|
1109
|
+
ensureTenantScope(ctx, asn.tenantId);
|
|
1110
|
+
ensureOrganizationScope(ctx, asn.organizationId);
|
|
1111
|
+
assertAsnMutable(asn);
|
|
1112
|
+
const locked = await findOneWithDecryption(
|
|
1113
|
+
trx,
|
|
1114
|
+
ReceivingLine,
|
|
1115
|
+
{ id, deletedAt: null },
|
|
1116
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1117
|
+
scope
|
|
1118
|
+
);
|
|
1119
|
+
if (!locked) throw new CrudHttpError(404, { error: "Receiving line not found." });
|
|
1120
|
+
ensureTenantScope(ctx, locked.tenantId);
|
|
1121
|
+
ensureOrganizationScope(ctx, locked.organizationId);
|
|
1122
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1123
|
+
resourceKind: WMS_RECEIVING_LINE_RESOURCE,
|
|
1124
|
+
resourceId: locked.id,
|
|
1125
|
+
current: locked.updatedAt,
|
|
1126
|
+
request: ctx.request ?? null
|
|
1127
|
+
});
|
|
1128
|
+
if (hasAsnDeleteBlockingLineActivity([locked])) {
|
|
1129
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
1130
|
+
}
|
|
1131
|
+
locked.deletedAt = /* @__PURE__ */ new Date();
|
|
1132
|
+
await trx.flush();
|
|
1133
|
+
return locked;
|
|
1134
|
+
});
|
|
1135
|
+
await emitAsnSideEffects(ctx, { lines: [{ entity: line, action: "deleted" }] });
|
|
1136
|
+
return { lineId: line.id };
|
|
1137
|
+
},
|
|
1138
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1139
|
+
const payload = extractUndoPayload(logEntry);
|
|
1140
|
+
const before = payload?.before;
|
|
1141
|
+
if (!before?.id) return;
|
|
1142
|
+
const em = resolveEm(ctx);
|
|
1143
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId };
|
|
1144
|
+
const line = await withLockedAsnReceivingLine(
|
|
1145
|
+
em,
|
|
1146
|
+
ctx,
|
|
1147
|
+
before.id,
|
|
1148
|
+
scope,
|
|
1149
|
+
{ requireActiveLine: false },
|
|
1150
|
+
async ({ asn, line: line2, trx }) => {
|
|
1151
|
+
assertAsnMutable(asn);
|
|
1152
|
+
if (hasAsnDeleteBlockingLineActivity([line2])) {
|
|
1153
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
1154
|
+
}
|
|
1155
|
+
if (hasAsnDeleteBlockingLineActivity([before])) {
|
|
1156
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
1157
|
+
}
|
|
1158
|
+
line2.deletedAt = null;
|
|
1159
|
+
await trx.flush();
|
|
1160
|
+
return line2;
|
|
1161
|
+
}
|
|
1162
|
+
);
|
|
1163
|
+
if (!line) return;
|
|
1164
|
+
await emitAsnSideEffects(ctx, { lines: [{ entity: line, action: "updated" }] });
|
|
1165
|
+
},
|
|
1166
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1167
|
+
const base = await buildCrudLog(
|
|
1168
|
+
ctx,
|
|
1169
|
+
input,
|
|
1170
|
+
result?.lineId ?? null,
|
|
1171
|
+
"wms.audit.receivingLine.delete",
|
|
1172
|
+
"Delete receiving line",
|
|
1173
|
+
WMS_RECEIVING_LINE_RESOURCE
|
|
1174
|
+
);
|
|
1175
|
+
const before = snapshots?.before;
|
|
1176
|
+
return { ...base, snapshotBefore: before, payload: { undo: { before } } };
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
async function ensureSerialReceiptPutaways(trx, scope, input) {
|
|
1180
|
+
const links = [];
|
|
1181
|
+
for (const serialNumber of input.serialNumbers) {
|
|
1182
|
+
const putawayKey = buildAsnReceivePutawayKey({
|
|
1183
|
+
attemptKey: input.attemptKey,
|
|
1184
|
+
serialNumber
|
|
1185
|
+
});
|
|
1186
|
+
const existing = await findExistingPutawayForReceipt(trx, scope, putawayKey);
|
|
1187
|
+
if (existing) {
|
|
1188
|
+
links.push({ task: existing, created: false });
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
let lotId = input.lotId;
|
|
1192
|
+
if (input.recreateWhenMissing) {
|
|
1193
|
+
const cancelled = lotId == null ? await findCancelledPutawayForReceipt(trx, scope, putawayKey) : null;
|
|
1194
|
+
if (lotId == null) lotId = cancelled?.lotId ?? null;
|
|
1195
|
+
const availability = await resolveUncommittedStagingPutawayQuantity(
|
|
1196
|
+
trx,
|
|
1197
|
+
scope,
|
|
1198
|
+
{
|
|
1199
|
+
warehouseId: input.warehouseId,
|
|
1200
|
+
sourceLocationId: input.sourceLocationId,
|
|
1201
|
+
catalogVariantId: input.catalogVariantId,
|
|
1202
|
+
lotId,
|
|
1203
|
+
serialNumber
|
|
1204
|
+
},
|
|
1205
|
+
{ lock: true }
|
|
1206
|
+
);
|
|
1207
|
+
const canCreate = availability != null && shouldRecreatePutawayOnAlreadyAtTarget({
|
|
1208
|
+
requestedQuantity: 1,
|
|
1209
|
+
remainingAvailable: availability.remaining
|
|
1210
|
+
});
|
|
1211
|
+
if (!canCreate) {
|
|
1212
|
+
const covering = availability ? selectCoveringOpenPutawayTask(
|
|
1213
|
+
availability.openTasks.map((task) => ({
|
|
1214
|
+
task,
|
|
1215
|
+
quantity: toNumber(task.quantity)
|
|
1216
|
+
})),
|
|
1217
|
+
1
|
|
1218
|
+
) : null;
|
|
1219
|
+
if (covering) links.push({ task: covering.task, created: false });
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
links.push(
|
|
1224
|
+
await findOrCreatePutawayForReceipt(trx, scope, putawayKey, {
|
|
1225
|
+
warehouseId: input.warehouseId,
|
|
1226
|
+
sourceLocationId: input.sourceLocationId,
|
|
1227
|
+
catalogVariantId: input.catalogVariantId,
|
|
1228
|
+
lotId,
|
|
1229
|
+
quantity: 1,
|
|
1230
|
+
serialNumber,
|
|
1231
|
+
asnId: input.asnId,
|
|
1232
|
+
lineId: input.lineId,
|
|
1233
|
+
metadata: input.metadata ?? null
|
|
1234
|
+
})
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
return links;
|
|
1238
|
+
}
|
|
1239
|
+
async function emitCreatedReceiptPutaways(ctx, scope, putaways, warehouseId) {
|
|
1240
|
+
for (const entry of putaways) {
|
|
1241
|
+
if (!entry.created) continue;
|
|
1242
|
+
await emitAsnReceivePutawayCreatedSideEffects(ctx, scope, entry.task, warehouseId);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const receiveAsnLineCommand = {
|
|
1246
|
+
id: "wms.asns.receive-line",
|
|
1247
|
+
// Stock-affecting on QC pass — align with Phase 1 inventory undo policy.
|
|
1248
|
+
isUndoable: false,
|
|
1249
|
+
async execute(rawInput, ctx) {
|
|
1250
|
+
const input = asnReceiveLineSchema.parse(rawInput ?? {});
|
|
1251
|
+
ensureTenantScope(ctx, input.tenantId);
|
|
1252
|
+
ensureOrganizationScope(ctx, input.organizationId);
|
|
1253
|
+
const rootEm = resolveEm(ctx);
|
|
1254
|
+
const scope = resolveScope(ctx, input);
|
|
1255
|
+
const serialNumbers = resolveSerialNumbers(input);
|
|
1256
|
+
assertUniqueSerialNumbers(serialNumbers);
|
|
1257
|
+
if (!shouldWriteStockOnQcPass(input.qcStatus)) {
|
|
1258
|
+
const locked = await rootEm.transactional(async (trx) => {
|
|
1259
|
+
const asn = await findOneWithDecryption(
|
|
1260
|
+
trx,
|
|
1261
|
+
Asn,
|
|
1262
|
+
{ id: input.asnId, deletedAt: null },
|
|
1263
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1264
|
+
scope
|
|
1265
|
+
);
|
|
1266
|
+
if (!asn) throw new CrudHttpError(404, { error: "ASN not found." });
|
|
1267
|
+
ensureTenantScope(ctx, asn.tenantId);
|
|
1268
|
+
ensureOrganizationScope(ctx, asn.organizationId);
|
|
1269
|
+
assertAsnMutable(asn);
|
|
1270
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1271
|
+
resourceKind: WMS_ASN_RESOURCE,
|
|
1272
|
+
resourceId: asn.id,
|
|
1273
|
+
current: asn.updatedAt,
|
|
1274
|
+
request: ctx.request ?? null
|
|
1275
|
+
});
|
|
1276
|
+
const line = await findOneWithDecryption(
|
|
1277
|
+
trx,
|
|
1278
|
+
ReceivingLine,
|
|
1279
|
+
{ id: input.lineId, deletedAt: null },
|
|
1280
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1281
|
+
scope
|
|
1282
|
+
);
|
|
1283
|
+
if (!line) throw new CrudHttpError(404, { error: "Receiving line not found." });
|
|
1284
|
+
ensureTenantScope(ctx, line.tenantId);
|
|
1285
|
+
ensureOrganizationScope(ctx, line.organizationId);
|
|
1286
|
+
const lineAsnId = typeof line.asn === "string" ? line.asn : line.asn.id;
|
|
1287
|
+
if (lineAsnId !== asn.id) {
|
|
1288
|
+
throw new CrudHttpError(422, { error: "line_asn_mismatch" });
|
|
1289
|
+
}
|
|
1290
|
+
assertAsnQcTransition(line.qcStatus, input.qcStatus);
|
|
1291
|
+
const priorReceivedQty = toNumber(line.receivedQty);
|
|
1292
|
+
const attempt = resolveAsnReceiveAttempt({
|
|
1293
|
+
lineId: line.id,
|
|
1294
|
+
priorReceivedQty,
|
|
1295
|
+
receivedQty: input.receivedQty,
|
|
1296
|
+
targetReceivedQty: input.targetReceivedQty,
|
|
1297
|
+
idempotencyKey: input.idempotencyKey
|
|
1298
|
+
});
|
|
1299
|
+
const warehouseId = typeof asn.warehouse === "string" ? asn.warehouse : asn.warehouse.id;
|
|
1300
|
+
const previousStatus = asn.status;
|
|
1301
|
+
const receivingStarted = asn.status === "draft" || asn.status === "in_transit";
|
|
1302
|
+
let lineUpdated = false;
|
|
1303
|
+
if (attempt.applyQty > 1e-6 && priorReceivedQty === toNumber(line.receivedQty)) {
|
|
1304
|
+
if (serialNumbers.length > 0 && priorReceivedQty > 1e-6) {
|
|
1305
|
+
assertIncomingSerialsAreNew(serialNumbers, line.serialNumbers);
|
|
1306
|
+
}
|
|
1307
|
+
line.receivedQty = toNumericString(attempt.targetReceivedQty);
|
|
1308
|
+
line.qcStatus = "failed";
|
|
1309
|
+
line.rejectionReason = normalizeOptionalString(input.rejectionReason) ?? line.rejectionReason ?? null;
|
|
1310
|
+
if (input.lotNumber !== void 0) {
|
|
1311
|
+
line.lotNumber = normalizeOptionalString(input.lotNumber) ?? line.lotNumber ?? null;
|
|
1312
|
+
}
|
|
1313
|
+
recordReceivedSerials(line, serialNumbers, priorReceivedQty);
|
|
1314
|
+
if (asn.status === "draft") asn.status = "in_transit";
|
|
1315
|
+
asn.updatedAt = /* @__PURE__ */ new Date();
|
|
1316
|
+
line.updatedAt = /* @__PURE__ */ new Date();
|
|
1317
|
+
await trx.flush();
|
|
1318
|
+
lineUpdated = true;
|
|
1319
|
+
}
|
|
1320
|
+
return {
|
|
1321
|
+
asn,
|
|
1322
|
+
line,
|
|
1323
|
+
warehouseId,
|
|
1324
|
+
previousStatus,
|
|
1325
|
+
receivingStarted,
|
|
1326
|
+
lineUpdated
|
|
1327
|
+
};
|
|
1328
|
+
});
|
|
1329
|
+
if (locked.lineUpdated) {
|
|
1330
|
+
await emitAsnSideEffects(ctx, {
|
|
1331
|
+
asns: [{ entity: locked.asn, action: "updated" }],
|
|
1332
|
+
lines: [{ entity: locked.line, action: "updated" }]
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
if (locked.lineUpdated && locked.receivingStarted && locked.previousStatus === "draft") {
|
|
1336
|
+
void emitWmsEvent("wms.asn.receiving_started", {
|
|
1337
|
+
id: locked.asn.id,
|
|
1338
|
+
warehouseId: locked.warehouseId,
|
|
1339
|
+
tenantId: scope.tenantId,
|
|
1340
|
+
organizationId: scope.organizationId
|
|
1341
|
+
}).catch(() => void 0);
|
|
1342
|
+
}
|
|
1343
|
+
if (locked.lineUpdated) {
|
|
1344
|
+
void emitWmsEvent("wms.inventory.receipt_qc_failed", {
|
|
1345
|
+
id: locked.line.id,
|
|
1346
|
+
asnId: locked.asn.id,
|
|
1347
|
+
lineId: locked.line.id,
|
|
1348
|
+
catalogVariantId: locked.line.catalogVariantId,
|
|
1349
|
+
receivedQty: toNumericString(input.receivedQty),
|
|
1350
|
+
rejectionReason: locked.line.rejectionReason,
|
|
1351
|
+
tenantId: scope.tenantId,
|
|
1352
|
+
organizationId: scope.organizationId
|
|
1353
|
+
}).catch(() => void 0);
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
movementIds: [],
|
|
1357
|
+
putawayTaskIds: [],
|
|
1358
|
+
lineId: locked.line.id,
|
|
1359
|
+
asnId: locked.asn.id,
|
|
1360
|
+
receivedQty: toNumber(locked.line.receivedQty),
|
|
1361
|
+
asnUpdatedAt: locked.asn.updatedAt.toISOString()
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
if (!input.targetStagingLocationId) {
|
|
1365
|
+
throw new CrudHttpError(422, { error: "staging_location_required" });
|
|
1366
|
+
}
|
|
1367
|
+
const outcome = await rootEm.transactional(async (trx) => {
|
|
1368
|
+
const asn = await findOneWithDecryption(
|
|
1369
|
+
trx,
|
|
1370
|
+
Asn,
|
|
1371
|
+
{ id: input.asnId, deletedAt: null },
|
|
1372
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1373
|
+
scope
|
|
1374
|
+
);
|
|
1375
|
+
if (!asn) throw new CrudHttpError(404, { error: "ASN not found." });
|
|
1376
|
+
ensureTenantScope(ctx, asn.tenantId);
|
|
1377
|
+
ensureOrganizationScope(ctx, asn.organizationId);
|
|
1378
|
+
assertAsnMutable(asn);
|
|
1379
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1380
|
+
resourceKind: WMS_ASN_RESOURCE,
|
|
1381
|
+
resourceId: asn.id,
|
|
1382
|
+
current: asn.updatedAt,
|
|
1383
|
+
request: ctx.request ?? null
|
|
1384
|
+
});
|
|
1385
|
+
const line = await findOneWithDecryption(
|
|
1386
|
+
trx,
|
|
1387
|
+
ReceivingLine,
|
|
1388
|
+
{ id: input.lineId, deletedAt: null },
|
|
1389
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1390
|
+
scope
|
|
1391
|
+
);
|
|
1392
|
+
if (!line) throw new CrudHttpError(404, { error: "Receiving line not found." });
|
|
1393
|
+
ensureTenantScope(ctx, line.tenantId);
|
|
1394
|
+
ensureOrganizationScope(ctx, line.organizationId);
|
|
1395
|
+
const lineAsnId = typeof line.asn === "string" ? line.asn : line.asn.id;
|
|
1396
|
+
if (lineAsnId !== asn.id) {
|
|
1397
|
+
throw new CrudHttpError(422, { error: "line_asn_mismatch" });
|
|
1398
|
+
}
|
|
1399
|
+
assertAsnQcTransition(line.qcStatus, input.qcStatus);
|
|
1400
|
+
const warehouseId = typeof asn.warehouse === "string" ? asn.warehouse : asn.warehouse.id;
|
|
1401
|
+
await requireStagingLocation(trx, ctx, input.targetStagingLocationId, warehouseId, scope);
|
|
1402
|
+
const priorReceivedQty = toNumber(line.receivedQty);
|
|
1403
|
+
const attempt = resolveAsnReceiveAttempt({
|
|
1404
|
+
lineId: line.id,
|
|
1405
|
+
priorReceivedQty,
|
|
1406
|
+
receivedQty: input.receivedQty,
|
|
1407
|
+
targetReceivedQty: input.targetReceivedQty,
|
|
1408
|
+
idempotencyKey: input.idempotencyKey
|
|
1409
|
+
});
|
|
1410
|
+
const previousStatus = asn.status;
|
|
1411
|
+
const receivingStarted = asn.status === "draft" || asn.status === "in_transit";
|
|
1412
|
+
const aggregatePutawayKey = buildAsnReceivePutawayKey({ attemptKey: attempt.attemptKey });
|
|
1413
|
+
const alreadyAtTarget = attempt.applyQty <= 1e-6;
|
|
1414
|
+
const trackedSerials = serialNumbers.length > 0 ? serialNumbers : normalizeStoredSerials(line.serialNumbers);
|
|
1415
|
+
const putawayShared = {
|
|
1416
|
+
attemptKey: attempt.attemptKey,
|
|
1417
|
+
warehouseId,
|
|
1418
|
+
sourceLocationId: input.targetStagingLocationId,
|
|
1419
|
+
catalogVariantId: line.catalogVariantId,
|
|
1420
|
+
asnId: asn.id,
|
|
1421
|
+
lineId: line.id,
|
|
1422
|
+
metadata: input.metadata ?? null
|
|
1423
|
+
};
|
|
1424
|
+
if (alreadyAtTarget) {
|
|
1425
|
+
if (trackedSerials.length > 0) {
|
|
1426
|
+
const putaways3 = await ensureSerialReceiptPutaways(trx, scope, {
|
|
1427
|
+
...putawayShared,
|
|
1428
|
+
serialNumbers: trackedSerials,
|
|
1429
|
+
recreateWhenMissing: true,
|
|
1430
|
+
lotId: input.lotId ?? null
|
|
1431
|
+
});
|
|
1432
|
+
return {
|
|
1433
|
+
movementResults: [],
|
|
1434
|
+
putaways: putaways3,
|
|
1435
|
+
lineUpdated: false,
|
|
1436
|
+
asn,
|
|
1437
|
+
line,
|
|
1438
|
+
warehouseId,
|
|
1439
|
+
previousStatus,
|
|
1440
|
+
receivingStarted,
|
|
1441
|
+
applyQty: 0,
|
|
1442
|
+
alreadyAtTarget: true,
|
|
1443
|
+
currentReceivedQty: toNumber(line.receivedQty)
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
const existingPutaway = await findExistingPutawayForReceipt(trx, scope, aggregatePutawayKey);
|
|
1447
|
+
const putaways2 = [];
|
|
1448
|
+
if (existingPutaway) {
|
|
1449
|
+
putaways2.push({ task: existingPutaway, created: false });
|
|
1450
|
+
} else if (shouldEnsurePutawayOnAlreadyAtTarget(existingPutaway)) {
|
|
1451
|
+
const cancelled = await findCancelledPutawayForReceipt(trx, scope, aggregatePutawayKey);
|
|
1452
|
+
const lotId2 = input.lotId ?? cancelled?.lotId ?? null;
|
|
1453
|
+
const quantity = resolvePutawayQuantityForAlreadyAtTargetRetry({
|
|
1454
|
+
cancelledTaskQuantity: cancelled ? toNumber(cancelled.quantity) : null,
|
|
1455
|
+
absoluteTargetQty: attempt.targetReceivedQty
|
|
1456
|
+
});
|
|
1457
|
+
if (quantity > 1e-6) {
|
|
1458
|
+
const availability = await resolveUncommittedStagingPutawayQuantity(
|
|
1459
|
+
trx,
|
|
1460
|
+
scope,
|
|
1461
|
+
{
|
|
1462
|
+
warehouseId,
|
|
1463
|
+
sourceLocationId: input.targetStagingLocationId,
|
|
1464
|
+
catalogVariantId: line.catalogVariantId,
|
|
1465
|
+
lotId: lotId2
|
|
1466
|
+
},
|
|
1467
|
+
{ lock: true }
|
|
1468
|
+
);
|
|
1469
|
+
if (availability && shouldRecreatePutawayOnAlreadyAtTarget({
|
|
1470
|
+
requestedQuantity: quantity,
|
|
1471
|
+
remainingAvailable: availability.remaining
|
|
1472
|
+
})) {
|
|
1473
|
+
putaways2.push(
|
|
1474
|
+
await findOrCreatePutawayForReceipt(trx, scope, aggregatePutawayKey, {
|
|
1475
|
+
warehouseId,
|
|
1476
|
+
sourceLocationId: input.targetStagingLocationId,
|
|
1477
|
+
catalogVariantId: line.catalogVariantId,
|
|
1478
|
+
lotId: lotId2,
|
|
1479
|
+
quantity,
|
|
1480
|
+
asnId: asn.id,
|
|
1481
|
+
lineId: line.id,
|
|
1482
|
+
metadata: input.metadata ?? null
|
|
1483
|
+
})
|
|
1484
|
+
);
|
|
1485
|
+
} else if (availability) {
|
|
1486
|
+
const covering = selectCoveringOpenPutawayTask(
|
|
1487
|
+
availability.openTasks.map((task) => ({
|
|
1488
|
+
task,
|
|
1489
|
+
quantity: toNumber(task.quantity)
|
|
1490
|
+
})),
|
|
1491
|
+
quantity
|
|
1492
|
+
);
|
|
1493
|
+
if (covering) putaways2.push({ task: covering.task, created: false });
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
movementResults: [],
|
|
1499
|
+
putaways: putaways2,
|
|
1500
|
+
lineUpdated: false,
|
|
1501
|
+
asn,
|
|
1502
|
+
line,
|
|
1503
|
+
warehouseId,
|
|
1504
|
+
previousStatus,
|
|
1505
|
+
receivingStarted,
|
|
1506
|
+
applyQty: 0,
|
|
1507
|
+
alreadyAtTarget: true,
|
|
1508
|
+
currentReceivedQty: toNumber(line.receivedQty)
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
if (serialNumbers.length > 0 && priorReceivedQty > 1e-6) {
|
|
1512
|
+
assertIncomingSerialsAreNew(serialNumbers, line.serialNumbers);
|
|
1513
|
+
}
|
|
1514
|
+
if (serialNumbers.length > 0 && serialNumbers.length !== attempt.applyQty) {
|
|
1515
|
+
throw new CrudHttpError(422, { error: "tracking_required" });
|
|
1516
|
+
}
|
|
1517
|
+
const movementResults = [];
|
|
1518
|
+
const receiveOnce = async (quantity, serialNumber) => {
|
|
1519
|
+
const movementPutawayKey = buildAsnReceivePutawayKey({
|
|
1520
|
+
attemptKey: attempt.attemptKey,
|
|
1521
|
+
serialNumber
|
|
1522
|
+
});
|
|
1523
|
+
const receiveInput = {
|
|
1524
|
+
organizationId: scope.organizationId,
|
|
1525
|
+
tenantId: scope.tenantId,
|
|
1526
|
+
warehouseId,
|
|
1527
|
+
locationId: input.targetStagingLocationId,
|
|
1528
|
+
catalogVariantId: line.catalogVariantId,
|
|
1529
|
+
lotId: input.lotId,
|
|
1530
|
+
lotNumber: input.lotNumber ?? line.lotNumber ?? void 0,
|
|
1531
|
+
serialNumber,
|
|
1532
|
+
quantity,
|
|
1533
|
+
referenceType: "po",
|
|
1534
|
+
referenceId: buildAsnReceiveReferenceId({
|
|
1535
|
+
attemptKey: attempt.attemptKey,
|
|
1536
|
+
serialNumber
|
|
1537
|
+
}),
|
|
1538
|
+
performedBy: input.performedBy,
|
|
1539
|
+
receivedAt: input.receivedAt,
|
|
1540
|
+
reasonCode: "asn_receive",
|
|
1541
|
+
metadata: {
|
|
1542
|
+
...input.metadata ?? {},
|
|
1543
|
+
asnId: asn.id,
|
|
1544
|
+
asnReferenceNumber: asn.referenceNumber ?? null,
|
|
1545
|
+
receivingLineId: line.id,
|
|
1546
|
+
putawayKey: movementPutawayKey,
|
|
1547
|
+
source: "asn_receive"
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
movementResults.push(await applyInventoryReceiveInTransaction(trx, ctx, receiveInput));
|
|
1551
|
+
};
|
|
1552
|
+
if (serialNumbers.length > 0) {
|
|
1553
|
+
for (const serialNumber of serialNumbers) {
|
|
1554
|
+
await receiveOnce(1, serialNumber);
|
|
1555
|
+
}
|
|
1556
|
+
} else {
|
|
1557
|
+
await receiveOnce(attempt.applyQty);
|
|
1558
|
+
}
|
|
1559
|
+
const lotId = input.lotId ?? movementResults.find((result) => result.lotId)?.lotId ?? null;
|
|
1560
|
+
const putaways = serialNumbers.length > 0 ? await ensureSerialReceiptPutaways(trx, scope, {
|
|
1561
|
+
...putawayShared,
|
|
1562
|
+
serialNumbers,
|
|
1563
|
+
recreateWhenMissing: false,
|
|
1564
|
+
lotId
|
|
1565
|
+
}) : [
|
|
1566
|
+
await findOrCreatePutawayForReceipt(trx, scope, aggregatePutawayKey, {
|
|
1567
|
+
warehouseId,
|
|
1568
|
+
sourceLocationId: input.targetStagingLocationId,
|
|
1569
|
+
catalogVariantId: line.catalogVariantId,
|
|
1570
|
+
lotId,
|
|
1571
|
+
quantity: attempt.applyQty,
|
|
1572
|
+
asnId: asn.id,
|
|
1573
|
+
lineId: line.id,
|
|
1574
|
+
metadata: input.metadata ?? null
|
|
1575
|
+
})
|
|
1576
|
+
];
|
|
1577
|
+
const currentReceived = toNumber(line.receivedQty);
|
|
1578
|
+
let lineUpdated = false;
|
|
1579
|
+
if (currentReceived + 1e-6 < attempt.targetReceivedQty) {
|
|
1580
|
+
line.receivedQty = toNumericString(attempt.targetReceivedQty);
|
|
1581
|
+
line.qcStatus = "passed";
|
|
1582
|
+
line.targetStagingLocationId = input.targetStagingLocationId;
|
|
1583
|
+
line.rejectionReason = null;
|
|
1584
|
+
if (input.lotNumber !== void 0) {
|
|
1585
|
+
line.lotNumber = normalizeOptionalString(input.lotNumber) ?? line.lotNumber ?? null;
|
|
1586
|
+
}
|
|
1587
|
+
recordReceivedSerials(line, serialNumbers, priorReceivedQty);
|
|
1588
|
+
if (asn.status === "draft") asn.status = "in_transit";
|
|
1589
|
+
asn.updatedAt = /* @__PURE__ */ new Date();
|
|
1590
|
+
line.updatedAt = /* @__PURE__ */ new Date();
|
|
1591
|
+
await trx.flush();
|
|
1592
|
+
lineUpdated = true;
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
movementResults,
|
|
1596
|
+
putaways,
|
|
1597
|
+
lineUpdated,
|
|
1598
|
+
asn,
|
|
1599
|
+
line,
|
|
1600
|
+
warehouseId,
|
|
1601
|
+
previousStatus,
|
|
1602
|
+
receivingStarted,
|
|
1603
|
+
applyQty: attempt.applyQty,
|
|
1604
|
+
alreadyAtTarget: false,
|
|
1605
|
+
currentReceivedQty: toNumber(line.receivedQty)
|
|
1606
|
+
};
|
|
1607
|
+
});
|
|
1608
|
+
if (outcome.alreadyAtTarget) {
|
|
1609
|
+
await emitCreatedReceiptPutaways(ctx, scope, outcome.putaways, outcome.warehouseId);
|
|
1610
|
+
return {
|
|
1611
|
+
movementIds: [],
|
|
1612
|
+
putawayTaskIds: outcome.putaways.map((entry) => entry.task.id),
|
|
1613
|
+
lineId: outcome.line.id,
|
|
1614
|
+
asnId: outcome.asn.id,
|
|
1615
|
+
receivedQty: outcome.currentReceivedQty,
|
|
1616
|
+
asnUpdatedAt: outcome.asn.updatedAt.toISOString()
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
for (const movementResult of outcome.movementResults) {
|
|
1620
|
+
await emitInventoryReceiveSideEffects(ctx, movementResult);
|
|
1621
|
+
}
|
|
1622
|
+
await emitCreatedReceiptPutaways(ctx, scope, outcome.putaways, outcome.warehouseId);
|
|
1623
|
+
if (outcome.lineUpdated) {
|
|
1624
|
+
await emitAsnSideEffects(ctx, {
|
|
1625
|
+
asns: [{ entity: outcome.asn, action: "updated" }],
|
|
1626
|
+
lines: [{ entity: outcome.line, action: "updated" }]
|
|
1627
|
+
});
|
|
1628
|
+
if (outcome.receivingStarted && outcome.previousStatus === "draft") {
|
|
1629
|
+
void emitWmsEvent("wms.asn.receiving_started", {
|
|
1630
|
+
id: outcome.asn.id,
|
|
1631
|
+
warehouseId: outcome.warehouseId,
|
|
1632
|
+
tenantId: scope.tenantId,
|
|
1633
|
+
organizationId: scope.organizationId
|
|
1634
|
+
}).catch(() => void 0);
|
|
1635
|
+
}
|
|
1636
|
+
void emitWmsEvent("wms.asn.line_received", {
|
|
1637
|
+
id: outcome.line.id,
|
|
1638
|
+
asnId: outcome.asn.id,
|
|
1639
|
+
lineId: outcome.line.id,
|
|
1640
|
+
movementIds: outcome.movementResults.map((result) => result.movementId),
|
|
1641
|
+
catalogVariantId: outcome.line.catalogVariantId,
|
|
1642
|
+
receivedQty: toNumericString(outcome.applyQty),
|
|
1643
|
+
locationId: input.targetStagingLocationId,
|
|
1644
|
+
tenantId: scope.tenantId,
|
|
1645
|
+
organizationId: scope.organizationId
|
|
1646
|
+
}).catch(() => void 0);
|
|
1647
|
+
}
|
|
1648
|
+
if (outcome.putaways.length === 0) {
|
|
1649
|
+
throw new CrudHttpError(500, { error: "[internal] putaway missing after QC-pass receive" });
|
|
1650
|
+
}
|
|
1651
|
+
return {
|
|
1652
|
+
movementIds: outcome.movementResults.map((result) => result.movementId),
|
|
1653
|
+
putawayTaskIds: outcome.putaways.map((entry) => entry.task.id),
|
|
1654
|
+
lineId: outcome.line.id,
|
|
1655
|
+
asnId: outcome.asn.id,
|
|
1656
|
+
receivedQty: toNumber(outcome.line.receivedQty),
|
|
1657
|
+
asnUpdatedAt: outcome.asn.updatedAt.toISOString()
|
|
1658
|
+
};
|
|
1659
|
+
},
|
|
1660
|
+
buildLog: async ({ input, result, ctx }) => {
|
|
1661
|
+
const { translate } = await resolveTranslations();
|
|
1662
|
+
return {
|
|
1663
|
+
actionLabel: translate("wms.audit.asn.receiveLine", "Receive ASN line"),
|
|
1664
|
+
resourceKind: WMS_ASN_RESOURCE,
|
|
1665
|
+
resourceId: result?.asnId ?? input?.asnId ?? null,
|
|
1666
|
+
parentResourceKind: WMS_RECEIVING_LINE_RESOURCE,
|
|
1667
|
+
parentResourceId: result?.lineId ?? input?.lineId ?? null,
|
|
1668
|
+
tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
|
|
1669
|
+
organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
1670
|
+
context: { cacheAliases: [WMS_INVENTORY_BALANCE_RESOURCE, WMS_RECEIVING_LINE_RESOURCE] }
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
const closeAsnCommand = {
|
|
1675
|
+
id: "wms.asns.close",
|
|
1676
|
+
isUndoable: false,
|
|
1677
|
+
async execute(rawInput, ctx) {
|
|
1678
|
+
const input = asnCloseSchema.parse(rawInput ?? {});
|
|
1679
|
+
ensureTenantScope(ctx, input.tenantId);
|
|
1680
|
+
ensureOrganizationScope(ctx, input.organizationId);
|
|
1681
|
+
const em = resolveEm(ctx);
|
|
1682
|
+
const scope = resolveScope(ctx, input);
|
|
1683
|
+
const locked = await em.transactional(async (trx) => {
|
|
1684
|
+
const asn = await findOneWithDecryption(
|
|
1685
|
+
trx,
|
|
1686
|
+
Asn,
|
|
1687
|
+
{ id: input.id, deletedAt: null },
|
|
1688
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
1689
|
+
scope
|
|
1690
|
+
);
|
|
1691
|
+
if (!asn) throw new CrudHttpError(404, { error: "ASN not found." });
|
|
1692
|
+
ensureTenantScope(ctx, asn.tenantId);
|
|
1693
|
+
ensureOrganizationScope(ctx, asn.organizationId);
|
|
1694
|
+
await enforceCommandOptimisticLockWithGuards(ctx.container, {
|
|
1695
|
+
resourceKind: WMS_ASN_RESOURCE,
|
|
1696
|
+
resourceId: asn.id,
|
|
1697
|
+
current: asn.updatedAt,
|
|
1698
|
+
request: ctx.request ?? null
|
|
1699
|
+
});
|
|
1700
|
+
if (asn.status === "closed") {
|
|
1701
|
+
return { asn, transitioned: false, emitReceived: false };
|
|
1702
|
+
}
|
|
1703
|
+
if (asn.status === "received") {
|
|
1704
|
+
asn.status = "closed";
|
|
1705
|
+
await trx.flush();
|
|
1706
|
+
return { asn, transitioned: true, emitReceived: false };
|
|
1707
|
+
}
|
|
1708
|
+
const lines = await findWithDecryption(
|
|
1709
|
+
trx,
|
|
1710
|
+
ReceivingLine,
|
|
1711
|
+
{ asn: asn.id, deletedAt: null },
|
|
1712
|
+
void 0,
|
|
1713
|
+
{ tenantId: asn.tenantId, organizationId: asn.organizationId }
|
|
1714
|
+
);
|
|
1715
|
+
if (!isAsnCloseable(lines, !!input.closeWhenShort)) {
|
|
1716
|
+
throw new CrudHttpError(409, { error: "invalid_receipt_state" });
|
|
1717
|
+
}
|
|
1718
|
+
asn.status = "received";
|
|
1719
|
+
await trx.flush();
|
|
1720
|
+
return { asn, transitioned: true, emitReceived: true };
|
|
1721
|
+
});
|
|
1722
|
+
if (locked.transitioned) {
|
|
1723
|
+
await emitAsnSideEffects(ctx, { asns: [{ entity: locked.asn, action: "updated" }] });
|
|
1724
|
+
}
|
|
1725
|
+
if (locked.emitReceived) {
|
|
1726
|
+
void emitWmsEvent("wms.asn.received", {
|
|
1727
|
+
id: locked.asn.id,
|
|
1728
|
+
status: locked.asn.status,
|
|
1729
|
+
closeWhenShort: !!input.closeWhenShort,
|
|
1730
|
+
tenantId: locked.asn.tenantId,
|
|
1731
|
+
organizationId: locked.asn.organizationId
|
|
1732
|
+
}).catch(() => void 0);
|
|
1733
|
+
}
|
|
1734
|
+
return { asnId: locked.asn.id, status: locked.asn.status };
|
|
1735
|
+
},
|
|
1736
|
+
buildLog: async ({ input, result, ctx }) => buildCrudLog(ctx, input, result?.asnId ?? null, "wms.audit.asn.close", "Complete ASN", WMS_ASN_RESOURCE)
|
|
1737
|
+
};
|
|
1738
|
+
registerCommand(createAsnCommand);
|
|
1739
|
+
registerCommand(updateAsnCommand);
|
|
1740
|
+
registerCommand(deleteAsnCommand);
|
|
1741
|
+
registerCommand(createReceivingLineCommand);
|
|
1742
|
+
registerCommand(updateReceivingLineCommand);
|
|
1743
|
+
registerCommand(deleteReceivingLineCommand);
|
|
1744
|
+
registerCommand(receiveAsnLineCommand);
|
|
1745
|
+
registerCommand(closeAsnCommand);
|
|
1746
|
+
//# sourceMappingURL=asn.js.map
|