@aglyn/plugins-commerce 1.0.0-beta.143

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 (417) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +10 -0
  3. package/package.json +57 -0
  4. package/src/index.d.ts +38 -0
  5. package/src/index.js +38 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/account.d.ts +31 -0
  8. package/src/lib/components/account.js +645 -0
  9. package/src/lib/components/account.js.map +1 -0
  10. package/src/lib/components/cart.d.ts +38 -0
  11. package/src/lib/components/cart.js +851 -0
  12. package/src/lib/components/cart.js.map +1 -0
  13. package/src/lib/components/commerce-console-page.d.ts +32 -0
  14. package/src/lib/components/commerce-console-page.js +296 -0
  15. package/src/lib/components/commerce-console-page.js.map +1 -0
  16. package/src/lib/components/commerce-console-sections.d.ts +34 -0
  17. package/src/lib/components/commerce-console-sections.js +62 -0
  18. package/src/lib/components/commerce-console-sections.js.map +1 -0
  19. package/src/lib/components/console/catalog-organization-card.component.d.ts +14 -0
  20. package/src/lib/components/console/catalog-organization-card.component.js +1087 -0
  21. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -0
  22. package/src/lib/components/console/commerce-analytics-card.component.d.ts +27 -0
  23. package/src/lib/components/console/commerce-analytics-card.component.js +310 -0
  24. package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -0
  25. package/src/lib/components/console/commerce-glance-card.component.d.ts +20 -0
  26. package/src/lib/components/console/commerce-glance-card.component.js +340 -0
  27. package/src/lib/components/console/commerce-glance-card.component.js.map +1 -0
  28. package/src/lib/components/console/commerce-stat-tile.component.d.ts +37 -0
  29. package/src/lib/components/console/commerce-stat-tile.component.js +72 -0
  30. package/src/lib/components/console/commerce-stat-tile.component.js.map +1 -0
  31. package/src/lib/components/console/discounts-card.component.d.ts +14 -0
  32. package/src/lib/components/console/discounts-card.component.js +443 -0
  33. package/src/lib/components/console/discounts-card.component.js.map +1 -0
  34. package/src/lib/components/console/entitlement-gate.component.d.ts +76 -0
  35. package/src/lib/components/console/entitlement-gate.component.js +115 -0
  36. package/src/lib/components/console/entitlement-gate.component.js.map +1 -0
  37. package/src/lib/components/console/gift-cards-card.component.d.ts +29 -0
  38. package/src/lib/components/console/gift-cards-card.component.js +379 -0
  39. package/src/lib/components/console/gift-cards-card.component.js.map +1 -0
  40. package/src/lib/components/console/host-coupons-card.component.d.ts +14 -0
  41. package/src/lib/components/console/host-coupons-card.component.js +252 -0
  42. package/src/lib/components/console/host-coupons-card.component.js.map +1 -0
  43. package/src/lib/components/console/host-orders-card.component.d.ts +13 -0
  44. package/src/lib/components/console/host-orders-card.component.js +861 -0
  45. package/src/lib/components/console/host-orders-card.component.js.map +1 -0
  46. package/src/lib/components/console/locations-card.component.d.ts +14 -0
  47. package/src/lib/components/console/locations-card.component.js +256 -0
  48. package/src/lib/components/console/locations-card.component.js.map +1 -0
  49. package/src/lib/components/console/member-posts-card.component.d.ts +8 -0
  50. package/src/lib/components/console/member-posts-card.component.js +283 -0
  51. package/src/lib/components/console/member-posts-card.component.js.map +1 -0
  52. package/src/lib/components/console/newest-site-users-card.component.d.ts +36 -0
  53. package/src/lib/components/console/newest-site-users-card.component.js +126 -0
  54. package/src/lib/components/console/newest-site-users-card.component.js.map +1 -0
  55. package/src/lib/components/console/order-detail-dialog.component.d.ts +28 -0
  56. package/src/lib/components/console/order-detail-dialog.component.js +976 -0
  57. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -0
  58. package/src/lib/components/console/paid-media.d.ts +127 -0
  59. package/src/lib/components/console/paid-media.js +397 -0
  60. package/src/lib/components/console/paid-media.js.map +1 -0
  61. package/src/lib/components/console/payments-settings-card.component.d.ts +13 -0
  62. package/src/lib/components/console/payments-settings-card.component.js +295 -0
  63. package/src/lib/components/console/payments-settings-card.component.js.map +1 -0
  64. package/src/lib/components/console/pos-page.component.d.ts +13 -0
  65. package/src/lib/components/console/pos-page.component.js +916 -0
  66. package/src/lib/components/console/pos-page.component.js.map +1 -0
  67. package/src/lib/components/console/product-editor-dialog.component.d.ts +32 -0
  68. package/src/lib/components/console/product-editor-dialog.component.js +1374 -0
  69. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -0
  70. package/src/lib/components/console/products-hub-card.component.d.ts +16 -0
  71. package/src/lib/components/console/products-hub-card.component.js +1280 -0
  72. package/src/lib/components/console/products-hub-card.component.js.map +1 -0
  73. package/src/lib/components/console/products-hub-zone.component.d.ts +50 -0
  74. package/src/lib/components/console/products-hub-zone.component.js +200 -0
  75. package/src/lib/components/console/products-hub-zone.component.js.map +1 -0
  76. package/src/lib/components/console/recovery-queue-card.component.d.ts +39 -0
  77. package/src/lib/components/console/recovery-queue-card.component.js +262 -0
  78. package/src/lib/components/console/recovery-queue-card.component.js.map +1 -0
  79. package/src/lib/components/console/registers-card.component.d.ts +21 -0
  80. package/src/lib/components/console/registers-card.component.js +296 -0
  81. package/src/lib/components/console/registers-card.component.js.map +1 -0
  82. package/src/lib/components/console/reservations-card.component.d.ts +14 -0
  83. package/src/lib/components/console/reservations-card.component.js +650 -0
  84. package/src/lib/components/console/reservations-card.component.js.map +1 -0
  85. package/src/lib/components/console/reviews-moderation-card.component.d.ts +8 -0
  86. package/src/lib/components/console/reviews-moderation-card.component.js +235 -0
  87. package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -0
  88. package/src/lib/components/console/shipping-settings-card.component.d.ts +38 -0
  89. package/src/lib/components/console/shipping-settings-card.component.js +560 -0
  90. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -0
  91. package/src/lib/components/console/stock-movements-card.component.d.ts +31 -0
  92. package/src/lib/components/console/stock-movements-card.component.js +333 -0
  93. package/src/lib/components/console/stock-movements-card.component.js.map +1 -0
  94. package/src/lib/components/console/store-settings-card.component.d.ts +14 -0
  95. package/src/lib/components/console/store-settings-card.component.js +300 -0
  96. package/src/lib/components/console/store-settings-card.component.js.map +1 -0
  97. package/src/lib/components/console/storefront-tax-summary-card.component.d.ts +73 -0
  98. package/src/lib/components/console/storefront-tax-summary-card.component.js +408 -0
  99. package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -0
  100. package/src/lib/components/console/suppliers-card.component.d.ts +8 -0
  101. package/src/lib/components/console/suppliers-card.component.js +328 -0
  102. package/src/lib/components/console/suppliers-card.component.js.map +1 -0
  103. package/src/lib/components/console/tax-settings-card.component.d.ts +14 -0
  104. package/src/lib/components/console/tax-settings-card.component.js +473 -0
  105. package/src/lib/components/console/tax-settings-card.component.js.map +1 -0
  106. package/src/lib/components/gate.d.ts +39 -0
  107. package/src/lib/components/gate.js +202 -0
  108. package/src/lib/components/gate.js.map +1 -0
  109. package/src/lib/components/gated-video.d.ts +40 -0
  110. package/src/lib/components/gated-video.js +366 -0
  111. package/src/lib/components/gated-video.js.map +1 -0
  112. package/src/lib/components/member-feed.d.ts +32 -0
  113. package/src/lib/components/member-feed.js +198 -0
  114. package/src/lib/components/member-feed.js.map +1 -0
  115. package/src/lib/components/member-recovery.d.ts +35 -0
  116. package/src/lib/components/member-recovery.js +313 -0
  117. package/src/lib/components/member-recovery.js.map +1 -0
  118. package/src/lib/components/member-signin.d.ts +35 -0
  119. package/src/lib/components/member-signin.js +257 -0
  120. package/src/lib/components/member-signin.js.map +1 -0
  121. package/src/lib/components/member-signup.d.ts +34 -0
  122. package/src/lib/components/member-signup.js +287 -0
  123. package/src/lib/components/member-signup.js.map +1 -0
  124. package/src/lib/components/newsletter-signup.d.ts +39 -0
  125. package/src/lib/components/newsletter-signup.js +229 -0
  126. package/src/lib/components/newsletter-signup.js.map +1 -0
  127. package/src/lib/components/product-detail.d.ts +38 -0
  128. package/src/lib/components/product-detail.js +1022 -0
  129. package/src/lib/components/product-detail.js.map +1 -0
  130. package/src/lib/components/product-grid.d.ts +67 -0
  131. package/src/lib/components/product-grid.js +909 -0
  132. package/src/lib/components/product-grid.js.map +1 -0
  133. package/src/lib/components/product-reviews.d.ts +32 -0
  134. package/src/lib/components/product-reviews.js +349 -0
  135. package/src/lib/components/product-reviews.js.map +1 -0
  136. package/src/lib/components/related-products.d.ts +33 -0
  137. package/src/lib/components/related-products.js +232 -0
  138. package/src/lib/components/related-products.js.map +1 -0
  139. package/src/lib/components/reservation-widget.d.ts +33 -0
  140. package/src/lib/components/reservation-widget.js +418 -0
  141. package/src/lib/components/reservation-widget.js.map +1 -0
  142. package/src/lib/components/storefront-payment-element-fallback.d.ts +18 -0
  143. package/src/lib/components/storefront-payment-element-fallback.js +51 -0
  144. package/src/lib/components/storefront-payment-element-fallback.js.map +1 -0
  145. package/src/lib/components/storefront-payment-element.d.ts +78 -0
  146. package/src/lib/components/storefront-payment-element.js +156 -0
  147. package/src/lib/components/storefront-payment-element.js.map +1 -0
  148. package/src/lib/components/wishlist.d.ts +46 -0
  149. package/src/lib/components/wishlist.js +304 -0
  150. package/src/lib/components/wishlist.js.map +1 -0
  151. package/src/lib/constants/bundle-common.d.ts +18 -0
  152. package/src/lib/constants/bundle-common.js +18 -0
  153. package/src/lib/constants/bundle-common.js.map +1 -0
  154. package/src/lib/constants/product-filters.d.ts +35 -0
  155. package/src/lib/constants/product-filters.js +109 -0
  156. package/src/lib/constants/product-filters.js.map +1 -0
  157. package/src/lib/model/commerce-cart.d.ts +52 -0
  158. package/src/lib/model/commerce-cart.js +102 -0
  159. package/src/lib/model/commerce-cart.js.map +1 -0
  160. package/src/lib/model/commerce-connect-transfer.d.ts +243 -0
  161. package/src/lib/model/commerce-connect-transfer.js +231 -0
  162. package/src/lib/model/commerce-connect-transfer.js.map +1 -0
  163. package/src/lib/model/commerce-discounts.d.ts +164 -0
  164. package/src/lib/model/commerce-discounts.js +231 -0
  165. package/src/lib/model/commerce-discounts.js.map +1 -0
  166. package/src/lib/model/commerce-dispute.d.ts +124 -0
  167. package/src/lib/model/commerce-dispute.js +225 -0
  168. package/src/lib/model/commerce-dispute.js.map +1 -0
  169. package/src/lib/model/commerce-gift-cards.d.ts +107 -0
  170. package/src/lib/model/commerce-gift-cards.js +121 -0
  171. package/src/lib/model/commerce-gift-cards.js.map +1 -0
  172. package/src/lib/model/commerce-io.d.ts +36 -0
  173. package/src/lib/model/commerce-io.js +245 -0
  174. package/src/lib/model/commerce-io.js.map +1 -0
  175. package/src/lib/model/commerce-orders.d.ts +1040 -0
  176. package/src/lib/model/commerce-orders.js +1022 -0
  177. package/src/lib/model/commerce-orders.js.map +1 -0
  178. package/src/lib/model/commerce-promotions.d.ts +188 -0
  179. package/src/lib/model/commerce-promotions.js +199 -0
  180. package/src/lib/model/commerce-promotions.js.map +1 -0
  181. package/src/lib/model/commerce-reservations.d.ts +175 -0
  182. package/src/lib/model/commerce-reservations.js +157 -0
  183. package/src/lib/model/commerce-reservations.js.map +1 -0
  184. package/src/lib/model/commerce-shipping.d.ts +316 -0
  185. package/src/lib/model/commerce-shipping.js +484 -0
  186. package/src/lib/model/commerce-shipping.js.map +1 -0
  187. package/src/lib/model/commerce-stock-holds.d.ts +199 -0
  188. package/src/lib/model/commerce-stock-holds.js +190 -0
  189. package/src/lib/model/commerce-stock-holds.js.map +1 -0
  190. package/src/lib/model/commerce-subscription-status.d.ts +67 -0
  191. package/src/lib/model/commerce-subscription-status.js +76 -0
  192. package/src/lib/model/commerce-subscription-status.js.map +1 -0
  193. package/src/lib/model/commerce-tax-decision.d.ts +222 -0
  194. package/src/lib/model/commerce-tax-decision.js +165 -0
  195. package/src/lib/model/commerce-tax-decision.js.map +1 -0
  196. package/src/lib/model/commerce-tax.d.ts +176 -0
  197. package/src/lib/model/commerce-tax.js +92 -0
  198. package/src/lib/model/commerce-tax.js.map +1 -0
  199. package/src/lib/model/commerce.d.ts +551 -0
  200. package/src/lib/model/commerce.js +739 -0
  201. package/src/lib/model/commerce.js.map +1 -0
  202. package/src/lib/model/index.d.ts +40 -0
  203. package/src/lib/model/index.js +40 -0
  204. package/src/lib/model/index.js.map +1 -0
  205. package/src/lib/model/order-figures.d.ts +48 -0
  206. package/src/lib/model/order-figures.js +74 -0
  207. package/src/lib/model/order-figures.js.map +1 -0
  208. package/src/lib/model/plugin-permissions.d.ts +23 -0
  209. package/src/lib/model/plugin-permissions.js +34 -0
  210. package/src/lib/model/plugin-permissions.js.map +1 -0
  211. package/src/lib/model/purchase-analytics.d.ts +186 -0
  212. package/src/lib/model/purchase-analytics.js +216 -0
  213. package/src/lib/model/purchase-analytics.js.map +1 -0
  214. package/src/lib/plugin-config.d.ts +62 -0
  215. package/src/lib/plugin-config.js +78 -0
  216. package/src/lib/plugin-config.js.map +1 -0
  217. package/src/lib/plugin.d.ts +28 -0
  218. package/src/lib/plugin.js +147 -0
  219. package/src/lib/plugin.js.map +1 -0
  220. package/src/lib/server/billing-webhook.d.ts +26 -0
  221. package/src/lib/server/billing-webhook.js +4011 -0
  222. package/src/lib/server/billing-webhook.js.map +1 -0
  223. package/src/lib/server/cancel-order.d.ts +87 -0
  224. package/src/lib/server/cancel-order.js +368 -0
  225. package/src/lib/server/cancel-order.js.map +1 -0
  226. package/src/lib/server/cart-checkout.d.ts +25 -0
  227. package/src/lib/server/cart-checkout.js +976 -0
  228. package/src/lib/server/cart-checkout.js.map +1 -0
  229. package/src/lib/server/cart-cookie.d.ts +32 -0
  230. package/src/lib/server/cart-cookie.js +77 -0
  231. package/src/lib/server/cart-cookie.js.map +1 -0
  232. package/src/lib/server/cart.d.ts +34 -0
  233. package/src/lib/server/cart.js +181 -0
  234. package/src/lib/server/cart.js.map +1 -0
  235. package/src/lib/server/catalog.d.ts +127 -0
  236. package/src/lib/server/catalog.js +277 -0
  237. package/src/lib/server/catalog.js.map +1 -0
  238. package/src/lib/server/checkout.d.ts +36 -0
  239. package/src/lib/server/checkout.js +915 -0
  240. package/src/lib/server/checkout.js.map +1 -0
  241. package/src/lib/server/connect.d.ts +26 -0
  242. package/src/lib/server/connect.js +238 -0
  243. package/src/lib/server/connect.js.map +1 -0
  244. package/src/lib/server/contact-refund.d.ts +117 -0
  245. package/src/lib/server/contact-refund.js +196 -0
  246. package/src/lib/server/contact-refund.js.map +1 -0
  247. package/src/lib/server/download.d.ts +39 -0
  248. package/src/lib/server/download.js +206 -0
  249. package/src/lib/server/download.js.map +1 -0
  250. package/src/lib/server/draft-order.d.ts +26 -0
  251. package/src/lib/server/draft-order.js +706 -0
  252. package/src/lib/server/draft-order.js.map +1 -0
  253. package/src/lib/server/feed.d.ts +24 -0
  254. package/src/lib/server/feed.js +59 -0
  255. package/src/lib/server/feed.js.map +1 -0
  256. package/src/lib/server/fulfill-order.d.ts +97 -0
  257. package/src/lib/server/fulfill-order.js +235 -0
  258. package/src/lib/server/fulfill-order.js.map +1 -0
  259. package/src/lib/server/gate.d.ts +36 -0
  260. package/src/lib/server/gate.js +112 -0
  261. package/src/lib/server/gate.js.map +1 -0
  262. package/src/lib/server/gift-cards.d.ts +42 -0
  263. package/src/lib/server/gift-cards.js +219 -0
  264. package/src/lib/server/gift-cards.js.map +1 -0
  265. package/src/lib/server/low-stock.d.ts +49 -0
  266. package/src/lib/server/low-stock.js +60 -0
  267. package/src/lib/server/low-stock.js.map +1 -0
  268. package/src/lib/server/manual-tax-rate.d.ts +82 -0
  269. package/src/lib/server/manual-tax-rate.js +113 -0
  270. package/src/lib/server/manual-tax-rate.js.map +1 -0
  271. package/src/lib/server/member-feed.d.ts +24 -0
  272. package/src/lib/server/member-feed.js +72 -0
  273. package/src/lib/server/member-feed.js.map +1 -0
  274. package/src/lib/server/member-name-search.d.ts +37 -0
  275. package/src/lib/server/member-name-search.js +42 -0
  276. package/src/lib/server/member-name-search.js.map +1 -0
  277. package/src/lib/server/member-post.d.ts +23 -0
  278. package/src/lib/server/member-post.js +166 -0
  279. package/src/lib/server/member-post.js.map +1 -0
  280. package/src/lib/server/membership-account.d.ts +37 -0
  281. package/src/lib/server/membership-account.js +148 -0
  282. package/src/lib/server/membership-account.js.map +1 -0
  283. package/src/lib/server/membership-admin-password.d.ts +33 -0
  284. package/src/lib/server/membership-admin-password.js +202 -0
  285. package/src/lib/server/membership-admin-password.js.map +1 -0
  286. package/src/lib/server/membership-content.d.ts +23 -0
  287. package/src/lib/server/membership-content.js +105 -0
  288. package/src/lib/server/membership-content.js.map +1 -0
  289. package/src/lib/server/membership-login.d.ts +19 -0
  290. package/src/lib/server/membership-login.js +137 -0
  291. package/src/lib/server/membership-login.js.map +1 -0
  292. package/src/lib/server/membership-logout.d.ts +19 -0
  293. package/src/lib/server/membership-logout.js +35 -0
  294. package/src/lib/server/membership-logout.js.map +1 -0
  295. package/src/lib/server/membership-recover.d.ts +60 -0
  296. package/src/lib/server/membership-recover.js +244 -0
  297. package/src/lib/server/membership-recover.js.map +1 -0
  298. package/src/lib/server/membership-register.d.ts +22 -0
  299. package/src/lib/server/membership-register.js +241 -0
  300. package/src/lib/server/membership-register.js.map +1 -0
  301. package/src/lib/server/membership-reset.d.ts +26 -0
  302. package/src/lib/server/membership-reset.js +88 -0
  303. package/src/lib/server/membership-reset.js.map +1 -0
  304. package/src/lib/server/membership-wishlist.d.ts +23 -0
  305. package/src/lib/server/membership-wishlist.js +89 -0
  306. package/src/lib/server/membership-wishlist.js.map +1 -0
  307. package/src/lib/server/membership.d.ts +96 -0
  308. package/src/lib/server/membership.js +222 -0
  309. package/src/lib/server/membership.js.map +1 -0
  310. package/src/lib/server/native-checkout.d.ts +170 -0
  311. package/src/lib/server/native-checkout.js +193 -0
  312. package/src/lib/server/native-checkout.js.map +1 -0
  313. package/src/lib/server/newsletter.d.ts +23 -0
  314. package/src/lib/server/newsletter.js +323 -0
  315. package/src/lib/server/newsletter.js.map +1 -0
  316. package/src/lib/server/notify-restock.d.ts +22 -0
  317. package/src/lib/server/notify-restock.js +84 -0
  318. package/src/lib/server/notify-restock.js.map +1 -0
  319. package/src/lib/server/order-analytics.d.ts +134 -0
  320. package/src/lib/server/order-analytics.js +263 -0
  321. package/src/lib/server/order-analytics.js.map +1 -0
  322. package/src/lib/server/order-figures.d.ts +33 -0
  323. package/src/lib/server/order-figures.js +220 -0
  324. package/src/lib/server/order-figures.js.map +1 -0
  325. package/src/lib/server/paid-media-delivery.d.ts +173 -0
  326. package/src/lib/server/paid-media-delivery.js +212 -0
  327. package/src/lib/server/paid-media-delivery.js.map +1 -0
  328. package/src/lib/server/pos-order.d.ts +54 -0
  329. package/src/lib/server/pos-order.js +1137 -0
  330. package/src/lib/server/pos-order.js.map +1 -0
  331. package/src/lib/server/process-abandoned.d.ts +54 -0
  332. package/src/lib/server/process-abandoned.js +222 -0
  333. package/src/lib/server/process-abandoned.js.map +1 -0
  334. package/src/lib/server/process-restock.d.ts +42 -0
  335. package/src/lib/server/process-restock.js +219 -0
  336. package/src/lib/server/process-restock.js.map +1 -0
  337. package/src/lib/server/product-card.d.ts +29 -0
  338. package/src/lib/server/product-card.js +54 -0
  339. package/src/lib/server/product-card.js.map +1 -0
  340. package/src/lib/server/product.d.ts +70 -0
  341. package/src/lib/server/product.js +105 -0
  342. package/src/lib/server/product.js.map +1 -0
  343. package/src/lib/server/promotion-hold.d.ts +144 -0
  344. package/src/lib/server/promotion-hold.js +174 -0
  345. package/src/lib/server/promotion-hold.js.map +1 -0
  346. package/src/lib/server/reconcile-stock.d.ts +106 -0
  347. package/src/lib/server/reconcile-stock.js +349 -0
  348. package/src/lib/server/reconcile-stock.js.map +1 -0
  349. package/src/lib/server/refund.d.ts +45 -0
  350. package/src/lib/server/refund.js +607 -0
  351. package/src/lib/server/refund.js.map +1 -0
  352. package/src/lib/server/related.d.ts +24 -0
  353. package/src/lib/server/related.js +104 -0
  354. package/src/lib/server/related.js.map +1 -0
  355. package/src/lib/server/reservation-availability.d.ts +23 -0
  356. package/src/lib/server/reservation-availability.js +111 -0
  357. package/src/lib/server/reservation-availability.js.map +1 -0
  358. package/src/lib/server/reserve-stock.d.ts +156 -0
  359. package/src/lib/server/reserve-stock.js +146 -0
  360. package/src/lib/server/reserve-stock.js.map +1 -0
  361. package/src/lib/server/reserve.d.ts +25 -0
  362. package/src/lib/server/reserve.js +426 -0
  363. package/src/lib/server/reserve.js.map +1 -0
  364. package/src/lib/server/restock-flag.d.ts +202 -0
  365. package/src/lib/server/restock-flag.js +449 -0
  366. package/src/lib/server/restock-flag.js.map +1 -0
  367. package/src/lib/server/reviews.d.ts +51 -0
  368. package/src/lib/server/reviews.js +132 -0
  369. package/src/lib/server/reviews.js.map +1 -0
  370. package/src/lib/server/site-page-enricher.d.ts +42 -0
  371. package/src/lib/server/site-page-enricher.js +149 -0
  372. package/src/lib/server/site-page-enricher.js.map +1 -0
  373. package/src/lib/server/site-page-resolver.d.ts +26 -0
  374. package/src/lib/server/site-page-resolver.js +223 -0
  375. package/src/lib/server/site-page-resolver.js.map +1 -0
  376. package/src/lib/server/stock-hold.d.ts +127 -0
  377. package/src/lib/server/stock-hold.js +258 -0
  378. package/src/lib/server/stock-hold.js.map +1 -0
  379. package/src/lib/server/storefront-tax-record.d.ts +22 -0
  380. package/src/lib/server/storefront-tax-record.js +218 -0
  381. package/src/lib/server/storefront-tax-record.js.map +1 -0
  382. package/src/lib/server/storefront-tax.d.ts +197 -0
  383. package/src/lib/server/storefront-tax.js +245 -0
  384. package/src/lib/server/storefront-tax.js.map +1 -0
  385. package/src/lib/server/stream.d.ts +25 -0
  386. package/src/lib/server/stream.js +191 -0
  387. package/src/lib/server/stream.js.map +1 -0
  388. package/src/lib/server/subscription-portal.d.ts +24 -0
  389. package/src/lib/server/subscription-portal.js +91 -0
  390. package/src/lib/server/subscription-portal.js.map +1 -0
  391. package/src/lib/server/supplier-outbox.d.ts +190 -0
  392. package/src/lib/server/supplier-outbox.js +342 -0
  393. package/src/lib/server/supplier-outbox.js.map +1 -0
  394. package/src/lib/server/supplier-update.d.ts +23 -0
  395. package/src/lib/server/supplier-update.js +289 -0
  396. package/src/lib/server/supplier-update.js.map +1 -0
  397. package/src/lib/server/tax-profile.d.ts +29 -0
  398. package/src/lib/server/tax-profile.js +40 -0
  399. package/src/lib/server/tax-profile.js.map +1 -0
  400. package/src/lib/server.d.ts +32 -0
  401. package/src/lib/server.js +311 -0
  402. package/src/lib/server.js.map +1 -0
  403. package/src/lib/site.d.ts +27 -0
  404. package/src/lib/site.js +134 -0
  405. package/src/lib/site.js.map +1 -0
  406. package/src/lib/utils/escape-html.d.ts +41 -0
  407. package/src/lib/utils/escape-html.js +41 -0
  408. package/src/lib/utils/escape-html.js.map +1 -0
  409. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  410. package/src/lib/utils/generate-preset-id.js +25 -0
  411. package/src/lib/utils/generate-preset-id.js.map +1 -0
  412. package/src/lib/utils/member-continue.d.ts +45 -0
  413. package/src/lib/utils/member-continue.js +49 -0
  414. package/src/lib/utils/member-continue.js.map +1 -0
  415. package/src/lib/utils/use-storefront-purchase-event.d.ts +60 -0
  416. package/src/lib/utils/use-storefront-purchase-event.js +142 -0
  417. package/src/lib/utils/use-storefront-purchase-event.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/refund.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 * as CommerceModel from '../model'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport { createHash } from 'crypto'\nimport { recordContactRefund } from './contact-refund'\nimport { flagOrderRestock } from './restock-flag'\n// Leaf import, not the barrel, for the reason `contact-refund.ts` states about\n// `updateExisting`: the specs in this library mock `@aglyn/tenant-data-admin`\n// wholesale, and a permissive stub would turn a reversal that never happened\n// green.\nimport { reverseEmailAttributedRevenue } from '@aglyn/tenant-data-admin/server/email-revenue-attribution'\n\n/**\n * A claim on one refund attempt (AGL-1696), the same primitive the POS sale\n * path uses (AGL-1691, `cab8aa36e`).\n */\ninterface RefundClaim {\n /** Stripe idempotency key for this attempt, or null when no key was sent. */\n stripeKey: string | null\n record: (status: number, body: unknown) => Promise<void>\n release: () => Promise<void>\n}\n\n/**\n * Retires the licence keys a refund withdrew (AGL-2454).\n *\n * Reads the order back so the decision is made from what actually SETTLED\n * rather than from what this request asked for — a concurrent partial may have\n * closed a line between the two — and retires only the keys of products the\n * order no longer entitles at all. `refundedProductIds` is deliberately strict\n * about that: a buyer who returned one of two copies still holds the product,\n * so nothing is retired for them.\n *\n * `revokedAtMs` with `assignedAtMs` left standing is the retired state. Keeping\n * `assignedAtMs` matters: `assignLicenseKeys` claims from\n * `where('assignedAtMs','==',null)`, so clearing it would put the key straight\n * back in front of the next buyer — the reissue this must not do.\n *\n * Swallows everything. The refund has already moved money.\n */\nasync function retireLicenseKeys(\n hostRef: FirebaseFirestore.DocumentReference,\n orderRef: FirebaseFirestore.DocumentReference,\n orderId: string,\n): Promise<number> {\n try {\n const fresh = CommerceModel.liftLegacyOrder(\n ((await orderRef.get()).data() ?? {}) as any,\n )\n const withdrawn = new Set(CommerceModel.refundedProductIds(fresh))\n if (withdrawn.size === 0) return 0\n const assigned = await hostRef\n .collection('licenseKeys')\n .where('orderId', '==', orderId)\n .limit(500)\n .get()\n let retired = 0\n for (const keySnapshot of assigned.docs) {\n if (!withdrawn.has(String(keySnapshot.get('productId') ?? ''))) continue\n // Already retired by an earlier partial, or by the merchant's own Revoke\n // button. Skipped so a second refund does not restamp the timestamp and\n // make the retirement look newer than it is.\n if (keySnapshot.get('revokedAtMs') != null) continue\n await keySnapshot.ref\n .set({ revokedAtMs: Date.now(), revokedOrderId: orderId }, { merge: true })\n .catch(() => undefined)\n retired++\n }\n if (retired > 0) {\n await orderRef\n .update({\n timeline: firebaseAdmin.firestore.FieldValue.arrayUnion({\n atMs: Date.now(),\n event: 'license-retired',\n detail:\n `${retired} license key${retired === 1 ? '' : 's'} retired. ` +\n 'The buyer already holds the key string, so it is not returned ' +\n 'to the pool — reissuing it would give two people one secret.',\n }),\n })\n .catch(() => undefined)\n }\n return retired\n } catch (error) {\n console.error('Licence key retirement failed', orderId, error)\n return 0\n }\n}\n\n/**\n * Order refunds (AGL-287): full or partial via Stripe, site-admin only\n * (it moves money). Destination charges reverse the transfer and the\n * platform fee proportionally. Full refunds transition the order to\n * `refunded`; partial refunds accumulate `refundedCents` and stay in\n * the current status.\n *\n * Two SEPARATE controls guard the money (AGL-1696), and conflating them is\n * how the original went wrong:\n *\n * - The idempotency key stops a DUPLICATE refund — one attempt sent twice\n * because the response was lost, the admin double-clicked, or a client\n * retried. It is minted per attempt by the console and deliberately not\n * derived from the order or the amount: two $10 refunds on a $50 order are\n * two real refunds, exactly as a cashier ringing the same coffee twice is a\n * real second sale.\n * - The cap stops an OVER-refund — several partials summing past what was\n * captured, including two admins refunding at once, where the two attempts\n * are genuinely distinct and no key can help. That needs the counter read\n * and written inside one transaction.\n *\n * The original had a cap that looked like both and was neither: it read\n * `refundedCents` and wrote it back only AFTER the Stripe call, outside any\n * transaction. A guard that reads state the guarded operation writes too late\n * is not a guard — measured, two concurrent refunds each sent a full $50 to\n * Stripe with no idempotency header on either.\n */\nexport const refundHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n if (!process.env.STRIPE_SECRET_KEY) {\n return res.status(501).json({ error: 'Payments are not configured.' })\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 const amountCents = body.amountCents == null ? null : Number(body.amountCents)\n /**\n * Lines the admin is refunding BY NAME (AGL-2454), and therefore the lines\n * whose digital entitlements come back. Optional: an amount-only refund is\n * still a legal refund and still revokes nothing per-line — see the guard\n * below for why that is a decision rather than an oversight.\n */\n const requestedLineIds: number[] = Array.isArray(body.lineItemIds)\n ? [\n ...new Set(\n (body.lineItemIds as unknown[])\n .map((value) => Math.round(Number(value)))\n .filter((value) => Number.isFinite(value) && value >= 0),\n ),\n ].sort((a, b) => a - b)\n : []\n // One refund attempt, minted by the console. Node lowercases incoming\n // headers, but read both spellings — the plugin API request type makes no\n // promise about casing.\n const idempotencyKey = String(\n req.headers['idempotency-key'] ?? req.headers['Idempotency-Key'] ?? '',\n )\n .trim()\n .slice(0, 200)\n if (!hostId || !orderId) {\n return res.status(400).json({ error: 'Missing hostId or orderId' })\n }\n\n let claim: RefundClaim | null = null\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 // TWO CHECKS, and they answer different questions (AGL-2372).\n //\n // The first is the HOST-level fact the Firestore rules also enforce, and\n // it stays: `memberRoles` is the projection the rules read, so dropping it\n // here would let this route and the database disagree.\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin') {\n return res.status(403).json({ error: 'Refunds require a site admin' })\n }\n // The second is WHOSE admin, and it is the one this gate was missing.\n //\n // `memberRoles` is a per-host projection of `hostAccess`, and\n // `/api/hosts/members` will grant a SITE COLLABORATOR `admin` on one site\n // (`hostAccess: { [hostId]: 'admin' }`, `allHosts: false`). That writes the\n // literal string `'admin'` into `memberRoles[uid]` — byte-identical to an\n // org owner's. So the check above cannot tell a contractor invited to run\n // one microsite from the person who owns the business, and refunding is\n // money leaving that business.\n //\n // `orgWide` is the discriminator, and it needs no new role: it is\n // `isOrgWideMember` (AGL-1026) — owner/admin of the org, an explicit\n // `allHosts` member, or the legacy pre-`allHosts` shape — and it is false\n // for every scoped collaborator. Same pairing `pos-order.ts` uses.\n //\n // BOTH halves are required. `hostRole` is re-tested rather than assumed\n // from `memberRole`: an org-wide member can still be scoped down to\n // `editor` on this host, and `resolveOrgPermissions` is the resolver that\n // knows it. It fails CLOSED on a lookup error when a host is named\n // (AGL-506), and `denied()` returns `orgWide: false` / `hostRole: null`,\n // so an absent membership refuses rather than folding to permitted.\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.orgWide || membership.hostRole !== 'admin') {\n return res\n .status(403)\n .json({ error: 'Refunds require an admin of the whole workspace' })\n }\n const orderRef = hostRef.collection('orders').doc(orderId)\n const orderSnapshot = await orderRef.get()\n if (!orderSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown order' })\n }\n const order = CommerceModel.liftLegacyOrder(orderSnapshot.data() as any)\n\n // Replay a settled attempt before anything else can reject it. This read\n // is only a short-circuit, never the dedupe primitive — the atomic\n // `create()` below is. It has to run ahead of the status guard because a\n // retried FULL refund would otherwise be answered \"orders in refunded\n // cannot refund\", which is the right money outcome reported as a failure,\n // and an admin who reads it as a failure refunds again by hand.\n const claimRef = idempotencyKey\n ? firestore\n .collection('apiIdempotency')\n .doc(\n createHash('sha256')\n // Scoped by the order, so a client that reused one key across\n // two orders cannot dedupe two legitimately distinct refunds.\n // NOT by the amount: that would swallow a real second partial.\n .update(`refund:${hostId}:${orderId}:${idempotencyKey}`)\n .digest('hex'),\n )\n : null\n if (claimRef) {\n const prior = await claimRef.get()\n const priorResponse = prior.get('response')\n if (priorResponse) {\n return res\n .status(Number(prior.get('responseStatus') ?? 200))\n .json(priorResponse)\n }\n }\n\n if (!CommerceModel.canTransitionOrder(order.status, 'refunded')) {\n return res\n .status(409)\n .json({ error: `Orders in \"${order.status}\" cannot refund` })\n }\n // A refund does not withdraw a dispute (AGL-1809). While a chargeback is\n // formally open the bank has already pulled the disputed funds, Stripe's\n // refund API refuses the charge (`charge_disputed`), and a refund that did\n // go through would pay the shopper twice — the merchant loses the refund\n // AND the dispute plus its fee. Refused HERE, before the claim and the\n // reservation, so a refusal burns no idempotency key and strands nothing:\n // no state has been written yet (AGL-1754's contract). An open INQUIRY\n // (`warning_*`) deliberately passes — no funds have moved and Stripe names\n // a full refund as the way to resolve one before it escalates — and the\n // status guard above already turns away a LOST dispute, which parked the\n // order in `refunded`. This reads the pre-transaction snapshot; a dispute\n // webhook racing this exact request is caught by the `charge_disputed`\n // mapping on the Stripe response below.\n if (CommerceModel.orderDisputeBlocksRefund(order)) {\n return res.status(409).json({\n error:\n 'A chargeback is open on this order, so it was not refunded. ' +\n 'Refunding would not withdraw the dispute — the bank has already ' +\n 'taken the disputed amount, and a refund on top of it would pay ' +\n 'the shopper twice. Respond to the dispute or accept it in the ' +\n 'Stripe dashboard; refund any remainder once it settles.',\n })\n }\n // WHAT THE NAMED LINES ARE WORTH, AND WHY THE AMOUNT MAY NOT BE LESS\n // (AGL-2454).\n //\n // A refund carries an amount, not lines — that is the blocker this issue\n // names, and `restock-flag.ts:48-55` already records it for stock. It\n // cannot be solved by inference: deciding for the merchant which lines a\n // bare figure covers would be a guess about their goods. It CAN be solved\n // by asking, which is what naming lines does, and the amount is then\n // derived from them rather than typed beside them.\n //\n // An explicit `amountCents` may still be LARGER (the admin is refunding the\n // line plus its share of tax or shipping, which this items-only sum does\n // not include). It may not be SMALLER: revoking a line the refund did not\n // actually cover is the silent over-revocation this issue forbids in the\n // same breath as silent under-revocation.\n const orderLines = order.lineItems ?? []\n const invalidLine = requestedLineIds.find(\n (index) => index >= orderLines.length,\n )\n if (invalidLine != null) {\n return res\n .status(400)\n .json({ error: `Line ${invalidLine} is not on this order` })\n }\n // NET OF THE ORDER'S DISCOUNT, not the list price.\n //\n // This was the bare `unitAmountCents x quantity`, which is what the line\n // was LISTED at rather than what the buyer paid for it. On a discounted\n // order the two differ, and both directions of the error land on the\n // merchant: a $10 coupon over two $50 lines means each line cost $45, so\n // refunding one at $50 gave back $5 that was never taken, and the order\n // then held less than its remaining line was worth — so the second line\n // refund hit the cap below and was refused outright, leaving the merchant\n // unable to finish a refund they had already half-issued.\n //\n // `orderLineRefundCents` apportions the discount across every line by list\n // value and returns the named lines' share, so refunding all of them sums\n // to exactly what was charged and no cent is stranded or invented.\n const namedLinesCents = CommerceModel.orderLineRefundCents(\n order,\n requestedLineIds,\n )\n if (\n requestedLineIds.length > 0 &&\n amountCents != null &&\n Math.round(amountCents) < namedLinesCents\n ) {\n return res.status(400).json({\n error:\n 'That amount is less than the lines you selected are worth. ' +\n 'Refund the full value of those lines, or refund an amount ' +\n 'without selecting lines.',\n })\n }\n const paymentIntentId =\n order.paymentIntentId ??\n // Legacy rows stored the checkout session as the doc id; resolve\n // the payment intent from Stripe.\n (await (async () => {\n const response = await fetch(\n `https://api.stripe.com/v1/checkout/sessions/${orderId}`,\n {\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n },\n },\n )\n const session = await response.json()\n return response.ok ? session?.payment_intent : null\n })())\n if (!paymentIntentId) {\n return res.status(409).json({ error: 'No payment to refund' })\n }\n\n // Point of no return: everything past here moves money. The claim is\n // `create()` — Firestore rejects a create on an existing document, and\n // that rejection IS the dedupe primitive. A read-then-write would race\n // exactly the double-submit it exists to stop. Storage reuses the REST\n // API's shape and its `orgId` field (AGL-618) rather than inventing a\n // second replay store, so `eraseOrgIdempotencyKeys` (AGL-1448) already\n // sweeps these on org erasure with no change there.\n if (claimRef) {\n const ownerOrg = await getOrgForHost(hostId)\n try {\n await claimRef.create({\n orgId: String(ownerOrg?.org?.id ?? '') || null,\n hostId,\n orderId,\n kind: 'commerce-refund',\n status: 'pending',\n createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n createdAtMs: Date.now(),\n // The second writer into `apiIdempotency` (AGL-1978). The shared\n // `claimAttempt` stamps this too; this local copy has to as well,\n // or refund claims are the one shape the TTL policy silently never\n // reaches — a policy that governs most of a collection reads, from\n // the outside, exactly like one that governs all of it.\n expiresAt: Aglyn.apiIdempotencyExpiry(),\n })\n } catch {\n const prior = await claimRef.get()\n const priorResponse = prior.get('response')\n if (priorResponse) {\n return res\n .status(Number(prior.get('responseStatus') ?? 200))\n .json(priorResponse)\n }\n // In flight, or stranded by a process that died mid-refund. Fail\n // CLOSED: the alternative is sending the money a second time.\n return res\n .status(409)\n .json({ error: 'This refund is already being processed' })\n }\n claim = {\n // The same digest goes to Stripe. That is the half that costs real\n // money: it covers the window where our claim is written but the\n // response never arrives, and makes Stripe replay its own refund\n // instead of moving the funds again.\n stripeKey: claimRef.id,\n record: async (status, payload) => {\n await claimRef\n .set(\n {\n status: 'done',\n responseStatus: status,\n response: payload,\n settledAtMs: Date.now(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n },\n release: async () => {\n await claimRef.delete().catch(() => undefined)\n },\n }\n }\n\n // RESERVE. The cap is read and written in one transaction, so two\n // concurrent refunds cannot both see the same `refundedCents` — which is\n // the whole failure the old ordering had, since it wrote the counter only\n // after Stripe had already been asked to move the money. Reserving BEFORE\n // the call rather than after also fails in the safe direction: a lost\n // response leaves the amount counted, so the retry refunds less, never\n // more.\n let refundCents = 0\n let totalCents = 0\n await firestore.runTransaction(async (transaction) => {\n const fresh = CommerceModel.liftLegacyOrder(\n ((await transaction.get(orderRef)).data() ?? {}) as any,\n )\n totalCents = fresh.totals?.totalCents ?? Number(fresh.amountCents ?? 0)\n const alreadyRefunded = Number(fresh.refundedCents ?? 0)\n const remaining = totalCents - alreadyRefunded\n // Named lines with no amount refund exactly what those lines are worth;\n // named lines WITH an amount use the amount (already guarded above as\n // no smaller than the lines). Neither is the whole order, which is what\n // `amountCents == null` alone still means.\n const asked =\n amountCents != null\n ? Math.round(amountCents)\n : requestedLineIds.length > 0\n ? namedLinesCents\n : remaining\n refundCents = Math.min(asked, remaining)\n if (!(refundCents > 0)) {\n refundCents = 0\n return\n }\n transaction.set(\n orderRef,\n { refundedCents: alreadyRefunded + refundCents },\n { merge: true },\n )\n })\n if (!(refundCents > 0)) {\n // Nothing moved, so the attempt key is released rather than burned.\n await claim?.release()\n return res.status(400).json({ error: 'Nothing left to refund' })\n }\n // The cap bit into the named lines (AGL-2454): earlier partials have left\n // less on this order than the selected lines are worth. REFUSED rather\n // than refunded-and-revoked, because revoking a line for less than its\n // value is precisely the silent over-revocation this issue forbids. The\n // reservation is given back — the same compensation a Stripe refusal does\n // below, and for the same reason: nothing has left the account yet.\n if (requestedLineIds.length > 0 && refundCents < namedLinesCents) {\n await firestore\n .runTransaction(async (transaction) => {\n const current = Number(\n (await transaction.get(orderRef)).get('refundedCents') ?? 0,\n )\n transaction.set(\n orderRef,\n { refundedCents: Math.max(0, current - refundCents) },\n { merge: true },\n )\n })\n .catch(() => undefined)\n await claim?.release()\n return res.status(409).json({\n error:\n `Only $${(refundCents / 100).toFixed(2)} is left to refund on this ` +\n `order, and the lines you selected are worth $${(\n namedLinesCents / 100\n ).toFixed(2)}. Refund an amount without selecting lines instead.`,\n })\n }\n\n const params = new URLSearchParams({\n payment_intent: String(paymentIntentId),\n amount: String(refundCents),\n reverse_transfer: 'true',\n refund_application_fee: 'true',\n })\n const response = await fetch('https://api.stripe.com/v1/refunds', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n ...(claim?.stripeKey\n ? { 'Idempotency-Key': claim.stripeKey }\n : {}),\n },\n body: params.toString(),\n })\n const refund = await response.json()\n if (!response.ok) {\n console.error('Stripe refund error', refund?.error)\n // Stripe said no, so we KNOW no money moved: give the reservation back\n // and let the same attempt be tried again.\n await firestore\n .runTransaction(async (transaction) => {\n const current = Number(\n (await transaction.get(orderRef)).get('refundedCents') ?? 0,\n )\n transaction.set(\n orderRef,\n { refundedCents: Math.max(0, current - refundCents) },\n { merge: true },\n )\n })\n .catch(() => undefined)\n await claim?.release()\n // Stripe refusing BECAUSE OF A DISPUTE is the guard above arriving by\n // the other door — our order document simply didn't know yet (webhook\n // lag, or an order from before disputes were subscribed at all). Same\n // answer, same accuracy: a 409 naming the dispute, not a 502 reading\n // \"The charge you're attempting to refund has been charged back\", which\n // an admin has no reason to connect to the Refund button they pressed.\n const stripeCode = String(refund?.error?.code ?? '')\n if (\n stripeCode === 'charge_disputed' ||\n stripeCode === 'refund_disputed_payment'\n ) {\n return res.status(409).json({\n error:\n 'Stripe refused this refund because the charge is disputed. ' +\n 'Respond to the dispute or accept it in the Stripe dashboard; ' +\n 'refund any remainder once it settles.',\n })\n }\n return res\n .status(502)\n .json({ error: refund?.error?.message ?? 'Refund failed' })\n }\n\n // SETTLE. Re-read inside the transaction: a concurrent partial may have\n // reserved against the same order, and the timeline must be appended to\n // whatever is there now rather than to the snapshot read at the top.\n let refundedCents = 0\n let fullyRefunded = false\n let closedTheOrder = false\n await firestore.runTransaction(async (transaction) => {\n const fresh = CommerceModel.liftLegacyOrder(\n ((await transaction.get(orderRef)).data() ?? {}) as any,\n )\n refundedCents = Number(fresh.refundedCents ?? 0)\n fullyRefunded = refundedCents >= totalCents\n // Whether THIS settle moved the order into `refunded`, which is not the\n // same question as whether the order is now fully refunded (AGL-1754).\n // Two partials that between them close an order can both reserve before\n // either settles, so both re-read the completed total and both compute\n // `fullyRefunded`. Writing `status: 'refunded'` twice is harmless;\n // incrementing a count twice is not. Reading the status inside the same\n // transaction that writes it makes the flip observable exactly once.\n closedTheOrder = fullyRefunded && fresh.status !== 'refunded'\n transaction.set(\n orderRef,\n {\n ...(fullyRefunded ? { status: 'refunded' } : {}),\n // The entitlement withdrawal, recorded WITH the money (AGL-2454).\n // `arrayUnion` rather than a written-back array: two admins refunding\n // different lines at once must not erase each other's, and this\n // transaction re-reads the order but a written array would still lose\n // a concurrent settle that committed between the two.\n ...(requestedLineIds.length > 0\n ? {\n refundedLineItemIds:\n firebaseAdmin.firestore.FieldValue.arrayUnion(\n ...requestedLineIds,\n ),\n }\n : {}),\n timeline: CommerceModel.appendOrderEvent(\n fresh,\n 'refund',\n `$${(refundCents / 100).toFixed(2)} refunded` +\n (fullyRefunded\n ? ' (full)'\n : requestedLineIds.length > 0\n ? ` — ${requestedLineIds.length} line${\n requestedLineIds.length === 1 ? '' : 's'\n } withdrawn`\n : ' — refunded by amount, no lines withdrawn'),\n ),\n },\n { merge: true },\n )\n })\n const payload = { refundedCents, fullyRefunded }\n await claim?.record(200, payload)\n // LICENCE KEYS ARE RETIRED, NEVER RETURNED TO THE POOL (AGL-2454).\n //\n // `assignLicenseKeys` stamps `assignedAtMs`, `orderId` and `email` onto a\n // pool document and nothing anywhere ever set them back — so a refunded\n // order consumed the merchant's key forever, and a merchant who sold one\n // key out of a hundred and refunded it had ninety-nine, permanently.\n //\n // Returning it to the pool is NOT the fix, and this is the decision the\n // issue asked for: the key string was mailed in the receipt and cannot be\n // invalidated by anything we own, so re-issuing it to the next paying\n // customer would hand two people one working secret. That is worse than\n // losing the key. A third state — retired: neither assigned to a live order\n // nor available — is the honest record, and `revokedAtMs` already IS that\n // state: the console's key dialog has written exactly this pair since it\n // shipped, so this reuses the merchant's own vocabulary rather than\n // inventing a second one.\n //\n // Best-effort and after the response is recorded, matching the contact and\n // restock ledgers below: the money has moved and nothing here may fail a\n // refund that already left the merchant's account.\n await retireLicenseKeys(hostRef, orderRef, orderId)\n // The customer's side of the ledger (AGL-1754). Everything above records\n // the money on the ORDER; without this the buyer's `ltvCents` still counts\n // a sale they returned, and only ever rises.\n //\n // Placed AFTER the attempt is recorded so a slow contacts write cannot\n // strand the claim: a retry that arrives while this is in flight replays\n // the recorded 200 instead of being turned away with \"already being\n // processed\". Awaited rather than fired off with `void` — the handler is\n // serverless, and work left running past the response is work the\n // container may be frozen before it finishes. `recordContactRefund`\n // swallows its own failures, so awaiting adds no way for this to fail a\n // refund that has already left the merchant's account.\n //\n // Amount is THIS attempt's `refundCents`, already capped against what was\n // left, so several partials sum to at most the order total — the same\n // number the order's own `refundedCents` follows. The retried and racing\n // cases need no key of their own: a keyed retry never reaches here (it\n // replays at the claim), and a keyless one is a genuinely new refund that\n // moved more money and should be counted.\n await recordContactRefund({\n hostId,\n orderId,\n email: order.customerEmail,\n amountCents: refundCents,\n closedTheOrder,\n })\n /*\n * The campaign's side of the same ledger.\n *\n * If a campaign was credited with this order, that credit is now partly\n * or wholly wrong — a campaign cannot go on being paid for a sale the\n * merchant reversed, and revenue attribution that only ever rises is the\n * flattering half of a measurement. Recorded beside the gross rather than\n * subtracted from it, for the reason `recordContactRefund` above records\n * `refundedCents` beside `ltvCents`.\n *\n * Keyed by the ORDER and not by the buyer, so it needs no email and works\n * for a guest checkout: the attribution record holds which campaign and\n * which currency, and this reads them back. Same placement, same\n * swallow-all contract and same awaited call as the two ledgers around\n * it — nothing here may fail a refund that has already left the\n * merchant's account.\n */\n await reverseEmailAttributedRevenue({\n hostId,\n orderId,\n amountCents: refundCents,\n closedTheOrder,\n })\n // The shelf's side of the ledger (AGL-1797). The sale decremented variant\n // inventory and nothing put it back, so a fully refunded order read one\n // unit light forever. This FLAGS rather than releases — a refund with no\n // return leaves the goods gone, and inventing stock the merchant does not\n // have is worse than under-counting it — and the merchant answers from the\n // stock adjustment they already have. Same placement and same swallow-all\n // contract as the contact write above, for the same reason: the money has\n // moved and the order records it, so nothing here may fail the refund.\n await flagOrderRestock({ hostId, orderId, kind: 'refund', closedTheOrder })\n return res.status(200).json(payload)\n } catch (error) {\n console.error(error)\n // Deliberately NOT released, which is where this diverges from the POS\n // sale path (AGL-1691). If the refund call threw we do not know whether\n // Stripe moved the money, and the two failure directions are not\n // symmetric: a stranded key costs a support ticket, a released one costs a\n // second refund. The retry gets a 409 and a human reconciles.\n return res.status(500).json({ error: 'Refund failed' })\n }\n}\n"],"names":["Aglyn","CommerceModel","firebaseAdmin","getOrgForHost","resolveOrgPermissions","createHash","recordContactRefund","flagOrderRestock","reverseEmailAttributedRevenue","retireLicenseKeys","hostRef","orderRef","orderId","fresh","liftLegacyOrder","get","data","withdrawn","Set","refundedProductIds","size","assigned","collection","where","limit","retired","keySnapshot","docs","has","String","ref","set","revokedAtMs","Date","now","revokedOrderId","merge","catch","undefined","update","timeline","firestore","FieldValue","arrayUnion","atMs","event","detail","error","console","refundHandler","req","res","body","method","status","json","process","env","STRIPE_SECRET_KEY","authorization","headers","idToken","startsWith","slice","length","JSON","parse","hostId","amountCents","Number","requestedLineIds","Array","isArray","lineItemIds","map","value","Math","round","filter","isFinite","sort","a","b","idempotencyKey","trim","claim","hostSnapshot","order","decoded","app","auth","verifyIdToken","doc","exists","memberRole","uid","membership","orgWide","hostRole","orderSnapshot","claimRef","digest","prior","priorResponse","canTransitionOrder","orderDisputeBlocksRefund","orderLines","lineItems","invalidLine","find","index","namedLinesCents","orderLineRefundCents","paymentIntentId","response","fetch","Authorization","session","ok","payment_intent","ownerOrg","create","orgId","org","id","kind","createdAt","serverTimestamp","createdAtMs","expiresAt","apiIdempotencyExpiry","stripeKey","record","payload","responseStatus","settledAtMs","release","delete","refundCents","totalCents","runTransaction","transaction","totals","alreadyRefunded","refundedCents","remaining","asked","min","current","max","toFixed","params","URLSearchParams","amount","reverse_transfer","refund_application_fee","toString","refund","stripeCode","code","message","fullyRefunded","closedTheOrder","refundedLineItemIds","appendOrderEvent","email","customerEmail"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAC5C,YAAYC,mBAAmB,oBAAU;AACzC,SAASC,aAAa,EAAEC,aAAa,QAAQ,2BAA0B;AAEvE,SAASC,qBAAqB,QAAQ,wCAAuC;AAC7E,SAASC,UAAU,QAAQ,SAAQ;AACnC,SAASC,mBAAmB,QAAQ,sBAAkB;AACtD,SAASC,gBAAgB,QAAQ,oBAAgB;AACjD,+EAA+E;AAC/E,8EAA8E;AAC9E,6EAA6E;AAC7E,SAAS;AACT,SAASC,6BAA6B,QAAQ,4DAA2D;AAazG;;;;;;;;;;;;;;;;CAgBC,GACD,eAAeC,kBACbC,OAA4C,EAC5CC,QAA6C,EAC7CC,OAAe;IAEf,IAAI;YAEC;QADH,MAAMC,QAAQZ,cAAca,eAAe,EACxC,QAAA,AAAC,CAAA,MAAMH,SAASI,GAAG,EAAC,EAAGC,IAAI,cAA3B,QAAiC,CAAC;QAErC,MAAMC,YAAY,IAAIC,IAAIjB,cAAckB,kBAAkB,CAACN;QAC3D,IAAII,UAAUG,IAAI,KAAK,GAAG,OAAO;QACjC,MAAMC,WAAW,MAAMX,QACpBY,UAAU,CAAC,eACXC,KAAK,CAAC,WAAW,MAAMX,SACvBY,KAAK,CAAC,KACNT,GAAG;QACN,IAAIU,UAAU;QACd,KAAK,MAAMC,eAAeL,SAASM,IAAI,CAAE;gBACbD;YAA1B,IAAI,CAACT,UAAUW,GAAG,CAACC,QAAOH,mBAAAA,YAAYX,GAAG,CAAC,wBAAhBW,mBAAgC,MAAM;YAChE,yEAAyE;YACzE,wEAAwE;YACxE,6CAA6C;YAC7C,IAAIA,YAAYX,GAAG,CAAC,kBAAkB,MAAM;YAC5C,MAAMW,YAAYI,GAAG,CAClBC,GAAG,CAAC;gBAAEC,aAAaC,KAAKC,GAAG;gBAAIC,gBAAgBvB;YAAQ,GAAG;gBAAEwB,OAAO;YAAK,GACxEC,KAAK,CAAC,IAAMC;YACfb;QACF;QACA,IAAIA,UAAU,GAAG;YACf,MAAMd,SACH4B,MAAM,CAAC;gBACNC,UAAUtC,cAAcuC,SAAS,CAACC,UAAU,CAACC,UAAU,CAAC;oBACtDC,MAAMX,KAAKC,GAAG;oBACdW,OAAO;oBACPC,QACE,GAAGrB,QAAQ,YAAY,EAAEA,YAAY,IAAI,KAAK,IAAI,UAAU,CAAC,GAC7D,mEACA;gBACJ;YACF,GACCY,KAAK,CAAC,IAAMC;QACjB;QACA,OAAOb;IACT,EAAE,OAAOsB,OAAO;QACdC,QAAQD,KAAK,CAAC,iCAAiCnC,SAASmC;QACxD,OAAO;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAME,gBAAkC,OAAOC,KAAKC;QAO5BD,4BAM4BA,WACnCE,cACCA,eAqBrBF,MAAAA;IAnCF,IAAIA,IAAIG,MAAM,KAAK,QAAQ;QACzB,OAAOF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAER,OAAO;QAAqB;IAC5D;IACA,IAAI,CAACS,QAAQC,GAAG,CAACC,iBAAiB,EAAE;QAClC,OAAOP,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAER,OAAO;QAA+B;IACtE;IACA,MAAMY,gBAAgB9B,QAAOqB,6BAAAA,IAAIU,OAAO,CAACD,aAAa,YAAzBT,6BAA6B;IAC1D,MAAMW,UAAUF,cAAcG,UAAU,CAAC,aACrCH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IACpC1B;IACJ,IAAI,CAACuB,SAAS,OAAOV,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAER,OAAO;IAAkB;IACrE,MAAMK,OACJ,OAAOF,IAAIE,IAAI,KAAK,WAAWa,KAAKC,KAAK,CAAChB,IAAIE,IAAI,KAAKF,YAAAA,IAAIE,IAAI,YAARF,YAAY,CAAC;IACtE,MAAMiB,SAAStC,QAAOuB,eAAAA,KAAKe,MAAM,YAAXf,eAAe;IACrC,MAAMxC,UAAUiB,QAAOuB,gBAAAA,KAAKxC,OAAO,YAAZwC,gBAAgB;IACvC,MAAMgB,cAAchB,KAAKgB,WAAW,IAAI,OAAO,OAAOC,OAAOjB,KAAKgB,WAAW;IAC7E;;;;;GAKC,GACD,MAAME,mBAA6BC,MAAMC,OAAO,CAACpB,KAAKqB,WAAW,IAC7D;WACK,IAAIvD,IACL,AAACkC,KAAKqB,WAAW,CACdC,GAAG,CAAC,CAACC,QAAUC,KAAKC,KAAK,CAACR,OAAOM,SACjCG,MAAM,CAAC,CAACH,QAAUN,OAAOU,QAAQ,CAACJ,UAAUA,SAAS;KAE3D,CAACK,IAAI,CAAC,CAACC,GAAGC,IAAMD,IAAIC,KACrB,EAAE;IACN,sEAAsE;IACtE,0EAA0E;IAC1E,wBAAwB;IACxB,MAAMC,iBAAiBtD,QACrBqB,QAAAA,8BAAAA,IAAIU,OAAO,CAAC,kBAAkB,YAA9BV,8BAAkCA,IAAIU,OAAO,CAAC,kBAAkB,YAAhEV,OAAoE,IAEnEkC,IAAI,GACJrB,KAAK,CAAC,GAAG;IACZ,IAAI,CAACI,UAAU,CAACvD,SAAS;QACvB,OAAOuC,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAER,OAAO;QAA4B;IACnE;IAEA,IAAIsC,QAA4B;IAChC,IAAI;YAakBC,mBA6GDC,kBAwCjBA;QAjKF,MAAMC,UAAU,MAAMtF,cAAcuF,GAAG,GAAGC,IAAI,GAAGC,aAAa,CAAC9B;QAC/D,MAAMpB,YAAYvC,cAAcuF,GAAG,GAAGhD,SAAS;QAC/C,MAAM/B,UAAU+B,UAAUnB,UAAU,CAAC,SAASsE,GAAG,CAACzB;QAClD,MAAMmB,eAAe,MAAM5E,QAAQK,GAAG;QACtC,IAAI,CAACuE,aAAaO,MAAM,EAAE;YACxB,OAAO1C,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAER,OAAO;YAAe;QACtD;QACA,8DAA8D;QAC9D,EAAE;QACF,yEAAyE;QACzE,2EAA2E;QAC3E,uDAAuD;QACvD,MAAM+C,aAAa,EAACR,oBAAAA,aAAavE,GAAG,CAAC,0BAAjBuE,oBAAmC,CAAC,EAAE,CAACE,QAAQO,GAAG,CAAC;QACvE,IAAID,eAAe,SAAS;YAC1B,OAAO3C,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAER,OAAO;YAA+B;QACtE;QACA,sEAAsE;QACtE,EAAE;QACF,8DAA8D;QAC9D,0EAA0E;QAC1E,4EAA4E;QAC5E,0EAA0E;QAC1E,0EAA0E;QAC1E,wEAAwE;QACxE,+BAA+B;QAC/B,EAAE;QACF,kEAAkE;QAClE,qEAAqE;QACrE,0EAA0E;QAC1E,mEAAmE;QACnE,EAAE;QACF,wEAAwE;QACxE,oEAAoE;QACpE,0EAA0E;QAC1E,mEAAmE;QACnE,yEAAyE;QACzE,oEAAoE;QACpE,MAAMiD,aAAa,MAAM5F,sBAAsBoF,QAAQO,GAAG,EAAE;YAAE5B;QAAO;QACrE,IAAI,CAAC6B,WAAWC,OAAO,IAAID,WAAWE,QAAQ,KAAK,SAAS;YAC1D,OAAO/C,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAER,OAAO;YAAkD;QACrE;QACA,MAAMpC,WAAWD,QAAQY,UAAU,CAAC,UAAUsE,GAAG,CAAChF;QAClD,MAAMuF,gBAAgB,MAAMxF,SAASI,GAAG;QACxC,IAAI,CAACoF,cAAcN,MAAM,EAAE;YACzB,OAAO1C,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAER,OAAO;YAAgB;QACvD;QACA,MAAMwC,QAAQtF,cAAca,eAAe,CAACqF,cAAcnF,IAAI;QAE9D,yEAAyE;QACzE,mEAAmE;QACnE,yEAAyE;QACzE,sEAAsE;QACtE,0EAA0E;QAC1E,gEAAgE;QAChE,MAAMoF,WAAWjB,iBACb1C,UACGnB,UAAU,CAAC,kBACXsE,GAAG,CACFvF,WAAW,SACT,8DAA8D;QAC9D,8DAA8D;QAC9D,+DAA+D;SAC9DkC,MAAM,CAAC,CAAC,OAAO,EAAE4B,OAAO,CAAC,EAAEvD,QAAQ,CAAC,EAAEuE,gBAAgB,EACtDkB,MAAM,CAAC,UAEd;QACJ,IAAID,UAAU;YACZ,MAAME,QAAQ,MAAMF,SAASrF,GAAG;YAChC,MAAMwF,gBAAgBD,MAAMvF,GAAG,CAAC;YAChC,IAAIwF,eAAe;oBAEAD;gBADjB,OAAOnD,IACJG,MAAM,CAACe,QAAOiC,aAAAA,MAAMvF,GAAG,CAAC,6BAAVuF,aAA+B,MAC7C/C,IAAI,CAACgD;YACV;QACF;QAEA,IAAI,CAACtG,cAAcuG,kBAAkB,CAACjB,MAAMjC,MAAM,EAAE,aAAa;YAC/D,OAAOH,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAER,OAAO,CAAC,WAAW,EAAEwC,MAAMjC,MAAM,CAAC,eAAe,CAAC;YAAC;QAC/D;QACA,yEAAyE;QACzE,yEAAyE;QACzE,2EAA2E;QAC3E,yEAAyE;QACzE,uEAAuE;QACvE,0EAA0E;QAC1E,uEAAuE;QACvE,2EAA2E;QAC3E,wEAAwE;QACxE,yEAAyE;QACzE,0EAA0E;QAC1E,uEAAuE;QACvE,wCAAwC;QACxC,IAAIrD,cAAcwG,wBAAwB,CAAClB,QAAQ;YACjD,OAAOpC,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BR,OACE,iEACA,qEACA,oEACA,mEACA;YACJ;QACF;QACA,qEAAqE;QACrE,cAAc;QACd,EAAE;QACF,yEAAyE;QACzE,sEAAsE;QACtE,yEAAyE;QACzE,0EAA0E;QAC1E,qEAAqE;QACrE,mDAAmD;QACnD,EAAE;QACF,4EAA4E;QAC5E,yEAAyE;QACzE,0EAA0E;QAC1E,yEAAyE;QACzE,0CAA0C;QAC1C,MAAM2D,cAAanB,mBAAAA,MAAMoB,SAAS,YAAfpB,mBAAmB,EAAE;QACxC,MAAMqB,cAActC,iBAAiBuC,IAAI,CACvC,CAACC,QAAUA,SAASJ,WAAW1C,MAAM;QAEvC,IAAI4C,eAAe,MAAM;YACvB,OAAOzD,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAER,OAAO,CAAC,KAAK,EAAE6D,YAAY,qBAAqB,CAAC;YAAC;QAC9D;QACA,mDAAmD;QACnD,EAAE;QACF,yEAAyE;QACzE,wEAAwE;QACxE,qEAAqE;QACrE,yEAAyE;QACzE,wEAAwE;QACxE,wEAAwE;QACxE,0EAA0E;QAC1E,0DAA0D;QAC1D,EAAE;QACF,2EAA2E;QAC3E,0EAA0E;QAC1E,mEAAmE;QACnE,MAAMG,kBAAkB9G,cAAc+G,oBAAoB,CACxDzB,OACAjB;QAEF,IACEA,iBAAiBN,MAAM,GAAG,KAC1BI,eAAe,QACfQ,KAAKC,KAAK,CAACT,eAAe2C,iBAC1B;YACA,OAAO5D,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BR,OACE,gEACA,+DACA;YACJ;QACF;QACA,MAAMkE,mBACJ1B,yBAAAA,MAAM0B,eAAe,YAArB1B,yBACA,iEAAiE;QACjE,kCAAkC;QACjC,MAAM,AAAC,CAAA;YACN,MAAM2B,WAAW,MAAMC,MACrB,CAAC,4CAA4C,EAAEvG,SAAS,EACxD;gBACEgD,SAAS;oBACPwD,eAAe,CAAC,OAAO,EAAE5D,QAAQC,GAAG,CAACC,iBAAiB,EAAE;gBAC1D;YACF;YAEF,MAAM2D,UAAU,MAAMH,SAAS3D,IAAI;YACnC,OAAO2D,SAASI,EAAE,GAAGD,2BAAAA,QAASE,cAAc,GAAG;QACjD,CAAA;QACF,IAAI,CAACN,iBAAiB;YACpB,OAAO9D,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAER,OAAO;YAAuB;QAC9D;QAEA,qEAAqE;QACrE,uEAAuE;QACvE,uEAAuE;QACvE,uEAAuE;QACvE,sEAAsE;QACtE,uEAAuE;QACvE,oDAAoD;QACpD,IAAIqD,UAAU;YACZ,MAAMoB,WAAW,MAAMrH,cAAcgE;YACrC,IAAI;;oBAEcqD;gBADhB,MAAMpB,SAASqB,MAAM,CAAC;oBACpBC,OAAO7F,gBAAO2F,6BAAAA,gBAAAA,SAAUG,GAAG,qBAAbH,cAAeI,EAAE,oBAAI,OAAO;oBAC1CzD;oBACAvD;oBACAiH,MAAM;oBACNvE,QAAQ;oBACRwE,WAAW5H,cAAcuC,SAAS,CAACC,UAAU,CAACqF,eAAe;oBAC7DC,aAAa/F,KAAKC,GAAG;oBACrB,iEAAiE;oBACjE,kEAAkE;oBAClE,mEAAmE;oBACnE,mEAAmE;oBACnE,wDAAwD;oBACxD+F,WAAWjI,MAAMkI,oBAAoB;gBACvC;YACF,EAAE,eAAM;gBACN,MAAM5B,QAAQ,MAAMF,SAASrF,GAAG;gBAChC,MAAMwF,gBAAgBD,MAAMvF,GAAG,CAAC;gBAChC,IAAIwF,eAAe;wBAEAD;oBADjB,OAAOnD,IACJG,MAAM,CAACe,QAAOiC,cAAAA,MAAMvF,GAAG,CAAC,6BAAVuF,cAA+B,MAC7C/C,IAAI,CAACgD;gBACV;gBACA,iEAAiE;gBACjE,8DAA8D;gBAC9D,OAAOpD,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;oBAAER,OAAO;gBAAyC;YAC5D;YACAsC,QAAQ;gBACN,mEAAmE;gBACnE,iEAAiE;gBACjE,iEAAiE;gBACjE,qCAAqC;gBACrC8C,WAAW/B,SAASwB,EAAE;gBACtBQ,QAAQ,OAAO9E,QAAQ+E;oBACrB,MAAMjC,SACHrE,GAAG,CACF;wBACEuB,QAAQ;wBACRgF,gBAAgBhF;wBAChB4D,UAAUmB;wBACVE,aAAatG,KAAKC,GAAG;oBACvB,GACA;wBAAEE,OAAO;oBAAK,GAEfC,KAAK,CAAC,IAAMC;gBACjB;gBACAkG,SAAS;oBACP,MAAMpC,SAASqC,MAAM,GAAGpG,KAAK,CAAC,IAAMC;gBACtC;YACF;QACF;QAEA,kEAAkE;QAClE,yEAAyE;QACzE,0EAA0E;QAC1E,0EAA0E;QAC1E,sEAAsE;QACtE,uEAAuE;QACvE,QAAQ;QACR,IAAIoG,cAAc;QAClB,IAAIC,aAAa;QACjB,MAAMlG,UAAUmG,cAAc,CAAC,OAAOC;gBAEjC,aAE6ChI,oBACjBA;gBADlBA;YAHb,MAAMA,QAAQZ,cAAca,eAAe,EACxC,QAAA,AAAC,CAAA,MAAM+H,YAAY9H,GAAG,CAACJ,SAAQ,EAAGK,IAAI,cAAtC,QAA4C,CAAC;YAEhD2H,sBAAa9H,gBAAAA,MAAMiI,MAAM,qBAAZjI,cAAc8H,UAAU,mBAAItE,QAAOxD,qBAAAA,MAAMuD,WAAW,YAAjBvD,qBAAqB;YACrE,MAAMkI,kBAAkB1E,QAAOxD,uBAAAA,MAAMmI,aAAa,YAAnBnI,uBAAuB;YACtD,MAAMoI,YAAYN,aAAaI;YAC/B,wEAAwE;YACxE,sEAAsE;YACtE,wEAAwE;YACxE,2CAA2C;YAC3C,MAAMG,QACJ9E,eAAe,OACXQ,KAAKC,KAAK,CAACT,eACXE,iBAAiBN,MAAM,GAAG,IACxB+C,kBACAkC;YACRP,cAAc9D,KAAKuE,GAAG,CAACD,OAAOD;YAC9B,IAAI,CAAEP,CAAAA,cAAc,CAAA,GAAI;gBACtBA,cAAc;gBACd;YACF;YACAG,YAAY9G,GAAG,CACbpB,UACA;gBAAEqI,eAAeD,kBAAkBL;YAAY,GAC/C;gBAAEtG,OAAO;YAAK;QAElB;QACA,IAAI,CAAEsG,CAAAA,cAAc,CAAA,GAAI;YACtB,oEAAoE;YACpE,OAAMrD,yBAAAA,MAAOmD,OAAO;YACpB,OAAOrF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAER,OAAO;YAAyB;QAChE;QACA,0EAA0E;QAC1E,uEAAuE;QACvE,uEAAuE;QACvE,wEAAwE;QACxE,0EAA0E;QAC1E,oEAAoE;QACpE,IAAIuB,iBAAiBN,MAAM,GAAG,KAAK0E,cAAc3B,iBAAiB;YAChE,MAAMtE,UACHmG,cAAc,CAAC,OAAOC;oBAEnB;gBADF,MAAMO,UAAU/E,QACd,OAAA,AAAC,CAAA,MAAMwE,YAAY9H,GAAG,CAACJ,SAAQ,EAAGI,GAAG,CAAC,4BAAtC,OAA0D;gBAE5D8H,YAAY9G,GAAG,CACbpB,UACA;oBAAEqI,eAAepE,KAAKyE,GAAG,CAAC,GAAGD,UAAUV;gBAAa,GACpD;oBAAEtG,OAAO;gBAAK;YAElB,GACCC,KAAK,CAAC,IAAMC;YACf,OAAM+C,yBAAAA,MAAOmD,OAAO;YACpB,OAAOrF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BR,OACE,CAAC,MAAM,EAAE,AAAC2F,CAAAA,cAAc,GAAE,EAAGY,OAAO,CAAC,GAAG,2BAA2B,CAAC,GACpE,CAAC,6CAA6C,EAAE,AAC9CvC,CAAAA,kBAAkB,GAAE,EACpBuC,OAAO,CAAC,GAAG,mDAAmD,CAAC;YACrE;QACF;QAEA,MAAMC,SAAS,IAAIC,gBAAgB;YACjCjC,gBAAgB1F,OAAOoF;YACvBwC,QAAQ5H,OAAO6G;YACfgB,kBAAkB;YAClBC,wBAAwB;QAC1B;QACA,MAAMzC,WAAW,MAAMC,MAAM,qCAAqC;YAChE9D,QAAQ;YACRO,SAAS;gBACPwD,eAAe,CAAC,OAAO,EAAE5D,QAAQC,GAAG,CAACC,iBAAiB,EAAE;gBACxD,gBAAgB;eACZ2B,CAAAA,yBAAAA,MAAO8C,SAAS,IAChB;gBAAE,mBAAmB9C,MAAM8C,SAAS;YAAC,IACrC,CAAC;YAEP/E,MAAMmG,OAAOK,QAAQ;QACvB;QACA,MAAMC,SAAS,MAAM3C,SAAS3D,IAAI;QAClC,IAAI,CAAC2D,SAASI,EAAE,EAAE;;gBAuBUuC,eAcTA;YApCjB7G,QAAQD,KAAK,CAAC,uBAAuB8G,0BAAAA,OAAQ9G,KAAK;YAClD,uEAAuE;YACvE,2CAA2C;YAC3C,MAAMN,UACHmG,cAAc,CAAC,OAAOC;oBAEnB;gBADF,MAAMO,UAAU/E,QACd,OAAA,AAAC,CAAA,MAAMwE,YAAY9H,GAAG,CAACJ,SAAQ,EAAGI,GAAG,CAAC,4BAAtC,OAA0D;gBAE5D8H,YAAY9G,GAAG,CACbpB,UACA;oBAAEqI,eAAepE,KAAKyE,GAAG,CAAC,GAAGD,UAAUV;gBAAa,GACpD;oBAAEtG,OAAO;gBAAK;YAElB,GACCC,KAAK,CAAC,IAAMC;YACf,OAAM+C,yBAAAA,MAAOmD,OAAO;YACpB,sEAAsE;YACtE,sEAAsE;YACtE,sEAAsE;YACtE,qEAAqE;YACrE,wEAAwE;YACxE,uEAAuE;YACvE,MAAMsB,aAAajI,gBAAOgI,2BAAAA,gBAAAA,OAAQ9G,KAAK,qBAAb8G,cAAeE,IAAI,oBAAI;YACjD,IACED,eAAe,qBACfA,eAAe,2BACf;gBACA,OAAO3G,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAC1BR,OACE,gEACA,kEACA;gBACJ;YACF;YACA,OAAOI,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;gBAAER,KAAK,WAAE8G,2BAAAA,iBAAAA,OAAQ9G,KAAK,qBAAb8G,eAAeG,OAAO,oBAAI;YAAgB;QAC7D;QAEA,wEAAwE;QACxE,wEAAwE;QACxE,qEAAqE;QACrE,IAAIhB,gBAAgB;QACpB,IAAIiB,gBAAgB;QACpB,IAAIC,iBAAiB;QACrB,MAAMzH,UAAUmG,cAAc,CAAC,OAAOC;gBAEjC,OAEoBhI;YAHvB,MAAMA,QAAQZ,cAAca,eAAe,EACxC,QAAA,AAAC,CAAA,MAAM+H,YAAY9H,GAAG,CAACJ,SAAQ,EAAGK,IAAI,cAAtC,QAA4C,CAAC;YAEhDgI,gBAAgB3E,QAAOxD,uBAAAA,MAAMmI,aAAa,YAAnBnI,uBAAuB;YAC9CoJ,gBAAgBjB,iBAAiBL;YACjC,wEAAwE;YACxE,uEAAuE;YACvE,wEAAwE;YACxE,uEAAuE;YACvE,mEAAmE;YACnE,wEAAwE;YACxE,qEAAqE;YACrEuB,iBAAiBD,iBAAiBpJ,MAAMyC,MAAM,KAAK;YACnDuF,YAAY9G,GAAG,CACbpB,UACA,aACMsJ,gBAAgB;gBAAE3G,QAAQ;YAAW,IAAI,CAAC,GAM1CgB,iBAAiBN,MAAM,GAAG,IAC1B;gBACEmG,qBACEjK,cAAcuC,SAAS,CAACC,UAAU,CAACC,UAAU,IACxC2B;YAET,IACA,CAAC;gBACL9B,UAAUvC,cAAcmK,gBAAgB,CACtCvJ,OACA,UACA,CAAC,CAAC,EAAE,AAAC6H,CAAAA,cAAc,GAAE,EAAGY,OAAO,CAAC,GAAG,SAAS,CAAC,GAC1CW,CAAAA,gBACG,YACA3F,iBAAiBN,MAAM,GAAG,IACxB,CAAC,GAAG,EAAEM,iBAAiBN,MAAM,CAAC,KAAK,EACjCM,iBAAiBN,MAAM,KAAK,IAAI,KAAK,IACtC,UAAU,CAAC,GACZ,2CAA0C;gBAGtD;gBAAE5B,OAAO;YAAK;QAElB;QACA,MAAMiG,UAAU;YAAEW;YAAeiB;QAAc;QAC/C,OAAM5E,yBAAAA,MAAO+C,MAAM,CAAC,KAAKC;QACzB,mEAAmE;QACnE,EAAE;QACF,0EAA0E;QAC1E,wEAAwE;QACxE,yEAAyE;QACzE,qEAAqE;QACrE,EAAE;QACF,wEAAwE;QACxE,0EAA0E;QAC1E,sEAAsE;QACtE,wEAAwE;QACxE,4EAA4E;QAC5E,0EAA0E;QAC1E,yEAAyE;QACzE,oEAAoE;QACpE,0BAA0B;QAC1B,EAAE;QACF,2EAA2E;QAC3E,yEAAyE;QACzE,mDAAmD;QACnD,MAAM5H,kBAAkBC,SAASC,UAAUC;QAC3C,yEAAyE;QACzE,2EAA2E;QAC3E,6CAA6C;QAC7C,EAAE;QACF,uEAAuE;QACvE,yEAAyE;QACzE,oEAAoE;QACpE,yEAAyE;QACzE,kEAAkE;QAClE,oEAAoE;QACpE,wEAAwE;QACxE,uDAAuD;QACvD,EAAE;QACF,0EAA0E;QAC1E,sEAAsE;QACtE,yEAAyE;QACzE,uEAAuE;QACvE,0EAA0E;QAC1E,0CAA0C;QAC1C,MAAMN,oBAAoB;YACxB6D;YACAvD;YACAyJ,OAAO9E,MAAM+E,aAAa;YAC1BlG,aAAasE;YACbwB;QACF;QACA;;;;;;;;;;;;;;;;KAgBC,GACD,MAAM1J,8BAA8B;YAClC2D;YACAvD;YACAwD,aAAasE;YACbwB;QACF;QACA,0EAA0E;QAC1E,wEAAwE;QACxE,yEAAyE;QACzE,0EAA0E;QAC1E,2EAA2E;QAC3E,0EAA0E;QAC1E,0EAA0E;QAC1E,uEAAuE;QACvE,MAAM3J,iBAAiB;YAAE4D;YAAQvD;YAASiH,MAAM;YAAUqC;QAAe;QACzE,OAAO/G,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC8E;IAC9B,EAAE,OAAOtF,OAAO;QACdC,QAAQD,KAAK,CAACA;QACd,uEAAuE;QACvE,wEAAwE;QACxE,iEAAiE;QACjE,2EAA2E;QAC3E,8DAA8D;QAC9D,OAAOI,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAER,OAAO;QAAgB;IACvD;AACF,EAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginApiHandler } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Related products (AGL-325), best source first: the product's manual
20
+ * list, then co-purchase pairs mined from recent orders ("frequently
21
+ * bought together"), then tag/category neighbors. Returns catalog ids —
22
+ * the block renders them through the ids-filtered catalog API.
23
+ */
24
+ export declare const relatedHandler: PluginApiHandler;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import * as CommerceModel from "../model/index.js";
17
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
18
+ /**
19
+ * Related products (AGL-325), best source first: the product's manual
20
+ * list, then co-purchase pairs mined from recent orders ("frequently
21
+ * bought together"), then tag/category neighbors. Returns catalog ids —
22
+ * the block renders them through the ids-filtered catalog API.
23
+ */ export const relatedHandler = async (req, res)=>{
24
+ var _req_query_hostId, _req_query_productId;
25
+ const hostId = String((_req_query_hostId = req.query.hostId) != null ? _req_query_hostId : '');
26
+ const productId = String((_req_query_productId = req.query.productId) != null ? _req_query_productId : '');
27
+ if (!hostId || !productId) {
28
+ return res.status(400).json({
29
+ error: 'Missing hostId or productId'
30
+ });
31
+ }
32
+ try {
33
+ var _productSnapshot_data, _product_relatedProductIds;
34
+ var _product_tags, _product_categoryIds;
35
+ const firestore = firebaseAdmin.app().firestore();
36
+ const hostRef = firestore.collection('hosts').doc(hostId);
37
+ const productSnapshot = await hostRef.collection('products').doc(productId).get();
38
+ const product = CommerceModel.liftLegacyProduct((_productSnapshot_data = productSnapshot.data()) != null ? _productSnapshot_data : {});
39
+ let ids = ((_product_relatedProductIds = product.relatedProductIds) != null ? _product_relatedProductIds : []).slice(0, 8);
40
+ if (ids.length === 0) {
41
+ // Co-purchase pairs mined from a bounded sample of multi-line orders.
42
+ //
43
+ // Deliberately NOT "recent", which is what this said before: there is
44
+ // no orderable recency field to ask for. Orders carry their timestamp
45
+ // as `createdAtMs` on some documents and `createdAt: {seconds}` on
46
+ // others — `orderCreatedAtMs` exists precisely to paper over that — and
47
+ // an `orderBy` on either one DROPS every order written in the other
48
+ // shape. Ordering by document id instead makes the sample the SAME
49
+ // hundred orders on every call rather than an arbitrary hundred the
50
+ // backend chose, so a product's "bought together" list stops changing
51
+ // between requests for no reason. Genuine recency needs one timestamp
52
+ // field across the collection first.
53
+ const orders = await hostRef.collection('orders').orderBy(firebaseAdmin.firestore.FieldPath.documentId()).limit(100).get();
54
+ const counts = new Map();
55
+ for (const docSnapshot of orders.docs){
56
+ var _order_lineItems;
57
+ const order = CommerceModel.liftLegacyOrder(docSnapshot.data());
58
+ const lineIds = ((_order_lineItems = order.lineItems) != null ? _order_lineItems : []).map((line)=>line.productId);
59
+ if (!lineIds.includes(productId)) continue;
60
+ for (const id of lineIds){
61
+ var _counts_get;
62
+ if (id === productId) continue;
63
+ counts.set(id, ((_counts_get = counts.get(id)) != null ? _counts_get : 0) + 1);
64
+ }
65
+ }
66
+ ids = [
67
+ ...counts.entries()
68
+ ].sort((a, b)=>b[1] - a[1]).slice(0, 8).map(([id])=>id);
69
+ }
70
+ if (ids.length === 0 && (((_product_tags = product.tags) == null ? void 0 : _product_tags.length) || ((_product_categoryIds = product.categoryIds) == null ? void 0 : _product_categoryIds.length))) {
71
+ // Tag/category neighbors, from the same bounded deterministic sample:
72
+ // an unordered `limit` let a catalog larger than 300 answer one request
73
+ // from one arbitrary slice and the next from another, so a product's
74
+ // neighbours appeared and disappeared without anything changing.
75
+ const catalog = await hostRef.collection('products').orderBy(firebaseAdmin.firestore.FieldPath.documentId()).limit(300).get();
76
+ ids = catalog.docs.filter((docSnapshot)=>{
77
+ var _candidate_tags, _candidate_categoryIds;
78
+ if (docSnapshot.id === productId) return false;
79
+ const candidate = docSnapshot.data();
80
+ if (candidate.deletedAt || candidate.status !== 'active') return false;
81
+ const sharedTag = ((_candidate_tags = candidate.tags) != null ? _candidate_tags : []).some((tag)=>{
82
+ var _product_tags;
83
+ return ((_product_tags = product.tags) != null ? _product_tags : []).includes(tag);
84
+ });
85
+ const sharedCategory = ((_candidate_categoryIds = candidate.categoryIds) != null ? _candidate_categoryIds : []).some((id)=>{
86
+ var _product_categoryIds;
87
+ return ((_product_categoryIds = product.categoryIds) != null ? _product_categoryIds : []).includes(id);
88
+ });
89
+ return sharedTag || sharedCategory;
90
+ }).slice(0, 8).map((docSnapshot)=>docSnapshot.id);
91
+ }
92
+ res.setHeader('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600');
93
+ return res.status(200).json({
94
+ productIds: ids
95
+ });
96
+ } catch (error) {
97
+ console.error(error);
98
+ return res.status(500).json({
99
+ error: 'Related unavailable'
100
+ });
101
+ }
102
+ };
103
+
104
+ //# sourceMappingURL=related.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/related.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 type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\n\n/**\n * Related products (AGL-325), best source first: the product's manual\n * list, then co-purchase pairs mined from recent orders (\"frequently\n * bought together\"), then tag/category neighbors. Returns catalog ids —\n * the block renders them through the ids-filtered catalog API.\n */\nexport const relatedHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n const productId = String(req.query.productId ?? '')\n if (!hostId || !productId) {\n return res.status(400).json({ error: 'Missing hostId or productId' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const productSnapshot = await hostRef\n .collection('products')\n .doc(productId)\n .get()\n const product = CommerceModel.liftLegacyProduct(\n (productSnapshot.data() as any) ?? {},\n )\n\n let ids = (product.relatedProductIds ?? []).slice(0, 8)\n\n if (ids.length === 0) {\n // Co-purchase pairs mined from a bounded sample of multi-line orders.\n //\n // Deliberately NOT \"recent\", which is what this said before: there is\n // no orderable recency field to ask for. Orders carry their timestamp\n // as `createdAtMs` on some documents and `createdAt: {seconds}` on\n // others — `orderCreatedAtMs` exists precisely to paper over that — and\n // an `orderBy` on either one DROPS every order written in the other\n // shape. Ordering by document id instead makes the sample the SAME\n // hundred orders on every call rather than an arbitrary hundred the\n // backend chose, so a product's \"bought together\" list stops changing\n // between requests for no reason. Genuine recency needs one timestamp\n // field across the collection first.\n const orders = await hostRef\n .collection('orders')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(100)\n .get()\n const counts = new Map<string, number>()\n for (const docSnapshot of orders.docs) {\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n const lineIds = (order.lineItems ?? []).map((line) => line.productId)\n if (!lineIds.includes(productId)) continue\n for (const id of lineIds) {\n if (id === productId) continue\n counts.set(id, (counts.get(id) ?? 0) + 1)\n }\n }\n ids = [...counts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 8)\n .map(([id]) => id)\n }\n\n if (ids.length === 0 && (product.tags?.length || product.categoryIds?.length)) {\n // Tag/category neighbors, from the same bounded deterministic sample:\n // an unordered `limit` let a catalog larger than 300 answer one request\n // from one arbitrary slice and the next from another, so a product's\n // neighbours appeared and disappeared without anything changing.\n const catalog = await hostRef\n .collection('products')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(300)\n .get()\n ids = catalog.docs\n .filter((docSnapshot) => {\n if (docSnapshot.id === productId) return false\n const candidate = docSnapshot.data() as any\n if (candidate.deletedAt || candidate.status !== 'active') return false\n const sharedTag = (candidate.tags ?? []).some((tag: string) =>\n (product.tags ?? []).includes(tag),\n )\n const sharedCategory = (candidate.categoryIds ?? []).some(\n (id: string) => (product.categoryIds ?? []).includes(id),\n )\n return sharedTag || sharedCategory\n })\n .slice(0, 8)\n .map((docSnapshot) => docSnapshot.id)\n }\n\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=300, stale-while-revalidate=600',\n )\n return res.status(200).json({ productIds: ids })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Related unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","relatedHandler","req","res","hostId","String","query","productId","status","json","error","productSnapshot","product","firestore","app","hostRef","collection","doc","get","liftLegacyProduct","data","ids","relatedProductIds","slice","length","orders","orderBy","FieldPath","documentId","limit","counts","Map","docSnapshot","docs","order","liftLegacyOrder","lineIds","lineItems","map","line","includes","id","set","entries","sort","a","b","tags","categoryIds","catalog","filter","candidate","deletedAt","sharedTag","some","tag","sharedCategory","setHeader","productIds","console"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginApiHandler } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Public reservation availability (AGL-310): resource display info plus
20
+ * the booked/blocked day ranges the calendar greys out. Guest details
21
+ * never leave the server.
22
+ */
23
+ export declare const reservationAvailabilityHandler: PluginApiHandler;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import * as CommerceModel from "../model/index.js";
17
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
18
+ /**
19
+ * Public reservation availability (AGL-310): resource display info plus
20
+ * the booked/blocked day ranges the calendar greys out. Guest details
21
+ * never leave the server.
22
+ */ export const reservationAvailabilityHandler = async (req, res)=>{
23
+ var _req_query_hostId, _req_query_resourceId;
24
+ const hostId = String((_req_query_hostId = req.query.hostId) != null ? _req_query_hostId : '');
25
+ const resourceId = String((_req_query_resourceId = req.query.resourceId) != null ? _req_query_resourceId : '');
26
+ if (!hostId || !resourceId) {
27
+ return res.status(400).json({
28
+ error: 'Missing hostId or resourceId'
29
+ });
30
+ }
31
+ try {
32
+ var _resource_blocks, _resource_description, _resource_capacity, _resource_photoUrls, _resource_amenities, _resource_weekendMultiplier, _resource_seasons, _resource_minNights, _resource_depositPct, _resource_cancellationHours;
33
+ const firestore = firebaseAdmin.app().firestore();
34
+ const hostRef = firestore.collection('hosts').doc(hostId);
35
+ /*
36
+ * ONLY STAYS THAT HAVE NOT ENDED, NEAREST FIRST (AGL-2159).
37
+ *
38
+ * `.where('resourceId','==',resourceId).limit(500)` with no ordering
39
+ * returns 500 documents in `__name__` order — an arbitrary slice of the
40
+ * resource's ENTIRE booking history. Every stay it has ever had competes
41
+ * for those 500 places with the ones that are still ahead, so past a few
42
+ * hundred lifetime bookings the calendar starts leaving LIVE stays out of
43
+ * the greyed-out days and offering an occupied room to the next visitor.
44
+ *
45
+ * This is the same defect, on the same collection, that `reserve.ts`
46
+ * carries the fix for. That one guards the authoritative overlap check;
47
+ * this one draws the calendar the visitor picks from, and the two must
48
+ * agree — a calendar offering a day the booking path then refuses is the
49
+ * same bug wearing a friendlier face.
50
+ *
51
+ * `checkOutDayMs >= todayMs` removes the whole of the past: a stay that
52
+ * ended before today cannot block a day anyone can still book. Ordering
53
+ * on the same field puts the nearest stays inside the limit rather than
54
+ * whichever sort early by id. Uses the existing composite index
55
+ * `reservations (resourceId ASC, checkOutDayMs ASC)`.
56
+ *
57
+ * Every writer sets `checkOutDayMs` — it is required on the reservation
58
+ * model — which matters because an `orderBy` silently drops documents
59
+ * missing the field it sorts on.
60
+ */ const todayMs = Date.parse(`${new Date().toISOString().slice(0, 10)}T00:00:00Z`);
61
+ const [resourceSnapshot, reservationsSnapshot] = await Promise.all([
62
+ hostRef.collection('resources').doc(resourceId).get(),
63
+ hostRef.collection('reservations').where('resourceId', '==', resourceId).where('checkOutDayMs', '>=', todayMs).orderBy('checkOutDayMs').limit(500).get()
64
+ ]);
65
+ const resource = resourceSnapshot.data();
66
+ if (!resource) return res.status(404).json({
67
+ error: 'Unknown resource'
68
+ });
69
+ // The SAME hold rule the booking door applies (`reserve.ts`), through the
70
+ // one predicate in the model. An inline dead-status set here knew nothing
71
+ // about the 30-minute lapse on an unpaid `pending`, and nothing ever
72
+ // clears such a row — so one guest abandoning the payment screen greyed
73
+ // those dates out on the date-picker forever, while `reserve.ts` would
74
+ // have sold them to the next guest who asked for them directly.
75
+ const nowMs = Date.now();
76
+ const unavailable = reservationsSnapshot.docs.filter((docSnapshot)=>{
77
+ var _docSnapshot_get;
78
+ return CommerceModel.reservationHoldsDates({
79
+ status: String(docSnapshot.get('status')),
80
+ createdAtMs: Number((_docSnapshot_get = docSnapshot.get('createdAtMs')) != null ? _docSnapshot_get : 0)
81
+ }, nowMs);
82
+ }).map((docSnapshot)=>({
83
+ fromDayMs: Number(docSnapshot.get('checkInDayMs')),
84
+ toDayMs: Number(docSnapshot.get('checkOutDayMs'))
85
+ })).concat((_resource_blocks = resource.blocks) != null ? _resource_blocks : []);
86
+ res.setHeader('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=120');
87
+ return res.status(200).json({
88
+ resource: {
89
+ name: resource.name,
90
+ description: (_resource_description = resource.description) != null ? _resource_description : null,
91
+ capacity: (_resource_capacity = resource.capacity) != null ? _resource_capacity : null,
92
+ photoUrls: (_resource_photoUrls = resource.photoUrls) != null ? _resource_photoUrls : [],
93
+ amenities: (_resource_amenities = resource.amenities) != null ? _resource_amenities : [],
94
+ nightlyRateUsd: resource.nightlyRateUsd,
95
+ weekendMultiplier: (_resource_weekendMultiplier = resource.weekendMultiplier) != null ? _resource_weekendMultiplier : null,
96
+ seasons: (_resource_seasons = resource.seasons) != null ? _resource_seasons : [],
97
+ minNights: (_resource_minNights = resource.minNights) != null ? _resource_minNights : null,
98
+ depositPct: (_resource_depositPct = resource.depositPct) != null ? _resource_depositPct : null,
99
+ cancellationHours: (_resource_cancellationHours = resource.cancellationHours) != null ? _resource_cancellationHours : null
100
+ },
101
+ unavailable
102
+ });
103
+ } catch (error) {
104
+ console.error(error);
105
+ return res.status(500).json({
106
+ error: 'Availability unavailable'
107
+ });
108
+ }
109
+ };
110
+
111
+ //# sourceMappingURL=reservation-availability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/reservation-availability.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 type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\n\n/**\n * Public reservation availability (AGL-310): resource display info plus\n * the booked/blocked day ranges the calendar greys out. Guest details\n * never leave the server.\n */\nexport const reservationAvailabilityHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n const resourceId = String(req.query.resourceId ?? '')\n if (!hostId || !resourceId) {\n return res.status(400).json({ error: 'Missing hostId or resourceId' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n /*\n * ONLY STAYS THAT HAVE NOT ENDED, NEAREST FIRST (AGL-2159).\n *\n * `.where('resourceId','==',resourceId).limit(500)` with no ordering\n * returns 500 documents in `__name__` order — an arbitrary slice of the\n * resource's ENTIRE booking history. Every stay it has ever had competes\n * for those 500 places with the ones that are still ahead, so past a few\n * hundred lifetime bookings the calendar starts leaving LIVE stays out of\n * the greyed-out days and offering an occupied room to the next visitor.\n *\n * This is the same defect, on the same collection, that `reserve.ts`\n * carries the fix for. That one guards the authoritative overlap check;\n * this one draws the calendar the visitor picks from, and the two must\n * agree — a calendar offering a day the booking path then refuses is the\n * same bug wearing a friendlier face.\n *\n * `checkOutDayMs >= todayMs` removes the whole of the past: a stay that\n * ended before today cannot block a day anyone can still book. Ordering\n * on the same field puts the nearest stays inside the limit rather than\n * whichever sort early by id. Uses the existing composite index\n * `reservations (resourceId ASC, checkOutDayMs ASC)`.\n *\n * Every writer sets `checkOutDayMs` — it is required on the reservation\n * model — which matters because an `orderBy` silently drops documents\n * missing the field it sorts on.\n */\n const todayMs = Date.parse(\n `${new Date().toISOString().slice(0, 10)}T00:00:00Z`,\n )\n const [resourceSnapshot, reservationsSnapshot] = await Promise.all([\n hostRef.collection('resources').doc(resourceId).get(),\n hostRef\n .collection('reservations')\n .where('resourceId', '==', resourceId)\n .where('checkOutDayMs', '>=', todayMs)\n .orderBy('checkOutDayMs')\n .limit(500)\n .get(),\n ])\n const resource = resourceSnapshot.data() as CommerceModel.HostResource | undefined\n if (!resource) return res.status(404).json({ error: 'Unknown resource' })\n\n // The SAME hold rule the booking door applies (`reserve.ts`), through the\n // one predicate in the model. An inline dead-status set here knew nothing\n // about the 30-minute lapse on an unpaid `pending`, and nothing ever\n // clears such a row — so one guest abandoning the payment screen greyed\n // those dates out on the date-picker forever, while `reserve.ts` would\n // have sold them to the next guest who asked for them directly.\n const nowMs = Date.now()\n const unavailable = reservationsSnapshot.docs\n .filter((docSnapshot) =>\n CommerceModel.reservationHoldsDates(\n {\n status: String(\n docSnapshot.get('status'),\n ) as CommerceModel.ReservationStatus,\n createdAtMs: Number(docSnapshot.get('createdAtMs') ?? 0),\n },\n nowMs,\n ),\n )\n .map((docSnapshot) => ({\n fromDayMs: Number(docSnapshot.get('checkInDayMs')),\n toDayMs: Number(docSnapshot.get('checkOutDayMs')),\n }))\n .concat(resource.blocks ?? [])\n\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=60, stale-while-revalidate=120',\n )\n return res.status(200).json({\n resource: {\n name: resource.name,\n description: resource.description ?? null,\n capacity: resource.capacity ?? null,\n photoUrls: resource.photoUrls ?? [],\n amenities: resource.amenities ?? [],\n nightlyRateUsd: resource.nightlyRateUsd,\n weekendMultiplier: resource.weekendMultiplier ?? null,\n seasons: resource.seasons ?? [],\n minNights: resource.minNights ?? null,\n depositPct: resource.depositPct ?? null,\n cancellationHours: resource.cancellationHours ?? null,\n },\n unavailable,\n })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Availability unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","reservationAvailabilityHandler","req","res","hostId","String","query","resourceId","status","json","error","resource","firestore","app","hostRef","collection","doc","todayMs","Date","parse","toISOString","slice","resourceSnapshot","reservationsSnapshot","Promise","all","get","where","orderBy","limit","data","nowMs","now","unavailable","docs","filter","docSnapshot","reservationHoldsDates","createdAtMs","Number","map","fromDayMs","toDayMs","concat","blocks","setHeader","name","description","capacity","photoUrls","amenities","nightlyRateUsd","weekendMultiplier","seasons","minNights","depositPct","cancellationHours","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,YAAYA,mBAAmB,oBAAU;AACzC,SAASC,aAAa,QAAQ,2BAA0B;AAExD;;;;CAIC,GACD,OAAO,MAAMC,iCAAmD,OAAOC,KAAKC;QACpDD,mBACIA;IAD1B,MAAME,SAASC,QAAOH,oBAAAA,IAAII,KAAK,CAACF,MAAM,YAAhBF,oBAAoB;IAC1C,MAAMK,aAAaF,QAAOH,wBAAAA,IAAII,KAAK,CAACC,UAAU,YAApBL,wBAAwB;IAClD,IAAI,CAACE,UAAU,CAACG,YAAY;QAC1B,OAAOJ,IAAIK,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA+B;IACtE;IACA,IAAI;YAoEQC,kBASOA,uBACHA,oBACCA,qBACAA,qBAEQA,6BACVA,mBACEA,qBACCA,sBACOA;QArFvB,MAAMC,YAAYZ,cAAca,GAAG,GAAGD,SAAS;QAC/C,MAAME,UAAUF,UAAUG,UAAU,CAAC,SAASC,GAAG,CAACZ;QAClD;;;;;;;;;;;;;;;;;;;;;;;;;KAyBC,GACD,MAAMa,UAAUC,KAAKC,KAAK,CACxB,GAAG,IAAID,OAAOE,WAAW,GAAGC,KAAK,CAAC,GAAG,IAAI,UAAU,CAAC;QAEtD,MAAM,CAACC,kBAAkBC,qBAAqB,GAAG,MAAMC,QAAQC,GAAG,CAAC;YACjEX,QAAQC,UAAU,CAAC,aAAaC,GAAG,CAACT,YAAYmB,GAAG;YACnDZ,QACGC,UAAU,CAAC,gBACXY,KAAK,CAAC,cAAc,MAAMpB,YAC1BoB,KAAK,CAAC,iBAAiB,MAAMV,SAC7BW,OAAO,CAAC,iBACRC,KAAK,CAAC,KACNH,GAAG;SACP;QACD,MAAMf,WAAWW,iBAAiBQ,IAAI;QACtC,IAAI,CAACnB,UAAU,OAAOR,IAAIK,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAmB;QAEvE,0EAA0E;QAC1E,0EAA0E;QAC1E,qEAAqE;QACrE,wEAAwE;QACxE,uEAAuE;QACvE,gEAAgE;QAChE,MAAMqB,QAAQb,KAAKc,GAAG;QACtB,MAAMC,cAAcV,qBAAqBW,IAAI,CAC1CC,MAAM,CAAC,CAACC;gBAMiBA;mBALxBrC,cAAcsC,qBAAqB,CACjC;gBACE7B,QAAQH,OACN+B,YAAYV,GAAG,CAAC;gBAElBY,aAAaC,QAAOH,mBAAAA,YAAYV,GAAG,CAAC,0BAAhBU,mBAAkC;YACxD,GACAL;WAGHS,GAAG,CAAC,CAACJ,cAAiB,CAAA;gBACrBK,WAAWF,OAAOH,YAAYV,GAAG,CAAC;gBAClCgB,SAASH,OAAOH,YAAYV,GAAG,CAAC;YAClC,CAAA,GACCiB,MAAM,EAAChC,mBAAAA,SAASiC,MAAM,YAAfjC,mBAAmB,EAAE;QAE/BR,IAAI0C,SAAS,CACX,iBACA;QAEF,OAAO1C,IAAIK,MAAM,CAAC,KAAKC,IAAI,CAAC;YAC1BE,UAAU;gBACRmC,MAAMnC,SAASmC,IAAI;gBACnBC,WAAW,GAAEpC,wBAAAA,SAASoC,WAAW,YAApBpC,wBAAwB;gBACrCqC,QAAQ,GAAErC,qBAAAA,SAASqC,QAAQ,YAAjBrC,qBAAqB;gBAC/BsC,SAAS,GAAEtC,sBAAAA,SAASsC,SAAS,YAAlBtC,sBAAsB,EAAE;gBACnCuC,SAAS,GAAEvC,sBAAAA,SAASuC,SAAS,YAAlBvC,sBAAsB,EAAE;gBACnCwC,gBAAgBxC,SAASwC,cAAc;gBACvCC,iBAAiB,GAAEzC,8BAAAA,SAASyC,iBAAiB,YAA1BzC,8BAA8B;gBACjD0C,OAAO,GAAE1C,oBAAAA,SAAS0C,OAAO,YAAhB1C,oBAAoB,EAAE;gBAC/B2C,SAAS,GAAE3C,sBAAAA,SAAS2C,SAAS,YAAlB3C,sBAAsB;gBACjC4C,UAAU,GAAE5C,uBAAAA,SAAS4C,UAAU,YAAnB5C,uBAAuB;gBACnC6C,iBAAiB,GAAE7C,8BAAAA,SAAS6C,iBAAiB,YAA1B7C,8BAA8B;YACnD;YACAsB;QACF;IACF,EAAE,OAAOvB,OAAO;QACd+C,QAAQ/C,KAAK,CAACA;QACd,OAAOP,IAAIK,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA2B;IAClE;AACF,EAAC"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import * as CommerceModel from '../model';
18
+ /**
19
+ * THE ATOMIC STOCK DECREMENT (AGL-2320).
20
+ *
21
+ * Every stock write in this plugin was a read → compute → `set(…, {merge:true})`
22
+ * of the whole `variants` array, with the read often far upstream of the write:
23
+ * the cart branch of `billing-webhook.ts` reads its products, then runs a
24
+ * transaction, a `cartRef.delete()`, a checkout-doc read, licence-key
25
+ * assignment and an email send before it decrements. Two sales landing in that
26
+ * window both computed from the same `variants` and both wrote; the later won
27
+ * and one decrement was LOST. The ledger showed two `sale` rows while the count
28
+ * moved once, so the shelf was short by one and the storefront went on offering
29
+ * the unit it had already sold — the loss compounds, because phantom stock is
30
+ * what admits the next oversell.
31
+ *
32
+ * `inventory` lives INSIDE the `variants` array, not at a document field path,
33
+ * so `FieldValue.increment` is not expressible against it. That is why this was
34
+ * filed as blocked on a data-model change. It is not the only way to make a
35
+ * decrement safe: a transaction is. `Transaction.get()` takes a lock on the
36
+ * product document, so two concurrent decrements SERIALIZE — the loser re-runs
37
+ * its callback against the winner's committed state and subtracts from the real
38
+ * remainder. The read-modify-write stays, the schema is untouched, and no
39
+ * migration is needed. The cost is one contended retry, not one extra read.
40
+ *
41
+ * WHAT THIS DOES NOT DO. It does not refuse a sale. Every storefront path
42
+ * gates on `canPurchase` when the Stripe Checkout session is created and
43
+ * decrements minutes later in the webhook, by which time the money has moved —
44
+ * refusing there would take payment and withhold goods. Closing the last-unit
45
+ * oversell needs a HOLD taken at session creation and released when the session
46
+ * expires, which is a feature with a new failure mode (an abandoned cart
47
+ * sitting on the last unit for the session's lifetime) and is deliberately not
48
+ * being introduced days before launch. So the oversell is made LOUD instead of
49
+ * silent: when the fresh read proves the shelf could not cover the sale, the
50
+ * managers are told, with the number that was actually short.
51
+ *
52
+ * The floor in `adjustVariantInventory` stays — a shelf count does not go
53
+ * negative, and every reader assumes it cannot (AGL-2149). What changes is that
54
+ * `applied` is now computed from the state the write actually lands on, so the
55
+ * ledger's `appliedDelta` tells the truth under concurrency. Before this, two
56
+ * concurrent sales of one unit each computed `appliedDelta: -1` from the same
57
+ * stale `inventory: 1`, and the ledger claimed two units left a shelf that only
58
+ * ever held one.
59
+ */
60
+ export interface StockDecrementOutcome {
61
+ /**
62
+ * What the count ACTUALLY moved by — zero or negative, and never more than
63
+ * the shelf held. The caller writes this as the ledger row's `appliedDelta`
64
+ * (AGL-2149) when it differs from `requested`.
65
+ */
66
+ applied: number;
67
+ /** The units sold, negated. */
68
+ requested: number;
69
+ /**
70
+ * The product as read INSIDE the transaction, and the same product with the
71
+ * committed variants. Both null when nothing was written — a missing product,
72
+ * an untracked variant, or a failed commit. Handed to
73
+ * `alertLowStockCrossing` as the pre/post pair, which is now the pair the
74
+ * write actually crossed rather than one computed from an upstream read.
75
+ */
76
+ before: CommerceModel.HostProduct | null;
77
+ after: CommerceModel.HostProduct | null;
78
+ /**
79
+ * The commit did not land. Distinct from `applied === 0`, which is a real
80
+ * outcome (a deny-policy product already at zero). A caller must not write a
81
+ * ledger row for a failed commit: a row whose count never moved is exactly
82
+ * what makes a later cancellation hand back units nobody has.
83
+ */
84
+ failed: boolean;
85
+ }
86
+ /**
87
+ * What the ledger row joins this movement to (AGL-2161).
88
+ *
89
+ * Passing this is what makes the count and the row ONE write. Omitting it
90
+ * decrements without a row, which only `reserve-stock`'s own specs do — every
91
+ * production caller passes it, and `stock-decrement-race.spec.ts` fails the
92
+ * build if one stops.
93
+ */
94
+ export interface StockLedgerJoin {
95
+ /** Why the count moved. Every sale path passes `sale`. */
96
+ reason: CommerceModel.InventoryAdjustmentReason;
97
+ /**
98
+ * The document this movement is answerable to. `cancel-order.ts` queries
99
+ * `where('orderId','==',…)` to decide whether to restock at all, so a row
100
+ * that names the wrong id is as bad as a missing one.
101
+ */
102
+ orderId: string;
103
+ }
104
+ /**
105
+ * Decrements one variant's stock inside a transaction and reports what the
106
+ * shelf could actually give up. Never throws: a stock write must not fail a
107
+ * webhook, because Stripe would redeliver and the order would be minted twice.
108
+ *
109
+ * ## THE LEDGER ROW RIDES INSIDE THE TRANSACTION (AGL-2161)
110
+ *
111
+ * The count and its `inventoryAdjustments` row used to be two separate writes
112
+ * at six call sites, the second one a bare
113
+ * `.add({…}).catch(() => undefined)`. Either could land without the other and
114
+ * nothing anywhere said so:
115
+ *
116
+ * - **row lost, count moved** — `cancel-order.ts:206-224` reads the rows for
117
+ * an order to decide whether a POS card sale was ever decremented, and reads
118
+ * `appliedDelta ?? delta` to cap the restock. No row means it refuses to
119
+ * release that stock at all, so the units are stranded permanently.
120
+ * - **count lost, row written** — the reverse hands back units nobody has, the
121
+ * failure `StockDecrementOutcome.failed` already exists to prevent.
122
+ *
123
+ * Both targets hang off the same `hostRef`, so nothing ever stopped them
124
+ * sharing one transaction; they simply did not. They do now, and the divergence
125
+ * is not made *loud*, it is made **impossible** — which is the stronger of the
126
+ * two options, and available here only because the helper already owned the
127
+ * transaction the count was written in.
128
+ *
129
+ * Consequences worth stating, because they change five call sites at once:
130
+ *
131
+ * - A ledger failure now ROLLS BACK the decrement instead of being swallowed.
132
+ * That is the direction that keeps the two readings in agreement: the sale
133
+ * is still paid, the shelf is untouched, and `reconcile-stock.ts` reports a
134
+ * missing decrement — which is true, and was previously a false positive
135
+ * whenever only the row was lost.
136
+ * - The row is still auto-id'd. A deterministic id keyed on the order would
137
+ * make redelivery idempotent, but two lines of one product in one order are
138
+ * two legitimate rows (`billing-webhook-low-stock.spec.ts`, "logs both sale
139
+ * rows"), and a deterministic key would silently fold them into one and
140
+ * under-cap the restock. Redelivery stays bounded upstream, where it was.
141
+ */
142
+ export declare function decrementVariantStock(options: {
143
+ firestore: any;
144
+ hostRef: any;
145
+ hostId: string;
146
+ productId: string;
147
+ variantId: string;
148
+ /** Units sold; sign is ignored. */
149
+ quantity: number;
150
+ locationId?: string;
151
+ /**
152
+ * The ledger row to write in the SAME transaction as the count (AGL-2161).
153
+ * Omit only where no movement should be recorded.
154
+ */
155
+ ledger?: StockLedgerJoin;
156
+ }): Promise<StockDecrementOutcome>;