@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/pos-order.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport {\n firebaseAdmin,\n getOrgForHost,\n getPluginConfig,\n} from '@aglyn/tenant-data-admin'\nimport { captureHostContact } from '@aglyn/tenant-runtime'\nimport { connectLinkageIsReady } from '@aglyn/tenant-data-admin/server/stripe-account-mode'\nimport {\n buildRoute,\n claimAttempt,\n Route,\n type AttemptClaim,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { alertLowStockCrossing } from './low-stock'\nimport { decrementVariantStock } from './reserve-stock'\nimport {\n type PromotionSlotHold,\n holdPromotionSlot,\n promotionHoldKey,\n settlePromotionSlot,\n} from './promotion-hold'\nimport { posMaxDiscountPct } from '../plugin-config'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\n\n/**\n * The settlement claim (AGL-1691) now lives in `@aglyn/aglyn/server`\n * (AGL-1697), because it is the answer for every unkeyed money route and not\n * just this one. Moved whole and unchanged in behaviour — the digest is still\n * `sha256('pos:{hostId}:{key}')`, so keys minted against the local version\n * still resolve. The only difference is the claim document: it carries\n * `scopeId` where this file wrote `hostId`, and no longer writes a\n * `serverTimestamp` sentinel alongside `createdAtMs`, since the shared version\n * takes a plain Firestore-shaped store rather than firebase-admin itself.\n */\n\n/**\n * POS sale (AGL-312): manager-gated, server-priced. Cash sales create a\n * paid `channel: 'pos'` order immediately (with change calculation);\n * card sales mint a Stripe payment link the register shows as a QR\n * (Stripe Terminal hardware can slot in behind the same endpoint\n * later). Inventory decrements per line from the register's location,\n * and sales can post to an open reservation folio instead (AGL-317).\n */\nexport const posOrderHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const payment = String(body.payment ?? 'cash') as 'cash' | 'link' | 'folio'\n const cashReceivedCents = Math.round(Number(body.cashReceivedCents ?? 0))\n const customerEmail = String(body.customerEmail ?? '').trim().toLowerCase()\n const reservationId = String(body.reservationId ?? '')\n const registerId = String(body.registerId ?? '')\n // A code the shopper hands the cashier (AGL-305). The same field name the\n // storefront doors read, so one resolver answers it the same way.\n const couponCode = String(body.couponCode ?? '').trim()\n const locationId = String(body.locationId ?? '')\n // REFUSED, NOT CLAMPED (AGL-2161). This used to be\n // `Math.min(100, Math.max(0, …))`, so a register asking for `150` rang up a\n // 100% comp and a register asking for `-20` rang up full price — in both\n // cases a sale the operator did not ask for, recorded as though they had,\n // and indistinguishable afterwards from a deliberate one. A clamp is a\n // silent correction of a request nobody can prove was made; the ceiling\n // below is a refusal the register can show its operator.\n //\n // The finiteness test stays and is now also a refusal (AGL-2224 kept its\n // teeth): `discountPct: 'half'` propagated `NaN` through the discount, the\n // platform fee, the tax and `totals.totalCents`, and `NaN` defeats\n // comparison rather than failing it — so the \"cash received is short\" guard\n // below (`cashReceivedCents < totals.totalCents`) read false and rang up a\n // sale that had taken no money. Answering that with `0` charged full price\n // on a request the route could not read; answering with a 400 says so.\n const rawDiscountPct = Number(body.discountPct ?? 0)\n if (!Number.isFinite(rawDiscountPct) || rawDiscountPct < 0) {\n return res.status(400).json({ error: 'Invalid discount' })\n }\n const discountPct = rawDiscountPct\n const rawLines = Array.isArray(body.lines) ? body.lines : []\n // One settlement attempt, minted by the register (AGL-1691). Node lowercases\n // incoming headers, but read both spellings — the plugin API request type\n // makes no promise about casing.\n const idempotencyKey = String(\n req.headers['idempotency-key'] ?? req.headers['Idempotency-Key'] ?? '',\n ).trim().slice(0, 200)\n if (!hostId || rawLines.length === 0) {\n return res.status(400).json({ error: 'Missing hostId or lines' })\n }\n\n let claim: AttemptClaim | null = null\n /**\n * The redemption slot this sale reserved (AGL-305), hoisted so every refusal\n * below the claim — and the catch — can hand it back. A throw after the hold\n * is placed is the one path that would otherwise leave a slot standing\n * against the merchant's cap until its TTL lapsed, with no sale to show for\n * it.\n */\n let discountSlot: PromotionSlotHold | null = null\n let discountHoldKey = ''\n const releaseDiscountSlot = async (): Promise<void> => {\n const slot = discountSlot\n discountSlot = null\n discountHoldKey = ''\n if (slot) await slot.release()\n }\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n // AN ALLOWLIST (AGL-2262), matching `cancel-order.ts`, `fulfill-order.ts`\n // and the Firestore rules' own `hostMemberRole(hostId) in\n // ['admin','editor']`. The old `!role || role === 'viewer'` denylist\n // admitted every string that was not literally `viewer` on the route that\n // takes cash, mints a card QR and decrements real inventory.\n // The union has since GROWN: `HostAccessRole` is `admin | editor | author\n // | viewer` (AGL-2334), and `author` is grantable on this host through\n // `/api/hosts/members`. So this allowlist now refuses a role a real\n // projection CAN produce, deliberately — an author edits content and does\n // not take money. That is the whole reason it is an allowlist: a role\n // added later is refused until someone decides otherwise, rather than\n // admitted by the silence of a denylist.\n //\n // What it does NOT decide is WHOSE admin. `memberRoles` is a per-host\n // projection, so a SITE COLLABORATOR granted `admin` on this one site\n // (`hostAccess: { [hostId]: 'admin' }`, `allHosts: false`) is\n // indistinguishable here from an org owner. That is correct for ringing a\n // sale — they were invited to run this till — but it means this value\n // alone cannot carry an elevated, manager-only control such as the\n // AGL-2372 oversell override. The discriminator is `orgWide` on the\n // membership resolved just below, not the role string.\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not permitted' })\n }\n // THE `managePos` PERMISSION, actually read (AGL-2474). The allowlist\n // above is a HOST-level fact and stays — it is what the Firestore rules\n // enforce and dropping it would widen this route. This is the org-level\n // half: `managePos` was declared by COMMERCE_PERMISSIONS, resolved into\n // every permission map, displayed nowhere and read by nothing, so the\n // capability was inert. Both must hold, matching the marketplace\n // install/publish routes' resolveOrgPermissions + memberRoles pairing.\n //\n // Behaviour is unchanged for anyone who has not been given an explicit\n // override: `managePos` defaults to true for the admin and editor tiers,\n // which is exactly the set the allowlist already admits. What it adds is\n // the ability to REVOKE POS from one admin without demoting them.\n const membership = await resolveOrgPermissions(decoded.uid, { hostId })\n if (!membership.permissions.managePos) {\n return res.status(403).json({ error: 'Not permitted' })\n }\n const ownerOrg = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(ownerOrg?.org as any, 'pos')) {\n return res\n .status(403)\n .json({ error: 'POS requires the Pro plan or above' })\n }\n // THE DISCOUNT CEILING (AGL-2161), refused rather than clamped.\n //\n // Enforced here and not in the UI: `pos-page.component.tsx` mirrors the\n // same bound on its input, but that field is a plain `TextField` and the\n // number arrives in the request body — a client bound is a courtesy, and\n // the route is where a discount stops being a suggestion.\n //\n // The ceiling defaults to 100 (see `plugin-config.ts` for why that number\n // is the merchant's to lower and not this fix's to invent). What changed\n // is that exceeding it is now an answer instead of a silent rounding.\n // Resolved for THIS SITE (AGL-428, AGL-1014): a chain that trusts one\n // branch's register with a deeper discount than the rest sets it on that\n // site, and\n // reading only the workspace's answer would refuse the sale the console\n // says is allowed.\n const maxDiscountPct = posMaxDiscountPct(\n await getPluginConfig(ownerOrg?.orgId, 'commerce', { hostId }),\n )\n if (discountPct > maxDiscountPct) {\n return res.status(403).json({\n error:\n `This register may discount up to ${maxDiscountPct}%. Ask a ` +\n 'manager to raise the limit in the commerce settings.',\n maxDiscountPct,\n })\n }\n // Register attribution + cap (AGL-472/482): a sale must run through a\n // named register that exists on this host. Creation is quota-gated by\n // the resources route, but that's creation-only — a paid→paid\n // downgrade (e.g. Business 2 → Pro 1) would otherwise keep every\n // existing register transacting. So re-check the cap at sale time:\n // rank the host's registers by creation order and refuse any whose\n // rank is beyond THIS SITE's register cap. Deterministic and\n // self-healing — no data is deleted, and re-upgrading restores them.\n //\n // The cap is per site and so is the check (AGL-1775):\n // `checkHostRegisterQuota` is the plan's cap plus the seats the org has\n // allocated to this host out of the purchased pool. The old\n // `checkQuota(org, 'posRegisters', …)` read the org-level value, which\n // since AGL-1775 carries no pool at all — leaving it here would have\n // stopped sales on registers a merchant is invoiced $89/mo for, which is\n // the expensive direction of getting this wrong.\n if (!registerId) {\n return res.status(400).json({ error: 'Missing registerId' })\n }\n const registerDocs = (\n await hostRef.collection('registers').get()\n ).docs\n .map((doc) => ({\n id: doc.id,\n createdAtMs: doc.get('createdAt')?.toMillis?.() ?? 0,\n }))\n .sort((a, b) => a.createdAtMs - b.createdAtMs || a.id.localeCompare(b.id))\n const rank = registerDocs.findIndex((r) => r.id === registerId)\n if (rank < 0) {\n return res.status(404).json({ error: 'Unknown register' })\n }\n if (\n !Aglyn.checkHostRegisterQuota(ownerOrg?.org as any, hostId, rank).allowed\n ) {\n return res.status(403).json({\n error:\n 'This register is over your plan’s limit — remove extra ' +\n 'registers or upgrade in Billing to use it.',\n })\n }\n\n // Server pricing per line.\n const uniqueIds: string[] = [\n ...new Set<string>(rawLines.map((line: any) => String(line.productId))),\n ]\n const productSnapshots = await Promise.all(\n uniqueIds.map((id) => hostRef.collection('products').doc(id).get()),\n )\n const productsById = new Map(\n productSnapshots.map((snapshot) => [\n snapshot.id,\n snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data() as any) : null,\n ]),\n )\n const lineItems: CommerceModel.OrderLineItem[] = []\n for (const raw of rawLines) {\n const product = productsById.get(String(raw.productId))\n if (!product) continue\n const variant =\n product.variants.find((item) => item.id === raw.variantId) ??\n product.variants[0]\n // A variant with no price yet (AGL-2916) is refused with its name, not\n // rung up at nothing.\n if (!CommerceModel.variantHasPrice(variant)) {\n return res.status(400).json({ error: `Set a price for ${product.name} before selling it.` })\n }\n const quantity = Math.max(1, Math.min(99, Math.round(Number(raw.quantity ?? 1))))\n lineItems.push({\n productId: String(raw.productId),\n ...(variant.id !== 'default' ? { variantId: variant.id } : {}),\n name: product.name,\n ...(Object.keys(variant.options ?? {}).length\n ? { variantLabel: Object.values(variant.options ?? {}).join(' / ') }\n : {}),\n ...(variant.sku ? { sku: variant.sku } : {}),\n productType: product.type,\n quantity,\n unitAmountCents: Math.round(Number(variant.priceUsd) * 100),\n })\n }\n if (lineItems.length === 0) {\n return res.status(400).json({ error: 'No valid lines' })\n }\n // WHAT THE COUNT SAYS, said out loud (AGL-2357). Every storefront door\n // gates on `canPurchase`; a register that skips it silently downgrades a\n // merchant's `oversellPolicy: 'deny'` to `backorder` at the till.\n //\n // The rule here is WARN, NEVER BLOCK. A till is the wrong\n // place for a stale number to stop a real sale — the cashier is holding\n // the goods — but a merchant who set \"deny\" is entitled to have the\n // register say something before it sells past zero. So the shortfall is\n // reported and the sale proceeds: there is no refusal anywhere below this\n // line, and adding one is a separate decision (AGL-2358, the\n // manager-override variant).\n //\n // Computed from the SERVER's product documents, not the register's cached\n // grid, and returned on both tenders. The client shows its own warning as\n // the basket is built (`pos-page.component.tsx`), which is the surface the\n // cashier reads; this is the authoritative one, taken at the moment of\n // sale and available to any other register client.\n const stockWarnings: CommerceModel.StockShortfall[] = []\n for (const line of lineItems) {\n const product = productsById.get(line.productId)\n if (!product) continue\n const shortfall = CommerceModel.stockShortfall(\n product,\n line.variantId ?? product.variants[0]?.id,\n line.quantity,\n )\n if (!shortfall) continue\n stockWarnings.push({\n productId: line.productId,\n ...(line.variantId ? { variantId: line.variantId } : {}),\n name: line.name,\n ...(line.variantLabel ? { variantLabel: line.variantLabel } : {}),\n requested: line.quantity,\n available: shortfall.available,\n })\n }\n const itemsCents = lineItems.reduce(\n (sum, line) => sum + line.unitAmountCents * line.quantity,\n 0,\n )\n const cashierDiscountCents = Math.round((itemsCents * discountPct) / 100)\n // THE DISCOUNTS HUB, AT THE TILL TOO (AGL-305).\n //\n // The register knew exactly one kind of discount: the cashier's own\n // percentage, bounded by `posMaxDiscountPct` above. A code from the\n // Discounts card — or a store-wide automatic promotion needing no code at\n // all — applied on the storefront and applied nothing here, so a shopper\n // who bought in person paid more than the same shopper buying the same\n // goods online, and the merchant's own advertised sale did not reach their\n // own counter.\n //\n // TWO DIFFERENT MECHANISMS, DELIBERATELY NOT MERGED. The cashier's\n // percentage is discretion — a manager's ceiling governs it because the\n // person holding the till decided it. A hub discount is the MERCHANT's own\n // published promotion, decided in the console; running it through\n // `posMaxDiscountPct` would let a register's discretionary ceiling silently\n // veto a store-wide sale, which is not what that ceiling is for. So they\n // are resolved separately and added, and only the discretionary half is\n // bounded.\n //\n // BEFORE TAX, because the sum lands in `discountCents` and the tax below\n // is already computed on `itemsCents - discountCents` — the position the\n // cashier's discount has always held, and the one the storefront doors\n // reach by their own route.\n const posDiscountLines = lineItems.map((line) => ({\n productId: line.productId,\n amountCents: line.unitAmountCents * line.quantity,\n }))\n const hubDiscounts = await hostRef.collection('discounts').limit(100).get()\n const resolvedDiscount = CommerceModel.resolveDiscount(\n hubDiscounts.docs.map((docSnapshot) => ({\n ...(docSnapshot.data() as CommerceModel.HostDiscount),\n $id: docSnapshot.id,\n })),\n {\n ...(couponCode ? { code: couponCode } : {}),\n subtotalCents: itemsCents,\n productIds: lineItems.map((line) => line.productId),\n lines: posDiscountLines,\n },\n )\n // A code the cashier typed that cannot apply is an ANSWER they can read to\n // the shopper, not a silent full-price sale. Above the claim and above\n // every write with the other deterministic refusals, so re-keying the code\n // works on the same button.\n if (resolvedDiscount?.codeProblem) {\n return res.status(400).json({ error: resolvedDiscount.codeProblem })\n }\n // FREE SHIPPING CANNOT BE APPLIED AT A TILL, AND SAYING SO IS THE POINT.\n //\n // There is no carriage in an in-person sale — no `shipping_options`, no\n // rate, nothing to zero — so this benefit resolves perfectly and is worth\n // nothing here. Falling through would ring the sale at full price with the\n // shopper holding a valid code, which is the exact shape of the defect that\n // charged $7.99 for free shipping. An AUTOMATIC free-shipping promotion is\n // passed over rather than refused: nobody asked for it at the counter, and\n // refusing the sale over a promotion the shopper never invoked would stop\n // the till dead.\n if (resolvedDiscount?.benefit.kind === 'free-shipping') {\n if (couponCode) {\n return res.status(400).json({\n error:\n 'That code is for free shipping, and there is nothing to ship on ' +\n 'an in-person sale.',\n })\n }\n }\n const hubDiscountCents =\n resolvedDiscount?.benefit.kind === 'amount'\n ? resolvedDiscount.benefit.centsOff\n : 0\n const appliedDiscountId = hubDiscountCents\n ? resolvedDiscount!.discountId\n : ''\n // Capped at the basket: two independent reductions can sum past it, and a\n // negative charge is not a sale. `computeOrderTotals` clamps too — this is\n // here so the tax base and the fee scaling below see the same number the\n // totals will.\n const discountCents = Math.min(\n itemsCents,\n cashierDiscountCents + hubDiscountCents,\n )\n // THE PLATFORM FEE (AGL-2110), and its absence was a straight loss.\n //\n // A POS card sale opens a DESTINATION charge\n // (`payment_intent_data[transfer_data][destination]`, below) with no\n // `transfer_data[amount]`, so Stripe transfers the whole charge to the\n // merchant while debiting its own processing fee (2.9% + 30¢) from the\n // PLATFORM's balance. Without an `application_fee_amount` beside it,\n // every in-person card sale moved money OUT of Aglyn — the exact shape\n // `plan-entitlements.ts` documents for a 0% rate on a destination charge\n // (AGL-2071). Online sales have carried the fee since AGL-307; the\n // register never did.\n //\n // PER LINE BY PRODUCT TYPE, summed — byte-for-byte the arithmetic\n // `cart-checkout.ts` uses, because a basket can mix a physical good\n // (Starter 2%) with a digital one (Starter 5%) and a single basket-wide\n // rate would price one of them wrong. A missing `type` reads as\n // `physical`, matching `liftLegacyProduct` and the cart.\n //\n // Scaled by the cashier's discount, again like the cart: the fee is a cut\n // of what the shopper actually paid, not of the list basket.\n // WHETHER a rate applies is a separate fact from what it rounds to\n // (AGL-2256). This path rounds TWICE — once per line, then again when the\n // cashier's discount scales the sum — and floored neither, so a small\n // basket on a low rate produced 0 and the emission guard below read that as\n // \"this plan charges no fee\" and sent none at all. Every other door floors\n // at `Math.max(1, …)` when the rate is above zero.\n let feeApplies = false\n const grossFeeCents = lineItems.reduce((sum, line) => {\n const linePct = Aglyn.resolveTransactionFeePct(\n ownerOrg?.org as any,\n (line.productType ?? 'physical') as 'physical' | 'digital' | 'service',\n )\n if (linePct > 0) feeApplies = true\n return (\n sum +\n Math.round((line.unitAmountCents * line.quantity * linePct) / 100)\n )\n }, 0)\n const chargedItemsCents = Math.max(0, itemsCents - discountCents)\n const scaledFeeCents =\n itemsCents > 0\n ? Math.round((grossFeeCents * chargedItemsCents) / itemsCents)\n : 0\n // Gated on what the shopper actually pays for goods, so a 100% cashier\n // discount still takes nothing — the fee is a cut of the goods, and there\n // are none left to cut.\n const takeFeeCents =\n feeApplies && chargedItemsCents > 0\n ? Math.max(1, scaledFeeCents)\n : scaledFeeCents\n // Origin tax (AGL-285) — in-person sales are origin-based by nature.\n const storeSettings = await hostRef.collection('settings').doc('store').get()\n const taxSettings = (storeSettings.get('tax') ?? {}) as CommerceModel.TaxSettings\n // AGL-1999: an unset tax mode is NOT a decision, and it must not sell.\n // `mode` was `undefined` for every store whose owner never opened the\n // Taxes card — the default state of every new storefront — and each\n // branch below tested for a specific string, so no branch was taken, no\n // tax was computed, and the shopper was charged an untaxed total. See\n // `commerce-tax-decision.ts` for why refusing beats defaulting to either\n // mode. `mode: 'none'` is the explicit opt-out and passes straight\n // through.\n const taxDecision = CommerceModel.storefrontTaxDecision({\n settings: taxSettings,\n })\n if (taxDecision.kind === 'undecided') {\n return res\n .status(409)\n .json({ error: CommerceModel.STOREFRONT_TAX_UNDECIDED_MESSAGE })\n }\n // `inPerson`, and it is the whole reason this argument exists (AGL-2145):\n // a store on Stripe Tax is served correctly ONLINE and must not be\n // refused there, but the register sends the basket as one opaque line and\n // sets no `automatic_tax` — it cannot, there is no customer address at a\n // till — and the cash and folio tenders never reach Stripe at all. Every\n // in-person sale at such a store charged ZERO tax, on both tenders, with\n // no refusal and no log: the AGL-1999 defect, still open at the one place\n // a shopper is standing in front of you.\n const taxMisconfigured = CommerceModel.storefrontTaxMisconfiguration(\n taxSettings,\n { inPerson: true },\n )\n if (taxMisconfigured) {\n return res.status(409).json({ error: taxMisconfigured })\n }\n const rate =\n taxDecision.kind === 'manual'\n ? CommerceModel.resolveTaxRate(taxSettings, taxSettings.origin ?? {})\n : null\n const taxCents =\n rate && !taxSettings.pricesIncludeTax\n ? CommerceModel.computeTaxCents(itemsCents - discountCents, rate.pct)\n : 0\n /*==========================================\n * THE FEE ATTACHES TO THE SALE, NOT TO THE TENDER (AGL-2111).\n *\n * It used to ride the CARD totals only, and the argument was mechanical:\n * cash and folio never touch Stripe, so there is no charge to take an\n * `application_fee_amount` out of, so record nothing. That left a merchant\n * on a non-zero rate one tap away from paying Aglyn nothing forever —\n * ring every in-person sale as cash. the decision, 2026-08-19: close\n * it. The tender is at most an input to the RATE; it is never a switch\n * that zeroes the fee.\n *\n * ## Cash is genuinely different from card, and the difference argues for\n * ## the SAME rate, not a lower one\n *\n * On a card sale the fee becomes `application_fee_amount` on a DESTINATION\n * charge, and Stripe's own processing (2.9% + 30¢) is debited from the\n * PLATFORM's balance beside it (AGL-2071). At the Starter physical rate of\n * 2% the fee does not even cover that processing — Aglyn nets negative on\n * the card leg. So the take rate demonstrably is NOT a processing\n * pass-through with margin on top; there is no processing component in it\n * to strip out for a tender that skips Stripe.\n *\n * What the fee buys is the same on both tenders: the catalogue that priced\n * the basket, the stock that decremented, the tax that computed, the order\n * record, the receipt, the reporting, the register itself. Aglyn's cost\n * basis on a cash sale is LOWER (no Stripe fee at all), so the same\n * percentage is more profitable there, not less justified.\n *\n * And any gap between tenders is itself the defect: a cheaper cash rate\n * would still pay a merchant to steer customers to cash. Parity is the\n * only rate at which the tender stops being a pricing lever.\n *\n * ## What changes is COLLECTION, not the rate\n *\n * There is still no charge to net it from, so the fee is ACCRUED against\n * the org's month (`offlineFees` below) and swept onto the\n * org's own monthly Aglyn invoice by `report-usage` — option 2 on\n * AGL-2111, the `aglyn_metered_usage` meter that already carries every\n * other overage. `feeCollection` on the order says which of the two routes\n * this sale's fee took, so a merchant reconciling a payout can tell \"short\n * by the fee\" from \"billed for it next month\".\n *\n * `feeCents` does not enter `totalCents` (see `computeOrderTotals`), so\n * the shopper pays exactly what they paid before — this moves no price at\n * the till, on either tender.\n *=========================================*/\n // STRIPE'S CARD COST, ON THE CARD TENDER ONLY (AGL-2152). `takeFeeCents`\n // above is the platform's advertised take and every tender carries it\n // (AGL-2111). This term is different in kind: it is not a rate, it is the\n // 2.9% + 30¢ Stripe debits from the PLATFORM's balance when a destination\n // charge settles — which the AGL-2111 note beside it already observes the\n // take does not cover. Cash and folio never reach Stripe, so there is no\n // such cost to pass through and adding one would invoice a merchant for a\n // fee nobody paid.\n //\n // The base is exactly what the card runs for: the goods after the\n // cashier's discount, plus the origin tax. A till knows both — there is no\n // shopper-chosen shipping and no Stripe Tax to wait on — so unlike the\n // online doors this one recovers the cost exactly.\n const feeCents =\n payment === 'link'\n ? takeFeeCents +\n Aglyn.storefrontProcessingCostCents(chargedItemsCents + taxCents)\n : takeFeeCents\n const totals = CommerceModel.computeOrderTotals(lineItems, {\n discountCents,\n taxCents,\n feeCents,\n })\n // Kept as a distinct name because the card branch reads it for the Stripe\n // parameters and the metadata witness; the two are now the same figure by\n // construction, which is the point of this change.\n const cardTotals = totals\n // How this sale's fee reaches Aglyn. `payout` = netted from the merchant's\n // Stripe transfer at settlement; `invoice` = accrued here and billed on\n // the org's next Aglyn invoice. Written on every POS order that carries a\n // fee, so the collection route is a recorded fact rather than something a\n // reader has to infer from the tender.\n const feeCollection: 'payout' | 'invoice' =\n payment === 'link' ? 'payout' : 'invoice'\n // WHERE THE INVOICED HALF ACCRUES (AGL-2111): one document per org-month,\n // `orgs/{id}/offlineFees/{YYYY-MM}`, read by `report-usage` and added to\n // the org-month's `billedCents` — the same `aglyn_metered_usage` meter\n // that already carries dataset, API and contact overage. Nothing new has\n // to be minted in Stripe for this to bill.\n //\n // Keyed by ORG, not by host: the org is the billing subject (AGL-238), and\n // a merchant with three storefronts gets one line rather than three.\n //\n // An org with no id is not billable and must not silently accrue into\n // `orgs//offlineFees/…`; the sale still rings, and the fee is forgone\n // rather than written somewhere no invoice will ever find it.\n const offlineFeeMonth = offlineFeeMonthKey()\n const offlineFeeOrgId = String(ownerOrg?.org?.id ?? '')\n const offlineFeeRef = offlineFeeOrgId\n ? firestore\n .collection('orgs')\n .doc(offlineFeeOrgId)\n .collection('offlineFees')\n .doc(offlineFeeMonth)\n : null\n\n // Deterministic tender rejections run BEFORE the claim is taken, so they\n // never burn the key (AGL-1691): \"cash received is short\" is a 400 the\n // cashier fixes by taking more cash and pressing the same button, and a\n // claimed key would replay the rejection forever. Hoisted above the card\n // branch so one claim site covers every tender; both are no-ops for `link`.\n if (payment === 'cash' && cashReceivedCents < totals.totalCents) {\n return res.status(400).json({ error: 'Cash received is short' })\n }\n // AGL-1760: the stay must EXIST before the sale is rung, and here is the\n // last place that can still be said. A `folio` tender moves no money at\n // this counter — no card is charged and no cash is taken; the order is\n // written `paid` because the charge is BOOKED against the stay and\n // collected at check-out, at the register, as its own sale. So an unknown\n // reservation is the same class of deterministic, retryable rejection as\n // \"cash received is short\": the cashier picks the right stay and presses\n // the same button. Sitting above the claim is what keeps the key unburned.\n //\n // The read is not an extra one. AGL-1757 already loaded this document a\n // few lines below, to attribute the sale to its guest; it is HOISTED here\n // rather than added, so a folio sale still costs exactly one reservation\n // read — and that one read now answers both questions.\n //\n // Unlike AGL-1757's, this read is authoritative rather than best-effort:\n // it IS the validation, so a Firestore failure must not be swallowed into\n // \"no such stay\". It falls to the 500 below — before the claim, before any\n // order — and the cashier retries the same key with nothing taken.\n let folioGuestEmail = ''\n let folioGuestName = ''\n if (payment === 'folio') {\n if (!reservationId) {\n return res.status(400).json({ error: 'Pick a reservation' })\n }\n const reservationSnapshot = await hostRef\n .collection('reservations')\n .doc(reservationId)\n .get()\n if (!reservationSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown reservation' })\n }\n // The one POS tender where the customer is already identified by the\n // system — `reserve.ts` populated these from the guest's own booking\n // (AGL-1757). The console walk-in writes `guestEmail: null`, which\n // leaves them empty and correctly attributes the sale to nobody.\n folioGuestEmail = String(reservationSnapshot.get('guestEmail') ?? '')\n .trim()\n .toLowerCase()\n folioGuestName = String(reservationSnapshot.get('guestName') ?? '')\n .trim()\n .slice(0, 120)\n }\n\n // Point of no return: everything past here writes an order or moves money.\n const claimed = await claimAttempt(firestore, {\n kind: 'pos',\n scopeId: hostId,\n orgId: String(ownerOrg?.org?.id ?? ''),\n key: idempotencyKey,\n busyMessage: 'This sale is already being processed',\n })\n if ('replay' in claimed) {\n return res.status(claimed.replay.status).json(claimed.replay.body)\n }\n claim = claimed.claim\n\n // THE REDEMPTION SLOT IS RESERVED HERE (AGL-305), so the till cannot\n // outrun a cap.\n //\n // Without it a merchant's \"first 50 customers\" promotion is uncapped at the\n // counter: nothing on this path ever incremented `redemptions`, so every\n // in-person sale would take the discount and none of them would be counted\n // — the cap would bound the website only. The CASH and FOLIO tenders settle\n // the slot the moment the sale is written, a few lines below, because there\n // is no webhook coming for them; the CARD tender passes it in the session\n // metadata and the `checkout.session.completed` branch settles it, exactly\n // as the storefront doors do.\n //\n // BELOW the claim because the hold is keyed by the attempt: a retry of one\n // sale must re-claim its own slot rather than be refused by it.\n if (appliedDiscountId) {\n const slot = await holdPromotionSlot({\n firestore,\n ref: hostRef.collection('discounts').doc(appliedDiscountId),\n holdKey: promotionHoldKey(claimed.claim.stripeKey),\n label: `pos discount ${appliedDiscountId}`,\n })\n if (!slot.ok) {\n // Nothing has been written and no money has moved, so the key goes\n // back and the cashier can re-ring the sale — at the correct price,\n // because `resolveDiscount` counts live holds and stops offering an\n // exhausted promotion.\n await claim.release()\n return res.status(409).json({\n error:\n couponCode && resolvedDiscount?.discount.code\n ? CommerceModel.PROMOTION_EXHAUSTED_MESSAGE\n : CommerceModel.PROMOTION_UNAVAILABLE_MESSAGE,\n })\n }\n if (slot.holdKey) {\n discountSlot = slot\n discountHoldKey = slot.holdKey\n }\n }\n\n if (payment === 'link') {\n // QR payment link on the merchant account, completed by webhook.\n const ownerProfile = await firestore\n .collection('profiles')\n .doc(String(ownerOrg?.org?.ownerUid ?? ''))\n .get()\n const accountId = ownerProfile.get('stripeAccountId')\n if (\n !connectLinkageIsReady(\n {\n accountId,\n chargesEnabled: ownerProfile.get('stripeChargesEnabled'),\n accountLivemode: ownerProfile.get('stripeAccountLivemode'),\n },\n { subject: `POS payment link host ${hostId}` },\n )\n ) {\n // The slot is handed back before the refusal (AGL-305): this returns\n // below the hold, and a register that keeps pressing a tender the\n // merchant never enabled would otherwise consume the promotion's cap\n // one attempt at a time without ever selling anything.\n await releaseDiscountSlot()\n return res.status(409).json({ error: 'Card payments not set up' })\n }\n const orderRef = hostRef.collection('orders').doc()\n const counterRef = hostRef.collection('counters').doc('orders')\n await firestore.runTransaction(async (transaction) => {\n const counter = await transaction.get(counterRef)\n const number = Number(counter.get('next') ?? 1)\n transaction.set(counterRef, { next: number + 1 }, { merge: true })\n transaction.set(orderRef, {\n number,\n status: 'pending',\n channel: 'pos',\n registerId,\n // WHO COMPED IT (AGL-2161). `totals.discountCents` recorded that\n // a discount happened and nothing recorded who asked for it or on\n // what basis — `decoded.uid` was read once, for the role gate, and\n // written nowhere. A 100% comp was therefore indistinguishable from\n // a correctly-priced sale after the fact, on the one channel where\n // the operator is standing in front of the goods.\n //\n // Written only when there IS a discount, so ordinary sales keep\n // their current shape: `gift-cards.ts` already audits this way\n // (`issuedBy`, `voidedBy`), and this is the same field for the same\n // reason.\n ...(discountPct > 0\n ? { discountPct, discountBy: decoded.uid }\n : {}),\n // WHICH PROMOTION, where one applied (AGL-305). `discountPct` and\n // `discountBy` answer for the cashier's own reduction and cannot\n // answer for this one — a hub discount has no operator and no\n // percentage the till chose. Without it a sale carrying a\n // `totals.discountCents` the cashier never entered reads as\n // unexplained.\n ...(appliedDiscountId ? { discountId: appliedDiscountId } : {}),\n // The bucket the sale comes out of (AGL-286). The cash write below\n // has stored this since AGL-1808; the card sale needs it MORE — its\n // decrement runs in the webhook (AGL-1825), where the register's\n // chosen location exists nowhere but on this document.\n ...(locationId ? { locationId } : {}),\n lineItems,\n // `feeCents` is what the merchant's payout is short by, and it\n // belongs on the document that records the charge. Every tender\n // carries the fee since AGL-2111; this is the one that is netted\n // from the payout rather than invoiced, and `feeCollection` says so.\n totals: cardTotals,\n // WHICH TAX THIS SALE CARRIED (AGL-2451), from the decision resolved\n // above. The register never reaches `stripe-automatic` — AGL-2145\n // refuses that store in person, because there is no shopper address\n // at a till — so this is `manual` or `none`, and the QR session\n // carries the same figure over as `metadata[taxCents]` for the\n // webhook to restate when the card is actually charged.\n taxMode: CommerceModel.storefrontTaxModeForDecision(\n taxDecision,\n taxCents,\n ),\n ...(feeCents > 0 ? { feeCollection } : {}),\n customerEmail: customerEmail || null,\n timeline: [{ atMs: Date.now(), event: 'pos-card-pending' }],\n createdAtMs: Date.now(),\n createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n })\n // Stripe returns the cashier to the console POS page. This was\n // `/{hostDocId}/pos`, the pre-AGL-621/622 shape, so completing a card\n // sale dropped the register on a 404 (AGL-685). POS is the plugin\n // route keyed by org slug + host SUBDOMAIN; server code cannot call\n // useConsoleHostRoute, so resolve them here, and fall back to the\n // origin root rather than a fabricated dead path.\n const posOrigin = `https://${req.headers.host}`\n const posSubdomain = (\n await firestore.collection('hostIndex').doc(hostId).get()\n ).get('subdomain') as string | undefined\n const posOrgSlug = ownerOrg?.org?.slug as string | undefined\n const posUrl =\n posOrgSlug && posSubdomain\n ? `${posOrigin}${buildRoute(Route.HOST_PLUGIN, {\n orgSlug: posOrgSlug,\n host: posSubdomain,\n pluginSlug: 'pos',\n })}`\n : posOrigin\n const params = new URLSearchParams({\n mode: 'payment',\n 'line_items[0][quantity]': '1',\n 'line_items[0][price_data][currency]': 'usd',\n 'line_items[0][price_data][unit_amount]': String(totals.totalCents),\n 'line_items[0][price_data][product_data][name]': 'In-store purchase',\n 'payment_intent_data[transfer_data][destination]': String(accountId),\n // Stripe rejects a zero application fee, so it is omitted rather than\n // sent as '0' — the same conditional `checkout.ts` uses (AGL-2110).\n ...(feeCents > 0\n ? { 'payment_intent_data[application_fee_amount]': String(feeCents) }\n : {}),\n success_url: `${posUrl}?paid=1`,\n cancel_url: `${posUrl}?paid=0`,\n 'metadata[type]': 'commerce-draft',\n 'metadata[hostId]': hostId,\n 'metadata[orderId]': orderRef.id,\n // The tax WITNESS (AGL-1953). The whole sale goes over as one\n // \"In-store purchase\" line at `totals.totalCents`, so the origin tax\n // computed above is invisible to Stripe: `total_details.amount_tax`\n // is 0 and nothing on the session says any part of the charge was\n // tax. The figure survived on the order document, but no downstream\n // reader of the Stripe object could decompose it — including\n // `recordStorefrontTax`, which reads exactly this key and was\n // therefore recording every POS card sale as `taxMode: 'none'`.\n //\n // A witness only: the `commerce-draft` webhook branch does not\n // rebuild totals from the session (it folds in shipping alone), so\n // this cannot double-count against the stored order.\n 'metadata[taxCents]': String(Math.max(0, taxCents)),\n // The fee WITNESS, the twin of the tax one above (AGL-2110): the\n // `commerce-draft` webhook branch that completes this order reads the\n // session, and without this key nothing on the Stripe object says\n // what Aglyn took. Recorded even at zero, so \"the plan charges no\n // fee\" is legible as a decision rather than as a missing field.\n 'metadata[feeCents]': String(Math.max(0, feeCents)),\n // WHAT THE WEBHOOK SETTLES (AGL-305), in the field names the\n // `checkout.session.completed` branch already reads. The card tender is\n // the one POS shape with a webhook to settle in — cash and folio settle\n // their own slot inline below.\n ...(appliedDiscountId\n ? { 'metadata[discountId]': appliedDiscountId }\n : {}),\n ...(discountHoldKey\n ? { 'metadata[discountHoldKey]': discountHoldKey }\n : {}),\n })\n const response = await fetch(\n 'https://api.stripe.com/v1/checkout/sessions',\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n // The half that costs real money (AGL-1691). Stripe replays the\n // existing session for a repeated key instead of opening a second\n // one on the merchant's live account, which covers the window\n // where our own claim is written but the response never arrives.\n ...(claim.stripeKey\n ? { 'Idempotency-Key': claim.stripeKey }\n : {}),\n },\n body: params.toString(),\n },\n )\n const session = (await response.json()) as {\n url?: string\n id?: string\n error?: any\n }\n if (!response.ok || !session.url) {\n await orderRef.delete().catch(() => undefined)\n // The sale did not happen — let the same key be tried again rather\n // than locking this basket out over one flaky moment. The slot goes\n // back with it, or the merchant's cap is one short for a sale nobody\n // made.\n await releaseDiscountSlot()\n await claim.release()\n return res.status(502).json({ error: 'Payment link failed' })\n }\n // The session id, stored the way `draft-order.ts` has always stored it\n // (AGL-2244). Without it a cancelled QR sale has no handle on the Stripe\n // page still sitting on the customer's phone, so `cancel-order.ts` had\n // nothing to expire and the register could take the money twice.\n await orderRef\n .set({ checkoutSessionId: session.id ?? null }, { merge: true })\n .catch(() => undefined)\n const cardPayload = {\n orderId: orderRef.id,\n url: session.url,\n totals: cardTotals,\n // AGL-2357. Present on both tenders and on the recorded replay, so a\n // cashier who re-submits the same key still sees what they were told\n // the first time.\n ...(stockWarnings.length > 0 ? { stockWarnings } : {}),\n }\n await claim.record(200, cardPayload)\n return res.status(200).json(cardPayload)\n }\n\n // Cash (or folio) sale: paid immediately. Tender validation for both ran\n // above, before the claim.\n const orderRef = hostRef.collection('orders').doc()\n const counterRef = hostRef.collection('counters').doc('orders')\n // Held in a local so the folio branch below can re-state the timeline with\n // a second event appended rather than blind-writing over this one.\n const paidEvent = {\n atMs: Date.now(),\n event: 'paid',\n detail:\n payment === 'folio'\n ? `Charged to reservation ${reservationId}`\n : `Cash — received $${(cashReceivedCents / 100).toFixed(2)}, ` +\n `change $${((cashReceivedCents - totals.totalCents) / 100).toFixed(2)}`,\n }\n // THE SLOT IS SETTLED HERE (AGL-305), not by a webhook, because none is\n // coming: a cash or folio sale is complete the moment this document is\n // written. Settled BEFORE the write rather than after, so a failure to\n // count the redemption refuses the sale instead of ringing an uncounted\n // one — the direction that keeps the merchant's cap honest. Idempotent\n // under a replayed attempt: the redemption is owed by the presence of the\n // HOLD, so a second pass finds none and takes nothing.\n if (appliedDiscountId && discountHoldKey) {\n const settlement = await settlePromotionSlot({\n firestore,\n ref: hostRef.collection('discounts').doc(appliedDiscountId),\n holdKey: discountHoldKey,\n label: `pos discount ${appliedDiscountId}`,\n })\n if (settlement === 'error') {\n await releaseDiscountSlot()\n await claim.release()\n return res.status(409).json({\n error: 'That discount could not be applied — try the sale again.',\n })\n }\n // Counted: this attempt no longer holds anything, so the catch below\n // must not hand back a slot that is already a redemption.\n discountSlot = null\n discountHoldKey = ''\n }\n await firestore.runTransaction(async (transaction) => {\n const counter = await transaction.get(counterRef)\n const number = Number(counter.get('next') ?? 1)\n transaction.set(counterRef, { next: number + 1 }, { merge: true })\n transaction.set(orderRef, {\n number,\n status: 'paid',\n channel: 'pos',\n registerId,\n // WHO COMPED IT (AGL-2161). `totals.discountCents` recorded that\n // a discount happened and nothing recorded who asked for it or on\n // what basis — `decoded.uid` was read once, for the role gate, and\n // written nowhere. A 100% comp was therefore indistinguishable from\n // a correctly-priced sale after the fact, on the one channel where\n // the operator is standing in front of the goods.\n //\n // Written only when there IS a discount, so ordinary sales keep\n // their current shape: `gift-cards.ts` already audits this way\n // (`issuedBy`, `voidedBy`), and this is the same field for the same\n // reason.\n ...(discountPct > 0\n ? { discountPct, discountBy: decoded.uid }\n : {}),\n // WHICH PROMOTION, where one applied (AGL-305) — see the card branch\n // above for why the cashier's two fields cannot answer for it.\n ...(appliedDiscountId ? { discountId: appliedDiscountId } : {}),\n // The bucket the decrement below comes out of (AGL-1808). Without it\n // a cancellation can only put the units back on the flat total, which\n // the next location-aware write recomputes away.\n ...(locationId ? { locationId } : {}),\n lineItems,\n totals,\n // WHICH TAX THIS SALE CARRIED (AGL-2451). Cash and folio never reach\n // Stripe at all, so no Stripe object will ever state this and NOTHING\n // else can supply it later — the decision resolved above is the only\n // witness there will be, and here is the one moment it can be written\n // down.\n taxMode: CommerceModel.storefrontTaxModeForDecision(\n taxDecision,\n taxCents,\n ),\n // AGL-2111: the fee is on `totals` for this tender too, and this says\n // it will arrive on the org's Aglyn invoice rather than as a short\n // payout — there is no payout, the merchant kept the cash.\n ...(feeCents > 0 ? { feeCollection } : {}),\n customerEmail: customerEmail || null,\n timeline: [paidEvent],\n ...(payment === 'folio' ? { reservationId } : {}),\n createdAtMs: Date.now(),\n createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n // ACCRUED IN THE SAME TRANSACTION AS THE ORDER (AGL-2111), so an order\n // and the fee it owes cannot diverge: either both land or neither does.\n // A separate write after the commit would drop the accrual on any crash\n // in the window and under-bill silently, which is the failure mode this\n // whole issue is about.\n //\n // `increment` rather than read-modify-write: two registers can ring\n // simultaneously, and a lost update here is lost revenue.\n if (feeCents > 0 && offlineFeeRef) {\n transaction.set(\n offlineFeeRef,\n {\n month: offlineFeeMonth,\n feeCents: firebaseAdmin.firestore.FieldValue.increment(feeCents),\n orders: firebaseAdmin.firestore.FieldValue.increment(1),\n },\n { merge: true },\n )\n }\n })\n\n // Folio (AGL-317): the stay settles the charge at check-out. The guest\n // identity behind `folioGuestEmail`/`folioGuestName` was read above, before\n // the claim (AGL-1757, hoisted by AGL-1760).\n if (payment === 'folio') {\n // AGL-1760: `update()`, never `set(..., { merge: true })`. A merge-set\n // against a missing path CREATES the document, which is how a typo or a\n // stale id used to mint a phantom stay — a `reservations/{id}` doc\n // holding one folio line and nothing else: no guest, no dates, no room,\n // invisible to a picker that filters on `status` and `NaN`-valued to the\n // availability maths that reads `checkInDayMs`. `update()` rejects on a\n // missing document instead of creating it, so the stub is not reachable\n // by any argument. Same defect `a7a2d90df` fixed for bookings; the guard\n // differs because that handler already had a transaction and a status to\n // read, whereas here existence is the only question and `update` IS the\n // check — atomic, and without a second read.\n //\n // The validation above makes a missing stay unreachable in practice.\n // What is left is the race — a manager deleting it in the window between\n // the two — and every other reason a write fails, which the old\n // `.catch(() => undefined)` swallowed indistinguishably. The sale is\n // committed by then, so this cannot refuse; and a paid sale must never be\n // lost, nor an orphaned attribution be silent. So the order stands\n // untouched and says so on its own timeline, which the console order\n // dialog renders. The note claims only what is known — that the line is\n // on no folio — rather than diagnosing why.\n await hostRef\n .collection('reservations')\n .doc(reservationId)\n .update({\n folio: firebaseAdmin.firestore.FieldValue.arrayUnion({\n orderId: orderRef.id,\n amountCents: totals.totalCents,\n note: lineItems\n .map((line) => `${line.quantity}× ${line.name}`)\n .join(', ')\n .slice(0, 120),\n atMs: Date.now(),\n }),\n })\n .catch(async (error) => {\n console.error('Folio append failed', reservationId, error)\n await orderRef\n .set(\n {\n timeline: [\n paidEvent,\n {\n atMs: Date.now(),\n event: 'folio-unattached',\n detail:\n `Not recorded on reservation ${reservationId} — this ` +\n `$${(totals.totalCents / 100).toFixed(2)} charge is on ` +\n 'no stay’s folio. Collect it at the register.',\n },\n ],\n },\n { merge: true },\n )\n .catch(() => undefined)\n })\n }\n\n // Inventory decrement per line (location-aware, AGL-286).\n for (const line of lineItems) {\n const product = productsById.get(line.productId)\n if (!product) continue\n const variantId = line.variantId ?? product.variants[0]?.id\n const tracked = product.variants.some(\n (variant) => variant.id === variantId && variant.inventory != null,\n )\n if (!variantId || !tracked) continue\n // Two lines of one product must COMPOUND (AGL-1828): the next line\n // starts from these variants, not from the product as first read —\n // recomputing from the original would erase this decrement when the\n // sibling line's merge-set landed, while the ledger below recorded\n // both. Same carry-forward as the webhook's POS card loop (AGL-1825).\n //\n // ATOMIC since AGL-2320, and the register is the channel that needed it\n // most: `productsById` was read at the top of this handler, before the\n // register quota check, the pricing pass, the settlement claim, the order\n // transaction and the folio append. A shopper's webhook landing anywhere\n // in that window had its decrement overwritten by this merge-set — two\n // sales, one unit off the shelf, both in the ledger. The transaction's\n // re-read is what compounds the lines now, and it sees the other\n // channel's committed write too.\n //\n // The register still SELLS whatever is rung up. Refusing goods a cashier\n // is physically holding is a product decision, not this fix's (AGL-2320);\n // what the merchant gets instead is the oversold notification the helper\n // fires when the shelf could not cover the sale.\n const moved = await decrementVariantStock({\n firestore,\n hostRef,\n hostId,\n productId: line.productId,\n variantId,\n quantity: line.quantity,\n locationId: locationId || undefined,\n ledger: { reason: 'sale', orderId: orderRef.id },\n })\n if (!moved.before || !moved.after) continue\n productsById.set(line.productId, moved.after)\n // Low-stock crossing alert (AGL-1826): the register — the channel most\n // likely to be selling down the last few units of physical shelf\n // stock — used to cross the threshold silently while the buy-now\n // button alerted. The compounded pair above means two lines of one\n // product cross exactly once, on the line that breaches; the\n // `claimAttempt` bounds a replayed settlement.\n alertLowStockCrossing(hostId, moved.before, moved.after)\n }\n // AGL-1757: the register's email box is optional, and a room charge is\n // exactly the sale nobody stops to re-ask an email for — so a stay's\n // ancillary revenue, the part a guest house most wants to see per guest,\n // was the part most likely to be anonymous. Fall back to the reservation's\n // guest, and prefer a TYPED address when there is one: a cashier\n // correcting the guest's address should beat stale reservation data.\n //\n // The guest's name rides along only when the address we are attributing to\n // IS the reservation's guest — if the cashier redirected the receipt to\n // some other address, that person is not the named guest. Passing it is\n // safe either way: `mergeContactInteraction` keeps an existing name, so\n // this fills a blank and never clobbers a better one.\n //\n // Nothing is invented. A walk-in reservation (`guestEmail: null`) leaves\n // this empty and no contact is created — the right answer, not a gap. The\n // other case AGL-1757 handled here, a `reservationId` pointing at nothing,\n // no longer arrives: AGL-1760 refuses that sale above, before the claim.\n const contactEmail = customerEmail || folioGuestEmail\n const contactName =\n contactEmail && contactEmail === folioGuestEmail ? folioGuestName : ''\n if (contactEmail) {\n // AWAITED, AND ITS FAILURE SWALLOWED (AGL-2473). `void` meant the\n // contact write was a promise nobody held: a serverless container is\n // free to freeze the moment the response is written, so on a cold-ish\n // instance the attribution simply did not happen, silently and\n // unreproducibly. Awaiting is what makes it durable.\n //\n // A BARE `await` would have been worse than the bug. This line sits\n // PAST the point of no return — the order is committed, the cash is in\n // the drawer, the stock is decremented — so letting a rejection reach\n // the `catch` below would answer a completed sale with a 500, release\n // the settlement claim, and invite the cashier to ring the same basket\n // again on the same key. A duplicate paid order is a far larger defect\n // than a missing contact row. So the failure is logged and dropped\n // here, and the sale still returns 200.\n await captureHostContact({\n hostId,\n email: contactEmail,\n ...(contactName ? { name: contactName } : {}),\n source: 'order',\n // A sale makes a customer of anybody who was not yet one and never\n // moves anybody back (AGL-2612).\n initialLifecycleStage: 'customer',\n // AGL-1748: the amount was formatted into the summary STRING below and\n // never passed to the field that exists to hold it, so `ltvCents`\n // counted online sales only — a shop-counter merchant's best customers\n // all ranked at zero. `totals.totalCents` is what the cashier actually\n // took (items + tax - discount, from `computeOrderTotals` above), not a\n // figure re-derived from the product docs.\n //\n // Gross of the platform fee and gross of refunds, matching the cart,\n // buy-now and subscription call sites; see the module note in\n // `upsert-contact.ts`. Replay safety is the `claimAttempt` above: this\n // line is past the point of no return, so a keyed double-submit never\n // reaches it, and a KEYLESS one already wrote a second order document\n // long before it got here — `purchaseCents` adds no hazard the order\n // write does not already have.\n purchaseCents: totals.totalCents,\n interaction: {\n refId: orderRef.id,\n // `source` stays `'order'` for both (AGL-1757): a folio sale is a\n // shop sale that happens to be settled against a stay, and the\n // reservation's own `'booking'` interaction already exists. Only the\n // human-readable summary distinguishes them, so a merchant reading\n // the timeline can tell a counter sale from a room charge.\n summary:\n payment === 'folio'\n ? `Room charge ($${(totals.totalCents / 100).toFixed(2)})`\n : `In-store purchase ($${(totals.totalCents / 100).toFixed(2)})`,\n },\n }).catch((error) => {\n console.error('[pos-order] contact upsert failed', error)\n })\n }\n const cashPayload = {\n orderId: orderRef.id,\n totals,\n changeCents:\n payment === 'cash' ? cashReceivedCents - totals.totalCents : 0,\n ...(stockWarnings.length > 0 ? { stockWarnings } : {}),\n }\n await claim.record(200, cashPayload)\n return res.status(200).json(cashPayload)\n } catch (error) {\n console.error(error)\n // Release on the way out so a transient failure does not strand the key\n // (AGL-1691). The order write itself may or may not have landed; the\n // cashier reconciles from the orders list, which is the same position\n // they were in before this endpoint had a claim at all. The redemption\n // slot goes back for the same reason, and only while it is still a HOLD —\n // a cash sale that already settled its slot cleared the local, so this\n // cannot give back a counted redemption.\n await releaseDiscountSlot()\n await claim?.release()\n return res.status(500).json({ error: 'Sale failed' })\n }\n}\n\n/**\n * The billing month a non-card POS fee accrues into (AGL-2111): `YYYY-MM` in\n * **UTC**, byte-identical to the key `apps/console/utils/billing-month.ts`\n * mints and to the twelve other month counters on the platform\n * (`orgs/{id}/apiUsage/*`, `orgs/{id}/assistUsage/*`, the per-host counters).\n *\n * It has to be UTC and it has to be this exact expression: `report-usage`\n * sweeps `previousMonth()` in UTC and reads the accrual document by that key,\n * so a local-time month would strand every sale rung in the offset window on a\n * document no sweep ever looks at — which is uncollected revenue that leaves\n * no trace. `offline-pos-fee-month-key.spec.ts` pins the two against each\n * other rather than trusting the comment.\n *\n * Local rather than imported because `apps/console` is an app: a plugin\n * library cannot import from it, and the repo's other eleven copies of this\n * one-liner are the established shape.\n */\nexport function offlineFeeMonthKey(now: Date = new Date()): string {\n return now.toISOString().slice(0, 7)\n}\n"],"names":["Aglyn","CommerceModel","firebaseAdmin","getOrgForHost","getPluginConfig","captureHostContact","connectLinkageIsReady","buildRoute","claimAttempt","Route","alertLowStockCrossing","decrementVariantStock","holdPromotionSlot","promotionHoldKey","settlePromotionSlot","posMaxDiscountPct","resolveOrgPermissions","posOrderHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","payment","cashReceivedCents","Math","round","Number","customerEmail","trim","toLowerCase","reservationId","registerId","couponCode","locationId","rawDiscountPct","discountPct","isFinite","rawLines","Array","isArray","lines","idempotencyKey","claim","discountSlot","discountHoldKey","releaseDiscountSlot","slot","release","hostSnapshot","storeSettings","taxSettings","ownerOrg","decoded","app","auth","verifyIdToken","firestore","hostRef","collection","doc","get","memberRole","uid","membership","permissions","managePos","checkEntitlement","org","maxDiscountPct","orgId","registerDocs","docs","map","id","createdAtMs","toMillis","sort","a","b","localeCompare","rank","findIndex","r","checkHostRegisterQuota","allowed","uniqueIds","Set","line","productId","productSnapshots","Promise","all","productsById","Map","snapshot","exists","liftLegacyProduct","data","lineItems","raw","product","variant","variants","find","item","variantId","variantHasPrice","name","quantity","max","min","push","Object","keys","options","variantLabel","values","join","sku","productType","type","unitAmountCents","priceUsd","stockWarnings","shortfall","stockShortfall","requested","available","itemsCents","reduce","sum","cashierDiscountCents","posDiscountLines","amountCents","hubDiscounts","limit","resolvedDiscount","resolveDiscount","docSnapshot","$id","code","subtotalCents","productIds","codeProblem","benefit","kind","hubDiscountCents","centsOff","appliedDiscountId","discountId","discountCents","feeApplies","grossFeeCents","linePct","resolveTransactionFeePct","chargedItemsCents","scaledFeeCents","takeFeeCents","taxDecision","storefrontTaxDecision","settings","STOREFRONT_TAX_UNDECIDED_MESSAGE","taxMisconfigured","storefrontTaxMisconfiguration","inPerson","rate","resolveTaxRate","origin","taxCents","pricesIncludeTax","computeTaxCents","pct","feeCents","storefrontProcessingCostCents","totals","computeOrderTotals","cardTotals","feeCollection","offlineFeeMonth","offlineFeeMonthKey","offlineFeeOrgId","offlineFeeRef","totalCents","folioGuestEmail","folioGuestName","reservationSnapshot","claimed","scopeId","key","busyMessage","replay","ref","holdKey","stripeKey","label","ok","discount","PROMOTION_EXHAUSTED_MESSAGE","PROMOTION_UNAVAILABLE_MESSAGE","session","ownerProfile","ownerUid","accountId","chargesEnabled","accountLivemode","subject","orderRef","counterRef","runTransaction","transaction","counter","number","set","next","merge","channel","discountBy","taxMode","storefrontTaxModeForDecision","timeline","atMs","Date","now","event","createdAt","FieldValue","serverTimestamp","posOrigin","host","posSubdomain","posOrgSlug","slug","posUrl","HOST_PLUGIN","orgSlug","pluginSlug","params","URLSearchParams","mode","success_url","cancel_url","response","fetch","Authorization","process","env","STRIPE_SECRET_KEY","toString","url","delete","catch","checkoutSessionId","cardPayload","orderId","record","paidEvent","detail","toFixed","settlement","month","increment","orders","update","folio","arrayUnion","note","console","tracked","some","inventory","moved","ledger","reason","before","after","contactEmail","contactName","email","source","initialLifecycleStage","purchaseCents","interaction","refId","summary","cashPayload","changeCents","toISOString"],"mappings":"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"}
@@ -0,0 +1,54 @@
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, type PluginJobHostGate } from '@aglyn/aglyn/server';
18
+ /** One pass over the open checkouts. Bounded (200) and idempotent. */
19
+ export interface AbandonedScanResult {
20
+ scanned: number;
21
+ sent: number;
22
+ /** Rows left untouched because their site is locked (AGL-2495). */
23
+ skippedLocked: number;
24
+ }
25
+ /**
26
+ * The abandoned-checkout pass itself, separated from its HTTP door (AGL-2227).
27
+ *
28
+ * It was only ever reachable through `processAbandonedHandler`, which is
29
+ * `x-cron-secret`-gated — and **nothing has ever POSTed to it**. It is not in
30
+ * `.github/workflows/scheduled-crons.yml`, there is no `crons` key in
31
+ * `vercel.json`, and it was not a `registerPluginJob`. `server.ts` described
32
+ * these as "the scheduler-driven jobs" in a comment, which is how a dead
33
+ * feature stays invisible: the code asserted the wiring rather than having it.
34
+ *
35
+ * So `abandonedCart` — a Pro-tier entitlement, sold on /pricing, with a
36
+ * console row added by AGL-2081 telling the merchant their template needs it —
37
+ * had never sent one email. Exporting the scan lets `server.ts` register it on
38
+ * the platform job beat (the every-minute `pluginJobsBeat` that AGL-2176
39
+ * proved out) while the HTTP door stays for manual/ops invocation and for the
40
+ * console's "send now" control.
41
+ */
42
+ export declare function scanAbandonedCheckouts(
43
+ /**
44
+ * The lockdown gate, injected by whoever drives the pass (AGL-2495). NOT
45
+ * optional: a default would make "forgot to thread it" compile, which is
46
+ * the whole failure this parameter exists to make impossible.
47
+ */
48
+ gate: PluginJobHostGate): Promise<AbandonedScanResult>;
49
+ /**
50
+ * HTTP door for the pass above (AGL-323). Kept `x-cron-secret`-gated so an
51
+ * external scheduler or an operator can still drive it; the job registration
52
+ * in `server.ts` is what makes it actually run.
53
+ */
54
+ export declare const processAbandonedHandler: PluginApiHandler;
@@ -0,0 +1,222 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import * as Aglyn from "@aglyn/aglyn/server";
18
+ import { firebaseAdmin, getOrgForHost, hostSendingIdentity, meterHostEmail } from "@aglyn/tenant-data-admin";
19
+ import { isDeferrableSendResult, isEmailConfigured, loadHostEmail, renderLoadedHostEmail, sendEmail } from "@aglyn/shared-util-email";
20
+ import { pluginJobHostGate } from "@aglyn/aglyn/server";
21
+ const REMIND_AFTER_MS = 60 * 60 * 1000;
22
+ const GIVE_UP_AFTER_MS = 7 * 24 * 60 * 60 * 1000;
23
+ /**
24
+ * The abandoned-checkout pass itself, separated from its HTTP door (AGL-2227).
25
+ *
26
+ * It was only ever reachable through `processAbandonedHandler`, which is
27
+ * `x-cron-secret`-gated — and **nothing has ever POSTed to it**. It is not in
28
+ * `.github/workflows/scheduled-crons.yml`, there is no `crons` key in
29
+ * `vercel.json`, and it was not a `registerPluginJob`. `server.ts` described
30
+ * these as "the scheduler-driven jobs" in a comment, which is how a dead
31
+ * feature stays invisible: the code asserted the wiring rather than having it.
32
+ *
33
+ * So `abandonedCart` — a Pro-tier entitlement, sold on /pricing, with a
34
+ * console row added by AGL-2081 telling the merchant their template needs it —
35
+ * had never sent one email. Exporting the scan lets `server.ts` register it on
36
+ * the platform job beat (the every-minute `pluginJobsBeat` that AGL-2176
37
+ * proved out) while the HTTP door stays for manual/ops invocation and for the
38
+ * console's "send now" control.
39
+ */ export async function scanAbandonedCheckouts(/**
40
+ * The lockdown gate, injected by whoever drives the pass (AGL-2495). NOT
41
+ * optional: a default would make "forgot to thread it" compile, which is
42
+ * the whole failure this parameter exists to make impossible.
43
+ */ gate) {
44
+ const firestore = firebaseAdmin.app().firestore();
45
+ const now = Date.now();
46
+ // Collection-group over every host's checkouts.
47
+ const openCheckouts = await firestore.collectionGroup('checkouts').where('status', '==', 'open').limit(200).get();
48
+ let sent = 0;
49
+ const entitledHosts = new Map();
50
+ // White-label brand per host (White-Label Phase 3): resolved once per host
51
+ // alongside the entitlement, from the same org doc, through the one shared
52
+ // resolver — so the recovery email's sender reads as the store's brand.
53
+ const brandingByHost = new Map();
54
+ /*
55
+ * One identity resolution per SITE, not per message.
56
+ *
57
+ * The same reason the branding map above exists: this sweep runs across
58
+ * every site, and an uncached resolution here would be two Firestore reads
59
+ * per recipient rather than per host. Scoped to the run, so a domain
60
+ * un-verified by the re-check sweep is picked up on the next one.
61
+ */ const identityByHost = new Map();
62
+ /** Each site's public origin, for the unsubscribe link on the reminder. */ const siteBaseByHost = new Map();
63
+ // Resolve each host's designed template once per run (AGL-770).
64
+ const templateCache = new Map();
65
+ let skippedLocked = 0;
66
+ for (const docSnapshot of openCheckouts.docs){
67
+ var _data_createdAtMs, _data_resumeUrl, _ref, _data_resumeUrl1, _siteBaseByHost_get;
68
+ var _docSnapshot_ref_parent_parent, _brandingByHost_get;
69
+ const data = docSnapshot.data();
70
+ // `hosts/{hostId}/checkouts/{id}` — the grandparent is the host.
71
+ const hostId = (_docSnapshot_ref_parent_parent = docSnapshot.ref.parent.parent) == null ? void 0 : _docSnapshot_ref_parent_parent.id;
72
+ if (!hostId) continue;
73
+ // LOCKDOWN (AGL-2495), and it is the FIRST thing in the loop body rather
74
+ // than sitting next to the email below, because the `status: 'expired'`
75
+ // stamp further down is itself a write for this host. Reordered for that
76
+ // reason: the cheap filters used to run first, and one of them wrote.
77
+ //
78
+ // SKIPPED, NOT DROPPED: `continue` leaves the checkout `open` and
79
+ // unstamped, so the reminder is sent — or the row expired — on the first
80
+ // beat after the lift. Nothing here marks a checkout done, so declining
81
+ // to act IS leaving it for later.
82
+ if (await gate.isLocked(hostId)) {
83
+ skippedLocked += 1;
84
+ continue;
85
+ }
86
+ const createdAtMs = Number((_data_createdAtMs = data.createdAtMs) != null ? _data_createdAtMs : 0);
87
+ if (!data.email || data.remindedAtMs) continue;
88
+ if (now - createdAtMs < REMIND_AFTER_MS) continue;
89
+ if (now - createdAtMs > GIVE_UP_AFTER_MS) {
90
+ await docSnapshot.ref.set({
91
+ status: 'expired'
92
+ }, {
93
+ merge: true
94
+ }).catch(()=>undefined);
95
+ continue;
96
+ }
97
+ if (!entitledHosts.has(hostId)) {
98
+ var _Aglyn_hostPublicOrigin;
99
+ const org = await getOrgForHost(hostId).catch(()=>null);
100
+ entitledHosts.set(hostId, Aglyn.checkEntitlement(org == null ? void 0 : org.org, 'abandonedCart'));
101
+ brandingByHost.set(hostId, Aglyn.resolveBrandingProfile(org == null ? void 0 : org.org));
102
+ // The site's own origin, for the unsubscribe link. Resolved once per
103
+ // host for the same reason the branding beside it is: this sweep is a
104
+ // `collectionGroup` over every site's checkouts, and a read per
105
+ // reminder would be a read per shopper.
106
+ const hostSnapshot = await firestore.collection('hosts').doc(hostId).get().catch(()=>null);
107
+ siteBaseByHost.set(hostId, (_Aglyn_hostPublicOrigin = Aglyn.hostPublicOrigin({
108
+ cname: hostSnapshot == null ? void 0 : hostSnapshot.get('cname'),
109
+ subdomain: hostSnapshot == null ? void 0 : hostSnapshot.get('subdomain')
110
+ })) != null ? _Aglyn_hostPublicOrigin : '');
111
+ }
112
+ if (!entitledHosts.get(hostId)) continue;
113
+ let loaded = templateCache.get(hostId);
114
+ if (loaded === undefined) {
115
+ loaded = await loadHostEmail(firestore, hostId, 'abandoned-cart');
116
+ templateCache.set(hostId, loaded);
117
+ }
118
+ const designed = loaded ? renderLoadedHostEmail(loaded, {
119
+ 'cart.url': String((_data_resumeUrl = data.resumeUrl) != null ? _data_resumeUrl : '')
120
+ }, Aglyn.sanitizeAuthorHtml) : null;
121
+ /*
122
+ * MARKETING, and `'bulk'` priority, which this sweep is entitled to
123
+ * precisely because it is resumable: a checkout left unstamped below is
124
+ * re-scanned on the next beat, so a refusal means "not this hour" rather
125
+ * than a reminder nobody ever gets.
126
+ *
127
+ * The gate adds the unsubscribe header pair and a visible link, checks
128
+ * both suppression lists, and counts this against how much mail the
129
+ * shopper has had from this site today.
130
+ */ const result = await sendEmail(_extends({
131
+ to: String(data.email),
132
+ subject: (_ref = designed == null ? void 0 : designed.subject) != null ? _ref : 'You left something in your cart',
133
+ text: (designed == null ? void 0 : designed.text) || 'Your cart is still waiting — pick up where you left off:\n\n' + `${(_data_resumeUrl1 = data.resumeUrl) != null ? _data_resumeUrl1 : ''}\n\n` + 'Your items are held but not reserved, so they may sell out.'
134
+ }, (designed == null ? void 0 : designed.html) ? {
135
+ html: designed.html
136
+ } : {}, {
137
+ fromName: (_brandingByHost_get = brandingByHost.get(hostId)) == null ? void 0 : _brandingByHost_get.fromName,
138
+ sendingIdentity: await hostSendingIdentity(hostId, identityByHost),
139
+ audience: 'tenant',
140
+ context: 'abandoned cart',
141
+ priority: 'bulk',
142
+ /*
143
+ * A cart reminder is promotional mail — the "Promotions and offers"
144
+ * stream, which is also where a campaign lands by default. Declared so
145
+ * a shopper who left that stream stops receiving these; the gate asks
146
+ * the topic only for callers that name one.
147
+ */ marketing: {
148
+ hostId,
149
+ siteBase: (_siteBaseByHost_get = siteBaseByHost.get(hostId)) != null ? _siteBaseByHost_get : '',
150
+ topicId: Aglyn.EMAIL_TOPIC_MARKETING
151
+ }
152
+ }));
153
+ /*
154
+ * SKIPPED, NOT DROPPED — the lockdown rule above, applied to the two
155
+ * refusals a later beat can pass.
156
+ *
157
+ * `remindedAtMs` is what retires a checkout from this sweep. Stamping it
158
+ * after the hourly ceiling or the frequency cap refused would turn a
159
+ * deferral into a reminder nobody ever gets; NOT stamping it after a
160
+ * suppression or a rejection would re-read the same doomed row on every
161
+ * beat until it crowded out the checkouts that could still be reminded.
162
+ * `isDeferrableSendResult` is the one place that distinction is made.
163
+ */ if (isDeferrableSendResult(result)) continue;
164
+ // Cost meter (AGL-1438). One reminder per abandoned checkout, triggered
165
+ // by that shopper's own action rather than composed as a broadcast, so
166
+ // it counts toward cost without entering the campaign cap. On the
167
+ // DELIVERED message only: a suppressed recipient produced no message, so
168
+ // there is no cost to record.
169
+ if (result.sent) await meterHostEmail(hostId);
170
+ await docSnapshot.ref.set({
171
+ remindedAtMs: now
172
+ }, {
173
+ merge: true
174
+ }).catch(()=>undefined);
175
+ if (result.sent) sent += 1;
176
+ }
177
+ return {
178
+ scanned: openCheckouts.size,
179
+ sent,
180
+ skippedLocked
181
+ };
182
+ }
183
+ /**
184
+ * HTTP door for the pass above (AGL-323). Kept `x-cron-secret`-gated so an
185
+ * external scheduler or an operator can still drive it; the job registration
186
+ * in `server.ts` is what makes it actually run.
187
+ */ export const processAbandonedHandler = async (req, res)=>{
188
+ if (req.method !== 'POST') {
189
+ return res.status(405).json({
190
+ error: 'Method not allowed'
191
+ });
192
+ }
193
+ const cronSecret = process.env.CRON_SECRET;
194
+ if (!cronSecret) {
195
+ return res.status(501).json({
196
+ error: 'Not configured (CRON_SECRET).'
197
+ });
198
+ }
199
+ if (req.headers['x-cron-secret'] !== cronSecret) {
200
+ return res.status(401).json({
201
+ error: 'Unauthorized'
202
+ });
203
+ }
204
+ if (!isEmailConfigured()) {
205
+ return res.status(501).json({
206
+ error: 'Email is not configured.'
207
+ });
208
+ }
209
+ try {
210
+ // The manual door asks the same question the beat does (AGL-2495): a
211
+ // forced pass is still a pass, and `x-cron-secret` is not an argument
212
+ // for mailing a suspended site's shoppers.
213
+ return res.status(200).json(await scanAbandonedCheckouts(pluginJobHostGate()));
214
+ } catch (error) {
215
+ console.error(error);
216
+ return res.status(500).json({
217
+ error: 'Processing failed'
218
+ });
219
+ }
220
+ };
221
+
222
+ //# sourceMappingURL=process-abandoned.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/process-abandoned.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 {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n} from '@aglyn/tenant-data-admin'\nimport {\n isDeferrableSendResult,\n isEmailConfigured,\n loadHostEmail,\n renderLoadedHostEmail,\n sendEmail,\n type LoadedHostEmail,\n} from '@aglyn/shared-util-email'\nimport {\n type PluginApiHandler,\n type PluginJobHostGate,\n pluginJobHostGate,\n} from '@aglyn/aglyn/server'\n\nconst REMIND_AFTER_MS = 60 * 60 * 1000\nconst GIVE_UP_AFTER_MS = 7 * 24 * 60 * 60 * 1000\n\n/** One pass over the open checkouts. Bounded (200) and idempotent. */\nexport interface AbandonedScanResult {\n scanned: number\n sent: number\n /** Rows left untouched because their site is locked (AGL-2495). */\n skippedLocked: number\n}\n\n/**\n * The abandoned-checkout pass itself, separated from its HTTP door (AGL-2227).\n *\n * It was only ever reachable through `processAbandonedHandler`, which is\n * `x-cron-secret`-gated — and **nothing has ever POSTed to it**. It is not in\n * `.github/workflows/scheduled-crons.yml`, there is no `crons` key in\n * `vercel.json`, and it was not a `registerPluginJob`. `server.ts` described\n * these as \"the scheduler-driven jobs\" in a comment, which is how a dead\n * feature stays invisible: the code asserted the wiring rather than having it.\n *\n * So `abandonedCart` — a Pro-tier entitlement, sold on /pricing, with a\n * console row added by AGL-2081 telling the merchant their template needs it —\n * had never sent one email. Exporting the scan lets `server.ts` register it on\n * the platform job beat (the every-minute `pluginJobsBeat` that AGL-2176\n * proved out) while the HTTP door stays for manual/ops invocation and for the\n * console's \"send now\" control.\n */\nexport async function scanAbandonedCheckouts(\n /**\n * The lockdown gate, injected by whoever drives the pass (AGL-2495). NOT\n * optional: a default would make \"forgot to thread it\" compile, which is\n * the whole failure this parameter exists to make impossible.\n */\n gate: PluginJobHostGate,\n): Promise<AbandonedScanResult> {\n const firestore = firebaseAdmin.app().firestore()\n const now = Date.now()\n // Collection-group over every host's checkouts.\n const openCheckouts = await firestore\n .collectionGroup('checkouts')\n .where('status', '==', 'open')\n .limit(200)\n .get()\n let sent = 0\n const entitledHosts = new Map<string, boolean>()\n // White-label brand per host (White-Label Phase 3): resolved once per host\n // alongside the entitlement, from the same org doc, through the one shared\n // resolver — so the recovery email's sender reads as the store's brand.\n const brandingByHost = new Map<string, Aglyn.ResolvedBrandingProfile>()\n /*\n * One identity resolution per SITE, not per message.\n *\n * The same reason the branding map above exists: this sweep runs across\n * every site, and an uncached resolution here would be two Firestore reads\n * per recipient rather than per host. Scoped to the run, so a domain\n * un-verified by the re-check sweep is picked up on the next one.\n */\n const identityByHost = new Map<\n string,\n Awaited<ReturnType<typeof hostSendingIdentity>>\n >()\n /** Each site's public origin, for the unsubscribe link on the reminder. */\n const siteBaseByHost = new Map<string, string>()\n // Resolve each host's designed template once per run (AGL-770).\n const templateCache = new Map<string, LoadedHostEmail | null>()\n let skippedLocked = 0\n for (const docSnapshot of openCheckouts.docs) {\n const data = docSnapshot.data() as any\n // `hosts/{hostId}/checkouts/{id}` — the grandparent is the host.\n const hostId = docSnapshot.ref.parent.parent?.id\n if (!hostId) continue\n // LOCKDOWN (AGL-2495), and it is the FIRST thing in the loop body rather\n // than sitting next to the email below, because the `status: 'expired'`\n // stamp further down is itself a write for this host. Reordered for that\n // reason: the cheap filters used to run first, and one of them wrote.\n //\n // SKIPPED, NOT DROPPED: `continue` leaves the checkout `open` and\n // unstamped, so the reminder is sent — or the row expired — on the first\n // beat after the lift. Nothing here marks a checkout done, so declining\n // to act IS leaving it for later.\n if (await gate.isLocked(hostId)) {\n skippedLocked += 1\n continue\n }\n const createdAtMs = Number(data.createdAtMs ?? 0)\n if (!data.email || data.remindedAtMs) continue\n if (now - createdAtMs < REMIND_AFTER_MS) continue\n if (now - createdAtMs > GIVE_UP_AFTER_MS) {\n await docSnapshot.ref\n .set({ status: 'expired' }, { merge: true })\n .catch(() => undefined)\n continue\n }\n if (!entitledHosts.has(hostId)) {\n const org = await getOrgForHost(hostId).catch(() => null)\n entitledHosts.set(\n hostId,\n Aglyn.checkEntitlement(org?.org as any, 'abandonedCart'),\n )\n brandingByHost.set(hostId, Aglyn.resolveBrandingProfile(org?.org as any))\n // The site's own origin, for the unsubscribe link. Resolved once per\n // host for the same reason the branding beside it is: this sweep is a\n // `collectionGroup` over every site's checkouts, and a read per\n // reminder would be a read per shopper.\n const hostSnapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .get()\n .catch(() => null)\n siteBaseByHost.set(\n hostId,\n Aglyn.hostPublicOrigin({\n cname: hostSnapshot?.get('cname'),\n subdomain: hostSnapshot?.get('subdomain'),\n }) ?? '',\n )\n }\n if (!entitledHosts.get(hostId)) continue\n let loaded = templateCache.get(hostId)\n if (loaded === undefined) {\n loaded = await loadHostEmail(firestore, hostId, 'abandoned-cart')\n templateCache.set(hostId, loaded)\n }\n const designed = loaded\n ? renderLoadedHostEmail(\n loaded,\n { 'cart.url': String(data.resumeUrl ?? '') },\n Aglyn.sanitizeAuthorHtml,\n )\n : null\n /*\n * MARKETING, and `'bulk'` priority, which this sweep is entitled to\n * precisely because it is resumable: a checkout left unstamped below is\n * re-scanned on the next beat, so a refusal means \"not this hour\" rather\n * than a reminder nobody ever gets.\n *\n * The gate adds the unsubscribe header pair and a visible link, checks\n * both suppression lists, and counts this against how much mail the\n * shopper has had from this site today.\n */\n const result = await sendEmail({\n to: String(data.email),\n subject: designed?.subject ?? 'You left something in your cart',\n text:\n designed?.text ||\n 'Your cart is still waiting — pick up where you left off:\\n\\n' +\n `${data.resumeUrl ?? ''}\\n\\n` +\n 'Your items are held but not reserved, so they may sell out.',\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: brandingByHost.get(hostId)?.fromName,\n sendingIdentity: await hostSendingIdentity(hostId, identityByHost),\n audience: 'tenant',\n context: 'abandoned cart',\n priority: 'bulk',\n /*\n * A cart reminder is promotional mail — the \"Promotions and offers\"\n * stream, which is also where a campaign lands by default. Declared so\n * a shopper who left that stream stops receiving these; the gate asks\n * the topic only for callers that name one.\n */\n marketing: {\n hostId,\n siteBase: siteBaseByHost.get(hostId) ?? '',\n topicId: Aglyn.EMAIL_TOPIC_MARKETING,\n },\n })\n /*\n * SKIPPED, NOT DROPPED — the lockdown rule above, applied to the two\n * refusals a later beat can pass.\n *\n * `remindedAtMs` is what retires a checkout from this sweep. Stamping it\n * after the hourly ceiling or the frequency cap refused would turn a\n * deferral into a reminder nobody ever gets; NOT stamping it after a\n * suppression or a rejection would re-read the same doomed row on every\n * beat until it crowded out the checkouts that could still be reminded.\n * `isDeferrableSendResult` is the one place that distinction is made.\n */\n if (isDeferrableSendResult(result)) continue\n // Cost meter (AGL-1438). One reminder per abandoned checkout, triggered\n // by that shopper's own action rather than composed as a broadcast, so\n // it counts toward cost without entering the campaign cap. On the\n // DELIVERED message only: a suppressed recipient produced no message, so\n // there is no cost to record.\n if (result.sent) await meterHostEmail(hostId)\n await docSnapshot.ref\n .set({ remindedAtMs: now }, { merge: true })\n .catch(() => undefined)\n if (result.sent) sent += 1\n }\n return { scanned: openCheckouts.size, sent, skippedLocked }\n}\n\n/**\n * HTTP door for the pass above (AGL-323). Kept `x-cron-secret`-gated so an\n * external scheduler or an operator can still drive it; the job registration\n * in `server.ts` is what makes it actually run.\n */\nexport const processAbandonedHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const cronSecret = process.env.CRON_SECRET\n if (!cronSecret) {\n return res.status(501).json({ error: 'Not configured (CRON_SECRET).' })\n }\n if (req.headers['x-cron-secret'] !== cronSecret) {\n return res.status(401).json({ error: 'Unauthorized' })\n }\n if (!isEmailConfigured()) {\n return res.status(501).json({ error: 'Email is not configured.' })\n }\n try {\n // The manual door asks the same question the beat does (AGL-2495): a\n // forced pass is still a pass, and `x-cron-secret` is not an argument\n // for mailing a suspended site's shoppers.\n return res.status(200).json(await scanAbandonedCheckouts(pluginJobHostGate()))\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Processing failed' })\n }\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","isDeferrableSendResult","isEmailConfigured","loadHostEmail","renderLoadedHostEmail","sendEmail","pluginJobHostGate","REMIND_AFTER_MS","GIVE_UP_AFTER_MS","scanAbandonedCheckouts","gate","firestore","app","now","Date","openCheckouts","collectionGroup","where","limit","get","sent","entitledHosts","Map","brandingByHost","identityByHost","siteBaseByHost","templateCache","skippedLocked","docSnapshot","docs","data","hostId","ref","parent","id","isLocked","createdAtMs","Number","email","remindedAtMs","set","status","merge","catch","undefined","has","org","checkEntitlement","resolveBrandingProfile","hostSnapshot","collection","doc","hostPublicOrigin","cname","subdomain","loaded","designed","String","resumeUrl","sanitizeAuthorHtml","result","to","subject","text","html","fromName","sendingIdentity","audience","context","priority","marketing","siteBase","topicId","EMAIL_TOPIC_MARKETING","scanned","size","processAbandonedHandler","req","res","method","json","error","cronSecret","process","env","CRON_SECRET","headers","console"],"mappings":"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"}
@@ -0,0 +1,42 @@
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, type PluginJobHostGate } from '@aglyn/aglyn/server';
18
+ export interface RestockScanResult {
19
+ scanned: number;
20
+ sent: number;
21
+ /** Alerts left unstamped because their site is locked (AGL-2495). */
22
+ skippedLocked: number;
23
+ }
24
+ /**
25
+ * The back-in-stock pass itself, separated from its HTTP door (AGL-2227).
26
+ *
27
+ * Same finding as `process-abandoned.ts` beside it: `x-cron-secret`-gated and
28
+ * **never invoked by anything**. `commerce/notify-restock` accepts the
29
+ * shopper's "tell me when it's back" from the storefront and writes
30
+ * `hosts/{hostId}/restockAlerts`; this is the only thing that would ever mail
31
+ * them. Shoppers have been entering an address into a queue with no drain.
32
+ *
33
+ * Exported so `server.ts` can put it on the platform job beat.
34
+ */
35
+ export declare function scanRestockAlerts(
36
+ /** The lockdown gate, injected by the caller (AGL-2495). Not optional. */
37
+ gate: PluginJobHostGate): Promise<RestockScanResult>;
38
+ /**
39
+ * HTTP door for the pass above (AGL-326), kept for manual/ops invocation and
40
+ * for the console's "send now" control.
41
+ */
42
+ export declare const processRestockHandler: PluginApiHandler;