@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,1866 @@
1
+ // =============================================================================
2
+ // WMS Inventory Mutation Commands — Undo Policy
3
+ // =============================================================================
4
+ //
5
+ // All inventory-mutation commands in this file are deliberately registered
6
+ // with `isUndoable: false` and therefore opt out of the generic command-bus
7
+ // undo flow. WMS inventory is modeled as an append-only ledger
8
+ // (`inventory_movements`) that drives live balances; subsequent reservations
9
+ // and movements may be made on top of any prior state, so a per-record
10
+ // "undo" cannot safely re-derive a point-in-time balance without a
11
+ // stop-the-world replay.
12
+ //
13
+ // Reversal is therefore exposed as an explicit, auditable counter-action in
14
+ // the domain model rather than as a generic undo verb:
15
+ // - reserve ↔ release
16
+ // - allocate ↔ release (cancels the allocation)
17
+ // - adjust(+N) ↔ adjust(-N)
18
+ // - receive ↔ adjust / RMA flow
19
+ // - move(A → B) ↔ move(B → A)
20
+ // - cycle count ↔ cycle count (re-recount)
21
+ //
22
+ // The audit log still captures the full before/after via `buildLog`, so any
23
+ // reversing counter-action is fully traceable.
24
+ // =============================================================================
25
+ import { LockMode } from '@mikro-orm/core'
26
+ import { randomUUID } from 'crypto'
27
+ import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
28
+ import { registerCommand } from '@open-mercato/shared/lib/commands'
29
+ import { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
30
+ import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
31
+ import type { EntityManager } from '@mikro-orm/postgresql'
32
+ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
33
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
34
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
35
+ import { emitWmsEvent } from '../events'
36
+ import { E } from '#generated/entities.ids.generated'
37
+ import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
38
+ import {
39
+ InventoryBalance,
40
+ InventoryLot,
41
+ InventoryMovement,
42
+ InventoryReservation,
43
+ ProductInventoryProfile,
44
+ Warehouse,
45
+ WarehouseLocation,
46
+ type InventoryStrategy,
47
+ } from '../data/entities'
48
+ import {
49
+ inventoryAdjustSchema,
50
+ inventoryCycleCountSchema,
51
+ inventoryMoveSchema,
52
+ inventoryReceiveSchema,
53
+ inventoryReservationAllocateSchema,
54
+ inventoryReservationCreateSchema,
55
+ inventoryReservationReleaseSchema,
56
+ type InventoryAdjustInput,
57
+ type InventoryCycleCountInput,
58
+ type InventoryMoveInput,
59
+ type InventoryReceiveInput,
60
+ type InventoryReservationAllocateInput,
61
+ type InventoryReservationCreateInput,
62
+ type InventoryReservationReleaseInput,
63
+ } from '../data/validators'
64
+ import {
65
+ evaluateLowStock,
66
+ isLotEligible,
67
+ resolveReservationStrategyFromProfile,
68
+ sortBucketsForStrategy,
69
+ type InventoryStrategyBucket,
70
+ } from '../lib/inventoryPolicy'
71
+ import { enforceInventoryTrackingRequirements } from '../lib/inventoryTrackingValidation'
72
+ import {
73
+ buildMovementIdempotencyKey,
74
+ buildReservationIdempotencyKey,
75
+ } from '../lib/inventoryIdempotency'
76
+ import {
77
+ ensureOrganizationScope,
78
+ ensureTenantScope,
79
+ inventoryBalanceCrudEvents,
80
+ inventoryBalanceCrudIndexer,
81
+ inventoryMovementCrudEvents,
82
+ inventoryMovementCrudIndexer,
83
+ inventoryReservationCrudEvents,
84
+ inventoryReservationCrudIndexer,
85
+ requireId,
86
+ toNumericString,
87
+ WMS_INVENTORY_BALANCE_RESOURCE,
88
+ WMS_INVENTORY_MOVEMENT_RESOURCE,
89
+ WMS_INVENTORY_RESERVATION_RESOURCE,
90
+ } from './shared'
91
+
92
+ type Scope = { tenantId: string; organizationId: string }
93
+ type AllocationBucket = {
94
+ balanceId: string
95
+ locationId: string
96
+ lotId: string | null
97
+ serialNumber: string | null
98
+ quantity: number
99
+ }
100
+
101
+ type ReservationMetadata = {
102
+ allocatedBuckets?: AllocationBucket[]
103
+ allocationState?: 'reserved' | 'allocated'
104
+ strategy?: InventoryStrategy
105
+ releasedReason?: string
106
+ releasedAt?: string
107
+ allocatedAt?: string
108
+ [key: string]: unknown
109
+ }
110
+
111
+ type ReservationCommandResult = {
112
+ reservationId: string
113
+ allocatedBuckets: Array<{ locationId: string; lotId: string | null; quantity: string }>
114
+ }
115
+
116
+ type MutationLogInput = {
117
+ actionKey: string
118
+ fallbackLabel: string
119
+ resourceKind: string
120
+ resourceId: string | null
121
+ parentResourceId?: string | null
122
+ tenantId: string | null
123
+ organizationId: string | null
124
+ cacheAliases?: string[]
125
+ }
126
+
127
+ type AffectedReservation = { entity: InventoryReservation; action: 'created' | 'updated' | 'deleted' }
128
+ type AffectedMovement = { entity: InventoryMovement; action: 'created' | 'updated' | 'deleted' }
129
+ type AffectedBalance = { entity: InventoryBalance; action: 'created' | 'updated' | 'deleted' }
130
+
131
+ type AffectedSideEffects = {
132
+ reservations?: AffectedReservation[]
133
+ movements?: AffectedMovement[]
134
+ balances?: AffectedBalance[]
135
+ }
136
+
137
+ async function emitInventorySideEffects(
138
+ ctx: CommandRuntimeContext,
139
+ affected: AffectedSideEffects,
140
+ ): Promise<void> {
141
+ let de: DataEngine | null = null
142
+ try {
143
+ de = ctx.container.resolve('dataEngine') as DataEngine
144
+ } catch {
145
+ de = null
146
+ }
147
+ if (!de) return
148
+
149
+ for (const { entity, action } of affected.reservations ?? []) {
150
+ await emitCrudSideEffects({
151
+ dataEngine: de,
152
+ action,
153
+ entity,
154
+ identifiers: {
155
+ id: entity.id,
156
+ organizationId: entity.organizationId,
157
+ tenantId: entity.tenantId,
158
+ },
159
+ indexer: inventoryReservationCrudIndexer,
160
+ events: inventoryReservationCrudEvents,
161
+ })
162
+ }
163
+ for (const { entity, action } of affected.movements ?? []) {
164
+ await emitCrudSideEffects({
165
+ dataEngine: de,
166
+ action,
167
+ entity,
168
+ identifiers: {
169
+ id: entity.id,
170
+ organizationId: entity.organizationId,
171
+ tenantId: entity.tenantId,
172
+ },
173
+ indexer: inventoryMovementCrudIndexer,
174
+ events: inventoryMovementCrudEvents,
175
+ })
176
+ }
177
+ for (const { entity, action } of affected.balances ?? []) {
178
+ await emitCrudSideEffects({
179
+ dataEngine: de,
180
+ action,
181
+ entity,
182
+ identifiers: {
183
+ id: entity.id,
184
+ organizationId: entity.organizationId,
185
+ tenantId: entity.tenantId,
186
+ },
187
+ indexer: inventoryBalanceCrudIndexer,
188
+ events: inventoryBalanceCrudEvents,
189
+ })
190
+ }
191
+ }
192
+
193
+ function resolveScope(ctx: CommandRuntimeContext, fallback?: { tenantId?: string | null; organizationId?: string | null }): Scope {
194
+ const tenantId = fallback?.tenantId ?? ctx.auth?.tenantId ?? null
195
+ const organizationId = fallback?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
196
+ if (!tenantId || !organizationId) {
197
+ throw new CrudHttpError(400, { error: 'Tenant and organization scope are required.' })
198
+ }
199
+ return { tenantId, organizationId }
200
+ }
201
+
202
+ function resolveEm(ctx: CommandRuntimeContext): EntityManager {
203
+ return (ctx.container.resolve('em') as EntityManager).fork()
204
+ }
205
+
206
+ async function runInTransaction<TResult>(
207
+ em: EntityManager,
208
+ operation: (trx: EntityManager) => Promise<TResult>,
209
+ ): Promise<TResult> {
210
+ const transactionalEm = em as EntityManager & {
211
+ transactional?: (callback: (trx: EntityManager) => Promise<TResult>) => Promise<TResult>
212
+ }
213
+ if (typeof transactionalEm.transactional === 'function') {
214
+ return transactionalEm.transactional((trx) => operation(trx))
215
+ }
216
+ return operation(em)
217
+ }
218
+
219
+ function toNumber(value: unknown): number {
220
+ if (typeof value === 'number' && Number.isFinite(value)) return value
221
+ if (typeof value === 'string' && value.trim().length > 0) {
222
+ const parsed = Number(value)
223
+ if (Number.isFinite(parsed)) return parsed
224
+ }
225
+ return 0
226
+ }
227
+
228
+ function isUniqueConstraintError(error: unknown): boolean {
229
+ if (!error || typeof error !== 'object') return false
230
+ const code = (error as { code?: string }).code
231
+ if (code === '23505') return true
232
+ const name = (error as { name?: string }).name
233
+ return name === 'UniqueConstraintViolationException'
234
+ }
235
+
236
+ async function findExistingMovementByIdempotencyKey(
237
+ em: EntityManager,
238
+ scope: Scope,
239
+ idempotencyKey: string,
240
+ ): Promise<InventoryMovement | null> {
241
+ return findOneWithDecryption(
242
+ em,
243
+ InventoryMovement,
244
+ {
245
+ organizationId: scope.organizationId,
246
+ idempotencyKey,
247
+ deletedAt: null,
248
+ },
249
+ undefined,
250
+ scope,
251
+ )
252
+ }
253
+
254
+ async function findExistingActiveReservationByIdempotencyKey(
255
+ em: EntityManager,
256
+ scope: Scope,
257
+ idempotencyKey: string,
258
+ ): Promise<InventoryReservation | null> {
259
+ return findOneWithDecryption(
260
+ em,
261
+ InventoryReservation,
262
+ {
263
+ organizationId: scope.organizationId,
264
+ idempotencyKey,
265
+ status: 'active',
266
+ deletedAt: null,
267
+ },
268
+ undefined,
269
+ scope,
270
+ )
271
+ }
272
+
273
+ function resolveLotFromBalance(balance: InventoryBalance): InventoryLot | null {
274
+ const lotRaw = balance.lot ?? null
275
+ if (!lotRaw || typeof lotRaw === 'string') return null
276
+ return lotRaw
277
+ }
278
+
279
+ function balanceHasEligibleLot(balance: InventoryBalance, now: Date): boolean {
280
+ const lot = resolveLotFromBalance(balance)
281
+ return isLotEligible(lot, now)
282
+ }
283
+
284
+ function setNumeric(target: { [key: string]: unknown }, key: string, value: number) {
285
+ target[key] = toNumericString(value)
286
+ }
287
+
288
+ function getAvailableQuantity(balance: InventoryBalance): number {
289
+ return toNumber(balance.quantityOnHand) - toNumber(balance.quantityReserved) - toNumber(balance.quantityAllocated)
290
+ }
291
+
292
+ function extractReservationMetadata(reservation: InventoryReservation): ReservationMetadata {
293
+ if (!reservation.metadata || typeof reservation.metadata !== 'object' || Array.isArray(reservation.metadata)) {
294
+ return {}
295
+ }
296
+ return { ...(reservation.metadata as Record<string, unknown>) }
297
+ }
298
+
299
+ function buildBucketKey(input: {
300
+ warehouseId: string
301
+ locationId: string
302
+ catalogVariantId: string
303
+ lotId: string | null
304
+ serialNumber: string | null
305
+ }): string {
306
+ return [
307
+ input.warehouseId,
308
+ input.locationId,
309
+ input.catalogVariantId,
310
+ input.lotId ?? '',
311
+ input.serialNumber ?? '',
312
+ ].join('::')
313
+ }
314
+
315
+ function serializeAllocationBuckets(buckets: AllocationBucket[]): Array<{ locationId: string; lotId: string | null; quantity: string }> {
316
+ return buckets.map((bucket) => ({
317
+ locationId: bucket.locationId,
318
+ lotId: bucket.lotId,
319
+ quantity: toNumericString(bucket.quantity),
320
+ }))
321
+ }
322
+
323
+ async function buildMutationLog(input: MutationLogInput) {
324
+ const { translate } = await resolveTranslations()
325
+ const aliases = Array.from(
326
+ new Set((input.cacheAliases ?? []).filter((alias) => typeof alias === 'string' && alias.length > 0)),
327
+ )
328
+ return {
329
+ actionLabel: translate(input.actionKey, input.fallbackLabel),
330
+ resourceKind: input.resourceKind,
331
+ resourceId: input.resourceId,
332
+ parentResourceKind: input.parentResourceId ? 'wms.inventory' : null,
333
+ parentResourceId: input.parentResourceId ?? null,
334
+ tenantId: input.tenantId,
335
+ organizationId: input.organizationId,
336
+ ...(aliases.length ? { context: { cacheAliases: aliases } } : {}),
337
+ }
338
+ }
339
+
340
+ async function requireWarehouse(em: EntityManager, ctx: CommandRuntimeContext, warehouseId: string, scope?: Scope) {
341
+ const resolvedScope = scope ?? resolveScope(ctx)
342
+ const warehouse = await findOneWithDecryption(
343
+ em,
344
+ Warehouse,
345
+ { id: warehouseId, deletedAt: null },
346
+ undefined,
347
+ resolvedScope,
348
+ )
349
+ if (!warehouse) throw new CrudHttpError(404, { error: 'Warehouse not found.' })
350
+ ensureTenantScope(ctx, warehouse.tenantId)
351
+ ensureOrganizationScope(ctx, warehouse.organizationId)
352
+ return warehouse
353
+ }
354
+
355
+ async function requireLocation(em: EntityManager, ctx: CommandRuntimeContext, locationId: string, scope?: Scope) {
356
+ const resolvedScope = scope ?? resolveScope(ctx)
357
+ const location = await findOneWithDecryption(
358
+ em,
359
+ WarehouseLocation,
360
+ { id: locationId, deletedAt: null },
361
+ undefined,
362
+ resolvedScope,
363
+ )
364
+ if (!location) throw new CrudHttpError(404, { error: 'Warehouse location not found.' })
365
+ ensureTenantScope(ctx, location.tenantId)
366
+ ensureOrganizationScope(ctx, location.organizationId)
367
+ return location
368
+ }
369
+
370
+ async function requireReservation(em: EntityManager, ctx: CommandRuntimeContext, reservationId: string, scope?: Scope, lock = false) {
371
+ const resolvedScope = scope ?? resolveScope(ctx)
372
+ const reservation = await findOneWithDecryption(
373
+ em,
374
+ InventoryReservation,
375
+ { id: reservationId, deletedAt: null },
376
+ lock ? { lockMode: LockMode.PESSIMISTIC_WRITE } : undefined,
377
+ resolvedScope,
378
+ )
379
+ if (!reservation) throw new CrudHttpError(404, { error: 'Inventory reservation not found.' })
380
+ ensureTenantScope(ctx, reservation.tenantId)
381
+ ensureOrganizationScope(ctx, reservation.organizationId)
382
+ return reservation
383
+ }
384
+
385
+ async function loadProfileForVariant(
386
+ em: EntityManager,
387
+ ctx: CommandRuntimeContext,
388
+ catalogVariantId: string,
389
+ scope?: Scope,
390
+ ) {
391
+ const resolvedScope = scope ?? resolveScope(ctx)
392
+ const profile = await findOneWithDecryption(
393
+ em,
394
+ ProductInventoryProfile,
395
+ { catalogVariantId, deletedAt: null },
396
+ undefined,
397
+ resolvedScope,
398
+ )
399
+ if (!profile) return null
400
+ ensureTenantScope(ctx, profile.tenantId)
401
+ ensureOrganizationScope(ctx, profile.organizationId)
402
+ return profile
403
+ }
404
+
405
+ async function resolveReceiveLotId(
406
+ em: EntityManager,
407
+ ctx: CommandRuntimeContext,
408
+ scope: Scope,
409
+ input: Pick<InventoryReceiveInput, 'catalogVariantId' | 'lotId' | 'lotNumber'>,
410
+ ): Promise<string | undefined> {
411
+ if (input.lotId?.trim()) return input.lotId.trim()
412
+ const lotNumber = input.lotNumber?.trim()
413
+ if (!lotNumber) return undefined
414
+
415
+ const existing = await findOneWithDecryption(
416
+ em,
417
+ InventoryLot,
418
+ {
419
+ catalogVariantId: input.catalogVariantId,
420
+ lotNumber,
421
+ deletedAt: null,
422
+ },
423
+ undefined,
424
+ scope,
425
+ )
426
+ if (existing) return existing.id
427
+
428
+ const queryEngine = ctx.container.resolve<QueryEngine>('queryEngine')
429
+ const variantResult = await queryEngine.query(E.catalog.catalog_product_variant, {
430
+ tenantId: scope.tenantId,
431
+ organizationId: scope.organizationId,
432
+ filters: { id: { $eq: input.catalogVariantId } },
433
+ fields: ['id', 'sku'],
434
+ page: { page: 1, pageSize: 1 },
435
+ })
436
+ const firstItem: unknown = variantResult.items?.[0]
437
+ const sku =
438
+ typeof firstItem === 'object' && firstItem !== null
439
+ ? String((firstItem as Record<string, unknown>).sku ?? '').trim()
440
+ : ''
441
+ if (!sku) {
442
+ throw new CrudHttpError(422, { error: 'variant_sku_missing' })
443
+ }
444
+
445
+ // Use a client-side UUID so the lot entity can be persisted into the
446
+ // surrounding transaction without a premature flush (defaultRaw would
447
+ // otherwise require a DB round-trip to obtain the generated id).
448
+ const lot = em.create(InventoryLot, {
449
+ id: randomUUID(),
450
+ organizationId: scope.organizationId,
451
+ tenantId: scope.tenantId,
452
+ catalogVariantId: input.catalogVariantId,
453
+ sku,
454
+ lotNumber,
455
+ status: 'available',
456
+ })
457
+ em.persist(lot)
458
+ return lot.id
459
+ }
460
+
461
+ async function listCandidateBalances(
462
+ em: EntityManager,
463
+ ctx: CommandRuntimeContext,
464
+ input: {
465
+ warehouseId: string
466
+ catalogVariantId: string
467
+ lotId?: string
468
+ serialNumber?: string
469
+ },
470
+ scope: Scope,
471
+ ) {
472
+ const where: Record<string, unknown> = {
473
+ warehouse: input.warehouseId,
474
+ catalogVariantId: input.catalogVariantId,
475
+ deletedAt: null,
476
+ }
477
+ if (input.lotId) where.lot = input.lotId
478
+ if (input.serialNumber) where.serialNumber = input.serialNumber
479
+ const balances = await findWithDecryption(
480
+ em,
481
+ InventoryBalance,
482
+ where,
483
+ {
484
+ populate: ['lot', 'location'],
485
+ lockMode: LockMode.PESSIMISTIC_WRITE,
486
+ orderBy: { createdAt: 'asc', id: 'asc' },
487
+ },
488
+ scope,
489
+ )
490
+ for (const balance of balances) {
491
+ ensureTenantScope(ctx, balance.tenantId)
492
+ ensureOrganizationScope(ctx, balance.organizationId)
493
+ }
494
+ return balances
495
+ }
496
+
497
+ async function listBalancesForVariant(
498
+ em: EntityManager,
499
+ ctx: CommandRuntimeContext,
500
+ catalogVariantId: string,
501
+ scope: Scope,
502
+ ) {
503
+ const balances = await findWithDecryption(
504
+ em,
505
+ InventoryBalance,
506
+ {
507
+ catalogVariantId,
508
+ deletedAt: null,
509
+ },
510
+ undefined,
511
+ scope,
512
+ )
513
+ for (const balance of balances) {
514
+ ensureTenantScope(ctx, balance.tenantId)
515
+ ensureOrganizationScope(ctx, balance.organizationId)
516
+ }
517
+ return balances
518
+ }
519
+
520
+ async function emitLowStockEventIfNeeded(
521
+ em: EntityManager,
522
+ ctx: CommandRuntimeContext,
523
+ scope: Scope,
524
+ catalogVariantId: string,
525
+ ) {
526
+ const profile = await loadProfileForVariant(em, ctx, catalogVariantId, scope)
527
+ if (!profile) return
528
+
529
+ const balances = await listBalancesForVariant(em, ctx, catalogVariantId, scope)
530
+ const availableQuantity = balances.reduce((total, balance) => total + getAvailableQuantity(balance), 0)
531
+ const lowStock = evaluateLowStock(profile, availableQuantity)
532
+ if (!lowStock) return
533
+
534
+ await emitWmsEvent('wms.inventory.low_stock', {
535
+ id: catalogVariantId,
536
+ catalogVariantId,
537
+ availableQuantity: lowStock.availableQuantity,
538
+ reorderPoint: lowStock.reorderPoint,
539
+ safetyStock: lowStock.safetyStock,
540
+ state: lowStock.state,
541
+ tenantId: scope.tenantId,
542
+ organizationId: scope.organizationId,
543
+ })
544
+ }
545
+
546
+ async function loadReceivedAtByBucket(
547
+ em: EntityManager,
548
+ balances: InventoryBalance[],
549
+ scope: Scope,
550
+ ): Promise<Map<string, Date>> {
551
+ if (balances.length === 0) return new Map()
552
+ const locationIds = Array.from(
553
+ new Set(
554
+ balances.map((balance) => (typeof balance.location === 'string' ? balance.location : balance.location.id)),
555
+ ),
556
+ )
557
+ const warehouseId = typeof balances[0].warehouse === 'string' ? balances[0].warehouse : balances[0].warehouse.id
558
+ const catalogVariantId = balances[0].catalogVariantId
559
+ const movements = await findWithDecryption(
560
+ em,
561
+ InventoryMovement,
562
+ {
563
+ warehouse: warehouseId,
564
+ catalogVariantId,
565
+ deletedAt: null,
566
+ $or: [
567
+ { locationFrom: { $in: locationIds } },
568
+ { locationTo: { $in: locationIds } },
569
+ ],
570
+ },
571
+ {
572
+ orderBy: { receivedAt: 'asc' },
573
+ },
574
+ scope,
575
+ )
576
+ const map = new Map<string, Date>()
577
+ for (const movement of movements) {
578
+ const locationIdRaw = movement.locationTo ?? movement.locationFrom ?? null
579
+ const locationId =
580
+ typeof locationIdRaw === 'string'
581
+ ? locationIdRaw
582
+ : locationIdRaw?.id ?? null
583
+ if (!locationId) continue
584
+ const lotIdRaw = movement.lot ?? null
585
+ const lotId = typeof lotIdRaw === 'string' ? lotIdRaw : lotIdRaw?.id ?? null
586
+ const key = buildBucketKey({
587
+ warehouseId,
588
+ locationId,
589
+ catalogVariantId: movement.catalogVariantId,
590
+ lotId,
591
+ serialNumber: movement.serialNumber ?? null,
592
+ })
593
+ if (!map.has(key)) {
594
+ map.set(key, movement.receivedAt)
595
+ }
596
+ }
597
+ return map
598
+ }
599
+
600
+ type BalanceSortRow = InventoryStrategyBucket & { balance: InventoryBalance }
601
+
602
+ function sortBalancesForStrategy(
603
+ balances: InventoryBalance[],
604
+ strategy: InventoryStrategy,
605
+ receiptMap: Map<string, Date>,
606
+ ): InventoryBalance[] {
607
+ const rows: BalanceSortRow[] = balances.map((balance) => {
608
+ const warehouseId = typeof balance.warehouse === 'string' ? balance.warehouse : balance.warehouse.id
609
+ const locationId = typeof balance.location === 'string' ? balance.location : balance.location.id
610
+ const lotIdRaw = balance.lot ?? null
611
+ const lotId = typeof lotIdRaw === 'string' ? lotIdRaw : lotIdRaw?.id ?? null
612
+ const lot = typeof balance.lot === 'string' ? null : balance.lot
613
+ return {
614
+ balance,
615
+ warehouseId,
616
+ locationId,
617
+ catalogVariantId: balance.catalogVariantId,
618
+ createdAt: balance.createdAt,
619
+ lotId,
620
+ lotExpiresAt: lot?.expiresAt ?? null,
621
+ serialNumber: balance.serialNumber ?? null,
622
+ }
623
+ })
624
+ const sorted = sortBucketsForStrategy(rows, strategy, receiptMap)
625
+ return sorted.map((row) => row.balance)
626
+ }
627
+
628
+ async function resolveReservationStrategy(
629
+ em: EntityManager,
630
+ ctx: CommandRuntimeContext,
631
+ input: InventoryReservationCreateInput,
632
+ scope: Scope,
633
+ ): Promise<InventoryStrategy> {
634
+ const profile = await loadProfileForVariant(em, ctx, input.catalogVariantId, scope)
635
+ return resolveReservationStrategyFromProfile(profile, input.strategy)
636
+ }
637
+
638
+ function requireSufficientAvailability(remaining: number) {
639
+ if (remaining > 0.000001) {
640
+ throw new CrudHttpError(409, { error: 'insufficient_stock' })
641
+ }
642
+ }
643
+
644
+ async function findExactBalanceForUpdate(
645
+ em: EntityManager,
646
+ scope: Scope,
647
+ input: {
648
+ warehouseId: string
649
+ locationId: string
650
+ catalogVariantId: string
651
+ lotId?: string
652
+ serialNumber?: string
653
+ },
654
+ ) {
655
+ return findOneWithDecryption(
656
+ em,
657
+ InventoryBalance,
658
+ {
659
+ warehouse: input.warehouseId,
660
+ location: input.locationId,
661
+ catalogVariantId: input.catalogVariantId,
662
+ lot: input.lotId ?? null,
663
+ serialNumber: input.serialNumber ?? null,
664
+ deletedAt: null,
665
+ },
666
+ { lockMode: LockMode.PESSIMISTIC_WRITE },
667
+ scope,
668
+ )
669
+ }
670
+
671
+ async function findReservationBucketBalance(
672
+ em: EntityManager,
673
+ scope: Scope,
674
+ reservation: InventoryReservation,
675
+ bucket: AllocationBucket,
676
+ ) {
677
+ const warehouseId =
678
+ typeof reservation.warehouse === 'string'
679
+ ? reservation.warehouse
680
+ : reservation.warehouse.id
681
+
682
+ if (typeof bucket.balanceId === 'string' && bucket.balanceId.length > 0) {
683
+ const balance = await findOneWithDecryption(
684
+ em,
685
+ InventoryBalance,
686
+ { id: bucket.balanceId, deletedAt: null },
687
+ { lockMode: LockMode.PESSIMISTIC_WRITE },
688
+ scope,
689
+ )
690
+ if (balance) return balance
691
+ }
692
+
693
+ return findExactBalanceForUpdate(em, scope, {
694
+ warehouseId,
695
+ locationId: bucket.locationId,
696
+ catalogVariantId: reservation.catalogVariantId,
697
+ lotId: bucket.lotId ?? undefined,
698
+ serialNumber: bucket.serialNumber ?? undefined,
699
+ })
700
+ }
701
+
702
+ async function upsertBalanceBucket(
703
+ em: EntityManager,
704
+ scope: Scope,
705
+ input: {
706
+ warehouseId: string
707
+ locationId: string
708
+ catalogVariantId: string
709
+ lotId?: string
710
+ serialNumber?: string
711
+ },
712
+ ): Promise<{ balance: InventoryBalance; created: boolean }> {
713
+ const existing = await findExactBalanceForUpdate(em, scope, input)
714
+ if (existing) return { balance: existing, created: false }
715
+ const balance = em.create(InventoryBalance, {
716
+ organizationId: scope.organizationId,
717
+ tenantId: scope.tenantId,
718
+ warehouse: em.getReference(Warehouse, input.warehouseId),
719
+ location: em.getReference(WarehouseLocation, input.locationId),
720
+ catalogVariantId: input.catalogVariantId,
721
+ lot: input.lotId ? em.getReference(InventoryLot, input.lotId) : null,
722
+ serialNumber: input.serialNumber ?? null,
723
+ quantityOnHand: '0',
724
+ quantityReserved: '0',
725
+ quantityAllocated: '0',
726
+ metadata: null,
727
+ })
728
+ em.persist(balance)
729
+ await em.flush()
730
+ return { balance, created: true }
731
+ }
732
+
733
+ async function resolveReceivedAtForBalance(
734
+ em: EntityManager,
735
+ balance: InventoryBalance | null,
736
+ scope: Scope,
737
+ fallback: Date,
738
+ ): Promise<Date> {
739
+ if (!balance) return fallback
740
+ const warehouseId = typeof balance.warehouse === 'string' ? balance.warehouse : balance.warehouse.id
741
+ const locationId = typeof balance.location === 'string' ? balance.location : balance.location.id
742
+ const lotIdRaw = balance.lot ?? null
743
+ const lotId = typeof lotIdRaw === 'string' ? lotIdRaw : lotIdRaw?.id ?? null
744
+ const movement = await findOneWithDecryption(
745
+ em,
746
+ InventoryMovement,
747
+ {
748
+ warehouse: warehouseId,
749
+ catalogVariantId: balance.catalogVariantId,
750
+ lot: lotId,
751
+ serialNumber: balance.serialNumber ?? null,
752
+ deletedAt: null,
753
+ $or: [
754
+ { locationFrom: locationId },
755
+ { locationTo: locationId },
756
+ ],
757
+ },
758
+ { orderBy: { receivedAt: 'asc' } },
759
+ scope,
760
+ )
761
+ return movement?.receivedAt ?? fallback
762
+ }
763
+
764
+ async function createMovement(
765
+ em: EntityManager,
766
+ scope: Scope,
767
+ input: {
768
+ warehouseId: string
769
+ locationFromId?: string | null
770
+ locationToId?: string | null
771
+ catalogVariantId: string
772
+ lotId?: string | null
773
+ serialNumber?: string | null
774
+ quantity: number
775
+ type: InventoryMovement['type']
776
+ referenceType: InventoryMovement['referenceType']
777
+ referenceId: string
778
+ performedBy: string
779
+ performedAt: Date
780
+ receivedAt: Date
781
+ reason?: string | null
782
+ reasonCode?: string | null
783
+ metadata?: Record<string, unknown> | null
784
+ idempotencyKey: string
785
+ },
786
+ ) {
787
+ const movement = em.create(InventoryMovement, {
788
+ organizationId: scope.organizationId,
789
+ tenantId: scope.tenantId,
790
+ warehouse: em.getReference(Warehouse, input.warehouseId),
791
+ locationFrom: input.locationFromId ? em.getReference(WarehouseLocation, input.locationFromId) : null,
792
+ locationTo: input.locationToId ? em.getReference(WarehouseLocation, input.locationToId) : null,
793
+ catalogVariantId: input.catalogVariantId,
794
+ lot: input.lotId ? em.getReference(InventoryLot, input.lotId) : null,
795
+ serialNumber: input.serialNumber ?? null,
796
+ quantity: toNumericString(input.quantity),
797
+ type: input.type,
798
+ referenceType: input.referenceType,
799
+ referenceId: input.referenceId,
800
+ performedBy: input.performedBy,
801
+ performedAt: input.performedAt,
802
+ receivedAt: input.receivedAt,
803
+ reason: input.reason ?? null,
804
+ reasonCode: input.reasonCode ?? null,
805
+ metadata: input.metadata ?? null,
806
+ idempotencyKey: input.idempotencyKey,
807
+ })
808
+ em.persist(movement)
809
+ return movement
810
+ }
811
+
812
+ type MovementMutationInput = {
813
+ warehouseId: string
814
+ locationFromId?: string | null
815
+ locationToId?: string | null
816
+ catalogVariantId: string
817
+ lotId?: string | null
818
+ serialNumber?: string | null
819
+ quantity: number
820
+ type: InventoryMovement['type']
821
+ referenceType: InventoryMovement['referenceType']
822
+ referenceId: string
823
+ performedBy: string
824
+ performedAt: Date
825
+ receivedAt: Date
826
+ reason?: string | null
827
+ reasonCode?: string | null
828
+ metadata?: Record<string, unknown> | null
829
+ }
830
+
831
+ async function persistMovementWithIdempotency(
832
+ em: EntityManager,
833
+ scope: Scope,
834
+ input: MovementMutationInput,
835
+ ): Promise<{ movement: InventoryMovement; idempotentReplay: boolean }> {
836
+ const idempotencyKey = buildMovementIdempotencyKey({
837
+ referenceType: input.referenceType,
838
+ referenceId: input.referenceId,
839
+ type: input.type,
840
+ warehouseId: input.warehouseId,
841
+ locationFromId: input.locationFromId,
842
+ locationToId: input.locationToId,
843
+ catalogVariantId: input.catalogVariantId,
844
+ lotId: input.lotId,
845
+ serialNumber: input.serialNumber,
846
+ quantity: input.quantity,
847
+ })
848
+ const existing = await findExistingMovementByIdempotencyKey(em, scope, idempotencyKey)
849
+ if (existing) {
850
+ return { movement: existing, idempotentReplay: true }
851
+ }
852
+ try {
853
+ const movement = await createMovement(em, scope, {
854
+ ...input,
855
+ idempotencyKey,
856
+ })
857
+ return { movement, idempotentReplay: false }
858
+ } catch (error) {
859
+ if (!isUniqueConstraintError(error)) throw error
860
+ const raced = await findExistingMovementByIdempotencyKey(em, scope, idempotencyKey)
861
+ if (!raced) throw error
862
+ return { movement: raced, idempotentReplay: true }
863
+ }
864
+ }
865
+
866
+ async function emitBalanceDriftIfNeeded(
867
+ balance: InventoryBalance,
868
+ field: 'quantityReserved' | 'quantityAllocated',
869
+ attemptedValue: number,
870
+ scope: Scope,
871
+ reservationId: string,
872
+ ) {
873
+ if (attemptedValue >= -0.000001) return
874
+ await emitWmsEvent('wms.inventory.balance_drift', {
875
+ id: balance.id,
876
+ balanceId: balance.id,
877
+ reservationId,
878
+ field,
879
+ attemptedValue: toNumericString(attemptedValue),
880
+ clampedValue: '0',
881
+ tenantId: scope.tenantId,
882
+ organizationId: scope.organizationId,
883
+ }).catch(() => undefined)
884
+ }
885
+
886
+ function enforceNonNegativeBalance(
887
+ balance: InventoryBalance,
888
+ field: 'quantityReserved' | 'quantityAllocated',
889
+ nextValue: number,
890
+ reservationId: string,
891
+ scope: Scope,
892
+ ) {
893
+ if (nextValue < -0.000001) {
894
+ void emitBalanceDriftIfNeeded(balance, field, nextValue, scope, reservationId)
895
+ throw new CrudHttpError(409, { error: 'balance_integrity_violation' })
896
+ }
897
+ setNumeric(balance as unknown as Record<string, unknown>, field, nextValue)
898
+ }
899
+
900
+ const reserveInventoryCommand: CommandHandler<InventoryReservationCreateInput, ReservationCommandResult> = {
901
+ id: 'wms.inventory.reserve',
902
+ // See "WMS Inventory Mutation Commands — Undo Policy" at top of file.
903
+ isUndoable: false,
904
+ async execute(rawInput, ctx) {
905
+ const input = inventoryReservationCreateSchema.parse(rawInput ?? {})
906
+ ensureTenantScope(ctx, input.tenantId)
907
+ ensureOrganizationScope(ctx, input.organizationId)
908
+ const em = resolveEm(ctx)
909
+ const result = await runInTransaction(em, async (trx) => {
910
+ const scope = resolveScope(ctx, input)
911
+ await requireWarehouse(trx, ctx, input.warehouseId, scope)
912
+ const reservationIdempotencyKey = buildReservationIdempotencyKey({
913
+ sourceType: input.sourceType,
914
+ sourceId: input.sourceId,
915
+ catalogVariantId: input.catalogVariantId,
916
+ warehouseId: input.warehouseId,
917
+ quantity: input.quantity,
918
+ })
919
+ const existingReservation = await findExistingActiveReservationByIdempotencyKey(
920
+ trx,
921
+ scope,
922
+ reservationIdempotencyKey,
923
+ )
924
+ if (existingReservation) {
925
+ const metadata = extractReservationMetadata(existingReservation)
926
+ const buckets = Array.isArray(metadata.allocatedBuckets)
927
+ ? (metadata.allocatedBuckets as AllocationBucket[])
928
+ : []
929
+ return {
930
+ reservationId: existingReservation.id,
931
+ allocatedBuckets: serializeAllocationBuckets(buckets),
932
+ tenantId: scope.tenantId,
933
+ organizationId: scope.organizationId,
934
+ warehouseId: input.warehouseId,
935
+ catalogVariantId: input.catalogVariantId,
936
+ quantity: input.quantity,
937
+ reservationEntity: existingReservation,
938
+ touchedBalances: [] as InventoryBalance[],
939
+ idempotentReplay: true,
940
+ }
941
+ }
942
+ const strategy = await resolveReservationStrategy(trx, ctx, input, scope)
943
+ const balances = await listCandidateBalances(trx, ctx, input, scope)
944
+ const receiptMap = await loadReceivedAtByBucket(trx, balances, scope)
945
+ const now = new Date()
946
+ const ordered = sortBalancesForStrategy(
947
+ balances.filter(
948
+ (balance) => balanceHasEligibleLot(balance, now) && getAvailableQuantity(balance) > 0,
949
+ ),
950
+ strategy,
951
+ receiptMap,
952
+ )
953
+ let remaining = input.quantity
954
+ const buckets: AllocationBucket[] = []
955
+ const touchedBalances: InventoryBalance[] = []
956
+ for (const balance of ordered) {
957
+ if (remaining <= 0) break
958
+ const available = getAvailableQuantity(balance)
959
+ if (available <= 0) continue
960
+ const quantity = Math.min(available, remaining)
961
+ const locationId = typeof balance.location === 'string' ? balance.location : balance.location.id
962
+ const lotIdRaw = balance.lot ?? null
963
+ const lotId = typeof lotIdRaw === 'string' ? lotIdRaw : lotIdRaw?.id ?? null
964
+ const persistedBalance = await findExactBalanceForUpdate(trx, scope, {
965
+ warehouseId: input.warehouseId,
966
+ locationId,
967
+ catalogVariantId: input.catalogVariantId,
968
+ lotId: lotId ?? undefined,
969
+ serialNumber: balance.serialNumber ?? undefined,
970
+ })
971
+ if (!persistedBalance) {
972
+ throw new CrudHttpError(409, { error: 'invalid_tracking_state' })
973
+ }
974
+ setNumeric(
975
+ persistedBalance as unknown as Record<string, unknown>,
976
+ 'quantityReserved',
977
+ toNumber(persistedBalance.quantityReserved) + quantity,
978
+ )
979
+ touchedBalances.push(persistedBalance)
980
+ buckets.push({
981
+ balanceId: persistedBalance.id,
982
+ locationId,
983
+ lotId,
984
+ serialNumber: balance.serialNumber ?? null,
985
+ quantity,
986
+ })
987
+ remaining -= quantity
988
+ }
989
+ requireSufficientAvailability(remaining)
990
+ let reservation: InventoryReservation
991
+ try {
992
+ reservation = trx.create(InventoryReservation, {
993
+ organizationId: scope.organizationId,
994
+ tenantId: scope.tenantId,
995
+ warehouse: trx.getReference(Warehouse, input.warehouseId),
996
+ catalogVariantId: input.catalogVariantId,
997
+ lot: buckets.length === 1 && buckets[0].lotId ? trx.getReference(InventoryLot, buckets[0].lotId) : null,
998
+ serialNumber: buckets.length === 1 ? buckets[0].serialNumber : null,
999
+ quantity: toNumericString(input.quantity),
1000
+ sourceType: input.sourceType,
1001
+ sourceId: input.sourceId,
1002
+ expiresAt: input.expiresAt ?? null,
1003
+ status: 'active',
1004
+ idempotencyKey: reservationIdempotencyKey,
1005
+ metadata: {
1006
+ ...(input.metadata ?? {}),
1007
+ allocatedBuckets: buckets,
1008
+ allocationState: 'reserved',
1009
+ strategy,
1010
+ },
1011
+ })
1012
+ trx.persist(reservation)
1013
+ await trx.flush()
1014
+ } catch (error) {
1015
+ if (!isUniqueConstraintError(error)) throw error
1016
+ const raced = await findExistingActiveReservationByIdempotencyKey(
1017
+ trx,
1018
+ scope,
1019
+ reservationIdempotencyKey,
1020
+ )
1021
+ if (!raced) throw error
1022
+ const metadata = extractReservationMetadata(raced)
1023
+ const racedBuckets = Array.isArray(metadata.allocatedBuckets)
1024
+ ? (metadata.allocatedBuckets as AllocationBucket[])
1025
+ : []
1026
+ return {
1027
+ reservationId: raced.id,
1028
+ allocatedBuckets: serializeAllocationBuckets(racedBuckets),
1029
+ tenantId: scope.tenantId,
1030
+ organizationId: scope.organizationId,
1031
+ warehouseId: input.warehouseId,
1032
+ catalogVariantId: input.catalogVariantId,
1033
+ quantity: input.quantity,
1034
+ reservationEntity: raced,
1035
+ touchedBalances: [] as InventoryBalance[],
1036
+ idempotentReplay: true,
1037
+ }
1038
+ }
1039
+ return {
1040
+ reservationId: reservation.id,
1041
+ allocatedBuckets: serializeAllocationBuckets(buckets),
1042
+ tenantId: scope.tenantId,
1043
+ organizationId: scope.organizationId,
1044
+ warehouseId: input.warehouseId,
1045
+ catalogVariantId: input.catalogVariantId,
1046
+ quantity: input.quantity,
1047
+ reservationEntity: reservation,
1048
+ touchedBalances,
1049
+ idempotentReplay: false,
1050
+ }
1051
+ })
1052
+ if (!result.idempotentReplay) {
1053
+ await emitInventorySideEffects(ctx, {
1054
+ reservations: [{ entity: result.reservationEntity, action: 'created' }],
1055
+ balances: result.touchedBalances.map((entity) => ({ entity, action: 'updated' as const })),
1056
+ })
1057
+ void emitWmsEvent('wms.inventory.reserved', {
1058
+ id: result.reservationId,
1059
+ reservationId: result.reservationId,
1060
+ warehouseId: result.warehouseId,
1061
+ catalogVariantId: result.catalogVariantId,
1062
+ quantity: toNumericString(result.quantity),
1063
+ tenantId: result.tenantId,
1064
+ organizationId: result.organizationId,
1065
+ }).catch(() => undefined)
1066
+ void emitLowStockEventIfNeeded(
1067
+ resolveEm(ctx),
1068
+ ctx,
1069
+ { tenantId: result.tenantId, organizationId: result.organizationId },
1070
+ result.catalogVariantId,
1071
+ ).catch(() => undefined)
1072
+ }
1073
+ return {
1074
+ reservationId: result.reservationId,
1075
+ allocatedBuckets: result.allocatedBuckets,
1076
+ }
1077
+ },
1078
+ buildLog: async ({ input, result, ctx }) =>
1079
+ buildMutationLog({
1080
+ actionKey: 'wms.audit.inventory.reserve',
1081
+ fallbackLabel: 'Reserve inventory',
1082
+ resourceKind: WMS_INVENTORY_RESERVATION_RESOURCE,
1083
+ resourceId: result?.reservationId ?? null,
1084
+ parentResourceId:
1085
+ input?.warehouseId && input?.catalogVariantId
1086
+ ? `${input.warehouseId}:${input.catalogVariantId}`
1087
+ : null,
1088
+ tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
1089
+ organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
1090
+ cacheAliases: [WMS_INVENTORY_BALANCE_RESOURCE],
1091
+ }),
1092
+ }
1093
+
1094
+ const releaseInventoryReservationCommand: CommandHandler<InventoryReservationReleaseInput, { reservationId: string }> = {
1095
+ id: 'wms.inventory.release',
1096
+ // See "WMS Inventory Mutation Commands — Undo Policy" at top of file.
1097
+ isUndoable: false,
1098
+ async execute(rawInput, ctx) {
1099
+ const input = inventoryReservationReleaseSchema.parse(rawInput ?? {})
1100
+ ensureTenantScope(ctx, input.tenantId)
1101
+ ensureOrganizationScope(ctx, input.organizationId)
1102
+ const em = resolveEm(ctx)
1103
+ const result = await runInTransaction(em, async (trx) => {
1104
+ const scope = resolveScope(ctx, input)
1105
+ const reservation = await requireReservation(trx, ctx, input.reservationId, scope, true)
1106
+ const metadata = extractReservationMetadata(reservation)
1107
+ const buckets = Array.isArray(metadata.allocatedBuckets) ? (metadata.allocatedBuckets as AllocationBucket[]) : []
1108
+ const allocationState = metadata.allocationState ?? 'reserved'
1109
+ const touchedBalances: InventoryBalance[] = []
1110
+ for (const bucket of buckets) {
1111
+ const balance = await findReservationBucketBalance(
1112
+ trx,
1113
+ scope,
1114
+ reservation,
1115
+ bucket,
1116
+ )
1117
+ if (!balance) continue
1118
+ if (allocationState === 'allocated') {
1119
+ enforceNonNegativeBalance(
1120
+ balance,
1121
+ 'quantityAllocated',
1122
+ toNumber(balance.quantityAllocated) - bucket.quantity,
1123
+ reservation.id,
1124
+ scope,
1125
+ )
1126
+ } else {
1127
+ enforceNonNegativeBalance(
1128
+ balance,
1129
+ 'quantityReserved',
1130
+ toNumber(balance.quantityReserved) - bucket.quantity,
1131
+ reservation.id,
1132
+ scope,
1133
+ )
1134
+ }
1135
+ touchedBalances.push(balance)
1136
+ }
1137
+ reservation.status = 'released'
1138
+ reservation.metadata = {
1139
+ ...metadata,
1140
+ releasedReason: input.reason,
1141
+ releasedAt: new Date().toISOString(),
1142
+ }
1143
+ await trx.flush()
1144
+ return {
1145
+ reservationId: reservation.id,
1146
+ warehouseId: typeof reservation.warehouse === 'string' ? reservation.warehouse : reservation.warehouse.id,
1147
+ catalogVariantId: reservation.catalogVariantId,
1148
+ quantity: reservation.quantity,
1149
+ tenantId: reservation.tenantId,
1150
+ organizationId: reservation.organizationId,
1151
+ reservationEntity: reservation,
1152
+ touchedBalances,
1153
+ }
1154
+ })
1155
+ await emitInventorySideEffects(ctx, {
1156
+ reservations: [{ entity: result.reservationEntity, action: 'updated' }],
1157
+ balances: result.touchedBalances.map((entity) => ({ entity, action: 'updated' as const })),
1158
+ })
1159
+ void emitWmsEvent('wms.inventory.released', {
1160
+ id: result.reservationId,
1161
+ reservationId: result.reservationId,
1162
+ warehouseId: result.warehouseId,
1163
+ catalogVariantId: result.catalogVariantId,
1164
+ quantity: result.quantity,
1165
+ tenantId: result.tenantId,
1166
+ organizationId: result.organizationId,
1167
+ }).catch(() => undefined)
1168
+ void emitLowStockEventIfNeeded(
1169
+ resolveEm(ctx),
1170
+ ctx,
1171
+ { tenantId: result.tenantId, organizationId: result.organizationId },
1172
+ result.catalogVariantId,
1173
+ ).catch(() => undefined)
1174
+ return { reservationId: result.reservationId }
1175
+ },
1176
+ buildLog: async ({ input, result, ctx }) =>
1177
+ buildMutationLog({
1178
+ actionKey: 'wms.audit.inventory.release',
1179
+ fallbackLabel: 'Release inventory reservation',
1180
+ resourceKind: WMS_INVENTORY_RESERVATION_RESOURCE,
1181
+ resourceId: result?.reservationId ?? input?.reservationId ?? null,
1182
+ tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
1183
+ organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
1184
+ cacheAliases: [WMS_INVENTORY_BALANCE_RESOURCE],
1185
+ }),
1186
+ }
1187
+
1188
+ const allocateInventoryReservationCommand: CommandHandler<InventoryReservationAllocateInput, { reservationId: string; allocationState: 'allocated' }> = {
1189
+ id: 'wms.inventory.allocate',
1190
+ // See "WMS Inventory Mutation Commands — Undo Policy" at top of file.
1191
+ isUndoable: false,
1192
+ async execute(rawInput, ctx) {
1193
+ const input = inventoryReservationAllocateSchema.parse(rawInput ?? {})
1194
+ ensureTenantScope(ctx, input.tenantId)
1195
+ ensureOrganizationScope(ctx, input.organizationId)
1196
+ const em = resolveEm(ctx)
1197
+ const result = await runInTransaction(em, async (trx) => {
1198
+ const scope = resolveScope(ctx, input)
1199
+ const reservation = await requireReservation(trx, ctx, input.reservationId, scope, true)
1200
+ const metadata = extractReservationMetadata(reservation)
1201
+ if (metadata.allocationState === 'allocated') {
1202
+ return {
1203
+ reservationId: reservation.id,
1204
+ allocationState: 'allocated' as const,
1205
+ warehouseId: typeof reservation.warehouse === 'string' ? reservation.warehouse : reservation.warehouse.id,
1206
+ catalogVariantId: reservation.catalogVariantId,
1207
+ quantity: reservation.quantity,
1208
+ tenantId: reservation.tenantId,
1209
+ organizationId: reservation.organizationId,
1210
+ reservationEntity: reservation,
1211
+ touchedBalances: [] as InventoryBalance[],
1212
+ }
1213
+ }
1214
+ const buckets = Array.isArray(metadata.allocatedBuckets) ? (metadata.allocatedBuckets as AllocationBucket[]) : []
1215
+ const touchedBalances: InventoryBalance[] = []
1216
+ for (const bucket of buckets) {
1217
+ const balance = await findReservationBucketBalance(
1218
+ trx,
1219
+ scope,
1220
+ reservation,
1221
+ bucket,
1222
+ )
1223
+ if (!balance) throw new CrudHttpError(409, { error: 'invalid_tracking_state' })
1224
+ if (toNumber(balance.quantityReserved) < bucket.quantity - 0.000001) {
1225
+ throw new CrudHttpError(409, { error: 'invalid_tracking_state' })
1226
+ }
1227
+ // Guard above ensures the subtraction is non-negative within numeric(16,4) precision.
1228
+ setNumeric(
1229
+ balance as unknown as Record<string, unknown>,
1230
+ 'quantityReserved',
1231
+ toNumber(balance.quantityReserved) - bucket.quantity,
1232
+ )
1233
+ setNumeric(
1234
+ balance as unknown as Record<string, unknown>,
1235
+ 'quantityAllocated',
1236
+ toNumber(balance.quantityAllocated) + bucket.quantity,
1237
+ )
1238
+ touchedBalances.push(balance)
1239
+ }
1240
+ reservation.metadata = {
1241
+ ...metadata,
1242
+ allocationState: 'allocated',
1243
+ allocatedAt: new Date().toISOString(),
1244
+ }
1245
+ await trx.flush()
1246
+ return {
1247
+ reservationId: reservation.id,
1248
+ allocationState: 'allocated' as const,
1249
+ warehouseId: typeof reservation.warehouse === 'string' ? reservation.warehouse : reservation.warehouse.id,
1250
+ catalogVariantId: reservation.catalogVariantId,
1251
+ quantity: reservation.quantity,
1252
+ tenantId: reservation.tenantId,
1253
+ organizationId: reservation.organizationId,
1254
+ reservationEntity: reservation,
1255
+ touchedBalances,
1256
+ }
1257
+ })
1258
+ await emitInventorySideEffects(ctx, {
1259
+ reservations: [{ entity: result.reservationEntity, action: 'updated' }],
1260
+ balances: result.touchedBalances.map((entity) => ({ entity, action: 'updated' as const })),
1261
+ })
1262
+ void emitWmsEvent('wms.inventory.allocated', {
1263
+ id: result.reservationId,
1264
+ reservationId: result.reservationId,
1265
+ warehouseId: result.warehouseId,
1266
+ catalogVariantId: result.catalogVariantId,
1267
+ quantity: result.quantity,
1268
+ tenantId: result.tenantId,
1269
+ organizationId: result.organizationId,
1270
+ }).catch(() => undefined)
1271
+ return { reservationId: result.reservationId, allocationState: result.allocationState }
1272
+ },
1273
+ buildLog: async ({ input, result, ctx }) =>
1274
+ buildMutationLog({
1275
+ actionKey: 'wms.audit.inventory.allocate',
1276
+ fallbackLabel: 'Allocate inventory reservation',
1277
+ resourceKind: WMS_INVENTORY_RESERVATION_RESOURCE,
1278
+ resourceId: result?.reservationId ?? input?.reservationId ?? null,
1279
+ tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
1280
+ organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
1281
+ cacheAliases: [WMS_INVENTORY_BALANCE_RESOURCE],
1282
+ }),
1283
+ }
1284
+
1285
+ const adjustInventoryCommand: CommandHandler<InventoryAdjustInput, { movementId: string }> = {
1286
+ id: 'wms.inventory.adjust',
1287
+ // See "WMS Inventory Mutation Commands — Undo Policy" at top of file.
1288
+ isUndoable: false,
1289
+ async execute(rawInput, ctx) {
1290
+ const input = inventoryAdjustSchema.parse(rawInput ?? {})
1291
+ ensureTenantScope(ctx, input.tenantId)
1292
+ ensureOrganizationScope(ctx, input.organizationId)
1293
+ const em = resolveEm(ctx)
1294
+ const result = await runInTransaction(em, async (trx) => {
1295
+ const scope = resolveScope(ctx, input)
1296
+ await requireWarehouse(trx, ctx, input.warehouseId, scope)
1297
+ const location = await requireLocation(trx, ctx, input.locationId, scope)
1298
+ const locationWarehouseId = typeof location.warehouse === 'string' ? location.warehouse : location.warehouse.id
1299
+ if (locationWarehouseId !== input.warehouseId) {
1300
+ throw new CrudHttpError(422, { error: 'invalid_location' })
1301
+ }
1302
+ const { balance, created: balanceWasNew } = await upsertBalanceBucket(trx, scope, {
1303
+ warehouseId: input.warehouseId,
1304
+ locationId: input.locationId,
1305
+ catalogVariantId: input.catalogVariantId,
1306
+ lotId: input.lotId,
1307
+ serialNumber: input.serialNumber,
1308
+ })
1309
+ const delta = input.delta
1310
+ const performedAt = input.performedAt ?? new Date()
1311
+ const movementInput: MovementMutationInput = {
1312
+ warehouseId: input.warehouseId,
1313
+ locationToId: input.locationId,
1314
+ catalogVariantId: input.catalogVariantId,
1315
+ lotId: input.lotId ?? null,
1316
+ serialNumber: input.serialNumber ?? null,
1317
+ quantity: delta,
1318
+ type: 'adjust',
1319
+ referenceType: input.referenceType,
1320
+ referenceId: input.referenceId,
1321
+ performedBy: input.performedBy,
1322
+ performedAt,
1323
+ receivedAt: performedAt,
1324
+ reason: input.reason,
1325
+ reasonCode: input.reasonCode ?? null,
1326
+ metadata: input.metadata ?? null,
1327
+ }
1328
+ const idempotencyKey = buildMovementIdempotencyKey({
1329
+ referenceType: movementInput.referenceType,
1330
+ referenceId: movementInput.referenceId,
1331
+ type: movementInput.type,
1332
+ warehouseId: movementInput.warehouseId,
1333
+ locationFromId: movementInput.locationFromId,
1334
+ locationToId: movementInput.locationToId,
1335
+ catalogVariantId: movementInput.catalogVariantId,
1336
+ lotId: movementInput.lotId,
1337
+ serialNumber: movementInput.serialNumber,
1338
+ quantity: movementInput.quantity,
1339
+ })
1340
+ const existingMovement = await findExistingMovementByIdempotencyKey(trx, scope, idempotencyKey)
1341
+ if (existingMovement) {
1342
+ return {
1343
+ movementId: existingMovement.id,
1344
+ warehouseId: input.warehouseId,
1345
+ catalogVariantId: input.catalogVariantId,
1346
+ quantity: delta,
1347
+ tenantId: scope.tenantId,
1348
+ organizationId: scope.organizationId,
1349
+ movementEntity: existingMovement,
1350
+ balanceEntity: balance,
1351
+ balanceAction: ('updated' as const),
1352
+ idempotentReplay: true,
1353
+ }
1354
+ }
1355
+ if (delta < 0 && getAvailableQuantity(balance) < Math.abs(delta) - 0.000001) {
1356
+ throw new CrudHttpError(409, { error: 'insufficient_stock' })
1357
+ }
1358
+ setNumeric(
1359
+ balance as unknown as Record<string, unknown>,
1360
+ 'quantityOnHand',
1361
+ toNumber(balance.quantityOnHand) + delta,
1362
+ )
1363
+ const { movement } = await persistMovementWithIdempotency(trx, scope, movementInput)
1364
+ await trx.flush()
1365
+ return {
1366
+ movementId: movement.id,
1367
+ warehouseId: input.warehouseId,
1368
+ catalogVariantId: input.catalogVariantId,
1369
+ quantity: delta,
1370
+ tenantId: scope.tenantId,
1371
+ organizationId: scope.organizationId,
1372
+ movementEntity: movement,
1373
+ balanceEntity: balance,
1374
+ balanceAction: balanceWasNew ? ('created' as const) : ('updated' as const),
1375
+ idempotentReplay: false,
1376
+ }
1377
+ })
1378
+ if (!result.idempotentReplay) {
1379
+ await emitInventorySideEffects(ctx, {
1380
+ movements: [{ entity: result.movementEntity, action: 'created' }],
1381
+ balances: [{ entity: result.balanceEntity, action: result.balanceAction }],
1382
+ })
1383
+ void emitWmsEvent('wms.inventory.adjusted', {
1384
+ id: result.movementId,
1385
+ movementId: result.movementId,
1386
+ warehouseId: result.warehouseId,
1387
+ catalogVariantId: result.catalogVariantId,
1388
+ quantity: toNumericString(result.quantity),
1389
+ tenantId: result.tenantId,
1390
+ organizationId: result.organizationId,
1391
+ }).catch(() => undefined)
1392
+ void emitLowStockEventIfNeeded(
1393
+ resolveEm(ctx),
1394
+ ctx,
1395
+ { tenantId: result.tenantId, organizationId: result.organizationId },
1396
+ result.catalogVariantId,
1397
+ ).catch(() => undefined)
1398
+ }
1399
+ return { movementId: result.movementId }
1400
+ },
1401
+ buildLog: async ({ input, result, ctx }) =>
1402
+ buildMutationLog({
1403
+ actionKey: 'wms.audit.inventory.adjust',
1404
+ fallbackLabel: 'Adjust inventory',
1405
+ resourceKind: WMS_INVENTORY_MOVEMENT_RESOURCE,
1406
+ resourceId: result?.movementId ?? null,
1407
+ parentResourceId:
1408
+ input?.warehouseId && input?.catalogVariantId
1409
+ ? `${input.warehouseId}:${input.catalogVariantId}`
1410
+ : null,
1411
+ tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
1412
+ organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
1413
+ cacheAliases: [WMS_INVENTORY_BALANCE_RESOURCE],
1414
+ }),
1415
+ }
1416
+
1417
+ const receiveInventoryCommand: CommandHandler<InventoryReceiveInput, { movementId: string }> = {
1418
+ id: 'wms.inventory.receive',
1419
+ // See "WMS Inventory Mutation Commands — Undo Policy" at top of file.
1420
+ isUndoable: false,
1421
+ async execute(rawInput, ctx) {
1422
+ const input = inventoryReceiveSchema.parse(rawInput ?? {})
1423
+ ensureTenantScope(ctx, input.tenantId)
1424
+ ensureOrganizationScope(ctx, input.organizationId)
1425
+ const em = resolveEm(ctx)
1426
+ const result = await runInTransaction(em, async (trx) => {
1427
+ const scope = resolveScope(ctx, input)
1428
+ await requireWarehouse(trx, ctx, input.warehouseId, scope)
1429
+ const location = await requireLocation(trx, ctx, input.locationId, scope)
1430
+ const locationWarehouseId = typeof location.warehouse === 'string' ? location.warehouse : location.warehouse.id
1431
+ if (locationWarehouseId !== input.warehouseId) {
1432
+ throw new CrudHttpError(422, { error: 'invalid_location' })
1433
+ }
1434
+ const profile = await loadProfileForVariant(trx, ctx, input.catalogVariantId, scope)
1435
+ const resolvedLotId = await resolveReceiveLotId(trx, ctx, scope, input)
1436
+ enforceInventoryTrackingRequirements(profile, {
1437
+ lotId: resolvedLotId ?? null,
1438
+ serialNumber: input.serialNumber ?? null,
1439
+ })
1440
+ const { balance, created: balanceWasNew } = await upsertBalanceBucket(trx, scope, {
1441
+ warehouseId: input.warehouseId,
1442
+ locationId: input.locationId,
1443
+ catalogVariantId: input.catalogVariantId,
1444
+ lotId: resolvedLotId,
1445
+ serialNumber: input.serialNumber,
1446
+ })
1447
+ const receivedAt = input.receivedAt ?? input.performedAt ?? new Date()
1448
+ const performedAt = input.performedAt ?? receivedAt
1449
+ const movementInput: MovementMutationInput = {
1450
+ warehouseId: input.warehouseId,
1451
+ locationToId: input.locationId,
1452
+ catalogVariantId: input.catalogVariantId,
1453
+ lotId: resolvedLotId ?? null,
1454
+ serialNumber: input.serialNumber ?? null,
1455
+ quantity: input.quantity,
1456
+ type: 'receipt',
1457
+ referenceType: input.referenceType,
1458
+ referenceId: input.referenceId,
1459
+ performedBy: input.performedBy,
1460
+ performedAt,
1461
+ receivedAt,
1462
+ reason: input.reason,
1463
+ metadata: input.metadata ?? null,
1464
+ }
1465
+ const idempotencyKey = buildMovementIdempotencyKey({
1466
+ referenceType: movementInput.referenceType,
1467
+ referenceId: movementInput.referenceId,
1468
+ type: movementInput.type,
1469
+ warehouseId: movementInput.warehouseId,
1470
+ locationFromId: movementInput.locationFromId,
1471
+ locationToId: movementInput.locationToId,
1472
+ catalogVariantId: movementInput.catalogVariantId,
1473
+ lotId: movementInput.lotId,
1474
+ serialNumber: movementInput.serialNumber,
1475
+ quantity: movementInput.quantity,
1476
+ })
1477
+ const existingMovement = await findExistingMovementByIdempotencyKey(trx, scope, idempotencyKey)
1478
+ if (existingMovement) {
1479
+ return {
1480
+ movementId: existingMovement.id,
1481
+ warehouseId: input.warehouseId,
1482
+ locationId: input.locationId,
1483
+ catalogVariantId: input.catalogVariantId,
1484
+ quantity: input.quantity,
1485
+ tenantId: scope.tenantId,
1486
+ organizationId: scope.organizationId,
1487
+ movementEntity: existingMovement,
1488
+ balanceEntity: balance,
1489
+ balanceAction: ('updated' as const),
1490
+ idempotentReplay: true,
1491
+ }
1492
+ }
1493
+ setNumeric(
1494
+ balance as unknown as Record<string, unknown>,
1495
+ 'quantityOnHand',
1496
+ toNumber(balance.quantityOnHand) + input.quantity,
1497
+ )
1498
+ const { movement } = await persistMovementWithIdempotency(trx, scope, movementInput)
1499
+ await trx.flush()
1500
+ return {
1501
+ movementId: movement.id,
1502
+ warehouseId: input.warehouseId,
1503
+ locationId: input.locationId,
1504
+ catalogVariantId: input.catalogVariantId,
1505
+ quantity: input.quantity,
1506
+ tenantId: scope.tenantId,
1507
+ organizationId: scope.organizationId,
1508
+ movementEntity: movement,
1509
+ balanceEntity: balance,
1510
+ balanceAction: balanceWasNew ? ('created' as const) : ('updated' as const),
1511
+ idempotentReplay: false,
1512
+ }
1513
+ })
1514
+ if (!result.idempotentReplay) {
1515
+ await emitInventorySideEffects(ctx, {
1516
+ movements: [{ entity: result.movementEntity, action: 'created' }],
1517
+ balances: [{ entity: result.balanceEntity, action: result.balanceAction }],
1518
+ })
1519
+ void emitWmsEvent('wms.inventory.received', {
1520
+ id: result.movementId,
1521
+ movementId: result.movementId,
1522
+ warehouseId: result.warehouseId,
1523
+ locationId: result.locationId,
1524
+ catalogVariantId: result.catalogVariantId,
1525
+ quantity: toNumericString(result.quantity),
1526
+ tenantId: result.tenantId,
1527
+ organizationId: result.organizationId,
1528
+ }).catch(() => undefined)
1529
+ void emitLowStockEventIfNeeded(
1530
+ resolveEm(ctx),
1531
+ ctx,
1532
+ { tenantId: result.tenantId, organizationId: result.organizationId },
1533
+ result.catalogVariantId,
1534
+ ).catch(() => undefined)
1535
+ }
1536
+ return { movementId: result.movementId }
1537
+ },
1538
+ buildLog: async ({ input, result, ctx }) =>
1539
+ buildMutationLog({
1540
+ actionKey: 'wms.audit.inventory.receive',
1541
+ fallbackLabel: 'Receive inventory',
1542
+ resourceKind: WMS_INVENTORY_MOVEMENT_RESOURCE,
1543
+ resourceId: result?.movementId ?? null,
1544
+ parentResourceId:
1545
+ input?.warehouseId && input?.catalogVariantId
1546
+ ? `${input.warehouseId}:${input.catalogVariantId}`
1547
+ : null,
1548
+ tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
1549
+ organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
1550
+ cacheAliases: [WMS_INVENTORY_BALANCE_RESOURCE],
1551
+ }),
1552
+ }
1553
+
1554
+ const moveInventoryCommand: CommandHandler<InventoryMoveInput, { movementId: string }> = {
1555
+ id: 'wms.inventory.move',
1556
+ // See "WMS Inventory Mutation Commands — Undo Policy" at top of file.
1557
+ isUndoable: false,
1558
+ async execute(rawInput, ctx) {
1559
+ const input = inventoryMoveSchema.parse(rawInput ?? {})
1560
+ ensureTenantScope(ctx, input.tenantId)
1561
+ ensureOrganizationScope(ctx, input.organizationId)
1562
+ const em = resolveEm(ctx)
1563
+ const result = await runInTransaction(em, async (trx) => {
1564
+ const scope = resolveScope(ctx, input)
1565
+ await requireWarehouse(trx, ctx, input.warehouseId, scope)
1566
+ const sourceLocation = await requireLocation(trx, ctx, input.fromLocationId, scope)
1567
+ const targetLocation = await requireLocation(trx, ctx, input.toLocationId, scope)
1568
+ const sourceWarehouseId = typeof sourceLocation.warehouse === 'string' ? sourceLocation.warehouse : sourceLocation.warehouse.id
1569
+ const targetWarehouseId = typeof targetLocation.warehouse === 'string' ? targetLocation.warehouse : targetLocation.warehouse.id
1570
+ if (sourceWarehouseId !== input.warehouseId || targetWarehouseId !== input.warehouseId) {
1571
+ throw new CrudHttpError(422, { error: 'invalid_location' })
1572
+ }
1573
+ const sourceResult = await upsertBalanceBucket(trx, scope, {
1574
+ warehouseId: input.warehouseId,
1575
+ locationId: input.fromLocationId,
1576
+ catalogVariantId: input.catalogVariantId,
1577
+ lotId: input.lotId,
1578
+ serialNumber: input.serialNumber,
1579
+ })
1580
+ const sourceBalance = sourceResult.balance
1581
+ const sourceBalanceAction = sourceResult.created ? ('created' as const) : ('updated' as const)
1582
+ const targetResult = await upsertBalanceBucket(trx, scope, {
1583
+ warehouseId: input.warehouseId,
1584
+ locationId: input.toLocationId,
1585
+ catalogVariantId: input.catalogVariantId,
1586
+ lotId: input.lotId,
1587
+ serialNumber: input.serialNumber,
1588
+ })
1589
+ const targetBalance = targetResult.balance
1590
+ const targetBalanceAction = targetResult.created ? ('created' as const) : ('updated' as const)
1591
+ const performedAt = input.performedAt ?? new Date()
1592
+ const receivedAt = await resolveReceivedAtForBalance(trx, sourceBalance, scope, performedAt)
1593
+ const movementInput: MovementMutationInput = {
1594
+ warehouseId: input.warehouseId,
1595
+ locationFromId: input.fromLocationId,
1596
+ locationToId: input.toLocationId,
1597
+ catalogVariantId: input.catalogVariantId,
1598
+ lotId: input.lotId ?? null,
1599
+ serialNumber: input.serialNumber ?? null,
1600
+ quantity: input.quantity,
1601
+ type: input.type ?? 'transfer',
1602
+ referenceType: input.referenceType,
1603
+ referenceId: input.referenceId,
1604
+ performedBy: input.performedBy,
1605
+ performedAt,
1606
+ receivedAt,
1607
+ reason: input.reason,
1608
+ reasonCode: input.reasonCode ?? null,
1609
+ metadata: input.metadata ?? null,
1610
+ }
1611
+ const idempotencyKey = buildMovementIdempotencyKey({
1612
+ referenceType: movementInput.referenceType,
1613
+ referenceId: movementInput.referenceId,
1614
+ type: movementInput.type,
1615
+ warehouseId: movementInput.warehouseId,
1616
+ locationFromId: movementInput.locationFromId,
1617
+ locationToId: movementInput.locationToId,
1618
+ catalogVariantId: movementInput.catalogVariantId,
1619
+ lotId: movementInput.lotId,
1620
+ serialNumber: movementInput.serialNumber,
1621
+ quantity: movementInput.quantity,
1622
+ })
1623
+ const existingMovement = await findExistingMovementByIdempotencyKey(trx, scope, idempotencyKey)
1624
+ if (existingMovement) {
1625
+ return {
1626
+ movementId: existingMovement.id,
1627
+ warehouseId: input.warehouseId,
1628
+ catalogVariantId: input.catalogVariantId,
1629
+ quantity: input.quantity,
1630
+ tenantId: scope.tenantId,
1631
+ organizationId: scope.organizationId,
1632
+ movementEntity: existingMovement,
1633
+ balances: [
1634
+ { entity: sourceBalance, action: sourceBalanceAction },
1635
+ { entity: targetBalance, action: targetBalanceAction },
1636
+ ] as AffectedBalance[],
1637
+ idempotentReplay: true,
1638
+ }
1639
+ }
1640
+ if (getAvailableQuantity(sourceBalance) < input.quantity - 0.000001) {
1641
+ throw new CrudHttpError(409, { error: 'insufficient_stock' })
1642
+ }
1643
+ setNumeric(
1644
+ sourceBalance as unknown as Record<string, unknown>,
1645
+ 'quantityOnHand',
1646
+ toNumber(sourceBalance.quantityOnHand) - input.quantity,
1647
+ )
1648
+ setNumeric(
1649
+ targetBalance as unknown as Record<string, unknown>,
1650
+ 'quantityOnHand',
1651
+ toNumber(targetBalance.quantityOnHand) + input.quantity,
1652
+ )
1653
+ const { movement } = await persistMovementWithIdempotency(trx, scope, movementInput)
1654
+ await trx.flush()
1655
+ return {
1656
+ movementId: movement.id,
1657
+ warehouseId: input.warehouseId,
1658
+ catalogVariantId: input.catalogVariantId,
1659
+ quantity: input.quantity,
1660
+ tenantId: scope.tenantId,
1661
+ organizationId: scope.organizationId,
1662
+ movementEntity: movement,
1663
+ balances: [
1664
+ { entity: sourceBalance, action: sourceBalanceAction },
1665
+ { entity: targetBalance, action: targetBalanceAction },
1666
+ ] as AffectedBalance[],
1667
+ idempotentReplay: false,
1668
+ }
1669
+ })
1670
+ if (!result.idempotentReplay) {
1671
+ await emitInventorySideEffects(ctx, {
1672
+ movements: [{ entity: result.movementEntity, action: 'created' }],
1673
+ balances: result.balances,
1674
+ })
1675
+ void emitWmsEvent('wms.inventory.moved', {
1676
+ id: result.movementId,
1677
+ movementId: result.movementId,
1678
+ warehouseId: result.warehouseId,
1679
+ catalogVariantId: result.catalogVariantId,
1680
+ quantity: toNumericString(result.quantity),
1681
+ tenantId: result.tenantId,
1682
+ organizationId: result.organizationId,
1683
+ }).catch(() => undefined)
1684
+ void emitLowStockEventIfNeeded(
1685
+ resolveEm(ctx),
1686
+ ctx,
1687
+ { tenantId: result.tenantId, organizationId: result.organizationId },
1688
+ result.catalogVariantId,
1689
+ ).catch(() => undefined)
1690
+ }
1691
+ return { movementId: result.movementId }
1692
+ },
1693
+ buildLog: async ({ input, result, ctx }) =>
1694
+ buildMutationLog({
1695
+ actionKey: 'wms.audit.inventory.move',
1696
+ fallbackLabel: 'Move inventory',
1697
+ resourceKind: WMS_INVENTORY_MOVEMENT_RESOURCE,
1698
+ resourceId: result?.movementId ?? null,
1699
+ parentResourceId:
1700
+ input?.warehouseId && input?.catalogVariantId
1701
+ ? `${input.warehouseId}:${input.catalogVariantId}`
1702
+ : null,
1703
+ tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
1704
+ organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
1705
+ cacheAliases: [WMS_INVENTORY_BALANCE_RESOURCE],
1706
+ }),
1707
+ }
1708
+
1709
+ const cycleCountInventoryCommand: CommandHandler<InventoryCycleCountInput, { adjustmentDelta: string; movementId?: string | null }> = {
1710
+ id: 'wms.inventory.cycleCount',
1711
+ // See "WMS Inventory Mutation Commands — Undo Policy" at top of file.
1712
+ isUndoable: false,
1713
+ async execute(rawInput, ctx) {
1714
+ const input = inventoryCycleCountSchema.parse(rawInput ?? {})
1715
+ ensureTenantScope(ctx, input.tenantId)
1716
+ ensureOrganizationScope(ctx, input.organizationId)
1717
+ const em = resolveEm(ctx)
1718
+ const result = await runInTransaction(em, async (trx) => {
1719
+ const scope = resolveScope(ctx, input)
1720
+ await requireWarehouse(trx, ctx, input.warehouseId, scope)
1721
+ const location = await requireLocation(trx, ctx, input.locationId, scope)
1722
+ const locationWarehouseId = typeof location.warehouse === 'string' ? location.warehouse : location.warehouse.id
1723
+ if (locationWarehouseId !== input.warehouseId) {
1724
+ throw new CrudHttpError(422, { error: 'invalid_location' })
1725
+ }
1726
+ const { balance, created: balanceWasNew } = await upsertBalanceBucket(trx, scope, {
1727
+ warehouseId: input.warehouseId,
1728
+ locationId: input.locationId,
1729
+ catalogVariantId: input.catalogVariantId,
1730
+ lotId: input.lotId,
1731
+ serialNumber: input.serialNumber,
1732
+ })
1733
+ const currentOnHand = toNumber(balance.quantityOnHand)
1734
+ const delta = input.countedQuantity - currentOnHand
1735
+ if (delta === 0) {
1736
+ return {
1737
+ adjustmentDelta: '0',
1738
+ movementId: null as string | null,
1739
+ warehouseId: input.warehouseId,
1740
+ catalogVariantId: input.catalogVariantId,
1741
+ tenantId: scope.tenantId,
1742
+ organizationId: scope.organizationId,
1743
+ movementEntity: null as InventoryMovement | null,
1744
+ balanceEntity: balance,
1745
+ balanceAction: ('updated' as const),
1746
+ }
1747
+ }
1748
+ if (!input.autoAdjust) {
1749
+ throw new CrudHttpError(422, { error: 'auto_adjust_required' })
1750
+ }
1751
+ const performedAt = input.performedAt ?? new Date()
1752
+ const receivedAt = await resolveReceivedAtForBalance(trx, balance, scope, performedAt)
1753
+ const movementInput: MovementMutationInput = {
1754
+ warehouseId: input.warehouseId,
1755
+ locationToId: input.locationId,
1756
+ catalogVariantId: input.catalogVariantId,
1757
+ lotId: input.lotId ?? null,
1758
+ serialNumber: input.serialNumber ?? null,
1759
+ quantity: delta,
1760
+ type: 'cycle_count',
1761
+ referenceType: 'manual',
1762
+ referenceId: input.referenceId,
1763
+ performedBy: input.performedBy,
1764
+ performedAt,
1765
+ receivedAt,
1766
+ reason: input.reason,
1767
+ metadata: input.metadata ?? null,
1768
+ }
1769
+ const idempotencyKey = buildMovementIdempotencyKey({
1770
+ referenceType: movementInput.referenceType,
1771
+ referenceId: movementInput.referenceId,
1772
+ type: movementInput.type,
1773
+ warehouseId: movementInput.warehouseId,
1774
+ locationFromId: movementInput.locationFromId,
1775
+ locationToId: movementInput.locationToId,
1776
+ catalogVariantId: movementInput.catalogVariantId,
1777
+ lotId: movementInput.lotId,
1778
+ serialNumber: movementInput.serialNumber,
1779
+ quantity: movementInput.quantity,
1780
+ })
1781
+ const existingMovement = await findExistingMovementByIdempotencyKey(trx, scope, idempotencyKey)
1782
+ if (existingMovement) {
1783
+ return {
1784
+ adjustmentDelta: toNumericString(delta),
1785
+ movementId: existingMovement.id,
1786
+ warehouseId: input.warehouseId,
1787
+ catalogVariantId: input.catalogVariantId,
1788
+ tenantId: scope.tenantId,
1789
+ organizationId: scope.organizationId,
1790
+ movementEntity: existingMovement,
1791
+ balanceEntity: balance,
1792
+ balanceAction: ('updated' as const),
1793
+ idempotentReplay: true,
1794
+ }
1795
+ }
1796
+ setNumeric(
1797
+ balance as unknown as Record<string, unknown>,
1798
+ 'quantityOnHand',
1799
+ input.countedQuantity,
1800
+ )
1801
+ const { movement } = await persistMovementWithIdempotency(trx, scope, movementInput)
1802
+ await trx.flush()
1803
+ return {
1804
+ adjustmentDelta: toNumericString(delta),
1805
+ movementId: movement.id as string | null,
1806
+ warehouseId: input.warehouseId,
1807
+ catalogVariantId: input.catalogVariantId,
1808
+ tenantId: scope.tenantId,
1809
+ organizationId: scope.organizationId,
1810
+ movementEntity: movement as InventoryMovement | null,
1811
+ balanceEntity: balance,
1812
+ balanceAction: balanceWasNew ? ('created' as const) : ('updated' as const),
1813
+ idempotentReplay: false,
1814
+ }
1815
+ })
1816
+ if (!result.idempotentReplay) {
1817
+ await emitInventorySideEffects(ctx, {
1818
+ movements: result.movementEntity ? [{ entity: result.movementEntity, action: 'created' }] : [],
1819
+ balances: [{ entity: result.balanceEntity, action: result.balanceAction }],
1820
+ })
1821
+ if (result.movementId) {
1822
+ void emitWmsEvent('wms.inventory.reconciled', {
1823
+ id: result.movementId,
1824
+ movementId: result.movementId,
1825
+ warehouseId: result.warehouseId,
1826
+ catalogVariantId: result.catalogVariantId,
1827
+ adjustmentDelta: result.adjustmentDelta,
1828
+ tenantId: result.tenantId,
1829
+ organizationId: result.organizationId,
1830
+ }).catch(() => undefined)
1831
+ void emitLowStockEventIfNeeded(
1832
+ resolveEm(ctx),
1833
+ ctx,
1834
+ { tenantId: result.tenantId, organizationId: result.organizationId },
1835
+ result.catalogVariantId,
1836
+ ).catch(() => undefined)
1837
+ }
1838
+ }
1839
+ return {
1840
+ adjustmentDelta: result.adjustmentDelta,
1841
+ movementId: result.movementId ?? null,
1842
+ }
1843
+ },
1844
+ buildLog: async ({ input, result, ctx }) =>
1845
+ buildMutationLog({
1846
+ actionKey: 'wms.audit.inventory.cycleCount',
1847
+ fallbackLabel: 'Run cycle count reconciliation',
1848
+ resourceKind: WMS_INVENTORY_MOVEMENT_RESOURCE,
1849
+ resourceId: result?.movementId ?? null,
1850
+ parentResourceId:
1851
+ input?.warehouseId && input?.catalogVariantId
1852
+ ? `${input.warehouseId}:${input.catalogVariantId}`
1853
+ : null,
1854
+ tenantId: input?.tenantId ?? ctx.auth?.tenantId ?? null,
1855
+ organizationId: input?.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
1856
+ cacheAliases: [WMS_INVENTORY_BALANCE_RESOURCE],
1857
+ }),
1858
+ }
1859
+
1860
+ registerCommand(reserveInventoryCommand)
1861
+ registerCommand(releaseInventoryReservationCommand)
1862
+ registerCommand(allocateInventoryReservationCommand)
1863
+ registerCommand(adjustInventoryCommand)
1864
+ registerCommand(receiveInventoryCommand)
1865
+ registerCommand(moveInventoryCommand)
1866
+ registerCommand(cycleCountInventoryCommand)