@aglyn/plugins-commerce 1.0.0-beta.285 → 1.0.0-beta.287

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 (420) hide show
  1. package/README.md +1 -1
  2. package/package.json +13 -13
  3. package/src/lib/components/cart.js +25 -12
  4. package/src/lib/components/cart.js.map +1 -1
  5. package/src/lib/components/commerce-console-page.js +70 -13
  6. package/src/lib/components/commerce-console-page.js.map +1 -1
  7. package/src/lib/components/console/order-detail-dialog.component.js +2 -1
  8. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  9. package/src/lib/components/console/payment-methods-card.component.js +1 -1
  10. package/src/lib/components/console/payment-methods-card.component.js.map +1 -1
  11. package/src/lib/components/console/pos/pos-cart-panel.component.d.ts +5 -0
  12. package/src/lib/components/console/pos/pos-cart-panel.component.js +18 -12
  13. package/src/lib/components/console/pos/pos-cart-panel.component.js.map +1 -1
  14. package/src/lib/components/console/pos/pos-hardware-api.d.ts +76 -5
  15. package/src/lib/components/console/pos/pos-hardware-api.js +53 -11
  16. package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -1
  17. package/src/lib/components/console/pos/pos-hardware-buyer.component.d.ts +18 -0
  18. package/src/lib/components/console/pos/pos-hardware-buyer.component.js +629 -0
  19. package/src/lib/components/console/pos/pos-hardware-buyer.component.js.map +1 -0
  20. package/src/lib/components/console/pos/pos-hardware-sale-api.d.ts +162 -0
  21. package/src/lib/components/console/pos/pos-hardware-sale-api.js +122 -0
  22. package/src/lib/components/console/pos/pos-hardware-sale-api.js.map +1 -0
  23. package/src/lib/components/console/pos/pos-hardware-sale-terms.component.d.ts +39 -0
  24. package/src/lib/components/console/pos/pos-hardware-sale-terms.component.js +251 -0
  25. package/src/lib/components/console/pos/pos-hardware-sale-terms.component.js.map +1 -0
  26. package/src/lib/components/console/pos/use-pos-display.js +50 -0
  27. package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
  28. package/src/lib/components/console/pos-cellular-control.component.d.ts +41 -0
  29. package/src/lib/components/console/pos-cellular-control.component.js +211 -0
  30. package/src/lib/components/console/pos-cellular-control.component.js.map +1 -0
  31. package/src/lib/components/console/pos-devices-card.component.d.ts +9 -1
  32. package/src/lib/components/console/pos-devices-card.component.js +47 -15
  33. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  34. package/src/lib/components/console/pos-display/pos-display-api.d.ts +6 -0
  35. package/src/lib/components/console/pos-display/pos-display-api.js +8 -1
  36. package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
  37. package/src/lib/components/console/pos-display-page.component.d.ts +13 -0
  38. package/src/lib/components/console/pos-display-page.component.js +44 -2
  39. package/src/lib/components/console/pos-display-page.component.js.map +1 -1
  40. package/src/lib/components/console/pos-hardware-card.component.d.ts +12 -4
  41. package/src/lib/components/console/pos-hardware-card.component.js +157 -36
  42. package/src/lib/components/console/pos-hardware-card.component.js.map +1 -1
  43. package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +360 -80
  44. package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -1
  45. package/src/lib/components/console/pos-hardware-terms-page.component.d.ts +34 -0
  46. package/src/lib/components/console/pos-hardware-terms-page.component.js +257 -0
  47. package/src/lib/components/console/pos-hardware-terms-page.component.js.map +1 -0
  48. package/src/lib/components/console/pos-hardware-terms-sign-page.component.d.ts +27 -0
  49. package/src/lib/components/console/pos-hardware-terms-sign-page.component.js +104 -0
  50. package/src/lib/components/console/pos-hardware-terms-sign-page.component.js.map +1 -0
  51. package/src/lib/components/console/pos-hardware-waiting-card.component.d.ts +24 -0
  52. package/src/lib/components/console/pos-hardware-waiting-card.component.js +205 -0
  53. package/src/lib/components/console/pos-hardware-waiting-card.component.js.map +1 -0
  54. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +297 -201
  55. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -1
  56. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +3 -0
  57. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +260 -92
  58. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
  59. package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +9 -0
  60. package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +15 -6
  61. package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
  62. package/src/lib/components/console/pos-ops/pos-rain-check-dialog.component.d.ts +25 -0
  63. package/src/lib/components/console/pos-ops/pos-rain-check-dialog.component.js +136 -0
  64. package/src/lib/components/console/pos-ops/pos-rain-check-dialog.component.js.map +1 -0
  65. package/src/lib/components/console/pos-ops/pos-shift-panel.component.d.ts +6 -0
  66. package/src/lib/components/console/pos-ops/pos-shift-panel.component.js +48 -31
  67. package/src/lib/components/console/pos-ops/pos-shift-panel.component.js.map +1 -1
  68. package/src/lib/components/console/pos-page.component.d.ts +1 -1
  69. package/src/lib/components/console/pos-page.component.js +63 -12
  70. package/src/lib/components/console/pos-page.component.js.map +1 -1
  71. package/src/lib/components/console/pos-reader-splash-setting.component.js +1 -1
  72. package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -1
  73. package/src/lib/components/console/product-check-in-fields.component.d.ts +4 -2
  74. package/src/lib/components/console/product-check-in-fields.component.js +60 -14
  75. package/src/lib/components/console/product-check-in-fields.component.js.map +1 -1
  76. package/src/lib/components/console/product-editor-dialog.component.js +44 -1
  77. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  78. package/src/lib/components/console/product-hardware-sku-field.component.d.ts +19 -0
  79. package/src/lib/components/console/product-hardware-sku-field.component.js +92 -0
  80. package/src/lib/components/console/product-hardware-sku-field.component.js.map +1 -0
  81. package/src/lib/components/console/product-kiosk-offer-field.component.d.ts +22 -0
  82. package/src/lib/components/console/product-kiosk-offer-field.component.js +94 -0
  83. package/src/lib/components/console/product-kiosk-offer-field.component.js.map +1 -0
  84. package/src/lib/components/console/product-register-fields.component.d.ts +4 -0
  85. package/src/lib/components/console/product-register-fields.component.js +31 -2
  86. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  87. package/src/lib/components/console/products-hub-card.component.js +11 -2
  88. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  89. package/src/lib/components/console/register-hardware-card.component.d.ts +11 -0
  90. package/src/lib/components/console/register-hardware-card.component.js +56 -27
  91. package/src/lib/components/console/register-hardware-card.component.js.map +1 -1
  92. package/src/lib/components/console/reservation-guests-dialog.component.d.ts +28 -0
  93. package/src/lib/components/console/reservation-guests-dialog.component.js +189 -0
  94. package/src/lib/components/console/reservation-guests-dialog.component.js.map +1 -0
  95. package/src/lib/components/console/reservations-card.component.d.ts +9 -0
  96. package/src/lib/components/console/reservations-card.component.js +158 -23
  97. package/src/lib/components/console/reservations-card.component.js.map +1 -1
  98. package/src/lib/components/console/shippingeasy-card.component.js +1 -1
  99. package/src/lib/components/console/shippingeasy-card.component.js.map +1 -1
  100. package/src/lib/components/console/shipstation-card.component.d.ts +1 -1
  101. package/src/lib/components/console/shipstation-card.component.js +3 -3
  102. package/src/lib/components/console/shipstation-card.component.js.map +1 -1
  103. package/src/lib/components/product-detail.d.ts +4 -0
  104. package/src/lib/components/product-detail.js +40 -5
  105. package/src/lib/components/product-detail.js.map +1 -1
  106. package/src/lib/components/product-reviews.js +28 -11
  107. package/src/lib/components/product-reviews.js.map +1 -1
  108. package/src/lib/components/reservation-widget.js +28 -5
  109. package/src/lib/components/reservation-widget.js.map +1 -1
  110. package/src/lib/components/use-day-picker.d.ts +25 -0
  111. package/src/lib/components/use-day-picker.js +57 -0
  112. package/src/lib/components/use-day-picker.js.map +1 -0
  113. package/src/lib/constants/operator-alerts.d.ts +55 -0
  114. package/src/lib/constants/operator-alerts.js +156 -1
  115. package/src/lib/constants/operator-alerts.js.map +1 -1
  116. package/src/lib/declarations.console-server.js +54 -1
  117. package/src/lib/declarations.console-server.js.map +1 -1
  118. package/src/lib/declarations.d.ts +4 -2
  119. package/src/lib/declarations.js +11 -4
  120. package/src/lib/declarations.js.map +1 -1
  121. package/src/lib/declarations.server.js +9 -0
  122. package/src/lib/declarations.server.js.map +1 -1
  123. package/src/lib/model/commerce-cart.d.ts +9 -1
  124. package/src/lib/model/commerce-cart.js +4 -3
  125. package/src/lib/model/commerce-cart.js.map +1 -1
  126. package/src/lib/model/commerce-check-in-product.d.ts +26 -0
  127. package/src/lib/model/commerce-check-in-product.js +64 -2
  128. package/src/lib/model/commerce-check-in-product.js.map +1 -1
  129. package/src/lib/model/commerce-check-in-record.d.ts +127 -0
  130. package/src/lib/model/commerce-check-in-record.js +36 -0
  131. package/src/lib/model/commerce-check-in-record.js.map +1 -0
  132. package/src/lib/model/commerce-check-in.d.ts +91 -87
  133. package/src/lib/model/commerce-check-in.js +186 -62
  134. package/src/lib/model/commerce-check-in.js.map +1 -1
  135. package/src/lib/model/commerce-kiosk-offer.d.ts +31 -0
  136. package/src/lib/model/commerce-kiosk-offer.js +35 -0
  137. package/src/lib/model/commerce-kiosk-offer.js.map +1 -0
  138. package/src/lib/model/commerce-orders.d.ts +76 -0
  139. package/src/lib/model/commerce-orders.js +6 -0
  140. package/src/lib/model/commerce-orders.js.map +1 -1
  141. package/src/lib/model/commerce-pos-day-report.d.ts +54 -1
  142. package/src/lib/model/commerce-pos-day-report.js +39 -2
  143. package/src/lib/model/commerce-pos-day-report.js.map +1 -1
  144. package/src/lib/model/commerce-pos-offline.d.ts +1 -1
  145. package/src/lib/model/commerce-pos-offline.js +6 -1
  146. package/src/lib/model/commerce-pos-offline.js.map +1 -1
  147. package/src/lib/model/commerce-pos-ops.js +3 -2
  148. package/src/lib/model/commerce-pos-ops.js.map +1 -1
  149. package/src/lib/model/commerce-pos.d.ts +36 -3
  150. package/src/lib/model/commerce-pos.js +14 -6
  151. package/src/lib/model/commerce-pos.js.map +1 -1
  152. package/src/lib/model/commerce-receipt.js +3 -2
  153. package/src/lib/model/commerce-receipt.js.map +1 -1
  154. package/src/lib/model/commerce-reservations.d.ts +69 -4
  155. package/src/lib/model/commerce-reservations.js +57 -5
  156. package/src/lib/model/commerce-reservations.js.map +1 -1
  157. package/src/lib/model/commerce.d.ts +66 -0
  158. package/src/lib/model/commerce.js +15 -0
  159. package/src/lib/model/commerce.js.map +1 -1
  160. package/src/lib/model/order-list-fields.d.ts +11 -0
  161. package/src/lib/model/order-list-fields.js +17 -1
  162. package/src/lib/model/order-list-fields.js.map +1 -1
  163. package/src/lib/model/own-store-tax-summary.d.ts +152 -0
  164. package/src/lib/model/own-store-tax-summary.js +288 -0
  165. package/src/lib/model/own-store-tax-summary.js.map +1 -0
  166. package/src/lib/model/plugin-permissions.d.ts +67 -0
  167. package/src/lib/model/plugin-permissions.js +224 -7
  168. package/src/lib/model/plugin-permissions.js.map +1 -1
  169. package/src/lib/model/pos-cellular.d.ts +223 -0
  170. package/src/lib/model/pos-cellular.js +293 -0
  171. package/src/lib/model/pos-cellular.js.map +1 -0
  172. package/src/lib/model/pos-hardware-in-person.d.ts +98 -0
  173. package/src/lib/model/pos-hardware-in-person.js +224 -0
  174. package/src/lib/model/pos-hardware-in-person.js.map +1 -0
  175. package/src/lib/model/pos-hardware-tax-summary.d.ts +166 -0
  176. package/src/lib/model/pos-hardware-tax-summary.js +225 -0
  177. package/src/lib/model/pos-hardware-tax-summary.js.map +1 -0
  178. package/src/lib/model/pos-hardware-terms.d.ts +74 -0
  179. package/src/lib/model/pos-hardware-terms.js +115 -0
  180. package/src/lib/model/pos-hardware-terms.js.map +1 -0
  181. package/src/lib/model/pos-hardware.d.ts +349 -7
  182. package/src/lib/model/pos-hardware.js +291 -20
  183. package/src/lib/model/pos-hardware.js.map +1 -1
  184. package/src/lib/model/product-write.d.ts +10 -0
  185. package/src/lib/model/product-write.js +4 -2
  186. package/src/lib/model/product-write.js.map +1 -1
  187. package/src/lib/plugin.js +16 -0
  188. package/src/lib/plugin.js.map +1 -1
  189. package/src/lib/server/billing-webhook.js +25 -1
  190. package/src/lib/server/billing-webhook.js.map +1 -1
  191. package/src/lib/server/cancel-order.js +7 -5
  192. package/src/lib/server/cancel-order.js.map +1 -1
  193. package/src/lib/server/cart-checkout.js +20 -8
  194. package/src/lib/server/cart-checkout.js.map +1 -1
  195. package/src/lib/server/cart.js +29 -0
  196. package/src/lib/server/cart.js.map +1 -1
  197. package/src/lib/server/checkout.js +42 -13
  198. package/src/lib/server/checkout.js.map +1 -1
  199. package/src/lib/server/collection-membership.d.ts +4 -0
  200. package/src/lib/server/collection-membership.js +5 -10
  201. package/src/lib/server/collection-membership.js.map +1 -1
  202. package/src/lib/server/commerce-site-keys.d.ts +29 -0
  203. package/src/lib/server/commerce-site-keys.js +40 -0
  204. package/src/lib/server/commerce-site-keys.js.map +1 -0
  205. package/src/lib/server/draft-order.js +8 -5
  206. package/src/lib/server/draft-order.js.map +1 -1
  207. package/src/lib/server/fulfill-order.js +7 -5
  208. package/src/lib/server/fulfill-order.js.map +1 -1
  209. package/src/lib/server/gift-cards.js +8 -5
  210. package/src/lib/server/gift-cards.js.map +1 -1
  211. package/src/lib/server/local-fulfillment-status.js +9 -6
  212. package/src/lib/server/local-fulfillment-status.js.map +1 -1
  213. package/src/lib/server/member-post.js +6 -5
  214. package/src/lib/server/member-post.js.map +1 -1
  215. package/src/lib/server/membership-admin-password.d.ts +4 -2
  216. package/src/lib/server/membership-admin-password.js +10 -8
  217. package/src/lib/server/membership-admin-password.js.map +1 -1
  218. package/src/lib/server/membership-admin-remove.d.ts +7 -5
  219. package/src/lib/server/membership-admin-remove.js +9 -9
  220. package/src/lib/server/membership-admin-remove.js.map +1 -1
  221. package/src/lib/server/order-notifications.js +5 -2
  222. package/src/lib/server/order-notifications.js.map +1 -1
  223. package/src/lib/server/order-print.js +7 -5
  224. package/src/lib/server/order-print.js.map +1 -1
  225. package/src/lib/server/order-receipt-send.js +8 -5
  226. package/src/lib/server/order-receipt-send.js.map +1 -1
  227. package/src/lib/server/orders-shipping-prepare.js +9 -6
  228. package/src/lib/server/orders-shipping-prepare.js.map +1 -1
  229. package/src/lib/server/own-store-tax-return-source.d.ts +47 -0
  230. package/src/lib/server/own-store-tax-return-source.js +312 -0
  231. package/src/lib/server/own-store-tax-return-source.js.map +1 -0
  232. package/src/lib/server/payment-methods.js +13 -9
  233. package/src/lib/server/payment-methods.js.map +1 -1
  234. package/src/lib/server/pos-auth.d.ts +37 -6
  235. package/src/lib/server/pos-auth.js +40 -15
  236. package/src/lib/server/pos-auth.js.map +1 -1
  237. package/src/lib/server/pos-cellular-billing.d.ts +81 -0
  238. package/src/lib/server/pos-cellular-billing.js +711 -0
  239. package/src/lib/server/pos-cellular-billing.js.map +1 -0
  240. package/src/lib/server/pos-cellular-notify.d.ts +47 -0
  241. package/src/lib/server/pos-cellular-notify.js +150 -0
  242. package/src/lib/server/pos-cellular-notify.js.map +1 -0
  243. package/src/lib/server/pos-cellular.d.ts +88 -0
  244. package/src/lib/server/pos-cellular.js +339 -0
  245. package/src/lib/server/pos-cellular.js.map +1 -0
  246. package/src/lib/server/pos-check-in.d.ts +5 -1
  247. package/src/lib/server/pos-check-in.js +296 -28
  248. package/src/lib/server/pos-check-in.js.map +1 -1
  249. package/src/lib/server/pos-day-report.js +29 -3
  250. package/src/lib/server/pos-day-report.js.map +1 -1
  251. package/src/lib/server/pos-display.d.ts +16 -0
  252. package/src/lib/server/pos-display.js +129 -2
  253. package/src/lib/server/pos-display.js.map +1 -1
  254. package/src/lib/server/pos-hardware-admin.d.ts +10 -1
  255. package/src/lib/server/pos-hardware-admin.js +250 -84
  256. package/src/lib/server/pos-hardware-admin.js.map +1 -1
  257. package/src/lib/server/pos-hardware-attach.d.ts +105 -0
  258. package/src/lib/server/pos-hardware-attach.js +443 -0
  259. package/src/lib/server/pos-hardware-attach.js.map +1 -0
  260. package/src/lib/server/pos-hardware-buyer-lookup.d.ts +58 -0
  261. package/src/lib/server/pos-hardware-buyer-lookup.js +180 -0
  262. package/src/lib/server/pos-hardware-buyer-lookup.js.map +1 -0
  263. package/src/lib/server/pos-hardware-buyer-record.d.ts +29 -0
  264. package/src/lib/server/pos-hardware-buyer-record.js +86 -0
  265. package/src/lib/server/pos-hardware-buyer-record.js.map +1 -0
  266. package/src/lib/server/pos-hardware-cancel.d.ts +58 -0
  267. package/src/lib/server/pos-hardware-cancel.js +147 -0
  268. package/src/lib/server/pos-hardware-cancel.js.map +1 -0
  269. package/src/lib/server/pos-hardware-closure-notices.d.ts +27 -0
  270. package/src/lib/server/pos-hardware-closure-notices.js +85 -0
  271. package/src/lib/server/pos-hardware-closure-notices.js.map +1 -0
  272. package/src/lib/server/pos-hardware-closure.d.ts +60 -0
  273. package/src/lib/server/pos-hardware-closure.js +153 -0
  274. package/src/lib/server/pos-hardware-closure.js.map +1 -0
  275. package/src/lib/server/pos-hardware-in-person.d.ts +61 -0
  276. package/src/lib/server/pos-hardware-in-person.js +236 -0
  277. package/src/lib/server/pos-hardware-in-person.js.map +1 -0
  278. package/src/lib/server/pos-hardware-notify.d.ts +40 -2
  279. package/src/lib/server/pos-hardware-notify.js +195 -14
  280. package/src/lib/server/pos-hardware-notify.js.map +1 -1
  281. package/src/lib/server/pos-hardware-readers.d.ts +115 -0
  282. package/src/lib/server/pos-hardware-readers.js +430 -0
  283. package/src/lib/server/pos-hardware-readers.js.map +1 -0
  284. package/src/lib/server/pos-hardware-returns.d.ts +116 -0
  285. package/src/lib/server/pos-hardware-returns.js +502 -0
  286. package/src/lib/server/pos-hardware-returns.js.map +1 -0
  287. package/src/lib/server/pos-hardware-sale-fields.d.ts +28 -0
  288. package/src/lib/server/pos-hardware-sale-fields.js +44 -0
  289. package/src/lib/server/pos-hardware-sale-fields.js.map +1 -0
  290. package/src/lib/server/pos-hardware-sale.d.ts +54 -0
  291. package/src/lib/server/pos-hardware-sale.js +369 -0
  292. package/src/lib/server/pos-hardware-sale.js.map +1 -0
  293. package/src/lib/server/pos-hardware-tax-return-source.d.ts +99 -0
  294. package/src/lib/server/pos-hardware-tax-return-source.js +462 -0
  295. package/src/lib/server/pos-hardware-tax-return-source.js.map +1 -0
  296. package/src/lib/server/pos-hardware-terms.d.ts +64 -0
  297. package/src/lib/server/pos-hardware-terms.js +154 -0
  298. package/src/lib/server/pos-hardware-terms.js.map +1 -0
  299. package/src/lib/server/pos-hardware-user-eraser.d.ts +3 -0
  300. package/src/lib/server/pos-hardware-user-eraser.js +31 -7
  301. package/src/lib/server/pos-hardware-user-eraser.js.map +1 -1
  302. package/src/lib/server/pos-hardware-webhook.d.ts +5 -0
  303. package/src/lib/server/pos-hardware-webhook.js +51 -6
  304. package/src/lib/server/pos-hardware-webhook.js.map +1 -1
  305. package/src/lib/server/pos-hardware.d.ts +74 -1
  306. package/src/lib/server/pos-hardware.js +175 -10
  307. package/src/lib/server/pos-hardware.js.map +1 -1
  308. package/src/lib/server/pos-kiosk.d.ts +2 -0
  309. package/src/lib/server/pos-kiosk.js +17 -12
  310. package/src/lib/server/pos-kiosk.js.map +1 -1
  311. package/src/lib/server/pos-offline-card-order.d.ts +64 -0
  312. package/src/lib/server/pos-offline-card-order.js +102 -0
  313. package/src/lib/server/pos-offline-card-order.js.map +1 -0
  314. package/src/lib/server/pos-offline-card-reconcile.d.ts +67 -1
  315. package/src/lib/server/pos-offline-card-reconcile.js +215 -18
  316. package/src/lib/server/pos-offline-card-reconcile.js.map +1 -1
  317. package/src/lib/server/pos-offline-sync.d.ts +39 -0
  318. package/src/lib/server/pos-offline-sync.js +190 -55
  319. package/src/lib/server/pos-offline-sync.js.map +1 -1
  320. package/src/lib/server/pos-ops-gate.d.ts +15 -4
  321. package/src/lib/server/pos-ops-gate.js +42 -9
  322. package/src/lib/server/pos-ops-gate.js.map +1 -1
  323. package/src/lib/server/pos-ops-routes.js +24 -0
  324. package/src/lib/server/pos-ops-routes.js.map +1 -1
  325. package/src/lib/server/pos-order-refund-ids.d.ts +28 -0
  326. package/src/lib/server/pos-order-refund-ids.js +31 -0
  327. package/src/lib/server/pos-order-refund-ids.js.map +1 -0
  328. package/src/lib/server/pos-order.js +52 -49
  329. package/src/lib/server/pos-order.js.map +1 -1
  330. package/src/lib/server/pos-payment.js +1 -1
  331. package/src/lib/server/pos-payment.js.map +1 -1
  332. package/src/lib/server/pos-reader-splash-admin.js +30 -32
  333. package/src/lib/server/pos-reader-splash-admin.js.map +1 -1
  334. package/src/lib/server/pos-reader-splash-images-route.d.ts +17 -0
  335. package/src/lib/server/pos-reader-splash-images-route.js +113 -0
  336. package/src/lib/server/pos-reader-splash-images-route.js.map +1 -0
  337. package/src/lib/server/pos-reader-splash-images.d.ts +51 -0
  338. package/src/lib/server/pos-reader-splash-images.js +123 -0
  339. package/src/lib/server/pos-reader-splash-images.js.map +1 -0
  340. package/src/lib/server/pos-reader-splash.d.ts +68 -1
  341. package/src/lib/server/pos-reader-splash.js +182 -11
  342. package/src/lib/server/pos-reader-splash.js.map +1 -1
  343. package/src/lib/server/pos-readers.d.ts +29 -0
  344. package/src/lib/server/pos-readers.js +134 -35
  345. package/src/lib/server/pos-readers.js.map +1 -1
  346. package/src/lib/server/pos-return.d.ts +10 -0
  347. package/src/lib/server/pos-return.js +26 -1
  348. package/src/lib/server/pos-return.js.map +1 -1
  349. package/src/lib/server/pos-sale.js +6 -1
  350. package/src/lib/server/pos-sale.js.map +1 -1
  351. package/src/lib/server/pos-shift.js +11 -0
  352. package/src/lib/server/pos-shift.js.map +1 -1
  353. package/src/lib/server/pos-staff-pin.js +25 -20
  354. package/src/lib/server/pos-staff-pin.js.map +1 -1
  355. package/src/lib/server/pos-terminal.d.ts +14 -4
  356. package/src/lib/server/pos-terminal.js +34 -38
  357. package/src/lib/server/pos-terminal.js.map +1 -1
  358. package/src/lib/server/printers.d.ts +7 -3
  359. package/src/lib/server/printers.js +18 -12
  360. package/src/lib/server/printers.js.map +1 -1
  361. package/src/lib/server/product-drafts.d.ts +11 -1
  362. package/src/lib/server/product-drafts.js +12 -8
  363. package/src/lib/server/product-drafts.js.map +1 -1
  364. package/src/lib/server/product.d.ts +15 -1
  365. package/src/lib/server/product.js +16 -3
  366. package/src/lib/server/product.js.map +1 -1
  367. package/src/lib/server/products-write.d.ts +26 -2
  368. package/src/lib/server/products-write.js +19 -7
  369. package/src/lib/server/products-write.js.map +1 -1
  370. package/src/lib/server/refund.js +34 -11
  371. package/src/lib/server/refund.js.map +1 -1
  372. package/src/lib/server/reservation-availability-query.d.ts +8 -0
  373. package/src/lib/server/reservation-availability-query.js +11 -1
  374. package/src/lib/server/reservation-availability-query.js.map +1 -1
  375. package/src/lib/server/reservation-availability.d.ts +1 -1
  376. package/src/lib/server/reservation-availability.js +7 -2
  377. package/src/lib/server/reservation-availability.js.map +1 -1
  378. package/src/lib/server/reservation-guests.d.ts +32 -0
  379. package/src/lib/server/reservation-guests.js +285 -0
  380. package/src/lib/server/reservation-guests.js.map +1 -0
  381. package/src/lib/server/reservation-walk-in.d.ts +11 -1
  382. package/src/lib/server/reservation-walk-in.js +56 -12
  383. package/src/lib/server/reservation-walk-in.js.map +1 -1
  384. package/src/lib/server/reserve.js +18 -5
  385. package/src/lib/server/reserve.js.map +1 -1
  386. package/src/lib/server/returns.js +9 -6
  387. package/src/lib/server/returns.js.map +1 -1
  388. package/src/lib/server/reviews.d.ts +22 -2
  389. package/src/lib/server/reviews.js +88 -20
  390. package/src/lib/server/reviews.js.map +1 -1
  391. package/src/lib/server/shipping-connectors.js +10 -11
  392. package/src/lib/server/shipping-connectors.js.map +1 -1
  393. package/src/lib/server/site-page-resolver.js +20 -3
  394. package/src/lib/server/site-page-resolver.js.map +1 -1
  395. package/src/lib/server/store-day.d.ts +33 -0
  396. package/src/lib/server/store-day.js +46 -0
  397. package/src/lib/server/store-day.js.map +1 -0
  398. package/src/lib/server/store-setup-ai-writer.d.ts +2 -1
  399. package/src/lib/server/store-setup-ai-writer.js +5 -6
  400. package/src/lib/server/store-setup-ai-writer.js.map +1 -1
  401. package/src/lib/server/storefront-tax.d.ts +13 -0
  402. package/src/lib/server/storefront-tax.js +8 -1
  403. package/src/lib/server/storefront-tax.js.map +1 -1
  404. package/src/lib/server/tax-return-source.d.ts +32 -3
  405. package/src/lib/server/tax-return-source.js +99 -20
  406. package/src/lib/server/tax-return-source.js.map +1 -1
  407. package/src/lib/server.js +100 -2
  408. package/src/lib/server.js.map +1 -1
  409. package/src/lib/testing/fake-firestore.js +58 -16
  410. package/src/lib/testing/fake-firestore.js.map +1 -1
  411. package/src/lib/testing/pos-ops-memory-firestore.js +18 -4
  412. package/src/lib/testing/pos-ops-memory-firestore.js.map +1 -1
  413. package/src/lib/testing/site-register-permissions.d.ts +29 -0
  414. package/src/lib/testing/site-register-permissions.js +38 -0
  415. package/src/lib/testing/site-register-permissions.js.map +1 -0
  416. package/src/lib/transfer/tracking.server.js +8 -5
  417. package/src/lib/transfer/tracking.server.js.map +1 -1
  418. package/src/lib/utils/add-to-cart.d.ts +2 -0
  419. package/src/lib/utils/add-to-cart.js +6 -3
  420. package/src/lib/utils/add-to-cart.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/fulfill-order.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as CommerceModel from '../model'\nimport { announcePluginShipment } from '@aglyn/aglyn/plugin-manager/plugin-shipment-records'\nimport { createResourceUid, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { notifyOrderBuyer } from './order-notifications'\nimport { ORDER_DELIVERED_EVENT, ORDER_FULFILLED_EVENT } from '../model/order-events'\nimport { fulfillmentEventView, stageOrderEvent } from './order-events'\nimport { createHash } from 'crypto'\n\n/**\n * The two fulfilment-side transitions, and ONLY those.\n *\n * `cancelled` releases stock under its own transaction and `refunded` moves\n * money under another, so neither belongs to a write whose contract is \"a\n * forward status flip plus a timeline entry, no stock moved, no money moved\".\n * Widening this union is how a caller gets a door around the specifics those\n * routes exist to enforce — the type is the door being shut.\n */\nexport type OrderFulfilmentTarget = 'fulfilled' | 'delivered'\n\n/** How long a carrier name and a tracking number may be, on every door. */\nexport const CARRIER_MAX = 40\nexport const TRACKING_NUMBER_MAX = 60\nconst URL_MAX = 2048\n\nexport interface RecordShipmentRequest {\n /** Site that owns the order. The CALLER has already proven ownership. */\n hostId: string\n orderId: string\n to: OrderFulfilmentTarget\n /** Free text, e.g. `'UPS'`. Bounded by the caller. */\n carrier?: string\n trackingNumber?: string\n /**\n * Which units this shipment covers (AGL-3611). Absent means everything\n * still to ship, which is what a fulfil meant before quantities existed.\n * Ignored for `delivered`.\n */\n lineItems?: ReadonlyArray<{ lineItemId: number; quantity: number }>\n /** An explicit tracking link; without one it is derived from the carrier. */\n trackingUrl?: string\n /** A label a shipping plugin bought for this parcel. */\n labelUrl?: string\n /** That plugin's id for the label (AGL-3612), kept on the fulfillment. */\n labelRef?: string\n /** What that label cost, in integer cents of the order's currency (AGL-3693). */\n labelCostCents?: number\n /**\n * What the merchant says this shipment cost to send, typed by hand in the\n * fulfill dialog (AGL-3705): integer cents of the store's currency, 0 for\n * free shipping. Stored in the SAME field a label's cost is\n * (`labelCostCents`), so it rides `order.fulfilled` to a marketplace that\n * pays shipping back. A label's own cost wins when both are given. Absent\n * means not known — nothing is stored. The caller validates\n * (`shippingCostCentsProblem`); an invalid value here is dropped.\n */\n shippingCostCents?: number\n /** Whether the buyer is to be told; recorded on the fulfillment. Default on. */\n notify?: boolean\n /**\n * One shipment attempt. A retry carrying the same key finds the fulfillment\n * it already wrote and answers `already`, so a partial shipment, which\n * leaves the order open to more, still cannot be recorded twice.\n */\n idempotencyKey?: string\n /**\n * A shipment whose tracking number an active fulfillment of this order\n * already carries is the same parcel, answered `already` (AGL-3613). For\n * the doors a shipping tool posts to — ShipStation's ShipNotice, a tracking\n * file — where the number is the parcel's identity and the same parcel may\n * have been recorded by hand first.\n */\n onceByTracking?: boolean\n}\n\n/**\n * What happened, as a domain fact rather than an HTTP status — each caller\n * maps it into its own error vocabulary (the console route into its JSON\n * shape, `/v1` into the published error envelope), because those two\n * vocabularies are contracts with different audiences and neither may leak\n * into the other.\n *\n * `already` is a SUCCESS and is distinct from `recorded` on purpose: a retried\n * request finds the order in the target status (or its keyed fulfillment\n * already written) and returns without writing, so a lost response can never\n * append a second copy of the same shipment.\n *\n * `recorded` carries the fulfillment it wrote (absent for `delivered`) and the\n * status the order is now in, so a caller can tell the buyer what shipped.\n */\nexport type RecordShipmentOutcome =\n | {\n outcome: 'recorded'\n status: CommerceModel.OrderStatus\n fulfillment?: CommerceModel.OrderFulfillment\n }\n | { outcome: 'already'; fulfillment?: CommerceModel.OrderFulfillment }\n | { outcome: 'no_such_order' }\n /** The transition rule refused. `from` is the status that refused it. */\n | { outcome: 'blocked'; from: string }\n /** The requested lines or quantities were refused; nothing was written. */\n | { outcome: 'invalid_lines'; problem: CommerceModel.FulfillmentLinesProblem; message: string }\n\n/** Trims a carrier and a tracking number to what every door stores. */\nexport function boundTracking(input: { carrier?: unknown; trackingNumber?: unknown }): {\n carrier: string\n trackingNumber: string\n} {\n return {\n carrier: String(input.carrier ?? '').trim().slice(0, CARRIER_MAX),\n trackingNumber: String(input.trackingNumber ?? '').trim().slice(0, TRACKING_NUMBER_MAX),\n }\n}\n\n/** An https link or nothing: a stored link is rendered to buyers as an anchor. */\nexport function safeHttpsUrl(value: unknown): string | undefined {\n const text = String(value ?? '').trim()\n if (!text || text.length > URL_MAX) return undefined\n try {\n const url = new URL(text)\n return url.protocol === 'https:' ? url.toString() : undefined\n } catch {\n return undefined\n }\n}\n\n/** The fulfillment id a keyed attempt writes, so a retry finds it. */\nexport function fulfillmentIdForKey(orderId: string, key: string): string {\n return `f-${createHash('sha256').update(`${orderId}:${key}`).digest('hex').slice(0, 20)}`\n}\n\n/** The tracking keys a fulfillment stores: the number, the carrier and the link. */\nfunction trackingFields(\n carrier: string,\n trackingNumber: string,\n explicitUrl: string | undefined,\n): Pick<CommerceModel.OrderFulfillment, 'carrier' | 'trackingNumber' | 'trackingUrl'> {\n const trackingUrl =\n explicitUrl ?? (trackingNumber ? CommerceModel.trackingUrlFor(carrier, trackingNumber) ?? undefined : undefined)\n return {\n ...(carrier ? { carrier } : {}),\n ...(trackingNumber ? { trackingNumber } : {}),\n ...(trackingUrl ? { trackingUrl } : {}),\n }\n}\n\n/** The timeline's words for a shipment. */\nfunction shipmentDetail(\n order: CommerceModel.HostOrder,\n lines: readonly CommerceModel.OrderFulfillmentLine[],\n partial: boolean,\n carrier: string,\n trackingNumber: string,\n): string {\n const tracking = trackingNumber ? `${carrier || 'Shipped'} ${trackingNumber}` : ''\n if (!partial) return tracking || 'Fulfilled'\n const what = CommerceModel.describeFulfillmentLines(order, lines)\n return tracking ? `${what} — ${tracking}` : `${what} shipped`\n}\n\nfunction orderRefFor(hostId: string, orderId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('orders')\n .doc(orderId)\n}\n\n/**\n * Record a shipment — the transaction, with NO authorization of any kind\n * (AGL-2461).\n *\n * This is the whole of what `fulfillOrderHandler` used to do below its auth\n * preamble, lifted out unchanged so a SECOND caller can have it: the customer\n * REST API's `PATCH /v1/sites/{id}/orders/{id}`\n * (`api-v1/orders-and-products.ts`), which authenticates an org API key and\n * so shares not one line of this function's former preamble.\n *\n * Lifted rather than copied, and that is the entire point: a second\n * `ORDER_TRANSITIONS` inside `/v1` would be two tables that drift into the\n * API writing a status the console forbids.\n *\n * **The caller authorizes.** Everything this function knows is the transition\n * rule. It does not know who is asking, and it will happily move any order on\n * any host — `hostId` is taken on trust because its two callers establish\n * trust in ways that have nothing in common (a Firebase uid in `memberRoles`\n * for the console; an org credential that owns the host for `/v1`).\n *\n * QUANTITIES (AGL-3611). A shipment names units, not just lines, and the\n * remaining count is computed from the transaction's own read, so two admins\n * shipping the last 3 mugs at once cannot ship 6: the second meets\n * `invalid_lines` with `over_fulfilled`. The resulting status is computed\n * from the fulfillments (`statusFromFulfillments`) — `partially_fulfilled`\n * until every shippable unit is covered — and the transition rule is re-asked\n * only when the status actually moves, since a second partial shipment on a\n * `partially_fulfilled` order changes nothing the table speaks to.\n *\n * Every guarantee AGL-1819 built is inside the transaction and therefore\n * inherited by both callers: `canTransitionOrder` is re-asked under the write\n * so a stale caller cannot skip a state, the already-in-target case returns\n * without writing so a retry cannot append a duplicate shipment, and the\n * lines and the timeline are computed from the transaction's OWN read so a\n * note landed from another tab survives.\n */\nexport async function recordOrderShipment(\n request: RecordShipmentRequest,\n): Promise<RecordShipmentOutcome> {\n const { hostId, orderId, to } = request\n const { carrier, trackingNumber } = boundTracking(request)\n const explicitUrl = safeHttpsUrl(request.trackingUrl)\n const labelUrl = safeHttpsUrl(request.labelUrl)\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = orderRefFor(hostId, orderId)\n const keyedId = request.idempotencyKey\n ? fulfillmentIdForKey(orderId, String(request.idempotencyKey).slice(0, 200))\n : null\n\n const result = await firestore.runTransaction(\n async (transaction): Promise<RecordShipmentOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder(\n (snapshot.data() ?? {}) as never,\n )\n const atMs = Date.now()\n\n if (to === 'delivered') {\n if (order.status === 'delivered') return { outcome: 'already' }\n if (!CommerceModel.canTransitionOrder(order.status, 'delivered')) {\n return { outcome: 'blocked', from: order.status }\n }\n const delivered = {\n status: 'delivered',\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(order, 'delivered', undefined, atMs),\n }\n transaction.update(orderRef, delivered)\n stageOrderEvent(transaction, ORDER_DELIVERED_EVENT, { hostId, orderId, key: 'delivered', order: { ...snapshot.data(), ...delivered } })\n return { outcome: 'recorded', status: 'delivered' }\n }\n\n // A redelivered keyed attempt: the fulfillment it wrote is there.\n if (keyedId) {\n const existing = (order.fulfillments ?? []).find((entry) => entry.id === keyedId)\n if (existing) return { outcome: 'already', fulfillment: existing }\n }\n if (request.onceByTracking && trackingNumber) {\n const same = CommerceModel.fulfillmentWithTracking(order, trackingNumber)\n if (same) return { outcome: 'already', fulfillment: same }\n }\n const named = (request.lineItems ?? []).length > 0\n // A redelivered click, or the second of two admins, on a \"fulfil\n // everything\". Success because it IS the state the caller asked for —\n // but nothing is written, so a retry cannot append a second copy.\n if (!named && order.status === 'fulfilled') return { outcome: 'already' }\n if (\n order.status !== 'partially_fulfilled' &&\n order.status !== 'fulfilled' &&\n !CommerceModel.canTransitionOrder(order.status, 'partially_fulfilled') &&\n !CommerceModel.canTransitionOrder(order.status, 'fulfilled')\n ) {\n return { outcome: 'blocked', from: order.status }\n }\n\n let lines: CommerceModel.OrderFulfillmentLine[]\n if (named) {\n const resolved = CommerceModel.resolveFulfillmentLines(order, request.lineItems ?? [])\n if (!('lines' in resolved)) {\n return {\n outcome: 'invalid_lines',\n problem: resolved,\n message: CommerceModel.describeFulfillmentLinesProblem(resolved),\n }\n }\n lines = resolved.lines\n } else {\n lines = CommerceModel.remainingFulfillmentLines(order)\n // Nothing physical left: a fulfil closes the digital and service\n // lines instead, which is what it did before quantities existed.\n if (lines.length === 0) {\n lines = CommerceModel.orderLineFulfillmentStates(order)\n .filter((state) => state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n }\n }\n\n const fulfillment: CommerceModel.OrderFulfillment = {\n id: keyedId ?? createResourceUid(),\n lineItemIds: lines.map((entry) => entry.lineItemId),\n lines,\n ...trackingFields(carrier, trackingNumber, explicitUrl),\n ...(labelUrl ? { labelUrl } : {}),\n ...(request.labelRef ? { labelRef: String(request.labelRef).slice(0, 80) } : {}),\n ...(Number.isSafeInteger(request.labelCostCents) && Number(request.labelCostCents) > 0\n ? { labelCostCents: Number(request.labelCostCents) }\n : request.shippingCostCents !== undefined &&\n CommerceModel.shippingCostCentsProblem(request.shippingCostCents) === null\n ? { labelCostCents: request.shippingCostCents }\n : {}),\n status: 'active',\n notify: request.notify !== false,\n atMs,\n }\n const fulfillments = [...(order.fulfillments ?? []), fulfillment]\n let status: CommerceModel.OrderStatus = CommerceModel.statusFromFulfillments({\n lineItems: order.lineItems,\n fulfillments,\n })\n // A fulfil with nothing left to cover still closes the order.\n if (status === 'paid') status = 'fulfilled'\n if (status !== order.status && !CommerceModel.canTransitionOrder(order.status, status)) {\n return { outcome: 'blocked', from: order.status }\n }\n const shipped = {\n status,\n fulfillments,\n // What a shipping tool's feed asks (AGL-3613): which orders changed.\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n status,\n shipmentDetail(order, lines, status === 'partially_fulfilled', carrier, trackingNumber),\n atMs,\n ),\n }\n transaction.update(orderRef, shipped)\n // Once per shipment (AGL-3611), in the same write, so the event exists\n // exactly when the shipment does.\n stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, { hostId, orderId, key: fulfillment.id, order: { ...snapshot.data(), ...shipped }, extra: { fulfillment: fulfillmentEventView(order, fulfillment) } })\n return { outcome: 'recorded', status, fulfillment }\n },\n )\n // The BUYER hears about it (AGL-3610): shipped with the tracking link, or\n // delivered. Once per shipment, after the write, and never fails the\n // shipment — see `order-notifications.ts`. A retry returns `already` above\n // and never reaches here. A merchant who unticked \"Notify customer\"\n // (AGL-3611) recorded the shipment with `notify: false`, and nobody is told.\n // Every shipment recorded, by any door, is announced (AGL-3612), so a\n // plugin that follows parcels can follow one typed in by hand too.\n if (result.outcome === 'recorded' && result.fulfillment?.trackingNumber) {\n await announcePluginShipment({\n hostId,\n recordId: orderId,\n shipmentId: result.fulfillment.id,\n ...(result.fulfillment.carrier ? { carrier: result.fulfillment.carrier } : {}),\n trackingNumber: result.fulfillment.trackingNumber,\n ...(request.labelRef ? { labelRef: request.labelRef } : {}),\n })\n }\n if (result.outcome === 'recorded' && result.fulfillment?.notify !== false) {\n await notifyOrderBuyer(\n { hostId, orderId },\n to === 'fulfilled' ? 'shipped' : 'delivered',\n result.fulfillment ? { fulfillmentId: result.fulfillment.id } : {},\n )\n }\n return result\n}\n\n/** What an edit or a cancel of one fulfillment came to. */\nexport type FulfillmentChangeOutcome =\n | { outcome: 'updated'; status: CommerceModel.OrderStatus; fulfillment: CommerceModel.OrderFulfillment }\n | { outcome: 'already'; fulfillment: CommerceModel.OrderFulfillment }\n | { outcome: 'no_such_order' }\n | { outcome: 'no_such_fulfillment' }\n /** The order is history (delivered, refunded, cancelled); `from` names it. */\n | { outcome: 'locked'; from: string }\n\n/**\n * Corrects the tracking on one fulfillment (AGL-3611): a mistyped number, a\n * carrier swapped, a label bought after the parcel was recorded. Moves no\n * status and no stock. A cancelled fulfillment is history and is refused.\n */\nexport async function updateFulfillmentTracking(request: {\n hostId: string\n orderId: string\n fulfillmentId: string\n carrier?: string\n trackingNumber?: string\n trackingUrl?: string\n}): Promise<FulfillmentChangeOutcome> {\n const { carrier, trackingNumber } = boundTracking(request)\n const explicitUrl = safeHttpsUrl(request.trackingUrl)\n const orderRef = orderRefFor(request.hostId, request.orderId)\n return firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction): Promise<FulfillmentChangeOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const index = (order.fulfillments ?? []).findIndex((entry) => entry.id === request.fulfillmentId)\n if (index < 0) return { outcome: 'no_such_fulfillment' }\n const current = (order.fulfillments ?? [])[index]\n if (!CommerceModel.fulfillmentIsActive(current) || !['paid', 'partially_fulfilled', 'fulfilled', 'delivered'].includes(order.status)) {\n return { outcome: 'locked', from: CommerceModel.fulfillmentIsActive(current) ? order.status : 'cancelled' }\n }\n const atMs = Date.now()\n const next: CommerceModel.OrderFulfillment = { ...current, updatedAtMs: atMs }\n delete next.carrier\n delete next.trackingNumber\n delete next.trackingUrl\n Object.assign(next, trackingFields(carrier, trackingNumber, explicitUrl))\n if (\n (current.carrier ?? '') === (next.carrier ?? '') &&\n (current.trackingNumber ?? '') === (next.trackingNumber ?? '') &&\n (current.trackingUrl ?? '') === (next.trackingUrl ?? '')\n ) {\n return { outcome: 'already', fulfillment: current }\n }\n const fulfillments = [...(order.fulfillments ?? [])]\n fulfillments[index] = next\n transaction.update(orderRef, {\n fulfillments,\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'tracking-updated',\n trackingNumber ? `${carrier || 'Tracking'} ${trackingNumber}` : 'Tracking removed',\n atMs,\n ),\n })\n return { outcome: 'updated', status: order.status, fulfillment: next }\n })\n}\n\n/**\n * Cancels one fulfillment (AGL-3611): the parcel was never handed over, or was\n * recorded against the wrong items. Its units go back to unfulfilled and the\n * order's status is recomputed from what remains — `fulfilled` can step back\n * to `partially_fulfilled` or `paid`.\n *\n * That step back is NOT an `ORDER_TRANSITIONS` edge, on purpose: the table\n * says what may happen to an order going forward, and admitting\n * `fulfilled → paid` there would open it to every other caller. This is the one writer\n * that may take it, and only from a status whose fulfillments are still open\n * (`orderFulfillmentsEditable`): a delivered, refunded or cancelled order's\n * shipments are what happened and stay that way.\n */\nexport async function cancelOrderFulfillment(request: {\n hostId: string\n orderId: string\n fulfillmentId: string\n}): Promise<FulfillmentChangeOutcome> {\n const orderRef = orderRefFor(request.hostId, request.orderId)\n return firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction): Promise<FulfillmentChangeOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const index = (order.fulfillments ?? []).findIndex((entry) => entry.id === request.fulfillmentId)\n if (index < 0) return { outcome: 'no_such_fulfillment' }\n const current = (order.fulfillments ?? [])[index]\n if (!CommerceModel.fulfillmentIsActive(current)) return { outcome: 'already', fulfillment: current }\n if (!CommerceModel.orderFulfillmentsEditable(order)) {\n return { outcome: 'locked', from: order.status }\n }\n const atMs = Date.now()\n const next: CommerceModel.OrderFulfillment = {\n ...current,\n status: 'cancelled',\n cancelledAtMs: atMs,\n updatedAtMs: atMs,\n }\n const fulfillments = [...(order.fulfillments ?? [])]\n fulfillments[index] = next\n const status = CommerceModel.statusFromFulfillments({ lineItems: order.lineItems, fulfillments })\n transaction.update(orderRef, {\n status,\n fulfillments,\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'fulfillment-cancelled',\n CommerceModel.describeFulfillmentLines(order, CommerceModel.fulfillmentLineQuantities(order, current)) +\n ' back to unfulfilled',\n atMs,\n ),\n })\n return { outcome: 'updated', status, fulfillment: next }\n })\n}\n\n/**\n * Fulfil and mark-delivered, with the transition re-asked under the write\n * (AGL-1819) — the last two order-status writes the console made with a client\n * `updateDoc`, moved behind the same shape as `cancel-order.ts`.\n *\n * THE STALE-DIALOG HOLE IS THE SAME ONE AGL-1818 CLOSED FOR CANCEL.\n * `canTransitionOrder` was consulted only to RENDER the buttons, so a dialog\n * opened on a `paid` order that was refunded (or cancelled) in another tab\n * could still write `fulfilled` straight onto it — `ORDER_TRANSITIONS` stayed\n * advisory because nothing consulted it on the way IN. Re-reading the order\n * inside the transaction that writes the status is what turns the table into\n * a guarantee; the race comes back as a 409 naming the state that refused.\n *\n * ONE ROUTE FOR THE TWO FULFILMENT-SIDE TRANSITIONS, and ONLY those. They are\n * the same act — a forward status flip plus a timeline entry, no stock moved,\n * no money moved — so they share a handler parameterized on `to`. It refuses\n * every other target with a 400 on purpose: `cancelled` releases stock under\n * its own transaction (`cancel-order.ts`) and `refunded` moves money\n * (`refund.ts`), so admitting them here would hand callers a door around\n * exactly the specifics those routes exist to enforce.\n *\n * NO STOCK MOVES HERE. The sale decremented inventory when the money landed\n * (see `cancel-order.ts`'s header); shipping the goods changes what is on the\n * shelf not at all. An open `restockCheck` — a partial refund's question,\n * flagged before the merchant ships the rest — survives untouched: the write\n * is an `update()` naming only status, fulfillments and timeline, and the\n * question is still the merchant's to answer.\n *\n * NO MANAGER NOTIFICATION, matching the client write this replaces: the\n * merchant fulfilled the order themselves, and `notifyHostManagers` would\n * tell them what they just did. (`supplier-update.ts` notifies because there\n * the SUPPLIER acted and the merchant is the one who needs to hear it.) The\n * BUYER is told, through `recordOrderShipment` (AGL-3610).\n *\n * ONCE. A retried request — a lost response, a second click — finds the order\n * already in the target status and returns success WITHOUT writing: appending\n * a second copy of the same fulfillment is the double this guard exists to\n * stop. It reports `already: true` so the console can say so. A fulfil retried\n * after the order moved on to `delivered` in another tab is a 409 instead,\n * which is the truth: that state cannot be fulfilled (again).\n *\n * THE SERVER'S ORDER, NOT THE DIALOG'S: `lineItemIds` and the timeline append\n * are computed from the transaction's own read, so a note landed from another\n * tab survives — the client write it replaces appended to the dialog's stale\n * copy and dropped it.\n *\n * Role gate: `admin` and `editor`, the role the client write already had\n * under the rules' host catch-all — same reasoning as `cancel-order.ts`.\n */\nexport const fulfillOrderHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const orderId = String(body.orderId ?? '')\n if (!hostId || !orderId) {\n return res.status(400).json({ error: 'Missing hostId or orderId' })\n }\n // Firestore reserves `__…__` ids and `.doc()` throws SYNCHRONOUSLY on one,\n // which would surface as a 500 on a caller's typo rather than the 400 it is.\n if (/^__.*__$/.test(hostId) || /^__.*__$/.test(orderId)) {\n return res.status(400).json({ error: 'Missing hostId or orderId' })\n }\n // Two more acts on one fulfillment (AGL-3611): correct its tracking, or\n // cancel it. Same route, same role gate; neither moves stock or money.\n const action = String(body.action ?? '')\n const fulfillmentId = String(body.fulfillmentId ?? '').slice(0, 100)\n if ((action === 'update-tracking' || action === 'cancel-fulfillment') && !fulfillmentId) {\n return res.status(400).json({ error: 'Missing fulfillmentId' })\n }\n const to = String(body.to ?? '') as OrderFulfilmentTarget\n if (!action && to !== 'fulfilled' && to !== 'delivered') {\n // Deliberately NOT a generic transition endpoint — see the header.\n return res.status(400).json({ error: 'to must be fulfilled or delivered' })\n }\n if (action && action !== 'update-tracking' && action !== 'cancel-fulfillment') {\n return res.status(400).json({ error: 'Unknown action' })\n }\n const { carrier, trackingNumber } = boundTracking(body)\n const lineItems = Array.isArray(body.lineItems)\n ? (body.lineItems as unknown[]).slice(0, 500).map((entry) => ({\n lineItemId: Number((entry as Record<string, unknown>)?.lineItemId),\n quantity: Number((entry as Record<string, unknown>)?.quantity),\n }))\n : undefined\n const idempotencyKey = String(req.headers['idempotency-key'] ?? body.idempotencyKey ?? '').slice(0, 200)\n // A hand-entered shipping cost (AGL-3705): whole cents, or absent. Refused\n // before anything is read, so a typo never half-records a shipment.\n const shippingCostCents = action || to !== 'fulfilled' ? undefined : body.shippingCostCents\n const shippingCostProblem = CommerceModel.shippingCostCentsProblem(shippingCostCents)\n if (shippingCostProblem) return res.status(400).json({ error: shippingCostProblem })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not permitted' })\n }\n\n if (action) {\n const change =\n action === 'update-tracking'\n ? await updateFulfillmentTracking({\n hostId,\n orderId,\n fulfillmentId,\n carrier,\n trackingNumber,\n trackingUrl: body.trackingUrl,\n })\n : await cancelOrderFulfillment({ hostId, orderId, fulfillmentId })\n // A corrected tracking number is a parcel to follow (AGL-3612).\n if (change.outcome === 'updated' && action === 'update-tracking' && change.fulfillment.trackingNumber) {\n await announcePluginShipment({\n hostId,\n recordId: orderId,\n shipmentId: change.fulfillment.id,\n ...(change.fulfillment.carrier ? { carrier: change.fulfillment.carrier } : {}),\n trackingNumber: change.fulfillment.trackingNumber,\n })\n }\n if (change.outcome === 'no_such_order') return res.status(404).json({ error: 'Unknown order' })\n if (change.outcome === 'no_such_fulfillment') {\n return res.status(404).json({ error: 'That shipment is no longer on this order' })\n }\n if (change.outcome === 'locked') {\n return res.status(409).json({\n error:\n change.from === 'cancelled'\n ? 'That shipment was canceled and can no longer be edited'\n : `Shipments on an order in \"${change.from}\" can no longer be changed`,\n })\n }\n return res.status(200).json({\n ok: true,\n ...(change.outcome === 'already' ? { already: true } : { status: change.status }),\n fulfillment: change.fulfillment,\n })\n }\n\n // Authorized above; the shipment itself is the shared transaction. The\n // console dialog branches on `ok`, `already` and the 409's `error`\n // string, and this route's contract is with that dialog, not with the\n // registry's vocabulary.\n const outcome = await recordOrderShipment({\n hostId,\n orderId,\n to,\n carrier,\n trackingNumber,\n ...(lineItems ? { lineItems } : {}),\n ...(body.trackingUrl ? { trackingUrl: String(body.trackingUrl) } : {}),\n ...(body.labelUrl ? { labelUrl: String(body.labelUrl) } : {}),\n ...(body.notify === false ? { notify: false } : {}),\n ...(typeof shippingCostCents === 'number' ? { shippingCostCents } : {}),\n ...(idempotencyKey ? { idempotencyKey } : {}),\n })\n if (outcome.outcome === 'no_such_order') {\n return res.status(404).json({ error: 'Unknown order' })\n }\n if (outcome.outcome === 'invalid_lines') {\n return res\n .status(outcome.problem.problem === 'over_fulfilled' ? 409 : 400)\n .json({ error: outcome.message })\n }\n if (outcome.outcome === 'blocked') {\n return res.status(409).json({\n error:\n to === 'delivered'\n ? `Orders in \"${outcome.from}\" cannot be marked delivered`\n : `Orders in \"${outcome.from}\" cannot be fulfilled`,\n })\n }\n if (outcome.outcome === 'already') {\n return res.status(200).json({\n ok: true,\n already: true,\n ...(outcome.fulfillment ? { fulfillment: outcome.fulfillment } : {}),\n })\n }\n return res.status(200).json({\n ok: true,\n status: outcome.status,\n ...(outcome.fulfillment ? { fulfillment: outcome.fulfillment } : {}),\n })\n } catch (error) {\n // Nothing landed — the transaction is all-or-nothing — so the order is\n // still where it was and the merchant can try again.\n console.error('fulfillOrder failed', error)\n return res.status(500).json({\n error: to === 'delivered' ? 'Mark delivered failed' : 'Fulfill failed',\n })\n }\n}\n"],"names":["CommerceModel","announcePluginShipment","createResourceUid","firebaseAdmin","notifyOrderBuyer","ORDER_DELIVERED_EVENT","ORDER_FULFILLED_EVENT","fulfillmentEventView","stageOrderEvent","createHash","CARRIER_MAX","TRACKING_NUMBER_MAX","URL_MAX","boundTracking","input","carrier","String","trim","slice","trackingNumber","safeHttpsUrl","value","text","length","undefined","url","URL","protocol","toString","fulfillmentIdForKey","orderId","key","update","digest","trackingFields","explicitUrl","trackingUrl","trackingUrlFor","shipmentDetail","order","lines","partial","tracking","what","describeFulfillmentLines","orderRefFor","hostId","app","firestore","collection","doc","recordOrderShipment","request","result","to","labelUrl","orderRef","keyedId","idempotencyKey","runTransaction","transaction","snapshot","get","exists","outcome","liftLegacyOrder","data","atMs","Date","now","status","canTransitionOrder","from","delivered","updatedAtMs","timeline","appendOrderEvent","existing","fulfillments","find","entry","id","fulfillment","onceByTracking","same","fulfillmentWithTracking","named","lineItems","resolved","resolveFulfillmentLines","problem","message","describeFulfillmentLinesProblem","remainingFulfillmentLines","orderLineFulfillmentStates","filter","state","remainingQuantity","map","lineItemId","quantity","lineItemIds","labelRef","Number","isSafeInteger","labelCostCents","shippingCostCents","shippingCostCentsProblem","notify","statusFromFulfillments","shipped","extra","recordId","shipmentId","fulfillmentId","updateFulfillmentTracking","current","next","index","findIndex","fulfillmentIsActive","includes","Object","assign","cancelOrderFulfillment","orderFulfillmentsEditable","cancelledAtMs","fulfillmentLineQuantities","fulfillOrderHandler","req","res","body","method","json","error","authorization","headers","idToken","startsWith","JSON","parse","test","action","Array","isArray","shippingCostProblem","hostSnapshot","decoded","auth","verifyIdToken","hostRef","memberRole","uid","change","ok","already","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/fulfill-order.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsCommerceSiteKey } from './commerce-site-keys'\nimport { commerceSitePermissionRefusal } from '../model/plugin-permissions'\nimport * as CommerceModel from '../model'\nimport { announcePluginShipment } from '@aglyn/aglyn/plugin-manager/plugin-shipment-records'\nimport { createResourceUid, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { notifyOrderBuyer } from './order-notifications'\nimport { ORDER_DELIVERED_EVENT, ORDER_FULFILLED_EVENT } from '../model/order-events'\nimport { fulfillmentEventView, stageOrderEvent } from './order-events'\nimport { createHash } from 'crypto'\n\n/**\n * The two fulfilment-side transitions, and ONLY those.\n *\n * `cancelled` releases stock under its own transaction and `refunded` moves\n * money under another, so neither belongs to a write whose contract is \"a\n * forward status flip plus a timeline entry, no stock moved, no money moved\".\n * Widening this union is how a caller gets a door around the specifics those\n * routes exist to enforce — the type is the door being shut.\n */\nexport type OrderFulfilmentTarget = 'fulfilled' | 'delivered'\n\n/** How long a carrier name and a tracking number may be, on every door. */\nexport const CARRIER_MAX = 40\nexport const TRACKING_NUMBER_MAX = 60\nconst URL_MAX = 2048\n\nexport interface RecordShipmentRequest {\n /** Site that owns the order. The CALLER has already proven ownership. */\n hostId: string\n orderId: string\n to: OrderFulfilmentTarget\n /** Free text, e.g. `'UPS'`. Bounded by the caller. */\n carrier?: string\n trackingNumber?: string\n /**\n * Which units this shipment covers (AGL-3611). Absent means everything\n * still to ship, which is what a fulfil meant before quantities existed.\n * Ignored for `delivered`.\n */\n lineItems?: ReadonlyArray<{ lineItemId: number; quantity: number }>\n /** An explicit tracking link; without one it is derived from the carrier. */\n trackingUrl?: string\n /** A label a shipping plugin bought for this parcel. */\n labelUrl?: string\n /** That plugin's id for the label (AGL-3612), kept on the fulfillment. */\n labelRef?: string\n /** What that label cost, in integer cents of the order's currency (AGL-3693). */\n labelCostCents?: number\n /**\n * What the merchant says this shipment cost to send, typed by hand in the\n * fulfill dialog (AGL-3705): integer cents of the store's currency, 0 for\n * free shipping. Stored in the SAME field a label's cost is\n * (`labelCostCents`), so it rides `order.fulfilled` to a marketplace that\n * pays shipping back. A label's own cost wins when both are given. Absent\n * means not known — nothing is stored. The caller validates\n * (`shippingCostCentsProblem`); an invalid value here is dropped.\n */\n shippingCostCents?: number\n /** Whether the buyer is to be told; recorded on the fulfillment. Default on. */\n notify?: boolean\n /**\n * One shipment attempt. A retry carrying the same key finds the fulfillment\n * it already wrote and answers `already`, so a partial shipment, which\n * leaves the order open to more, still cannot be recorded twice.\n */\n idempotencyKey?: string\n /**\n * A shipment whose tracking number an active fulfillment of this order\n * already carries is the same parcel, answered `already` (AGL-3613). For\n * the doors a shipping tool posts to — ShipStation's ShipNotice, a tracking\n * file — where the number is the parcel's identity and the same parcel may\n * have been recorded by hand first.\n */\n onceByTracking?: boolean\n}\n\n/**\n * What happened, as a domain fact rather than an HTTP status — each caller\n * maps it into its own error vocabulary (the console route into its JSON\n * shape, `/v1` into the published error envelope), because those two\n * vocabularies are contracts with different audiences and neither may leak\n * into the other.\n *\n * `already` is a SUCCESS and is distinct from `recorded` on purpose: a retried\n * request finds the order in the target status (or its keyed fulfillment\n * already written) and returns without writing, so a lost response can never\n * append a second copy of the same shipment.\n *\n * `recorded` carries the fulfillment it wrote (absent for `delivered`) and the\n * status the order is now in, so a caller can tell the buyer what shipped.\n */\nexport type RecordShipmentOutcome =\n | {\n outcome: 'recorded'\n status: CommerceModel.OrderStatus\n fulfillment?: CommerceModel.OrderFulfillment\n }\n | { outcome: 'already'; fulfillment?: CommerceModel.OrderFulfillment }\n | { outcome: 'no_such_order' }\n /** The transition rule refused. `from` is the status that refused it. */\n | { outcome: 'blocked'; from: string }\n /** The requested lines or quantities were refused; nothing was written. */\n | { outcome: 'invalid_lines'; problem: CommerceModel.FulfillmentLinesProblem; message: string }\n\n/** Trims a carrier and a tracking number to what every door stores. */\nexport function boundTracking(input: { carrier?: unknown; trackingNumber?: unknown }): {\n carrier: string\n trackingNumber: string\n} {\n return {\n carrier: String(input.carrier ?? '').trim().slice(0, CARRIER_MAX),\n trackingNumber: String(input.trackingNumber ?? '').trim().slice(0, TRACKING_NUMBER_MAX),\n }\n}\n\n/** An https link or nothing: a stored link is rendered to buyers as an anchor. */\nexport function safeHttpsUrl(value: unknown): string | undefined {\n const text = String(value ?? '').trim()\n if (!text || text.length > URL_MAX) return undefined\n try {\n const url = new URL(text)\n return url.protocol === 'https:' ? url.toString() : undefined\n } catch {\n return undefined\n }\n}\n\n/** The fulfillment id a keyed attempt writes, so a retry finds it. */\nexport function fulfillmentIdForKey(orderId: string, key: string): string {\n return `f-${createHash('sha256').update(`${orderId}:${key}`).digest('hex').slice(0, 20)}`\n}\n\n/** The tracking keys a fulfillment stores: the number, the carrier and the link. */\nfunction trackingFields(\n carrier: string,\n trackingNumber: string,\n explicitUrl: string | undefined,\n): Pick<CommerceModel.OrderFulfillment, 'carrier' | 'trackingNumber' | 'trackingUrl'> {\n const trackingUrl =\n explicitUrl ?? (trackingNumber ? CommerceModel.trackingUrlFor(carrier, trackingNumber) ?? undefined : undefined)\n return {\n ...(carrier ? { carrier } : {}),\n ...(trackingNumber ? { trackingNumber } : {}),\n ...(trackingUrl ? { trackingUrl } : {}),\n }\n}\n\n/** The timeline's words for a shipment. */\nfunction shipmentDetail(\n order: CommerceModel.HostOrder,\n lines: readonly CommerceModel.OrderFulfillmentLine[],\n partial: boolean,\n carrier: string,\n trackingNumber: string,\n): string {\n const tracking = trackingNumber ? `${carrier || 'Shipped'} ${trackingNumber}` : ''\n if (!partial) return tracking || 'Fulfilled'\n const what = CommerceModel.describeFulfillmentLines(order, lines)\n return tracking ? `${what} — ${tracking}` : `${what} shipped`\n}\n\nfunction orderRefFor(hostId: string, orderId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('orders')\n .doc(orderId)\n}\n\n/**\n * Record a shipment — the transaction, with NO authorization of any kind\n * (AGL-2461).\n *\n * This is the whole of what `fulfillOrderHandler` used to do below its auth\n * preamble, lifted out unchanged so a SECOND caller can have it: the customer\n * REST API's `PATCH /v1/sites/{id}/orders/{id}`\n * (`api-v1/orders-and-products.ts`), which authenticates an org API key and\n * so shares not one line of this function's former preamble.\n *\n * Lifted rather than copied, and that is the entire point: a second\n * `ORDER_TRANSITIONS` inside `/v1` would be two tables that drift into the\n * API writing a status the console forbids.\n *\n * **The caller authorizes.** Everything this function knows is the transition\n * rule. It does not know who is asking, and it will happily move any order on\n * any host — `hostId` is taken on trust because its two callers establish\n * trust in ways that have nothing in common (a Firebase uid in `memberRoles`\n * for the console; an org credential that owns the host for `/v1`).\n *\n * QUANTITIES (AGL-3611). A shipment names units, not just lines, and the\n * remaining count is computed from the transaction's own read, so two admins\n * shipping the last 3 mugs at once cannot ship 6: the second meets\n * `invalid_lines` with `over_fulfilled`. The resulting status is computed\n * from the fulfillments (`statusFromFulfillments`) — `partially_fulfilled`\n * until every shippable unit is covered — and the transition rule is re-asked\n * only when the status actually moves, since a second partial shipment on a\n * `partially_fulfilled` order changes nothing the table speaks to.\n *\n * Every guarantee AGL-1819 built is inside the transaction and therefore\n * inherited by both callers: `canTransitionOrder` is re-asked under the write\n * so a stale caller cannot skip a state, the already-in-target case returns\n * without writing so a retry cannot append a duplicate shipment, and the\n * lines and the timeline are computed from the transaction's OWN read so a\n * note landed from another tab survives.\n */\nexport async function recordOrderShipment(\n request: RecordShipmentRequest,\n): Promise<RecordShipmentOutcome> {\n const { hostId, orderId, to } = request\n const { carrier, trackingNumber } = boundTracking(request)\n const explicitUrl = safeHttpsUrl(request.trackingUrl)\n const labelUrl = safeHttpsUrl(request.labelUrl)\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = orderRefFor(hostId, orderId)\n const keyedId = request.idempotencyKey\n ? fulfillmentIdForKey(orderId, String(request.idempotencyKey).slice(0, 200))\n : null\n\n const result = await firestore.runTransaction(\n async (transaction): Promise<RecordShipmentOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder(\n (snapshot.data() ?? {}) as never,\n )\n const atMs = Date.now()\n\n if (to === 'delivered') {\n if (order.status === 'delivered') return { outcome: 'already' }\n if (!CommerceModel.canTransitionOrder(order.status, 'delivered')) {\n return { outcome: 'blocked', from: order.status }\n }\n const delivered = {\n status: 'delivered',\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(order, 'delivered', undefined, atMs),\n }\n transaction.update(orderRef, delivered)\n stageOrderEvent(transaction, ORDER_DELIVERED_EVENT, { hostId, orderId, key: 'delivered', order: { ...snapshot.data(), ...delivered } })\n return { outcome: 'recorded', status: 'delivered' }\n }\n\n // A redelivered keyed attempt: the fulfillment it wrote is there.\n if (keyedId) {\n const existing = (order.fulfillments ?? []).find((entry) => entry.id === keyedId)\n if (existing) return { outcome: 'already', fulfillment: existing }\n }\n if (request.onceByTracking && trackingNumber) {\n const same = CommerceModel.fulfillmentWithTracking(order, trackingNumber)\n if (same) return { outcome: 'already', fulfillment: same }\n }\n const named = (request.lineItems ?? []).length > 0\n // A redelivered click, or the second of two admins, on a \"fulfil\n // everything\". Success because it IS the state the caller asked for —\n // but nothing is written, so a retry cannot append a second copy.\n if (!named && order.status === 'fulfilled') return { outcome: 'already' }\n if (\n order.status !== 'partially_fulfilled' &&\n order.status !== 'fulfilled' &&\n !CommerceModel.canTransitionOrder(order.status, 'partially_fulfilled') &&\n !CommerceModel.canTransitionOrder(order.status, 'fulfilled')\n ) {\n return { outcome: 'blocked', from: order.status }\n }\n\n let lines: CommerceModel.OrderFulfillmentLine[]\n if (named) {\n const resolved = CommerceModel.resolveFulfillmentLines(order, request.lineItems ?? [])\n if (!('lines' in resolved)) {\n return {\n outcome: 'invalid_lines',\n problem: resolved,\n message: CommerceModel.describeFulfillmentLinesProblem(resolved),\n }\n }\n lines = resolved.lines\n } else {\n lines = CommerceModel.remainingFulfillmentLines(order)\n // Nothing physical left: a fulfil closes the digital and service\n // lines instead, which is what it did before quantities existed.\n if (lines.length === 0) {\n lines = CommerceModel.orderLineFulfillmentStates(order)\n .filter((state) => state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n }\n }\n\n const fulfillment: CommerceModel.OrderFulfillment = {\n id: keyedId ?? createResourceUid(),\n lineItemIds: lines.map((entry) => entry.lineItemId),\n lines,\n ...trackingFields(carrier, trackingNumber, explicitUrl),\n ...(labelUrl ? { labelUrl } : {}),\n ...(request.labelRef ? { labelRef: String(request.labelRef).slice(0, 80) } : {}),\n ...(Number.isSafeInteger(request.labelCostCents) && Number(request.labelCostCents) > 0\n ? { labelCostCents: Number(request.labelCostCents) }\n : request.shippingCostCents !== undefined &&\n CommerceModel.shippingCostCentsProblem(request.shippingCostCents) === null\n ? { labelCostCents: request.shippingCostCents }\n : {}),\n status: 'active',\n notify: request.notify !== false,\n atMs,\n }\n const fulfillments = [...(order.fulfillments ?? []), fulfillment]\n let status: CommerceModel.OrderStatus = CommerceModel.statusFromFulfillments({\n lineItems: order.lineItems,\n fulfillments,\n })\n // A fulfil with nothing left to cover still closes the order.\n if (status === 'paid') status = 'fulfilled'\n if (status !== order.status && !CommerceModel.canTransitionOrder(order.status, status)) {\n return { outcome: 'blocked', from: order.status }\n }\n const shipped = {\n status,\n fulfillments,\n // What a shipping tool's feed asks (AGL-3613): which orders changed.\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n status,\n shipmentDetail(order, lines, status === 'partially_fulfilled', carrier, trackingNumber),\n atMs,\n ),\n }\n transaction.update(orderRef, shipped)\n // Once per shipment (AGL-3611), in the same write, so the event exists\n // exactly when the shipment does.\n stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, { hostId, orderId, key: fulfillment.id, order: { ...snapshot.data(), ...shipped }, extra: { fulfillment: fulfillmentEventView(order, fulfillment) } })\n return { outcome: 'recorded', status, fulfillment }\n },\n )\n // The BUYER hears about it (AGL-3610): shipped with the tracking link, or\n // delivered. Once per shipment, after the write, and never fails the\n // shipment — see `order-notifications.ts`. A retry returns `already` above\n // and never reaches here. A merchant who unticked \"Notify customer\"\n // (AGL-3611) recorded the shipment with `notify: false`, and nobody is told.\n // Every shipment recorded, by any door, is announced (AGL-3612), so a\n // plugin that follows parcels can follow one typed in by hand too.\n if (result.outcome === 'recorded' && result.fulfillment?.trackingNumber) {\n await announcePluginShipment({\n hostId,\n recordId: orderId,\n shipmentId: result.fulfillment.id,\n ...(result.fulfillment.carrier ? { carrier: result.fulfillment.carrier } : {}),\n trackingNumber: result.fulfillment.trackingNumber,\n ...(request.labelRef ? { labelRef: request.labelRef } : {}),\n })\n }\n if (result.outcome === 'recorded' && result.fulfillment?.notify !== false) {\n await notifyOrderBuyer(\n { hostId, orderId },\n to === 'fulfilled' ? 'shipped' : 'delivered',\n result.fulfillment ? { fulfillmentId: result.fulfillment.id } : {},\n )\n }\n return result\n}\n\n/** What an edit or a cancel of one fulfillment came to. */\nexport type FulfillmentChangeOutcome =\n | { outcome: 'updated'; status: CommerceModel.OrderStatus; fulfillment: CommerceModel.OrderFulfillment }\n | { outcome: 'already'; fulfillment: CommerceModel.OrderFulfillment }\n | { outcome: 'no_such_order' }\n | { outcome: 'no_such_fulfillment' }\n /** The order is history (delivered, refunded, cancelled); `from` names it. */\n | { outcome: 'locked'; from: string }\n\n/**\n * Corrects the tracking on one fulfillment (AGL-3611): a mistyped number, a\n * carrier swapped, a label bought after the parcel was recorded. Moves no\n * status and no stock. A cancelled fulfillment is history and is refused.\n */\nexport async function updateFulfillmentTracking(request: {\n hostId: string\n orderId: string\n fulfillmentId: string\n carrier?: string\n trackingNumber?: string\n trackingUrl?: string\n}): Promise<FulfillmentChangeOutcome> {\n const { carrier, trackingNumber } = boundTracking(request)\n const explicitUrl = safeHttpsUrl(request.trackingUrl)\n const orderRef = orderRefFor(request.hostId, request.orderId)\n return firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction): Promise<FulfillmentChangeOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const index = (order.fulfillments ?? []).findIndex((entry) => entry.id === request.fulfillmentId)\n if (index < 0) return { outcome: 'no_such_fulfillment' }\n const current = (order.fulfillments ?? [])[index]\n if (!CommerceModel.fulfillmentIsActive(current) || !['paid', 'partially_fulfilled', 'fulfilled', 'delivered'].includes(order.status)) {\n return { outcome: 'locked', from: CommerceModel.fulfillmentIsActive(current) ? order.status : 'cancelled' }\n }\n const atMs = Date.now()\n const next: CommerceModel.OrderFulfillment = { ...current, updatedAtMs: atMs }\n delete next.carrier\n delete next.trackingNumber\n delete next.trackingUrl\n Object.assign(next, trackingFields(carrier, trackingNumber, explicitUrl))\n if (\n (current.carrier ?? '') === (next.carrier ?? '') &&\n (current.trackingNumber ?? '') === (next.trackingNumber ?? '') &&\n (current.trackingUrl ?? '') === (next.trackingUrl ?? '')\n ) {\n return { outcome: 'already', fulfillment: current }\n }\n const fulfillments = [...(order.fulfillments ?? [])]\n fulfillments[index] = next\n transaction.update(orderRef, {\n fulfillments,\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'tracking-updated',\n trackingNumber ? `${carrier || 'Tracking'} ${trackingNumber}` : 'Tracking removed',\n atMs,\n ),\n })\n return { outcome: 'updated', status: order.status, fulfillment: next }\n })\n}\n\n/**\n * Cancels one fulfillment (AGL-3611): the parcel was never handed over, or was\n * recorded against the wrong items. Its units go back to unfulfilled and the\n * order's status is recomputed from what remains — `fulfilled` can step back\n * to `partially_fulfilled` or `paid`.\n *\n * That step back is NOT an `ORDER_TRANSITIONS` edge, on purpose: the table\n * says what may happen to an order going forward, and admitting\n * `fulfilled → paid` there would open it to every other caller. This is the one writer\n * that may take it, and only from a status whose fulfillments are still open\n * (`orderFulfillmentsEditable`): a delivered, refunded or cancelled order's\n * shipments are what happened and stay that way.\n */\nexport async function cancelOrderFulfillment(request: {\n hostId: string\n orderId: string\n fulfillmentId: string\n}): Promise<FulfillmentChangeOutcome> {\n const orderRef = orderRefFor(request.hostId, request.orderId)\n return firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction): Promise<FulfillmentChangeOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const index = (order.fulfillments ?? []).findIndex((entry) => entry.id === request.fulfillmentId)\n if (index < 0) return { outcome: 'no_such_fulfillment' }\n const current = (order.fulfillments ?? [])[index]\n if (!CommerceModel.fulfillmentIsActive(current)) return { outcome: 'already', fulfillment: current }\n if (!CommerceModel.orderFulfillmentsEditable(order)) {\n return { outcome: 'locked', from: order.status }\n }\n const atMs = Date.now()\n const next: CommerceModel.OrderFulfillment = {\n ...current,\n status: 'cancelled',\n cancelledAtMs: atMs,\n updatedAtMs: atMs,\n }\n const fulfillments = [...(order.fulfillments ?? [])]\n fulfillments[index] = next\n const status = CommerceModel.statusFromFulfillments({ lineItems: order.lineItems, fulfillments })\n transaction.update(orderRef, {\n status,\n fulfillments,\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'fulfillment-cancelled',\n CommerceModel.describeFulfillmentLines(order, CommerceModel.fulfillmentLineQuantities(order, current)) +\n ' back to unfulfilled',\n atMs,\n ),\n })\n return { outcome: 'updated', status, fulfillment: next }\n })\n}\n\n/**\n * Fulfil and mark-delivered, with the transition re-asked under the write\n * (AGL-1819) — the last two order-status writes the console made with a client\n * `updateDoc`, moved behind the same shape as `cancel-order.ts`.\n *\n * THE STALE-DIALOG HOLE IS THE SAME ONE AGL-1818 CLOSED FOR CANCEL.\n * `canTransitionOrder` was consulted only to RENDER the buttons, so a dialog\n * opened on a `paid` order that was refunded (or cancelled) in another tab\n * could still write `fulfilled` straight onto it — `ORDER_TRANSITIONS` stayed\n * advisory because nothing consulted it on the way IN. Re-reading the order\n * inside the transaction that writes the status is what turns the table into\n * a guarantee; the race comes back as a 409 naming the state that refused.\n *\n * ONE ROUTE FOR THE TWO FULFILMENT-SIDE TRANSITIONS, and ONLY those. They are\n * the same act — a forward status flip plus a timeline entry, no stock moved,\n * no money moved — so they share a handler parameterized on `to`. It refuses\n * every other target with a 400 on purpose: `cancelled` releases stock under\n * its own transaction (`cancel-order.ts`) and `refunded` moves money\n * (`refund.ts`), so admitting them here would hand callers a door around\n * exactly the specifics those routes exist to enforce.\n *\n * NO STOCK MOVES HERE. The sale decremented inventory when the money landed\n * (see `cancel-order.ts`'s header); shipping the goods changes what is on the\n * shelf not at all. An open `restockCheck` — a partial refund's question,\n * flagged before the merchant ships the rest — survives untouched: the write\n * is an `update()` naming only status, fulfillments and timeline, and the\n * question is still the merchant's to answer.\n *\n * NO MANAGER NOTIFICATION, matching the client write this replaces: the\n * merchant fulfilled the order themselves, and `notifyHostManagers` would\n * tell them what they just did. (`supplier-update.ts` notifies because there\n * the SUPPLIER acted and the merchant is the one who needs to hear it.) The\n * BUYER is told, through `recordOrderShipment` (AGL-3610).\n *\n * ONCE. A retried request — a lost response, a second click — finds the order\n * already in the target status and returns success WITHOUT writing: appending\n * a second copy of the same fulfillment is the double this guard exists to\n * stop. It reports `already: true` so the console can say so. A fulfil retried\n * after the order moved on to `delivered` in another tab is a 409 instead,\n * which is the truth: that state cannot be fulfilled (again).\n *\n * THE SERVER'S ORDER, NOT THE DIALOG'S: `lineItemIds` and the timeline append\n * are computed from the transaction's own read, so a note landed from another\n * tab survives — the client write it replaces appended to the dialog's stale\n * copy and dropped it.\n *\n * Role gate: `admin` and `editor`, the role the client write already had\n * under the rules' host catch-all — same reasoning as `cancel-order.ts`.\n */\nexport const fulfillOrderHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const orderId = String(body.orderId ?? '')\n if (!hostId || !orderId) {\n return res.status(400).json({ error: 'Missing hostId or orderId' })\n }\n // Firestore reserves `__…__` ids and `.doc()` throws SYNCHRONOUSLY on one,\n // which would surface as a 500 on a caller's typo rather than the 400 it is.\n if (/^__.*__$/.test(hostId) || /^__.*__$/.test(orderId)) {\n return res.status(400).json({ error: 'Missing hostId or orderId' })\n }\n // Two more acts on one fulfillment (AGL-3611): correct its tracking, or\n // cancel it. Same route, same role gate; neither moves stock or money.\n const action = String(body.action ?? '')\n const fulfillmentId = String(body.fulfillmentId ?? '').slice(0, 100)\n if ((action === 'update-tracking' || action === 'cancel-fulfillment') && !fulfillmentId) {\n return res.status(400).json({ error: 'Missing fulfillmentId' })\n }\n const to = String(body.to ?? '') as OrderFulfilmentTarget\n if (!action && to !== 'fulfilled' && to !== 'delivered') {\n // Deliberately NOT a generic transition endpoint — see the header.\n return res.status(400).json({ error: 'to must be fulfilled or delivered' })\n }\n if (action && action !== 'update-tracking' && action !== 'cancel-fulfillment') {\n return res.status(400).json({ error: 'Unknown action' })\n }\n const { carrier, trackingNumber } = boundTracking(body)\n const lineItems = Array.isArray(body.lineItems)\n ? (body.lineItems as unknown[]).slice(0, 500).map((entry) => ({\n lineItemId: Number((entry as Record<string, unknown>)?.lineItemId),\n quantity: Number((entry as Record<string, unknown>)?.quantity),\n }))\n : undefined\n const idempotencyKey = String(req.headers['idempotency-key'] ?? body.idempotencyKey ?? '').slice(0, 200)\n // A hand-entered shipping cost (AGL-3705): whole cents, or absent. Refused\n // before anything is read, so a typo never half-records a shipment.\n const shippingCostCents = action || to !== 'fulfilled' ? undefined : body.shippingCostCents\n const shippingCostProblem = CommerceModel.shippingCostCentsProblem(shippingCostCents)\n if (shippingCostProblem) return res.status(400).json({ error: shippingCostProblem })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // \"Manage orders\" (`commerce.orders`, AGL-4047), whose preset is the site's Admin\n // and Editor this asked for, read as the rules would read it.\n if (!holdsCommerceSiteKey(hostSnapshot, decoded.uid, 'commerce.orders')) {\n return res.status(403).json({ error: commerceSitePermissionRefusal('commerce.orders') })\n }\n\n if (action) {\n const change =\n action === 'update-tracking'\n ? await updateFulfillmentTracking({\n hostId,\n orderId,\n fulfillmentId,\n carrier,\n trackingNumber,\n trackingUrl: body.trackingUrl,\n })\n : await cancelOrderFulfillment({ hostId, orderId, fulfillmentId })\n // A corrected tracking number is a parcel to follow (AGL-3612).\n if (change.outcome === 'updated' && action === 'update-tracking' && change.fulfillment.trackingNumber) {\n await announcePluginShipment({\n hostId,\n recordId: orderId,\n shipmentId: change.fulfillment.id,\n ...(change.fulfillment.carrier ? { carrier: change.fulfillment.carrier } : {}),\n trackingNumber: change.fulfillment.trackingNumber,\n })\n }\n if (change.outcome === 'no_such_order') return res.status(404).json({ error: 'Unknown order' })\n if (change.outcome === 'no_such_fulfillment') {\n return res.status(404).json({ error: 'That shipment is no longer on this order' })\n }\n if (change.outcome === 'locked') {\n return res.status(409).json({\n error:\n change.from === 'cancelled'\n ? 'That shipment was canceled and can no longer be edited'\n : `Shipments on an order in \"${change.from}\" can no longer be changed`,\n })\n }\n return res.status(200).json({\n ok: true,\n ...(change.outcome === 'already' ? { already: true } : { status: change.status }),\n fulfillment: change.fulfillment,\n })\n }\n\n // Authorized above; the shipment itself is the shared transaction. The\n // console dialog branches on `ok`, `already` and the 409's `error`\n // string, and this route's contract is with that dialog, not with the\n // registry's vocabulary.\n const outcome = await recordOrderShipment({\n hostId,\n orderId,\n to,\n carrier,\n trackingNumber,\n ...(lineItems ? { lineItems } : {}),\n ...(body.trackingUrl ? { trackingUrl: String(body.trackingUrl) } : {}),\n ...(body.labelUrl ? { labelUrl: String(body.labelUrl) } : {}),\n ...(body.notify === false ? { notify: false } : {}),\n ...(typeof shippingCostCents === 'number' ? { shippingCostCents } : {}),\n ...(idempotencyKey ? { idempotencyKey } : {}),\n })\n if (outcome.outcome === 'no_such_order') {\n return res.status(404).json({ error: 'Unknown order' })\n }\n if (outcome.outcome === 'invalid_lines') {\n return res\n .status(outcome.problem.problem === 'over_fulfilled' ? 409 : 400)\n .json({ error: outcome.message })\n }\n if (outcome.outcome === 'blocked') {\n return res.status(409).json({\n error:\n to === 'delivered'\n ? `Orders in \"${outcome.from}\" cannot be marked delivered`\n : `Orders in \"${outcome.from}\" cannot be fulfilled`,\n })\n }\n if (outcome.outcome === 'already') {\n return res.status(200).json({\n ok: true,\n already: true,\n ...(outcome.fulfillment ? { fulfillment: outcome.fulfillment } : {}),\n })\n }\n return res.status(200).json({\n ok: true,\n status: outcome.status,\n ...(outcome.fulfillment ? { fulfillment: outcome.fulfillment } : {}),\n })\n } catch (error) {\n // Nothing landed — the transaction is all-or-nothing — so the order is\n // still where it was and the merchant can try again.\n console.error('fulfillOrder failed', error)\n return res.status(500).json({\n error: to === 'delivered' ? 'Mark delivered failed' : 'Fulfill failed',\n })\n }\n}\n"],"names":["holdsCommerceSiteKey","commerceSitePermissionRefusal","CommerceModel","announcePluginShipment","createResourceUid","firebaseAdmin","notifyOrderBuyer","ORDER_DELIVERED_EVENT","ORDER_FULFILLED_EVENT","fulfillmentEventView","stageOrderEvent","createHash","CARRIER_MAX","TRACKING_NUMBER_MAX","URL_MAX","boundTracking","input","carrier","String","trim","slice","trackingNumber","safeHttpsUrl","value","text","length","undefined","url","URL","protocol","toString","fulfillmentIdForKey","orderId","key","update","digest","trackingFields","explicitUrl","trackingUrl","trackingUrlFor","shipmentDetail","order","lines","partial","tracking","what","describeFulfillmentLines","orderRefFor","hostId","app","firestore","collection","doc","recordOrderShipment","request","result","to","labelUrl","orderRef","keyedId","idempotencyKey","runTransaction","transaction","snapshot","get","exists","outcome","liftLegacyOrder","data","atMs","Date","now","status","canTransitionOrder","from","delivered","updatedAtMs","timeline","appendOrderEvent","existing","fulfillments","find","entry","id","fulfillment","onceByTracking","same","fulfillmentWithTracking","named","lineItems","resolved","resolveFulfillmentLines","problem","message","describeFulfillmentLinesProblem","remainingFulfillmentLines","orderLineFulfillmentStates","filter","state","remainingQuantity","map","lineItemId","quantity","lineItemIds","labelRef","Number","isSafeInteger","labelCostCents","shippingCostCents","shippingCostCentsProblem","notify","statusFromFulfillments","shipped","extra","recordId","shipmentId","fulfillmentId","updateFulfillmentTracking","current","next","index","findIndex","fulfillmentIsActive","includes","Object","assign","cancelOrderFulfillment","orderFulfillmentsEditable","cancelledAtMs","fulfillmentLineQuantities","fulfillOrderHandler","req","res","body","method","json","error","authorization","headers","idToken","startsWith","JSON","parse","test","action","Array","isArray","shippingCostProblem","decoded","auth","verifyIdToken","hostRef","hostSnapshot","uid","change","ok","already","console"],"mappings":"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"}
@@ -13,7 +13,9 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import * as Aglyn from "@aglyn/aglyn/server";
16
+ */ import { holdsCommerceSiteKey } from "./commerce-site-keys.js";
17
+ import { commerceSitePermissionRefusal } from "../model/plugin-permissions.js";
18
+ import * as Aglyn from "@aglyn/aglyn/server";
17
19
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
18
20
  import { GiftCardIssueRefusal, issueGiftCard } from "./gift-card-issue.js";
19
21
  /**
@@ -63,7 +65,7 @@ import { GiftCardIssueRefusal, issueGiftCard } from "./gift-card-issue.js";
63
65
  });
64
66
  }
65
67
  try {
66
- var _hostSnapshot_get, _body_amountCents, _body_recipientEmail, _body_note, _decoded_email;
68
+ var _body_amountCents, _body_recipientEmail, _body_note, _decoded_email;
67
69
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
68
70
  const firestore = firebaseAdmin.app().firestore();
69
71
  const hostRef = firestore.collection('hosts').doc(hostId);
@@ -88,10 +90,11 @@ import { GiftCardIssueRefusal, issueGiftCard } from "./gift-card-issue.js";
88
90
  // added tomorrow is now refused until someone decides otherwise, rather
89
91
  // than admitted by silence. Absent stays refused: `undefined` is not in
90
92
  // the set, which matters with `strictNullChecks` off.
91
- const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
92
- if (memberRole !== 'admin' && memberRole !== 'editor') {
93
+ // "Issue gift cards" (`commerce.giftCards`, AGL-4047), whose preset is the site's Admin
94
+ // and Editor this asked for, read as the rules would read it.
95
+ if (!holdsCommerceSiteKey(hostSnapshot, decoded.uid, 'commerce.giftCards')) {
93
96
  return res.status(403).json({
94
- error: 'Not permitted'
97
+ error: commerceSitePermissionRefusal('commerce.giftCards')
95
98
  });
96
99
  }
97
100
  const owner = await getOrgForHost(hostId).catch(()=>null);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/gift-cards.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { GiftCardIssueRefusal, issueGiftCard } from './gift-card-issue'\n\n/**\n * Gift-card issue / void (AGL-2226).\n *\n * The `giftCards` subcollection has existed since AGL-322 — minted by\n * `billing-webhook.ts` when a gift-card product is bought, decremented by\n * `cart-checkout.ts` at redemption. Neither end had a console: grepping for\n * that collection returned exactly those two server files, so a\n * merchant on Business (the tier that carries `giftCards`) could sell store\n * credit and then not see one cent of the liability they had taken on, help\n * a customer who lost a code, or void a leaked one.\n *\n * WRITES ARE SERVER-SIDE, and that is not a style preference. The Firestore\n * rules' host catch-all would happily grant an admin `create`/`update` on\n * `giftCards` — it is not in any exclusion list. A client-writable\n * `balanceCents` is a self-service discount machine: set it to any number,\n * redeem it at checkout, and `cart-checkout.ts` applies it as amount-off\n * against a Stripe coupon. The reads stay client-side (the catch-all's\n * `allow read` already grants them, and the card wants live updates), so\n * only the two operations that move money come through here.\n *\n * Re-checks the entitlement server-side for the same reason\n * `billing-webhook.ts:2285` does: a card issued while the plan was live is\n * honoured, but a lapsed plan must not mint new ones.\n */\nexport const giftCardsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body || '{}') : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const action = String(body.action ?? '')\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n if (action !== 'issue' && action !== 'void' && action !== 'release') {\n return res.status(400).json({ error: 'Unknown action' })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // AN ALLOWLIST (AGL-2372), not the `!role || role === 'viewer'` denylist\n // this route used to carry — the same correction AGL-2262 made to\n // `pos-order.ts` and `draft-order.ts` and did not reach here.\n //\n // A denylist silently WIDENS every time the role union grows. It did:\n // `HostAccessRole` is `admin | editor | author | viewer` since AGL-2334,\n // and `author` is grantable on any host through `/api/hosts/members`. So\n // \"not literally viewer\" started admitting authors — a role that exists to\n // edit content — to a route that mints and destroys store credit.\n //\n // `admin | editor` is the bar this route always meant (the comment here\n // said \"owner/admin/editor, not viewer\"); the difference is that a role\n // added tomorrow is now refused until someone decides otherwise, rather\n // than admitted by silence. Absent stays refused: `undefined` is not in\n // the set, which matters with `strictNullChecks` off.\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not permitted' })\n }\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!Aglyn.checkEntitlement(owner?.org as never, 'giftCards')) {\n return res\n .status(402)\n .json({ error: 'Gift cards are not included on this plan' })\n }\n\n // RELEASE A HELD CARD (AGL-3363). A fraud warning, review or dispute on\n // the payment that bought a card freezes it; the merchant who has\n // satisfied themselves the payment is genuine lifts that here. A voided\n // card stays voided: release restores redemption, never a balance.\n if (action === 'release') {\n const code = String(body.code ?? '').trim()\n if (!code) return res.status(400).json({ error: 'Missing code' })\n const cardRef = hostRef.collection('giftCards').doc(code)\n const existing = await cardRef.get()\n if (!existing.exists) {\n return res.status(404).json({ error: 'Unknown gift card' })\n }\n if (!(Number(existing.get('frozenAtMs')) > 0)) {\n return res.status(409).json({ error: 'This gift card is not on hold' })\n }\n await cardRef.set(\n {\n frozenAtMs: null,\n frozenReason: null,\n releasedAtMs: Date.now(),\n releasedBy: decoded.uid,\n },\n { merge: true },\n )\n return res.status(200).json({ ok: true, code })\n }\n\n if (action === 'void') {\n const code = String(body.code ?? '').trim()\n if (!code) return res.status(400).json({ error: 'Missing code' })\n const cardRef = hostRef.collection('giftCards').doc(code)\n // Zero the balance rather than delete the document. AGL-1767 is the\n // reason: a deleted card that a checkout still had in flight was\n // RESURRECTED by the webhook's `increment(-cents)` into a NEGATIVE\n // balance, which then read as a card with money on it and understated\n // the outstanding total. A zeroed card decrements to a negative number\n // that no redemption path will ever accept, and the row stays\n // auditable.\n const existing = await cardRef.get()\n if (!existing.exists) {\n return res.status(404).json({ error: 'Unknown gift card' })\n }\n await cardRef.set(\n {\n balanceCents: 0,\n voidedAtMs: Date.now(),\n voidedBy: decoded.uid,\n },\n { merge: true },\n )\n return res.status(200).json({ ok: true, code })\n }\n\n // Issued through the one issue path the gift card import uses too\n // (AGL-3551): the amount bounded, the code minted and never overwritten,\n // the activity line written, and the recipient emailed.\n const issued = await issueGiftCard({\n firestore,\n hostId,\n amountCents: Number(body.amountCents ?? 0),\n recipientEmail: String(body.recipientEmail ?? ''),\n note: String(body.note ?? ''),\n issuer: { uid: decoded.uid, email: decoded.email ?? null },\n email: true,\n ownerOrg: owner?.org,\n })\n const { code, emailed } = issued\n return res.status(200).json({ ok: true, code, emailed })\n } catch (error) {\n if (error instanceof GiftCardIssueRefusal) {\n return res.status(error.status).json({ error: error.message })\n }\n console.error(error)\n return res.status(500).json({ error: 'Gift card operation failed' })\n }\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","GiftCardIssueRefusal","issueGiftCard","giftCardsHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","action","hostSnapshot","decoded","app","auth","verifyIdToken","firestore","hostRef","collection","doc","get","exists","memberRole","uid","owner","catch","checkEntitlement","org","code","trim","cardRef","existing","Number","set","frozenAtMs","frozenReason","releasedAtMs","Date","now","releasedBy","merge","ok","balanceCents","voidedAtMs","voidedBy","issued","amountCents","recipientEmail","note","issuer","email","ownerOrg","emailed","message","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/gift-cards.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsCommerceSiteKey } from './commerce-site-keys'\nimport { commerceSitePermissionRefusal } from '../model/plugin-permissions'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { GiftCardIssueRefusal, issueGiftCard } from './gift-card-issue'\n\n/**\n * Gift-card issue / void (AGL-2226).\n *\n * The `giftCards` subcollection has existed since AGL-322 — minted by\n * `billing-webhook.ts` when a gift-card product is bought, decremented by\n * `cart-checkout.ts` at redemption. Neither end had a console: grepping for\n * that collection returned exactly those two server files, so a\n * merchant on Business (the tier that carries `giftCards`) could sell store\n * credit and then not see one cent of the liability they had taken on, help\n * a customer who lost a code, or void a leaked one.\n *\n * WRITES ARE SERVER-SIDE, and that is not a style preference. The Firestore\n * rules' host catch-all would happily grant an admin `create`/`update` on\n * `giftCards` — it is not in any exclusion list. A client-writable\n * `balanceCents` is a self-service discount machine: set it to any number,\n * redeem it at checkout, and `cart-checkout.ts` applies it as amount-off\n * against a Stripe coupon. The reads stay client-side (the catch-all's\n * `allow read` already grants them, and the card wants live updates), so\n * only the two operations that move money come through here.\n *\n * Re-checks the entitlement server-side for the same reason\n * `billing-webhook.ts:2285` does: a card issued while the plan was live is\n * honoured, but a lapsed plan must not mint new ones.\n */\nexport const giftCardsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body || '{}') : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const action = String(body.action ?? '')\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n if (action !== 'issue' && action !== 'void' && action !== 'release') {\n return res.status(400).json({ error: 'Unknown action' })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // AN ALLOWLIST (AGL-2372), not the `!role || role === 'viewer'` denylist\n // this route used to carry — the same correction AGL-2262 made to\n // `pos-order.ts` and `draft-order.ts` and did not reach here.\n //\n // A denylist silently WIDENS every time the role union grows. It did:\n // `HostAccessRole` is `admin | editor | author | viewer` since AGL-2334,\n // and `author` is grantable on any host through `/api/hosts/members`. So\n // \"not literally viewer\" started admitting authors — a role that exists to\n // edit content — to a route that mints and destroys store credit.\n //\n // `admin | editor` is the bar this route always meant (the comment here\n // said \"owner/admin/editor, not viewer\"); the difference is that a role\n // added tomorrow is now refused until someone decides otherwise, rather\n // than admitted by silence. Absent stays refused: `undefined` is not in\n // the set, which matters with `strictNullChecks` off.\n // \"Issue gift cards\" (`commerce.giftCards`, AGL-4047), whose preset is the site's Admin\n // and Editor this asked for, read as the rules would read it.\n if (!holdsCommerceSiteKey(hostSnapshot, decoded.uid, 'commerce.giftCards')) {\n return res.status(403).json({ error: commerceSitePermissionRefusal('commerce.giftCards') })\n }\n const owner = await getOrgForHost(hostId).catch(() => null)\n if (!Aglyn.checkEntitlement(owner?.org as never, 'giftCards')) {\n return res\n .status(402)\n .json({ error: 'Gift cards are not included on this plan' })\n }\n\n // RELEASE A HELD CARD (AGL-3363). A fraud warning, review or dispute on\n // the payment that bought a card freezes it; the merchant who has\n // satisfied themselves the payment is genuine lifts that here. A voided\n // card stays voided: release restores redemption, never a balance.\n if (action === 'release') {\n const code = String(body.code ?? '').trim()\n if (!code) return res.status(400).json({ error: 'Missing code' })\n const cardRef = hostRef.collection('giftCards').doc(code)\n const existing = await cardRef.get()\n if (!existing.exists) {\n return res.status(404).json({ error: 'Unknown gift card' })\n }\n if (!(Number(existing.get('frozenAtMs')) > 0)) {\n return res.status(409).json({ error: 'This gift card is not on hold' })\n }\n await cardRef.set(\n {\n frozenAtMs: null,\n frozenReason: null,\n releasedAtMs: Date.now(),\n releasedBy: decoded.uid,\n },\n { merge: true },\n )\n return res.status(200).json({ ok: true, code })\n }\n\n if (action === 'void') {\n const code = String(body.code ?? '').trim()\n if (!code) return res.status(400).json({ error: 'Missing code' })\n const cardRef = hostRef.collection('giftCards').doc(code)\n // Zero the balance rather than delete the document. AGL-1767 is the\n // reason: a deleted card that a checkout still had in flight was\n // RESURRECTED by the webhook's `increment(-cents)` into a NEGATIVE\n // balance, which then read as a card with money on it and understated\n // the outstanding total. A zeroed card decrements to a negative number\n // that no redemption path will ever accept, and the row stays\n // auditable.\n const existing = await cardRef.get()\n if (!existing.exists) {\n return res.status(404).json({ error: 'Unknown gift card' })\n }\n await cardRef.set(\n {\n balanceCents: 0,\n voidedAtMs: Date.now(),\n voidedBy: decoded.uid,\n },\n { merge: true },\n )\n return res.status(200).json({ ok: true, code })\n }\n\n // Issued through the one issue path the gift card import uses too\n // (AGL-3551): the amount bounded, the code minted and never overwritten,\n // the activity line written, and the recipient emailed.\n const issued = await issueGiftCard({\n firestore,\n hostId,\n amountCents: Number(body.amountCents ?? 0),\n recipientEmail: String(body.recipientEmail ?? ''),\n note: String(body.note ?? ''),\n issuer: { uid: decoded.uid, email: decoded.email ?? null },\n email: true,\n ownerOrg: owner?.org,\n })\n const { code, emailed } = issued\n return res.status(200).json({ ok: true, code, emailed })\n } catch (error) {\n if (error instanceof GiftCardIssueRefusal) {\n return res.status(error.status).json({ error: error.message })\n }\n console.error(error)\n return res.status(500).json({ error: 'Gift card operation failed' })\n }\n}\n"],"names":["holdsCommerceSiteKey","commerceSitePermissionRefusal","Aglyn","firebaseAdmin","getOrgForHost","GiftCardIssueRefusal","issueGiftCard","giftCardsHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","action","decoded","app","auth","verifyIdToken","firestore","hostRef","collection","doc","hostSnapshot","get","exists","uid","owner","catch","checkEntitlement","org","code","trim","cardRef","existing","Number","set","frozenAtMs","frozenReason","releasedAtMs","Date","now","releasedBy","merge","ok","balanceCents","voidedAtMs","voidedBy","issued","amountCents","recipientEmail","note","issuer","email","ownerOrg","emailed","message","console"],"mappings":"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"}
@@ -14,7 +14,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { pluginShippingRateQuoter } from "@aglyn/aglyn/plugin-manager/plugin-shipping-rates";
17
+ */ import { holdsCommerceSiteKey } from "./commerce-site-keys.js";
18
+ import { commerceSitePermissionRefusal } from "../model/plugin-permissions.js";
19
+ import { pluginShippingRateQuoter } from "@aglyn/aglyn/plugin-manager/plugin-shipping-rates";
18
20
  import { createResourceUid } from "@aglyn/aglyn/server";
19
21
  import * as Aglyn from "@aglyn/aglyn/server";
20
22
  import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
@@ -283,16 +285,17 @@ export const localFulfillmentHandler = async (req, res)=>{
283
285
  });
284
286
  }
285
287
  try {
286
- var _host_get;
287
288
  const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId);
288
289
  const host = await hostRef.get();
289
290
  if (!host.exists) return res.status(404).json({
290
291
  error: 'Unknown site'
291
292
  });
292
- const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
293
- if (role !== 'admin' && role !== 'editor') return res.status(403).json({
294
- error: 'Not permitted'
295
- });
293
+ // "Manage orders" (`commerce.orders`, AGL-4047).
294
+ if (!holdsCommerceSiteKey(host, uid, 'commerce.orders')) {
295
+ return res.status(403).json({
296
+ error: commerceSitePermissionRefusal('commerce.orders')
297
+ });
298
+ }
296
299
  if (req.method === 'GET') {
297
300
  // Distance zones need the store and the buyer placed on a map, which
298
301
  // only the shipping plugin's address check can do here.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/local-fulfillment-status.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginShippingRateQuoter } from '@aglyn/aglyn/plugin-manager/plugin-shipping-rates'\nimport { createResourceUid, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport * as CommerceModel from '../model'\nimport { ORDER_DELIVERED_EVENT, ORDER_FULFILLED_EVENT } from '../model/order-events'\nimport { notifyOrderBuyer, type OrderBuyerEvent } from './order-notifications'\nimport { fulfillmentEventView, stageOrderEvent } from './order-events'\nimport { placeDeliveryAddress } from './local-fulfillment'\n\n/**\n * `commerce/local-fulfillment` (AGL-3624): the store moving a pickup or a\n * local delivery along — from the console's queue, the order dialog, or the\n * native Aglyn app, which posts the same body with the member's ID token.\n *\n * POST { hostId, orderId, action }\n * pickup: `ready` · `preparing` (undo a ready) · `picked_up`\n * delivery: `out_for_delivery` · `delivered` · `delivery_failed`\n * POST { hostId, action: 'locate' } places the store on the map for\n * distance zones, through the shipping plugin's address check\n * GET ?hostId whether distance zones can work here\n *\n * Every status write is ONE transaction that re-reads the order and re-asks\n * the transition, so a stale queue row or a second device cannot skip a\n * step or hand the same order over twice: a repeat of the move the order\n * already made answers `already` and writes nothing.\n *\n * HANDING OVER IS FULFILLING. `picked_up` and `delivered` record a\n * fulfillment covering every unit still open (marked `handover`, so no\n * \"shipped\" message and no carrier), move the order to `fulfilled` and on to\n * `delivered`, and raise `order.fulfilled` and `order.delivered` exactly as a\n * shipment does — accounting, outbound webhooks and workflows hear the same\n * events whichever way the goods left. No stock moves: it came off the\n * location's shelf when the money landed.\n *\n * The buyer hears about each step through the AGL-3610 notifier, once per\n * step, and only after the write lands.\n *\n * Role gate: `admin` and `editor`, as `fulfill-order.ts`.\n */\n\ntype PickupAction = 'ready' | 'preparing' | 'picked_up'\ntype DeliveryAction = 'out_for_delivery' | 'delivered' | 'delivery_failed'\nexport type LocalFulfillmentAction = PickupAction | DeliveryAction\n\nconst PICKUP_ACTIONS: readonly string[] = ['ready', 'preparing', 'picked_up']\nconst DELIVERY_ACTIONS: readonly string[] = ['out_for_delivery', 'delivered', 'delivery_failed']\n\nexport type LocalFulfillmentOutcome =\n | { outcome: 'updated'; status: CommerceModel.OrderStatus; notify: OrderBuyerEvent | null }\n | { outcome: 'already' }\n | { outcome: 'no_such_order' }\n /** The order is not a pickup (or not a local delivery) order. */\n | { outcome: 'wrong_method' }\n /** The step is not allowed from where the order is; `from` names it. */\n | { outcome: 'blocked'; from: string }\n\nconst NOTE_MAX = 200\n\n/**\n * Moves one order a step. The transaction, with NO authorization — the\n * caller has already proven the member may.\n */\nexport async function moveLocalFulfillment(request: {\n hostId: string\n orderId: string\n action: LocalFulfillmentAction\n /** `picked_up`: who collected it, when the counter noted a name. */\n pickedUpBy?: string\n /** `delivery_failed`: why. */\n reason?: string\n nowMs?: number\n}): Promise<LocalFulfillmentOutcome> {\n const { hostId, orderId, action } = request\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = firestore.collection('hosts').doc(hostId).collection('orders').doc(orderId)\n return firestore.runTransaction(async (transaction): Promise<LocalFulfillmentOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const raw = (snapshot.data() ?? {}) as Record<string, unknown>\n const order = CommerceModel.liftLegacyOrder(raw as never)\n const atMs = request.nowMs ?? Date.now()\n const isPickup = PICKUP_ACTIONS.includes(action)\n if (isPickup ? order.fulfillmentMethod !== 'pickup' || !order.pickup : order.fulfillmentMethod !== 'local_delivery' || !order.localDelivery) {\n return { outcome: 'wrong_method' }\n }\n // Nothing to hand over on an order that was cancelled or refunded.\n if (order.status === 'cancelled' || order.status === 'refunded' || order.status === 'pending') {\n return { outcome: 'blocked', from: order.status }\n }\n\n const handover = action === 'picked_up' || action === 'delivered'\n let next: Record<string, unknown>\n let detail: string\n let event: string\n let notify: OrderBuyerEvent | null\n let status: CommerceModel.OrderStatus = order.status\n\n if (isPickup) {\n const pickup = order.pickup as CommerceModel.OrderPickup\n const from = CommerceModel.orderPickupStatus(pickup.status)\n const to: CommerceModel.OrderPickupStatus = action as PickupAction\n if (from === to) return { outcome: 'already' }\n if (!CommerceModel.canTransitionPickup(from, to)) return { outcome: 'blocked', from: CommerceModel.ORDER_PICKUP_STATUS_LABELS[from] }\n const pickedUpBy = String(request.pickedUpBy ?? '').trim().slice(0, NOTE_MAX)\n const updated: CommerceModel.OrderPickup = {\n ...pickup,\n status: to,\n updatedAtMs: atMs,\n ...(to === 'ready' ? { readyAtMs: atMs } : {}),\n ...(to === 'picked_up' ? { pickedUpAtMs: atMs, ...(pickedUpBy ? { pickedUpBy } : {}) } : {}),\n }\n if (to === 'preparing') delete updated.readyAtMs\n next = { pickup: updated }\n event = to === 'ready' ? 'ready-for-pickup' : to === 'picked_up' ? 'picked-up' : 'pickup-preparing'\n detail =\n to === 'ready'\n ? `Ready for pickup at ${pickup.locationName}`\n : to === 'picked_up'\n ? `Picked up at ${pickup.locationName}${pickedUpBy ? ` by ${pickedUpBy}` : ''}`\n : 'Back to preparing'\n notify = to === 'ready' ? 'ready_for_pickup' : to === 'picked_up' ? 'picked_up' : null\n } else {\n const delivery = order.localDelivery as CommerceModel.OrderLocalDelivery\n const from = CommerceModel.orderLocalDeliveryStatus(delivery.status)\n const to: CommerceModel.OrderLocalDeliveryStatus =\n action === 'delivery_failed' ? 'failed' : (action as 'out_for_delivery' | 'delivered')\n if (from === to) return { outcome: 'already' }\n if (!CommerceModel.canTransitionLocalDelivery(from, to)) {\n return { outcome: 'blocked', from: CommerceModel.ORDER_LOCAL_DELIVERY_STATUS_LABELS[from] }\n }\n const reason = String(request.reason ?? '').trim().slice(0, NOTE_MAX)\n const updated: CommerceModel.OrderLocalDelivery = {\n ...delivery,\n status: to,\n updatedAtMs: atMs,\n ...(to === 'out_for_delivery' ? { outForDeliveryAtMs: atMs } : {}),\n ...(to === 'delivered' ? { deliveredAtMs: atMs } : {}),\n ...(to === 'failed' ? { failedAtMs: atMs, ...(reason ? { failedReason: reason } : {}) } : {}),\n }\n next = { localDelivery: updated }\n event = to === 'out_for_delivery' ? 'out-for-delivery' : to === 'delivered' ? 'delivered-locally' : 'delivery-failed'\n detail =\n to === 'out_for_delivery'\n ? 'Out for delivery'\n : to === 'delivered'\n ? 'Delivered by the store'\n : `Delivery failed${reason ? `: ${reason}` : ''}`\n notify = to === 'out_for_delivery' ? 'out_for_delivery' : to === 'delivered' ? 'delivered' : null\n }\n\n let timeline = CommerceModel.appendOrderEvent(order, event, detail, atMs)\n let fulfillment: CommerceModel.OrderFulfillment | null = null\n if (handover) {\n // Every unit still open, whatever it is: the buyer has the whole order.\n const lines = CommerceModel.orderLineFulfillmentStates(order)\n .filter((state) => state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n if (lines.length) {\n fulfillment = {\n id: createResourceUid(),\n lineItemIds: lines.map((entry) => entry.lineItemId),\n lines,\n status: 'active',\n handover: isPickup ? 'pickup' : 'local_delivery',\n // The buyer is told by this route's own message, never \"shipped\".\n notify: false,\n atMs,\n }\n }\n const fulfillments = [...(order.fulfillments ?? []), ...(fulfillment ? [fulfillment] : [])]\n const fulfilled = CommerceModel.statusFromFulfillments({ lineItems: order.lineItems, fulfillments })\n const through = fulfilled === 'paid' ? 'fulfilled' : fulfilled\n if (order.status === 'delivered') {\n // Marked delivered by hand already: the handover only records itself.\n status = 'delivered'\n } else {\n if (through !== order.status && !CommerceModel.canTransitionOrder(order.status, through)) {\n return { outcome: 'blocked', from: order.status }\n }\n status = CommerceModel.canTransitionOrder(through, 'delivered') ? 'delivered' : through\n }\n if (fulfillment) {\n timeline = [...timeline, { atMs, event: 'fulfilled', detail: isPickup ? 'Handed over at pickup' : 'Handed over on delivery' }]\n }\n if (status === 'delivered') timeline = [...timeline, { atMs, event: 'delivered' }]\n next = { ...next, fulfillments }\n }\n const written = {\n ...next,\n status,\n updatedAtMs: atMs,\n timeline,\n }\n const listFields = CommerceModel.orderLocalFulfillmentListFields({\n ...order,\n ...(written as Partial<CommerceModel.HostOrder>),\n })\n transaction.update(orderRef, { ...written, ...(listFields ?? {}) })\n const after = { ...raw, ...written }\n if (fulfillment) {\n stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, {\n hostId,\n orderId,\n key: fulfillment.id,\n order: after,\n extra: { fulfillment: fulfillmentEventView(order, fulfillment) },\n })\n }\n if (status === 'delivered' && order.status !== 'delivered') {\n stageOrderEvent(transaction, ORDER_DELIVERED_EVENT, { hostId, orderId, key: 'delivered', order: after })\n }\n return { outcome: 'updated', status, notify }\n })\n}\n\nfunction readBody(req: Parameters<PluginApiHandler>[0]): Record<string, unknown> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body || '{}')\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? (req.body as Record<string, unknown>) : {}\n}\n\nconst badId = (value: string) => !value || value.includes('/') || /^__.*__$/.test(value)\n\nexport const localFulfillmentHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST' && req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n if (!authorization.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthenticated' })\n const body = req.method === 'POST' ? readBody(req) : ((req.query ?? {}) as Record<string, unknown>)\n const hostId = String(body['hostId'] ?? '')\n if (badId(hostId)) return res.status(400).json({ error: 'Missing hostId' })\n const action = String(body['action'] ?? '')\n const orderId = String(body['orderId'] ?? '')\n if (req.method === 'POST' && action !== 'locate') {\n if (!PICKUP_ACTIONS.includes(action) && !DELIVERY_ACTIONS.includes(action)) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n if (badId(orderId)) return res.status(400).json({ error: 'Missing orderId' })\n }\n\n let uid: string\n try {\n uid = (await firebaseAdmin.app().auth().verifyIdToken(authorization.slice('Bearer '.length))).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n try {\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (host.get('memberRoles') ?? {})[uid]\n if (role !== 'admin' && role !== 'editor') return res.status(403).json({ error: 'Not permitted' })\n\n if (req.method === 'GET') {\n // Distance zones need the store and the buyer placed on a map, which\n // only the shipping plugin's address check can do here.\n const quoter = pluginShippingRateQuoter()\n const radius = Boolean(\n quoter?.validateAddress && (await quoter.available(hostId).catch(() => false)),\n )\n return res.status(200).json({ radius })\n }\n\n const ownerOrg = await getOrgForHost(hostId).catch(() => null)\n if (!Aglyn.checkEntitlement(ownerOrg?.org as any, 'commerce')) {\n return res.status(403).json({ error: 'Selling is not enabled on this plan' })\n }\n\n if (action === 'locate') {\n const locationId = String(body['locationId'] ?? '')\n if (badId(locationId)) return res.status(400).json({ error: 'Choose the location deliveries leave from' })\n const location = await hostRef.collection('locations').doc(locationId).get()\n const address = CommerceModel.normalizePostalAddress(location.get('postalAddress'))\n if (!location.exists || !address?.line1 || !address.postalCode || !address.country) {\n return res.status(409).json({ error: 'Add a full street address to that location first.' })\n }\n const coordinates = await placeDeliveryAddress(hostId, { ...address, country: address.country })\n if (!coordinates) {\n return res.status(409).json({\n error: 'Your address check could not place this address on a map. Use postal-code zones instead.',\n })\n }\n return res.status(200).json({ ok: true, origin: coordinates })\n }\n\n const outcome = await moveLocalFulfillment({\n hostId,\n orderId,\n action: action as LocalFulfillmentAction,\n pickedUpBy: typeof body['pickedUpBy'] === 'string' ? body['pickedUpBy'] : undefined,\n reason: typeof body['reason'] === 'string' ? body['reason'] : undefined,\n })\n if (outcome.outcome === 'no_such_order') return res.status(404).json({ error: 'Unknown order' })\n if (outcome.outcome === 'wrong_method') {\n return res.status(409).json({\n error: PICKUP_ACTIONS.includes(action)\n ? 'This order is not a pickup order'\n : 'This order is not a local delivery order',\n })\n }\n if (outcome.outcome === 'blocked') {\n return res.status(409).json({ error: `This order can’t move on from \"${outcome.from}\"` })\n }\n if (outcome.outcome === 'already') return res.status(200).json({ ok: true, already: true })\n // After the write, never inside it, and never failing it (AGL-3610).\n if (outcome.notify) await notifyOrderBuyer({ hostId, orderId }, outcome.notify)\n return res.status(200).json({ ok: true, status: outcome.status })\n } catch (error) {\n // The transaction is all-or-nothing: nothing landed, a retry is safe.\n console.error('[local-fulfillment] move failed', hostId, orderId, action, error)\n return res.status(500).json({ error: 'The order could not be updated' })\n }\n}\n"],"names":["pluginShippingRateQuoter","createResourceUid","Aglyn","firebaseAdmin","getOrgForHost","isRefusedIdToken","CommerceModel","ORDER_DELIVERED_EVENT","ORDER_FULFILLED_EVENT","notifyOrderBuyer","fulfillmentEventView","stageOrderEvent","placeDeliveryAddress","PICKUP_ACTIONS","DELIVERY_ACTIONS","NOTE_MAX","moveLocalFulfillment","request","hostId","orderId","action","firestore","app","orderRef","collection","doc","runTransaction","transaction","snapshot","get","exists","outcome","raw","data","order","liftLegacyOrder","atMs","nowMs","Date","now","isPickup","includes","fulfillmentMethod","pickup","localDelivery","status","from","handover","next","detail","event","notify","orderPickupStatus","to","canTransitionPickup","ORDER_PICKUP_STATUS_LABELS","pickedUpBy","String","trim","slice","updated","updatedAtMs","readyAtMs","pickedUpAtMs","locationName","delivery","orderLocalDeliveryStatus","canTransitionLocalDelivery","ORDER_LOCAL_DELIVERY_STATUS_LABELS","reason","outForDeliveryAtMs","deliveredAtMs","failedAtMs","failedReason","timeline","appendOrderEvent","fulfillment","lines","orderLineFulfillmentStates","filter","state","remainingQuantity","map","lineItemId","quantity","length","id","lineItemIds","entry","fulfillments","fulfilled","statusFromFulfillments","lineItems","through","canTransitionOrder","written","listFields","orderLocalFulfillmentListFields","update","after","key","extra","readBody","req","body","parsed","JSON","parse","badId","value","test","localFulfillmentHandler","res","method","json","error","authorization","headers","startsWith","query","uid","auth","verifyIdToken","host","hostRef","role","quoter","radius","Boolean","validateAddress","available","catch","ownerOrg","checkEntitlement","org","locationId","location","address","normalizePostalAddress","line1","postalCode","country","coordinates","ok","origin","undefined","already","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/local-fulfillment-status.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { holdsCommerceSiteKey } from './commerce-site-keys'\nimport { commerceSitePermissionRefusal } from '../model/plugin-permissions'\nimport { pluginShippingRateQuoter } from '@aglyn/aglyn/plugin-manager/plugin-shipping-rates'\nimport { createResourceUid, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport * as CommerceModel from '../model'\nimport { ORDER_DELIVERED_EVENT, ORDER_FULFILLED_EVENT } from '../model/order-events'\nimport { notifyOrderBuyer, type OrderBuyerEvent } from './order-notifications'\nimport { fulfillmentEventView, stageOrderEvent } from './order-events'\nimport { placeDeliveryAddress } from './local-fulfillment'\n\n/**\n * `commerce/local-fulfillment` (AGL-3624): the store moving a pickup or a\n * local delivery along — from the console's queue, the order dialog, or the\n * native Aglyn app, which posts the same body with the member's ID token.\n *\n * POST { hostId, orderId, action }\n * pickup: `ready` · `preparing` (undo a ready) · `picked_up`\n * delivery: `out_for_delivery` · `delivered` · `delivery_failed`\n * POST { hostId, action: 'locate' } places the store on the map for\n * distance zones, through the shipping plugin's address check\n * GET ?hostId whether distance zones can work here\n *\n * Every status write is ONE transaction that re-reads the order and re-asks\n * the transition, so a stale queue row or a second device cannot skip a\n * step or hand the same order over twice: a repeat of the move the order\n * already made answers `already` and writes nothing.\n *\n * HANDING OVER IS FULFILLING. `picked_up` and `delivered` record a\n * fulfillment covering every unit still open (marked `handover`, so no\n * \"shipped\" message and no carrier), move the order to `fulfilled` and on to\n * `delivered`, and raise `order.fulfilled` and `order.delivered` exactly as a\n * shipment does — accounting, outbound webhooks and workflows hear the same\n * events whichever way the goods left. No stock moves: it came off the\n * location's shelf when the money landed.\n *\n * The buyer hears about each step through the AGL-3610 notifier, once per\n * step, and only after the write lands.\n *\n * Role gate: `admin` and `editor`, as `fulfill-order.ts`.\n */\n\ntype PickupAction = 'ready' | 'preparing' | 'picked_up'\ntype DeliveryAction = 'out_for_delivery' | 'delivered' | 'delivery_failed'\nexport type LocalFulfillmentAction = PickupAction | DeliveryAction\n\nconst PICKUP_ACTIONS: readonly string[] = ['ready', 'preparing', 'picked_up']\nconst DELIVERY_ACTIONS: readonly string[] = ['out_for_delivery', 'delivered', 'delivery_failed']\n\nexport type LocalFulfillmentOutcome =\n | { outcome: 'updated'; status: CommerceModel.OrderStatus; notify: OrderBuyerEvent | null }\n | { outcome: 'already' }\n | { outcome: 'no_such_order' }\n /** The order is not a pickup (or not a local delivery) order. */\n | { outcome: 'wrong_method' }\n /** The step is not allowed from where the order is; `from` names it. */\n | { outcome: 'blocked'; from: string }\n\nconst NOTE_MAX = 200\n\n/**\n * Moves one order a step. The transaction, with NO authorization — the\n * caller has already proven the member may.\n */\nexport async function moveLocalFulfillment(request: {\n hostId: string\n orderId: string\n action: LocalFulfillmentAction\n /** `picked_up`: who collected it, when the counter noted a name. */\n pickedUpBy?: string\n /** `delivery_failed`: why. */\n reason?: string\n nowMs?: number\n}): Promise<LocalFulfillmentOutcome> {\n const { hostId, orderId, action } = request\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = firestore.collection('hosts').doc(hostId).collection('orders').doc(orderId)\n return firestore.runTransaction(async (transaction): Promise<LocalFulfillmentOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const raw = (snapshot.data() ?? {}) as Record<string, unknown>\n const order = CommerceModel.liftLegacyOrder(raw as never)\n const atMs = request.nowMs ?? Date.now()\n const isPickup = PICKUP_ACTIONS.includes(action)\n if (isPickup ? order.fulfillmentMethod !== 'pickup' || !order.pickup : order.fulfillmentMethod !== 'local_delivery' || !order.localDelivery) {\n return { outcome: 'wrong_method' }\n }\n // Nothing to hand over on an order that was cancelled or refunded.\n if (order.status === 'cancelled' || order.status === 'refunded' || order.status === 'pending') {\n return { outcome: 'blocked', from: order.status }\n }\n\n const handover = action === 'picked_up' || action === 'delivered'\n let next: Record<string, unknown>\n let detail: string\n let event: string\n let notify: OrderBuyerEvent | null\n let status: CommerceModel.OrderStatus = order.status\n\n if (isPickup) {\n const pickup = order.pickup as CommerceModel.OrderPickup\n const from = CommerceModel.orderPickupStatus(pickup.status)\n const to: CommerceModel.OrderPickupStatus = action as PickupAction\n if (from === to) return { outcome: 'already' }\n if (!CommerceModel.canTransitionPickup(from, to)) return { outcome: 'blocked', from: CommerceModel.ORDER_PICKUP_STATUS_LABELS[from] }\n const pickedUpBy = String(request.pickedUpBy ?? '').trim().slice(0, NOTE_MAX)\n const updated: CommerceModel.OrderPickup = {\n ...pickup,\n status: to,\n updatedAtMs: atMs,\n ...(to === 'ready' ? { readyAtMs: atMs } : {}),\n ...(to === 'picked_up' ? { pickedUpAtMs: atMs, ...(pickedUpBy ? { pickedUpBy } : {}) } : {}),\n }\n if (to === 'preparing') delete updated.readyAtMs\n next = { pickup: updated }\n event = to === 'ready' ? 'ready-for-pickup' : to === 'picked_up' ? 'picked-up' : 'pickup-preparing'\n detail =\n to === 'ready'\n ? `Ready for pickup at ${pickup.locationName}`\n : to === 'picked_up'\n ? `Picked up at ${pickup.locationName}${pickedUpBy ? ` by ${pickedUpBy}` : ''}`\n : 'Back to preparing'\n notify = to === 'ready' ? 'ready_for_pickup' : to === 'picked_up' ? 'picked_up' : null\n } else {\n const delivery = order.localDelivery as CommerceModel.OrderLocalDelivery\n const from = CommerceModel.orderLocalDeliveryStatus(delivery.status)\n const to: CommerceModel.OrderLocalDeliveryStatus =\n action === 'delivery_failed' ? 'failed' : (action as 'out_for_delivery' | 'delivered')\n if (from === to) return { outcome: 'already' }\n if (!CommerceModel.canTransitionLocalDelivery(from, to)) {\n return { outcome: 'blocked', from: CommerceModel.ORDER_LOCAL_DELIVERY_STATUS_LABELS[from] }\n }\n const reason = String(request.reason ?? '').trim().slice(0, NOTE_MAX)\n const updated: CommerceModel.OrderLocalDelivery = {\n ...delivery,\n status: to,\n updatedAtMs: atMs,\n ...(to === 'out_for_delivery' ? { outForDeliveryAtMs: atMs } : {}),\n ...(to === 'delivered' ? { deliveredAtMs: atMs } : {}),\n ...(to === 'failed' ? { failedAtMs: atMs, ...(reason ? { failedReason: reason } : {}) } : {}),\n }\n next = { localDelivery: updated }\n event = to === 'out_for_delivery' ? 'out-for-delivery' : to === 'delivered' ? 'delivered-locally' : 'delivery-failed'\n detail =\n to === 'out_for_delivery'\n ? 'Out for delivery'\n : to === 'delivered'\n ? 'Delivered by the store'\n : `Delivery failed${reason ? `: ${reason}` : ''}`\n notify = to === 'out_for_delivery' ? 'out_for_delivery' : to === 'delivered' ? 'delivered' : null\n }\n\n let timeline = CommerceModel.appendOrderEvent(order, event, detail, atMs)\n let fulfillment: CommerceModel.OrderFulfillment | null = null\n if (handover) {\n // Every unit still open, whatever it is: the buyer has the whole order.\n const lines = CommerceModel.orderLineFulfillmentStates(order)\n .filter((state) => state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n if (lines.length) {\n fulfillment = {\n id: createResourceUid(),\n lineItemIds: lines.map((entry) => entry.lineItemId),\n lines,\n status: 'active',\n handover: isPickup ? 'pickup' : 'local_delivery',\n // The buyer is told by this route's own message, never \"shipped\".\n notify: false,\n atMs,\n }\n }\n const fulfillments = [...(order.fulfillments ?? []), ...(fulfillment ? [fulfillment] : [])]\n const fulfilled = CommerceModel.statusFromFulfillments({ lineItems: order.lineItems, fulfillments })\n const through = fulfilled === 'paid' ? 'fulfilled' : fulfilled\n if (order.status === 'delivered') {\n // Marked delivered by hand already: the handover only records itself.\n status = 'delivered'\n } else {\n if (through !== order.status && !CommerceModel.canTransitionOrder(order.status, through)) {\n return { outcome: 'blocked', from: order.status }\n }\n status = CommerceModel.canTransitionOrder(through, 'delivered') ? 'delivered' : through\n }\n if (fulfillment) {\n timeline = [...timeline, { atMs, event: 'fulfilled', detail: isPickup ? 'Handed over at pickup' : 'Handed over on delivery' }]\n }\n if (status === 'delivered') timeline = [...timeline, { atMs, event: 'delivered' }]\n next = { ...next, fulfillments }\n }\n const written = {\n ...next,\n status,\n updatedAtMs: atMs,\n timeline,\n }\n const listFields = CommerceModel.orderLocalFulfillmentListFields({\n ...order,\n ...(written as Partial<CommerceModel.HostOrder>),\n })\n transaction.update(orderRef, { ...written, ...(listFields ?? {}) })\n const after = { ...raw, ...written }\n if (fulfillment) {\n stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, {\n hostId,\n orderId,\n key: fulfillment.id,\n order: after,\n extra: { fulfillment: fulfillmentEventView(order, fulfillment) },\n })\n }\n if (status === 'delivered' && order.status !== 'delivered') {\n stageOrderEvent(transaction, ORDER_DELIVERED_EVENT, { hostId, orderId, key: 'delivered', order: after })\n }\n return { outcome: 'updated', status, notify }\n })\n}\n\nfunction readBody(req: Parameters<PluginApiHandler>[0]): Record<string, unknown> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body || '{}')\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? (req.body as Record<string, unknown>) : {}\n}\n\nconst badId = (value: string) => !value || value.includes('/') || /^__.*__$/.test(value)\n\nexport const localFulfillmentHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST' && req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n if (!authorization.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthenticated' })\n const body = req.method === 'POST' ? readBody(req) : ((req.query ?? {}) as Record<string, unknown>)\n const hostId = String(body['hostId'] ?? '')\n if (badId(hostId)) return res.status(400).json({ error: 'Missing hostId' })\n const action = String(body['action'] ?? '')\n const orderId = String(body['orderId'] ?? '')\n if (req.method === 'POST' && action !== 'locate') {\n if (!PICKUP_ACTIONS.includes(action) && !DELIVERY_ACTIONS.includes(action)) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n if (badId(orderId)) return res.status(400).json({ error: 'Missing orderId' })\n }\n\n let uid: string\n try {\n uid = (await firebaseAdmin.app().auth().verifyIdToken(authorization.slice('Bearer '.length))).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n try {\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n // \"Manage orders\" (`commerce.orders`, AGL-4047).\n if (!holdsCommerceSiteKey(host, uid, 'commerce.orders')) {\n return res.status(403).json({ error: commerceSitePermissionRefusal('commerce.orders') })\n }\n\n if (req.method === 'GET') {\n // Distance zones need the store and the buyer placed on a map, which\n // only the shipping plugin's address check can do here.\n const quoter = pluginShippingRateQuoter()\n const radius = Boolean(\n quoter?.validateAddress && (await quoter.available(hostId).catch(() => false)),\n )\n return res.status(200).json({ radius })\n }\n\n const ownerOrg = await getOrgForHost(hostId).catch(() => null)\n if (!Aglyn.checkEntitlement(ownerOrg?.org as any, 'commerce')) {\n return res.status(403).json({ error: 'Selling is not enabled on this plan' })\n }\n\n if (action === 'locate') {\n const locationId = String(body['locationId'] ?? '')\n if (badId(locationId)) return res.status(400).json({ error: 'Choose the location deliveries leave from' })\n const location = await hostRef.collection('locations').doc(locationId).get()\n const address = CommerceModel.normalizePostalAddress(location.get('postalAddress'))\n if (!location.exists || !address?.line1 || !address.postalCode || !address.country) {\n return res.status(409).json({ error: 'Add a full street address to that location first.' })\n }\n const coordinates = await placeDeliveryAddress(hostId, { ...address, country: address.country })\n if (!coordinates) {\n return res.status(409).json({\n error: 'Your address check could not place this address on a map. Use postal-code zones instead.',\n })\n }\n return res.status(200).json({ ok: true, origin: coordinates })\n }\n\n const outcome = await moveLocalFulfillment({\n hostId,\n orderId,\n action: action as LocalFulfillmentAction,\n pickedUpBy: typeof body['pickedUpBy'] === 'string' ? body['pickedUpBy'] : undefined,\n reason: typeof body['reason'] === 'string' ? body['reason'] : undefined,\n })\n if (outcome.outcome === 'no_such_order') return res.status(404).json({ error: 'Unknown order' })\n if (outcome.outcome === 'wrong_method') {\n return res.status(409).json({\n error: PICKUP_ACTIONS.includes(action)\n ? 'This order is not a pickup order'\n : 'This order is not a local delivery order',\n })\n }\n if (outcome.outcome === 'blocked') {\n return res.status(409).json({ error: `This order can’t move on from \"${outcome.from}\"` })\n }\n if (outcome.outcome === 'already') return res.status(200).json({ ok: true, already: true })\n // After the write, never inside it, and never failing it (AGL-3610).\n if (outcome.notify) await notifyOrderBuyer({ hostId, orderId }, outcome.notify)\n return res.status(200).json({ ok: true, status: outcome.status })\n } catch (error) {\n // The transaction is all-or-nothing: nothing landed, a retry is safe.\n console.error('[local-fulfillment] move failed', hostId, orderId, action, error)\n return res.status(500).json({ error: 'The order could not be updated' })\n }\n}\n"],"names":["holdsCommerceSiteKey","commerceSitePermissionRefusal","pluginShippingRateQuoter","createResourceUid","Aglyn","firebaseAdmin","getOrgForHost","isRefusedIdToken","CommerceModel","ORDER_DELIVERED_EVENT","ORDER_FULFILLED_EVENT","notifyOrderBuyer","fulfillmentEventView","stageOrderEvent","placeDeliveryAddress","PICKUP_ACTIONS","DELIVERY_ACTIONS","NOTE_MAX","moveLocalFulfillment","request","hostId","orderId","action","firestore","app","orderRef","collection","doc","runTransaction","transaction","snapshot","get","exists","outcome","raw","data","order","liftLegacyOrder","atMs","nowMs","Date","now","isPickup","includes","fulfillmentMethod","pickup","localDelivery","status","from","handover","next","detail","event","notify","orderPickupStatus","to","canTransitionPickup","ORDER_PICKUP_STATUS_LABELS","pickedUpBy","String","trim","slice","updated","updatedAtMs","readyAtMs","pickedUpAtMs","locationName","delivery","orderLocalDeliveryStatus","canTransitionLocalDelivery","ORDER_LOCAL_DELIVERY_STATUS_LABELS","reason","outForDeliveryAtMs","deliveredAtMs","failedAtMs","failedReason","timeline","appendOrderEvent","fulfillment","lines","orderLineFulfillmentStates","filter","state","remainingQuantity","map","lineItemId","quantity","length","id","lineItemIds","entry","fulfillments","fulfilled","statusFromFulfillments","lineItems","through","canTransitionOrder","written","listFields","orderLocalFulfillmentListFields","update","after","key","extra","readBody","req","body","parsed","JSON","parse","badId","value","test","localFulfillmentHandler","res","method","json","error","authorization","headers","startsWith","query","uid","auth","verifyIdToken","hostRef","host","quoter","radius","Boolean","validateAddress","available","catch","ownerOrg","checkEntitlement","org","locationId","location","address","normalizePostalAddress","line1","postalCode","country","coordinates","ok","origin","undefined","already","console"],"mappings":"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"}
@@ -14,7 +14,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { firebaseAdmin, getOrgForHost, hostSendingIdentity, meterHostEmail } from "@aglyn/tenant-data-admin";
17
+ */ import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
18
+ import { firebaseAdmin, getOrgForHost, hostSendingIdentity, meterHostEmail } from "@aglyn/tenant-data-admin";
18
19
  import { loadHostEmailWithTokens, renderLoadedHostEmailWithTokens } from "@aglyn/tenant-data-admin/server/host-email-tokens";
19
20
  import * as CommerceModel from "../model/index.js";
20
21
  import { isEmailConfigured, sendEmail } from "@aglyn/shared-util-email";
@@ -51,7 +52,6 @@ import { consentGroupForHost, hostPublicOrigin, resolveBrandingProfile } from "@
51
52
  });
52
53
  }
53
54
  try {
54
- var _hostSnapshot_get;
55
55
  const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
56
56
  const firestore = firebaseAdmin.app().firestore();
57
57
  const hostRef = firestore.collection('hosts').doc(hostId);
@@ -63,10 +63,11 @@ import { consentGroupForHost, hostPublicOrigin, resolveBrandingProfile } from "@
63
63
  // that publishes to paying subscribers and sends them mail on the
64
64
  // merchant's brand. A denylist that widens whenever the role union grows
65
65
  // is the defect; the set is the fix. Absent is refused, not permitted.
66
- const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
67
- if (memberRole !== 'admin' && memberRole !== 'editor') {
66
+ // "Publish site content" (`site.publish`, AGL-4047): a post goes to paying
67
+ // subscribers the moment it exists, as the rules' delete asks it.
68
+ if (!holdsSiteKey(hostSnapshot, decoded.uid, 'site.publish')) {
68
69
  return res.status(403).json({
69
- error: 'Not permitted'
70
+ error: siteKeyRefusal('site.publish')
70
71
  });
71
72
  }
72
73
  const postRef = await hostRef.collection('memberPosts').add(_extends({