@open-mercato/core 0.6.7-develop.6661.1.0043ed1d03 → 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.
Files changed (494) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/inventory_balance/index.js +21 -0
  3. package/dist/generated/entities/inventory_balance/index.js.map +7 -0
  4. package/dist/generated/entities/inventory_lot/index.js +19 -0
  5. package/dist/generated/entities/inventory_lot/index.js.map +7 -0
  6. package/dist/generated/entities/inventory_movement/index.js +35 -0
  7. package/dist/generated/entities/inventory_movement/index.js.map +7 -0
  8. package/dist/generated/entities/inventory_reservation/index.js +23 -0
  9. package/dist/generated/entities/inventory_reservation/index.js.map +7 -0
  10. package/dist/generated/entities/product_inventory_profile/index.js +21 -0
  11. package/dist/generated/entities/product_inventory_profile/index.js.map +7 -0
  12. package/dist/generated/entities/sales_order_warehouse_assignment/index.js +11 -0
  13. package/dist/generated/entities/sales_order_warehouse_assignment/index.js.map +7 -0
  14. package/dist/generated/entities/warehouse/index.js +25 -0
  15. package/dist/generated/entities/warehouse/index.js.map +7 -0
  16. package/dist/generated/entities/warehouse_location/index.js +21 -0
  17. package/dist/generated/entities/warehouse_location/index.js.map +7 -0
  18. package/dist/generated/entities/warehouse_zone/index.js +11 -0
  19. package/dist/generated/entities/warehouse_zone/index.js.map +7 -0
  20. package/dist/generated/entities.ids.generated.js +12 -0
  21. package/dist/generated/entities.ids.generated.js.map +2 -2
  22. package/dist/generated/entity-fields-registry.js +98 -0
  23. package/dist/generated/entity-fields-registry.js.map +2 -2
  24. package/dist/modules/auth/backend/roles/page.js +12 -4
  25. package/dist/modules/auth/backend/roles/page.js.map +2 -2
  26. package/dist/modules/auth/lib/backendChrome.js +1 -0
  27. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  28. package/dist/modules/auth/services/rbacService.js +16 -17
  29. package/dist/modules/auth/services/rbacService.js.map +2 -2
  30. package/dist/modules/catalog/api/products/route.js +3 -0
  31. package/dist/modules/catalog/api/products/route.js.map +2 -2
  32. package/dist/modules/catalog/api/variants/route.js +3 -0
  33. package/dist/modules/catalog/api/variants/route.js.map +2 -2
  34. package/dist/modules/customer_accounts/migrations/Migration20260724120000_customer_accounts.js +56 -0
  35. package/dist/modules/customer_accounts/migrations/Migration20260724120000_customer_accounts.js.map +7 -0
  36. package/dist/modules/directory/api/organizations/route.js +1 -1
  37. package/dist/modules/directory/api/organizations/route.js.map +1 -1
  38. package/dist/modules/entities/api/definitions.js +2 -0
  39. package/dist/modules/entities/api/definitions.js.map +2 -2
  40. package/dist/modules/entities/lib/helpers.js +1 -0
  41. package/dist/modules/entities/lib/helpers.js.map +2 -2
  42. package/dist/modules/query_index/lib/engine.js +6 -6
  43. package/dist/modules/query_index/lib/engine.js.map +2 -2
  44. package/dist/modules/sales/api/documents/factory.js +1 -0
  45. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  46. package/dist/modules/sales/api/quotes/accept/route.js +7 -2
  47. package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
  48. package/dist/modules/sales/api/quotes/public/[token]/route.js +2 -1
  49. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  50. package/dist/modules/sales/commands/documents.js +53 -0
  51. package/dist/modules/sales/commands/documents.js.map +2 -2
  52. package/dist/modules/sales/components/documents/ItemsSection.js +51 -2
  53. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  54. package/dist/modules/sales/events.js +2 -0
  55. package/dist/modules/sales/events.js.map +2 -2
  56. package/dist/modules/sales/lib/publicQuoteTenantScope.js +24 -0
  57. package/dist/modules/sales/lib/publicQuoteTenantScope.js.map +7 -0
  58. package/dist/modules/sales/widgets/injection/order-items-context.js +11 -0
  59. package/dist/modules/sales/widgets/injection/order-items-context.js.map +7 -0
  60. package/dist/modules/wms/acl.js +18 -0
  61. package/dist/modules/wms/acl.js.map +7 -0
  62. package/dist/modules/wms/analytics.js +11 -0
  63. package/dist/modules/wms/analytics.js.map +7 -0
  64. package/dist/modules/wms/api/dashboard/operational/route.js +80 -0
  65. package/dist/modules/wms/api/dashboard/operational/route.js.map +7 -0
  66. package/dist/modules/wms/api/interceptors.js +90 -0
  67. package/dist/modules/wms/api/interceptors.js.map +7 -0
  68. package/dist/modules/wms/api/inventory/adjust/route.js +51 -0
  69. package/dist/modules/wms/api/inventory/adjust/route.js.map +7 -0
  70. package/dist/modules/wms/api/inventory/allocate/route.js +51 -0
  71. package/dist/modules/wms/api/inventory/allocate/route.js.map +7 -0
  72. package/dist/modules/wms/api/inventory/balances/route.js +172 -0
  73. package/dist/modules/wms/api/inventory/balances/route.js.map +7 -0
  74. package/dist/modules/wms/api/inventory/cycle-count/route.js +52 -0
  75. package/dist/modules/wms/api/inventory/cycle-count/route.js.map +7 -0
  76. package/dist/modules/wms/api/inventory/helpers.js +113 -0
  77. package/dist/modules/wms/api/inventory/helpers.js.map +7 -0
  78. package/dist/modules/wms/api/inventory/import/apply/route.js +46 -0
  79. package/dist/modules/wms/api/inventory/import/apply/route.js.map +7 -0
  80. package/dist/modules/wms/api/inventory/import/helpers.js +195 -0
  81. package/dist/modules/wms/api/inventory/import/helpers.js.map +7 -0
  82. package/dist/modules/wms/api/inventory/import/template/route.js +32 -0
  83. package/dist/modules/wms/api/inventory/import/template/route.js.map +7 -0
  84. package/dist/modules/wms/api/inventory/import/validate/route.js +54 -0
  85. package/dist/modules/wms/api/inventory/import/validate/route.js.map +7 -0
  86. package/dist/modules/wms/api/inventory/move/route.js +51 -0
  87. package/dist/modules/wms/api/inventory/move/route.js.map +7 -0
  88. package/dist/modules/wms/api/inventory/movements/route.js +164 -0
  89. package/dist/modules/wms/api/inventory/movements/route.js.map +7 -0
  90. package/dist/modules/wms/api/inventory/receive/route.js +50 -0
  91. package/dist/modules/wms/api/inventory/receive/route.js.map +7 -0
  92. package/dist/modules/wms/api/inventory/release/route.js +44 -0
  93. package/dist/modules/wms/api/inventory/release/route.js.map +7 -0
  94. package/dist/modules/wms/api/inventory/reservations/route.js +128 -0
  95. package/dist/modules/wms/api/inventory/reservations/route.js.map +7 -0
  96. package/dist/modules/wms/api/inventory/reserve/route.js +59 -0
  97. package/dist/modules/wms/api/inventory/reserve/route.js.map +7 -0
  98. package/dist/modules/wms/api/inventory-profiles/route.js +178 -0
  99. package/dist/modules/wms/api/inventory-profiles/route.js.map +7 -0
  100. package/dist/modules/wms/api/listEnrichers.js +181 -0
  101. package/dist/modules/wms/api/listEnrichers.js.map +7 -0
  102. package/dist/modules/wms/api/listSearch.js +77 -0
  103. package/dist/modules/wms/api/listSearch.js.map +7 -0
  104. package/dist/modules/wms/api/locations/route.js +172 -0
  105. package/dist/modules/wms/api/locations/route.js.map +7 -0
  106. package/dist/modules/wms/api/lots/route.js +204 -0
  107. package/dist/modules/wms/api/lots/route.js.map +7 -0
  108. package/dist/modules/wms/api/openapi.js +27 -0
  109. package/dist/modules/wms/api/openapi.js.map +7 -0
  110. package/dist/modules/wms/api/sales-orders/[salesOrderId]/re-run-reservation/route.js +54 -0
  111. package/dist/modules/wms/api/sales-orders/[salesOrderId]/re-run-reservation/route.js.map +7 -0
  112. package/dist/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.js +279 -0
  113. package/dist/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.js.map +7 -0
  114. package/dist/modules/wms/api/sales-orders/assign-warehouse/route.js +56 -0
  115. package/dist/modules/wms/api/sales-orders/assign-warehouse/route.js.map +7 -0
  116. package/dist/modules/wms/api/sales-orders/unassign-warehouse/route.js +43 -0
  117. package/dist/modules/wms/api/sales-orders/unassign-warehouse/route.js.map +7 -0
  118. package/dist/modules/wms/api/warehouses/route.js +181 -0
  119. package/dist/modules/wms/api/warehouses/route.js.map +7 -0
  120. package/dist/modules/wms/api/zones/route.js +159 -0
  121. package/dist/modules/wms/api/zones/route.js.map +7 -0
  122. package/dist/modules/wms/backend/config/wms/page.js +9 -0
  123. package/dist/modules/wms/backend/config/wms/page.js.map +7 -0
  124. package/dist/modules/wms/backend/config/wms/page.meta.js +18 -0
  125. package/dist/modules/wms/backend/config/wms/page.meta.js.map +7 -0
  126. package/dist/modules/wms/backend/page.js +9 -0
  127. package/dist/modules/wms/backend/page.js.map +7 -0
  128. package/dist/modules/wms/backend/page.meta.js +16 -0
  129. package/dist/modules/wms/backend/page.meta.js.map +7 -0
  130. package/dist/modules/wms/backend/wms/inventory/page.js +9 -0
  131. package/dist/modules/wms/backend/wms/inventory/page.js.map +7 -0
  132. package/dist/modules/wms/backend/wms/inventory/page.meta.js +18 -0
  133. package/dist/modules/wms/backend/wms/inventory/page.meta.js.map +7 -0
  134. package/dist/modules/wms/backend/wms/location/[id]/page.js +9 -0
  135. package/dist/modules/wms/backend/wms/location/[id]/page.js.map +7 -0
  136. package/dist/modules/wms/backend/wms/location/[id]/page.meta.js +18 -0
  137. package/dist/modules/wms/backend/wms/location/[id]/page.meta.js.map +7 -0
  138. package/dist/modules/wms/backend/wms/locations/page.js +10 -0
  139. package/dist/modules/wms/backend/wms/locations/page.js.map +7 -0
  140. package/dist/modules/wms/backend/wms/locations/page.meta.js +18 -0
  141. package/dist/modules/wms/backend/wms/locations/page.meta.js.map +7 -0
  142. package/dist/modules/wms/backend/wms/lot/[id]/page.js +9 -0
  143. package/dist/modules/wms/backend/wms/lot/[id]/page.js.map +7 -0
  144. package/dist/modules/wms/backend/wms/lot/[id]/page.meta.js +18 -0
  145. package/dist/modules/wms/backend/wms/lot/[id]/page.meta.js.map +7 -0
  146. package/dist/modules/wms/backend/wms/lots/page.js +9 -0
  147. package/dist/modules/wms/backend/wms/lots/page.js.map +7 -0
  148. package/dist/modules/wms/backend/wms/lots/page.meta.js +18 -0
  149. package/dist/modules/wms/backend/wms/lots/page.meta.js.map +7 -0
  150. package/dist/modules/wms/backend/wms/movements/page.js +22 -0
  151. package/dist/modules/wms/backend/wms/movements/page.js.map +7 -0
  152. package/dist/modules/wms/backend/wms/movements/page.meta.js +18 -0
  153. package/dist/modules/wms/backend/wms/movements/page.meta.js.map +7 -0
  154. package/dist/modules/wms/backend/wms/reservations/page.js +23 -0
  155. package/dist/modules/wms/backend/wms/reservations/page.js.map +7 -0
  156. package/dist/modules/wms/backend/wms/reservations/page.meta.js +18 -0
  157. package/dist/modules/wms/backend/wms/reservations/page.meta.js.map +7 -0
  158. package/dist/modules/wms/backend/wms/sku/[id]/page.js +9 -0
  159. package/dist/modules/wms/backend/wms/sku/[id]/page.js.map +7 -0
  160. package/dist/modules/wms/backend/wms/sku/[id]/page.meta.js +18 -0
  161. package/dist/modules/wms/backend/wms/sku/[id]/page.meta.js.map +7 -0
  162. package/dist/modules/wms/backend/wms/warehouses/page.js +10 -0
  163. package/dist/modules/wms/backend/wms/warehouses/page.js.map +7 -0
  164. package/dist/modules/wms/backend/wms/warehouses/page.meta.js +18 -0
  165. package/dist/modules/wms/backend/wms/warehouses/page.meta.js.map +7 -0
  166. package/dist/modules/wms/backend/wms/zones/page.js +10 -0
  167. package/dist/modules/wms/backend/wms/zones/page.js.map +7 -0
  168. package/dist/modules/wms/backend/wms/zones/page.meta.js +18 -0
  169. package/dist/modules/wms/backend/wms/zones/page.meta.js.map +7 -0
  170. package/dist/modules/wms/ce.js +80 -0
  171. package/dist/modules/wms/ce.js.map +7 -0
  172. package/dist/modules/wms/cli.js +83 -0
  173. package/dist/modules/wms/cli.js.map +7 -0
  174. package/dist/modules/wms/commands/configuration.js +1519 -0
  175. package/dist/modules/wms/commands/configuration.js.map +7 -0
  176. package/dist/modules/wms/commands/index.js +4 -0
  177. package/dist/modules/wms/commands/index.js.map +7 -0
  178. package/dist/modules/wms/commands/inventory-actions.js +1539 -0
  179. package/dist/modules/wms/commands/inventory-actions.js.map +7 -0
  180. package/dist/modules/wms/commands/sales-order-assignment.js +277 -0
  181. package/dist/modules/wms/commands/sales-order-assignment.js.map +7 -0
  182. package/dist/modules/wms/commands/shared.js +82 -0
  183. package/dist/modules/wms/commands/shared.js.map +7 -0
  184. package/dist/modules/wms/components/backend/AdjustInventoryDialog.js +755 -0
  185. package/dist/modules/wms/components/backend/AdjustInventoryDialog.js.map +7 -0
  186. package/dist/modules/wms/components/backend/ChangeLotStatusDialog.js +269 -0
  187. package/dist/modules/wms/components/backend/ChangeLotStatusDialog.js.map +7 -0
  188. package/dist/modules/wms/components/backend/CycleCountWizardDialog.js +1783 -0
  189. package/dist/modules/wms/components/backend/CycleCountWizardDialog.js.map +7 -0
  190. package/dist/modules/wms/components/backend/ImportInventoryDialog.js +766 -0
  191. package/dist/modules/wms/components/backend/ImportInventoryDialog.js.map +7 -0
  192. package/dist/modules/wms/components/backend/InventoryOperationsSection.js +119 -0
  193. package/dist/modules/wms/components/backend/InventoryOperationsSection.js.map +7 -0
  194. package/dist/modules/wms/components/backend/LocationEditDialog.js +140 -0
  195. package/dist/modules/wms/components/backend/LocationEditDialog.js.map +7 -0
  196. package/dist/modules/wms/components/backend/MoveInventoryDialog.js +791 -0
  197. package/dist/modules/wms/components/backend/MoveInventoryDialog.js.map +7 -0
  198. package/dist/modules/wms/components/backend/ReceiveInventoryDialog.js +656 -0
  199. package/dist/modules/wms/components/backend/ReceiveInventoryDialog.js.map +7 -0
  200. package/dist/modules/wms/components/backend/ReleaseReservationDialog.js +344 -0
  201. package/dist/modules/wms/components/backend/ReleaseReservationDialog.js.map +7 -0
  202. package/dist/modules/wms/components/backend/ReserveInventoryDialog.js +475 -0
  203. package/dist/modules/wms/components/backend/ReserveInventoryDialog.js.map +7 -0
  204. package/dist/modules/wms/components/backend/WmsConfigurationPage.js +1112 -0
  205. package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +7 -0
  206. package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js +908 -0
  207. package/dist/modules/wms/components/backend/WmsInventoryConsolePage.js.map +7 -0
  208. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +1015 -0
  209. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +7 -0
  210. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +1049 -0
  211. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +7 -0
  212. package/dist/modules/wms/components/backend/WmsLotsListPage.js +297 -0
  213. package/dist/modules/wms/components/backend/WmsLotsListPage.js.map +7 -0
  214. package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js +757 -0
  215. package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js.map +7 -0
  216. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +975 -0
  217. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +7 -0
  218. package/dist/modules/wms/components/backend/inventoryMutationLoaders.js +710 -0
  219. package/dist/modules/wms/components/backend/inventoryMutationLoaders.js.map +7 -0
  220. package/dist/modules/wms/components/backend/useWmsInventoryMutationAccess.js +79 -0
  221. package/dist/modules/wms/components/backend/useWmsInventoryMutationAccess.js.map +7 -0
  222. package/dist/modules/wms/components/backend/useWmsInventoryScopeFromSearchParams.js +24 -0
  223. package/dist/modules/wms/components/backend/useWmsInventoryScopeFromSearchParams.js.map +7 -0
  224. package/dist/modules/wms/components/backend/wmsLookupLoaders.js +55 -0
  225. package/dist/modules/wms/components/backend/wmsLookupLoaders.js.map +7 -0
  226. package/dist/modules/wms/data/enrichers.js +501 -0
  227. package/dist/modules/wms/data/enrichers.js.map +7 -0
  228. package/dist/modules/wms/data/entities.js +467 -0
  229. package/dist/modules/wms/data/entities.js.map +7 -0
  230. package/dist/modules/wms/data/validators.js +399 -0
  231. package/dist/modules/wms/data/validators.js.map +7 -0
  232. package/dist/modules/wms/di.js +27 -0
  233. package/dist/modules/wms/di.js.map +7 -0
  234. package/dist/modules/wms/events.js +42 -0
  235. package/dist/modules/wms/events.js.map +7 -0
  236. package/dist/modules/wms/index.js +17 -0
  237. package/dist/modules/wms/index.js.map +7 -0
  238. package/dist/modules/wms/lib/catalogInventoryProfileIntent.js +54 -0
  239. package/dist/modules/wms/lib/catalogInventoryProfileIntent.js.map +7 -0
  240. package/dist/modules/wms/lib/catalogInventoryProfileSync.js +144 -0
  241. package/dist/modules/wms/lib/catalogInventoryProfileSync.js.map +7 -0
  242. package/dist/modules/wms/lib/expiry.js +29 -0
  243. package/dist/modules/wms/lib/expiry.js.map +7 -0
  244. package/dist/modules/wms/lib/flashMutationError.js +51 -0
  245. package/dist/modules/wms/lib/flashMutationError.js.map +7 -0
  246. package/dist/modules/wms/lib/inventoryDisplayUi.js +188 -0
  247. package/dist/modules/wms/lib/inventoryDisplayUi.js.map +7 -0
  248. package/dist/modules/wms/lib/inventoryIdempotency.js +37 -0
  249. package/dist/modules/wms/lib/inventoryIdempotency.js.map +7 -0
  250. package/dist/modules/wms/lib/inventoryImportCsv.js +173 -0
  251. package/dist/modules/wms/lib/inventoryImportCsv.js.map +7 -0
  252. package/dist/modules/wms/lib/inventoryImportService.js +468 -0
  253. package/dist/modules/wms/lib/inventoryImportService.js.map +7 -0
  254. package/dist/modules/wms/lib/inventoryMutationUi.js +25 -0
  255. package/dist/modules/wms/lib/inventoryMutationUi.js.map +7 -0
  256. package/dist/modules/wms/lib/inventoryPolicy.js +61 -0
  257. package/dist/modules/wms/lib/inventoryPolicy.js.map +7 -0
  258. package/dist/modules/wms/lib/inventoryReconciliation.js +193 -0
  259. package/dist/modules/wms/lib/inventoryReconciliation.js.map +7 -0
  260. package/dist/modules/wms/lib/inventoryTrackingValidation.js +13 -0
  261. package/dist/modules/wms/lib/inventoryTrackingValidation.js.map +7 -0
  262. package/dist/modules/wms/lib/loadOperationalDashboard.js +580 -0
  263. package/dist/modules/wms/lib/loadOperationalDashboard.js.map +7 -0
  264. package/dist/modules/wms/lib/lowStockBalanceFilter.js +49 -0
  265. package/dist/modules/wms/lib/lowStockBalanceFilter.js.map +7 -0
  266. package/dist/modules/wms/lib/primaryWarehousePolicy.js +32 -0
  267. package/dist/modules/wms/lib/primaryWarehousePolicy.js.map +7 -0
  268. package/dist/modules/wms/lib/roleFeatures.js +30 -0
  269. package/dist/modules/wms/lib/roleFeatures.js.map +7 -0
  270. package/dist/modules/wms/lib/salesOrderInventoryAutomation.js +291 -0
  271. package/dist/modules/wms/lib/salesOrderInventoryAutomation.js.map +7 -0
  272. package/dist/modules/wms/lib/salesOrderWarehouseAssignment.js +54 -0
  273. package/dist/modules/wms/lib/salesOrderWarehouseAssignment.js.map +7 -0
  274. package/dist/modules/wms/lib/wmsIntegrationToggles.js +71 -0
  275. package/dist/modules/wms/lib/wmsIntegrationToggles.js.map +7 -0
  276. package/dist/modules/wms/migrations/Migration20260428110546.js +78 -0
  277. package/dist/modules/wms/migrations/Migration20260428110546.js.map +7 -0
  278. package/dist/modules/wms/migrations/Migration20260527120000.js +15 -0
  279. package/dist/modules/wms/migrations/Migration20260527120000.js.map +7 -0
  280. package/dist/modules/wms/migrations/Migration20260527140000.js +37 -0
  281. package/dist/modules/wms/migrations/Migration20260527140000.js.map +7 -0
  282. package/dist/modules/wms/migrations/Migration20260613120000.js +23 -0
  283. package/dist/modules/wms/migrations/Migration20260613120000.js.map +7 -0
  284. package/dist/modules/wms/migrations/Migration20260616090000.js +13 -0
  285. package/dist/modules/wms/migrations/Migration20260616090000.js.map +7 -0
  286. package/dist/modules/wms/migrations/Migration20260707180000.js +15 -0
  287. package/dist/modules/wms/migrations/Migration20260707180000.js.map +7 -0
  288. package/dist/modules/wms/notifications.client.js +18 -0
  289. package/dist/modules/wms/notifications.client.js.map +7 -0
  290. package/dist/modules/wms/notifications.js +53 -0
  291. package/dist/modules/wms/notifications.js.map +7 -0
  292. package/dist/modules/wms/search.js +196 -0
  293. package/dist/modules/wms/search.js.map +7 -0
  294. package/dist/modules/wms/setup.js +30 -0
  295. package/dist/modules/wms/setup.js.map +7 -0
  296. package/dist/modules/wms/subscribers/low-stock-notification.js +40 -0
  297. package/dist/modules/wms/subscribers/low-stock-notification.js.map +7 -0
  298. package/dist/modules/wms/subscribers/reservation-shortfall-notification.js +69 -0
  299. package/dist/modules/wms/subscribers/reservation-shortfall-notification.js.map +7 -0
  300. package/dist/modules/wms/subscribers/sales-order-cancelled-release.js +14 -0
  301. package/dist/modules/wms/subscribers/sales-order-cancelled-release.js.map +7 -0
  302. package/dist/modules/wms/subscribers/sales-order-confirmed-reserve.js +14 -0
  303. package/dist/modules/wms/subscribers/sales-order-confirmed-reserve.js.map +7 -0
  304. package/dist/modules/wms/translations.js +10 -0
  305. package/dist/modules/wms/translations.js.map +7 -0
  306. package/dist/modules/wms/vector.js +9 -0
  307. package/dist/modules/wms/vector.js.map +7 -0
  308. package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js +200 -0
  309. package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js.map +7 -0
  310. package/dist/modules/wms/widgets/injection/order-items-stock-column/widget.js +104 -0
  311. package/dist/modules/wms/widgets/injection/order-items-stock-column/widget.js.map +7 -0
  312. package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.client.js +389 -0
  313. package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.client.js.map +7 -0
  314. package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.js +16 -0
  315. package/dist/modules/wms/widgets/injection/sales-order-stock-context/widget.js.map +7 -0
  316. package/dist/modules/wms/widgets/injection-table.js +36 -0
  317. package/dist/modules/wms/widgets/injection-table.js.map +7 -0
  318. package/dist/modules/wms/widgets/notifications/WmsLowStockRenderer.js +150 -0
  319. package/dist/modules/wms/widgets/notifications/WmsLowStockRenderer.js.map +7 -0
  320. package/dist/modules/wms/widgets/notifications/WmsReservationShortfallRenderer.js +186 -0
  321. package/dist/modules/wms/widgets/notifications/WmsReservationShortfallRenderer.js.map +7 -0
  322. package/dist/modules/wms/widgets/notifications/index.js +7 -0
  323. package/dist/modules/wms/widgets/notifications/index.js.map +7 -0
  324. package/generated/entities/inventory_balance/index.ts +9 -0
  325. package/generated/entities/inventory_lot/index.ts +8 -0
  326. package/generated/entities/inventory_movement/index.ts +16 -0
  327. package/generated/entities/inventory_reservation/index.ts +10 -0
  328. package/generated/entities/product_inventory_profile/index.ts +9 -0
  329. package/generated/entities/sales_order_warehouse_assignment/index.ts +4 -0
  330. package/generated/entities/warehouse/index.ts +11 -0
  331. package/generated/entities/warehouse_location/index.ts +9 -0
  332. package/generated/entities/warehouse_zone/index.ts +4 -0
  333. package/generated/entities.ids.generated.ts +12 -0
  334. package/generated/entity-fields-registry.ts +98 -0
  335. package/package.json +7 -7
  336. package/src/modules/auth/backend/roles/page.tsx +13 -4
  337. package/src/modules/auth/i18n/de.json +2 -0
  338. package/src/modules/auth/i18n/en.json +2 -0
  339. package/src/modules/auth/i18n/es.json +2 -0
  340. package/src/modules/auth/i18n/pl.json +2 -0
  341. package/src/modules/auth/lib/backendChrome.tsx +1 -0
  342. package/src/modules/auth/services/rbacService.ts +25 -21
  343. package/src/modules/catalog/api/products/route.ts +3 -0
  344. package/src/modules/catalog/api/variants/route.ts +3 -0
  345. package/src/modules/customer_accounts/migrations/Migration20260724120000_customer_accounts.ts +96 -0
  346. package/src/modules/directory/api/organizations/route.ts +1 -1
  347. package/src/modules/entities/api/definitions.ts +2 -0
  348. package/src/modules/entities/lib/helpers.ts +1 -0
  349. package/src/modules/query_index/lib/engine.ts +18 -6
  350. package/src/modules/sales/api/documents/factory.ts +1 -0
  351. package/src/modules/sales/api/quotes/accept/route.ts +9 -2
  352. package/src/modules/sales/api/quotes/public/[token]/route.ts +2 -1
  353. package/src/modules/sales/commands/documents.ts +71 -0
  354. package/src/modules/sales/components/documents/ItemsSection.tsx +58 -0
  355. package/src/modules/sales/events.ts +2 -0
  356. package/src/modules/sales/lib/publicQuoteTenantScope.ts +49 -0
  357. package/src/modules/sales/widgets/injection/order-items-context.ts +15 -0
  358. package/src/modules/wms/acl.ts +14 -0
  359. package/src/modules/wms/analytics.ts +9 -0
  360. package/src/modules/wms/api/dashboard/operational/route.ts +83 -0
  361. package/src/modules/wms/api/interceptors.ts +108 -0
  362. package/src/modules/wms/api/inventory/adjust/route.ts +50 -0
  363. package/src/modules/wms/api/inventory/allocate/route.ts +50 -0
  364. package/src/modules/wms/api/inventory/balances/route.ts +178 -0
  365. package/src/modules/wms/api/inventory/cycle-count/route.ts +51 -0
  366. package/src/modules/wms/api/inventory/helpers.ts +128 -0
  367. package/src/modules/wms/api/inventory/import/apply/route.ts +47 -0
  368. package/src/modules/wms/api/inventory/import/helpers.ts +223 -0
  369. package/src/modules/wms/api/inventory/import/template/route.ts +31 -0
  370. package/src/modules/wms/api/inventory/import/validate/route.ts +56 -0
  371. package/src/modules/wms/api/inventory/move/route.ts +50 -0
  372. package/src/modules/wms/api/inventory/movements/route.ts +164 -0
  373. package/src/modules/wms/api/inventory/receive/route.ts +49 -0
  374. package/src/modules/wms/api/inventory/release/route.ts +43 -0
  375. package/src/modules/wms/api/inventory/reservations/route.ts +128 -0
  376. package/src/modules/wms/api/inventory/reserve/route.ts +59 -0
  377. package/src/modules/wms/api/inventory-profiles/route.ts +177 -0
  378. package/src/modules/wms/api/listEnrichers.ts +312 -0
  379. package/src/modules/wms/api/listSearch.ts +121 -0
  380. package/src/modules/wms/api/locations/route.ts +171 -0
  381. package/src/modules/wms/api/lots/route.ts +205 -0
  382. package/src/modules/wms/api/openapi.ts +28 -0
  383. package/src/modules/wms/api/sales-orders/[salesOrderId]/re-run-reservation/route.ts +58 -0
  384. package/src/modules/wms/api/sales-orders/[salesOrderId]/warehouse-assignment/route.ts +306 -0
  385. package/src/modules/wms/api/sales-orders/assign-warehouse/route.ts +57 -0
  386. package/src/modules/wms/api/sales-orders/unassign-warehouse/route.ts +43 -0
  387. package/src/modules/wms/api/warehouses/route.ts +181 -0
  388. package/src/modules/wms/api/zones/route.ts +158 -0
  389. package/src/modules/wms/backend/config/wms/page.meta.ts +14 -0
  390. package/src/modules/wms/backend/config/wms/page.tsx +5 -0
  391. package/src/modules/wms/backend/page.meta.ts +12 -0
  392. package/src/modules/wms/backend/page.tsx +5 -0
  393. package/src/modules/wms/backend/wms/inventory/page.meta.ts +14 -0
  394. package/src/modules/wms/backend/wms/inventory/page.tsx +5 -0
  395. package/src/modules/wms/backend/wms/location/[id]/page.meta.ts +14 -0
  396. package/src/modules/wms/backend/wms/location/[id]/page.tsx +5 -0
  397. package/src/modules/wms/backend/wms/locations/page.meta.ts +14 -0
  398. package/src/modules/wms/backend/wms/locations/page.tsx +12 -0
  399. package/src/modules/wms/backend/wms/lot/[id]/page.meta.ts +14 -0
  400. package/src/modules/wms/backend/wms/lot/[id]/page.tsx +5 -0
  401. package/src/modules/wms/backend/wms/lots/page.meta.ts +14 -0
  402. package/src/modules/wms/backend/wms/lots/page.tsx +5 -0
  403. package/src/modules/wms/backend/wms/movements/page.meta.ts +14 -0
  404. package/src/modules/wms/backend/wms/movements/page.tsx +23 -0
  405. package/src/modules/wms/backend/wms/reservations/page.meta.ts +14 -0
  406. package/src/modules/wms/backend/wms/reservations/page.tsx +24 -0
  407. package/src/modules/wms/backend/wms/sku/[id]/page.meta.ts +14 -0
  408. package/src/modules/wms/backend/wms/sku/[id]/page.tsx +5 -0
  409. package/src/modules/wms/backend/wms/warehouses/page.meta.ts +14 -0
  410. package/src/modules/wms/backend/wms/warehouses/page.tsx +12 -0
  411. package/src/modules/wms/backend/wms/zones/page.meta.ts +14 -0
  412. package/src/modules/wms/backend/wms/zones/page.tsx +12 -0
  413. package/src/modules/wms/ce.ts +76 -0
  414. package/src/modules/wms/cli.ts +96 -0
  415. package/src/modules/wms/commands/configuration.ts +1844 -0
  416. package/src/modules/wms/commands/index.ts +3 -0
  417. package/src/modules/wms/commands/inventory-actions.ts +1866 -0
  418. package/src/modules/wms/commands/sales-order-assignment.ts +346 -0
  419. package/src/modules/wms/commands/shared.ts +90 -0
  420. package/src/modules/wms/components/backend/AdjustInventoryDialog.tsx +820 -0
  421. package/src/modules/wms/components/backend/ChangeLotStatusDialog.tsx +312 -0
  422. package/src/modules/wms/components/backend/CycleCountWizardDialog.tsx +2007 -0
  423. package/src/modules/wms/components/backend/ImportInventoryDialog.tsx +1018 -0
  424. package/src/modules/wms/components/backend/InventoryOperationsSection.tsx +159 -0
  425. package/src/modules/wms/components/backend/LocationEditDialog.tsx +185 -0
  426. package/src/modules/wms/components/backend/MoveInventoryDialog.tsx +907 -0
  427. package/src/modules/wms/components/backend/ReceiveInventoryDialog.tsx +695 -0
  428. package/src/modules/wms/components/backend/ReleaseReservationDialog.tsx +424 -0
  429. package/src/modules/wms/components/backend/ReserveInventoryDialog.tsx +511 -0
  430. package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +1340 -0
  431. package/src/modules/wms/components/backend/WmsInventoryConsolePage.tsx +1132 -0
  432. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +1310 -0
  433. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +1355 -0
  434. package/src/modules/wms/components/backend/WmsLotsListPage.tsx +359 -0
  435. package/src/modules/wms/components/backend/WmsOperationalDashboardPage.tsx +958 -0
  436. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +1245 -0
  437. package/src/modules/wms/components/backend/inventoryMutationLoaders.ts +960 -0
  438. package/src/modules/wms/components/backend/useWmsInventoryMutationAccess.ts +89 -0
  439. package/src/modules/wms/components/backend/useWmsInventoryScopeFromSearchParams.ts +32 -0
  440. package/src/modules/wms/components/backend/wmsLookupLoaders.ts +78 -0
  441. package/src/modules/wms/data/enrichers.ts +718 -0
  442. package/src/modules/wms/data/entities.ts +436 -0
  443. package/src/modules/wms/data/validators.ts +463 -0
  444. package/src/modules/wms/di.ts +25 -0
  445. package/src/modules/wms/events.ts +41 -0
  446. package/src/modules/wms/i18n/de.json +1051 -0
  447. package/src/modules/wms/i18n/en.json +1051 -0
  448. package/src/modules/wms/i18n/es.json +1051 -0
  449. package/src/modules/wms/i18n/pl.json +1051 -0
  450. package/src/modules/wms/index.ts +15 -0
  451. package/src/modules/wms/lib/catalogInventoryProfileIntent.ts +65 -0
  452. package/src/modules/wms/lib/catalogInventoryProfileSync.ts +199 -0
  453. package/src/modules/wms/lib/expiry.ts +30 -0
  454. package/src/modules/wms/lib/flashMutationError.ts +74 -0
  455. package/src/modules/wms/lib/inventoryDisplayUi.ts +249 -0
  456. package/src/modules/wms/lib/inventoryIdempotency.ts +54 -0
  457. package/src/modules/wms/lib/inventoryImportCsv.ts +212 -0
  458. package/src/modules/wms/lib/inventoryImportService.ts +629 -0
  459. package/src/modules/wms/lib/inventoryMutationUi.ts +21 -0
  460. package/src/modules/wms/lib/inventoryPolicy.ts +111 -0
  461. package/src/modules/wms/lib/inventoryReconciliation.ts +263 -0
  462. package/src/modules/wms/lib/inventoryTrackingValidation.ts +23 -0
  463. package/src/modules/wms/lib/loadOperationalDashboard.ts +776 -0
  464. package/src/modules/wms/lib/lowStockBalanceFilter.ts +74 -0
  465. package/src/modules/wms/lib/primaryWarehousePolicy.ts +46 -0
  466. package/src/modules/wms/lib/roleFeatures.ts +32 -0
  467. package/src/modules/wms/lib/salesOrderInventoryAutomation.ts +390 -0
  468. package/src/modules/wms/lib/salesOrderWarehouseAssignment.ts +78 -0
  469. package/src/modules/wms/lib/wmsIntegrationToggles.ts +81 -0
  470. package/src/modules/wms/migrations/.snapshot-open-mercato.json +2880 -0
  471. package/src/modules/wms/migrations/Migration20260428110546.ts +91 -0
  472. package/src/modules/wms/migrations/Migration20260527120000.ts +15 -0
  473. package/src/modules/wms/migrations/Migration20260527140000.ts +35 -0
  474. package/src/modules/wms/migrations/Migration20260613120000.ts +21 -0
  475. package/src/modules/wms/migrations/Migration20260616090000.ts +11 -0
  476. package/src/modules/wms/migrations/Migration20260707180000.ts +13 -0
  477. package/src/modules/wms/notifications.client.ts +18 -0
  478. package/src/modules/wms/notifications.ts +51 -0
  479. package/src/modules/wms/search.ts +230 -0
  480. package/src/modules/wms/setup.ts +29 -0
  481. package/src/modules/wms/subscribers/low-stock-notification.ts +54 -0
  482. package/src/modules/wms/subscribers/reservation-shortfall-notification.ts +98 -0
  483. package/src/modules/wms/subscribers/sales-order-cancelled-release.ts +21 -0
  484. package/src/modules/wms/subscribers/sales-order-confirmed-reserve.ts +21 -0
  485. package/src/modules/wms/translations.ts +6 -0
  486. package/src/modules/wms/vector.ts +7 -0
  487. package/src/modules/wms/widgets/injection/catalog-inventory-profile/widget.ts +238 -0
  488. package/src/modules/wms/widgets/injection/order-items-stock-column/widget.tsx +153 -0
  489. package/src/modules/wms/widgets/injection/sales-order-stock-context/widget.client.tsx +476 -0
  490. package/src/modules/wms/widgets/injection/sales-order-stock-context/widget.ts +15 -0
  491. package/src/modules/wms/widgets/injection-table.ts +34 -0
  492. package/src/modules/wms/widgets/notifications/WmsLowStockRenderer.tsx +153 -0
  493. package/src/modules/wms/widgets/notifications/WmsReservationShortfallRenderer.tsx +194 -0
  494. 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)