@aglyn/plugins-commerce 1.0.0-beta.232 → 1.0.0-beta.234

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 (228) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/cart-extras.d.ts +6 -0
  3. package/src/lib/components/cart-extras.js +4 -1
  4. package/src/lib/components/cart-extras.js.map +1 -1
  5. package/src/lib/components/cart-fulfillment.d.ts +117 -0
  6. package/src/lib/components/cart-fulfillment.js +367 -0
  7. package/src/lib/components/cart-fulfillment.js.map +1 -0
  8. package/src/lib/components/cart.js +52 -10
  9. package/src/lib/components/cart.js.map +1 -1
  10. package/src/lib/components/commerce-console-page.js +42 -1
  11. package/src/lib/components/commerce-console-page.js.map +1 -1
  12. package/src/lib/components/console/host-orders-card.component.js +38 -10
  13. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  14. package/src/lib/components/console/local-delivery-settings-card.component.d.ts +37 -0
  15. package/src/lib/components/console/local-delivery-settings-card.component.js +542 -0
  16. package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -0
  17. package/src/lib/components/console/locations-card.component.js +177 -3
  18. package/src/lib/components/console/locations-card.component.js.map +1 -1
  19. package/src/lib/components/console/order-detail-dialog.component.js +10 -0
  20. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  21. package/src/lib/components/console/order-local-fulfillment-panel.component.d.ts +44 -0
  22. package/src/lib/components/console/order-local-fulfillment-panel.component.js +265 -0
  23. package/src/lib/components/console/order-local-fulfillment-panel.component.js.map +1 -0
  24. package/src/lib/components/console/order-offline-notice.component.d.ts +12 -0
  25. package/src/lib/components/console/order-offline-notice.component.js +53 -0
  26. package/src/lib/components/console/order-offline-notice.component.js.map +1 -0
  27. package/src/lib/components/console/pickup-delivery-queue-card.component.d.ts +35 -0
  28. package/src/lib/components/console/pickup-delivery-queue-card.component.js +222 -0
  29. package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -0
  30. package/src/lib/components/console/pos/pos-api.d.ts +24 -1
  31. package/src/lib/components/console/pos/pos-api.js +13 -0
  32. package/src/lib/components/console/pos/pos-api.js.map +1 -1
  33. package/src/lib/components/console/pos/pos-item-dialog.component.d.ts +12 -0
  34. package/src/lib/components/console/pos/pos-item-dialog.component.js +10 -8
  35. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  36. package/src/lib/components/console/pos/pos-kiosk-queue.component.d.ts +22 -0
  37. package/src/lib/components/console/pos/pos-kiosk-queue.component.js +168 -0
  38. package/src/lib/components/console/pos/pos-kiosk-queue.component.js.map +1 -0
  39. package/src/lib/components/console/pos/pos-tender-dialogs.component.d.ts +18 -0
  40. package/src/lib/components/console/pos/pos-tender-dialogs.component.js +116 -1
  41. package/src/lib/components/console/pos/pos-tender-dialogs.component.js.map +1 -1
  42. package/src/lib/components/console/pos/pos-tender-panel.component.d.ts +8 -0
  43. package/src/lib/components/console/pos/pos-tender-panel.component.js +77 -18
  44. package/src/lib/components/console/pos/pos-tender-panel.component.js.map +1 -1
  45. package/src/lib/components/console/pos/use-pos-display.d.ts +5 -2
  46. package/src/lib/components/console/pos/use-pos-display.js +3 -2
  47. package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
  48. package/src/lib/components/console/pos-devices-card.component.js +2 -2
  49. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  50. package/src/lib/components/console/pos-display/pairing-screen.d.ts +5 -2
  51. package/src/lib/components/console/pos-display/pairing-screen.js +7 -5
  52. package/src/lib/components/console/pos-display/pairing-screen.js.map +1 -1
  53. package/src/lib/components/console/pos-display/pos-display-api.d.ts +7 -1
  54. package/src/lib/components/console/pos-display/pos-display-api.js +7 -2
  55. package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
  56. package/src/lib/components/console/pos-kiosk/kiosk-checkout.d.ts +41 -0
  57. package/src/lib/components/console/pos-kiosk/kiosk-checkout.js +275 -0
  58. package/src/lib/components/console/pos-kiosk/kiosk-checkout.js.map +1 -0
  59. package/src/lib/components/console/pos-kiosk/kiosk-menu.d.ts +32 -0
  60. package/src/lib/components/console/pos-kiosk/kiosk-menu.js +344 -0
  61. package/src/lib/components/console/pos-kiosk/kiosk-menu.js.map +1 -0
  62. package/src/lib/components/console/pos-kiosk/kiosk-staff.d.ts +12 -0
  63. package/src/lib/components/console/pos-kiosk/kiosk-staff.js +257 -0
  64. package/src/lib/components/console/pos-kiosk/kiosk-staff.js.map +1 -0
  65. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.d.ts +114 -0
  66. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js +189 -0
  67. package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js.map +1 -0
  68. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.d.ts +16 -0
  69. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js +80 -0
  70. package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js.map +1 -0
  71. package/src/lib/components/console/pos-kiosk-page.component.d.ts +28 -0
  72. package/src/lib/components/console/pos-kiosk-page.component.js +791 -0
  73. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -0
  74. package/src/lib/components/console/pos-offline/pos-offline-api.d.ts +44 -0
  75. package/src/lib/components/console/pos-offline/pos-offline-api.js +69 -0
  76. package/src/lib/components/console/pos-offline/pos-offline-api.js.map +1 -0
  77. package/src/lib/components/console/pos-offline/pos-offline-banner.component.d.ts +7 -0
  78. package/src/lib/components/console/pos-offline/pos-offline-banner.component.js +135 -0
  79. package/src/lib/components/console/pos-offline/pos-offline-banner.component.js.map +1 -0
  80. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.d.ts +32 -0
  81. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js +287 -0
  82. package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js.map +1 -0
  83. package/src/lib/components/console/pos-offline/pos-offline-store.d.ts +63 -0
  84. package/src/lib/components/console/pos-offline/pos-offline-store.js +145 -0
  85. package/src/lib/components/console/pos-offline/pos-offline-store.js.map +1 -0
  86. package/src/lib/components/console/pos-offline/register-offline.d.ts +24 -0
  87. package/src/lib/components/console/pos-offline/register-offline.js +24 -0
  88. package/src/lib/components/console/pos-offline/register-offline.js.map +1 -0
  89. package/src/lib/components/console/pos-offline/use-pos-offline.d.ts +85 -0
  90. package/src/lib/components/console/pos-offline/use-pos-offline.js +542 -0
  91. package/src/lib/components/console/pos-offline/use-pos-offline.js.map +1 -0
  92. package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js +72 -17
  93. package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js.map +1 -1
  94. package/src/lib/components/console/pos-page.component.js +174 -27
  95. package/src/lib/components/console/pos-page.component.js.map +1 -1
  96. package/src/lib/components/console/products-hub-card.component.js +35 -10
  97. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  98. package/src/lib/components/console/shipping-settings-card.component.js +1 -1
  99. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
  100. package/src/lib/components/console/stock-movements-card.component.js +48 -16
  101. package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
  102. package/src/lib/components/console/store-zones.d.ts +8 -1
  103. package/src/lib/components/console/store-zones.js +11 -1
  104. package/src/lib/components/console/store-zones.js.map +1 -1
  105. package/src/lib/components/order-status.js +66 -0
  106. package/src/lib/components/order-status.js.map +1 -1
  107. package/src/lib/components/product-detail.js +42 -15
  108. package/src/lib/components/product-detail.js.map +1 -1
  109. package/src/lib/constants/local-fulfillment-list-query.d.ts +48 -0
  110. package/src/lib/constants/local-fulfillment-list-query.js +84 -0
  111. package/src/lib/constants/local-fulfillment-list-query.js.map +1 -0
  112. package/src/lib/constants/orders-list-query.d.ts +7 -2
  113. package/src/lib/constants/orders-list-query.js +89 -8
  114. package/src/lib/constants/orders-list-query.js.map +1 -1
  115. package/src/lib/constants/pos-shift-list-query.d.ts +22 -0
  116. package/src/lib/constants/pos-shift-list-query.js +115 -0
  117. package/src/lib/constants/pos-shift-list-query.js.map +1 -0
  118. package/src/lib/constants/product-list-query.d.ts +6 -1
  119. package/src/lib/constants/product-list-query.js +77 -8
  120. package/src/lib/constants/product-list-query.js.map +1 -1
  121. package/src/lib/constants/stock-movement-query.d.ts +3 -2
  122. package/src/lib/constants/stock-movement-query.js +52 -8
  123. package/src/lib/constants/stock-movement-query.js.map +1 -1
  124. package/src/lib/model/buyer-notifications.d.ts +5 -3
  125. package/src/lib/model/buyer-notifications.js +25 -4
  126. package/src/lib/model/buyer-notifications.js.map +1 -1
  127. package/src/lib/model/commerce-orders.d.ts +59 -0
  128. package/src/lib/model/commerce-orders.js.map +1 -1
  129. package/src/lib/model/commerce-pos-kiosk.d.ts +175 -0
  130. package/src/lib/model/commerce-pos-kiosk.js +147 -0
  131. package/src/lib/model/commerce-pos-kiosk.js.map +1 -0
  132. package/src/lib/model/commerce-pos-offline.d.ts +234 -0
  133. package/src/lib/model/commerce-pos-offline.js +301 -0
  134. package/src/lib/model/commerce-pos-offline.js.map +1 -0
  135. package/src/lib/model/commerce-pos-ops.d.ts +16 -6
  136. package/src/lib/model/commerce-pos-ops.js +12 -4
  137. package/src/lib/model/commerce-pos-ops.js.map +1 -1
  138. package/src/lib/model/commerce-pos.d.ts +13 -1
  139. package/src/lib/model/commerce-pos.js +5 -4
  140. package/src/lib/model/commerce-pos.js.map +1 -1
  141. package/src/lib/model/commerce-stock-holds.d.ts +24 -3
  142. package/src/lib/model/commerce-stock-holds.js +26 -8
  143. package/src/lib/model/commerce-stock-holds.js.map +1 -1
  144. package/src/lib/model/commerce.d.ts +3 -0
  145. package/src/lib/model/commerce.js.map +1 -1
  146. package/src/lib/model/index.d.ts +4 -0
  147. package/src/lib/model/index.js +4 -0
  148. package/src/lib/model/index.js.map +1 -1
  149. package/src/lib/model/local-fulfillment-settings.d.ts +178 -0
  150. package/src/lib/model/local-fulfillment-settings.js +361 -0
  151. package/src/lib/model/local-fulfillment-settings.js.map +1 -0
  152. package/src/lib/model/order-list-fields.d.ts +10 -1
  153. package/src/lib/model/order-list-fields.js +1 -0
  154. package/src/lib/model/order-list-fields.js.map +1 -1
  155. package/src/lib/model/order-local-fulfillment.d.ts +131 -0
  156. package/src/lib/model/order-local-fulfillment.js +125 -0
  157. package/src/lib/model/order-local-fulfillment.js.map +1 -0
  158. package/src/lib/model/order-shipping-export.js +4 -0
  159. package/src/lib/model/order-shipping-export.js.map +1 -1
  160. package/src/lib/model/order-status-view.d.ts +20 -1
  161. package/src/lib/model/order-status-view.js +60 -6
  162. package/src/lib/model/order-status-view.js.map +1 -1
  163. package/src/lib/plugin-config.js +6 -2
  164. package/src/lib/plugin-config.js.map +1 -1
  165. package/src/lib/plugin.js +8 -0
  166. package/src/lib/plugin.js.map +1 -1
  167. package/src/lib/pos-kiosk-config.d.ts +26 -0
  168. package/src/lib/pos-kiosk-config.js +59 -0
  169. package/src/lib/pos-kiosk-config.js.map +1 -0
  170. package/src/lib/server/api-v1/openapi.js +8 -0
  171. package/src/lib/server/api-v1/openapi.js.map +1 -1
  172. package/src/lib/server/api-v1/order-view.d.ts +19 -0
  173. package/src/lib/server/api-v1/order-view.js +35 -0
  174. package/src/lib/server/api-v1/order-view.js.map +1 -1
  175. package/src/lib/server/billing-webhook.js +238 -87
  176. package/src/lib/server/billing-webhook.js.map +1 -1
  177. package/src/lib/server/cart-checkout.d.ts +2 -0
  178. package/src/lib/server/cart-checkout.js +173 -13
  179. package/src/lib/server/cart-checkout.js.map +1 -1
  180. package/src/lib/server/checkout-extras.js +18 -4
  181. package/src/lib/server/checkout-extras.js.map +1 -1
  182. package/src/lib/server/checkout.js +71 -14
  183. package/src/lib/server/checkout.js.map +1 -1
  184. package/src/lib/server/local-fulfillment-status.d.ts +86 -0
  185. package/src/lib/server/local-fulfillment-status.js +379 -0
  186. package/src/lib/server/local-fulfillment-status.js.map +1 -0
  187. package/src/lib/server/local-fulfillment.d.ts +190 -0
  188. package/src/lib/server/local-fulfillment.js +580 -0
  189. package/src/lib/server/local-fulfillment.js.map +1 -0
  190. package/src/lib/server/membership-register.js +3 -1
  191. package/src/lib/server/membership-register.js.map +1 -1
  192. package/src/lib/server/order-notifications.js +61 -0
  193. package/src/lib/server/order-notifications.js.map +1 -1
  194. package/src/lib/server/pos-display.d.ts +27 -1
  195. package/src/lib/server/pos-display.js +50 -9
  196. package/src/lib/server/pos-display.js.map +1 -1
  197. package/src/lib/server/pos-kiosk-principal.d.ts +29 -0
  198. package/src/lib/server/pos-kiosk-principal.js +42 -0
  199. package/src/lib/server/pos-kiosk-principal.js.map +1 -0
  200. package/src/lib/server/pos-kiosk.d.ts +54 -0
  201. package/src/lib/server/pos-kiosk.js +972 -0
  202. package/src/lib/server/pos-kiosk.js.map +1 -0
  203. package/src/lib/server/pos-offline-sync.d.ts +85 -0
  204. package/src/lib/server/pos-offline-sync.js +765 -0
  205. package/src/lib/server/pos-offline-sync.js.map +1 -0
  206. package/src/lib/server/pos-order.js +22 -4
  207. package/src/lib/server/pos-order.js.map +1 -1
  208. package/src/lib/server/pos-payment.d.ts +6 -0
  209. package/src/lib/server/pos-payment.js +225 -6
  210. package/src/lib/server/pos-payment.js.map +1 -1
  211. package/src/lib/server/pos-print.js +5 -0
  212. package/src/lib/server/pos-print.js.map +1 -1
  213. package/src/lib/server/pos-return.js +27 -0
  214. package/src/lib/server/pos-return.js.map +1 -1
  215. package/src/lib/server/pos-sale.d.ts +6 -0
  216. package/src/lib/server/pos-sale.js +2 -0
  217. package/src/lib/server/pos-sale.js.map +1 -1
  218. package/src/lib/server/pos-shift.js +8 -1
  219. package/src/lib/server/pos-shift.js.map +1 -1
  220. package/src/lib/server/refund.js +3 -0
  221. package/src/lib/server/refund.js.map +1 -1
  222. package/src/lib/server/stock-hold.d.ts +9 -1
  223. package/src/lib/server/stock-hold.js +15 -7
  224. package/src/lib/server/stock-hold.js.map +1 -1
  225. package/src/lib/server.js +16 -0
  226. package/src/lib/server.js.map +1 -1
  227. package/src/lib/tenant-emails.js +161 -0
  228. package/src/lib/tenant-emails.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/checkout-extras.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n hasPluginCheckoutExtras,\n quotePluginCheckoutExtras,\n type QuotedPluginCheckoutExtra,\n} from '@aglyn/aglyn/plugin-manager/plugin-checkout-extras'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport * as CommerceModel from '../model'\nimport { readCartId } from './cart-cookie'\n\n/**\n * Optional lines another plugin offers at the cart (AGL-3635) — package\n * protection is the first — through core's checkout-extras seam. Commerce\n * names no insurer: it asks the seam, shows each offer as a box, and charges\n * the ones the buyer ticked as ordinary lines on the session.\n *\n * Asked only for a basket that ships: an extra is about a parcel, and a cart\n * of downloads has none.\n */\n\n/** How long the cart and checkout wait for an offer before selling without it. */\nexport const CHECKOUT_EXTRAS_TIMEOUT_MS = 2_500\n\n/** One cart line, priced, as the extras quote reads it. */\nexport interface CheckoutExtrasLine {\n productId: string\n name: string\n sku?: string\n quantity: number\n unitCents: number\n physical: boolean\n}\n\n/** Every offer for a basket, or none. Never throws. */\nexport async function quoteCartExtras(input: {\n hostId: string\n lines: CheckoutExtrasLine[]\n destination?: { country?: string; postalCode?: string }\n}): Promise<QuotedPluginCheckoutExtra[]> {\n if (!hasPluginCheckoutExtras()) return []\n const lines = input.lines.filter((line) => line.quantity > 0 && line.unitCents >= 0)\n if (!lines.some((line) => line.physical)) return []\n const itemsCents = lines.reduce((sum, line) => sum + line.unitCents * line.quantity, 0)\n if (itemsCents <= 0) return []\n const country = String(input.destination?.country ?? '').trim().toUpperCase().slice(0, 2)\n const postalCode = String(input.destination?.postalCode ?? '').trim().slice(0, 12)\n return quotePluginCheckoutExtras(\n {\n hostId: input.hostId,\n // Every storefront session is charged in dollars today (`cart-checkout.ts`).\n currency: 'usd',\n itemsCents,\n lines: lines.map((line) => ({\n itemId: line.productId,\n name: line.name,\n ...(line.sku ? { sku: line.sku } : {}),\n quantity: line.quantity,\n unitCents: line.unitCents,\n ships: line.physical,\n })),\n ...(country || postalCode\n ? { destination: { ...(country ? { country } : {}), ...(postalCode ? { postalCode } : {}) } }\n : {}),\n },\n { timeoutMs: CHECKOUT_EXTRAS_TIMEOUT_MS },\n )\n}\n\n/** What the cart shows of an offer: never the quote id. */\nexport interface CartExtraView {\n id: string\n label: string\n description?: string\n amountCents: number\n defaultSelected: boolean\n termsUrl?: string\n}\n\nexport function cartExtraView(extra: QuotedPluginCheckoutExtra): CartExtraView {\n return {\n id: extra.id,\n label: extra.label,\n ...(extra.description ? { description: extra.description } : {}),\n amountCents: extra.amountCents,\n defaultSelected: extra.defaultSelected,\n ...(extra.termsUrl ? { termsUrl: extra.termsUrl } : {}),\n }\n}\n\n/**\n * `GET /api/commerce/cart-extras?hostId=…` — the offers for the visitor's own\n * cart (cookie). Asked by the cart when it is SHOWN, never by the badge on\n * every page. A store with no offering plugin answers `[]` without reading a\n * document. `private, no-store`: the answer is this basket's.\n */\nexport const cartExtrasHandler: PluginApiHandler = async (req, res) => {\n res.setHeader('Cache-Control', 'private, no-store')\n if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' })\n const hostId = String(req.query.hostId ?? '')\n if (!isDocumentId(hostId)) return res.status(400).json({ error: 'Missing or invalid hostId' })\n if (!hasPluginCheckoutExtras()) return res.status(200).json({ extras: [] })\n const cartId = readCartId(req.cookies, hostId)\n if (!cartId) return res.status(200).json({ extras: [] })\n try {\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const cart = ((await hostRef.collection('carts').doc(cartId).get()).data() as CommerceModel.HostCart | undefined) ?? {\n lines: [],\n }\n const lines = await resolveCartExtrasLines(hostRef, cart.lines ?? [])\n const extras = await quoteCartExtras({\n hostId,\n lines,\n destination: {\n country: String(req.query.country ?? ''),\n postalCode: String(req.query.postalCode ?? ''),\n },\n })\n return res.status(200).json({ extras: extras.map(cartExtraView) })\n } catch (error) {\n console.error('cart extras failed', error)\n // An offer is never worth a broken cart.\n return res.status(200).json({ extras: [] })\n }\n}\n\n/** The cart's purchasable lines, priced from the product documents. */\nasync function resolveCartExtrasLines(\n hostRef: FirebaseFirestore.DocumentReference,\n cartLines: CommerceModel.CartLine[],\n): Promise<CheckoutExtrasLine[]> {\n const ids = [...new Set(cartLines.map((line) => line.productId))].filter((id) => isDocumentId(id))\n const snapshots = await Promise.all(ids.map((id) => hostRef.collection('products').doc(id).get()))\n const products = new Map(\n snapshots.map((snapshot) => [\n snapshot.id,\n snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data() as never) : null,\n ]),\n )\n const lines: CheckoutExtrasLine[] = []\n for (const line of cartLines) {\n const product = products.get(line.productId)\n if (!product || product.deletedAt || product.status !== 'active') continue\n const variant = line.variantId\n ? product.variants.find((item) => item.id === line.variantId)\n : product.variants[0]\n if (!variant || !CommerceModel.variantHasPrice(variant)) continue\n lines.push({\n productId: line.productId,\n name: product.name,\n ...(variant.sku ? { sku: variant.sku } : {}),\n quantity: Math.max(0, Math.round(Number(line.quantity) || 0)),\n unitCents: Math.round(Number(variant.priceUsd) * 100),\n physical: (product.type ?? 'physical') === 'physical',\n })\n }\n return lines\n}\n"],"names":["hasPluginCheckoutExtras","quotePluginCheckoutExtras","firebaseAdmin","isDocumentId","CommerceModel","readCartId","CHECKOUT_EXTRAS_TIMEOUT_MS","quoteCartExtras","input","lines","filter","line","quantity","unitCents","some","physical","itemsCents","reduce","sum","country","String","destination","trim","toUpperCase","slice","postalCode","hostId","currency","map","itemId","productId","name","sku","ships","timeoutMs","cartExtraView","extra","id","label","description","amountCents","defaultSelected","termsUrl","cartExtrasHandler","req","res","setHeader","method","status","json","error","query","extras","cartId","cookies","cart","hostRef","app","firestore","collection","doc","get","data","resolveCartExtrasLines","console","cartLines","ids","Set","snapshots","Promise","all","products","Map","snapshot","exists","liftLegacyProduct","product","deletedAt","variant","variantId","variants","find","item","variantHasPrice","push","Math","max","round","Number","priceUsd","type"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/checkout-extras.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n hasPluginCheckoutExtras,\n quotePluginCheckoutExtras,\n type QuotedPluginCheckoutExtra,\n} from '@aglyn/aglyn/plugin-manager/plugin-checkout-extras'\nimport {\n hasPluginCheckoutCredits,\n offeredCheckoutCredits,\n} from '@aglyn/aglyn/plugin-manager/plugin-checkout-credits'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport * as CommerceModel from '../model'\nimport { readCartId } from './cart-cookie'\n\n/**\n * Optional lines another plugin offers at the cart (AGL-3635) — package\n * protection is the first — through core's checkout-extras seam. Commerce\n * names no insurer: it asks the seam, shows each offer as a box, and charges\n * the ones the buyer ticked as ordinary lines on the session.\n *\n * Asked only for a basket that ships: an extra is about a parcel, and a cart\n * of downloads has none.\n */\n\n/** How long the cart and checkout wait for an offer before selling without it. */\nexport const CHECKOUT_EXTRAS_TIMEOUT_MS = 2_500\n\n/** One cart line, priced, as the extras quote reads it. */\nexport interface CheckoutExtrasLine {\n productId: string\n name: string\n sku?: string\n quantity: number\n unitCents: number\n physical: boolean\n}\n\n/** Every offer for a basket, or none. Never throws. */\nexport async function quoteCartExtras(input: {\n hostId: string\n lines: CheckoutExtrasLine[]\n destination?: { country?: string; postalCode?: string }\n}): Promise<QuotedPluginCheckoutExtra[]> {\n if (!hasPluginCheckoutExtras()) return []\n const lines = input.lines.filter((line) => line.quantity > 0 && line.unitCents >= 0)\n if (!lines.some((line) => line.physical)) return []\n const itemsCents = lines.reduce((sum, line) => sum + line.unitCents * line.quantity, 0)\n if (itemsCents <= 0) return []\n const country = String(input.destination?.country ?? '').trim().toUpperCase().slice(0, 2)\n const postalCode = String(input.destination?.postalCode ?? '').trim().slice(0, 12)\n return quotePluginCheckoutExtras(\n {\n hostId: input.hostId,\n // Every storefront session is charged in dollars today (`cart-checkout.ts`).\n currency: 'usd',\n itemsCents,\n lines: lines.map((line) => ({\n itemId: line.productId,\n name: line.name,\n ...(line.sku ? { sku: line.sku } : {}),\n quantity: line.quantity,\n unitCents: line.unitCents,\n ships: line.physical,\n })),\n ...(country || postalCode\n ? { destination: { ...(country ? { country } : {}), ...(postalCode ? { postalCode } : {}) } }\n : {}),\n },\n { timeoutMs: CHECKOUT_EXTRAS_TIMEOUT_MS },\n )\n}\n\n/** What the cart shows of an offer: never the quote id. */\nexport interface CartExtraView {\n id: string\n label: string\n description?: string\n amountCents: number\n defaultSelected: boolean\n termsUrl?: string\n}\n\nexport function cartExtraView(extra: QuotedPluginCheckoutExtra): CartExtraView {\n return {\n id: extra.id,\n label: extra.label,\n ...(extra.description ? { description: extra.description } : {}),\n amountCents: extra.amountCents,\n defaultSelected: extra.defaultSelected,\n ...(extra.termsUrl ? { termsUrl: extra.termsUrl } : {}),\n }\n}\n\n/**\n * `GET /api/commerce/cart-extras?hostId=…` — the offers for the visitor's own\n * cart (cookie). Asked by the cart when it is SHOWN, never by the badge on\n * every page. A store with no offering plugin answers `[]` without reading a\n * document. `private, no-store`: the answer is this basket's.\n */\nexport const cartExtrasHandler: PluginApiHandler = async (req, res) => {\n res.setHeader('Cache-Control', 'private, no-store')\n if (req.method !== 'GET') return res.status(405).json({ error: 'Method not allowed' })\n const hostId = String(req.query.hostId ?? '')\n if (!isDocumentId(hostId)) return res.status(400).json({ error: 'Missing or invalid hostId' })\n // Which store-credit codes the cart takes (AGL-3640) — rewards, referral\n // credit — by provider and name only, so the cart draws the field.\n const credits = hasPluginCheckoutCredits()\n ? (await offeredCheckoutCredits({ hostId, channel: 'online' })).map((credit) => ({\n providerId: credit.providerId,\n label: credit.label,\n }))\n : []\n if (!hasPluginCheckoutExtras()) return res.status(200).json({ extras: [], credits })\n const cartId = readCartId(req.cookies, hostId)\n if (!cartId) return res.status(200).json({ extras: [], credits })\n try {\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const cart = ((await hostRef.collection('carts').doc(cartId).get()).data() as CommerceModel.HostCart | undefined) ?? {\n lines: [],\n }\n const lines = await resolveCartExtrasLines(hostRef, cart.lines ?? [])\n const extras = await quoteCartExtras({\n hostId,\n lines,\n destination: {\n country: String(req.query.country ?? ''),\n postalCode: String(req.query.postalCode ?? ''),\n },\n })\n return res.status(200).json({ extras: extras.map(cartExtraView), credits })\n } catch (error) {\n console.error('cart extras failed', error)\n // An offer is never worth a broken cart.\n return res.status(200).json({ extras: [], credits })\n }\n}\n\n/** The cart's purchasable lines, priced from the product documents. */\nasync function resolveCartExtrasLines(\n hostRef: FirebaseFirestore.DocumentReference,\n cartLines: CommerceModel.CartLine[],\n): Promise<CheckoutExtrasLine[]> {\n const ids = [...new Set(cartLines.map((line) => line.productId))].filter((id) => isDocumentId(id))\n const snapshots = await Promise.all(ids.map((id) => hostRef.collection('products').doc(id).get()))\n const products = new Map(\n snapshots.map((snapshot) => [\n snapshot.id,\n snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data() as never) : null,\n ]),\n )\n const lines: CheckoutExtrasLine[] = []\n for (const line of cartLines) {\n const product = products.get(line.productId)\n if (!product || product.deletedAt || product.status !== 'active') continue\n const variant = line.variantId\n ? product.variants.find((item) => item.id === line.variantId)\n : product.variants[0]\n if (!variant || !CommerceModel.variantHasPrice(variant)) continue\n lines.push({\n productId: line.productId,\n name: product.name,\n ...(variant.sku ? { sku: variant.sku } : {}),\n quantity: Math.max(0, Math.round(Number(line.quantity) || 0)),\n unitCents: Math.round(Number(variant.priceUsd) * 100),\n physical: (product.type ?? 'physical') === 'physical',\n })\n }\n return lines\n}\n"],"names":["hasPluginCheckoutExtras","quotePluginCheckoutExtras","hasPluginCheckoutCredits","offeredCheckoutCredits","firebaseAdmin","isDocumentId","CommerceModel","readCartId","CHECKOUT_EXTRAS_TIMEOUT_MS","quoteCartExtras","input","lines","filter","line","quantity","unitCents","some","physical","itemsCents","reduce","sum","country","String","destination","trim","toUpperCase","slice","postalCode","hostId","currency","map","itemId","productId","name","sku","ships","timeoutMs","cartExtraView","extra","id","label","description","amountCents","defaultSelected","termsUrl","cartExtrasHandler","req","res","setHeader","method","status","json","error","query","credits","channel","credit","providerId","extras","cartId","cookies","cart","hostRef","app","firestore","collection","doc","get","data","resolveCartExtrasLines","console","cartLines","ids","Set","snapshots","Promise","all","products","Map","snapshot","exists","liftLegacyProduct","product","deletedAt","variant","variantId","variants","find","item","variantHasPrice","push","Math","max","round","Number","priceUsd","type"],"mappings":"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"}
@@ -28,6 +28,7 @@ import { taxEngineSessionMetadata } from "../model/commerce-tax-engine.js";
28
28
  import { quoteSaleTaxWithEngine } from "./tax-engine-quote.js";
29
29
  import { holdPromotionSlot, promotionHoldKey } from "./promotion-hold.js";
30
30
  import { holdStock, stockHoldKey } from "./stock-hold.js";
31
+ import { appendLocalFulfillmentMetadata, localFulfillmentLocationId, localStockRefusalMessage, planLocalFulfillment, readLocalFulfillmentRequest, readLocalFulfillmentStore } from "./local-fulfillment.js";
31
32
  import { applyNativeCheckoutParams, nativeCheckoutStripeHeaders, readCheckoutSessionPayload, resolveNativeCheckoutMode } from "./native-checkout.js";
32
33
  import { ensureCheckoutDomain } from "./payment-method-domains.js";
33
34
  /**
@@ -106,7 +107,7 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
106
107
  if (held) await held.release();
107
108
  };
108
109
  try {
109
- var _variant_priceUsd, _req_headers_referer, _req_headers_host, _storeSettings_get, _lifted_name, _ref1, _lifted_type, _variant_weightGrams, _lifted_type1, _lifted_type2, _product_name, _ref2;
110
+ var _variant_priceUsd, _req_headers_referer, _req_headers_host, _storeSettings_get, _lifted_type, _ref1, _ref2, _lifted_name, _ref3, _lifted_type1, _variant_weightGrams, _lifted_type2, _lifted_type3, _product_name, _ref4;
110
111
  var _ownerOrg_org, _ownerOrg_org1, _ownerOrg_org2;
111
112
  const firestore = firebaseAdmin.app().firestore();
112
113
  const hostRef = firestore.collection('hosts').doc(hostId);
@@ -324,10 +325,10 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
324
325
  // hub did not already answer for this code.
325
326
  let appliedCoupon = '';
326
327
  if (couponCode && !resolvedDiscount) {
327
- var _ref3;
328
+ var _ref5;
328
329
  const couponSnapshot = await hostRef.collection('coupons').doc(couponCode).get();
329
330
  const coupon = couponSnapshot.data();
330
- const percentOff = Number((_ref3 = coupon == null ? void 0 : coupon.percentOff) != null ? _ref3 : 0);
331
+ const percentOff = Number((_ref5 = coupon == null ? void 0 : coupon.percentOff) != null ? _ref5 : 0);
331
332
  const expired = (coupon == null ? void 0 : coupon.expiresAtMs) != null && Number(coupon.expiresAtMs) < Date.now();
332
333
  // A PRE-FILTER, no longer the enforcement (AGL-2453). It answers early so
333
334
  // an obviously spent code 400s before a claim is minted, and it counts
@@ -381,6 +382,31 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
381
382
  // checkout, AGL-296). Exempt products skip both.
382
383
  const storeSettings = await hostRef.collection('settings').doc('store').get();
383
384
  const taxSettings = (_storeSettings_get = storeSettings.get('tax')) != null ? _storeSettings_get : {};
385
+ // PICKUP OR THE STORE'S OWN DELIVERY (AGL-3624), for a one-time physical
386
+ // sale, decided from the store's settings exactly as the cart decides it.
387
+ // Above the claim, so a refusal keeps the key. See `local-fulfillment.ts`.
388
+ const localRequest = readLocalFulfillmentRequest(body.fulfillment);
389
+ const localPlan = localRequest && ((_lifted_type = lifted.type) != null ? _lifted_type : 'physical') === 'physical' && !isSubscription ? await planLocalFulfillment({
390
+ hostId,
391
+ request: localRequest,
392
+ itemsCents: listUnitAmountCents * quantity,
393
+ hasPhysicalLine: true,
394
+ store: await readLocalFulfillmentStore({
395
+ hostRef,
396
+ storeSettings: (_ref1 = storeSettings.data == null ? void 0 : storeSettings.data.call(storeSettings)) != null ? _ref1 : null,
397
+ org: ownerOrg.org,
398
+ host: (_ref2 = hostSnapshot.data == null ? void 0 : hostSnapshot.data.call(hostSnapshot)) != null ? _ref2 : null
399
+ })
400
+ }) : {
401
+ kind: 'shipping'
402
+ };
403
+ if (localPlan.kind === 'refusal') {
404
+ return res.status(localPlan.status).json(_extends({
405
+ error: localPlan.error
406
+ }, localPlan.changed ? {
407
+ fulfillmentChanged: localPlan.changed
408
+ } : {}));
409
+ }
384
410
  // AGL-1999: an unset tax mode is NOT a decision, and it must not sell.
385
411
  // `mode` was `undefined` for every store whose owner never opened the
386
412
  // Taxes card — the default state of every new storefront — and each
@@ -488,7 +514,7 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
488
514
  const claimed = await claimAttempt(firestore, {
489
515
  kind: 'commerce-checkout',
490
516
  scopeId: hostId,
491
- orgId: String((_ref1 = ownerOrg == null ? void 0 : (_ownerOrg_org1 = ownerOrg.org) == null ? void 0 : _ownerOrg_org1.id) != null ? _ref1 : ''),
517
+ orgId: String((_ref3 = ownerOrg == null ? void 0 : (_ownerOrg_org1 = ownerOrg.org) == null ? void 0 : _ownerOrg_org1.id) != null ? _ref3 : ''),
492
518
  key: idempotencyKey,
493
519
  busyMessage: 'This checkout is already being processed'
494
520
  });
@@ -535,9 +561,12 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
535
561
  quantity
536
562
  }
537
563
  ] : [],
538
- label: `buy-now ${hostId}/${productId}`
564
+ label: `buy-now ${hostId}/${productId}`,
565
+ // At the pickup location, or the one deliveries leave from (AGL-3624).
566
+ locationId: localFulfillmentLocationId(localPlan)
539
567
  });
540
568
  if (!held.ok) {
569
+ var _lifted_name1;
541
570
  // Nothing has been minted yet, so the key goes back and the same
542
571
  // button works when the units return (AGL-1697). `sold-out` is named
543
572
  // separately from an empty shelf: the merchant HAS stock and someone
@@ -546,7 +575,7 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
546
575
  await releaseStock();
547
576
  await claim.release();
548
577
  return res.status(409).json({
549
- error: held.reason === 'sold-out' ? CommerceModel.STOCK_HELD_MESSAGE : 'Sold out'
578
+ error: held.reason === 'sold-out-at-location' ? localStockRefusalMessage(localPlan, String((_lifted_name1 = lifted.name) != null ? _lifted_name1 : '')) : held.reason === 'sold-out' ? CommerceModel.STOCK_HELD_MESSAGE : 'Sold out'
550
579
  });
551
580
  }
552
581
  if (held.holdKey) {
@@ -596,13 +625,13 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
596
625
  // paths quietly diverge. Behaviour here is unchanged.
597
626
  let recurringTaxRateId = '';
598
627
  if (isSubscription && taxCents > 0) {
599
- var _ref4;
600
- recurringTaxRateId = (_ref4 = await resolveManualTaxRateId({
628
+ var _ref6;
629
+ recurringTaxRateId = (_ref6 = await resolveManualTaxRateId({
601
630
  hostRef,
602
631
  taxPct,
603
632
  taxLabel,
604
633
  headers: stripeKeyHeader('tax-rate')
605
- })) != null ? _ref4 : '';
634
+ })) != null ? _ref6 : '';
606
635
  if (!recurringTaxRateId) {
607
636
  // Nothing was sold — hand the key back so the same button works
608
637
  // once Stripe does (AGL-1697).
@@ -661,10 +690,21 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
661
690
  // `shipping_rate_data` carries no country. `planCheckoutShipping` pairs
662
691
  // the two lists, and refuses rather than guessing.
663
692
  const shippingSettings = storeSettings.get('shipping');
664
- const shipsPhysically = ((_lifted_type = lifted.type) != null ? _lifted_type : 'physical') === 'physical' && !isSubscription;
693
+ const shipsPhysically = ((_lifted_type1 = lifted.type) != null ? _lifted_type1 : 'physical') === 'physical' && !isSubscription;
665
694
  // Live carrier rates (AGL-3612) are quoted here, before the session,
666
695
  // for the destination the shopper declared; see `carrier-shipping.ts`.
667
- const shippingPlan = shipsPhysically ? await planCheckoutShippingWithCarriers({
696
+ const shippingPlan = // Collected: nothing to price, no address (AGL-3624).
697
+ localPlan.kind === 'pickup' ? {
698
+ countries: [],
699
+ options: []
700
+ } : localPlan.kind === 'local_delivery' ? {
701
+ countries: [
702
+ localPlan.country
703
+ ],
704
+ options: [
705
+ localPlan.option
706
+ ]
707
+ } : shipsPhysically ? await planCheckoutShippingWithCarriers({
668
708
  hostId,
669
709
  settings: shippingSettings,
670
710
  cart: _extends({
@@ -681,6 +721,19 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
681
721
  countries: CommerceModel.CHECKOUT_SHIPPING_COUNTRIES,
682
722
  options: []
683
723
  };
724
+ // The unnamed `Local pickup` rate gives way to the store's pickup
725
+ // locations, which the shopper chooses before checkout (AGL-3624).
726
+ if (localPlan.kind === 'shipping' && (shippingSettings == null ? void 0 : shippingSettings.localPickup) && shippingPlan.options.some((option)=>option.rateId === CommerceModel.LOCAL_PICKUP_RATE_ID)) {
727
+ var _ref7;
728
+ const { pickupLocations } = await readLocalFulfillmentStore({
729
+ hostRef,
730
+ storeSettings: (_ref7 = storeSettings.data == null ? void 0 : storeSettings.data.call(storeSettings)) != null ? _ref7 : null,
731
+ host: null
732
+ });
733
+ if (pickupLocations.length) {
734
+ shippingPlan.options = shippingPlan.options.filter((option)=>option.rateId !== CommerceModel.LOCAL_PICKUP_RATE_ID);
735
+ }
736
+ }
684
737
  if (shippingPlan.needsPostalCode) {
685
738
  await releaseCouponSlot();
686
739
  await releaseStock();
@@ -769,8 +822,8 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
769
822
  // out on `subscription_data` further down, and `feeCents` here is what
770
823
  // that percent takes of the opening cycle's goods — the figure
771
824
  // `metadata[feeCents]` records as what Aglyn keeps.
772
- const subscriptionFeePercent = isSubscription ? Aglyn.resolveSubscriptionFeePercent(ownerOrg.org, (_lifted_type1 = lifted.type) != null ? _lifted_type1 : 'physical', amountCents) : 0;
773
- const feeCents = isSubscription ? subscriptionFeePercent > 0 ? Math.max(1, Math.round(amountCents * subscriptionFeePercent / 100)) : 0 : Aglyn.resolveTransactionFeeCents(ownerOrg.org, (_lifted_type2 = lifted.type) != null ? _lifted_type2 : 'physical', amountCents, amountCents + taxCents + shippingCeilingCents);
825
+ const subscriptionFeePercent = isSubscription ? Aglyn.resolveSubscriptionFeePercent(ownerOrg.org, (_lifted_type2 = lifted.type) != null ? _lifted_type2 : 'physical', amountCents) : 0;
826
+ const feeCents = isSubscription ? subscriptionFeePercent > 0 ? Math.max(1, Math.round(amountCents * subscriptionFeePercent / 100)) : 0 : Aglyn.resolveTransactionFeeCents(ownerOrg.org, (_lifted_type3 = lifted.type) != null ? _lifted_type3 : 'physical', amountCents, amountCents + taxCents + shippingCeilingCents);
774
827
  // The unit price Stripe actually charges — the coupon is priced INTO it
775
828
  // rather than sent as a Stripe discount, which is why `amount_discount` is
776
829
  // 0 on every buy-now session and the discount has to ride the metadata
@@ -922,6 +975,10 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
922
975
  // to the one this handler built before shipping existed on it. Buy-now has
923
976
  // never collected an address, so declaring one unconditionally would put a
924
977
  // shipping form in front of every shopper on a store that charges none.
978
+ // How the order reaches the buyer (AGL-3624): the webhook routes it.
979
+ if (localPlan.kind === 'pickup' || localPlan.kind === 'local_delivery') {
980
+ appendLocalFulfillmentMetadata(params, localPlan.metadata);
981
+ }
925
982
  if (shippingPlan.options.length > 0) {
926
983
  CommerceModel.appendShippingAddressCollectionParams(params, shippingPlan.countries);
927
984
  CommerceModel.appendCheckoutShippingParams(params, // Free shipping is a zeroed rate, never a coupon — see the cart's note
@@ -947,7 +1004,7 @@ import { ensureCheckoutDomain } from "./payment-method-domains.js";
947
1004
  //
948
1005
  // Off unless the flag is on for this org AND a publishable key exists, so
949
1006
  // the default remains the redirect — see `resolveNativeCheckoutMode`.
950
- const nativeMode = await resolveNativeCheckoutMode(String((_ref2 = ownerOrg == null ? void 0 : (_ownerOrg_org2 = ownerOrg.org) == null ? void 0 : _ownerOrg_org2.id) != null ? _ref2 : ''));
1007
+ const nativeMode = await resolveNativeCheckoutMode(String((_ref4 = ownerOrg == null ? void 0 : (_ownerOrg_org2 = ownerOrg.org) == null ? void 0 : _ownerOrg_org2.id) != null ? _ref4 : ''));
951
1008
  if (nativeMode.native) {
952
1009
  applyNativeCheckoutParams(params, `${backUrl}${separator}order=success&session_id={CHECKOUT_SESSION_ID}`);
953
1010
  }