@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,1137 @@
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 * as CommerceModel from "../model/index.js";
19
+ import { firebaseAdmin, getOrgForHost, getPluginConfig } from "@aglyn/tenant-data-admin";
20
+ import { captureHostContact } from "@aglyn/tenant-runtime";
21
+ import { connectLinkageIsReady } from "@aglyn/tenant-data-admin/server/stripe-account-mode";
22
+ import { buildRoute, claimAttempt, Route } from "@aglyn/aglyn/server";
23
+ import { alertLowStockCrossing } from "./low-stock.js";
24
+ import { decrementVariantStock } from "./reserve-stock.js";
25
+ import { holdPromotionSlot, promotionHoldKey, settlePromotionSlot } from "./promotion-hold.js";
26
+ import { posMaxDiscountPct } from "../plugin-config.js";
27
+ import { resolveOrgPermissions } from "@aglyn/tenant-runtime/org-permissions";
28
+ /**
29
+ * The settlement claim (AGL-1691) now lives in `@aglyn/aglyn/server`
30
+ * (AGL-1697), because it is the answer for every unkeyed money route and not
31
+ * just this one. Moved whole and unchanged in behaviour — the digest is still
32
+ * `sha256('pos:{hostId}:{key}')`, so keys minted against the local version
33
+ * still resolve. The only difference is the claim document: it carries
34
+ * `scopeId` where this file wrote `hostId`, and no longer writes a
35
+ * `serverTimestamp` sentinel alongside `createdAtMs`, since the shared version
36
+ * takes a plain Firestore-shaped store rather than firebase-admin itself.
37
+ */ /**
38
+ * POS sale (AGL-312): manager-gated, server-priced. Cash sales create a
39
+ * paid `channel: 'pos'` order immediately (with change calculation);
40
+ * card sales mint a Stripe payment link the register shows as a QR
41
+ * (Stripe Terminal hardware can slot in behind the same endpoint
42
+ * later). Inventory decrements per line from the register's location,
43
+ * and sales can post to an open reservation folio instead (AGL-317).
44
+ */ export const posOrderHandler = async (req, res)=>{
45
+ var _req_headers_authorization, _req_body, _body_hostId, _body_payment, _body_cashReceivedCents, _body_customerEmail, _body_reservationId, _body_registerId, _body_couponCode, _body_locationId, _body_discountPct, _ref, _req_headers_idempotencykey;
46
+ if (req.method !== 'POST') {
47
+ return res.status(405).json({
48
+ error: 'Method not allowed'
49
+ });
50
+ }
51
+ const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
52
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
53
+ if (!idToken) return res.status(401).json({
54
+ error: 'Unauthenticated'
55
+ });
56
+ const body = typeof req.body === 'string' ? JSON.parse(req.body) : (_req_body = req.body) != null ? _req_body : {};
57
+ const hostId = String((_body_hostId = body.hostId) != null ? _body_hostId : '');
58
+ const payment = String((_body_payment = body.payment) != null ? _body_payment : 'cash');
59
+ const cashReceivedCents = Math.round(Number((_body_cashReceivedCents = body.cashReceivedCents) != null ? _body_cashReceivedCents : 0));
60
+ const customerEmail = String((_body_customerEmail = body.customerEmail) != null ? _body_customerEmail : '').trim().toLowerCase();
61
+ const reservationId = String((_body_reservationId = body.reservationId) != null ? _body_reservationId : '');
62
+ const registerId = String((_body_registerId = body.registerId) != null ? _body_registerId : '');
63
+ // A code the shopper hands the cashier (AGL-305). The same field name the
64
+ // storefront doors read, so one resolver answers it the same way.
65
+ const couponCode = String((_body_couponCode = body.couponCode) != null ? _body_couponCode : '').trim();
66
+ const locationId = String((_body_locationId = body.locationId) != null ? _body_locationId : '');
67
+ // REFUSED, NOT CLAMPED (AGL-2161). This used to be
68
+ // `Math.min(100, Math.max(0, …))`, so a register asking for `150` rang up a
69
+ // 100% comp and a register asking for `-20` rang up full price — in both
70
+ // cases a sale the operator did not ask for, recorded as though they had,
71
+ // and indistinguishable afterwards from a deliberate one. A clamp is a
72
+ // silent correction of a request nobody can prove was made; the ceiling
73
+ // below is a refusal the register can show its operator.
74
+ //
75
+ // The finiteness test stays and is now also a refusal (AGL-2224 kept its
76
+ // teeth): `discountPct: 'half'` propagated `NaN` through the discount, the
77
+ // platform fee, the tax and `totals.totalCents`, and `NaN` defeats
78
+ // comparison rather than failing it — so the "cash received is short" guard
79
+ // below (`cashReceivedCents < totals.totalCents`) read false and rang up a
80
+ // sale that had taken no money. Answering that with `0` charged full price
81
+ // on a request the route could not read; answering with a 400 says so.
82
+ const rawDiscountPct = Number((_body_discountPct = body.discountPct) != null ? _body_discountPct : 0);
83
+ if (!Number.isFinite(rawDiscountPct) || rawDiscountPct < 0) {
84
+ return res.status(400).json({
85
+ error: 'Invalid discount'
86
+ });
87
+ }
88
+ const discountPct = rawDiscountPct;
89
+ const rawLines = Array.isArray(body.lines) ? body.lines : [];
90
+ // One settlement attempt, minted by the register (AGL-1691). Node lowercases
91
+ // incoming headers, but read both spellings — the plugin API request type
92
+ // makes no promise about casing.
93
+ const idempotencyKey = String((_ref = (_req_headers_idempotencykey = req.headers['idempotency-key']) != null ? _req_headers_idempotencykey : req.headers['Idempotency-Key']) != null ? _ref : '').trim().slice(0, 200);
94
+ if (!hostId || rawLines.length === 0) {
95
+ return res.status(400).json({
96
+ error: 'Missing hostId or lines'
97
+ });
98
+ }
99
+ let claim = null;
100
+ /**
101
+ * The redemption slot this sale reserved (AGL-305), hoisted so every refusal
102
+ * below the claim — and the catch — can hand it back. A throw after the hold
103
+ * is placed is the one path that would otherwise leave a slot standing
104
+ * against the merchant's cap until its TTL lapsed, with no sale to show for
105
+ * it.
106
+ */ let discountSlot = null;
107
+ let discountHoldKey = '';
108
+ const releaseDiscountSlot = async ()=>{
109
+ const slot = discountSlot;
110
+ discountSlot = null;
111
+ discountHoldKey = '';
112
+ if (slot) await slot.release();
113
+ };
114
+ try {
115
+ var _hostSnapshot_get, _storeSettings_get, _taxSettings_origin, _ref1, _ref2;
116
+ var _ownerOrg_org, _ownerOrg_org1;
117
+ const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
118
+ const firestore = firebaseAdmin.app().firestore();
119
+ const hostRef = firestore.collection('hosts').doc(hostId);
120
+ const hostSnapshot = await hostRef.get();
121
+ // AN ALLOWLIST (AGL-2262), matching `cancel-order.ts`, `fulfill-order.ts`
122
+ // and the Firestore rules' own `hostMemberRole(hostId) in
123
+ // ['admin','editor']`. The old `!role || role === 'viewer'` denylist
124
+ // admitted every string that was not literally `viewer` on the route that
125
+ // takes cash, mints a card QR and decrements real inventory.
126
+ // The union has since GROWN: `HostAccessRole` is `admin | editor | author
127
+ // | viewer` (AGL-2334), and `author` is grantable on this host through
128
+ // `/api/hosts/members`. So this allowlist now refuses a role a real
129
+ // projection CAN produce, deliberately — an author edits content and does
130
+ // not take money. That is the whole reason it is an allowlist: a role
131
+ // added later is refused until someone decides otherwise, rather than
132
+ // admitted by the silence of a denylist.
133
+ //
134
+ // What it does NOT decide is WHOSE admin. `memberRoles` is a per-host
135
+ // projection, so a SITE COLLABORATOR granted `admin` on this one site
136
+ // (`hostAccess: { [hostId]: 'admin' }`, `allHosts: false`) is
137
+ // indistinguishable here from an org owner. That is correct for ringing a
138
+ // sale — they were invited to run this till — but it means this value
139
+ // alone cannot carry an elevated, manager-only control such as the
140
+ // AGL-2372 oversell override. The discriminator is `orgWide` on the
141
+ // membership resolved just below, not the role string.
142
+ const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
143
+ if (memberRole !== 'admin' && memberRole !== 'editor') {
144
+ return res.status(403).json({
145
+ error: 'Not permitted'
146
+ });
147
+ }
148
+ // THE `managePos` PERMISSION, actually read (AGL-2474). The allowlist
149
+ // above is a HOST-level fact and stays — it is what the Firestore rules
150
+ // enforce and dropping it would widen this route. This is the org-level
151
+ // half: `managePos` was declared by COMMERCE_PERMISSIONS, resolved into
152
+ // every permission map, displayed nowhere and read by nothing, so the
153
+ // capability was inert. Both must hold, matching the marketplace
154
+ // install/publish routes' resolveOrgPermissions + memberRoles pairing.
155
+ //
156
+ // Behaviour is unchanged for anyone who has not been given an explicit
157
+ // override: `managePos` defaults to true for the admin and editor tiers,
158
+ // which is exactly the set the allowlist already admits. What it adds is
159
+ // the ability to REVOKE POS from one admin without demoting them.
160
+ const membership = await resolveOrgPermissions(decoded.uid, {
161
+ hostId
162
+ });
163
+ if (!membership.permissions.managePos) {
164
+ return res.status(403).json({
165
+ error: 'Not permitted'
166
+ });
167
+ }
168
+ const ownerOrg = await getOrgForHost(hostId);
169
+ if (!Aglyn.checkEntitlement(ownerOrg == null ? void 0 : ownerOrg.org, 'pos')) {
170
+ return res.status(403).json({
171
+ error: 'POS requires the Pro plan or above'
172
+ });
173
+ }
174
+ // THE DISCOUNT CEILING (AGL-2161), refused rather than clamped.
175
+ //
176
+ // Enforced here and not in the UI: `pos-page.component.tsx` mirrors the
177
+ // same bound on its input, but that field is a plain `TextField` and the
178
+ // number arrives in the request body — a client bound is a courtesy, and
179
+ // the route is where a discount stops being a suggestion.
180
+ //
181
+ // The ceiling defaults to 100 (see `plugin-config.ts` for why that number
182
+ // is the merchant's to lower and not this fix's to invent). What changed
183
+ // is that exceeding it is now an answer instead of a silent rounding.
184
+ // Resolved for THIS SITE (AGL-428, AGL-1014): a chain that trusts one
185
+ // branch's register with a deeper discount than the rest sets it on that
186
+ // site, and
187
+ // reading only the workspace's answer would refuse the sale the console
188
+ // says is allowed.
189
+ const maxDiscountPct = posMaxDiscountPct(await getPluginConfig(ownerOrg == null ? void 0 : ownerOrg.orgId, 'commerce', {
190
+ hostId
191
+ }));
192
+ if (discountPct > maxDiscountPct) {
193
+ return res.status(403).json({
194
+ error: `This register may discount up to ${maxDiscountPct}%. Ask a ` + 'manager to raise the limit in the commerce settings.',
195
+ maxDiscountPct
196
+ });
197
+ }
198
+ // Register attribution + cap (AGL-472/482): a sale must run through a
199
+ // named register that exists on this host. Creation is quota-gated by
200
+ // the resources route, but that's creation-only — a paid→paid
201
+ // downgrade (e.g. Business 2 → Pro 1) would otherwise keep every
202
+ // existing register transacting. So re-check the cap at sale time:
203
+ // rank the host's registers by creation order and refuse any whose
204
+ // rank is beyond THIS SITE's register cap. Deterministic and
205
+ // self-healing — no data is deleted, and re-upgrading restores them.
206
+ //
207
+ // The cap is per site and so is the check (AGL-1775):
208
+ // `checkHostRegisterQuota` is the plan's cap plus the seats the org has
209
+ // allocated to this host out of the purchased pool. The old
210
+ // `checkQuota(org, 'posRegisters', …)` read the org-level value, which
211
+ // since AGL-1775 carries no pool at all — leaving it here would have
212
+ // stopped sales on registers a merchant is invoiced $89/mo for, which is
213
+ // the expensive direction of getting this wrong.
214
+ if (!registerId) {
215
+ return res.status(400).json({
216
+ error: 'Missing registerId'
217
+ });
218
+ }
219
+ const registerDocs = (await hostRef.collection('registers').get()).docs.map((doc)=>{
220
+ var _ref;
221
+ var _doc_get_toMillis, _doc_get;
222
+ return {
223
+ id: doc.id,
224
+ createdAtMs: (_ref = (_doc_get = doc.get('createdAt')) == null ? void 0 : (_doc_get_toMillis = _doc_get.toMillis) == null ? void 0 : _doc_get_toMillis.call(_doc_get)) != null ? _ref : 0
225
+ };
226
+ }).sort((a, b)=>a.createdAtMs - b.createdAtMs || a.id.localeCompare(b.id));
227
+ const rank = registerDocs.findIndex((r)=>r.id === registerId);
228
+ if (rank < 0) {
229
+ return res.status(404).json({
230
+ error: 'Unknown register'
231
+ });
232
+ }
233
+ if (!Aglyn.checkHostRegisterQuota(ownerOrg == null ? void 0 : ownerOrg.org, hostId, rank).allowed) {
234
+ return res.status(403).json({
235
+ error: 'This register is over your plan’s limit — remove extra ' + 'registers or upgrade in Billing to use it.'
236
+ });
237
+ }
238
+ // Server pricing per line.
239
+ const uniqueIds = [
240
+ ...new Set(rawLines.map((line)=>String(line.productId)))
241
+ ];
242
+ const productSnapshots = await Promise.all(uniqueIds.map((id)=>hostRef.collection('products').doc(id).get()));
243
+ const productsById = new Map(productSnapshots.map((snapshot)=>[
244
+ snapshot.id,
245
+ snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data()) : null
246
+ ]));
247
+ const lineItems = [];
248
+ for (const raw of rawLines){
249
+ var _product_variants_find, _raw_quantity, _variant_options, _variant_options1;
250
+ const product = productsById.get(String(raw.productId));
251
+ if (!product) continue;
252
+ const variant = (_product_variants_find = product.variants.find((item)=>item.id === raw.variantId)) != null ? _product_variants_find : product.variants[0];
253
+ // A variant with no price yet (AGL-2916) is refused with its name, not
254
+ // rung up at nothing.
255
+ if (!CommerceModel.variantHasPrice(variant)) {
256
+ return res.status(400).json({
257
+ error: `Set a price for ${product.name} before selling it.`
258
+ });
259
+ }
260
+ const quantity = Math.max(1, Math.min(99, Math.round(Number((_raw_quantity = raw.quantity) != null ? _raw_quantity : 1))));
261
+ lineItems.push(_extends({
262
+ productId: String(raw.productId)
263
+ }, variant.id !== 'default' ? {
264
+ variantId: variant.id
265
+ } : {}, {
266
+ name: product.name
267
+ }, Object.keys((_variant_options = variant.options) != null ? _variant_options : {}).length ? {
268
+ variantLabel: Object.values((_variant_options1 = variant.options) != null ? _variant_options1 : {}).join(' / ')
269
+ } : {}, variant.sku ? {
270
+ sku: variant.sku
271
+ } : {}, {
272
+ productType: product.type,
273
+ quantity,
274
+ unitAmountCents: Math.round(Number(variant.priceUsd) * 100)
275
+ }));
276
+ }
277
+ if (lineItems.length === 0) {
278
+ return res.status(400).json({
279
+ error: 'No valid lines'
280
+ });
281
+ }
282
+ // WHAT THE COUNT SAYS, said out loud (AGL-2357). Every storefront door
283
+ // gates on `canPurchase`; a register that skips it silently downgrades a
284
+ // merchant's `oversellPolicy: 'deny'` to `backorder` at the till.
285
+ //
286
+ // The rule here is WARN, NEVER BLOCK. A till is the wrong
287
+ // place for a stale number to stop a real sale — the cashier is holding
288
+ // the goods — but a merchant who set "deny" is entitled to have the
289
+ // register say something before it sells past zero. So the shortfall is
290
+ // reported and the sale proceeds: there is no refusal anywhere below this
291
+ // line, and adding one is a separate decision (AGL-2358, the
292
+ // manager-override variant).
293
+ //
294
+ // Computed from the SERVER's product documents, not the register's cached
295
+ // grid, and returned on both tenders. The client shows its own warning as
296
+ // the basket is built (`pos-page.component.tsx`), which is the surface the
297
+ // cashier reads; this is the authoritative one, taken at the moment of
298
+ // sale and available to any other register client.
299
+ const stockWarnings = [];
300
+ for (const line of lineItems){
301
+ var _line_variantId;
302
+ var _product_variants_;
303
+ const product = productsById.get(line.productId);
304
+ if (!product) continue;
305
+ const shortfall = CommerceModel.stockShortfall(product, (_line_variantId = line.variantId) != null ? _line_variantId : (_product_variants_ = product.variants[0]) == null ? void 0 : _product_variants_.id, line.quantity);
306
+ if (!shortfall) continue;
307
+ stockWarnings.push(_extends({
308
+ productId: line.productId
309
+ }, line.variantId ? {
310
+ variantId: line.variantId
311
+ } : {}, {
312
+ name: line.name
313
+ }, line.variantLabel ? {
314
+ variantLabel: line.variantLabel
315
+ } : {}, {
316
+ requested: line.quantity,
317
+ available: shortfall.available
318
+ }));
319
+ }
320
+ const itemsCents = lineItems.reduce((sum, line)=>sum + line.unitAmountCents * line.quantity, 0);
321
+ const cashierDiscountCents = Math.round(itemsCents * discountPct / 100);
322
+ // THE DISCOUNTS HUB, AT THE TILL TOO (AGL-305).
323
+ //
324
+ // The register knew exactly one kind of discount: the cashier's own
325
+ // percentage, bounded by `posMaxDiscountPct` above. A code from the
326
+ // Discounts card — or a store-wide automatic promotion needing no code at
327
+ // all — applied on the storefront and applied nothing here, so a shopper
328
+ // who bought in person paid more than the same shopper buying the same
329
+ // goods online, and the merchant's own advertised sale did not reach their
330
+ // own counter.
331
+ //
332
+ // TWO DIFFERENT MECHANISMS, DELIBERATELY NOT MERGED. The cashier's
333
+ // percentage is discretion — a manager's ceiling governs it because the
334
+ // person holding the till decided it. A hub discount is the MERCHANT's own
335
+ // published promotion, decided in the console; running it through
336
+ // `posMaxDiscountPct` would let a register's discretionary ceiling silently
337
+ // veto a store-wide sale, which is not what that ceiling is for. So they
338
+ // are resolved separately and added, and only the discretionary half is
339
+ // bounded.
340
+ //
341
+ // BEFORE TAX, because the sum lands in `discountCents` and the tax below
342
+ // is already computed on `itemsCents - discountCents` — the position the
343
+ // cashier's discount has always held, and the one the storefront doors
344
+ // reach by their own route.
345
+ const posDiscountLines = lineItems.map((line)=>({
346
+ productId: line.productId,
347
+ amountCents: line.unitAmountCents * line.quantity
348
+ }));
349
+ const hubDiscounts = await hostRef.collection('discounts').limit(100).get();
350
+ const resolvedDiscount = CommerceModel.resolveDiscount(hubDiscounts.docs.map((docSnapshot)=>_extends({}, docSnapshot.data(), {
351
+ $id: docSnapshot.id
352
+ })), _extends({}, couponCode ? {
353
+ code: couponCode
354
+ } : {}, {
355
+ subtotalCents: itemsCents,
356
+ productIds: lineItems.map((line)=>line.productId),
357
+ lines: posDiscountLines
358
+ }));
359
+ // A code the cashier typed that cannot apply is an ANSWER they can read to
360
+ // the shopper, not a silent full-price sale. Above the claim and above
361
+ // every write with the other deterministic refusals, so re-keying the code
362
+ // works on the same button.
363
+ if (resolvedDiscount == null ? void 0 : resolvedDiscount.codeProblem) {
364
+ return res.status(400).json({
365
+ error: resolvedDiscount.codeProblem
366
+ });
367
+ }
368
+ // FREE SHIPPING CANNOT BE APPLIED AT A TILL, AND SAYING SO IS THE POINT.
369
+ //
370
+ // There is no carriage in an in-person sale — no `shipping_options`, no
371
+ // rate, nothing to zero — so this benefit resolves perfectly and is worth
372
+ // nothing here. Falling through would ring the sale at full price with the
373
+ // shopper holding a valid code, which is the exact shape of the defect that
374
+ // charged $7.99 for free shipping. An AUTOMATIC free-shipping promotion is
375
+ // passed over rather than refused: nobody asked for it at the counter, and
376
+ // refusing the sale over a promotion the shopper never invoked would stop
377
+ // the till dead.
378
+ if ((resolvedDiscount == null ? void 0 : resolvedDiscount.benefit.kind) === 'free-shipping') {
379
+ if (couponCode) {
380
+ return res.status(400).json({
381
+ error: 'That code is for free shipping, and there is nothing to ship on ' + 'an in-person sale.'
382
+ });
383
+ }
384
+ }
385
+ const hubDiscountCents = (resolvedDiscount == null ? void 0 : resolvedDiscount.benefit.kind) === 'amount' ? resolvedDiscount.benefit.centsOff : 0;
386
+ const appliedDiscountId = hubDiscountCents ? resolvedDiscount.discountId : '';
387
+ // Capped at the basket: two independent reductions can sum past it, and a
388
+ // negative charge is not a sale. `computeOrderTotals` clamps too — this is
389
+ // here so the tax base and the fee scaling below see the same number the
390
+ // totals will.
391
+ const discountCents = Math.min(itemsCents, cashierDiscountCents + hubDiscountCents);
392
+ // THE PLATFORM FEE (AGL-2110), and its absence was a straight loss.
393
+ //
394
+ // A POS card sale opens a DESTINATION charge
395
+ // (`payment_intent_data[transfer_data][destination]`, below) with no
396
+ // `transfer_data[amount]`, so Stripe transfers the whole charge to the
397
+ // merchant while debiting its own processing fee (2.9% + 30¢) from the
398
+ // PLATFORM's balance. Without an `application_fee_amount` beside it,
399
+ // every in-person card sale moved money OUT of Aglyn — the exact shape
400
+ // `plan-entitlements.ts` documents for a 0% rate on a destination charge
401
+ // (AGL-2071). Online sales have carried the fee since AGL-307; the
402
+ // register never did.
403
+ //
404
+ // PER LINE BY PRODUCT TYPE, summed — byte-for-byte the arithmetic
405
+ // `cart-checkout.ts` uses, because a basket can mix a physical good
406
+ // (Starter 2%) with a digital one (Starter 5%) and a single basket-wide
407
+ // rate would price one of them wrong. A missing `type` reads as
408
+ // `physical`, matching `liftLegacyProduct` and the cart.
409
+ //
410
+ // Scaled by the cashier's discount, again like the cart: the fee is a cut
411
+ // of what the shopper actually paid, not of the list basket.
412
+ // WHETHER a rate applies is a separate fact from what it rounds to
413
+ // (AGL-2256). This path rounds TWICE — once per line, then again when the
414
+ // cashier's discount scales the sum — and floored neither, so a small
415
+ // basket on a low rate produced 0 and the emission guard below read that as
416
+ // "this plan charges no fee" and sent none at all. Every other door floors
417
+ // at `Math.max(1, …)` when the rate is above zero.
418
+ let feeApplies = false;
419
+ const grossFeeCents = lineItems.reduce((sum, line)=>{
420
+ var _line_productType;
421
+ const linePct = Aglyn.resolveTransactionFeePct(ownerOrg == null ? void 0 : ownerOrg.org, (_line_productType = line.productType) != null ? _line_productType : 'physical');
422
+ if (linePct > 0) feeApplies = true;
423
+ return sum + Math.round(line.unitAmountCents * line.quantity * linePct / 100);
424
+ }, 0);
425
+ const chargedItemsCents = Math.max(0, itemsCents - discountCents);
426
+ const scaledFeeCents = itemsCents > 0 ? Math.round(grossFeeCents * chargedItemsCents / itemsCents) : 0;
427
+ // Gated on what the shopper actually pays for goods, so a 100% cashier
428
+ // discount still takes nothing — the fee is a cut of the goods, and there
429
+ // are none left to cut.
430
+ const takeFeeCents = feeApplies && chargedItemsCents > 0 ? Math.max(1, scaledFeeCents) : scaledFeeCents;
431
+ // Origin tax (AGL-285) — in-person sales are origin-based by nature.
432
+ const storeSettings = await hostRef.collection('settings').doc('store').get();
433
+ const taxSettings = (_storeSettings_get = storeSettings.get('tax')) != null ? _storeSettings_get : {};
434
+ // AGL-1999: an unset tax mode is NOT a decision, and it must not sell.
435
+ // `mode` was `undefined` for every store whose owner never opened the
436
+ // Taxes card — the default state of every new storefront — and each
437
+ // branch below tested for a specific string, so no branch was taken, no
438
+ // tax was computed, and the shopper was charged an untaxed total. See
439
+ // `commerce-tax-decision.ts` for why refusing beats defaulting to either
440
+ // mode. `mode: 'none'` is the explicit opt-out and passes straight
441
+ // through.
442
+ const taxDecision = CommerceModel.storefrontTaxDecision({
443
+ settings: taxSettings
444
+ });
445
+ if (taxDecision.kind === 'undecided') {
446
+ return res.status(409).json({
447
+ error: CommerceModel.STOREFRONT_TAX_UNDECIDED_MESSAGE
448
+ });
449
+ }
450
+ // `inPerson`, and it is the whole reason this argument exists (AGL-2145):
451
+ // a store on Stripe Tax is served correctly ONLINE and must not be
452
+ // refused there, but the register sends the basket as one opaque line and
453
+ // sets no `automatic_tax` — it cannot, there is no customer address at a
454
+ // till — and the cash and folio tenders never reach Stripe at all. Every
455
+ // in-person sale at such a store charged ZERO tax, on both tenders, with
456
+ // no refusal and no log: the AGL-1999 defect, still open at the one place
457
+ // a shopper is standing in front of you.
458
+ const taxMisconfigured = CommerceModel.storefrontTaxMisconfiguration(taxSettings, {
459
+ inPerson: true
460
+ });
461
+ if (taxMisconfigured) {
462
+ return res.status(409).json({
463
+ error: taxMisconfigured
464
+ });
465
+ }
466
+ const rate = taxDecision.kind === 'manual' ? CommerceModel.resolveTaxRate(taxSettings, (_taxSettings_origin = taxSettings.origin) != null ? _taxSettings_origin : {}) : null;
467
+ const taxCents = rate && !taxSettings.pricesIncludeTax ? CommerceModel.computeTaxCents(itemsCents - discountCents, rate.pct) : 0;
468
+ /*==========================================
469
+ * THE FEE ATTACHES TO THE SALE, NOT TO THE TENDER (AGL-2111).
470
+ *
471
+ * It used to ride the CARD totals only, and the argument was mechanical:
472
+ * cash and folio never touch Stripe, so there is no charge to take an
473
+ * `application_fee_amount` out of, so record nothing. That left a merchant
474
+ * on a non-zero rate one tap away from paying Aglyn nothing forever —
475
+ * ring every in-person sale as cash. the decision, 2026-08-19: close
476
+ * it. The tender is at most an input to the RATE; it is never a switch
477
+ * that zeroes the fee.
478
+ *
479
+ * ## Cash is genuinely different from card, and the difference argues for
480
+ * ## the SAME rate, not a lower one
481
+ *
482
+ * On a card sale the fee becomes `application_fee_amount` on a DESTINATION
483
+ * charge, and Stripe's own processing (2.9% + 30¢) is debited from the
484
+ * PLATFORM's balance beside it (AGL-2071). At the Starter physical rate of
485
+ * 2% the fee does not even cover that processing — Aglyn nets negative on
486
+ * the card leg. So the take rate demonstrably is NOT a processing
487
+ * pass-through with margin on top; there is no processing component in it
488
+ * to strip out for a tender that skips Stripe.
489
+ *
490
+ * What the fee buys is the same on both tenders: the catalogue that priced
491
+ * the basket, the stock that decremented, the tax that computed, the order
492
+ * record, the receipt, the reporting, the register itself. Aglyn's cost
493
+ * basis on a cash sale is LOWER (no Stripe fee at all), so the same
494
+ * percentage is more profitable there, not less justified.
495
+ *
496
+ * And any gap between tenders is itself the defect: a cheaper cash rate
497
+ * would still pay a merchant to steer customers to cash. Parity is the
498
+ * only rate at which the tender stops being a pricing lever.
499
+ *
500
+ * ## What changes is COLLECTION, not the rate
501
+ *
502
+ * There is still no charge to net it from, so the fee is ACCRUED against
503
+ * the org's month (`offlineFees` below) and swept onto the
504
+ * org's own monthly Aglyn invoice by `report-usage` — option 2 on
505
+ * AGL-2111, the `aglyn_metered_usage` meter that already carries every
506
+ * other overage. `feeCollection` on the order says which of the two routes
507
+ * this sale's fee took, so a merchant reconciling a payout can tell "short
508
+ * by the fee" from "billed for it next month".
509
+ *
510
+ * `feeCents` does not enter `totalCents` (see `computeOrderTotals`), so
511
+ * the shopper pays exactly what they paid before — this moves no price at
512
+ * the till, on either tender.
513
+ *=========================================*/ // STRIPE'S CARD COST, ON THE CARD TENDER ONLY (AGL-2152). `takeFeeCents`
514
+ // above is the platform's advertised take and every tender carries it
515
+ // (AGL-2111). This term is different in kind: it is not a rate, it is the
516
+ // 2.9% + 30¢ Stripe debits from the PLATFORM's balance when a destination
517
+ // charge settles — which the AGL-2111 note beside it already observes the
518
+ // take does not cover. Cash and folio never reach Stripe, so there is no
519
+ // such cost to pass through and adding one would invoice a merchant for a
520
+ // fee nobody paid.
521
+ //
522
+ // The base is exactly what the card runs for: the goods after the
523
+ // cashier's discount, plus the origin tax. A till knows both — there is no
524
+ // shopper-chosen shipping and no Stripe Tax to wait on — so unlike the
525
+ // online doors this one recovers the cost exactly.
526
+ const feeCents = payment === 'link' ? takeFeeCents + Aglyn.storefrontProcessingCostCents(chargedItemsCents + taxCents) : takeFeeCents;
527
+ const totals = CommerceModel.computeOrderTotals(lineItems, {
528
+ discountCents,
529
+ taxCents,
530
+ feeCents
531
+ });
532
+ // Kept as a distinct name because the card branch reads it for the Stripe
533
+ // parameters and the metadata witness; the two are now the same figure by
534
+ // construction, which is the point of this change.
535
+ const cardTotals = totals;
536
+ // How this sale's fee reaches Aglyn. `payout` = netted from the merchant's
537
+ // Stripe transfer at settlement; `invoice` = accrued here and billed on
538
+ // the org's next Aglyn invoice. Written on every POS order that carries a
539
+ // fee, so the collection route is a recorded fact rather than something a
540
+ // reader has to infer from the tender.
541
+ const feeCollection = payment === 'link' ? 'payout' : 'invoice';
542
+ // WHERE THE INVOICED HALF ACCRUES (AGL-2111): one document per org-month,
543
+ // `orgs/{id}/offlineFees/{YYYY-MM}`, read by `report-usage` and added to
544
+ // the org-month's `billedCents` — the same `aglyn_metered_usage` meter
545
+ // that already carries dataset, API and contact overage. Nothing new has
546
+ // to be minted in Stripe for this to bill.
547
+ //
548
+ // Keyed by ORG, not by host: the org is the billing subject (AGL-238), and
549
+ // a merchant with three storefronts gets one line rather than three.
550
+ //
551
+ // An org with no id is not billable and must not silently accrue into
552
+ // `orgs//offlineFees/…`; the sale still rings, and the fee is forgone
553
+ // rather than written somewhere no invoice will ever find it.
554
+ const offlineFeeMonth = offlineFeeMonthKey();
555
+ const offlineFeeOrgId = String((_ref1 = ownerOrg == null ? void 0 : (_ownerOrg_org = ownerOrg.org) == null ? void 0 : _ownerOrg_org.id) != null ? _ref1 : '');
556
+ const offlineFeeRef = offlineFeeOrgId ? firestore.collection('orgs').doc(offlineFeeOrgId).collection('offlineFees').doc(offlineFeeMonth) : null;
557
+ // Deterministic tender rejections run BEFORE the claim is taken, so they
558
+ // never burn the key (AGL-1691): "cash received is short" is a 400 the
559
+ // cashier fixes by taking more cash and pressing the same button, and a
560
+ // claimed key would replay the rejection forever. Hoisted above the card
561
+ // branch so one claim site covers every tender; both are no-ops for `link`.
562
+ if (payment === 'cash' && cashReceivedCents < totals.totalCents) {
563
+ return res.status(400).json({
564
+ error: 'Cash received is short'
565
+ });
566
+ }
567
+ // AGL-1760: the stay must EXIST before the sale is rung, and here is the
568
+ // last place that can still be said. A `folio` tender moves no money at
569
+ // this counter — no card is charged and no cash is taken; the order is
570
+ // written `paid` because the charge is BOOKED against the stay and
571
+ // collected at check-out, at the register, as its own sale. So an unknown
572
+ // reservation is the same class of deterministic, retryable rejection as
573
+ // "cash received is short": the cashier picks the right stay and presses
574
+ // the same button. Sitting above the claim is what keeps the key unburned.
575
+ //
576
+ // The read is not an extra one. AGL-1757 already loaded this document a
577
+ // few lines below, to attribute the sale to its guest; it is HOISTED here
578
+ // rather than added, so a folio sale still costs exactly one reservation
579
+ // read — and that one read now answers both questions.
580
+ //
581
+ // Unlike AGL-1757's, this read is authoritative rather than best-effort:
582
+ // it IS the validation, so a Firestore failure must not be swallowed into
583
+ // "no such stay". It falls to the 500 below — before the claim, before any
584
+ // order — and the cashier retries the same key with nothing taken.
585
+ let folioGuestEmail = '';
586
+ let folioGuestName = '';
587
+ if (payment === 'folio') {
588
+ var _reservationSnapshot_get, _reservationSnapshot_get1;
589
+ if (!reservationId) {
590
+ return res.status(400).json({
591
+ error: 'Pick a reservation'
592
+ });
593
+ }
594
+ const reservationSnapshot = await hostRef.collection('reservations').doc(reservationId).get();
595
+ if (!reservationSnapshot.exists) {
596
+ return res.status(404).json({
597
+ error: 'Unknown reservation'
598
+ });
599
+ }
600
+ // The one POS tender where the customer is already identified by the
601
+ // system — `reserve.ts` populated these from the guest's own booking
602
+ // (AGL-1757). The console walk-in writes `guestEmail: null`, which
603
+ // leaves them empty and correctly attributes the sale to nobody.
604
+ folioGuestEmail = String((_reservationSnapshot_get = reservationSnapshot.get('guestEmail')) != null ? _reservationSnapshot_get : '').trim().toLowerCase();
605
+ folioGuestName = String((_reservationSnapshot_get1 = reservationSnapshot.get('guestName')) != null ? _reservationSnapshot_get1 : '').trim().slice(0, 120);
606
+ }
607
+ // Point of no return: everything past here writes an order or moves money.
608
+ const claimed = await claimAttempt(firestore, {
609
+ kind: 'pos',
610
+ scopeId: hostId,
611
+ orgId: String((_ref2 = ownerOrg == null ? void 0 : (_ownerOrg_org1 = ownerOrg.org) == null ? void 0 : _ownerOrg_org1.id) != null ? _ref2 : ''),
612
+ key: idempotencyKey,
613
+ busyMessage: 'This sale is already being processed'
614
+ });
615
+ if ('replay' in claimed) {
616
+ return res.status(claimed.replay.status).json(claimed.replay.body);
617
+ }
618
+ claim = claimed.claim;
619
+ // THE REDEMPTION SLOT IS RESERVED HERE (AGL-305), so the till cannot
620
+ // outrun a cap.
621
+ //
622
+ // Without it a merchant's "first 50 customers" promotion is uncapped at the
623
+ // counter: nothing on this path ever incremented `redemptions`, so every
624
+ // in-person sale would take the discount and none of them would be counted
625
+ // — the cap would bound the website only. The CASH and FOLIO tenders settle
626
+ // the slot the moment the sale is written, a few lines below, because there
627
+ // is no webhook coming for them; the CARD tender passes it in the session
628
+ // metadata and the `checkout.session.completed` branch settles it, exactly
629
+ // as the storefront doors do.
630
+ //
631
+ // BELOW the claim because the hold is keyed by the attempt: a retry of one
632
+ // sale must re-claim its own slot rather than be refused by it.
633
+ if (appliedDiscountId) {
634
+ const slot = await holdPromotionSlot({
635
+ firestore,
636
+ ref: hostRef.collection('discounts').doc(appliedDiscountId),
637
+ holdKey: promotionHoldKey(claimed.claim.stripeKey),
638
+ label: `pos discount ${appliedDiscountId}`
639
+ });
640
+ if (!slot.ok) {
641
+ // Nothing has been written and no money has moved, so the key goes
642
+ // back and the cashier can re-ring the sale — at the correct price,
643
+ // because `resolveDiscount` counts live holds and stops offering an
644
+ // exhausted promotion.
645
+ await claim.release();
646
+ return res.status(409).json({
647
+ error: couponCode && (resolvedDiscount == null ? void 0 : resolvedDiscount.discount.code) ? CommerceModel.PROMOTION_EXHAUSTED_MESSAGE : CommerceModel.PROMOTION_UNAVAILABLE_MESSAGE
648
+ });
649
+ }
650
+ if (slot.holdKey) {
651
+ discountSlot = slot;
652
+ discountHoldKey = slot.holdKey;
653
+ }
654
+ }
655
+ if (payment === 'link') {
656
+ var _ref3, _session_id;
657
+ var _ownerOrg_org2, _ownerOrg_org3;
658
+ // QR payment link on the merchant account, completed by webhook.
659
+ const ownerProfile = await firestore.collection('profiles').doc(String((_ref3 = ownerOrg == null ? void 0 : (_ownerOrg_org2 = ownerOrg.org) == null ? void 0 : _ownerOrg_org2.ownerUid) != null ? _ref3 : '')).get();
660
+ const accountId = ownerProfile.get('stripeAccountId');
661
+ if (!connectLinkageIsReady({
662
+ accountId,
663
+ chargesEnabled: ownerProfile.get('stripeChargesEnabled'),
664
+ accountLivemode: ownerProfile.get('stripeAccountLivemode')
665
+ }, {
666
+ subject: `POS payment link host ${hostId}`
667
+ })) {
668
+ // The slot is handed back before the refusal (AGL-305): this returns
669
+ // below the hold, and a register that keeps pressing a tender the
670
+ // merchant never enabled would otherwise consume the promotion's cap
671
+ // one attempt at a time without ever selling anything.
672
+ await releaseDiscountSlot();
673
+ return res.status(409).json({
674
+ error: 'Card payments not set up'
675
+ });
676
+ }
677
+ const orderRef = hostRef.collection('orders').doc();
678
+ const counterRef = hostRef.collection('counters').doc('orders');
679
+ await firestore.runTransaction(async (transaction)=>{
680
+ var _counter_get;
681
+ const counter = await transaction.get(counterRef);
682
+ const number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
683
+ transaction.set(counterRef, {
684
+ next: number + 1
685
+ }, {
686
+ merge: true
687
+ });
688
+ transaction.set(orderRef, _extends({
689
+ number,
690
+ status: 'pending',
691
+ channel: 'pos',
692
+ registerId
693
+ }, discountPct > 0 ? {
694
+ discountPct,
695
+ discountBy: decoded.uid
696
+ } : {}, appliedDiscountId ? {
697
+ discountId: appliedDiscountId
698
+ } : {}, locationId ? {
699
+ locationId
700
+ } : {}, {
701
+ lineItems,
702
+ // `feeCents` is what the merchant's payout is short by, and it
703
+ // belongs on the document that records the charge. Every tender
704
+ // carries the fee since AGL-2111; this is the one that is netted
705
+ // from the payout rather than invoiced, and `feeCollection` says so.
706
+ totals: cardTotals,
707
+ // WHICH TAX THIS SALE CARRIED (AGL-2451), from the decision resolved
708
+ // above. The register never reaches `stripe-automatic` — AGL-2145
709
+ // refuses that store in person, because there is no shopper address
710
+ // at a till — so this is `manual` or `none`, and the QR session
711
+ // carries the same figure over as `metadata[taxCents]` for the
712
+ // webhook to restate when the card is actually charged.
713
+ taxMode: CommerceModel.storefrontTaxModeForDecision(taxDecision, taxCents)
714
+ }, feeCents > 0 ? {
715
+ feeCollection
716
+ } : {}, {
717
+ customerEmail: customerEmail || null,
718
+ timeline: [
719
+ {
720
+ atMs: Date.now(),
721
+ event: 'pos-card-pending'
722
+ }
723
+ ],
724
+ createdAtMs: Date.now(),
725
+ createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
726
+ }));
727
+ });
728
+ // Stripe returns the cashier to the console POS page. This was
729
+ // `/{hostDocId}/pos`, the pre-AGL-621/622 shape, so completing a card
730
+ // sale dropped the register on a 404 (AGL-685). POS is the plugin
731
+ // route keyed by org slug + host SUBDOMAIN; server code cannot call
732
+ // useConsoleHostRoute, so resolve them here, and fall back to the
733
+ // origin root rather than a fabricated dead path.
734
+ const posOrigin = `https://${req.headers.host}`;
735
+ const posSubdomain = (await firestore.collection('hostIndex').doc(hostId).get()).get('subdomain');
736
+ const posOrgSlug = ownerOrg == null ? void 0 : (_ownerOrg_org3 = ownerOrg.org) == null ? void 0 : _ownerOrg_org3.slug;
737
+ const posUrl = posOrgSlug && posSubdomain ? `${posOrigin}${buildRoute(Route.HOST_PLUGIN, {
738
+ orgSlug: posOrgSlug,
739
+ host: posSubdomain,
740
+ pluginSlug: 'pos'
741
+ })}` : posOrigin;
742
+ const params = new URLSearchParams(_extends({
743
+ mode: 'payment',
744
+ 'line_items[0][quantity]': '1',
745
+ 'line_items[0][price_data][currency]': 'usd',
746
+ 'line_items[0][price_data][unit_amount]': String(totals.totalCents),
747
+ 'line_items[0][price_data][product_data][name]': 'In-store purchase',
748
+ 'payment_intent_data[transfer_data][destination]': String(accountId)
749
+ }, feeCents > 0 ? {
750
+ 'payment_intent_data[application_fee_amount]': String(feeCents)
751
+ } : {}, {
752
+ success_url: `${posUrl}?paid=1`,
753
+ cancel_url: `${posUrl}?paid=0`,
754
+ 'metadata[type]': 'commerce-draft',
755
+ 'metadata[hostId]': hostId,
756
+ 'metadata[orderId]': orderRef.id,
757
+ // The tax WITNESS (AGL-1953). The whole sale goes over as one
758
+ // "In-store purchase" line at `totals.totalCents`, so the origin tax
759
+ // computed above is invisible to Stripe: `total_details.amount_tax`
760
+ // is 0 and nothing on the session says any part of the charge was
761
+ // tax. The figure survived on the order document, but no downstream
762
+ // reader of the Stripe object could decompose it — including
763
+ // `recordStorefrontTax`, which reads exactly this key and was
764
+ // therefore recording every POS card sale as `taxMode: 'none'`.
765
+ //
766
+ // A witness only: the `commerce-draft` webhook branch does not
767
+ // rebuild totals from the session (it folds in shipping alone), so
768
+ // this cannot double-count against the stored order.
769
+ 'metadata[taxCents]': String(Math.max(0, taxCents)),
770
+ // The fee WITNESS, the twin of the tax one above (AGL-2110): the
771
+ // `commerce-draft` webhook branch that completes this order reads the
772
+ // session, and without this key nothing on the Stripe object says
773
+ // what Aglyn took. Recorded even at zero, so "the plan charges no
774
+ // fee" is legible as a decision rather than as a missing field.
775
+ 'metadata[feeCents]': String(Math.max(0, feeCents))
776
+ }, appliedDiscountId ? {
777
+ 'metadata[discountId]': appliedDiscountId
778
+ } : {}, discountHoldKey ? {
779
+ 'metadata[discountHoldKey]': discountHoldKey
780
+ } : {}));
781
+ const response = await fetch('https://api.stripe.com/v1/checkout/sessions', {
782
+ method: 'POST',
783
+ headers: _extends({
784
+ Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
785
+ 'Content-Type': 'application/x-www-form-urlencoded'
786
+ }, claim.stripeKey ? {
787
+ 'Idempotency-Key': claim.stripeKey
788
+ } : {}),
789
+ body: params.toString()
790
+ });
791
+ const session = await response.json();
792
+ if (!response.ok || !session.url) {
793
+ await orderRef.delete().catch(()=>undefined);
794
+ // The sale did not happen — let the same key be tried again rather
795
+ // than locking this basket out over one flaky moment. The slot goes
796
+ // back with it, or the merchant's cap is one short for a sale nobody
797
+ // made.
798
+ await releaseDiscountSlot();
799
+ await claim.release();
800
+ return res.status(502).json({
801
+ error: 'Payment link failed'
802
+ });
803
+ }
804
+ // The session id, stored the way `draft-order.ts` has always stored it
805
+ // (AGL-2244). Without it a cancelled QR sale has no handle on the Stripe
806
+ // page still sitting on the customer's phone, so `cancel-order.ts` had
807
+ // nothing to expire and the register could take the money twice.
808
+ await orderRef.set({
809
+ checkoutSessionId: (_session_id = session.id) != null ? _session_id : null
810
+ }, {
811
+ merge: true
812
+ }).catch(()=>undefined);
813
+ const cardPayload = _extends({
814
+ orderId: orderRef.id,
815
+ url: session.url,
816
+ totals: cardTotals
817
+ }, stockWarnings.length > 0 ? {
818
+ stockWarnings
819
+ } : {});
820
+ await claim.record(200, cardPayload);
821
+ return res.status(200).json(cardPayload);
822
+ }
823
+ // Cash (or folio) sale: paid immediately. Tender validation for both ran
824
+ // above, before the claim.
825
+ const orderRef = hostRef.collection('orders').doc();
826
+ const counterRef = hostRef.collection('counters').doc('orders');
827
+ // Held in a local so the folio branch below can re-state the timeline with
828
+ // a second event appended rather than blind-writing over this one.
829
+ const paidEvent = {
830
+ atMs: Date.now(),
831
+ event: 'paid',
832
+ detail: payment === 'folio' ? `Charged to reservation ${reservationId}` : `Cash — received $${(cashReceivedCents / 100).toFixed(2)}, ` + `change $${((cashReceivedCents - totals.totalCents) / 100).toFixed(2)}`
833
+ };
834
+ // THE SLOT IS SETTLED HERE (AGL-305), not by a webhook, because none is
835
+ // coming: a cash or folio sale is complete the moment this document is
836
+ // written. Settled BEFORE the write rather than after, so a failure to
837
+ // count the redemption refuses the sale instead of ringing an uncounted
838
+ // one — the direction that keeps the merchant's cap honest. Idempotent
839
+ // under a replayed attempt: the redemption is owed by the presence of the
840
+ // HOLD, so a second pass finds none and takes nothing.
841
+ if (appliedDiscountId && discountHoldKey) {
842
+ const settlement = await settlePromotionSlot({
843
+ firestore,
844
+ ref: hostRef.collection('discounts').doc(appliedDiscountId),
845
+ holdKey: discountHoldKey,
846
+ label: `pos discount ${appliedDiscountId}`
847
+ });
848
+ if (settlement === 'error') {
849
+ await releaseDiscountSlot();
850
+ await claim.release();
851
+ return res.status(409).json({
852
+ error: 'That discount could not be applied — try the sale again.'
853
+ });
854
+ }
855
+ // Counted: this attempt no longer holds anything, so the catch below
856
+ // must not hand back a slot that is already a redemption.
857
+ discountSlot = null;
858
+ discountHoldKey = '';
859
+ }
860
+ await firestore.runTransaction(async (transaction)=>{
861
+ var _counter_get;
862
+ const counter = await transaction.get(counterRef);
863
+ const number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
864
+ transaction.set(counterRef, {
865
+ next: number + 1
866
+ }, {
867
+ merge: true
868
+ });
869
+ transaction.set(orderRef, _extends({
870
+ number,
871
+ status: 'paid',
872
+ channel: 'pos',
873
+ registerId
874
+ }, discountPct > 0 ? {
875
+ discountPct,
876
+ discountBy: decoded.uid
877
+ } : {}, appliedDiscountId ? {
878
+ discountId: appliedDiscountId
879
+ } : {}, locationId ? {
880
+ locationId
881
+ } : {}, {
882
+ lineItems,
883
+ totals,
884
+ // WHICH TAX THIS SALE CARRIED (AGL-2451). Cash and folio never reach
885
+ // Stripe at all, so no Stripe object will ever state this and NOTHING
886
+ // else can supply it later — the decision resolved above is the only
887
+ // witness there will be, and here is the one moment it can be written
888
+ // down.
889
+ taxMode: CommerceModel.storefrontTaxModeForDecision(taxDecision, taxCents)
890
+ }, feeCents > 0 ? {
891
+ feeCollection
892
+ } : {}, {
893
+ customerEmail: customerEmail || null,
894
+ timeline: [
895
+ paidEvent
896
+ ]
897
+ }, payment === 'folio' ? {
898
+ reservationId
899
+ } : {}, {
900
+ createdAtMs: Date.now(),
901
+ createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp()
902
+ }));
903
+ // ACCRUED IN THE SAME TRANSACTION AS THE ORDER (AGL-2111), so an order
904
+ // and the fee it owes cannot diverge: either both land or neither does.
905
+ // A separate write after the commit would drop the accrual on any crash
906
+ // in the window and under-bill silently, which is the failure mode this
907
+ // whole issue is about.
908
+ //
909
+ // `increment` rather than read-modify-write: two registers can ring
910
+ // simultaneously, and a lost update here is lost revenue.
911
+ if (feeCents > 0 && offlineFeeRef) {
912
+ transaction.set(offlineFeeRef, {
913
+ month: offlineFeeMonth,
914
+ feeCents: firebaseAdmin.firestore.FieldValue.increment(feeCents),
915
+ orders: firebaseAdmin.firestore.FieldValue.increment(1)
916
+ }, {
917
+ merge: true
918
+ });
919
+ }
920
+ });
921
+ // Folio (AGL-317): the stay settles the charge at check-out. The guest
922
+ // identity behind `folioGuestEmail`/`folioGuestName` was read above, before
923
+ // the claim (AGL-1757, hoisted by AGL-1760).
924
+ if (payment === 'folio') {
925
+ // AGL-1760: `update()`, never `set(..., { merge: true })`. A merge-set
926
+ // against a missing path CREATES the document, which is how a typo or a
927
+ // stale id used to mint a phantom stay — a `reservations/{id}` doc
928
+ // holding one folio line and nothing else: no guest, no dates, no room,
929
+ // invisible to a picker that filters on `status` and `NaN`-valued to the
930
+ // availability maths that reads `checkInDayMs`. `update()` rejects on a
931
+ // missing document instead of creating it, so the stub is not reachable
932
+ // by any argument. Same defect `a7a2d90df` fixed for bookings; the guard
933
+ // differs because that handler already had a transaction and a status to
934
+ // read, whereas here existence is the only question and `update` IS the
935
+ // check — atomic, and without a second read.
936
+ //
937
+ // The validation above makes a missing stay unreachable in practice.
938
+ // What is left is the race — a manager deleting it in the window between
939
+ // the two — and every other reason a write fails, which the old
940
+ // `.catch(() => undefined)` swallowed indistinguishably. The sale is
941
+ // committed by then, so this cannot refuse; and a paid sale must never be
942
+ // lost, nor an orphaned attribution be silent. So the order stands
943
+ // untouched and says so on its own timeline, which the console order
944
+ // dialog renders. The note claims only what is known — that the line is
945
+ // on no folio — rather than diagnosing why.
946
+ await hostRef.collection('reservations').doc(reservationId).update({
947
+ folio: firebaseAdmin.firestore.FieldValue.arrayUnion({
948
+ orderId: orderRef.id,
949
+ amountCents: totals.totalCents,
950
+ note: lineItems.map((line)=>`${line.quantity}× ${line.name}`).join(', ').slice(0, 120),
951
+ atMs: Date.now()
952
+ })
953
+ }).catch(async (error)=>{
954
+ console.error('Folio append failed', reservationId, error);
955
+ await orderRef.set({
956
+ timeline: [
957
+ paidEvent,
958
+ {
959
+ atMs: Date.now(),
960
+ event: 'folio-unattached',
961
+ detail: `Not recorded on reservation ${reservationId} — this ` + `$${(totals.totalCents / 100).toFixed(2)} charge is on ` + 'no stay’s folio. Collect it at the register.'
962
+ }
963
+ ]
964
+ }, {
965
+ merge: true
966
+ }).catch(()=>undefined);
967
+ });
968
+ }
969
+ // Inventory decrement per line (location-aware, AGL-286).
970
+ for (const line of lineItems){
971
+ var _line_variantId1;
972
+ var _product_variants_1;
973
+ const product = productsById.get(line.productId);
974
+ if (!product) continue;
975
+ const variantId = (_line_variantId1 = line.variantId) != null ? _line_variantId1 : (_product_variants_1 = product.variants[0]) == null ? void 0 : _product_variants_1.id;
976
+ const tracked = product.variants.some((variant)=>variant.id === variantId && variant.inventory != null);
977
+ if (!variantId || !tracked) continue;
978
+ // Two lines of one product must COMPOUND (AGL-1828): the next line
979
+ // starts from these variants, not from the product as first read —
980
+ // recomputing from the original would erase this decrement when the
981
+ // sibling line's merge-set landed, while the ledger below recorded
982
+ // both. Same carry-forward as the webhook's POS card loop (AGL-1825).
983
+ //
984
+ // ATOMIC since AGL-2320, and the register is the channel that needed it
985
+ // most: `productsById` was read at the top of this handler, before the
986
+ // register quota check, the pricing pass, the settlement claim, the order
987
+ // transaction and the folio append. A shopper's webhook landing anywhere
988
+ // in that window had its decrement overwritten by this merge-set — two
989
+ // sales, one unit off the shelf, both in the ledger. The transaction's
990
+ // re-read is what compounds the lines now, and it sees the other
991
+ // channel's committed write too.
992
+ //
993
+ // The register still SELLS whatever is rung up. Refusing goods a cashier
994
+ // is physically holding is a product decision, not this fix's (AGL-2320);
995
+ // what the merchant gets instead is the oversold notification the helper
996
+ // fires when the shelf could not cover the sale.
997
+ const moved = await decrementVariantStock({
998
+ firestore,
999
+ hostRef,
1000
+ hostId,
1001
+ productId: line.productId,
1002
+ variantId,
1003
+ quantity: line.quantity,
1004
+ locationId: locationId || undefined,
1005
+ ledger: {
1006
+ reason: 'sale',
1007
+ orderId: orderRef.id
1008
+ }
1009
+ });
1010
+ if (!moved.before || !moved.after) continue;
1011
+ productsById.set(line.productId, moved.after);
1012
+ // Low-stock crossing alert (AGL-1826): the register — the channel most
1013
+ // likely to be selling down the last few units of physical shelf
1014
+ // stock — used to cross the threshold silently while the buy-now
1015
+ // button alerted. The compounded pair above means two lines of one
1016
+ // product cross exactly once, on the line that breaches; the
1017
+ // `claimAttempt` bounds a replayed settlement.
1018
+ alertLowStockCrossing(hostId, moved.before, moved.after);
1019
+ }
1020
+ // AGL-1757: the register's email box is optional, and a room charge is
1021
+ // exactly the sale nobody stops to re-ask an email for — so a stay's
1022
+ // ancillary revenue, the part a guest house most wants to see per guest,
1023
+ // was the part most likely to be anonymous. Fall back to the reservation's
1024
+ // guest, and prefer a TYPED address when there is one: a cashier
1025
+ // correcting the guest's address should beat stale reservation data.
1026
+ //
1027
+ // The guest's name rides along only when the address we are attributing to
1028
+ // IS the reservation's guest — if the cashier redirected the receipt to
1029
+ // some other address, that person is not the named guest. Passing it is
1030
+ // safe either way: `mergeContactInteraction` keeps an existing name, so
1031
+ // this fills a blank and never clobbers a better one.
1032
+ //
1033
+ // Nothing is invented. A walk-in reservation (`guestEmail: null`) leaves
1034
+ // this empty and no contact is created — the right answer, not a gap. The
1035
+ // other case AGL-1757 handled here, a `reservationId` pointing at nothing,
1036
+ // no longer arrives: AGL-1760 refuses that sale above, before the claim.
1037
+ const contactEmail = customerEmail || folioGuestEmail;
1038
+ const contactName = contactEmail && contactEmail === folioGuestEmail ? folioGuestName : '';
1039
+ if (contactEmail) {
1040
+ // AWAITED, AND ITS FAILURE SWALLOWED (AGL-2473). `void` meant the
1041
+ // contact write was a promise nobody held: a serverless container is
1042
+ // free to freeze the moment the response is written, so on a cold-ish
1043
+ // instance the attribution simply did not happen, silently and
1044
+ // unreproducibly. Awaiting is what makes it durable.
1045
+ //
1046
+ // A BARE `await` would have been worse than the bug. This line sits
1047
+ // PAST the point of no return — the order is committed, the cash is in
1048
+ // the drawer, the stock is decremented — so letting a rejection reach
1049
+ // the `catch` below would answer a completed sale with a 500, release
1050
+ // the settlement claim, and invite the cashier to ring the same basket
1051
+ // again on the same key. A duplicate paid order is a far larger defect
1052
+ // than a missing contact row. So the failure is logged and dropped
1053
+ // here, and the sale still returns 200.
1054
+ await captureHostContact(_extends({
1055
+ hostId,
1056
+ email: contactEmail
1057
+ }, contactName ? {
1058
+ name: contactName
1059
+ } : {}, {
1060
+ source: 'order',
1061
+ // A sale makes a customer of anybody who was not yet one and never
1062
+ // moves anybody back (AGL-2612).
1063
+ initialLifecycleStage: 'customer',
1064
+ // AGL-1748: the amount was formatted into the summary STRING below and
1065
+ // never passed to the field that exists to hold it, so `ltvCents`
1066
+ // counted online sales only — a shop-counter merchant's best customers
1067
+ // all ranked at zero. `totals.totalCents` is what the cashier actually
1068
+ // took (items + tax - discount, from `computeOrderTotals` above), not a
1069
+ // figure re-derived from the product docs.
1070
+ //
1071
+ // Gross of the platform fee and gross of refunds, matching the cart,
1072
+ // buy-now and subscription call sites; see the module note in
1073
+ // `upsert-contact.ts`. Replay safety is the `claimAttempt` above: this
1074
+ // line is past the point of no return, so a keyed double-submit never
1075
+ // reaches it, and a KEYLESS one already wrote a second order document
1076
+ // long before it got here — `purchaseCents` adds no hazard the order
1077
+ // write does not already have.
1078
+ purchaseCents: totals.totalCents,
1079
+ interaction: {
1080
+ refId: orderRef.id,
1081
+ // `source` stays `'order'` for both (AGL-1757): a folio sale is a
1082
+ // shop sale that happens to be settled against a stay, and the
1083
+ // reservation's own `'booking'` interaction already exists. Only the
1084
+ // human-readable summary distinguishes them, so a merchant reading
1085
+ // the timeline can tell a counter sale from a room charge.
1086
+ summary: payment === 'folio' ? `Room charge ($${(totals.totalCents / 100).toFixed(2)})` : `In-store purchase ($${(totals.totalCents / 100).toFixed(2)})`
1087
+ }
1088
+ })).catch((error)=>{
1089
+ console.error('[pos-order] contact upsert failed', error);
1090
+ });
1091
+ }
1092
+ const cashPayload = _extends({
1093
+ orderId: orderRef.id,
1094
+ totals,
1095
+ changeCents: payment === 'cash' ? cashReceivedCents - totals.totalCents : 0
1096
+ }, stockWarnings.length > 0 ? {
1097
+ stockWarnings
1098
+ } : {});
1099
+ await claim.record(200, cashPayload);
1100
+ return res.status(200).json(cashPayload);
1101
+ } catch (error) {
1102
+ console.error(error);
1103
+ // Release on the way out so a transient failure does not strand the key
1104
+ // (AGL-1691). The order write itself may or may not have landed; the
1105
+ // cashier reconciles from the orders list, which is the same position
1106
+ // they were in before this endpoint had a claim at all. The redemption
1107
+ // slot goes back for the same reason, and only while it is still a HOLD —
1108
+ // a cash sale that already settled its slot cleared the local, so this
1109
+ // cannot give back a counted redemption.
1110
+ await releaseDiscountSlot();
1111
+ await (claim == null ? void 0 : claim.release());
1112
+ return res.status(500).json({
1113
+ error: 'Sale failed'
1114
+ });
1115
+ }
1116
+ };
1117
+ /**
1118
+ * The billing month a non-card POS fee accrues into (AGL-2111): `YYYY-MM` in
1119
+ * **UTC**, byte-identical to the key `apps/console/utils/billing-month.ts`
1120
+ * mints and to the twelve other month counters on the platform
1121
+ * (`orgs/{id}/apiUsage/*`, `orgs/{id}/assistUsage/*`, the per-host counters).
1122
+ *
1123
+ * It has to be UTC and it has to be this exact expression: `report-usage`
1124
+ * sweeps `previousMonth()` in UTC and reads the accrual document by that key,
1125
+ * so a local-time month would strand every sale rung in the offset window on a
1126
+ * document no sweep ever looks at — which is uncollected revenue that leaves
1127
+ * no trace. `offline-pos-fee-month-key.spec.ts` pins the two against each
1128
+ * other rather than trusting the comment.
1129
+ *
1130
+ * Local rather than imported because `apps/console` is an app: a plugin
1131
+ * library cannot import from it, and the repo's other eleven copies of this
1132
+ * one-liner are the established shape.
1133
+ */ export function offlineFeeMonthKey(now = new Date()) {
1134
+ return now.toISOString().slice(0, 7);
1135
+ }
1136
+
1137
+ //# sourceMappingURL=pos-order.js.map