@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,52 @@
1
+ import { extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
2
+
3
+ /**
4
+ * Apply a 409 optimistic-lock `currentUpdatedAt` into a mutable token holder so
5
+ * ASN complete retries send a fresh If-Match.
6
+ * Returns the refreshed token, or null when the response is not a lock conflict.
7
+ */
8
+ export function applyAsnCompleteLockTokenFromConflict(
9
+ tokenHolder: { current: string | null },
10
+ response: { status: number; body: unknown },
11
+ ): string | null {
12
+ const conflict = extractOptimisticLockConflict({
13
+ status: response.status,
14
+ body: response.body,
15
+ })
16
+ if (!conflict?.currentUpdatedAt) return null
17
+ tokenHolder.current = conflict.currentUpdatedAt
18
+ return conflict.currentUpdatedAt
19
+ }
20
+
21
+ /**
22
+ * Prefer a mutable ASN lock ref (updated on receive success) over the async
23
+ * query `updated_at`, so a second receive before header refetch does not send
24
+ * a stale If-Match.
25
+ */
26
+ export function resolveAsnReceiveLockToken(
27
+ tokenHolder: { current: string | null },
28
+ asnUpdatedAt?: string | null,
29
+ ): string | null {
30
+ if (typeof tokenHolder.current === 'string' && tokenHolder.current.trim()) {
31
+ return tokenHolder.current.trim()
32
+ }
33
+ if (typeof asnUpdatedAt === 'string' && asnUpdatedAt.trim()) {
34
+ return asnUpdatedAt.trim()
35
+ }
36
+ return asnUpdatedAt ?? null
37
+ }
38
+
39
+ /**
40
+ * Store ASN `updated_at` from a successful receive so the next openReceive /
41
+ * query-cache seed uses a fresh If-Match without waiting on refetch.
42
+ */
43
+ export function applyAsnReceiveLockTokenFromSuccess(
44
+ tokenHolder: { current: string | null },
45
+ asnUpdatedAt?: string | null,
46
+ ): string | null {
47
+ if (typeof asnUpdatedAt !== 'string') return null
48
+ const trimmed = asnUpdatedAt.trim()
49
+ if (!trimmed) return null
50
+ tokenHolder.current = trimmed
51
+ return trimmed
52
+ }
@@ -0,0 +1,83 @@
1
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
2
+
3
+ /** API max pageSize (AGENTS.md); ASN detail loops until short page. */
4
+ export const ASN_RECEIVING_LINES_PAGE_SIZE = 100
5
+
6
+ export type AsnReceivingLineRow = {
7
+ id: string
8
+ asn_id?: string | null
9
+ catalog_variant_id?: string | null
10
+ expected_qty?: string | number | null
11
+ received_qty?: string | number | null
12
+ lot_number?: string | null
13
+ serial_numbers?: string[] | null
14
+ qc_status?: string | null
15
+ target_staging_location_id?: string | null
16
+ rejection_reason?: string | null
17
+ updated_at?: string | null
18
+ }
19
+
20
+ type PagedResponse<T> = {
21
+ items: T[]
22
+ total: number
23
+ totalPages: number
24
+ }
25
+
26
+ export type LoadAllAsnReceivingLinesResult =
27
+ | { ok: true; items: AsnReceivingLineRow[] }
28
+ | { ok: false; response: Response }
29
+
30
+ type FetchPage = (
31
+ page: number,
32
+ pageSize: number,
33
+ ) => Promise<{
34
+ ok: boolean
35
+ response: Response
36
+ result?: PagedResponse<AsnReceivingLineRow> | null
37
+ }>
38
+
39
+ /**
40
+ * Load every receiving line for an ASN detail console.
41
+ * Short-page termination only — never use `totalPages` (OM_LIST_COUNT_CAP).
42
+ */
43
+ export async function loadAllAsnReceivingLines(
44
+ asnId: string,
45
+ options?: {
46
+ pageSize?: number
47
+ fetchPage?: FetchPage
48
+ },
49
+ ): Promise<LoadAllAsnReceivingLinesResult> {
50
+ const pageSize = Math.min(
51
+ Math.max(1, options?.pageSize ?? ASN_RECEIVING_LINES_PAGE_SIZE),
52
+ ASN_RECEIVING_LINES_PAGE_SIZE,
53
+ )
54
+ const fetchPage: FetchPage =
55
+ options?.fetchPage ??
56
+ (async (page, size) => {
57
+ const call = await apiCall<PagedResponse<AsnReceivingLineRow>>(
58
+ `/api/wms/receiving-lines?asnId=${encodeURIComponent(asnId)}&page=${page}&pageSize=${size}`,
59
+ )
60
+ return {
61
+ ok: call.ok,
62
+ response: call.response,
63
+ result: call.result ?? null,
64
+ }
65
+ })
66
+
67
+ const items: AsnReceivingLineRow[] = []
68
+ let page = 1
69
+ // Short-page termination: `totalPages` derives from `total`, which is a
70
+ // display value that can be capped (OM_LIST_COUNT_CAP) — never a loop bound.
71
+ for (;;) {
72
+ const call = await fetchPage(page, pageSize)
73
+ if (!call.ok) {
74
+ return { ok: false, response: call.response }
75
+ }
76
+ const batch = call.result?.items ?? []
77
+ items.push(...batch)
78
+ if (batch.length < pageSize) break
79
+ page += 1
80
+ }
81
+
82
+ return { ok: true, items }
83
+ }
@@ -0,0 +1,154 @@
1
+ import type { StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
2
+ import { hasAsnReceiptActivity, isAsnCloseable } from '../../lib/asnReceiving'
3
+
4
+ export type AsnStatus = 'draft' | 'in_transit' | 'received' | 'closed'
5
+ export type ReceivingLineQcStatus = 'pending' | 'passed' | 'failed'
6
+ export type PutawayTaskStatus = 'open' | 'in_progress' | 'done' | 'cancelled'
7
+
8
+ export type AsnCompleteGateLine = {
9
+ expected_qty?: string | number | null
10
+ received_qty?: string | number | null
11
+ qc_status?: string | null
12
+ }
13
+
14
+ export function asnStatusVariant(status: string | null | undefined): StatusBadgeVariant {
15
+ switch (status) {
16
+ case 'in_transit':
17
+ return 'info'
18
+ case 'received':
19
+ return 'success'
20
+ case 'closed':
21
+ return 'neutral'
22
+ case 'draft':
23
+ default:
24
+ return 'neutral'
25
+ }
26
+ }
27
+
28
+ export function qcStatusVariant(status: string | null | undefined): StatusBadgeVariant {
29
+ switch (status) {
30
+ case 'passed':
31
+ return 'success'
32
+ case 'failed':
33
+ return 'error'
34
+ case 'pending':
35
+ default:
36
+ return 'warning'
37
+ }
38
+ }
39
+
40
+ export function putawayStatusVariant(status: string | null | undefined): StatusBadgeVariant {
41
+ switch (status) {
42
+ case 'in_progress':
43
+ return 'warning'
44
+ case 'done':
45
+ return 'success'
46
+ case 'cancelled':
47
+ return 'neutral'
48
+ case 'open':
49
+ default:
50
+ return 'info'
51
+ }
52
+ }
53
+
54
+ export function formatAgingLabel(createdAt: string | null | undefined, nowMs = Date.now()): string | null {
55
+ if (!createdAt) return null
56
+ const createdMs = new Date(createdAt).getTime()
57
+ if (Number.isNaN(createdMs)) return null
58
+ const minutes = Math.max(0, Math.floor((nowMs - createdMs) / 60_000))
59
+ if (minutes < 60) return `${minutes}m`
60
+ const hours = Math.floor(minutes / 60)
61
+ if (hours < 48) return `${hours}h`
62
+ const days = Math.floor(hours / 24)
63
+ return `${days}d`
64
+ }
65
+
66
+ /** Map putaway queue column ids to list API `sortField` values. Aging is derived from created_at. */
67
+ export function mapPutawayQueueSortField(columnId: string): string {
68
+ return columnId === 'aging' ? 'createdAt' : columnId
69
+ }
70
+
71
+ /**
72
+ * Map UI sort direction to list API `sortDir`.
73
+ * Aging is age-since-created: UI "desc" (oldest first) is createdAt ascending,
74
+ * and UI "asc" (newest first) is createdAt descending.
75
+ */
76
+ export function mapPutawayQueueSortDir(columnId: string, desc: boolean): 'asc' | 'desc' {
77
+ const dir = desc ? 'desc' : 'asc'
78
+ if (columnId === 'aging') {
79
+ return desc ? 'asc' : 'desc'
80
+ }
81
+ return dir
82
+ }
83
+
84
+ export function lineHasDiscrepancy(
85
+ expectedQty: string | number | null | undefined,
86
+ receivedQty: string | number | null | undefined,
87
+ ): boolean {
88
+ const expected = Number(expectedQty ?? 0)
89
+ const received = Number(receivedQty ?? 0)
90
+ if (!Number.isFinite(expected) || !Number.isFinite(received)) return false
91
+ return received !== expected && received > 0
92
+ }
93
+
94
+ /**
95
+ * UI visibility for putaway Complete (and scan putaway complete CTAs).
96
+ * Matches what callers can actually invoke successfully:
97
+ * - HTTP floor on complete + scan/putaway requires `wms.adjust_inventory`
98
+ * - Command then allows manage (any task) or adjust + assignee match
99
+ * Manage-only without adjust must not see Complete (would 403 on the route).
100
+ */
101
+ export function canShowPutawayCompleteAction(input: {
102
+ canManagePutaway: boolean
103
+ canAdjustInventory: boolean
104
+ isAssignee: boolean
105
+ }): boolean {
106
+ if (!input.canAdjustInventory) return false
107
+ return input.canManagePutaway || input.isAssignee
108
+ }
109
+
110
+ function toCloseableLines(lines: AsnCompleteGateLine[]) {
111
+ return lines.map((line) => ({
112
+ expectedQty: line.expected_qty ?? 0,
113
+ receivedQty: line.received_qty ?? 0,
114
+ qcStatus: line.qc_status,
115
+ }))
116
+ }
117
+
118
+ /**
119
+ * Complete ASN affordance aligned with `wms.asns.close` / `isAsnCloseable`:
120
+ * - Show when manage + open ASN + any receipt activity (empty / untouched lines hide Complete)
121
+ * - Enable default submit only when QC-passed accepted qty meets expected on every line
122
+ * - When short / QC-fail, expose close-when-short and enable submit only after that opt-in
123
+ * (avoids enabling a default Complete that always 409s as invalid_receipt_state)
124
+ * - Header-only / zero-activity ASNs stay non-closeable (isAsnCloseable requires receipt activity)
125
+ */
126
+ export function resolveAsnCompleteGate(input: {
127
+ canManageAsn: boolean
128
+ asnStatus: string | null | undefined
129
+ lines: AsnCompleteGateLine[]
130
+ closeWhenShort: boolean
131
+ }): {
132
+ canShowComplete: boolean
133
+ canSubmitComplete: boolean
134
+ showCloseWhenShort: boolean
135
+ } {
136
+ const closeableLines = toCloseableLines(input.lines)
137
+ const canReceiveMore = input.asnStatus === 'draft' || input.asnStatus === 'in_transit'
138
+ const canShowComplete =
139
+ input.canManageAsn && canReceiveMore && hasAsnReceiptActivity(closeableLines)
140
+ if (!canShowComplete) {
141
+ return {
142
+ canShowComplete: false,
143
+ canSubmitComplete: false,
144
+ showCloseWhenShort: false,
145
+ }
146
+ }
147
+
148
+ const fullyCloseable = isAsnCloseable(closeableLines, false)
149
+ return {
150
+ canShowComplete: true,
151
+ canSubmitComplete: isAsnCloseable(closeableLines, input.closeWhenShort),
152
+ showCloseWhenShort: !fullyCloseable,
153
+ }
154
+ }
@@ -405,6 +405,26 @@ export async function loadBinLocationOptions(
405
405
  }
406
406
  }
407
407
 
408
+ export async function loadStagingLocationOptions(
409
+ warehouseId: string,
410
+ query?: string,
411
+ ): Promise<CrudFieldOption[]> {
412
+ if (!warehouseId) return []
413
+ try {
414
+ const [staging, dock] = await Promise.all([
415
+ loadLocationPage(warehouseId, { type: 'staging', search: query }),
416
+ loadLocationPage(warehouseId, { type: 'dock', search: query }),
417
+ ])
418
+ const byId = new Map<string, CrudFieldOption>()
419
+ for (const option of mapLocationOptions([...(staging.items ?? []), ...(dock.items ?? [])])) {
420
+ byId.set(option.value, option)
421
+ }
422
+ return Array.from(byId.values())
423
+ } catch {
424
+ return []
425
+ }
426
+ }
427
+
408
428
  export async function loadZoneOptions(
409
429
  warehouseId: string,
410
430
  query?: string,
@@ -0,0 +1,20 @@
1
+ import { extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
2
+
3
+ /**
4
+ * Apply a 409 optimistic-lock `currentUpdatedAt` into a mutable token map so
5
+ * putaway queue assign/start/cancel retries send a fresh If-Match.
6
+ * Returns the refreshed token, or null when the response is not a lock conflict.
7
+ */
8
+ export function applyPutawayLockTokenFromConflict(
9
+ tokens: Record<string, string>,
10
+ taskId: string,
11
+ response: { status: number; body: unknown },
12
+ ): string | null {
13
+ const conflict = extractOptimisticLockConflict({
14
+ status: response.status,
15
+ body: response.body,
16
+ })
17
+ if (!conflict?.currentUpdatedAt) return null
18
+ tokens[taskId] = conflict.currentUpdatedAt
19
+ return conflict.currentUpdatedAt
20
+ }
@@ -35,6 +35,8 @@ export function useWmsInventoryMutationAccess() {
35
35
  'wms.adjust_inventory',
36
36
  'wms.manage_inventory',
37
37
  'wms.receive_inventory',
38
+ 'wms.manage_asn',
39
+ 'wms.manage_putaway',
38
40
  'wms.cycle_count',
39
41
  'wms.import',
40
42
  'wms.manage_reservations',
@@ -75,6 +77,8 @@ export function useWmsInventoryMutationAccess() {
75
77
  canAdjust: hasFeature(granted, 'wms.adjust_inventory'),
76
78
  canManage: hasFeature(granted, 'wms.manage_inventory'),
77
79
  canReceive: hasFeature(granted, 'wms.receive_inventory'),
80
+ canManageAsn: hasFeature(granted, 'wms.manage_asn'),
81
+ canManagePutaway: hasFeature(granted, 'wms.manage_putaway'),
78
82
  canCycleCount: hasFeature(granted, 'wms.cycle_count'),
79
83
  canImport: hasFeature(granted, 'wms.import'),
80
84
  canMove: hasFeature(granted, 'wms.adjust_inventory'),
@@ -5,6 +5,8 @@ import type { FeatureTogglesService } from '@open-mercato/core/modules/feature_t
5
5
  import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
6
6
  import { E } from '#generated/entities.ids.generated'
7
7
  import { InventoryBalance, InventoryReservation, ProductInventoryProfile, SalesOrderWarehouseAssignment, Warehouse } from './entities'
8
+ import { formatCatalogVariantLabel } from '../lib/inventoryDisplayUi'
9
+ import { isBalanceLocationReservable } from '../lib/inventoryPolicy'
8
10
  import { resolvePrimaryWarehouseId } from '../lib/primaryWarehousePolicy'
9
11
  import {
10
12
  WMS_ENRICHER_CACHE_TTL_MS,
@@ -28,7 +30,21 @@ type SalesOrderLineRow = {
28
30
  type CatalogVariantRow = {
29
31
  id?: string
30
32
  product_id?: string | null
33
+ name?: string | null
34
+ sku?: string | null
31
35
  }
36
+
37
+ type ReservationSummaryItem = {
38
+ id: string
39
+ reservationLabel: string
40
+ catalogVariantId: string
41
+ warehouseId: string | null
42
+ warehouseName: string | null
43
+ quantity: string
44
+ variantName: string | null
45
+ variantSku: string | null
46
+ }
47
+
32
48
  type ReservationStatus = 'unreserved' | 'partially_reserved' | 'fully_reserved'
33
49
  type ReorderState = 'no_profile' | 'healthy' | 'below_reorder_point' | 'below_safety_stock'
34
50
 
@@ -60,6 +76,11 @@ type ReorderStatus = {
60
76
  safetyStock: string
61
77
  }
62
78
 
79
+ type InboundSummary = {
80
+ openAsnCount: number
81
+ nextExpectedAt: string | null
82
+ }
83
+
63
84
  type SalesOrderWmsEnrichment = {
64
85
  _wms: {
65
86
  assignedWarehouseId: string | null
@@ -73,7 +94,9 @@ type SalesOrderWmsEnrichment = {
73
94
  reservationSummary: {
74
95
  status: ReservationStatus
75
96
  reservationIds: string[]
97
+ reservations: ReservationSummaryItem[]
76
98
  }
99
+ inboundSummary: InboundSummary
77
100
  }
78
101
  }
79
102
 
@@ -90,6 +113,11 @@ type Scope = { organizationId: string; tenantId: string }
90
113
  type BalanceAggregate = { onHand: number; reserved: number; allocated: number; available: number }
91
114
 
92
115
  import { resolveWmsIntegrationToggleEnabled } from '../lib/wmsIntegrationToggles'
116
+ const EMPTY_INBOUND_SUMMARY: InboundSummary = {
117
+ openAsnCount: 0,
118
+ nextExpectedAt: null,
119
+ }
120
+
93
121
  const EMPTY_ENRICHMENT: SalesOrderWmsEnrichment = {
94
122
  _wms: {
95
123
  assignedWarehouseId: null,
@@ -99,7 +127,9 @@ const EMPTY_ENRICHMENT: SalesOrderWmsEnrichment = {
99
127
  reservationSummary: {
100
128
  status: 'unreserved',
101
129
  reservationIds: [],
130
+ reservations: [],
102
131
  },
132
+ inboundSummary: EMPTY_INBOUND_SUMMARY,
103
133
  },
104
134
  }
105
135
 
@@ -167,6 +197,55 @@ function resolveReservationStatus(requiredQuantity: number, reservedQuantity: nu
167
197
  return 'fully_reserved'
168
198
  }
169
199
 
200
+ function buildReservationLabel(input: {
201
+ id: string
202
+ variantName: string | null
203
+ variantSku: string | null
204
+ warehouseName: string | null
205
+ quantity: string
206
+ }): string {
207
+ const variantLabel = formatCatalogVariantLabel({
208
+ variant_name: input.variantName,
209
+ variant_sku: input.variantSku,
210
+ })
211
+ const segments: string[] = []
212
+ if (variantLabel && variantLabel !== '—') segments.push(variantLabel)
213
+ const warehouseName = input.warehouseName?.trim()
214
+ if (warehouseName) segments.push(warehouseName)
215
+ const quantity = formatQuantity(Number(input.quantity))
216
+ if (quantity !== '0') segments.push(quantity)
217
+ return segments.length > 0 ? segments.join(' · ') : input.id
218
+ }
219
+
220
+ function toReservationSummaryItem(
221
+ reservation: InventoryReservation,
222
+ warehouseNamesById: Map<string, string>,
223
+ variantLabelsById: Map<string, { name: string | null; sku: string | null }>,
224
+ ): ReservationSummaryItem {
225
+ const warehouseId = extractWarehouseId(reservation)
226
+ const warehouseName = warehouseId ? (warehouseNamesById.get(warehouseId) ?? null) : null
227
+ const variantMeta = variantLabelsById.get(reservation.catalogVariantId)
228
+ const variantName = variantMeta?.name ?? null
229
+ const variantSku = variantMeta?.sku ?? null
230
+ const quantity = formatQuantity(Number(reservation.quantity))
231
+ return {
232
+ id: reservation.id,
233
+ reservationLabel: buildReservationLabel({
234
+ id: reservation.id,
235
+ variantName,
236
+ variantSku,
237
+ warehouseName,
238
+ quantity,
239
+ }),
240
+ catalogVariantId: reservation.catalogVariantId,
241
+ warehouseId,
242
+ warehouseName,
243
+ quantity,
244
+ variantName,
245
+ variantSku,
246
+ }
247
+ }
248
+
170
249
  function resolveReorderStatus(
171
250
  availableQuantity: number,
172
251
  profile: ProductInventoryProfile | null | undefined,
@@ -204,6 +283,10 @@ function buildBalanceSummaryByVariant(
204
283
  const byVariant = new Map<string, BalanceAggregate>()
205
284
 
206
285
  for (const balance of balances) {
286
+ // Match reservation / sales-automation: staging/dock are inbound holding
287
+ // and must not inflate widget "Available" (or sellable on-hand aggregates).
288
+ if (!isBalanceLocationReservable(balance)) continue
289
+
207
290
  const current = byVariant.get(balance.catalogVariantId) ?? {
208
291
  onHand: 0,
209
292
  reserved: 0,
@@ -254,6 +337,31 @@ async function isSalesOrderInventoryEnabled(context: EnricherContext): Promise<b
254
337
  }
255
338
  }
256
339
 
340
+ async function loadInboundSummary(em: EntityManager, scope: Scope): Promise<InboundSummary> {
341
+ // Aggregate only — never load all open ASN rows (backlog can blow the enricher timeout).
342
+ const rows = await em.getConnection().execute<
343
+ Array<{ open_asn_count: string | number; next_expected_at: Date | string | null }>
344
+ >(
345
+ `select count(*)::int as open_asn_count,
346
+ min(expected_at) as next_expected_at
347
+ from wms_asns
348
+ where organization_id = ?
349
+ and tenant_id = ?
350
+ and deleted_at is null
351
+ and status in ('draft', 'in_transit')`,
352
+ [scope.organizationId, scope.tenantId],
353
+ )
354
+ const row = rows[0]
355
+ const openAsnCount = Number(row?.open_asn_count ?? 0)
356
+ const nextExpected = row?.next_expected_at
357
+ return {
358
+ openAsnCount: Number.isFinite(openAsnCount) ? openAsnCount : 0,
359
+ nextExpectedAt: nextExpected
360
+ ? (nextExpected instanceof Date ? nextExpected : new Date(nextExpected)).toISOString()
361
+ : null,
362
+ }
363
+ }
364
+
257
365
  async function loadExplicitAssignments(
258
366
  em: EntityManager,
259
367
  orderIds: string[],
@@ -352,7 +460,8 @@ async function loadBalances(
352
460
  tenantId: scope.tenantId,
353
461
  deletedAt: null,
354
462
  },
355
- undefined,
463
+ // Populate location so isBalanceLocationReservable can exclude staging/dock.
464
+ { populate: ['location'] },
356
465
  scope,
357
466
  )
358
467
  }
@@ -376,6 +485,32 @@ async function loadCatalogVariants(
376
485
  return result.items
377
486
  }
378
487
 
488
+ async function loadCatalogVariantLabelsByIds(
489
+ context: EnricherContext,
490
+ variantIds: string[],
491
+ scope: Scope,
492
+ ): Promise<Map<string, { name: string | null; sku: string | null }>> {
493
+ if (variantIds.length === 0) return new Map()
494
+ const container = context.container as { resolve: (name: string) => unknown }
495
+ const queryEngine = container.resolve('queryEngine') as QueryEngine
496
+ const result = await queryEngine.query<CatalogVariantRow>(E.catalog.catalog_product_variant, {
497
+ tenantId: scope.tenantId,
498
+ organizationId: scope.organizationId,
499
+ filters: { id: { $in: variantIds } },
500
+ fields: ['id', 'name', 'sku'],
501
+ page: { page: 1, pageSize: Math.max(variantIds.length, 1) },
502
+ })
503
+ const labels = new Map<string, { name: string | null; sku: string | null }>()
504
+ for (const row of result.items) {
505
+ if (typeof row.id !== 'string' || row.id.length === 0) continue
506
+ labels.set(row.id, {
507
+ name: typeof row.name === 'string' ? row.name : null,
508
+ sku: typeof row.sku === 'string' ? row.sku : null,
509
+ })
510
+ }
511
+ return labels
512
+ }
513
+
379
514
  async function loadProductInventoryProfiles(
380
515
  em: EntityManager,
381
516
  productIds: string[],
@@ -462,12 +597,16 @@ const salesOrderInventoryEnricher: ResponseEnricher<SalesOrderRecord, SalesOrder
462
597
  ),
463
598
  )
464
599
 
465
- const [reservations, balances, primaryWarehouseId, explicitAssignments] = await Promise.all([
466
- loadReservations(em, orderIds, scope),
467
- loadBalances(em, variantIds, scope),
468
- resolvePrimaryWarehouseId(em, scope),
469
- loadExplicitAssignments(em, orderIds, scope),
470
- ])
600
+ // inboundSummary is best-effort (raw ASN SQL); isolate so a missing table /
601
+ // migration lag cannot reject the whole Promise.all and drop stock/reservation.
602
+ const [reservations, balances, primaryWarehouseId, explicitAssignments, inboundSummary] =
603
+ await Promise.all([
604
+ loadReservations(em, orderIds, scope),
605
+ loadBalances(em, variantIds, scope),
606
+ resolvePrimaryWarehouseId(em, scope),
607
+ loadExplicitAssignments(em, orderIds, scope),
608
+ loadInboundSummary(em, scope).catch(() => EMPTY_INBOUND_SUMMARY),
609
+ ])
471
610
 
472
611
  const explicitWarehouseIdsByOrder = new Map<string, string>()
473
612
  for (const assignment of explicitAssignments) {
@@ -491,14 +630,31 @@ const salesOrderInventoryEnricher: ResponseEnricher<SalesOrderRecord, SalesOrder
491
630
  const warehouses = await loadWarehousesByIds(em, allRelevantWarehouseIds, scope)
492
631
  const warehouseNamesById = new Map(warehouses.map((w) => [w.id, w.name]))
493
632
 
633
+ const activeReservationVariantIds = Array.from(
634
+ new Set(
635
+ reservations
636
+ .filter((reservation) => reservation.status === 'active')
637
+ .map((reservation) => reservation.catalogVariantId)
638
+ .filter((value): value is string => typeof value === 'string' && value.length > 0),
639
+ ),
640
+ )
641
+ const variantLabelsById = await loadCatalogVariantLabelsByIds(
642
+ context,
643
+ activeReservationVariantIds,
644
+ scope,
645
+ )
646
+
494
647
  const variantAvailability = new Map<string, number>()
495
648
  const variantReserved = new Map<string, number>()
496
649
  for (const balance of balances) {
497
- addToMap(
498
- variantAvailability,
499
- balance.catalogVariantId,
500
- Number(balance.quantityOnHand) - Number(balance.quantityReserved) - Number(balance.quantityAllocated),
501
- )
650
+ // Staging/dock must not count toward Available (same rule as reserve path).
651
+ if (isBalanceLocationReservable(balance)) {
652
+ addToMap(
653
+ variantAvailability,
654
+ balance.catalogVariantId,
655
+ Number(balance.quantityOnHand) - Number(balance.quantityReserved) - Number(balance.quantityAllocated),
656
+ )
657
+ }
502
658
  addToMap(variantReserved, balance.catalogVariantId, balance.quantityReserved)
503
659
  }
504
660
 
@@ -564,6 +720,10 @@ const salesOrderInventoryEnricher: ResponseEnricher<SalesOrderRecord, SalesOrder
564
720
  ? (warehouseNamesById.get(assignedWarehouseId) ?? null)
565
721
  : null
566
722
 
723
+ const reservationItems = reservationsForOrder.map((reservation) =>
724
+ toReservationSummaryItem(reservation, warehouseNamesById, variantLabelsById),
725
+ )
726
+
567
727
  return {
568
728
  ...record,
569
729
  _wms: {
@@ -580,8 +740,10 @@ const salesOrderInventoryEnricher: ResponseEnricher<SalesOrderRecord, SalesOrder
580
740
  requiredBySalesOrder.get(orderId) ?? 0,
581
741
  reservedBySalesOrder.get(orderId) ?? 0,
582
742
  ),
583
- reservationIds: reservationsForOrder.map((reservation) => reservation.id),
743
+ reservationIds: reservationItems.map((item) => item.id),
744
+ reservations: reservationItems,
584
745
  },
746
+ inboundSummary,
585
747
  },
586
748
  }
587
749
  })