@open-mercato/core 0.6.7-develop.6669.1.40b669666b → 0.6.7-develop.6670.1.4efa7961c6
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/inventory_balance/index.js +21 -0
- package/dist/generated/entities/inventory_balance/index.js.map +7 -0
- package/dist/generated/entities/inventory_lot/index.js +19 -0
- package/dist/generated/entities/inventory_lot/index.js.map +7 -0
- package/dist/generated/entities/inventory_movement/index.js +35 -0
- package/dist/generated/entities/inventory_movement/index.js.map +7 -0
- package/dist/generated/entities/inventory_reservation/index.js +23 -0
- package/dist/generated/entities/inventory_reservation/index.js.map +7 -0
- package/dist/generated/entities/product_inventory_profile/index.js +21 -0
- package/dist/generated/entities/product_inventory_profile/index.js.map +7 -0
- package/dist/generated/entities/sales_order_warehouse_assignment/index.js +11 -0
- package/dist/generated/entities/sales_order_warehouse_assignment/index.js.map +7 -0
- package/dist/generated/entities/warehouse/index.js +25 -0
- package/dist/generated/entities/warehouse/index.js.map +7 -0
- package/dist/generated/entities/warehouse_location/index.js +21 -0
- package/dist/generated/entities/warehouse_location/index.js.map +7 -0
- package/dist/generated/entities/warehouse_zone/index.js +11 -0
- package/dist/generated/entities/warehouse_zone/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +12 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +98 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/auth/backend/roles/page.js +12 -4
- package/dist/modules/auth/backend/roles/page.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +1 -0
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +16 -17
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/catalog/api/products/route.js +3 -0
- package/dist/modules/catalog/api/products/route.js.map +2 -2
- package/dist/modules/catalog/api/variants/route.js +3 -0
- package/dist/modules/catalog/api/variants/route.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +6 -6
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +1 -0
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +53 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +51 -2
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/events.js +2 -0
- package/dist/modules/sales/events.js.map +2 -2
- package/dist/modules/sales/widgets/injection/order-items-context.js +11 -0
- package/dist/modules/sales/widgets/injection/order-items-context.js.map +7 -0
- package/dist/modules/wms/acl.js +18 -0
- package/dist/modules/wms/acl.js.map +7 -0
- package/dist/modules/wms/analytics.js +11 -0
- package/dist/modules/wms/analytics.js.map +7 -0
- package/dist/modules/wms/api/dashboard/operational/route.js +80 -0
- package/dist/modules/wms/api/dashboard/operational/route.js.map +7 -0
- package/dist/modules/wms/api/interceptors.js +90 -0
- package/dist/modules/wms/api/interceptors.js.map +7 -0
- package/dist/modules/wms/api/inventory/adjust/route.js +51 -0
- package/dist/modules/wms/api/inventory/adjust/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/allocate/route.js +51 -0
- package/dist/modules/wms/api/inventory/allocate/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/balances/route.js +172 -0
- package/dist/modules/wms/api/inventory/balances/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/cycle-count/route.js +52 -0
- package/dist/modules/wms/api/inventory/cycle-count/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/helpers.js +113 -0
- package/dist/modules/wms/api/inventory/helpers.js.map +7 -0
- package/dist/modules/wms/api/inventory/import/apply/route.js +46 -0
- package/dist/modules/wms/api/inventory/import/apply/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/import/helpers.js +195 -0
- package/dist/modules/wms/api/inventory/import/helpers.js.map +7 -0
- package/dist/modules/wms/api/inventory/import/template/route.js +32 -0
- package/dist/modules/wms/api/inventory/import/template/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/import/validate/route.js +54 -0
- package/dist/modules/wms/api/inventory/import/validate/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/move/route.js +51 -0
- package/dist/modules/wms/api/inventory/move/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/movements/route.js +164 -0
- package/dist/modules/wms/api/inventory/movements/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/receive/route.js +50 -0
- package/dist/modules/wms/api/inventory/receive/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/release/route.js +44 -0
- package/dist/modules/wms/api/inventory/release/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/reservations/route.js +128 -0
- package/dist/modules/wms/api/inventory/reservations/route.js.map +7 -0
- package/dist/modules/wms/api/inventory/reserve/route.js +59 -0
- package/dist/modules/wms/api/inventory/reserve/route.js.map +7 -0
- package/dist/modules/wms/api/inventory-profiles/route.js +178 -0
- package/dist/modules/wms/api/inventory-profiles/route.js.map +7 -0
- package/dist/modules/wms/api/listEnrichers.js +181 -0
- package/dist/modules/wms/api/listEnrichers.js.map +7 -0
- package/dist/modules/wms/api/listSearch.js +77 -0
- package/dist/modules/wms/api/listSearch.js.map +7 -0
- package/dist/modules/wms/api/locations/route.js +172 -0
- package/dist/modules/wms/api/locations/route.js.map +7 -0
- package/dist/modules/wms/api/lots/route.js +204 -0
- package/dist/modules/wms/api/lots/route.js.map +7 -0
- package/dist/modules/wms/api/openapi.js +27 -0
- package/dist/modules/wms/api/openapi.js.map +7 -0
- package/dist/modules/wms/api/sales-orders/[salesOrderId]/re-run-reservation/route.js +54 -0
- package/dist/modules/wms/api/sales-orders/[salesOrderId]/re-run-reservation/route.js.map +7 -0
- package/dist/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.js +279 -0
- package/dist/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.js.map +7 -0
- package/dist/modules/wms/api/sales-orders/assign-warehouse/route.js +56 -0
- package/dist/modules/wms/api/sales-orders/assign-warehouse/route.js.map +7 -0
- package/dist/modules/wms/api/sales-orders/unassign-warehouse/route.js +43 -0
- package/dist/modules/wms/api/sales-orders/unassign-warehouse/route.js.map +7 -0
- package/dist/modules/wms/api/warehouses/route.js +181 -0
- package/dist/modules/wms/api/warehouses/route.js.map +7 -0
- package/dist/modules/wms/api/zones/route.js +159 -0
- package/dist/modules/wms/api/zones/route.js.map +7 -0
- package/dist/modules/wms/backend/config/wms/page.js +9 -0
- package/dist/modules/wms/backend/config/wms/page.js.map +7 -0
- package/dist/modules/wms/backend/config/wms/page.meta.js +18 -0
- package/dist/modules/wms/backend/config/wms/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/page.js +9 -0
- package/dist/modules/wms/backend/page.js.map +7 -0
- package/dist/modules/wms/backend/page.meta.js +16 -0
- package/dist/modules/wms/backend/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/inventory/page.js +9 -0
- package/dist/modules/wms/backend/wms/inventory/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/inventory/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/inventory/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/location/[id]/page.js +9 -0
- package/dist/modules/wms/backend/wms/location/[id]/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/location/[id]/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/location/[id]/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/locations/page.js +10 -0
- package/dist/modules/wms/backend/wms/locations/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/locations/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/locations/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/lot/[id]/page.js +9 -0
- package/dist/modules/wms/backend/wms/lot/[id]/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/lot/[id]/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/lot/[id]/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/lots/page.js +9 -0
- package/dist/modules/wms/backend/wms/lots/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/lots/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/lots/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/movements/page.js +22 -0
- package/dist/modules/wms/backend/wms/movements/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/movements/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/movements/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/reservations/page.js +23 -0
- package/dist/modules/wms/backend/wms/reservations/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/reservations/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/reservations/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/sku/[id]/page.js +9 -0
- package/dist/modules/wms/backend/wms/sku/[id]/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/sku/[id]/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/sku/[id]/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/warehouses/page.js +10 -0
- package/dist/modules/wms/backend/wms/warehouses/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/warehouses/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/warehouses/page.meta.js.map +7 -0
- package/dist/modules/wms/backend/wms/zones/page.js +10 -0
- package/dist/modules/wms/backend/wms/zones/page.js.map +7 -0
- package/dist/modules/wms/backend/wms/zones/page.meta.js +18 -0
- package/dist/modules/wms/backend/wms/zones/page.meta.js.map +7 -0
- package/dist/modules/wms/ce.js +80 -0
- package/dist/modules/wms/ce.js.map +7 -0
- package/dist/modules/wms/cli.js +83 -0
- package/dist/modules/wms/cli.js.map +7 -0
- package/dist/modules/wms/commands/configuration.js +1519 -0
- package/dist/modules/wms/commands/configuration.js.map +7 -0
- package/dist/modules/wms/commands/index.js +4 -0
- package/dist/modules/wms/commands/index.js.map +7 -0
- package/dist/modules/wms/commands/inventory-actions.js +1539 -0
- package/dist/modules/wms/commands/inventory-actions.js.map +7 -0
- package/dist/modules/wms/commands/sales-order-assignment.js +277 -0
- package/dist/modules/wms/commands/sales-order-assignment.js.map +7 -0
- package/dist/modules/wms/commands/shared.js +82 -0
- package/dist/modules/wms/commands/shared.js.map +7 -0
- package/dist/modules/wms/components/backend/AdjustInventoryDialog.js +755 -0
- package/dist/modules/wms/components/backend/AdjustInventoryDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/ChangeLotStatusDialog.js +269 -0
- package/dist/modules/wms/components/backend/ChangeLotStatusDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/CycleCountWizardDialog.js +1783 -0
- package/dist/modules/wms/components/backend/CycleCountWizardDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/ImportInventoryDialog.js +766 -0
- package/dist/modules/wms/components/backend/ImportInventoryDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/InventoryOperationsSection.js +119 -0
- package/dist/modules/wms/components/backend/InventoryOperationsSection.js.map +7 -0
- package/dist/modules/wms/components/backend/LocationEditDialog.js +140 -0
- package/dist/modules/wms/components/backend/LocationEditDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/MoveInventoryDialog.js +791 -0
- package/dist/modules/wms/components/backend/MoveInventoryDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/ReceiveInventoryDialog.js +656 -0
- package/dist/modules/wms/components/backend/ReceiveInventoryDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/ReleaseReservationDialog.js +344 -0
- package/dist/modules/wms/components/backend/ReleaseReservationDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/ReserveInventoryDialog.js +475 -0
- package/dist/modules/wms/components/backend/ReserveInventoryDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js +1112 -0
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js +908 -0
- package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +1015 -0
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js +1049 -0
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsLotsListPage.js +297 -0
- package/dist/modules/wms/components/backend/WmsLotsListPage.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js +757 -0
- package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +975 -0
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +7 -0
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js +710 -0
- package/dist/modules/wms/components/backend/inventoryMutationLoaders.js.map +7 -0
- package/dist/modules/wms/components/backend/useWmsInventoryMutationAccess.js +79 -0
- package/dist/modules/wms/components/backend/useWmsInventoryMutationAccess.js.map +7 -0
- package/dist/modules/wms/components/backend/useWmsInventoryScopeFromSearchParams.js +24 -0
- package/dist/modules/wms/components/backend/useWmsInventoryScopeFromSearchParams.js.map +7 -0
- package/dist/modules/wms/components/backend/wmsLookupLoaders.js +55 -0
- package/dist/modules/wms/components/backend/wmsLookupLoaders.js.map +7 -0
- package/dist/modules/wms/data/enrichers.js +501 -0
- package/dist/modules/wms/data/enrichers.js.map +7 -0
- package/dist/modules/wms/data/entities.js +467 -0
- package/dist/modules/wms/data/entities.js.map +7 -0
- package/dist/modules/wms/data/validators.js +399 -0
- package/dist/modules/wms/data/validators.js.map +7 -0
- package/dist/modules/wms/di.js +27 -0
- package/dist/modules/wms/di.js.map +7 -0
- package/dist/modules/wms/events.js +42 -0
- package/dist/modules/wms/events.js.map +7 -0
- package/dist/modules/wms/index.js +17 -0
- package/dist/modules/wms/index.js.map +7 -0
- package/dist/modules/wms/lib/catalogInventoryProfileIntent.js +54 -0
- package/dist/modules/wms/lib/catalogInventoryProfileIntent.js.map +7 -0
- package/dist/modules/wms/lib/catalogInventoryProfileSync.js +144 -0
- package/dist/modules/wms/lib/catalogInventoryProfileSync.js.map +7 -0
- package/dist/modules/wms/lib/expiry.js +29 -0
- package/dist/modules/wms/lib/expiry.js.map +7 -0
- package/dist/modules/wms/lib/flashMutationError.js +51 -0
- package/dist/modules/wms/lib/flashMutationError.js.map +7 -0
- package/dist/modules/wms/lib/inventoryDisplayUi.js +188 -0
- package/dist/modules/wms/lib/inventoryDisplayUi.js.map +7 -0
- package/dist/modules/wms/lib/inventoryIdempotency.js +37 -0
- package/dist/modules/wms/lib/inventoryIdempotency.js.map +7 -0
- package/dist/modules/wms/lib/inventoryImportCsv.js +173 -0
- package/dist/modules/wms/lib/inventoryImportCsv.js.map +7 -0
- package/dist/modules/wms/lib/inventoryImportService.js +468 -0
- package/dist/modules/wms/lib/inventoryImportService.js.map +7 -0
- package/dist/modules/wms/lib/inventoryMutationUi.js +25 -0
- package/dist/modules/wms/lib/inventoryMutationUi.js.map +7 -0
- package/dist/modules/wms/lib/inventoryPolicy.js +61 -0
- package/dist/modules/wms/lib/inventoryPolicy.js.map +7 -0
- package/dist/modules/wms/lib/inventoryReconciliation.js +193 -0
- package/dist/modules/wms/lib/inventoryReconciliation.js.map +7 -0
- package/dist/modules/wms/lib/inventoryTrackingValidation.js +13 -0
- package/dist/modules/wms/lib/inventoryTrackingValidation.js.map +7 -0
- package/dist/modules/wms/lib/loadOperationalDashboard.js +580 -0
- package/dist/modules/wms/lib/loadOperationalDashboard.js.map +7 -0
- package/dist/modules/wms/lib/lowStockBalanceFilter.js +49 -0
- package/dist/modules/wms/lib/lowStockBalanceFilter.js.map +7 -0
- package/dist/modules/wms/lib/primaryWarehousePolicy.js +32 -0
- package/dist/modules/wms/lib/primaryWarehousePolicy.js.map +7 -0
- package/dist/modules/wms/lib/roleFeatures.js +30 -0
- package/dist/modules/wms/lib/roleFeatures.js.map +7 -0
- package/dist/modules/wms/lib/salesOrderInventoryAutomation.js +291 -0
- package/dist/modules/wms/lib/salesOrderInventoryAutomation.js.map +7 -0
- package/dist/modules/wms/lib/salesOrderWarehouseAssignment.js +54 -0
- package/dist/modules/wms/lib/salesOrderWarehouseAssignment.js.map +7 -0
- package/dist/modules/wms/lib/wmsIntegrationToggles.js +71 -0
- package/dist/modules/wms/lib/wmsIntegrationToggles.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20260428110546.js +78 -0
- package/dist/modules/wms/migrations/Migration20260428110546.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20260527120000.js +15 -0
- package/dist/modules/wms/migrations/Migration20260527120000.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20260527140000.js +37 -0
- package/dist/modules/wms/migrations/Migration20260527140000.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20260613120000.js +23 -0
- package/dist/modules/wms/migrations/Migration20260613120000.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20260616090000.js +13 -0
- package/dist/modules/wms/migrations/Migration20260616090000.js.map +7 -0
- package/dist/modules/wms/migrations/Migration20260707180000.js +15 -0
- package/dist/modules/wms/migrations/Migration20260707180000.js.map +7 -0
- package/dist/modules/wms/notifications.client.js +18 -0
- package/dist/modules/wms/notifications.client.js.map +7 -0
- package/dist/modules/wms/notifications.js +53 -0
- package/dist/modules/wms/notifications.js.map +7 -0
- package/dist/modules/wms/search.js +196 -0
- package/dist/modules/wms/search.js.map +7 -0
- package/dist/modules/wms/setup.js +30 -0
- package/dist/modules/wms/setup.js.map +7 -0
- package/dist/modules/wms/subscribers/low-stock-notification.js +40 -0
- package/dist/modules/wms/subscribers/low-stock-notification.js.map +7 -0
- package/dist/modules/wms/subscribers/reservation-shortfall-notification.js +69 -0
- package/dist/modules/wms/subscribers/reservation-shortfall-notification.js.map +7 -0
- package/dist/modules/wms/subscribers/sales-order-cancelled-release.js +14 -0
- package/dist/modules/wms/subscribers/sales-order-cancelled-release.js.map +7 -0
- package/dist/modules/wms/subscribers/sales-order-confirmed-reserve.js +14 -0
- package/dist/modules/wms/subscribers/sales-order-confirmed-reserve.js.map +7 -0
- package/dist/modules/wms/translations.js +10 -0
- package/dist/modules/wms/translations.js.map +7 -0
- package/dist/modules/wms/vector.js +9 -0
- package/dist/modules/wms/vector.js.map +7 -0
- package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js +200 -0
- package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js.map +7 -0
- package/dist/modules/wms/widgets/injection/order-items-stock-column/widget.js +104 -0
- package/dist/modules/wms/widgets/injection/order-items-stock-column/widget.js.map +7 -0
- package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.client.js +389 -0
- package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.client.js.map +7 -0
- package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.js +16 -0
- package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.js.map +7 -0
- package/dist/modules/wms/widgets/injection-table.js +36 -0
- package/dist/modules/wms/widgets/injection-table.js.map +7 -0
- package/dist/modules/wms/widgets/notifications/WmsLowStockRenderer.js +150 -0
- package/dist/modules/wms/widgets/notifications/WmsLowStockRenderer.js.map +7 -0
- package/dist/modules/wms/widgets/notifications/WmsReservationShortfallRenderer.js +186 -0
- package/dist/modules/wms/widgets/notifications/WmsReservationShortfallRenderer.js.map +7 -0
- package/dist/modules/wms/widgets/notifications/index.js +7 -0
- package/dist/modules/wms/widgets/notifications/index.js.map +7 -0
- package/generated/entities/inventory_balance/index.ts +9 -0
- package/generated/entities/inventory_lot/index.ts +8 -0
- package/generated/entities/inventory_movement/index.ts +16 -0
- package/generated/entities/inventory_reservation/index.ts +10 -0
- package/generated/entities/product_inventory_profile/index.ts +9 -0
- package/generated/entities/sales_order_warehouse_assignment/index.ts +4 -0
- package/generated/entities/warehouse/index.ts +11 -0
- package/generated/entities/warehouse_location/index.ts +9 -0
- package/generated/entities/warehouse_zone/index.ts +4 -0
- package/generated/entities.ids.generated.ts +12 -0
- package/generated/entity-fields-registry.ts +98 -0
- package/package.json +7 -7
- package/src/modules/auth/backend/roles/page.tsx +13 -4
- 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/pl.json +2 -0
- package/src/modules/auth/lib/backendChrome.tsx +1 -0
- package/src/modules/auth/services/rbacService.ts +25 -21
- package/src/modules/catalog/api/products/route.ts +3 -0
- package/src/modules/catalog/api/variants/route.ts +3 -0
- package/src/modules/query_index/lib/engine.ts +18 -6
- package/src/modules/sales/api/documents/factory.ts +1 -0
- package/src/modules/sales/commands/documents.ts +71 -0
- package/src/modules/sales/components/documents/ItemsSection.tsx +58 -0
- package/src/modules/sales/events.ts +2 -0
- package/src/modules/sales/widgets/injection/order-items-context.ts +15 -0
- package/src/modules/wms/acl.ts +14 -0
- package/src/modules/wms/analytics.ts +9 -0
- package/src/modules/wms/api/dashboard/operational/route.ts +83 -0
- package/src/modules/wms/api/interceptors.ts +108 -0
- package/src/modules/wms/api/inventory/adjust/route.ts +50 -0
- package/src/modules/wms/api/inventory/allocate/route.ts +50 -0
- package/src/modules/wms/api/inventory/balances/route.ts +178 -0
- package/src/modules/wms/api/inventory/cycle-count/route.ts +51 -0
- package/src/modules/wms/api/inventory/helpers.ts +128 -0
- package/src/modules/wms/api/inventory/import/apply/route.ts +47 -0
- package/src/modules/wms/api/inventory/import/helpers.ts +223 -0
- package/src/modules/wms/api/inventory/import/template/route.ts +31 -0
- package/src/modules/wms/api/inventory/import/validate/route.ts +56 -0
- package/src/modules/wms/api/inventory/move/route.ts +50 -0
- package/src/modules/wms/api/inventory/movements/route.ts +164 -0
- package/src/modules/wms/api/inventory/receive/route.ts +49 -0
- package/src/modules/wms/api/inventory/release/route.ts +43 -0
- package/src/modules/wms/api/inventory/reservations/route.ts +128 -0
- package/src/modules/wms/api/inventory/reserve/route.ts +59 -0
- package/src/modules/wms/api/inventory-profiles/route.ts +177 -0
- package/src/modules/wms/api/listEnrichers.ts +312 -0
- package/src/modules/wms/api/listSearch.ts +121 -0
- package/src/modules/wms/api/locations/route.ts +171 -0
- package/src/modules/wms/api/lots/route.ts +205 -0
- package/src/modules/wms/api/openapi.ts +28 -0
- package/src/modules/wms/api/sales-orders/[salesOrderId]/re-run-reservation/route.ts +58 -0
- package/src/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.ts +306 -0
- package/src/modules/wms/api/sales-orders/assign-warehouse/route.ts +57 -0
- package/src/modules/wms/api/sales-orders/unassign-warehouse/route.ts +43 -0
- package/src/modules/wms/api/warehouses/route.ts +181 -0
- package/src/modules/wms/api/zones/route.ts +158 -0
- package/src/modules/wms/backend/config/wms/page.meta.ts +14 -0
- package/src/modules/wms/backend/config/wms/page.tsx +5 -0
- package/src/modules/wms/backend/page.meta.ts +12 -0
- package/src/modules/wms/backend/page.tsx +5 -0
- package/src/modules/wms/backend/wms/inventory/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/inventory/page.tsx +5 -0
- package/src/modules/wms/backend/wms/location/[id]/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/location/[id]/page.tsx +5 -0
- package/src/modules/wms/backend/wms/locations/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/locations/page.tsx +12 -0
- package/src/modules/wms/backend/wms/lot/[id]/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/lot/[id]/page.tsx +5 -0
- package/src/modules/wms/backend/wms/lots/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/lots/page.tsx +5 -0
- package/src/modules/wms/backend/wms/movements/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/movements/page.tsx +23 -0
- package/src/modules/wms/backend/wms/reservations/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/reservations/page.tsx +24 -0
- package/src/modules/wms/backend/wms/sku/[id]/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/sku/[id]/page.tsx +5 -0
- package/src/modules/wms/backend/wms/warehouses/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/warehouses/page.tsx +12 -0
- package/src/modules/wms/backend/wms/zones/page.meta.ts +14 -0
- package/src/modules/wms/backend/wms/zones/page.tsx +12 -0
- package/src/modules/wms/ce.ts +76 -0
- package/src/modules/wms/cli.ts +96 -0
- package/src/modules/wms/commands/configuration.ts +1844 -0
- package/src/modules/wms/commands/index.ts +3 -0
- package/src/modules/wms/commands/inventory-actions.ts +1866 -0
- package/src/modules/wms/commands/sales-order-assignment.ts +346 -0
- package/src/modules/wms/commands/shared.ts +90 -0
- package/src/modules/wms/components/backend/AdjustInventoryDialog.tsx +820 -0
- package/src/modules/wms/components/backend/ChangeLotStatusDialog.tsx +312 -0
- package/src/modules/wms/components/backend/CycleCountWizardDialog.tsx +2007 -0
- package/src/modules/wms/components/backend/ImportInventoryDialog.tsx +1018 -0
- package/src/modules/wms/components/backend/InventoryOperationsSection.tsx +159 -0
- package/src/modules/wms/components/backend/LocationEditDialog.tsx +185 -0
- package/src/modules/wms/components/backend/MoveInventoryDialog.tsx +907 -0
- package/src/modules/wms/components/backend/ReceiveInventoryDialog.tsx +695 -0
- package/src/modules/wms/components/backend/ReleaseReservationDialog.tsx +424 -0
- package/src/modules/wms/components/backend/ReserveInventoryDialog.tsx +511 -0
- package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +1340 -0
- package/src/modules/wms/components/backend/WmsInventoryConsolePage.tsx +1132 -0
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +1310 -0
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +1355 -0
- package/src/modules/wms/components/backend/WmsLotsListPage.tsx +359 -0
- package/src/modules/wms/components/backend/WmsOperationalDashboardPage.tsx +958 -0
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +1245 -0
- package/src/modules/wms/components/backend/inventoryMutationLoaders.ts +960 -0
- package/src/modules/wms/components/backend/useWmsInventoryMutationAccess.ts +89 -0
- package/src/modules/wms/components/backend/useWmsInventoryScopeFromSearchParams.ts +32 -0
- package/src/modules/wms/components/backend/wmsLookupLoaders.ts +78 -0
- package/src/modules/wms/data/enrichers.ts +718 -0
- package/src/modules/wms/data/entities.ts +436 -0
- package/src/modules/wms/data/validators.ts +463 -0
- package/src/modules/wms/di.ts +25 -0
- package/src/modules/wms/events.ts +41 -0
- package/src/modules/wms/i18n/de.json +1051 -0
- package/src/modules/wms/i18n/en.json +1051 -0
- package/src/modules/wms/i18n/es.json +1051 -0
- package/src/modules/wms/i18n/pl.json +1051 -0
- package/src/modules/wms/index.ts +15 -0
- package/src/modules/wms/lib/catalogInventoryProfileIntent.ts +65 -0
- package/src/modules/wms/lib/catalogInventoryProfileSync.ts +199 -0
- package/src/modules/wms/lib/expiry.ts +30 -0
- package/src/modules/wms/lib/flashMutationError.ts +74 -0
- package/src/modules/wms/lib/inventoryDisplayUi.ts +249 -0
- package/src/modules/wms/lib/inventoryIdempotency.ts +54 -0
- package/src/modules/wms/lib/inventoryImportCsv.ts +212 -0
- package/src/modules/wms/lib/inventoryImportService.ts +629 -0
- package/src/modules/wms/lib/inventoryMutationUi.ts +21 -0
- package/src/modules/wms/lib/inventoryPolicy.ts +111 -0
- package/src/modules/wms/lib/inventoryReconciliation.ts +263 -0
- package/src/modules/wms/lib/inventoryTrackingValidation.ts +23 -0
- package/src/modules/wms/lib/loadOperationalDashboard.ts +776 -0
- package/src/modules/wms/lib/lowStockBalanceFilter.ts +74 -0
- package/src/modules/wms/lib/primaryWarehousePolicy.ts +46 -0
- package/src/modules/wms/lib/roleFeatures.ts +32 -0
- package/src/modules/wms/lib/salesOrderInventoryAutomation.ts +390 -0
- package/src/modules/wms/lib/salesOrderWarehouseAssignment.ts +78 -0
- package/src/modules/wms/lib/wmsIntegrationToggles.ts +81 -0
- package/src/modules/wms/migrations/.snapshot-open-mercato.json +2880 -0
- package/src/modules/wms/migrations/Migration20260428110546.ts +91 -0
- package/src/modules/wms/migrations/Migration20260527120000.ts +15 -0
- package/src/modules/wms/migrations/Migration20260527140000.ts +35 -0
- package/src/modules/wms/migrations/Migration20260613120000.ts +21 -0
- package/src/modules/wms/migrations/Migration20260616090000.ts +11 -0
- package/src/modules/wms/migrations/Migration20260707180000.ts +13 -0
- package/src/modules/wms/notifications.client.ts +18 -0
- package/src/modules/wms/notifications.ts +51 -0
- package/src/modules/wms/search.ts +230 -0
- package/src/modules/wms/setup.ts +29 -0
- package/src/modules/wms/subscribers/low-stock-notification.ts +54 -0
- package/src/modules/wms/subscribers/reservation-shortfall-notification.ts +98 -0
- package/src/modules/wms/subscribers/sales-order-cancelled-release.ts +21 -0
- package/src/modules/wms/subscribers/sales-order-confirmed-reserve.ts +21 -0
- package/src/modules/wms/translations.ts +6 -0
- package/src/modules/wms/vector.ts +7 -0
- package/src/modules/wms/widgets/injection/catalog-inventory-profile/widget.ts +238 -0
- package/src/modules/wms/widgets/injection/order-items-stock-column/widget.tsx +153 -0
- package/src/modules/wms/widgets/injection/sales-order-stock-context/widget.client.tsx +476 -0
- package/src/modules/wms/widgets/injection/sales-order-stock-context/widget.ts +15 -0
- package/src/modules/wms/widgets/injection-table.ts +34 -0
- package/src/modules/wms/widgets/notifications/WmsLowStockRenderer.tsx +153 -0
- package/src/modules/wms/widgets/notifications/WmsReservationShortfallRenderer.tsx +194 -0
- package/src/modules/wms/widgets/notifications/index.ts +2 -0
|
@@ -0,0 +1,1844 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// WMS Configuration Commands — Undo Policy
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// All warehouse / zone / location / inventory-profile / lot CRUD commands in
|
|
6
|
+
// this file follow the standard Open Mercato undoable pattern: each handler
|
|
7
|
+
// declares `prepare` (snapshot before), `captureAfter` (snapshot after) and
|
|
8
|
+
// `undo` (restore from snapshot) so the generic command-bus undo flow works
|
|
9
|
+
// out of the box, mirroring `catalog/commands/categories.ts`.
|
|
10
|
+
//
|
|
11
|
+
// Undo semantics per kind:
|
|
12
|
+
// - create — undo soft-deletes the created record (`deletedAt = now`).
|
|
13
|
+
// - update — undo restores all scalar/FK fields from the `before` snapshot;
|
|
14
|
+
// re-creates the record with the original id if it was hard-deleted.
|
|
15
|
+
// - delete — undo clears `deletedAt` and restores all fields from snapshot.
|
|
16
|
+
//
|
|
17
|
+
// Notes for downstream consistency:
|
|
18
|
+
// - Inventory balances / reservations / movements reference these records
|
|
19
|
+
// by FK. Undo does NOT replay those ledger rows; if a warehouse / zone /
|
|
20
|
+
// location was deleted while ledger rows still pointed at it, undoing the
|
|
21
|
+
// delete simply makes those references resolvable again. Live ledger state
|
|
22
|
+
// itself is not rewound — that is the responsibility of the inventory
|
|
23
|
+
// mutation commands' counter-actions documented in `inventory-actions.ts`.
|
|
24
|
+
// - Cascading children (zones inside a warehouse, child locations inside a
|
|
25
|
+
// parent location) are NOT auto-undone; each affected record needs its own
|
|
26
|
+
// undo entry. The audit log preserves enough data per-entity for that.
|
|
27
|
+
// - Primary reassignment demotes sibling warehouses via `nativeUpdate`. Create
|
|
28
|
+
// and update undo payloads capture `demotedPrimariesBefore` so undo restores
|
|
29
|
+
// the previous primary flag on affected siblings.
|
|
30
|
+
// =============================================================================
|
|
31
|
+
import type { CommandHandler } from '@open-mercato/shared/lib/commands'
|
|
32
|
+
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
33
|
+
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
34
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
35
|
+
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
36
|
+
import { CrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
|
|
37
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
38
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
39
|
+
import { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
40
|
+
import type { JsonValue } from '@open-mercato/shared/lib/json'
|
|
41
|
+
import {
|
|
42
|
+
InventoryLot,
|
|
43
|
+
type InventoryLotStatus,
|
|
44
|
+
ProductInventoryProfile,
|
|
45
|
+
type InventoryStrategy,
|
|
46
|
+
Warehouse,
|
|
47
|
+
WarehouseLocation,
|
|
48
|
+
type WarehouseLocationType,
|
|
49
|
+
WarehouseZone,
|
|
50
|
+
} from '../data/entities'
|
|
51
|
+
import {
|
|
52
|
+
inventoryLotCreateSchema,
|
|
53
|
+
inventoryLotUpdateSchema,
|
|
54
|
+
productInventoryProfileCreateSchema,
|
|
55
|
+
productInventoryProfileUpdateSchema,
|
|
56
|
+
warehouseCreateSchema,
|
|
57
|
+
warehouseLocationCreateSchema,
|
|
58
|
+
warehouseLocationUpdateSchema,
|
|
59
|
+
warehouseUpdateSchema,
|
|
60
|
+
warehouseZoneCreateSchema,
|
|
61
|
+
warehouseZoneUpdateSchema,
|
|
62
|
+
type InventoryLotCreateInput,
|
|
63
|
+
type InventoryLotUpdateInput,
|
|
64
|
+
type ProductInventoryProfileCreateInput,
|
|
65
|
+
type ProductInventoryProfileUpdateInput,
|
|
66
|
+
type WarehouseCreateInput,
|
|
67
|
+
type WarehouseLocationCreateInput,
|
|
68
|
+
type WarehouseLocationUpdateInput,
|
|
69
|
+
type WarehouseUpdateInput,
|
|
70
|
+
type WarehouseZoneCreateInput,
|
|
71
|
+
type WarehouseZoneUpdateInput,
|
|
72
|
+
} from '../data/validators'
|
|
73
|
+
import {
|
|
74
|
+
ensureOrganizationScope,
|
|
75
|
+
ensureTenantScope,
|
|
76
|
+
normalizeOptionalString,
|
|
77
|
+
requireId,
|
|
78
|
+
toNumericString,
|
|
79
|
+
} from './shared'
|
|
80
|
+
import { emitWmsEvent } from '../events'
|
|
81
|
+
|
|
82
|
+
function resolveScope(ctx: CommandRuntimeContext, fallback?: { tenantId?: string | null; organizationId?: string | null }) {
|
|
83
|
+
return {
|
|
84
|
+
tenantId: fallback?.tenantId ?? ctx.auth?.tenantId ?? null,
|
|
85
|
+
organizationId: fallback?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveEm(ctx: CommandRuntimeContext): EntityManager {
|
|
90
|
+
return (ctx.container.resolve('em') as EntityManager).fork()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function toJsonValue(value: Record<string, unknown> | null | undefined): JsonValue | null | undefined {
|
|
94
|
+
if (value === undefined) return undefined
|
|
95
|
+
return (value ?? null) as JsonValue | null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function buildCrudLog(
|
|
99
|
+
ctx: CommandRuntimeContext,
|
|
100
|
+
input: { tenantId?: string | null; organizationId?: string | null; id?: string | null } | undefined,
|
|
101
|
+
resultId: string | null,
|
|
102
|
+
actionKey: string,
|
|
103
|
+
fallbackLabel: string,
|
|
104
|
+
resourceKind: string,
|
|
105
|
+
) {
|
|
106
|
+
const { translate } = await resolveTranslations()
|
|
107
|
+
return {
|
|
108
|
+
actionLabel: translate(actionKey, fallbackLabel),
|
|
109
|
+
resourceKind,
|
|
110
|
+
resourceId: resultId ?? input?.id ?? null,
|
|
111
|
+
tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
|
|
112
|
+
organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function loadWarehouse(em: EntityManager, ctx: CommandRuntimeContext, id: string) {
|
|
117
|
+
const warehouse = await findOneWithDecryption(em, Warehouse, { id, deletedAt: null }, undefined, resolveScope(ctx))
|
|
118
|
+
if (!warehouse) throw new CrudHttpError(404, { error: 'Warehouse not found.' })
|
|
119
|
+
ensureTenantScope(ctx, warehouse.tenantId)
|
|
120
|
+
ensureOrganizationScope(ctx, warehouse.organizationId)
|
|
121
|
+
return warehouse
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function loadZone(em: EntityManager, ctx: CommandRuntimeContext, id: string) {
|
|
125
|
+
const zone = await findOneWithDecryption(em, WarehouseZone, { id, deletedAt: null }, undefined, resolveScope(ctx))
|
|
126
|
+
if (!zone) throw new CrudHttpError(404, { error: 'Warehouse zone not found.' })
|
|
127
|
+
ensureTenantScope(ctx, zone.tenantId)
|
|
128
|
+
ensureOrganizationScope(ctx, zone.organizationId)
|
|
129
|
+
return zone
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function loadLocation(em: EntityManager, ctx: CommandRuntimeContext, id: string) {
|
|
133
|
+
const location = await findOneWithDecryption(em, WarehouseLocation, { id, deletedAt: null }, undefined, resolveScope(ctx))
|
|
134
|
+
if (!location) throw new CrudHttpError(404, { error: 'Warehouse location not found.' })
|
|
135
|
+
ensureTenantScope(ctx, location.tenantId)
|
|
136
|
+
ensureOrganizationScope(ctx, location.organizationId)
|
|
137
|
+
return location
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function loadProfile(em: EntityManager, ctx: CommandRuntimeContext, id: string) {
|
|
141
|
+
const profile = await findOneWithDecryption(em, ProductInventoryProfile, { id, deletedAt: null }, undefined, resolveScope(ctx))
|
|
142
|
+
if (!profile) throw new CrudHttpError(404, { error: 'Inventory profile not found.' })
|
|
143
|
+
ensureTenantScope(ctx, profile.tenantId)
|
|
144
|
+
ensureOrganizationScope(ctx, profile.organizationId)
|
|
145
|
+
return profile
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function loadLot(em: EntityManager, ctx: CommandRuntimeContext, id: string) {
|
|
149
|
+
const lot = await findOneWithDecryption(em, InventoryLot, { id, deletedAt: null }, undefined, resolveScope(ctx))
|
|
150
|
+
if (!lot) throw new CrudHttpError(404, { error: 'Inventory lot not found.' })
|
|
151
|
+
ensureTenantScope(ctx, lot.tenantId)
|
|
152
|
+
ensureOrganizationScope(ctx, lot.organizationId)
|
|
153
|
+
return lot
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Snapshot types — used by `prepare` / `captureAfter` / `undo` to round-trip
|
|
158
|
+
// the full record through the audit log payload.
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
type WarehouseSnapshot = {
|
|
162
|
+
id: string
|
|
163
|
+
organizationId: string
|
|
164
|
+
tenantId: string
|
|
165
|
+
name: string
|
|
166
|
+
code: string
|
|
167
|
+
isActive: boolean
|
|
168
|
+
isPrimary: boolean
|
|
169
|
+
addressLine1: string | null
|
|
170
|
+
city: string | null
|
|
171
|
+
postalCode: string | null
|
|
172
|
+
country: string | null
|
|
173
|
+
timezone: string | null
|
|
174
|
+
metadata: JsonValue | null
|
|
175
|
+
createdAt: string
|
|
176
|
+
updatedAt: string
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type PrimaryDemotionSnapshot = {
|
|
180
|
+
id: string
|
|
181
|
+
isPrimary: boolean
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type WarehouseUndoPayload = {
|
|
185
|
+
before?: WarehouseSnapshot | null
|
|
186
|
+
after?: WarehouseSnapshot | null
|
|
187
|
+
demotedPrimariesBefore?: PrimaryDemotionSnapshot[]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type WarehouseZoneSnapshot = {
|
|
191
|
+
id: string
|
|
192
|
+
organizationId: string
|
|
193
|
+
tenantId: string
|
|
194
|
+
warehouseId: string
|
|
195
|
+
code: string
|
|
196
|
+
name: string
|
|
197
|
+
priority: number
|
|
198
|
+
metadata: JsonValue | null
|
|
199
|
+
createdAt: string
|
|
200
|
+
updatedAt: string
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
type WarehouseZoneUndoPayload = { before?: WarehouseZoneSnapshot | null; after?: WarehouseZoneSnapshot | null }
|
|
204
|
+
|
|
205
|
+
type WarehouseLocationSnapshot = {
|
|
206
|
+
id: string
|
|
207
|
+
organizationId: string
|
|
208
|
+
tenantId: string
|
|
209
|
+
warehouseId: string
|
|
210
|
+
parentId: string | null
|
|
211
|
+
code: string
|
|
212
|
+
type: WarehouseLocationType
|
|
213
|
+
isActive: boolean
|
|
214
|
+
capacityUnits: string | null
|
|
215
|
+
capacityWeight: string | null
|
|
216
|
+
constraints: JsonValue | null
|
|
217
|
+
metadata: JsonValue | null
|
|
218
|
+
createdAt: string
|
|
219
|
+
updatedAt: string
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
type WarehouseLocationUndoPayload = {
|
|
223
|
+
before?: WarehouseLocationSnapshot | null
|
|
224
|
+
after?: WarehouseLocationSnapshot | null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
type ProductInventoryProfileSnapshot = {
|
|
228
|
+
id: string
|
|
229
|
+
organizationId: string
|
|
230
|
+
tenantId: string
|
|
231
|
+
catalogProductId: string
|
|
232
|
+
catalogVariantId: string | null
|
|
233
|
+
defaultUom: string
|
|
234
|
+
trackLot: boolean
|
|
235
|
+
trackSerial: boolean
|
|
236
|
+
trackExpiration: boolean
|
|
237
|
+
defaultStrategy: InventoryStrategy
|
|
238
|
+
reorderPoint: string
|
|
239
|
+
safetyStock: string
|
|
240
|
+
metadata: JsonValue | null
|
|
241
|
+
createdAt: string
|
|
242
|
+
updatedAt: string
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
type ProductInventoryProfileUndoPayload = {
|
|
246
|
+
before?: ProductInventoryProfileSnapshot | null
|
|
247
|
+
after?: ProductInventoryProfileSnapshot | null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
type InventoryLotSnapshot = {
|
|
251
|
+
id: string
|
|
252
|
+
organizationId: string
|
|
253
|
+
tenantId: string
|
|
254
|
+
catalogVariantId: string
|
|
255
|
+
sku: string
|
|
256
|
+
lotNumber: string
|
|
257
|
+
batchNumber: string | null
|
|
258
|
+
manufacturedAt: string | null
|
|
259
|
+
bestBeforeAt: string | null
|
|
260
|
+
expiresAt: string | null
|
|
261
|
+
status: InventoryLotStatus
|
|
262
|
+
metadata: JsonValue | null
|
|
263
|
+
createdAt: string
|
|
264
|
+
updatedAt: string
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type InventoryLotUndoPayload = { before?: InventoryLotSnapshot | null; after?: InventoryLotSnapshot | null }
|
|
268
|
+
|
|
269
|
+
function isoOrNull(value: Date | string | null | undefined): string | null {
|
|
270
|
+
if (!value) return null
|
|
271
|
+
if (value instanceof Date) return value.toISOString()
|
|
272
|
+
if (typeof value === 'string') return value
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function dateOrNull(value: string | Date | null | undefined): Date | null {
|
|
277
|
+
if (!value) return null
|
|
278
|
+
if (value instanceof Date) return value
|
|
279
|
+
if (typeof value === 'string') return new Date(value)
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function snapshotWarehouse(record: Warehouse): WarehouseSnapshot {
|
|
284
|
+
return {
|
|
285
|
+
id: record.id,
|
|
286
|
+
organizationId: record.organizationId,
|
|
287
|
+
tenantId: record.tenantId,
|
|
288
|
+
name: record.name,
|
|
289
|
+
code: record.code,
|
|
290
|
+
isActive: !!record.isActive,
|
|
291
|
+
isPrimary: !!record.isPrimary,
|
|
292
|
+
addressLine1: record.addressLine1 ?? null,
|
|
293
|
+
city: record.city ?? null,
|
|
294
|
+
postalCode: record.postalCode ?? null,
|
|
295
|
+
country: record.country ?? null,
|
|
296
|
+
timezone: record.timezone ?? null,
|
|
297
|
+
metadata: (record.metadata ?? null) as JsonValue | null,
|
|
298
|
+
createdAt: record.createdAt.toISOString(),
|
|
299
|
+
updatedAt: record.updatedAt.toISOString(),
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function loadWarehouseSnapshot(
|
|
304
|
+
em: EntityManager,
|
|
305
|
+
ctx: CommandRuntimeContext,
|
|
306
|
+
id: string,
|
|
307
|
+
): Promise<WarehouseSnapshot | null> {
|
|
308
|
+
const record = await findOneWithDecryption(em, Warehouse, { id }, undefined, resolveScope(ctx))
|
|
309
|
+
return record ? snapshotWarehouse(record) : null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function snapshotWarehouseZone(record: WarehouseZone): WarehouseZoneSnapshot {
|
|
313
|
+
const warehouseId = typeof record.warehouse === 'string' ? record.warehouse : record.warehouse.id
|
|
314
|
+
return {
|
|
315
|
+
id: record.id,
|
|
316
|
+
organizationId: record.organizationId,
|
|
317
|
+
tenantId: record.tenantId,
|
|
318
|
+
warehouseId,
|
|
319
|
+
code: record.code,
|
|
320
|
+
name: record.name,
|
|
321
|
+
priority: record.priority ?? 0,
|
|
322
|
+
metadata: (record.metadata ?? null) as JsonValue | null,
|
|
323
|
+
createdAt: record.createdAt.toISOString(),
|
|
324
|
+
updatedAt: record.updatedAt.toISOString(),
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function loadWarehouseZoneSnapshot(
|
|
329
|
+
em: EntityManager,
|
|
330
|
+
ctx: CommandRuntimeContext,
|
|
331
|
+
id: string,
|
|
332
|
+
): Promise<WarehouseZoneSnapshot | null> {
|
|
333
|
+
const record = await findOneWithDecryption(em, WarehouseZone, { id }, undefined, resolveScope(ctx))
|
|
334
|
+
return record ? snapshotWarehouseZone(record) : null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function snapshotWarehouseLocation(record: WarehouseLocation): WarehouseLocationSnapshot {
|
|
338
|
+
const warehouseId = typeof record.warehouse === 'string' ? record.warehouse : record.warehouse.id
|
|
339
|
+
let parentId: string | null = null
|
|
340
|
+
if (record.parent) {
|
|
341
|
+
parentId = typeof record.parent === 'string' ? record.parent : record.parent.id
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
id: record.id,
|
|
345
|
+
organizationId: record.organizationId,
|
|
346
|
+
tenantId: record.tenantId,
|
|
347
|
+
warehouseId,
|
|
348
|
+
parentId,
|
|
349
|
+
code: record.code,
|
|
350
|
+
type: record.type,
|
|
351
|
+
isActive: !!record.isActive,
|
|
352
|
+
capacityUnits: record.capacityUnits ?? null,
|
|
353
|
+
capacityWeight: record.capacityWeight ?? null,
|
|
354
|
+
constraints: (record.constraints ?? null) as JsonValue | null,
|
|
355
|
+
metadata: (record.metadata ?? null) as JsonValue | null,
|
|
356
|
+
createdAt: record.createdAt.toISOString(),
|
|
357
|
+
updatedAt: record.updatedAt.toISOString(),
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function loadWarehouseLocationSnapshot(
|
|
362
|
+
em: EntityManager,
|
|
363
|
+
ctx: CommandRuntimeContext,
|
|
364
|
+
id: string,
|
|
365
|
+
): Promise<WarehouseLocationSnapshot | null> {
|
|
366
|
+
const record = await findOneWithDecryption(em, WarehouseLocation, { id }, undefined, resolveScope(ctx))
|
|
367
|
+
return record ? snapshotWarehouseLocation(record) : null
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function snapshotInventoryProfile(record: ProductInventoryProfile): ProductInventoryProfileSnapshot {
|
|
371
|
+
return {
|
|
372
|
+
id: record.id,
|
|
373
|
+
organizationId: record.organizationId,
|
|
374
|
+
tenantId: record.tenantId,
|
|
375
|
+
catalogProductId: record.catalogProductId,
|
|
376
|
+
catalogVariantId: record.catalogVariantId ?? null,
|
|
377
|
+
defaultUom: record.defaultUom,
|
|
378
|
+
trackLot: !!record.trackLot,
|
|
379
|
+
trackSerial: !!record.trackSerial,
|
|
380
|
+
trackExpiration: !!record.trackExpiration,
|
|
381
|
+
defaultStrategy: record.defaultStrategy,
|
|
382
|
+
reorderPoint: record.reorderPoint,
|
|
383
|
+
safetyStock: record.safetyStock,
|
|
384
|
+
metadata: (record.metadata ?? null) as JsonValue | null,
|
|
385
|
+
createdAt: record.createdAt.toISOString(),
|
|
386
|
+
updatedAt: record.updatedAt.toISOString(),
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function loadInventoryProfileSnapshot(
|
|
391
|
+
em: EntityManager,
|
|
392
|
+
ctx: CommandRuntimeContext,
|
|
393
|
+
id: string,
|
|
394
|
+
): Promise<ProductInventoryProfileSnapshot | null> {
|
|
395
|
+
const record = await findOneWithDecryption(em, ProductInventoryProfile, { id }, undefined, resolveScope(ctx))
|
|
396
|
+
return record ? snapshotInventoryProfile(record) : null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function snapshotInventoryLot(record: InventoryLot): InventoryLotSnapshot {
|
|
400
|
+
return {
|
|
401
|
+
id: record.id,
|
|
402
|
+
organizationId: record.organizationId,
|
|
403
|
+
tenantId: record.tenantId,
|
|
404
|
+
catalogVariantId: record.catalogVariantId,
|
|
405
|
+
sku: record.sku,
|
|
406
|
+
lotNumber: record.lotNumber,
|
|
407
|
+
batchNumber: record.batchNumber ?? null,
|
|
408
|
+
manufacturedAt: isoOrNull(record.manufacturedAt ?? null),
|
|
409
|
+
bestBeforeAt: isoOrNull(record.bestBeforeAt ?? null),
|
|
410
|
+
expiresAt: isoOrNull(record.expiresAt ?? null),
|
|
411
|
+
status: record.status,
|
|
412
|
+
metadata: (record.metadata ?? null) as JsonValue | null,
|
|
413
|
+
createdAt: record.createdAt.toISOString(),
|
|
414
|
+
updatedAt: record.updatedAt.toISOString(),
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function loadInventoryLotSnapshot(
|
|
419
|
+
em: EntityManager,
|
|
420
|
+
ctx: CommandRuntimeContext,
|
|
421
|
+
id: string,
|
|
422
|
+
): Promise<InventoryLotSnapshot | null> {
|
|
423
|
+
const record = await findOneWithDecryption(em, InventoryLot, { id }, undefined, resolveScope(ctx))
|
|
424
|
+
return record ? snapshotInventoryLot(record) : null
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function ensureWarehouseCodeUnique(
|
|
428
|
+
em: EntityManager,
|
|
429
|
+
tenantId: string,
|
|
430
|
+
organizationId: string,
|
|
431
|
+
code: string,
|
|
432
|
+
excludeId?: string,
|
|
433
|
+
) {
|
|
434
|
+
const existing = await findOneWithDecryption(
|
|
435
|
+
em,
|
|
436
|
+
Warehouse,
|
|
437
|
+
{ tenantId, organizationId, code, deletedAt: null },
|
|
438
|
+
undefined,
|
|
439
|
+
{ tenantId, organizationId }
|
|
440
|
+
)
|
|
441
|
+
if (existing && existing.id !== excludeId) {
|
|
442
|
+
throw new CrudHttpError(409, { error: 'Warehouse code already exists in this organization.' })
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function loadDemotedPrimarySnapshots(
|
|
447
|
+
em: EntityManager,
|
|
448
|
+
organizationId: string,
|
|
449
|
+
tenantId: string,
|
|
450
|
+
warehouseId: string,
|
|
451
|
+
): Promise<PrimaryDemotionSnapshot[]> {
|
|
452
|
+
const siblings = await findWithDecryption(
|
|
453
|
+
em,
|
|
454
|
+
Warehouse,
|
|
455
|
+
{
|
|
456
|
+
organizationId,
|
|
457
|
+
id: { $ne: warehouseId },
|
|
458
|
+
isPrimary: true,
|
|
459
|
+
deletedAt: null,
|
|
460
|
+
},
|
|
461
|
+
undefined,
|
|
462
|
+
{ tenantId, organizationId },
|
|
463
|
+
)
|
|
464
|
+
return siblings.map((record) => ({ id: record.id, isPrimary: true }))
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function enforcePrimaryWarehouse(
|
|
468
|
+
em: EntityManager,
|
|
469
|
+
organizationId: string,
|
|
470
|
+
tenantId: string,
|
|
471
|
+
warehouseId: string,
|
|
472
|
+
): Promise<PrimaryDemotionSnapshot[]> {
|
|
473
|
+
const demotedPrimariesBefore = await loadDemotedPrimarySnapshots(em, organizationId, tenantId, warehouseId)
|
|
474
|
+
if (demotedPrimariesBefore.length === 0) return []
|
|
475
|
+
await em.nativeUpdate(
|
|
476
|
+
Warehouse,
|
|
477
|
+
{
|
|
478
|
+
organizationId,
|
|
479
|
+
id: { $ne: warehouseId },
|
|
480
|
+
isPrimary: true,
|
|
481
|
+
deletedAt: null,
|
|
482
|
+
},
|
|
483
|
+
{ isPrimary: false },
|
|
484
|
+
)
|
|
485
|
+
return demotedPrimariesBefore
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function rejectInactivePrimaryWarehouse(ctx: CommandRuntimeContext): Promise<never> {
|
|
489
|
+
const { translate } = await resolveTranslations()
|
|
490
|
+
const message = translate(
|
|
491
|
+
'wms.validation.warehouse.inactivePrimary',
|
|
492
|
+
'Inactive warehouses cannot be marked as primary.',
|
|
493
|
+
)
|
|
494
|
+
throw new CrudHttpError(422, { error: message, fieldErrors: { isPrimary: message } })
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const WMS_WAREHOUSE_PRIMARY_UNIQUE_CONSTRAINT = 'wms_warehouses_org_primary_unique_idx'
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Defense in depth: a concurrent request can still win the race against the
|
|
501
|
+
* `wms_warehouses_org_primary_unique_idx` partial unique index between this
|
|
502
|
+
* request's demotion step and its own promote-to-primary flush. Translate
|
|
503
|
+
* that residual DB-level conflict into an actionable 409 instead of letting a
|
|
504
|
+
* raw Postgres unique-violation surface as a generic 500 (#4096).
|
|
505
|
+
*/
|
|
506
|
+
async function rejectPrimaryWarehouseConflict(): Promise<never> {
|
|
507
|
+
const { translate } = await resolveTranslations()
|
|
508
|
+
const message = translate(
|
|
509
|
+
'wms.validation.warehouse.primaryConflict',
|
|
510
|
+
'Another warehouse was just set as primary for this organization. Please retry.',
|
|
511
|
+
)
|
|
512
|
+
throw new CrudHttpError(409, { error: message, fieldErrors: { isPrimary: message } })
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function restoreDemotedPrimaryWarehouses(
|
|
516
|
+
em: EntityManager,
|
|
517
|
+
ctx: CommandRuntimeContext,
|
|
518
|
+
demotedPrimariesBefore: PrimaryDemotionSnapshot[] | undefined | null,
|
|
519
|
+
): Promise<void> {
|
|
520
|
+
if (!demotedPrimariesBefore?.length) return
|
|
521
|
+
for (const snapshot of demotedPrimariesBefore) {
|
|
522
|
+
const record = await findOneWithDecryption(em, Warehouse, { id: snapshot.id }, undefined, resolveScope(ctx))
|
|
523
|
+
if (!record) continue
|
|
524
|
+
ensureTenantScope(ctx, record.tenantId)
|
|
525
|
+
ensureOrganizationScope(ctx, record.organizationId)
|
|
526
|
+
record.isPrimary = snapshot.isPrimary
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function ensureZoneCodeUnique(
|
|
531
|
+
em: EntityManager,
|
|
532
|
+
warehouseId: string,
|
|
533
|
+
tenantId: string,
|
|
534
|
+
organizationId: string,
|
|
535
|
+
code: string,
|
|
536
|
+
excludeId?: string,
|
|
537
|
+
) {
|
|
538
|
+
const existing = await findOneWithDecryption(
|
|
539
|
+
em,
|
|
540
|
+
WarehouseZone,
|
|
541
|
+
{ warehouse: warehouseId, tenantId, organizationId, code, deletedAt: null },
|
|
542
|
+
undefined,
|
|
543
|
+
{ tenantId, organizationId }
|
|
544
|
+
)
|
|
545
|
+
if (existing && existing.id !== excludeId) {
|
|
546
|
+
throw new CrudHttpError(409, { error: 'Zone code already exists in this warehouse.' })
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function ensureLocationCodeUnique(
|
|
551
|
+
em: EntityManager,
|
|
552
|
+
warehouseId: string,
|
|
553
|
+
tenantId: string,
|
|
554
|
+
organizationId: string,
|
|
555
|
+
code: string,
|
|
556
|
+
excludeId?: string,
|
|
557
|
+
) {
|
|
558
|
+
const existing = await findOneWithDecryption(
|
|
559
|
+
em,
|
|
560
|
+
WarehouseLocation,
|
|
561
|
+
{ warehouse: warehouseId, tenantId, organizationId, code, deletedAt: null },
|
|
562
|
+
undefined,
|
|
563
|
+
{ tenantId, organizationId }
|
|
564
|
+
)
|
|
565
|
+
if (existing && existing.id !== excludeId) {
|
|
566
|
+
throw new CrudHttpError(409, { error: 'Location code already exists in this warehouse.' })
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function ensureProfileUniqueness(
|
|
571
|
+
em: EntityManager,
|
|
572
|
+
input: {
|
|
573
|
+
tenantId: string
|
|
574
|
+
organizationId: string
|
|
575
|
+
catalogProductId: string
|
|
576
|
+
catalogVariantId?: string | null
|
|
577
|
+
},
|
|
578
|
+
excludeId?: string,
|
|
579
|
+
) {
|
|
580
|
+
const where = input.catalogVariantId
|
|
581
|
+
? {
|
|
582
|
+
tenantId: input.tenantId,
|
|
583
|
+
organizationId: input.organizationId,
|
|
584
|
+
catalogVariantId: input.catalogVariantId,
|
|
585
|
+
deletedAt: null,
|
|
586
|
+
}
|
|
587
|
+
: {
|
|
588
|
+
tenantId: input.tenantId,
|
|
589
|
+
organizationId: input.organizationId,
|
|
590
|
+
catalogProductId: input.catalogProductId,
|
|
591
|
+
catalogVariantId: null,
|
|
592
|
+
deletedAt: null,
|
|
593
|
+
}
|
|
594
|
+
const existing = await findOneWithDecryption(
|
|
595
|
+
em,
|
|
596
|
+
ProductInventoryProfile,
|
|
597
|
+
where,
|
|
598
|
+
undefined,
|
|
599
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
600
|
+
)
|
|
601
|
+
if (existing && existing.id !== excludeId) {
|
|
602
|
+
throw new CrudHttpError(409, { error: 'Inventory profile already exists for this product scope.' })
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function ensureLotUniqueness(
|
|
607
|
+
em: EntityManager,
|
|
608
|
+
input: { tenantId: string; organizationId: string; catalogVariantId: string; lotNumber: string },
|
|
609
|
+
excludeId?: string,
|
|
610
|
+
) {
|
|
611
|
+
const existing = await findOneWithDecryption(
|
|
612
|
+
em,
|
|
613
|
+
InventoryLot,
|
|
614
|
+
{
|
|
615
|
+
tenantId: input.tenantId,
|
|
616
|
+
organizationId: input.organizationId,
|
|
617
|
+
catalogVariantId: input.catalogVariantId,
|
|
618
|
+
lotNumber: input.lotNumber,
|
|
619
|
+
deletedAt: null,
|
|
620
|
+
},
|
|
621
|
+
undefined,
|
|
622
|
+
{ tenantId: input.tenantId, organizationId: input.organizationId }
|
|
623
|
+
)
|
|
624
|
+
if (existing && existing.id !== excludeId) {
|
|
625
|
+
throw new CrudHttpError(409, { error: 'Lot number already exists for this variant.' })
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function resolveParentLocation(
|
|
630
|
+
em: EntityManager,
|
|
631
|
+
ctx: CommandRuntimeContext,
|
|
632
|
+
warehouseId: string,
|
|
633
|
+
parentId: string | null | undefined,
|
|
634
|
+
) {
|
|
635
|
+
if (!parentId) return null
|
|
636
|
+
const parent = await loadLocation(em, ctx, parentId)
|
|
637
|
+
const parentWarehouseId = typeof parent.warehouse === 'string' ? parent.warehouse : parent.warehouse.id
|
|
638
|
+
if (parentWarehouseId !== warehouseId) {
|
|
639
|
+
throw new CrudHttpError(422, { error: 'Parent location must belong to the same warehouse.' })
|
|
640
|
+
}
|
|
641
|
+
return parent
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const createWarehouseCommand: CommandHandler<
|
|
645
|
+
WarehouseCreateInput,
|
|
646
|
+
{ warehouseId: string; demotedPrimariesBefore?: PrimaryDemotionSnapshot[] }
|
|
647
|
+
> = {
|
|
648
|
+
id: 'wms.warehouses.create',
|
|
649
|
+
async execute(rawInput, ctx) {
|
|
650
|
+
const parsed = warehouseCreateSchema.parse(rawInput ?? {})
|
|
651
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
652
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
653
|
+
const em = resolveEm(ctx)
|
|
654
|
+
await ensureWarehouseCodeUnique(em, parsed.tenantId, parsed.organizationId, parsed.code)
|
|
655
|
+
const isActive = parsed.isActive ?? true
|
|
656
|
+
const isPrimary = parsed.isPrimary ?? false
|
|
657
|
+
if (isPrimary && !isActive) {
|
|
658
|
+
await rejectInactivePrimaryWarehouse(ctx)
|
|
659
|
+
}
|
|
660
|
+
const warehouse = em.create(Warehouse, {
|
|
661
|
+
organizationId: parsed.organizationId,
|
|
662
|
+
tenantId: parsed.tenantId,
|
|
663
|
+
name: parsed.name,
|
|
664
|
+
code: parsed.code,
|
|
665
|
+
isActive,
|
|
666
|
+
// Always insert as non-primary first: inserting with isPrimary=true would
|
|
667
|
+
// race the `wms_warehouses_org_primary_unique_idx` partial unique index
|
|
668
|
+
// against an existing sibling primary before that sibling gets demoted
|
|
669
|
+
// below (Postgres checks partial unique indexes per-statement, not at
|
|
670
|
+
// commit time).
|
|
671
|
+
isPrimary: false,
|
|
672
|
+
addressLine1: normalizeOptionalString(parsed.addressLine1),
|
|
673
|
+
city: normalizeOptionalString(parsed.city),
|
|
674
|
+
postalCode: normalizeOptionalString(parsed.postalCode),
|
|
675
|
+
country: normalizeOptionalString(parsed.country),
|
|
676
|
+
timezone: normalizeOptionalString(parsed.timezone),
|
|
677
|
+
metadata: toJsonValue(parsed.metadata),
|
|
678
|
+
})
|
|
679
|
+
let demotedPrimariesBefore: PrimaryDemotionSnapshot[] = []
|
|
680
|
+
try {
|
|
681
|
+
await withAtomicFlush(
|
|
682
|
+
em,
|
|
683
|
+
[
|
|
684
|
+
() => {
|
|
685
|
+
em.persist(warehouse)
|
|
686
|
+
},
|
|
687
|
+
async () => {
|
|
688
|
+
if (!isPrimary) return
|
|
689
|
+
demotedPrimariesBefore = await enforcePrimaryWarehouse(
|
|
690
|
+
em,
|
|
691
|
+
warehouse.organizationId,
|
|
692
|
+
warehouse.tenantId,
|
|
693
|
+
warehouse.id,
|
|
694
|
+
)
|
|
695
|
+
},
|
|
696
|
+
() => {
|
|
697
|
+
if (isPrimary) warehouse.isPrimary = true
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
{ transaction: true, label: 'wms.warehouses.create' },
|
|
701
|
+
)
|
|
702
|
+
} catch (err) {
|
|
703
|
+
if (isUniqueViolation(err, WMS_WAREHOUSE_PRIMARY_UNIQUE_CONSTRAINT)) {
|
|
704
|
+
await rejectPrimaryWarehouseConflict()
|
|
705
|
+
}
|
|
706
|
+
throw err
|
|
707
|
+
}
|
|
708
|
+
void emitWmsEvent('wms.warehouse.created', {
|
|
709
|
+
id: warehouse.id,
|
|
710
|
+
warehouseId: warehouse.id,
|
|
711
|
+
tenantId: warehouse.tenantId,
|
|
712
|
+
organizationId: warehouse.organizationId,
|
|
713
|
+
}).catch(() => undefined)
|
|
714
|
+
return {
|
|
715
|
+
warehouseId: warehouse.id,
|
|
716
|
+
...(demotedPrimariesBefore.length > 0 ? { demotedPrimariesBefore } : {}),
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
captureAfter: async (_input, result, ctx) => {
|
|
720
|
+
const em = resolveEm(ctx)
|
|
721
|
+
return loadWarehouseSnapshot(em, ctx, result.warehouseId)
|
|
722
|
+
},
|
|
723
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
724
|
+
const base = await buildCrudLog(ctx, input, result?.warehouseId ?? null, 'wms.audit.warehouse.create', 'Create warehouse', 'wms.warehouse')
|
|
725
|
+
const after = snapshots?.after as WarehouseSnapshot | undefined
|
|
726
|
+
const demotedPrimariesBefore = result?.demotedPrimariesBefore
|
|
727
|
+
return {
|
|
728
|
+
...base,
|
|
729
|
+
snapshotAfter: after,
|
|
730
|
+
payload: {
|
|
731
|
+
undo: {
|
|
732
|
+
after,
|
|
733
|
+
...(demotedPrimariesBefore?.length ? { demotedPrimariesBefore } : {}),
|
|
734
|
+
} satisfies WarehouseUndoPayload,
|
|
735
|
+
},
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
undo: async ({ logEntry, ctx }) => {
|
|
739
|
+
const payload = extractUndoPayload<WarehouseUndoPayload>(logEntry)
|
|
740
|
+
const after = payload?.after
|
|
741
|
+
if (!after) return
|
|
742
|
+
const em = resolveEm(ctx)
|
|
743
|
+
const record = await findOneWithDecryption(
|
|
744
|
+
em,
|
|
745
|
+
Warehouse,
|
|
746
|
+
{ id: after.id },
|
|
747
|
+
undefined,
|
|
748
|
+
resolveScope(ctx, { tenantId: after.tenantId, organizationId: after.organizationId }),
|
|
749
|
+
)
|
|
750
|
+
if (!record) return
|
|
751
|
+
ensureTenantScope(ctx, record.tenantId)
|
|
752
|
+
ensureOrganizationScope(ctx, record.organizationId)
|
|
753
|
+
record.deletedAt = new Date()
|
|
754
|
+
await em.flush()
|
|
755
|
+
if (payload.demotedPrimariesBefore?.length) {
|
|
756
|
+
await restoreDemotedPrimaryWarehouses(em, ctx, payload.demotedPrimariesBefore)
|
|
757
|
+
await em.flush()
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const updateWarehouseCommand: CommandHandler<
|
|
763
|
+
WarehouseUpdateInput,
|
|
764
|
+
{ warehouseId: string; demotedPrimariesBefore?: PrimaryDemotionSnapshot[] }
|
|
765
|
+
> = {
|
|
766
|
+
id: 'wms.warehouses.update',
|
|
767
|
+
prepare: async (input, ctx) => {
|
|
768
|
+
const id = requireId(input?.id, 'Warehouse')
|
|
769
|
+
const em = resolveEm(ctx)
|
|
770
|
+
const before = await loadWarehouseSnapshot(em, ctx, id)
|
|
771
|
+
return before ? { before } : {}
|
|
772
|
+
},
|
|
773
|
+
async execute(rawInput, ctx) {
|
|
774
|
+
const parsed = warehouseUpdateSchema.parse(rawInput ?? {})
|
|
775
|
+
const em = resolveEm(ctx)
|
|
776
|
+
const warehouse = await loadWarehouse(em, ctx, parsed.id)
|
|
777
|
+
if (parsed.code !== undefined && parsed.code !== warehouse.code) {
|
|
778
|
+
await ensureWarehouseCodeUnique(em, warehouse.tenantId, warehouse.organizationId, parsed.code, warehouse.id)
|
|
779
|
+
}
|
|
780
|
+
// Resolve the intended isActive/isPrimary state before mutating anything so
|
|
781
|
+
// validation can run before any write. Deactivating (isActive explicitly
|
|
782
|
+
// false in this request) silently auto-clears isPrimary; otherwise ending up
|
|
783
|
+
// primary while inactive is rejected.
|
|
784
|
+
let nextIsActive = warehouse.isActive
|
|
785
|
+
let nextIsPrimary = warehouse.isPrimary
|
|
786
|
+
if (parsed.isActive !== undefined) nextIsActive = parsed.isActive
|
|
787
|
+
if (parsed.isPrimary !== undefined) nextIsPrimary = parsed.isPrimary
|
|
788
|
+
if (parsed.isActive === false && nextIsPrimary) {
|
|
789
|
+
nextIsPrimary = false
|
|
790
|
+
} else if (nextIsPrimary && !nextIsActive) {
|
|
791
|
+
await rejectInactivePrimaryWarehouse(ctx)
|
|
792
|
+
}
|
|
793
|
+
let demotedPrimariesBefore: PrimaryDemotionSnapshot[] = []
|
|
794
|
+
try {
|
|
795
|
+
await withAtomicFlush(
|
|
796
|
+
em,
|
|
797
|
+
[
|
|
798
|
+
() => {
|
|
799
|
+
if (parsed.code !== undefined) warehouse.code = parsed.code
|
|
800
|
+
if (parsed.name !== undefined) warehouse.name = parsed.name
|
|
801
|
+
if (parsed.addressLine1 !== undefined) warehouse.addressLine1 = normalizeOptionalString(parsed.addressLine1)
|
|
802
|
+
if (parsed.city !== undefined) warehouse.city = normalizeOptionalString(parsed.city)
|
|
803
|
+
if (parsed.postalCode !== undefined) warehouse.postalCode = normalizeOptionalString(parsed.postalCode)
|
|
804
|
+
if (parsed.country !== undefined) warehouse.country = normalizeOptionalString(parsed.country)
|
|
805
|
+
if (parsed.timezone !== undefined) warehouse.timezone = normalizeOptionalString(parsed.timezone)
|
|
806
|
+
if (parsed.metadata !== undefined) warehouse.metadata = toJsonValue(parsed.metadata)
|
|
807
|
+
warehouse.isActive = nextIsActive
|
|
808
|
+
// Hold this row's own primary flag at false until siblings are demoted
|
|
809
|
+
// (next phase) so the at-most-one-primary partial unique index never
|
|
810
|
+
// sees two primary rows for the org within the same SQL statement.
|
|
811
|
+
warehouse.isPrimary = false
|
|
812
|
+
},
|
|
813
|
+
async () => {
|
|
814
|
+
if (!nextIsPrimary) return
|
|
815
|
+
demotedPrimariesBefore = await enforcePrimaryWarehouse(
|
|
816
|
+
em,
|
|
817
|
+
warehouse.organizationId,
|
|
818
|
+
warehouse.tenantId,
|
|
819
|
+
warehouse.id,
|
|
820
|
+
)
|
|
821
|
+
},
|
|
822
|
+
() => {
|
|
823
|
+
warehouse.isPrimary = nextIsPrimary
|
|
824
|
+
},
|
|
825
|
+
],
|
|
826
|
+
{ transaction: true, label: 'wms.warehouses.update' },
|
|
827
|
+
)
|
|
828
|
+
} catch (err) {
|
|
829
|
+
if (isUniqueViolation(err, WMS_WAREHOUSE_PRIMARY_UNIQUE_CONSTRAINT)) {
|
|
830
|
+
await rejectPrimaryWarehouseConflict()
|
|
831
|
+
}
|
|
832
|
+
throw err
|
|
833
|
+
}
|
|
834
|
+
void emitWmsEvent('wms.warehouse.updated', {
|
|
835
|
+
id: warehouse.id,
|
|
836
|
+
warehouseId: warehouse.id,
|
|
837
|
+
tenantId: warehouse.tenantId,
|
|
838
|
+
organizationId: warehouse.organizationId,
|
|
839
|
+
}).catch(() => undefined)
|
|
840
|
+
return {
|
|
841
|
+
warehouseId: warehouse.id,
|
|
842
|
+
...(demotedPrimariesBefore.length > 0 ? { demotedPrimariesBefore } : {}),
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
captureAfter: async (_input, result, ctx) => {
|
|
846
|
+
const em = resolveEm(ctx)
|
|
847
|
+
return loadWarehouseSnapshot(em, ctx, result.warehouseId)
|
|
848
|
+
},
|
|
849
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
850
|
+
const base = await buildCrudLog(ctx, input, result?.warehouseId ?? null, 'wms.audit.warehouse.update', 'Update warehouse', 'wms.warehouse')
|
|
851
|
+
const before = snapshots?.before as WarehouseSnapshot | undefined
|
|
852
|
+
const after = snapshots?.after as WarehouseSnapshot | undefined
|
|
853
|
+
const demotedPrimariesBefore = result?.demotedPrimariesBefore
|
|
854
|
+
return {
|
|
855
|
+
...base,
|
|
856
|
+
snapshotBefore: before,
|
|
857
|
+
snapshotAfter: after,
|
|
858
|
+
payload: {
|
|
859
|
+
undo: {
|
|
860
|
+
before,
|
|
861
|
+
after,
|
|
862
|
+
...(demotedPrimariesBefore?.length ? { demotedPrimariesBefore } : {}),
|
|
863
|
+
} satisfies WarehouseUndoPayload,
|
|
864
|
+
},
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
undo: async ({ logEntry, ctx }) => {
|
|
868
|
+
const payload = extractUndoPayload<WarehouseUndoPayload>(logEntry)
|
|
869
|
+
const before = payload?.before
|
|
870
|
+
if (!before) return
|
|
871
|
+
const em = resolveEm(ctx)
|
|
872
|
+
let record = await findOneWithDecryption(
|
|
873
|
+
em,
|
|
874
|
+
Warehouse,
|
|
875
|
+
{ id: before.id },
|
|
876
|
+
undefined,
|
|
877
|
+
resolveScope(ctx, { tenantId: before.tenantId, organizationId: before.organizationId }),
|
|
878
|
+
)
|
|
879
|
+
if (!record) {
|
|
880
|
+
record = em.create(Warehouse, {
|
|
881
|
+
id: before.id,
|
|
882
|
+
organizationId: before.organizationId,
|
|
883
|
+
tenantId: before.tenantId,
|
|
884
|
+
name: before.name,
|
|
885
|
+
code: before.code,
|
|
886
|
+
isActive: before.isActive,
|
|
887
|
+
isPrimary: before.isPrimary,
|
|
888
|
+
addressLine1: before.addressLine1,
|
|
889
|
+
city: before.city,
|
|
890
|
+
postalCode: before.postalCode,
|
|
891
|
+
country: before.country,
|
|
892
|
+
timezone: before.timezone,
|
|
893
|
+
metadata: before.metadata,
|
|
894
|
+
createdAt: new Date(before.createdAt),
|
|
895
|
+
updatedAt: new Date(before.updatedAt),
|
|
896
|
+
})
|
|
897
|
+
em.persist(record)
|
|
898
|
+
} else {
|
|
899
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
900
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
901
|
+
record.name = before.name
|
|
902
|
+
record.code = before.code
|
|
903
|
+
record.isActive = before.isActive
|
|
904
|
+
record.isPrimary = before.isPrimary
|
|
905
|
+
record.addressLine1 = before.addressLine1
|
|
906
|
+
record.city = before.city
|
|
907
|
+
record.postalCode = before.postalCode
|
|
908
|
+
record.country = before.country
|
|
909
|
+
record.timezone = before.timezone
|
|
910
|
+
record.metadata = before.metadata
|
|
911
|
+
record.deletedAt = null
|
|
912
|
+
}
|
|
913
|
+
await em.flush()
|
|
914
|
+
if (payload.demotedPrimariesBefore?.length) {
|
|
915
|
+
await restoreDemotedPrimaryWarehouses(em, ctx, payload.demotedPrimariesBefore)
|
|
916
|
+
await em.flush()
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const deleteWarehouseCommand: CommandHandler<{ id?: string }, { warehouseId: string }> = {
|
|
922
|
+
id: 'wms.warehouses.delete',
|
|
923
|
+
prepare: async (input, ctx) => {
|
|
924
|
+
const id = requireId(input?.id, 'Warehouse')
|
|
925
|
+
const em = resolveEm(ctx)
|
|
926
|
+
const before = await loadWarehouseSnapshot(em, ctx, id)
|
|
927
|
+
return before ? { before } : {}
|
|
928
|
+
},
|
|
929
|
+
async execute(input, ctx) {
|
|
930
|
+
const warehouseId = requireId(input?.id, 'Warehouse')
|
|
931
|
+
const em = resolveEm(ctx)
|
|
932
|
+
const warehouse = await loadWarehouse(em, ctx, warehouseId)
|
|
933
|
+
warehouse.deletedAt = new Date()
|
|
934
|
+
await em.flush()
|
|
935
|
+
return { warehouseId: warehouse.id }
|
|
936
|
+
},
|
|
937
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
938
|
+
const base = await buildCrudLog(ctx, input, result?.warehouseId ?? null, 'wms.audit.warehouse.delete', 'Delete warehouse', 'wms.warehouse')
|
|
939
|
+
const before = snapshots?.before as WarehouseSnapshot | undefined
|
|
940
|
+
return { ...base, snapshotBefore: before, payload: { undo: { before } satisfies WarehouseUndoPayload } }
|
|
941
|
+
},
|
|
942
|
+
undo: async ({ logEntry, ctx }) => {
|
|
943
|
+
const payload = extractUndoPayload<WarehouseUndoPayload>(logEntry)
|
|
944
|
+
const before = payload?.before
|
|
945
|
+
if (!before) return
|
|
946
|
+
const em = resolveEm(ctx)
|
|
947
|
+
let record = await findOneWithDecryption(
|
|
948
|
+
em,
|
|
949
|
+
Warehouse,
|
|
950
|
+
{ id: before.id },
|
|
951
|
+
undefined,
|
|
952
|
+
resolveScope(ctx, { tenantId: before.tenantId, organizationId: before.organizationId }),
|
|
953
|
+
)
|
|
954
|
+
if (!record) {
|
|
955
|
+
record = em.create(Warehouse, {
|
|
956
|
+
id: before.id,
|
|
957
|
+
organizationId: before.organizationId,
|
|
958
|
+
tenantId: before.tenantId,
|
|
959
|
+
name: before.name,
|
|
960
|
+
code: before.code,
|
|
961
|
+
isActive: before.isActive,
|
|
962
|
+
isPrimary: before.isPrimary,
|
|
963
|
+
addressLine1: before.addressLine1,
|
|
964
|
+
city: before.city,
|
|
965
|
+
postalCode: before.postalCode,
|
|
966
|
+
country: before.country,
|
|
967
|
+
timezone: before.timezone,
|
|
968
|
+
metadata: before.metadata,
|
|
969
|
+
createdAt: new Date(before.createdAt),
|
|
970
|
+
updatedAt: new Date(before.updatedAt),
|
|
971
|
+
})
|
|
972
|
+
em.persist(record)
|
|
973
|
+
} else {
|
|
974
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
975
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
976
|
+
record.deletedAt = null
|
|
977
|
+
record.name = before.name
|
|
978
|
+
record.code = before.code
|
|
979
|
+
record.isActive = before.isActive
|
|
980
|
+
record.isPrimary = before.isPrimary
|
|
981
|
+
record.addressLine1 = before.addressLine1
|
|
982
|
+
record.city = before.city
|
|
983
|
+
record.postalCode = before.postalCode
|
|
984
|
+
record.country = before.country
|
|
985
|
+
record.timezone = before.timezone
|
|
986
|
+
record.metadata = before.metadata
|
|
987
|
+
}
|
|
988
|
+
await em.flush()
|
|
989
|
+
},
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
async function restoreZoneFromSnapshot(em: EntityManager, before: WarehouseZoneSnapshot): Promise<void> {
|
|
993
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
994
|
+
const warehouseRef = await findOneWithDecryption(em, Warehouse, { id: before.warehouseId }, undefined, scope)
|
|
995
|
+
if (!warehouseRef) {
|
|
996
|
+
throw new CrudHttpError(409, { error: 'Cannot undo zone: parent warehouse no longer exists.' })
|
|
997
|
+
}
|
|
998
|
+
let record = await findOneWithDecryption(em, WarehouseZone, { id: before.id }, undefined, scope)
|
|
999
|
+
if (!record) {
|
|
1000
|
+
record = em.create(WarehouseZone, {
|
|
1001
|
+
id: before.id,
|
|
1002
|
+
organizationId: before.organizationId,
|
|
1003
|
+
tenantId: before.tenantId,
|
|
1004
|
+
warehouse: warehouseRef,
|
|
1005
|
+
code: before.code,
|
|
1006
|
+
name: before.name,
|
|
1007
|
+
priority: before.priority,
|
|
1008
|
+
metadata: before.metadata,
|
|
1009
|
+
createdAt: new Date(before.createdAt),
|
|
1010
|
+
updatedAt: new Date(before.updatedAt),
|
|
1011
|
+
})
|
|
1012
|
+
em.persist(record)
|
|
1013
|
+
} else {
|
|
1014
|
+
record.warehouse = warehouseRef
|
|
1015
|
+
record.code = before.code
|
|
1016
|
+
record.name = before.name
|
|
1017
|
+
record.priority = before.priority
|
|
1018
|
+
record.metadata = before.metadata
|
|
1019
|
+
record.deletedAt = null
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const createWarehouseZoneCommand: CommandHandler<WarehouseZoneCreateInput, { zoneId: string }> = {
|
|
1024
|
+
id: 'wms.zones.create',
|
|
1025
|
+
async execute(rawInput, ctx) {
|
|
1026
|
+
const parsed = warehouseZoneCreateSchema.parse(rawInput ?? {})
|
|
1027
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
1028
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
1029
|
+
const em = resolveEm(ctx)
|
|
1030
|
+
const warehouse = await loadWarehouse(em, ctx, parsed.warehouseId)
|
|
1031
|
+
await ensureZoneCodeUnique(em, warehouse.id, warehouse.tenantId, warehouse.organizationId, parsed.code)
|
|
1032
|
+
const zone = em.create(WarehouseZone, {
|
|
1033
|
+
organizationId: parsed.organizationId,
|
|
1034
|
+
tenantId: parsed.tenantId,
|
|
1035
|
+
warehouse,
|
|
1036
|
+
code: parsed.code,
|
|
1037
|
+
name: parsed.name,
|
|
1038
|
+
priority: parsed.priority ?? 0,
|
|
1039
|
+
metadata: toJsonValue(parsed.metadata),
|
|
1040
|
+
})
|
|
1041
|
+
await em.persist(zone).flush()
|
|
1042
|
+
void emitWmsEvent('wms.zone.created', {
|
|
1043
|
+
id: zone.id,
|
|
1044
|
+
zoneId: zone.id,
|
|
1045
|
+
warehouseId: typeof zone.warehouse === 'string' ? zone.warehouse : zone.warehouse.id,
|
|
1046
|
+
tenantId: zone.tenantId,
|
|
1047
|
+
organizationId: zone.organizationId,
|
|
1048
|
+
}).catch(() => undefined)
|
|
1049
|
+
return { zoneId: zone.id }
|
|
1050
|
+
},
|
|
1051
|
+
captureAfter: async (_input, result, ctx) => {
|
|
1052
|
+
const em = resolveEm(ctx)
|
|
1053
|
+
return loadWarehouseZoneSnapshot(em, ctx, result.zoneId)
|
|
1054
|
+
},
|
|
1055
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1056
|
+
const base = await buildCrudLog(ctx, input, result?.zoneId ?? null, 'wms.audit.zone.create', 'Create warehouse zone', 'wms.zone')
|
|
1057
|
+
const after = snapshots?.after as WarehouseZoneSnapshot | undefined
|
|
1058
|
+
return { ...base, snapshotAfter: after, payload: { undo: { after } satisfies WarehouseZoneUndoPayload } }
|
|
1059
|
+
},
|
|
1060
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1061
|
+
const payload = extractUndoPayload<WarehouseZoneUndoPayload>(logEntry)
|
|
1062
|
+
const after = payload?.after
|
|
1063
|
+
if (!after) return
|
|
1064
|
+
const em = resolveEm(ctx)
|
|
1065
|
+
const record = await findOneWithDecryption(
|
|
1066
|
+
em,
|
|
1067
|
+
WarehouseZone,
|
|
1068
|
+
{ id: after.id },
|
|
1069
|
+
undefined,
|
|
1070
|
+
resolveScope(ctx, { tenantId: after.tenantId, organizationId: after.organizationId }),
|
|
1071
|
+
)
|
|
1072
|
+
if (!record) return
|
|
1073
|
+
ensureTenantScope(ctx, record.tenantId)
|
|
1074
|
+
ensureOrganizationScope(ctx, record.organizationId)
|
|
1075
|
+
record.deletedAt = new Date()
|
|
1076
|
+
await em.flush()
|
|
1077
|
+
},
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const updateWarehouseZoneCommand: CommandHandler<WarehouseZoneUpdateInput, { zoneId: string }> = {
|
|
1081
|
+
id: 'wms.zones.update',
|
|
1082
|
+
prepare: async (input, ctx) => {
|
|
1083
|
+
const id = requireId(input?.id, 'Zone')
|
|
1084
|
+
const em = resolveEm(ctx)
|
|
1085
|
+
const before = await loadWarehouseZoneSnapshot(em, ctx, id)
|
|
1086
|
+
return before ? { before } : {}
|
|
1087
|
+
},
|
|
1088
|
+
async execute(rawInput, ctx) {
|
|
1089
|
+
const parsed = warehouseZoneUpdateSchema.parse(rawInput ?? {})
|
|
1090
|
+
const em = resolveEm(ctx)
|
|
1091
|
+
const zone = await loadZone(em, ctx, parsed.id)
|
|
1092
|
+
if (parsed.warehouseId !== undefined) {
|
|
1093
|
+
const warehouse = await loadWarehouse(em, ctx, parsed.warehouseId)
|
|
1094
|
+
zone.warehouse = warehouse
|
|
1095
|
+
}
|
|
1096
|
+
const warehouseId = typeof zone.warehouse === 'string' ? zone.warehouse : zone.warehouse.id
|
|
1097
|
+
if (parsed.code !== undefined && parsed.code !== zone.code) {
|
|
1098
|
+
await ensureZoneCodeUnique(em, warehouseId, zone.tenantId, zone.organizationId, parsed.code, zone.id)
|
|
1099
|
+
zone.code = parsed.code
|
|
1100
|
+
}
|
|
1101
|
+
if (parsed.name !== undefined) zone.name = parsed.name
|
|
1102
|
+
if (parsed.priority !== undefined) zone.priority = parsed.priority
|
|
1103
|
+
if (parsed.metadata !== undefined) zone.metadata = toJsonValue(parsed.metadata)
|
|
1104
|
+
await em.flush()
|
|
1105
|
+
void emitWmsEvent('wms.zone.updated', {
|
|
1106
|
+
id: zone.id,
|
|
1107
|
+
zoneId: zone.id,
|
|
1108
|
+
warehouseId: typeof zone.warehouse === 'string' ? zone.warehouse : zone.warehouse.id,
|
|
1109
|
+
tenantId: zone.tenantId,
|
|
1110
|
+
organizationId: zone.organizationId,
|
|
1111
|
+
}).catch(() => undefined)
|
|
1112
|
+
return { zoneId: zone.id }
|
|
1113
|
+
},
|
|
1114
|
+
captureAfter: async (_input, result, ctx) => {
|
|
1115
|
+
const em = resolveEm(ctx)
|
|
1116
|
+
return loadWarehouseZoneSnapshot(em, ctx, result.zoneId)
|
|
1117
|
+
},
|
|
1118
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1119
|
+
const base = await buildCrudLog(ctx, input, result?.zoneId ?? null, 'wms.audit.zone.update', 'Update warehouse zone', 'wms.zone')
|
|
1120
|
+
const before = snapshots?.before as WarehouseZoneSnapshot | undefined
|
|
1121
|
+
const after = snapshots?.after as WarehouseZoneSnapshot | undefined
|
|
1122
|
+
return { ...base, snapshotBefore: before, snapshotAfter: after, payload: { undo: { before, after } satisfies WarehouseZoneUndoPayload } }
|
|
1123
|
+
},
|
|
1124
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1125
|
+
const payload = extractUndoPayload<WarehouseZoneUndoPayload>(logEntry)
|
|
1126
|
+
const before = payload?.before
|
|
1127
|
+
if (!before) return
|
|
1128
|
+
const em = resolveEm(ctx)
|
|
1129
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
1130
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
1131
|
+
await restoreZoneFromSnapshot(em, before)
|
|
1132
|
+
await em.flush()
|
|
1133
|
+
},
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const deleteWarehouseZoneCommand: CommandHandler<{ id?: string }, { zoneId: string }> = {
|
|
1137
|
+
id: 'wms.zones.delete',
|
|
1138
|
+
prepare: async (input, ctx) => {
|
|
1139
|
+
const id = requireId(input?.id, 'Zone')
|
|
1140
|
+
const em = resolveEm(ctx)
|
|
1141
|
+
const before = await loadWarehouseZoneSnapshot(em, ctx, id)
|
|
1142
|
+
return before ? { before } : {}
|
|
1143
|
+
},
|
|
1144
|
+
async execute(input, ctx) {
|
|
1145
|
+
const zoneId = requireId(input?.id, 'Zone')
|
|
1146
|
+
const em = resolveEm(ctx)
|
|
1147
|
+
const zone = await loadZone(em, ctx, zoneId)
|
|
1148
|
+
zone.deletedAt = new Date()
|
|
1149
|
+
await em.flush()
|
|
1150
|
+
return { zoneId: zone.id }
|
|
1151
|
+
},
|
|
1152
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1153
|
+
const base = await buildCrudLog(ctx, input, result?.zoneId ?? null, 'wms.audit.zone.delete', 'Delete warehouse zone', 'wms.zone')
|
|
1154
|
+
const before = snapshots?.before as WarehouseZoneSnapshot | undefined
|
|
1155
|
+
return { ...base, snapshotBefore: before, payload: { undo: { before } satisfies WarehouseZoneUndoPayload } }
|
|
1156
|
+
},
|
|
1157
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1158
|
+
const payload = extractUndoPayload<WarehouseZoneUndoPayload>(logEntry)
|
|
1159
|
+
const before = payload?.before
|
|
1160
|
+
if (!before) return
|
|
1161
|
+
const em = resolveEm(ctx)
|
|
1162
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
1163
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
1164
|
+
await restoreZoneFromSnapshot(em, before)
|
|
1165
|
+
await em.flush()
|
|
1166
|
+
},
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async function restoreLocationFromSnapshot(em: EntityManager, before: WarehouseLocationSnapshot): Promise<void> {
|
|
1170
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
1171
|
+
const warehouseRef = await findOneWithDecryption(em, Warehouse, { id: before.warehouseId }, undefined, scope)
|
|
1172
|
+
if (!warehouseRef) {
|
|
1173
|
+
throw new CrudHttpError(409, { error: 'Cannot undo location: parent warehouse no longer exists.' })
|
|
1174
|
+
}
|
|
1175
|
+
let parentRef: WarehouseLocation | null = null
|
|
1176
|
+
if (before.parentId) {
|
|
1177
|
+
parentRef = await findOneWithDecryption(em, WarehouseLocation, { id: before.parentId }, undefined, scope)
|
|
1178
|
+
// Parent may have been deleted independently — fall back to root level
|
|
1179
|
+
// rather than blocking the undo (the audit log still records the original
|
|
1180
|
+
// parent id for manual reconciliation).
|
|
1181
|
+
}
|
|
1182
|
+
let record = await findOneWithDecryption(em, WarehouseLocation, { id: before.id }, undefined, scope)
|
|
1183
|
+
if (!record) {
|
|
1184
|
+
record = em.create(WarehouseLocation, {
|
|
1185
|
+
id: before.id,
|
|
1186
|
+
organizationId: before.organizationId,
|
|
1187
|
+
tenantId: before.tenantId,
|
|
1188
|
+
warehouse: warehouseRef,
|
|
1189
|
+
parent: parentRef,
|
|
1190
|
+
code: before.code,
|
|
1191
|
+
type: before.type,
|
|
1192
|
+
isActive: before.isActive,
|
|
1193
|
+
capacityUnits: before.capacityUnits,
|
|
1194
|
+
capacityWeight: before.capacityWeight,
|
|
1195
|
+
constraints: before.constraints,
|
|
1196
|
+
metadata: before.metadata,
|
|
1197
|
+
createdAt: new Date(before.createdAt),
|
|
1198
|
+
updatedAt: new Date(before.updatedAt),
|
|
1199
|
+
})
|
|
1200
|
+
em.persist(record)
|
|
1201
|
+
} else {
|
|
1202
|
+
record.warehouse = warehouseRef
|
|
1203
|
+
record.parent = parentRef
|
|
1204
|
+
record.code = before.code
|
|
1205
|
+
record.type = before.type
|
|
1206
|
+
record.isActive = before.isActive
|
|
1207
|
+
record.capacityUnits = before.capacityUnits
|
|
1208
|
+
record.capacityWeight = before.capacityWeight
|
|
1209
|
+
record.constraints = before.constraints
|
|
1210
|
+
record.metadata = before.metadata
|
|
1211
|
+
record.deletedAt = null
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const createWarehouseLocationCommand: CommandHandler<WarehouseLocationCreateInput, { locationId: string }> = {
|
|
1216
|
+
id: 'wms.locations.create',
|
|
1217
|
+
async execute(rawInput, ctx) {
|
|
1218
|
+
const parsed = warehouseLocationCreateSchema.parse(rawInput ?? {})
|
|
1219
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
1220
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
1221
|
+
const em = resolveEm(ctx)
|
|
1222
|
+
const warehouse = await loadWarehouse(em, ctx, parsed.warehouseId)
|
|
1223
|
+
await ensureLocationCodeUnique(em, warehouse.id, warehouse.tenantId, warehouse.organizationId, parsed.code)
|
|
1224
|
+
const parent = await resolveParentLocation(em, ctx, warehouse.id, parsed.parentId ?? null)
|
|
1225
|
+
const location = em.create(WarehouseLocation, {
|
|
1226
|
+
organizationId: parsed.organizationId,
|
|
1227
|
+
tenantId: parsed.tenantId,
|
|
1228
|
+
warehouse,
|
|
1229
|
+
code: parsed.code,
|
|
1230
|
+
type: parsed.type,
|
|
1231
|
+
parent,
|
|
1232
|
+
isActive: parsed.isActive ?? true,
|
|
1233
|
+
capacityUnits: parsed.capacityUnits !== undefined ? toNumericString(parsed.capacityUnits) : null,
|
|
1234
|
+
capacityWeight: parsed.capacityWeight !== undefined ? toNumericString(parsed.capacityWeight) : null,
|
|
1235
|
+
constraints: toJsonValue(parsed.constraints),
|
|
1236
|
+
metadata: toJsonValue(parsed.metadata),
|
|
1237
|
+
})
|
|
1238
|
+
await em.persist(location).flush()
|
|
1239
|
+
void emitWmsEvent('wms.location.created', {
|
|
1240
|
+
id: location.id,
|
|
1241
|
+
locationId: location.id,
|
|
1242
|
+
warehouseId: typeof location.warehouse === 'string' ? location.warehouse : location.warehouse.id,
|
|
1243
|
+
tenantId: location.tenantId,
|
|
1244
|
+
organizationId: location.organizationId,
|
|
1245
|
+
}).catch(() => undefined)
|
|
1246
|
+
return { locationId: location.id }
|
|
1247
|
+
},
|
|
1248
|
+
captureAfter: async (_input, result, ctx) => {
|
|
1249
|
+
const em = resolveEm(ctx)
|
|
1250
|
+
return loadWarehouseLocationSnapshot(em, ctx, result.locationId)
|
|
1251
|
+
},
|
|
1252
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1253
|
+
const base = await buildCrudLog(ctx, input, result?.locationId ?? null, 'wms.audit.location.create', 'Create warehouse location', 'wms.location')
|
|
1254
|
+
const after = snapshots?.after as WarehouseLocationSnapshot | undefined
|
|
1255
|
+
return { ...base, snapshotAfter: after, payload: { undo: { after } satisfies WarehouseLocationUndoPayload } }
|
|
1256
|
+
},
|
|
1257
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1258
|
+
const payload = extractUndoPayload<WarehouseLocationUndoPayload>(logEntry)
|
|
1259
|
+
const after = payload?.after
|
|
1260
|
+
if (!after) return
|
|
1261
|
+
const em = resolveEm(ctx)
|
|
1262
|
+
const record = await findOneWithDecryption(
|
|
1263
|
+
em,
|
|
1264
|
+
WarehouseLocation,
|
|
1265
|
+
{ id: after.id },
|
|
1266
|
+
undefined,
|
|
1267
|
+
resolveScope(ctx, { tenantId: after.tenantId, organizationId: after.organizationId }),
|
|
1268
|
+
)
|
|
1269
|
+
if (!record) return
|
|
1270
|
+
ensureTenantScope(ctx, record.tenantId)
|
|
1271
|
+
ensureOrganizationScope(ctx, record.organizationId)
|
|
1272
|
+
record.deletedAt = new Date()
|
|
1273
|
+
await em.flush()
|
|
1274
|
+
},
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const updateWarehouseLocationCommand: CommandHandler<WarehouseLocationUpdateInput, { locationId: string }> = {
|
|
1278
|
+
id: 'wms.locations.update',
|
|
1279
|
+
prepare: async (input, ctx) => {
|
|
1280
|
+
const id = requireId(input?.id, 'Location')
|
|
1281
|
+
const em = resolveEm(ctx)
|
|
1282
|
+
const before = await loadWarehouseLocationSnapshot(em, ctx, id)
|
|
1283
|
+
return before ? { before } : {}
|
|
1284
|
+
},
|
|
1285
|
+
async execute(rawInput, ctx) {
|
|
1286
|
+
const parsed = warehouseLocationUpdateSchema.parse(rawInput ?? {})
|
|
1287
|
+
const em = resolveEm(ctx)
|
|
1288
|
+
const location = await loadLocation(em, ctx, parsed.id)
|
|
1289
|
+
if (parsed.warehouseId !== undefined) {
|
|
1290
|
+
const warehouse = await loadWarehouse(em, ctx, parsed.warehouseId)
|
|
1291
|
+
location.warehouse = warehouse
|
|
1292
|
+
}
|
|
1293
|
+
const warehouseId = typeof location.warehouse === 'string' ? location.warehouse : location.warehouse.id
|
|
1294
|
+
if (parsed.parentId !== undefined) {
|
|
1295
|
+
location.parent = await resolveParentLocation(em, ctx, warehouseId, parsed.parentId)
|
|
1296
|
+
}
|
|
1297
|
+
if (parsed.code !== undefined && parsed.code !== location.code) {
|
|
1298
|
+
await ensureLocationCodeUnique(em, warehouseId, location.tenantId, location.organizationId, parsed.code, location.id)
|
|
1299
|
+
location.code = parsed.code
|
|
1300
|
+
}
|
|
1301
|
+
if (parsed.type !== undefined) location.type = parsed.type
|
|
1302
|
+
if (parsed.isActive !== undefined) location.isActive = parsed.isActive
|
|
1303
|
+
if (parsed.capacityUnits !== undefined) location.capacityUnits = toNumericString(parsed.capacityUnits)
|
|
1304
|
+
if (parsed.capacityWeight !== undefined) location.capacityWeight = toNumericString(parsed.capacityWeight)
|
|
1305
|
+
if (parsed.constraints !== undefined) location.constraints = toJsonValue(parsed.constraints)
|
|
1306
|
+
if (parsed.metadata !== undefined) location.metadata = toJsonValue(parsed.metadata)
|
|
1307
|
+
await em.flush()
|
|
1308
|
+
void emitWmsEvent('wms.location.updated', {
|
|
1309
|
+
id: location.id,
|
|
1310
|
+
locationId: location.id,
|
|
1311
|
+
warehouseId: typeof location.warehouse === 'string' ? location.warehouse : location.warehouse.id,
|
|
1312
|
+
tenantId: location.tenantId,
|
|
1313
|
+
organizationId: location.organizationId,
|
|
1314
|
+
}).catch(() => undefined)
|
|
1315
|
+
return { locationId: location.id }
|
|
1316
|
+
},
|
|
1317
|
+
captureAfter: async (_input, result, ctx) => {
|
|
1318
|
+
const em = resolveEm(ctx)
|
|
1319
|
+
return loadWarehouseLocationSnapshot(em, ctx, result.locationId)
|
|
1320
|
+
},
|
|
1321
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1322
|
+
const base = await buildCrudLog(ctx, input, result?.locationId ?? null, 'wms.audit.location.update', 'Update warehouse location', 'wms.location')
|
|
1323
|
+
const before = snapshots?.before as WarehouseLocationSnapshot | undefined
|
|
1324
|
+
const after = snapshots?.after as WarehouseLocationSnapshot | undefined
|
|
1325
|
+
return { ...base, snapshotBefore: before, snapshotAfter: after, payload: { undo: { before, after } satisfies WarehouseLocationUndoPayload } }
|
|
1326
|
+
},
|
|
1327
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1328
|
+
const payload = extractUndoPayload<WarehouseLocationUndoPayload>(logEntry)
|
|
1329
|
+
const before = payload?.before
|
|
1330
|
+
if (!before) return
|
|
1331
|
+
const em = resolveEm(ctx)
|
|
1332
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
1333
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
1334
|
+
await restoreLocationFromSnapshot(em, before)
|
|
1335
|
+
await em.flush()
|
|
1336
|
+
},
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const deleteWarehouseLocationCommand: CommandHandler<{ id?: string }, { locationId: string }> = {
|
|
1340
|
+
id: 'wms.locations.delete',
|
|
1341
|
+
prepare: async (input, ctx) => {
|
|
1342
|
+
const id = requireId(input?.id, 'Location')
|
|
1343
|
+
const em = resolveEm(ctx)
|
|
1344
|
+
const before = await loadWarehouseLocationSnapshot(em, ctx, id)
|
|
1345
|
+
return before ? { before } : {}
|
|
1346
|
+
},
|
|
1347
|
+
async execute(input, ctx) {
|
|
1348
|
+
const locationId = requireId(input?.id, 'Location')
|
|
1349
|
+
const em = resolveEm(ctx)
|
|
1350
|
+
const location = await loadLocation(em, ctx, locationId)
|
|
1351
|
+
location.deletedAt = new Date()
|
|
1352
|
+
await em.flush()
|
|
1353
|
+
return { locationId: location.id }
|
|
1354
|
+
},
|
|
1355
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1356
|
+
const base = await buildCrudLog(ctx, input, result?.locationId ?? null, 'wms.audit.location.delete', 'Delete warehouse location', 'wms.location')
|
|
1357
|
+
const before = snapshots?.before as WarehouseLocationSnapshot | undefined
|
|
1358
|
+
return { ...base, snapshotBefore: before, payload: { undo: { before } satisfies WarehouseLocationUndoPayload } }
|
|
1359
|
+
},
|
|
1360
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1361
|
+
const payload = extractUndoPayload<WarehouseLocationUndoPayload>(logEntry)
|
|
1362
|
+
const before = payload?.before
|
|
1363
|
+
if (!before) return
|
|
1364
|
+
const em = resolveEm(ctx)
|
|
1365
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
1366
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
1367
|
+
await restoreLocationFromSnapshot(em, before)
|
|
1368
|
+
await em.flush()
|
|
1369
|
+
},
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function restoreInventoryProfileFromSnapshot(em: EntityManager, before: ProductInventoryProfileSnapshot, record: ProductInventoryProfile): void {
|
|
1373
|
+
record.catalogProductId = before.catalogProductId
|
|
1374
|
+
record.catalogVariantId = before.catalogVariantId
|
|
1375
|
+
record.defaultUom = before.defaultUom
|
|
1376
|
+
record.trackLot = before.trackLot
|
|
1377
|
+
record.trackSerial = before.trackSerial
|
|
1378
|
+
record.trackExpiration = before.trackExpiration
|
|
1379
|
+
record.defaultStrategy = before.defaultStrategy
|
|
1380
|
+
record.reorderPoint = before.reorderPoint
|
|
1381
|
+
record.safetyStock = before.safetyStock
|
|
1382
|
+
record.metadata = before.metadata
|
|
1383
|
+
record.deletedAt = null
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfileCreateInput, { profileId: string }> = {
|
|
1387
|
+
id: 'wms.inventoryProfiles.create',
|
|
1388
|
+
async execute(rawInput, ctx) {
|
|
1389
|
+
const parsed = productInventoryProfileCreateSchema.parse(rawInput ?? {})
|
|
1390
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
1391
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
1392
|
+
const em = resolveEm(ctx)
|
|
1393
|
+
await ensureProfileUniqueness(em, parsed)
|
|
1394
|
+
const profile = em.create(ProductInventoryProfile, {
|
|
1395
|
+
organizationId: parsed.organizationId,
|
|
1396
|
+
tenantId: parsed.tenantId,
|
|
1397
|
+
catalogProductId: parsed.catalogProductId,
|
|
1398
|
+
catalogVariantId: parsed.catalogVariantId ?? null,
|
|
1399
|
+
defaultUom: parsed.defaultUom,
|
|
1400
|
+
trackLot: parsed.trackLot ?? false,
|
|
1401
|
+
trackSerial: parsed.trackSerial ?? false,
|
|
1402
|
+
trackExpiration: parsed.trackExpiration ?? false,
|
|
1403
|
+
defaultStrategy: parsed.defaultStrategy,
|
|
1404
|
+
reorderPoint: toNumericString(parsed.reorderPoint),
|
|
1405
|
+
safetyStock: toNumericString(parsed.safetyStock),
|
|
1406
|
+
metadata: toJsonValue(parsed.metadata),
|
|
1407
|
+
})
|
|
1408
|
+
await em.persist(profile).flush()
|
|
1409
|
+
void emitWmsEvent('wms.inventory_profile.created', {
|
|
1410
|
+
id: profile.id,
|
|
1411
|
+
profileId: profile.id,
|
|
1412
|
+
catalogProductId: profile.catalogProductId,
|
|
1413
|
+
catalogVariantId: profile.catalogVariantId ?? null,
|
|
1414
|
+
tenantId: profile.tenantId,
|
|
1415
|
+
organizationId: profile.organizationId,
|
|
1416
|
+
}).catch(() => undefined)
|
|
1417
|
+
return { profileId: profile.id }
|
|
1418
|
+
},
|
|
1419
|
+
captureAfter: async (_input, result, ctx) => {
|
|
1420
|
+
const em = resolveEm(ctx)
|
|
1421
|
+
return loadInventoryProfileSnapshot(em, ctx, result.profileId)
|
|
1422
|
+
},
|
|
1423
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1424
|
+
const base = await buildCrudLog(ctx, input, result?.profileId ?? null, 'wms.audit.inventoryProfile.create', 'Create inventory profile', 'wms.inventoryProfile')
|
|
1425
|
+
const after = snapshots?.after as ProductInventoryProfileSnapshot | undefined
|
|
1426
|
+
return { ...base, snapshotAfter: after, payload: { undo: { after } satisfies ProductInventoryProfileUndoPayload } }
|
|
1427
|
+
},
|
|
1428
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1429
|
+
const payload = extractUndoPayload<ProductInventoryProfileUndoPayload>(logEntry)
|
|
1430
|
+
const after = payload?.after
|
|
1431
|
+
if (!after) return
|
|
1432
|
+
const em = resolveEm(ctx)
|
|
1433
|
+
const record = await findOneWithDecryption(
|
|
1434
|
+
em,
|
|
1435
|
+
ProductInventoryProfile,
|
|
1436
|
+
{ id: after.id },
|
|
1437
|
+
undefined,
|
|
1438
|
+
resolveScope(ctx, { tenantId: after.tenantId, organizationId: after.organizationId }),
|
|
1439
|
+
)
|
|
1440
|
+
if (!record) return
|
|
1441
|
+
ensureTenantScope(ctx, record.tenantId)
|
|
1442
|
+
ensureOrganizationScope(ctx, record.organizationId)
|
|
1443
|
+
record.deletedAt = new Date()
|
|
1444
|
+
await em.flush()
|
|
1445
|
+
},
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const updateProductInventoryProfileCommand: CommandHandler<ProductInventoryProfileUpdateInput, { profileId: string }> = {
|
|
1449
|
+
id: 'wms.inventoryProfiles.update',
|
|
1450
|
+
prepare: async (input, ctx) => {
|
|
1451
|
+
const id = requireId(input?.id, 'Inventory profile')
|
|
1452
|
+
const em = resolveEm(ctx)
|
|
1453
|
+
const before = await loadInventoryProfileSnapshot(em, ctx, id)
|
|
1454
|
+
return before ? { before } : {}
|
|
1455
|
+
},
|
|
1456
|
+
async execute(rawInput, ctx) {
|
|
1457
|
+
const parsed = productInventoryProfileUpdateSchema.parse(rawInput ?? {})
|
|
1458
|
+
const em = resolveEm(ctx)
|
|
1459
|
+
const profile = await loadProfile(em, ctx, parsed.id)
|
|
1460
|
+
const next = {
|
|
1461
|
+
tenantId: profile.tenantId,
|
|
1462
|
+
organizationId: profile.organizationId,
|
|
1463
|
+
catalogProductId: parsed.catalogProductId ?? profile.catalogProductId,
|
|
1464
|
+
catalogVariantId: parsed.catalogVariantId !== undefined ? parsed.catalogVariantId ?? null : profile.catalogVariantId ?? null,
|
|
1465
|
+
}
|
|
1466
|
+
if (
|
|
1467
|
+
parsed.catalogProductId !== undefined ||
|
|
1468
|
+
parsed.catalogVariantId !== undefined
|
|
1469
|
+
) {
|
|
1470
|
+
await ensureProfileUniqueness(em, next, profile.id)
|
|
1471
|
+
}
|
|
1472
|
+
if (parsed.catalogProductId !== undefined) profile.catalogProductId = parsed.catalogProductId
|
|
1473
|
+
if (parsed.catalogVariantId !== undefined) profile.catalogVariantId = parsed.catalogVariantId ?? null
|
|
1474
|
+
if (parsed.defaultUom !== undefined) profile.defaultUom = parsed.defaultUom
|
|
1475
|
+
if (parsed.trackLot !== undefined) profile.trackLot = parsed.trackLot
|
|
1476
|
+
if (parsed.trackSerial !== undefined) profile.trackSerial = parsed.trackSerial
|
|
1477
|
+
if (parsed.trackExpiration !== undefined) profile.trackExpiration = parsed.trackExpiration
|
|
1478
|
+
if (parsed.defaultStrategy !== undefined) profile.defaultStrategy = parsed.defaultStrategy
|
|
1479
|
+
if (parsed.reorderPoint !== undefined) profile.reorderPoint = toNumericString(parsed.reorderPoint)
|
|
1480
|
+
if (parsed.safetyStock !== undefined) profile.safetyStock = toNumericString(parsed.safetyStock)
|
|
1481
|
+
if (parsed.metadata !== undefined) profile.metadata = toJsonValue(parsed.metadata)
|
|
1482
|
+
await em.flush()
|
|
1483
|
+
void emitWmsEvent('wms.inventory_profile.updated', {
|
|
1484
|
+
id: profile.id,
|
|
1485
|
+
profileId: profile.id,
|
|
1486
|
+
catalogProductId: profile.catalogProductId,
|
|
1487
|
+
catalogVariantId: profile.catalogVariantId ?? null,
|
|
1488
|
+
tenantId: profile.tenantId,
|
|
1489
|
+
organizationId: profile.organizationId,
|
|
1490
|
+
}).catch(() => undefined)
|
|
1491
|
+
return { profileId: profile.id }
|
|
1492
|
+
},
|
|
1493
|
+
captureAfter: async (_input, result, ctx) => {
|
|
1494
|
+
const em = resolveEm(ctx)
|
|
1495
|
+
return loadInventoryProfileSnapshot(em, ctx, result.profileId)
|
|
1496
|
+
},
|
|
1497
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1498
|
+
const base = await buildCrudLog(ctx, input, result?.profileId ?? null, 'wms.audit.inventoryProfile.update', 'Update inventory profile', 'wms.inventoryProfile')
|
|
1499
|
+
const before = snapshots?.before as ProductInventoryProfileSnapshot | undefined
|
|
1500
|
+
const after = snapshots?.after as ProductInventoryProfileSnapshot | undefined
|
|
1501
|
+
return { ...base, snapshotBefore: before, snapshotAfter: after, payload: { undo: { before, after } satisfies ProductInventoryProfileUndoPayload } }
|
|
1502
|
+
},
|
|
1503
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1504
|
+
const payload = extractUndoPayload<ProductInventoryProfileUndoPayload>(logEntry)
|
|
1505
|
+
const before = payload?.before
|
|
1506
|
+
if (!before) return
|
|
1507
|
+
const em = resolveEm(ctx)
|
|
1508
|
+
let record = await findOneWithDecryption(
|
|
1509
|
+
em,
|
|
1510
|
+
ProductInventoryProfile,
|
|
1511
|
+
{ id: before.id },
|
|
1512
|
+
undefined,
|
|
1513
|
+
resolveScope(ctx, { tenantId: before.tenantId, organizationId: before.organizationId }),
|
|
1514
|
+
)
|
|
1515
|
+
if (!record) {
|
|
1516
|
+
record = em.create(ProductInventoryProfile, {
|
|
1517
|
+
id: before.id,
|
|
1518
|
+
organizationId: before.organizationId,
|
|
1519
|
+
tenantId: before.tenantId,
|
|
1520
|
+
catalogProductId: before.catalogProductId,
|
|
1521
|
+
catalogVariantId: before.catalogVariantId,
|
|
1522
|
+
defaultUom: before.defaultUom,
|
|
1523
|
+
trackLot: before.trackLot,
|
|
1524
|
+
trackSerial: before.trackSerial,
|
|
1525
|
+
trackExpiration: before.trackExpiration,
|
|
1526
|
+
defaultStrategy: before.defaultStrategy,
|
|
1527
|
+
reorderPoint: before.reorderPoint,
|
|
1528
|
+
safetyStock: before.safetyStock,
|
|
1529
|
+
metadata: before.metadata,
|
|
1530
|
+
createdAt: new Date(before.createdAt),
|
|
1531
|
+
updatedAt: new Date(before.updatedAt),
|
|
1532
|
+
})
|
|
1533
|
+
em.persist(record)
|
|
1534
|
+
} else {
|
|
1535
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
1536
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
1537
|
+
restoreInventoryProfileFromSnapshot(em, before, record)
|
|
1538
|
+
}
|
|
1539
|
+
await em.flush()
|
|
1540
|
+
},
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const deleteProductInventoryProfileCommand: CommandHandler<{ id?: string }, { profileId: string }> = {
|
|
1544
|
+
id: 'wms.inventoryProfiles.delete',
|
|
1545
|
+
prepare: async (input, ctx) => {
|
|
1546
|
+
const id = requireId(input?.id, 'Inventory profile')
|
|
1547
|
+
const em = resolveEm(ctx)
|
|
1548
|
+
const before = await loadInventoryProfileSnapshot(em, ctx, id)
|
|
1549
|
+
return before ? { before } : {}
|
|
1550
|
+
},
|
|
1551
|
+
async execute(input, ctx) {
|
|
1552
|
+
const profileId = requireId(input?.id, 'Inventory profile')
|
|
1553
|
+
const em = resolveEm(ctx)
|
|
1554
|
+
const profile = await loadProfile(em, ctx, profileId)
|
|
1555
|
+
profile.deletedAt = new Date()
|
|
1556
|
+
await em.flush()
|
|
1557
|
+
return { profileId: profile.id }
|
|
1558
|
+
},
|
|
1559
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1560
|
+
const base = await buildCrudLog(ctx, input, result?.profileId ?? null, 'wms.audit.inventoryProfile.delete', 'Delete inventory profile', 'wms.inventoryProfile')
|
|
1561
|
+
const before = snapshots?.before as ProductInventoryProfileSnapshot | undefined
|
|
1562
|
+
return { ...base, snapshotBefore: before, payload: { undo: { before } satisfies ProductInventoryProfileUndoPayload } }
|
|
1563
|
+
},
|
|
1564
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1565
|
+
const payload = extractUndoPayload<ProductInventoryProfileUndoPayload>(logEntry)
|
|
1566
|
+
const before = payload?.before
|
|
1567
|
+
if (!before) return
|
|
1568
|
+
const em = resolveEm(ctx)
|
|
1569
|
+
let record = await findOneWithDecryption(
|
|
1570
|
+
em,
|
|
1571
|
+
ProductInventoryProfile,
|
|
1572
|
+
{ id: before.id },
|
|
1573
|
+
undefined,
|
|
1574
|
+
resolveScope(ctx, { tenantId: before.tenantId, organizationId: before.organizationId }),
|
|
1575
|
+
)
|
|
1576
|
+
if (!record) {
|
|
1577
|
+
record = em.create(ProductInventoryProfile, {
|
|
1578
|
+
id: before.id,
|
|
1579
|
+
organizationId: before.organizationId,
|
|
1580
|
+
tenantId: before.tenantId,
|
|
1581
|
+
catalogProductId: before.catalogProductId,
|
|
1582
|
+
catalogVariantId: before.catalogVariantId,
|
|
1583
|
+
defaultUom: before.defaultUom,
|
|
1584
|
+
trackLot: before.trackLot,
|
|
1585
|
+
trackSerial: before.trackSerial,
|
|
1586
|
+
trackExpiration: before.trackExpiration,
|
|
1587
|
+
defaultStrategy: before.defaultStrategy,
|
|
1588
|
+
reorderPoint: before.reorderPoint,
|
|
1589
|
+
safetyStock: before.safetyStock,
|
|
1590
|
+
metadata: before.metadata,
|
|
1591
|
+
createdAt: new Date(before.createdAt),
|
|
1592
|
+
updatedAt: new Date(before.updatedAt),
|
|
1593
|
+
})
|
|
1594
|
+
em.persist(record)
|
|
1595
|
+
} else {
|
|
1596
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
1597
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
1598
|
+
restoreInventoryProfileFromSnapshot(em, before, record)
|
|
1599
|
+
}
|
|
1600
|
+
await em.flush()
|
|
1601
|
+
},
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function restoreInventoryLotFromSnapshot(_em: EntityManager, before: InventoryLotSnapshot, record: InventoryLot): void {
|
|
1605
|
+
record.catalogVariantId = before.catalogVariantId
|
|
1606
|
+
record.sku = before.sku
|
|
1607
|
+
record.lotNumber = before.lotNumber
|
|
1608
|
+
record.batchNumber = before.batchNumber
|
|
1609
|
+
record.manufacturedAt = dateOrNull(before.manufacturedAt)
|
|
1610
|
+
record.bestBeforeAt = dateOrNull(before.bestBeforeAt)
|
|
1611
|
+
record.expiresAt = dateOrNull(before.expiresAt)
|
|
1612
|
+
record.status = before.status
|
|
1613
|
+
record.metadata = before.metadata
|
|
1614
|
+
record.deletedAt = null
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const createInventoryLotCommand: CommandHandler<InventoryLotCreateInput, { lotId: string }> = {
|
|
1618
|
+
id: 'wms.lots.create',
|
|
1619
|
+
async execute(rawInput, ctx) {
|
|
1620
|
+
const parsed = inventoryLotCreateSchema.parse(rawInput ?? {})
|
|
1621
|
+
ensureTenantScope(ctx, parsed.tenantId)
|
|
1622
|
+
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
1623
|
+
const em = resolveEm(ctx)
|
|
1624
|
+
await ensureLotUniqueness(em, parsed)
|
|
1625
|
+
const lot = em.create(InventoryLot, {
|
|
1626
|
+
organizationId: parsed.organizationId,
|
|
1627
|
+
tenantId: parsed.tenantId,
|
|
1628
|
+
catalogVariantId: parsed.catalogVariantId,
|
|
1629
|
+
sku: parsed.sku,
|
|
1630
|
+
lotNumber: parsed.lotNumber,
|
|
1631
|
+
batchNumber: normalizeOptionalString(parsed.batchNumber),
|
|
1632
|
+
manufacturedAt: parsed.manufacturedAt ?? null,
|
|
1633
|
+
bestBeforeAt: parsed.bestBeforeAt ?? null,
|
|
1634
|
+
expiresAt: parsed.expiresAt ?? null,
|
|
1635
|
+
status: parsed.status ?? 'available',
|
|
1636
|
+
metadata: toJsonValue(parsed.metadata),
|
|
1637
|
+
})
|
|
1638
|
+
await em.persist(lot).flush()
|
|
1639
|
+
return { lotId: lot.id }
|
|
1640
|
+
},
|
|
1641
|
+
captureAfter: async (_input, result, ctx) => {
|
|
1642
|
+
const em = resolveEm(ctx)
|
|
1643
|
+
return loadInventoryLotSnapshot(em, ctx, result.lotId)
|
|
1644
|
+
},
|
|
1645
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1646
|
+
const base = await buildCrudLog(ctx, input, result?.lotId ?? null, 'wms.audit.lot.create', 'Create inventory lot', 'wms.inventoryLot')
|
|
1647
|
+
const after = snapshots?.after as InventoryLotSnapshot | undefined
|
|
1648
|
+
return { ...base, snapshotAfter: after, payload: { undo: { after } satisfies InventoryLotUndoPayload } }
|
|
1649
|
+
},
|
|
1650
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1651
|
+
const payload = extractUndoPayload<InventoryLotUndoPayload>(logEntry)
|
|
1652
|
+
const after = payload?.after
|
|
1653
|
+
if (!after) return
|
|
1654
|
+
const em = resolveEm(ctx)
|
|
1655
|
+
const record = await findOneWithDecryption(
|
|
1656
|
+
em,
|
|
1657
|
+
InventoryLot,
|
|
1658
|
+
{ id: after.id },
|
|
1659
|
+
undefined,
|
|
1660
|
+
resolveScope(ctx, { tenantId: after.tenantId, organizationId: after.organizationId }),
|
|
1661
|
+
)
|
|
1662
|
+
if (!record) return
|
|
1663
|
+
ensureTenantScope(ctx, record.tenantId)
|
|
1664
|
+
ensureOrganizationScope(ctx, record.organizationId)
|
|
1665
|
+
record.deletedAt = new Date()
|
|
1666
|
+
await em.flush()
|
|
1667
|
+
},
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const updateInventoryLotCommand: CommandHandler<InventoryLotUpdateInput, { lotId: string }> = {
|
|
1671
|
+
id: 'wms.lots.update',
|
|
1672
|
+
prepare: async (input, ctx) => {
|
|
1673
|
+
const id = requireId(input?.id, 'Inventory lot')
|
|
1674
|
+
const em = resolveEm(ctx)
|
|
1675
|
+
const before = await loadInventoryLotSnapshot(em, ctx, id)
|
|
1676
|
+
return before ? { before } : {}
|
|
1677
|
+
},
|
|
1678
|
+
async execute(rawInput, ctx) {
|
|
1679
|
+
const parsed = inventoryLotUpdateSchema.parse(rawInput ?? {})
|
|
1680
|
+
const em = resolveEm(ctx)
|
|
1681
|
+
const lot = await loadLot(em, ctx, parsed.id)
|
|
1682
|
+
const nextVariantId = parsed.catalogVariantId ?? lot.catalogVariantId
|
|
1683
|
+
const nextLotNumber = parsed.lotNumber ?? lot.lotNumber
|
|
1684
|
+
if (
|
|
1685
|
+
parsed.catalogVariantId !== undefined ||
|
|
1686
|
+
parsed.lotNumber !== undefined
|
|
1687
|
+
) {
|
|
1688
|
+
await ensureLotUniqueness(
|
|
1689
|
+
em,
|
|
1690
|
+
{
|
|
1691
|
+
tenantId: lot.tenantId,
|
|
1692
|
+
organizationId: lot.organizationId,
|
|
1693
|
+
catalogVariantId: nextVariantId,
|
|
1694
|
+
lotNumber: nextLotNumber,
|
|
1695
|
+
},
|
|
1696
|
+
lot.id,
|
|
1697
|
+
)
|
|
1698
|
+
}
|
|
1699
|
+
if (parsed.catalogVariantId !== undefined) lot.catalogVariantId = parsed.catalogVariantId
|
|
1700
|
+
if (parsed.sku !== undefined) lot.sku = parsed.sku
|
|
1701
|
+
if (parsed.lotNumber !== undefined) lot.lotNumber = parsed.lotNumber
|
|
1702
|
+
if (parsed.batchNumber !== undefined) lot.batchNumber = normalizeOptionalString(parsed.batchNumber)
|
|
1703
|
+
if (parsed.manufacturedAt !== undefined) lot.manufacturedAt = parsed.manufacturedAt ?? null
|
|
1704
|
+
if (parsed.bestBeforeAt !== undefined) lot.bestBeforeAt = parsed.bestBeforeAt ?? null
|
|
1705
|
+
if (parsed.expiresAt !== undefined) lot.expiresAt = parsed.expiresAt ?? null
|
|
1706
|
+
if (parsed.status !== undefined) lot.status = parsed.status
|
|
1707
|
+
if (parsed.metadata !== undefined) lot.metadata = toJsonValue(parsed.metadata)
|
|
1708
|
+
if (parsed.notes !== undefined) {
|
|
1709
|
+
const existing =
|
|
1710
|
+
lot.metadata && typeof lot.metadata === 'object' && !Array.isArray(lot.metadata)
|
|
1711
|
+
? { ...(lot.metadata as Record<string, unknown>) }
|
|
1712
|
+
: {}
|
|
1713
|
+
const trimmedNotes = parsed.notes.trim()
|
|
1714
|
+
if (trimmedNotes) existing.notes = trimmedNotes
|
|
1715
|
+
else delete existing.notes
|
|
1716
|
+
lot.metadata = toJsonValue(existing)
|
|
1717
|
+
}
|
|
1718
|
+
await em.flush()
|
|
1719
|
+
return { lotId: lot.id }
|
|
1720
|
+
},
|
|
1721
|
+
captureAfter: async (_input, result, ctx) => {
|
|
1722
|
+
const em = resolveEm(ctx)
|
|
1723
|
+
return loadInventoryLotSnapshot(em, ctx, result.lotId)
|
|
1724
|
+
},
|
|
1725
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1726
|
+
const base = await buildCrudLog(ctx, input, result?.lotId ?? null, 'wms.audit.lot.update', 'Update inventory lot', 'wms.inventoryLot')
|
|
1727
|
+
const before = snapshots?.before as InventoryLotSnapshot | undefined
|
|
1728
|
+
const after = snapshots?.after as InventoryLotSnapshot | undefined
|
|
1729
|
+
return { ...base, snapshotBefore: before, snapshotAfter: after, payload: { undo: { before, after } satisfies InventoryLotUndoPayload } }
|
|
1730
|
+
},
|
|
1731
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1732
|
+
const payload = extractUndoPayload<InventoryLotUndoPayload>(logEntry)
|
|
1733
|
+
const before = payload?.before
|
|
1734
|
+
if (!before) return
|
|
1735
|
+
const em = resolveEm(ctx)
|
|
1736
|
+
let record = await findOneWithDecryption(
|
|
1737
|
+
em,
|
|
1738
|
+
InventoryLot,
|
|
1739
|
+
{ id: before.id },
|
|
1740
|
+
undefined,
|
|
1741
|
+
resolveScope(ctx, { tenantId: before.tenantId, organizationId: before.organizationId }),
|
|
1742
|
+
)
|
|
1743
|
+
if (!record) {
|
|
1744
|
+
record = em.create(InventoryLot, {
|
|
1745
|
+
id: before.id,
|
|
1746
|
+
organizationId: before.organizationId,
|
|
1747
|
+
tenantId: before.tenantId,
|
|
1748
|
+
catalogVariantId: before.catalogVariantId,
|
|
1749
|
+
sku: before.sku,
|
|
1750
|
+
lotNumber: before.lotNumber,
|
|
1751
|
+
batchNumber: before.batchNumber,
|
|
1752
|
+
manufacturedAt: dateOrNull(before.manufacturedAt),
|
|
1753
|
+
bestBeforeAt: dateOrNull(before.bestBeforeAt),
|
|
1754
|
+
expiresAt: dateOrNull(before.expiresAt),
|
|
1755
|
+
status: before.status,
|
|
1756
|
+
metadata: before.metadata,
|
|
1757
|
+
createdAt: new Date(before.createdAt),
|
|
1758
|
+
updatedAt: new Date(before.updatedAt),
|
|
1759
|
+
})
|
|
1760
|
+
em.persist(record)
|
|
1761
|
+
} else {
|
|
1762
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
1763
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
1764
|
+
restoreInventoryLotFromSnapshot(em, before, record)
|
|
1765
|
+
}
|
|
1766
|
+
await em.flush()
|
|
1767
|
+
},
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const deleteInventoryLotCommand: CommandHandler<{ id?: string }, { lotId: string }> = {
|
|
1771
|
+
id: 'wms.lots.delete',
|
|
1772
|
+
prepare: async (input, ctx) => {
|
|
1773
|
+
const id = requireId(input?.id, 'Inventory lot')
|
|
1774
|
+
const em = resolveEm(ctx)
|
|
1775
|
+
const before = await loadInventoryLotSnapshot(em, ctx, id)
|
|
1776
|
+
return before ? { before } : {}
|
|
1777
|
+
},
|
|
1778
|
+
async execute(input, ctx) {
|
|
1779
|
+
const lotId = requireId(input?.id, 'Inventory lot')
|
|
1780
|
+
const em = resolveEm(ctx)
|
|
1781
|
+
const lot = await loadLot(em, ctx, lotId)
|
|
1782
|
+
lot.deletedAt = new Date()
|
|
1783
|
+
await em.flush()
|
|
1784
|
+
return { lotId: lot.id }
|
|
1785
|
+
},
|
|
1786
|
+
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
1787
|
+
const base = await buildCrudLog(ctx, input, result?.lotId ?? null, 'wms.audit.lot.delete', 'Delete inventory lot', 'wms.inventoryLot')
|
|
1788
|
+
const before = snapshots?.before as InventoryLotSnapshot | undefined
|
|
1789
|
+
return { ...base, snapshotBefore: before, payload: { undo: { before } satisfies InventoryLotUndoPayload } }
|
|
1790
|
+
},
|
|
1791
|
+
undo: async ({ logEntry, ctx }) => {
|
|
1792
|
+
const payload = extractUndoPayload<InventoryLotUndoPayload>(logEntry)
|
|
1793
|
+
const before = payload?.before
|
|
1794
|
+
if (!before) return
|
|
1795
|
+
const em = resolveEm(ctx)
|
|
1796
|
+
let record = await findOneWithDecryption(
|
|
1797
|
+
em,
|
|
1798
|
+
InventoryLot,
|
|
1799
|
+
{ id: before.id },
|
|
1800
|
+
undefined,
|
|
1801
|
+
resolveScope(ctx, { tenantId: before.tenantId, organizationId: before.organizationId }),
|
|
1802
|
+
)
|
|
1803
|
+
if (!record) {
|
|
1804
|
+
record = em.create(InventoryLot, {
|
|
1805
|
+
id: before.id,
|
|
1806
|
+
organizationId: before.organizationId,
|
|
1807
|
+
tenantId: before.tenantId,
|
|
1808
|
+
catalogVariantId: before.catalogVariantId,
|
|
1809
|
+
sku: before.sku,
|
|
1810
|
+
lotNumber: before.lotNumber,
|
|
1811
|
+
batchNumber: before.batchNumber,
|
|
1812
|
+
manufacturedAt: dateOrNull(before.manufacturedAt),
|
|
1813
|
+
bestBeforeAt: dateOrNull(before.bestBeforeAt),
|
|
1814
|
+
expiresAt: dateOrNull(before.expiresAt),
|
|
1815
|
+
status: before.status,
|
|
1816
|
+
metadata: before.metadata,
|
|
1817
|
+
createdAt: new Date(before.createdAt),
|
|
1818
|
+
updatedAt: new Date(before.updatedAt),
|
|
1819
|
+
})
|
|
1820
|
+
em.persist(record)
|
|
1821
|
+
} else {
|
|
1822
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
1823
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
1824
|
+
restoreInventoryLotFromSnapshot(em, before, record)
|
|
1825
|
+
}
|
|
1826
|
+
await em.flush()
|
|
1827
|
+
},
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
registerCommand(createWarehouseCommand)
|
|
1831
|
+
registerCommand(updateWarehouseCommand)
|
|
1832
|
+
registerCommand(deleteWarehouseCommand)
|
|
1833
|
+
registerCommand(createWarehouseZoneCommand)
|
|
1834
|
+
registerCommand(updateWarehouseZoneCommand)
|
|
1835
|
+
registerCommand(deleteWarehouseZoneCommand)
|
|
1836
|
+
registerCommand(createWarehouseLocationCommand)
|
|
1837
|
+
registerCommand(updateWarehouseLocationCommand)
|
|
1838
|
+
registerCommand(deleteWarehouseLocationCommand)
|
|
1839
|
+
registerCommand(createProductInventoryProfileCommand)
|
|
1840
|
+
registerCommand(updateProductInventoryProfileCommand)
|
|
1841
|
+
registerCommand(deleteProductInventoryProfileCommand)
|
|
1842
|
+
registerCommand(createInventoryLotCommand)
|
|
1843
|
+
registerCommand(updateInventoryLotCommand)
|
|
1844
|
+
registerCommand(deleteInventoryLotCommand)
|