@open-mercato/core 0.10.1-develop.7709.1.046124599c → 0.10.1-develop.7714.1.9dbb8200d8

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