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

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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/local-fulfillment.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 { resolveSiteTimeZone } from '@aglyn/aglyn/app-utils/collection-entry-date'\nimport { normalizePhone } from '@aglyn/aglyn/foundation/definitions/contact.types'\nimport { pluginSmsMessaging } from '@aglyn/aglyn/plugin-manager/plugin-sms-messaging'\nimport { pluginShippingRateQuoter } from '@aglyn/aglyn/plugin-manager/plugin-shipping-rates'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { consumeRateLimit, firebaseAdmin, getOrgForHost } 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 * Buy online, pick up in store, and local delivery at checkout (AGL-3624).\n *\n * The buyer chooses at the CART, before payment — the only place the store\n * still controls what is offered: a Stripe Checkout Session prices shipping\n * from the options it is created with and asks nothing back. So the cart\n * declares one of three ways, and this module turns the declaration into the\n * session it needs, or refuses it in words the cart can show:\n *\n * - `pickup` at one location: no shipping charged, no address collected,\n * the stock reserved at that location, and the order routed there.\n * - `local_delivery`: the store's OWN driver (not a marketplace courier —\n * that is the delivery-apps plugin, which never reads this). The postal\n * code — or, for a distance zone, the address — picks a zone, the zone\n * prices the fee and enforces its minimum, and the buyer books a window.\n * The session carries the fee as its one shipping option and collects an\n * address only in the store's country.\n * - `shipping`: everything as it was.\n *\n * A DECLARATION IS A REQUEST, NEVER A PRICE. The cart sends a location id, a\n * postal code and a window; the fee, the minimum and whether the window is\n * still open are all decided here from the store's own settings, on every\n * attempt.\n *\n * DISTANCE ZONES need both ends on a map, and the repository has no geocoder\n * of its own: the destination is placed by the shipping plugin's address\n * check (core's `core.shipping-rate-quoter` seam) when its provider returns a\n * position. Where none does, a distance zone matches nothing and the buyer is\n * told the store does not deliver there; postal-code zones need nothing.\n */\n\n/** What the cart declares. */\nexport interface LocalFulfillmentRequest {\n method: CommerceModel.OrderFulfillmentMethod\n locationId?: string\n postalCode?: string\n windowStartMs?: number\n /** The street address, asked only when the store has distance zones. */\n address?: { line1?: string; line2?: string; city?: string; state?: string; postalCode?: string }\n /**\n * The mobile number the buyer gave AT THE CART for texts about this pickup\n * or delivery (AGL-3624), in E.164. The only phone an online order's texts\n * go to: asked for that purpose, never taken from an address.\n */\n textPhone?: string\n}\n\n/** Reads the cart's declaration; `null` when it made none (a shipped order). */\nexport function readLocalFulfillmentRequest(value: unknown): LocalFulfillmentRequest | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n const method = raw['method']\n if (method !== 'pickup' && method !== 'local_delivery') return null\n const text = (input: unknown, max: number) => String(input ?? '').trim().slice(0, max)\n const request: LocalFulfillmentRequest = { method }\n const phone = normalizePhone(text(raw['textPhone'], 32))\n if (phone) request.textPhone = phone\n if (method === 'pickup') {\n request.locationId = text(raw['locationId'], 120)\n return request\n }\n const postalCode = text(raw['postalCode'], 16)\n if (postalCode) request.postalCode = postalCode\n const start = Math.round(Number(raw['windowStartMs']))\n if (Number.isFinite(start) && start > 0) request.windowStartMs = start\n const address = raw['address']\n if (address && typeof address === 'object') {\n const parts = address as Record<string, unknown>\n request.address = {\n line1: text(parts['line1'], 120),\n line2: text(parts['line2'], 120),\n city: text(parts['city'], 80),\n state: text(parts['state'], 40),\n postalCode: text(parts['postalCode'], 16) || postalCode,\n }\n }\n return request\n}\n\n/** The store's pickup locations and delivery settings, read once per attempt. */\nexport interface LocalFulfillmentStore {\n pickupLocations: CommerceModel.PickupLocationOption[]\n delivery: CommerceModel.LocalDeliverySettings\n timeZone: string\n}\n\n/** Reads what checkout and the cart need. Never throws: a failed read offers nothing. */\nexport async function readLocalFulfillmentStore(input: {\n hostRef: any\n /** `settings/store` as already read, to save a read. */\n storeSettings?: Record<string, unknown> | null\n org?: { timeZone?: string } | null\n host?: { timeZone?: string } | null\n}): Promise<LocalFulfillmentStore> {\n const { hostRef } = input\n const [locationsSnapshot, settings, host] = await Promise.all([\n hostRef\n .collection('locations')\n .limit(CommerceModel.PICKUP_LOCATIONS_MAX)\n .get()\n .catch(() => null),\n input.storeSettings !== undefined\n ? Promise.resolve(input.storeSettings)\n : hostRef\n .collection('settings')\n .doc('store')\n .get()\n .then((snapshot: any) => (snapshot?.data?.() ?? null) as Record<string, unknown> | null)\n .catch(() => null),\n input.host !== undefined\n ? Promise.resolve(input.host)\n : hostRef\n .get()\n .then((snapshot: any) => (snapshot?.data?.() ?? null) as { timeZone?: string } | null)\n .catch(() => null),\n ])\n const locations = (locationsSnapshot?.docs ?? []).map((snapshot: any) => ({\n ...(snapshot.data() as CommerceModel.InventoryLocation),\n id: String(snapshot.id),\n }))\n return {\n pickupLocations: CommerceModel.pickupLocationOptions(locations),\n delivery: CommerceModel.normalizeLocalDeliverySettings(settings?.['localDelivery']),\n timeZone: resolveSiteTimeZone(input.org ?? null, host ?? null),\n }\n}\n\n/** How long checkout waits for an address to be placed on a map. */\nexport const LOCAL_DELIVERY_GEOCODE_TIMEOUT_MS = 3_000\n\n/**\n * Places an address on the map through the shipping plugin's address check,\n * or answers `null`: no plugin, not available on this site, no position in\n * the answer, or slower than the timeout. Never throws.\n */\nexport async function placeDeliveryAddress(\n hostId: string,\n address: { line1?: string; line2?: string; city?: string; state?: string; postalCode?: string; country: string },\n): Promise<CommerceModel.LocalDeliveryCoordinates | null> {\n if (!address.line1 || !address.postalCode) return null\n const quoter = pluginShippingRateQuoter()\n if (!quoter?.validateAddress) return null\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n const placed = await Promise.race([\n (async () => {\n if (!(await quoter.available(hostId))) return null\n const check = await quoter.validateAddress?.(hostId, {\n line1: address.line1,\n ...(address.line2 ? { line2: address.line2 } : {}),\n ...(address.city ? { city: address.city } : {}),\n ...(address.state ? { state: address.state } : {}),\n postalCode: address.postalCode,\n country: address.country,\n } as never)\n return check?.verdict === 'invalid' ? null : (check?.coordinates ?? null)\n })().catch(() => null),\n new Promise<null>((resolve) => {\n timer = setTimeout(() => resolve(null), LOCAL_DELIVERY_GEOCODE_TIMEOUT_MS)\n }),\n ])\n return placed ?? null\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\n/** The shipping option id a local delivery fee is declared under. */\nexport const LOCAL_DELIVERY_RATE_ID = 'local-delivery'\n\nexport type LocalFulfillmentPlan =\n | { kind: 'shipping' }\n | {\n kind: 'refusal'\n status: 400 | 409\n error: string\n /** What the cart should re-ask: the options changed under the buyer. */\n changed?: 'pickup' | 'delivery'\n }\n | {\n kind: 'pickup'\n location: CommerceModel.PickupLocationOption\n metadata: Record<string, string>\n }\n | {\n kind: 'local_delivery'\n zone: CommerceModel.LocalDeliveryZone\n window: CommerceModel.LocalDeliveryWindow\n feeCents: number\n country: string\n locationId?: string\n option: CommerceModel.ResolvedShippingRate\n metadata: Record<string, string>\n }\n\nconst money = (cents: number) => CommerceModel.formatOrderMoney(cents, 'USD')\n\n/**\n * Turns the cart's declaration into what the session needs, or a refusal.\n * Taken BEFORE any reservation or Stripe call, so a refusal costs nothing\n * and the buyer's same button works once they choose again.\n */\nexport async function planLocalFulfillment(input: {\n hostId: string\n request: LocalFulfillmentRequest | null\n /** The goods, pre-discount — what a minimum and a free-over threshold read. */\n itemsCents: number\n hasPhysicalLine: boolean\n store: LocalFulfillmentStore\n nowMs?: number\n}): Promise<LocalFulfillmentPlan> {\n const { request, store } = input\n // A cart of downloads is not collected or driven anywhere.\n if (!request || request.method === 'shipping' || !input.hasPhysicalLine) return { kind: 'shipping' }\n const nowMs = input.nowMs ?? Date.now()\n\n if (request.method === 'pickup') {\n const location = store.pickupLocations.find((entry) => entry.id === request.locationId)\n if (!location) {\n return {\n kind: 'refusal',\n status: 409,\n error: 'That pickup location is no longer available. Choose another.',\n changed: 'pickup',\n }\n }\n return {\n kind: 'pickup',\n location,\n metadata: {\n fulfillment: 'pickup',\n pickupLocationId: location.id,\n ...(request.textPhone ? { orderTextPhone: request.textPhone } : {}),\n },\n }\n }\n\n const delivery = store.delivery\n if (!CommerceModel.localDeliveryOffered(delivery)) {\n return {\n kind: 'refusal',\n status: 409,\n error: 'Local delivery is not available right now. Choose shipping or pickup.',\n changed: 'delivery',\n }\n }\n const country = String(delivery.country)\n const postalCode = CommerceModel.normalizePostcode(\n request.postalCode ?? request.address?.postalCode,\n country,\n )\n if (!postalCode) {\n return { kind: 'refusal', status: 400, error: 'Enter your postal code for delivery.' }\n }\n let zone = CommerceModel.matchLocalDeliveryZone(delivery, { country, postalCode })\n if (!zone && CommerceModel.localDeliveryHasRadiusZones(delivery)) {\n if (!request.address?.line1) {\n return { kind: 'refusal', status: 400, error: 'Enter your street address for delivery.' }\n }\n const coordinates = await placeDeliveryAddress(input.hostId, {\n ...request.address,\n postalCode,\n country,\n })\n zone = CommerceModel.matchLocalDeliveryZone(delivery, { country, postalCode, coordinates })\n }\n if (!zone) {\n return {\n kind: 'refusal',\n status: 409,\n error: `We don’t deliver to ${postalCode}. Choose shipping or pickup instead.`,\n }\n }\n const shortfall = CommerceModel.localDeliveryMinimumShortfall(zone, input.itemsCents)\n if (shortfall > 0) {\n return {\n kind: 'refusal',\n status: 409,\n error:\n `Delivery to ${postalCode} needs an order of at least ${money(zone.minimumCents ?? 0)}. ` +\n `Add ${money(shortfall)} more, or choose shipping or pickup.`,\n }\n }\n const window = CommerceModel.findLocalDeliveryWindow(delivery, request.windowStartMs, {\n nowMs,\n timeZone: store.timeZone,\n })\n if (!window) {\n return {\n kind: 'refusal',\n status: 409,\n error: 'That delivery time is no longer available. Choose another.',\n changed: 'delivery',\n }\n }\n const feeCents = CommerceModel.localDeliveryFeeCents(zone, input.itemsCents)\n const label = CommerceModel.formatLocalDeliveryWindow(window, store.timeZone)\n return {\n kind: 'local_delivery',\n zone,\n window,\n feeCents,\n country,\n ...(delivery.locationId ? { locationId: delivery.locationId } : {}),\n option: {\n rateId: LOCAL_DELIVERY_RATE_ID,\n name: `Local delivery — ${label}`.slice(0, 100),\n amountCents: feeCents,\n },\n metadata: {\n fulfillment: 'local_delivery',\n deliveryZoneId: zone.id,\n deliveryWindowStartMs: String(window.startMs),\n deliveryWindowEndMs: String(window.endMs),\n deliveryPostalCode: postalCode,\n deliveryFeeCents: String(feeCents),\n ...(delivery.locationId ? { deliveryLocationId: delivery.locationId } : {}),\n ...(request.textPhone ? { orderTextPhone: request.textPhone } : {}),\n },\n }\n}\n\n/** Sets a plan's metadata on a session's form body. */\nexport function appendLocalFulfillmentMetadata(params: URLSearchParams, metadata: Record<string, string>): void {\n for (const [key, value] of Object.entries(metadata)) {\n params.set(`metadata[${key}]`, String(value).slice(0, 480))\n }\n}\n\n/** The location a plan takes its stock from, if any. */\nexport function localFulfillmentLocationId(plan: LocalFulfillmentPlan): string | undefined {\n if (plan.kind === 'pickup') return plan.location.id\n if (plan.kind === 'local_delivery') return plan.locationId\n return undefined\n}\n\n/** A stock refusal for a plan that reserves at one location (AGL-3624). */\nexport function localStockRefusalMessage(plan: LocalFulfillmentPlan, productName: string): string {\n const what = productName ? `\"${productName}\"` : 'An item in your cart'\n if (plan.kind === 'pickup') {\n return `${what} is not in stock at ${plan.location.name}. Choose another location, or have it shipped.`\n }\n return `${what} is not in stock for local delivery. Choose pickup at another location, or have it shipped.`\n}\n\n// ---------------------------------------------------------------------------\n// The order, as the webhook records it\n// ---------------------------------------------------------------------------\n\n/** What a paid session's declaration becomes on its order. */\nexport interface OrderLocalFulfillment {\n fields: {\n fulfillmentMethod: CommerceModel.OrderFulfillmentMethod\n pickup?: CommerceModel.OrderPickup\n localDelivery?: CommerceModel.OrderLocalDelivery\n locationId?: string\n /** The number the buyer asked to be texted at, at the cart. */\n customerPhone?: string\n } & CommerceModel.OrderLocalFulfillmentListFields\n /** The line the receipt adds, so the buyer knows where and when. */\n summary: string\n /** The address entered at payment is outside the zone the fee was charged for. */\n outsideZone: boolean\n}\n\n/**\n * Reads a paid session's `fulfillment` metadata into order fields, or `null`\n * for a shipped order. The location and zone are re-read so the order keeps\n * their names as they were when the buyer paid.\n */\nexport async function orderLocalFulfillmentFromSession(input: {\n hostId: string\n hostRef: any\n metadata: Record<string, unknown> | null | undefined\n /** The address the buyer entered at payment, when the session collected one. */\n shippingAddress?: { postalCode?: string | null; country?: string | null } | null\n createdAtMs: number\n}): Promise<OrderLocalFulfillment | null> {\n const metadata = input.metadata ?? {}\n const method = metadata['fulfillment']\n if (method !== 'pickup' && method !== 'local_delivery') return null\n const textPhone = normalizePhone(String(metadata['orderTextPhone'] ?? ''))\n const phone = textPhone ? { customerPhone: textPhone } : {}\n const org = await getOrgForHost(input.hostId).catch(() => null)\n const store = await readLocalFulfillmentStore({\n hostRef: input.hostRef,\n org: (org?.org ?? null) as { timeZone?: string } | null,\n })\n\n if (method === 'pickup') {\n const locationId = String(metadata['pickupLocationId'] ?? '')\n let location = store.pickupLocations.find((entry) => entry.id === locationId)\n // A location renamed or switched off after the buyer paid: the order\n // still goes there, under the name the location doc now has.\n if (!location && locationId) {\n const snapshot = await input.hostRef\n .collection('locations')\n .doc(locationId)\n .get()\n .catch(() => null)\n const data = (snapshot?.exists ? snapshot.data() : null) as CommerceModel.InventoryLocation | null\n location = {\n id: locationId,\n name: String(data?.name || 'the store'),\n ...(data\n ? { address: CommerceModel.formatPostalAddressLine(data.postalAddress, data.address) || undefined }\n : {}),\n }\n }\n if (!location) return null\n const pickup: CommerceModel.OrderPickup = {\n locationId: location.id,\n locationName: location.name,\n ...(location.address ? { address: location.address } : {}),\n ...(location.instructions ? { instructions: location.instructions } : {}),\n ...(location.hours ? { hours: location.hours } : {}),\n status: 'preparing',\n updatedAtMs: input.createdAtMs,\n }\n const order = { fulfillmentMethod: 'pickup' as const, pickup, createdAtMs: input.createdAtMs }\n const list = CommerceModel.orderLocalFulfillmentListFields(order) as CommerceModel.OrderLocalFulfillmentListFields\n const ready = CommerceModel.pickupReadyWithinLabel(location.readyWithinMinutes)\n return {\n fields: { fulfillmentMethod: 'pickup', pickup, locationId: location.id, ...phone, ...list },\n summary: [\n `Pickup at ${location.name}${location.address ? `, ${location.address}` : ''}.`,\n `We’ll let you know when it’s ready${ready ? ` (${ready.toLowerCase()})` : ''}.`,\n location.instructions ?? '',\n ]\n .filter(Boolean)\n .join(' '),\n outsideZone: false,\n }\n }\n\n const zoneId = String(metadata['deliveryZoneId'] ?? '')\n const zone = (store.delivery.zones ?? []).find((entry) => entry.id === zoneId)\n const windowStartMs = Number(metadata['deliveryWindowStartMs']) || 0\n const windowEndMs = Number(metadata['deliveryWindowEndMs']) || windowStartMs\n const declared = String(metadata['deliveryPostalCode'] ?? '')\n const country = store.delivery.country ?? String(input.shippingAddress?.country ?? '')\n const entered = CommerceModel.normalizePostcode(input.shippingAddress?.postalCode, country)\n // The fee was charged for the code the buyer declared. The address they\n // then typed at payment must be in a zone too — a zone of ANY fee, since\n // the store delivers there — or the store is told.\n const outsideZone = Boolean(\n entered &&\n entered !== declared &&\n !CommerceModel.matchLocalDeliveryZone(store.delivery, { country, postalCode: entered }),\n )\n const windowLabel = windowStartMs\n ? CommerceModel.formatLocalDeliveryWindow({ startMs: windowStartMs, endMs: windowEndMs }, store.timeZone)\n : ''\n const locationId = String(metadata['deliveryLocationId'] ?? '') || undefined\n const localDelivery: CommerceModel.OrderLocalDelivery = {\n zoneId,\n zoneName: zone?.name ?? 'Local delivery',\n feeCents: Math.max(0, Math.round(Number(metadata['deliveryFeeCents']) || 0)),\n windowStartMs,\n windowEndMs,\n ...(windowLabel ? { windowLabel } : {}),\n ...(declared ? { postalCode: declared } : {}),\n ...(locationId ? { locationId } : {}),\n status: 'scheduled',\n ...(outsideZone ? { addressOutsideZone: true } : {}),\n updatedAtMs: input.createdAtMs,\n }\n const order = { fulfillmentMethod: 'local_delivery' as const, localDelivery, createdAtMs: input.createdAtMs }\n const list = CommerceModel.orderLocalFulfillmentListFields(order) as CommerceModel.OrderLocalFulfillmentListFields\n return {\n fields: {\n fulfillmentMethod: 'local_delivery',\n localDelivery,\n ...(locationId ? { locationId } : {}),\n ...phone,\n ...list,\n },\n summary: [\n `Local delivery${windowLabel ? `: ${windowLabel}` : ''}.`,\n store.delivery.instructions ?? '',\n ]\n .filter(Boolean)\n .join(' '),\n outsideZone,\n }\n}\n\n// ---------------------------------------------------------------------------\n// The cart's question: what can this basket be?\n// ---------------------------------------------------------------------------\n\n/** Distance zones place an address through a provider: a visitor may ask this often. */\nconst PLACE_LIMIT = { limit: 20, windowMs: 60 * 60 * 1000 }\n\n/**\n * `POST commerce/local-fulfillment-options` (AGL-3624): what the cart offers\n * besides shipping — the pickup locations and, with a postal code, the\n * delivery zone's fee, minimum and the windows open to book.\n *\n * Public, because a shopper asks it; it returns only what a storefront would\n * print anyway (location names, addresses, hours, fees). It reads the\n * visitor's own cart through the cart cookie and offers nothing for a\n * basket with nothing physical in it.\n */\nexport const localFulfillmentOptionsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST' && req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n let body: Record<string, any>\n try {\n body =\n req.method === 'POST'\n ? typeof req.body === 'string'\n ? JSON.parse(req.body || '{}')\n : (req.body ?? {})\n : ((req.query ?? {}) as Record<string, any>)\n } catch {\n return res.status(400).json({ error: 'Invalid request' })\n }\n const hostId = String(body.hostId ?? '')\n if (!isDocumentId(hostId)) return res.status(400).json({ error: 'Missing or invalid hostId' })\n const nothing = { pickup: [], delivery: null }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const ownerOrg = await getOrgForHost(hostId).catch(() => null)\n if (!ownerOrg?.org || !Aglyn.checkEntitlement(ownerOrg.org as any, 'commerce')) {\n return res.status(200).json(nothing)\n }\n // The visitor's own basket decides whether there is anything to collect\n // — or, from a product's Buy button, that one product (AGL-3624).\n const buyNowProductId = typeof body.productId === 'string' ? body.productId : ''\n let cart: Pick<CommerceModel.HostCart, 'lines'>\n if (buyNowProductId) {\n if (!isDocumentId(buyNowProductId)) return res.status(400).json({ error: 'Invalid productId' })\n cart = {\n lines: [\n {\n productId: buyNowProductId,\n ...(typeof body.variantId === 'string' && body.variantId ? { variantId: body.variantId } : {}),\n quantity: Math.min(99, Math.max(1, Math.round(Number(body.quantity) || 1))),\n },\n ],\n }\n } else {\n const cartId = readCartId(req.cookies, hostId)\n const cartSnapshot = cartId ? await hostRef.collection('carts').doc(cartId).get().catch(() => null) : null\n cart = (cartSnapshot?.data?.() as CommerceModel.HostCart | undefined) ?? { lines: [] }\n }\n const productIds = [...new Set((cart.lines ?? []).map((line) => line.productId))].slice(0, 50)\n const products = await Promise.all(\n productIds.map((id) =>\n hostRef\n .collection('products')\n .doc(id)\n .get()\n .catch(() => null),\n ),\n )\n let itemsCents = 0\n let hasPhysicalLine = false\n for (const line of cart.lines ?? []) {\n const snapshot = products[productIds.indexOf(line.productId)]\n if (!snapshot?.exists) continue\n const product = CommerceModel.liftLegacyProduct(snapshot.data() as any)\n const variant = line.variantId\n ? product.variants.find((item) => item.id === line.variantId)\n : product.variants[0]\n itemsCents += Math.round(Number(variant?.priceUsd ?? 0) * 100) * (Number(line.quantity) || 0)\n if ((product.type ?? 'physical') === 'physical') hasPhysicalLine = true\n }\n if (!hasPhysicalLine) return res.status(200).json(nothing)\n\n const store = await readLocalFulfillmentStore({\n hostRef,\n org: ownerOrg.org as { timeZone?: string },\n })\n const nowMs = Date.now()\n let delivery: Record<string, unknown> | null = null\n if (CommerceModel.localDeliveryOffered(store.delivery)) {\n const settings = store.delivery\n const country = String(settings.country)\n const windows = CommerceModel.upcomingLocalDeliveryWindows(settings, { nowMs, timeZone: store.timeZone }).map(\n (window) => ({\n ...window,\n label: CommerceModel.formatLocalDeliveryWindow(window, store.timeZone),\n }),\n )\n const radius = CommerceModel.localDeliveryHasRadiusZones(settings)\n delivery = {\n country,\n ...(settings.instructions ? { instructions: settings.instructions } : {}),\n needsAddress: radius,\n windows,\n }\n const request = readLocalFulfillmentRequest({\n method: 'local_delivery',\n postalCode: body.postalCode,\n address: body.address,\n })\n const postalCode = CommerceModel.normalizePostcode(request?.postalCode ?? request?.address?.postalCode, country)\n if (postalCode) {\n let zone = CommerceModel.matchLocalDeliveryZone(settings, { country, postalCode })\n if (!zone && radius && request?.address?.line1) {\n const visitor = String(req.socket?.remoteAddress ?? '') || 'no-address'\n const allowed = await consumeRateLimit(`local-delivery:place:${hostId}:${visitor}`, PLACE_LIMIT)\n .then((result) => result.allowed)\n .catch(() => false)\n const coordinates = allowed\n ? await placeDeliveryAddress(hostId, { ...request.address, postalCode, country })\n : null\n zone = CommerceModel.matchLocalDeliveryZone(settings, { country, postalCode, coordinates })\n }\n delivery['quote'] = zone\n ? {\n zoneId: zone.id,\n zoneName: zone.name,\n feeCents: CommerceModel.localDeliveryFeeCents(zone, itemsCents),\n ...(zone.minimumCents ? { minimumCents: zone.minimumCents } : {}),\n ...(zone.freeOverCents ? { freeOverCents: zone.freeOverCents } : {}),\n shortfallCents: CommerceModel.localDeliveryMinimumShortfall(zone, itemsCents),\n }\n : { unavailable: `We don’t deliver to ${postalCode}.` }\n }\n }\n // Whether the cart may offer texts about the pickup or delivery: the\n // platform can send them and the store has not turned them off.\n const toggles = (await hostRef\n .collection('settings')\n .doc('store')\n .get()\n .then((snapshot: any) => snapshot?.get?.('buyerNotifications'))\n .catch(() => null)) as unknown\n const texts = Boolean(\n pluginSmsMessaging()?.isConfigured() && CommerceModel.buyerNotificationEnabled(toggles, 'texts'),\n )\n return res.status(200).json({ pickup: store.pickupLocations, delivery, texts })\n } catch (error) {\n console.error('[local-fulfillment] options failed', hostId, error)\n return res.status(200).json(nothing)\n }\n}\n"],"names":["resolveSiteTimeZone","normalizePhone","pluginSmsMessaging","pluginShippingRateQuoter","Aglyn","consumeRateLimit","firebaseAdmin","getOrgForHost","isDocumentId","CommerceModel","readCartId","readLocalFulfillmentRequest","value","raw","method","text","input","max","String","trim","slice","request","phone","textPhone","locationId","postalCode","start","Math","round","Number","isFinite","windowStartMs","address","parts","line1","line2","city","state","readLocalFulfillmentStore","hostRef","locationsSnapshot","settings","host","Promise","all","collection","limit","PICKUP_LOCATIONS_MAX","get","catch","storeSettings","undefined","resolve","doc","then","snapshot","data","locations","docs","map","id","pickupLocations","pickupLocationOptions","delivery","normalizeLocalDeliverySettings","timeZone","org","LOCAL_DELIVERY_GEOCODE_TIMEOUT_MS","placeDeliveryAddress","hostId","quoter","validateAddress","timer","placed","race","available","check","country","verdict","coordinates","setTimeout","clearTimeout","LOCAL_DELIVERY_RATE_ID","money","cents","formatOrderMoney","planLocalFulfillment","store","hasPhysicalLine","kind","nowMs","Date","now","location","find","entry","status","error","changed","metadata","fulfillment","pickupLocationId","orderTextPhone","localDeliveryOffered","normalizePostcode","zone","matchLocalDeliveryZone","localDeliveryHasRadiusZones","shortfall","localDeliveryMinimumShortfall","itemsCents","minimumCents","window","findLocalDeliveryWindow","feeCents","localDeliveryFeeCents","label","formatLocalDeliveryWindow","option","rateId","name","amountCents","deliveryZoneId","deliveryWindowStartMs","startMs","deliveryWindowEndMs","endMs","deliveryPostalCode","deliveryFeeCents","deliveryLocationId","appendLocalFulfillmentMetadata","params","key","Object","entries","set","localFulfillmentLocationId","plan","localStockRefusalMessage","productName","what","orderLocalFulfillmentFromSession","customerPhone","exists","formatPostalAddressLine","postalAddress","pickup","locationName","instructions","hours","updatedAtMs","createdAtMs","order","fulfillmentMethod","list","orderLocalFulfillmentListFields","ready","pickupReadyWithinLabel","readyWithinMinutes","fields","summary","toLowerCase","filter","Boolean","join","outsideZone","zoneId","zones","windowEndMs","declared","shippingAddress","entered","windowLabel","localDelivery","zoneName","addressOutsideZone","PLACE_LIMIT","windowMs","localFulfillmentOptionsHandler","req","res","body","json","JSON","parse","query","nothing","cart","firestore","app","ownerOrg","checkEntitlement","buyNowProductId","productId","lines","variantId","quantity","min","cartSnapshot","cartId","cookies","productIds","Set","line","products","product","indexOf","liftLegacyProduct","variant","variants","item","priceUsd","type","windows","upcomingLocalDeliveryWindows","radius","needsAddress","visitor","socket","remoteAddress","allowed","result","freeOverCents","shortfallCents","unavailable","toggles","texts","isConfigured","buyerNotificationEnabled","console"],"mappings":"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"}
@@ -131,7 +131,9 @@ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
131
131
  });
132
132
  tx.create(memberRef, _extends({
133
133
  email
134
- }, displayName ? memberNameSearchFields(displayName) : {}, {
134
+ }, displayName ? memberNameSearchFields(displayName) : {
135
+ displayNameLower: null
136
+ }, {
135
137
  // The Site users list's search: the name and the address (AGL-3321).
136
138
  searchTokens: memberSearchTokens(displayName, email),
137
139
  /*
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/membership-register.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 { memberNameSearchFields, memberSearchTokens } from './member-name-search'\nimport {\n checkVisitorRecordCeiling,\n HOST_TOKENS,\n marketingConsentFieldsForHost,\n SITE_MEMBER_CEILING_CODE,\n SITE_MEMBER_UNAVAILABLE_MESSAGE,\n SITE_MEMBERS_MAX_PER_HOST,\n} from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n rearmVisitorRecordCeilingNotice,\n recordVisitorRecordCeilingTrip,\n} from '@aglyn/tenant-data-admin'\nimport { emitHostEvent } from '@aglyn/tenant-runtime'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\nimport {\n CONVERSION_TOUCH_DETAIL,\n resolveConversionTouch,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport {\n hashMemberPassword,\n mintMemberSession,\n setMemberCookie,\n} from './membership'\nimport { memberCredentialRef } from './member-credentials'\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * Site member sign-up (AGL-109): creates the member record and its credential\n * document (the scrypt hash, AGL-3308), doubles as a lead, and signs the\n * visitor in via the session cookie.\n */\nexport const membershipRegisterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const email = String(req.body?.email ?? '')\n .trim()\n .toLowerCase()\n const password = String(req.body?.password ?? '')\n const displayName = String(req.body?.displayName ?? '')\n .trim()\n .slice(0, 80)\n // Explicit opt-in checkbox (AGL-2499) — never inferred from signing up.\n const marketingConsent = req.body?.marketingConsent === true\n if (!hostId) return res.status(400).json({ error: 'Missing host' })\n if (!EMAIL_PATTERN.test(email)) {\n return res.status(400).json({ error: 'Enter a valid email' })\n }\n if (password.length < 8) {\n return res\n .status(400)\n .json({ error: 'Password must be at least 8 characters' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // Member accounts are UNLIMITED on every plan (AGL-889) — no seat or\n // quota check belongs here. Audience monetization happens downstream:\n // contact bands meter the CRM projection (AGL-890) and paid\n // memberships carry the plan's digital transaction fee (AGL-892).\n //\n // `SITE_MEMBERS_MAX_PER_HOST` below is NOT that check and does not make\n // it one (AGL-1529). It is a flat PLATFORM ceiling — same number on every\n // plan, no `OrgEntitlements` key, nothing on the price list — and it\n // exists because this handler is reachable by an ANONYMOUS VISITOR, so\n // without it the collection was bounded only by a per-(host, IP) rate\n // limiter that fails soft and bounds the RATE rather than the TOTAL. An\n // abuse control is not something we sell; \"unlimited member accounts on\n // every plan\" stays literally true.\n const membersRef = hostRef.collection('siteMembers')\n const memberRef = membersRef.doc()\n /*\n * COUNT, DEDUPE AND CREATE IN ONE TRANSACTION (AGL-1529, the AGL-2231\n * treatment).\n *\n * Read-then-decide-then-`set()` is the create-time quota that laundered\n * everywhere else in this repo: N concurrent sign-ups each read the same\n * pre-count, each find room, and each land — and nothing re-counts\n * afterwards, so the extra accounts are permanent. The fix is WHEN the\n * count is evaluated, not the counting rule.\n * `Transaction.get(AggregateQuery)` serialises the count against a\n * concurrent create into this collection: the loser retries, re-reads the\n * higher count and is refused. The duplicate-email read moves inside for\n * free, which also closes the smaller race it always had — two\n * simultaneous sign-ups on one address both saw `empty` and both wrote.\n *\n * ALL READS BEFORE THE WRITE, which Firestore requires.\n *\n * The scrypt hash sits between the last read and the create ON PURPOSE.\n * It is ~100 ms of CPU and it is the most expensive thing in the request,\n * so a refused sign-up must never pay it — hashing before the transaction\n * would hand a flood a CPU amplifier on exactly the path the ceiling\n * exists to contain, and would also be a regression, since today a\n * duplicate-email refusal does not hash either. It is a pure computation,\n * not an effect, so it does not violate the \"nothing happens in a\n * transaction body\" rule the sibling routes state.\n *\n * A refusal is returned as DATA and rendered outside: a body that can run\n * several times must not be the place a response is built.\n */\n // The count the new account was judged against, for the notice re-arm.\n let usedBeforeCreate = 0\n const refusal = await firestore.runTransaction(async (tx) => {\n const existing = await tx.get(membersRef.where('email', '==', email).limit(1))\n if (!existing.empty) return { duplicate: true, ceiling: 0 }\n const used = (await tx.get(membersRef.count())).data().count\n usedBeforeCreate = used\n // Live documents only, so removing a member in the inbox frees the slot.\n const verdict = checkVisitorRecordCeiling(used, SITE_MEMBERS_MAX_PER_HOST)\n if (verdict.exceeded) return { duplicate: false, ceiling: verdict.ceiling }\n // The hash goes to the credential document, which no client can read\n // (AGL-3308), and in this transaction, so a member never exists without\n // the password they chose. The profile below is what the console lists.\n tx.set(memberCredentialRef(hostRef, memberRef.id), {\n passwordScrypt: hashMemberPassword(password),\n })\n tx.create(memberRef, {\n email,\n ...(displayName ? memberNameSearchFields(displayName) : {}),\n // The Site users list's search: the name and the address (AGL-3321).\n searchTokens: memberSearchTokens(displayName, email),\n /*\n * Stored `false`, not left out: the console's Status filter asks\n * `suspended == false` for the active members, and a query cannot\n * find a document that lacks the field. The drawer's Suspend and\n * Reactivate write `true` and `false` over it.\n */\n suspended: false,\n /*\n * The checkbox is PERSISTED on the member, not only forwarded.\n *\n * It reached the lead and the contact from the two lines below and\n * was dropped for this document, so `hosts/{hostId}/siteMembers` had\n * no consent field of any kind and `audience: 'members'` had nothing\n * for the send-time join to read — the audience could not be filtered\n * even in principle (`docs/specs/email-overhaul.md` §1d/§3f). The\n * other two documents are not a substitute: a member is deduped in\n * this transaction while leads append every time, and contacts are\n * ORG-scoped where a member is the site's own.\n *\n * Written only when ticked. Signing up is not opting in — that is why\n * the checkbox exists and why it defaults unchecked — so the omitted\n * case stores nothing and reads back as an unrecorded basis rather\n * than as a refusal.\n *\n * Under this site's key even though `siteMembers` already lives\n * beneath it, so that one reader answers for every silo — see\n * `addHostLead`, which makes the same write for the same reason.\n */\n ...(marketingConsent\n ? marketingConsentFieldsForHost(hostId, Date.now())\n : {}),\n createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n return null\n })\n if (refusal?.duplicate) {\n return res.status(409).json({ error: 'That email is already a member' })\n }\n if (refusal) {\n // Visible to the HOST — the rule that a control nobody can see in\n // the console did not ship. Durable counter + one notification.\n await recordVisitorRecordCeilingTrip({\n hostRef,\n hostId,\n kind: 'siteMembers',\n ceiling: refusal.ceiling,\n })\n // Opaque to the VISITOR, who is a stranger to this site and not our\n // customer (AGL-1666's rules, restated in the message's own docblock).\n // The one genuinely useful thing to hand them is a door that still\n // opens, so the site's OWN published support address rides along when\n // it has one — read through the host-token registry, whose description\n // of this field is literally \"where visitors should write for help\".\n // Nothing else off the host document may leave the console.\n const contact = HOST_TOKENS['supportEmail']?.resolve(\n hostSnapshot.data() as any,\n )\n return res.status(429).json({\n error: SITE_MEMBER_UNAVAILABLE_MESSAGE,\n // Machine-readable, because the dispatcher's rate limiter answers 429\n // too and the status therefore discriminates nothing.\n code: SITE_MEMBER_CEILING_CODE,\n ...(contact ? { contact } : {}),\n })\n }\n // The account took a slot below the ceiling. When it took the last one,\n // the next refusal is a new crossing and is announced again (AGL-3442).\n await rearmVisitorRecordCeilingNotice({\n hostRef,\n kind: 'siteMembers',\n used: usedBeforeCreate,\n ceiling: SITE_MEMBERS_MAX_PER_HOST,\n })\n // Sign-ups double as leads for the site owner (AGL-109), through the one\n // writer that enforces `LEADS_MAX_PER_HOST` (AGL-1529). A refused lead\n // never fails the sign-up: the visitor asked for an account, not for a\n // lead record, and the trip is recorded for the owner either way.\n /*\n * WHERE THE VISITOR ARRIVED FROM, RESOLVED ONCE FOR THE WHOLE SIGN-UP.\n *\n * A sign-up is the identify moment for a visitor who has been anonymous\n * until now, and it writes two records an arrival can be credited with —\n * the lead and the contact. One resolve, by the plugin that credits\n * outcomes, and the two cannot end up credited to different arrivals.\n *\n * The MEMBER record itself is not attributed. A member is an account the\n * visitor holds and the lead is the site's record of the same act, so\n * crediting both would count one sign-up twice under two names.\n */\n const signedUpAtMs = Date.now()\n const arrival = await resolveConversionTouch({\n hostId,\n wire: req.body?.campaignTouch,\n email,\n atMs: signedUpAtMs,\n })\n /*\n * Contacts ingestion (AGL-197), reported to whichever plugin keeps people\n * rather than written by this one (AGL-3080). Commerce knows it just met\n * somebody; what a person record is belongs to the plugin that models it,\n * and a workspace that keeps none is a quiet, correct answer here.\n */\n /*\n * A member account is a RELATIONSHIP (AGL-3232): the person is a\n * contact from this moment, and an open lead the site held for the\n * address is closed as converted onto that contact by the record\n * system — a sign-up files no lead of its own any more, the way a\n * portal user is a contact and not a lead in Salesforce.\n */\n void recordCapturedContact({\n /*\n * Not resolved here. The record system keys a person on the SITE they\n * were met on, so this costs the capture nothing — and resolving it\n * would put a Firestore read in front of the writer, which is the one\n * thing `recordCapturedContact` asks a door not to do.\n */\n orgId: '',\n hostId,\n identity: { email, name: displayName || undefined },\n interaction: {\n source: 'member',\n refId: memberRef.id,\n summary: 'Joined as a member',\n },\n surface: 'relationship',\n // An account is a subscription to the site, not an enquiry (AGL-2612):\n // the stage on the contact says only that the person asked to be\n // kept. A floor, so a customer who opens an account stays a customer.\n lifecycleFloor: 'subscriber',\n ...(marketingConsent ? { marketingConsent: true } : {}),\n // Where the visitor ARRIVED from — a fact about this visit and not\n // about the person, which is what `detail` carries.\n ...(arrival ? { detail: { [CONVERSION_TOUCH_DETAIL]: arrival } } : {}),\n })\n // Event trigger (AGL-128/148). A sign-up is no longer a lead (AGL-3232),\n // so the `lead` event is the lead surfaces' to emit.\n await emitHostEvent(\n hostId,\n 'memberSignUp',\n { email },\n { actor: { kind: 'visitor', email } },\n )\n setMemberCookie(res, hostId, mintMemberSession(hostId, memberRef.id))\n return res.status(200).json({ ok: true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Sign-up failed' })\n }\n}\n"],"names":["memberNameSearchFields","memberSearchTokens","checkVisitorRecordCeiling","HOST_TOKENS","marketingConsentFieldsForHost","SITE_MEMBER_CEILING_CODE","SITE_MEMBER_UNAVAILABLE_MESSAGE","SITE_MEMBERS_MAX_PER_HOST","firebaseAdmin","rearmVisitorRecordCeilingNotice","recordVisitorRecordCeilingTrip","emitHostEvent","recordCapturedContact","CONVERSION_TOUCH_DETAIL","resolveConversionTouch","hashMemberPassword","mintMemberSession","setMemberCookie","memberCredentialRef","EMAIL_PATTERN","membershipRegisterHandler","req","res","method","status","json","error","hostId","String","body","email","trim","toLowerCase","password","displayName","slice","marketingConsent","test","length","firestore","app","hostRef","collection","doc","hostSnapshot","get","exists","membersRef","memberRef","usedBeforeCreate","refusal","runTransaction","tx","existing","where","limit","empty","duplicate","ceiling","used","count","data","verdict","exceeded","set","id","passwordScrypt","create","searchTokens","suspended","Date","now","createdAt","FieldValue","serverTimestamp","kind","contact","resolve","code","signedUpAtMs","arrival","wire","campaignTouch","atMs","orgId","identity","name","undefined","interaction","source","refId","summary","surface","lifecycleFloor","detail","actor","ok","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/membership-register.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 { memberNameSearchFields, memberSearchTokens } from './member-name-search'\nimport {\n checkVisitorRecordCeiling,\n HOST_TOKENS,\n marketingConsentFieldsForHost,\n SITE_MEMBER_CEILING_CODE,\n SITE_MEMBER_UNAVAILABLE_MESSAGE,\n SITE_MEMBERS_MAX_PER_HOST,\n} from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n rearmVisitorRecordCeilingNotice,\n recordVisitorRecordCeilingTrip,\n} from '@aglyn/tenant-data-admin'\nimport { emitHostEvent } from '@aglyn/tenant-runtime'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\nimport {\n CONVERSION_TOUCH_DETAIL,\n resolveConversionTouch,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport {\n hashMemberPassword,\n mintMemberSession,\n setMemberCookie,\n} from './membership'\nimport { memberCredentialRef } from './member-credentials'\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * Site member sign-up (AGL-109): creates the member record and its credential\n * document (the scrypt hash, AGL-3308), doubles as a lead, and signs the\n * visitor in via the session cookie.\n */\nexport const membershipRegisterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const email = String(req.body?.email ?? '')\n .trim()\n .toLowerCase()\n const password = String(req.body?.password ?? '')\n const displayName = String(req.body?.displayName ?? '')\n .trim()\n .slice(0, 80)\n // Explicit opt-in checkbox (AGL-2499) — never inferred from signing up.\n const marketingConsent = req.body?.marketingConsent === true\n if (!hostId) return res.status(400).json({ error: 'Missing host' })\n if (!EMAIL_PATTERN.test(email)) {\n return res.status(400).json({ error: 'Enter a valid email' })\n }\n if (password.length < 8) {\n return res\n .status(400)\n .json({ error: 'Password must be at least 8 characters' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n // Member accounts are UNLIMITED on every plan (AGL-889) — no seat or\n // quota check belongs here. Audience monetization happens downstream:\n // contact bands meter the CRM projection (AGL-890) and paid\n // memberships carry the plan's digital transaction fee (AGL-892).\n //\n // `SITE_MEMBERS_MAX_PER_HOST` below is NOT that check and does not make\n // it one (AGL-1529). It is a flat PLATFORM ceiling — same number on every\n // plan, no `OrgEntitlements` key, nothing on the price list — and it\n // exists because this handler is reachable by an ANONYMOUS VISITOR, so\n // without it the collection was bounded only by a per-(host, IP) rate\n // limiter that fails soft and bounds the RATE rather than the TOTAL. An\n // abuse control is not something we sell; \"unlimited member accounts on\n // every plan\" stays literally true.\n const membersRef = hostRef.collection('siteMembers')\n const memberRef = membersRef.doc()\n /*\n * COUNT, DEDUPE AND CREATE IN ONE TRANSACTION (AGL-1529, the AGL-2231\n * treatment).\n *\n * Read-then-decide-then-`set()` is the create-time quota that laundered\n * everywhere else in this repo: N concurrent sign-ups each read the same\n * pre-count, each find room, and each land — and nothing re-counts\n * afterwards, so the extra accounts are permanent. The fix is WHEN the\n * count is evaluated, not the counting rule.\n * `Transaction.get(AggregateQuery)` serialises the count against a\n * concurrent create into this collection: the loser retries, re-reads the\n * higher count and is refused. The duplicate-email read moves inside for\n * free, which also closes the smaller race it always had — two\n * simultaneous sign-ups on one address both saw `empty` and both wrote.\n *\n * ALL READS BEFORE THE WRITE, which Firestore requires.\n *\n * The scrypt hash sits between the last read and the create ON PURPOSE.\n * It is ~100 ms of CPU and it is the most expensive thing in the request,\n * so a refused sign-up must never pay it — hashing before the transaction\n * would hand a flood a CPU amplifier on exactly the path the ceiling\n * exists to contain, and would also be a regression, since today a\n * duplicate-email refusal does not hash either. It is a pure computation,\n * not an effect, so it does not violate the \"nothing happens in a\n * transaction body\" rule the sibling routes state.\n *\n * A refusal is returned as DATA and rendered outside: a body that can run\n * several times must not be the place a response is built.\n */\n // The count the new account was judged against, for the notice re-arm.\n let usedBeforeCreate = 0\n const refusal = await firestore.runTransaction(async (tx) => {\n const existing = await tx.get(membersRef.where('email', '==', email).limit(1))\n if (!existing.empty) return { duplicate: true, ceiling: 0 }\n const used = (await tx.get(membersRef.count())).data().count\n usedBeforeCreate = used\n // Live documents only, so removing a member in the inbox frees the slot.\n const verdict = checkVisitorRecordCeiling(used, SITE_MEMBERS_MAX_PER_HOST)\n if (verdict.exceeded) return { duplicate: false, ceiling: verdict.ceiling }\n // The hash goes to the credential document, which no client can read\n // (AGL-3308), and in this transaction, so a member never exists without\n // the password they chose. The profile below is what the console lists.\n tx.set(memberCredentialRef(hostRef, memberRef.id), {\n passwordScrypt: hashMemberPassword(password),\n })\n tx.create(memberRef, {\n email,\n /*\n * `displayNameLower` is stored `null` for a member who gave no name,\n * not left out: the console's Site users list sorts by Name with an\n * `orderBy` on it, and an `orderBy` drops every document that lacks\n * the field (AGL-3680).\n */\n ...(displayName\n ? memberNameSearchFields(displayName)\n : { displayNameLower: null }),\n // The Site users list's search: the name and the address (AGL-3321).\n searchTokens: memberSearchTokens(displayName, email),\n /*\n * Stored `false`, not left out: the console's Status filter asks\n * `suspended == false` for the active members, and a query cannot\n * find a document that lacks the field. The drawer's Suspend and\n * Reactivate write `true` and `false` over it.\n */\n suspended: false,\n /*\n * The checkbox is PERSISTED on the member, not only forwarded.\n *\n * It reached the lead and the contact from the two lines below and\n * was dropped for this document, so `hosts/{hostId}/siteMembers` had\n * no consent field of any kind and `audience: 'members'` had nothing\n * for the send-time join to read — the audience could not be filtered\n * even in principle (`docs/specs/email-overhaul.md` §1d/§3f). The\n * other two documents are not a substitute: a member is deduped in\n * this transaction while leads append every time, and contacts are\n * ORG-scoped where a member is the site's own.\n *\n * Written only when ticked. Signing up is not opting in — that is why\n * the checkbox exists and why it defaults unchecked — so the omitted\n * case stores nothing and reads back as an unrecorded basis rather\n * than as a refusal.\n *\n * Under this site's key even though `siteMembers` already lives\n * beneath it, so that one reader answers for every silo — see\n * `addHostLead`, which makes the same write for the same reason.\n */\n ...(marketingConsent\n ? marketingConsentFieldsForHost(hostId, Date.now())\n : {}),\n createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n return null\n })\n if (refusal?.duplicate) {\n return res.status(409).json({ error: 'That email is already a member' })\n }\n if (refusal) {\n // Visible to the HOST — the rule that a control nobody can see in\n // the console did not ship. Durable counter + one notification.\n await recordVisitorRecordCeilingTrip({\n hostRef,\n hostId,\n kind: 'siteMembers',\n ceiling: refusal.ceiling,\n })\n // Opaque to the VISITOR, who is a stranger to this site and not our\n // customer (AGL-1666's rules, restated in the message's own docblock).\n // The one genuinely useful thing to hand them is a door that still\n // opens, so the site's OWN published support address rides along when\n // it has one — read through the host-token registry, whose description\n // of this field is literally \"where visitors should write for help\".\n // Nothing else off the host document may leave the console.\n const contact = HOST_TOKENS['supportEmail']?.resolve(\n hostSnapshot.data() as any,\n )\n return res.status(429).json({\n error: SITE_MEMBER_UNAVAILABLE_MESSAGE,\n // Machine-readable, because the dispatcher's rate limiter answers 429\n // too and the status therefore discriminates nothing.\n code: SITE_MEMBER_CEILING_CODE,\n ...(contact ? { contact } : {}),\n })\n }\n // The account took a slot below the ceiling. When it took the last one,\n // the next refusal is a new crossing and is announced again (AGL-3442).\n await rearmVisitorRecordCeilingNotice({\n hostRef,\n kind: 'siteMembers',\n used: usedBeforeCreate,\n ceiling: SITE_MEMBERS_MAX_PER_HOST,\n })\n // Sign-ups double as leads for the site owner (AGL-109), through the one\n // writer that enforces `LEADS_MAX_PER_HOST` (AGL-1529). A refused lead\n // never fails the sign-up: the visitor asked for an account, not for a\n // lead record, and the trip is recorded for the owner either way.\n /*\n * WHERE THE VISITOR ARRIVED FROM, RESOLVED ONCE FOR THE WHOLE SIGN-UP.\n *\n * A sign-up is the identify moment for a visitor who has been anonymous\n * until now, and it writes two records an arrival can be credited with —\n * the lead and the contact. One resolve, by the plugin that credits\n * outcomes, and the two cannot end up credited to different arrivals.\n *\n * The MEMBER record itself is not attributed. A member is an account the\n * visitor holds and the lead is the site's record of the same act, so\n * crediting both would count one sign-up twice under two names.\n */\n const signedUpAtMs = Date.now()\n const arrival = await resolveConversionTouch({\n hostId,\n wire: req.body?.campaignTouch,\n email,\n atMs: signedUpAtMs,\n })\n /*\n * Contacts ingestion (AGL-197), reported to whichever plugin keeps people\n * rather than written by this one (AGL-3080). Commerce knows it just met\n * somebody; what a person record is belongs to the plugin that models it,\n * and a workspace that keeps none is a quiet, correct answer here.\n */\n /*\n * A member account is a RELATIONSHIP (AGL-3232): the person is a\n * contact from this moment, and an open lead the site held for the\n * address is closed as converted onto that contact by the record\n * system — a sign-up files no lead of its own any more, the way a\n * portal user is a contact and not a lead in Salesforce.\n */\n void recordCapturedContact({\n /*\n * Not resolved here. The record system keys a person on the SITE they\n * were met on, so this costs the capture nothing — and resolving it\n * would put a Firestore read in front of the writer, which is the one\n * thing `recordCapturedContact` asks a door not to do.\n */\n orgId: '',\n hostId,\n identity: { email, name: displayName || undefined },\n interaction: {\n source: 'member',\n refId: memberRef.id,\n summary: 'Joined as a member',\n },\n surface: 'relationship',\n // An account is a subscription to the site, not an enquiry (AGL-2612):\n // the stage on the contact says only that the person asked to be\n // kept. A floor, so a customer who opens an account stays a customer.\n lifecycleFloor: 'subscriber',\n ...(marketingConsent ? { marketingConsent: true } : {}),\n // Where the visitor ARRIVED from — a fact about this visit and not\n // about the person, which is what `detail` carries.\n ...(arrival ? { detail: { [CONVERSION_TOUCH_DETAIL]: arrival } } : {}),\n })\n // Event trigger (AGL-128/148). A sign-up is no longer a lead (AGL-3232),\n // so the `lead` event is the lead surfaces' to emit.\n await emitHostEvent(\n hostId,\n 'memberSignUp',\n { email },\n { actor: { kind: 'visitor', email } },\n )\n setMemberCookie(res, hostId, mintMemberSession(hostId, memberRef.id))\n return res.status(200).json({ ok: true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Sign-up failed' })\n }\n}\n"],"names":["memberNameSearchFields","memberSearchTokens","checkVisitorRecordCeiling","HOST_TOKENS","marketingConsentFieldsForHost","SITE_MEMBER_CEILING_CODE","SITE_MEMBER_UNAVAILABLE_MESSAGE","SITE_MEMBERS_MAX_PER_HOST","firebaseAdmin","rearmVisitorRecordCeilingNotice","recordVisitorRecordCeilingTrip","emitHostEvent","recordCapturedContact","CONVERSION_TOUCH_DETAIL","resolveConversionTouch","hashMemberPassword","mintMemberSession","setMemberCookie","memberCredentialRef","EMAIL_PATTERN","membershipRegisterHandler","req","res","method","status","json","error","hostId","String","body","email","trim","toLowerCase","password","displayName","slice","marketingConsent","test","length","firestore","app","hostRef","collection","doc","hostSnapshot","get","exists","membersRef","memberRef","usedBeforeCreate","refusal","runTransaction","tx","existing","where","limit","empty","duplicate","ceiling","used","count","data","verdict","exceeded","set","id","passwordScrypt","create","displayNameLower","searchTokens","suspended","Date","now","createdAt","FieldValue","serverTimestamp","kind","contact","resolve","code","signedUpAtMs","arrival","wire","campaignTouch","atMs","orgId","identity","name","undefined","interaction","source","refId","summary","surface","lifecycleFloor","detail","actor","ok","console"],"mappings":"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"}
@@ -111,6 +111,9 @@ function lineLabel(line) {
111
111
  return ((_b_atMs = b.atMs) != null ? _b_atMs : 0) - ((_a_atMs = a.atMs) != null ? _a_atMs : 0);
112
112
  })[0];
113
113
  if (!fulfillment) return null;
114
+ // Collected at the counter or handed over by the store's driver
115
+ // (AGL-3624): "picked up" and "delivered" say so, never "shipped".
116
+ if (fulfillment.handover) return null;
114
117
  // The units THIS shipment carried (AGL-3611): a partial shipment of a
115
118
  // line names its own count, not the line's.
116
119
  const shipped = CommerceModel.fulfillmentLineQuantities(order, fulfillment).map((entry)=>lines[entry.lineItemId] ? _extends({}, lines[entry.lineItemId], {
@@ -164,6 +167,64 @@ function lineLabel(line) {
164
167
  timelineDetail: 'Delivery confirmation sent'
165
168
  };
166
169
  }
170
+ if (event === 'ready_for_pickup' || event === 'picked_up') {
171
+ var _pickup_instructions;
172
+ const pickup = order.pickup;
173
+ if (order.fulfillmentMethod !== 'pickup' || !pickup) return null;
174
+ const summary = lines.map(lineLabel).join('\n');
175
+ const where = `${pickup.locationName}${pickup.address ? `, ${pickup.address}` : ''}`;
176
+ const hours = pickup.hours ? `Pickup hours:\n${pickup.hours}` : '';
177
+ const instructions = (_pickup_instructions = pickup.instructions) != null ? _pickup_instructions : '';
178
+ if (event === 'picked_up') {
179
+ return {
180
+ occurrence: 'order',
181
+ emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.picked_up,
182
+ tokens: _extends({}, base, {
183
+ 'order.summary': summary,
184
+ 'pickup.location': where
185
+ }),
186
+ subject: `You picked up order ${number}`,
187
+ text: `Order ${number} was picked up from ${where}. Thanks for shopping with ${store}.\n\n${summary}` + link,
188
+ sms: `${store}: order ${number} was picked up. Thank you!${smsLink}`,
189
+ timelineDetail: 'Pickup confirmation sent'
190
+ };
191
+ }
192
+ return {
193
+ occurrence: 'order',
194
+ emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.ready_for_pickup,
195
+ tokens: _extends({}, base, {
196
+ 'order.summary': summary,
197
+ 'pickup.location': where,
198
+ 'pickup.hours': hours,
199
+ 'pickup.instructions': instructions
200
+ }),
201
+ subject: `Your order ${number} is ready for pickup`,
202
+ text: `Your order ${number} is ready for pickup at ${where}.\n\n${summary}` + (hours ? `\n\n${hours}` : '') + (instructions ? `\n\n${instructions}` : '') + `\n\nBring your order number, ${number}.` + link,
203
+ sms: `${store}: order ${number} is ready for pickup at ${pickup.locationName}.${smsLink}`,
204
+ timelineDetail: 'Ready-for-pickup notice sent'
205
+ };
206
+ }
207
+ if (event === 'out_for_delivery') {
208
+ var _delivery_windowLabel, _delivery_outForDeliveryAtMs;
209
+ const delivery = order.localDelivery;
210
+ if (order.fulfillmentMethod !== 'local_delivery' || !delivery) return null;
211
+ const summary = lines.map(lineLabel).join('\n');
212
+ const window = (_delivery_windowLabel = delivery.windowLabel) != null ? _delivery_windowLabel : '';
213
+ return {
214
+ // Each run out the door is its own message: a failed drop sent out
215
+ // again tells the buyer again.
216
+ occurrence: String((_delivery_outForDeliveryAtMs = delivery.outForDeliveryAtMs) != null ? _delivery_outForDeliveryAtMs : 'order'),
217
+ emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.out_for_delivery,
218
+ tokens: _extends({}, base, {
219
+ 'order.summary': summary,
220
+ 'delivery.window': window
221
+ }),
222
+ subject: `Your order ${number} is out for delivery`,
223
+ text: `${store} is on the way with order ${number}` + (window ? `, in your window ${window}` : '') + `.\n\n${summary}` + link,
224
+ sms: `${store}: order ${number} is out for delivery${window ? ` (${window})` : ''}.${smsLink}`,
225
+ timelineDetail: 'Out-for-delivery notice sent'
226
+ };
227
+ }
167
228
  if (event === 'refunded') {
168
229
  var _options_refundCents, _options_refundLineIndexes, _order_refundedCents;
169
230
  const refundCents = Math.max(0, Math.round(Number((_options_refundCents = options.refundCents) != null ? _options_refundCents : 0)));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-notifications.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { pluginSmsMessaging } from '@aglyn/aglyn/plugin-manager/plugin-sms-messaging'\nimport { resolveSiteTimeZone } from '@aglyn/aglyn/app-utils/collection-entry-date'\nimport {\n resolveHostToken,\n type HostTokenSource,\n} from '@aglyn/aglyn/app-utils/host-tokens'\nimport {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n renderHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport * as CommerceModel from '../model'\nimport { mintDownloadToken } from './download'\nimport { orderStatusUrl } from './order-status-token'\n\n/**\n * Every message a buyer gets about their order, through ONE door (AGL-3610).\n *\n * Before this, the buyer heard about their order once — the online receipt —\n * and never again: a register sale sent nothing, a paid payment link sent\n * nothing, and shipping, delivery, refunds and cancellations were silent\n * (`fulfill-order.ts` said so on purpose). Every path that moves an order now\n * makes one call here, and this decides what that moment owes the buyer:\n *\n * - whether the store turned the moment off (`buyerNotifications` on the\n * store settings; ON unless explicitly off — see the model),\n * - which channels can carry it (email when mail is configured and the\n * order has an address; a text when the platform's SMS provider is\n * configured and the order carries the buyer's phone),\n * - and whether it was already sent.\n *\n * ## ONCE PER MOMENT, PER OCCURRENCE, PER CHANNEL\n *\n * Stripe redelivers webhooks, admins click twice, a lost response is retried.\n * Each message is CLAIMED before it is sent: a transaction reads the order's\n * `buyerNotifications` map and writes a `sending` marker for\n * `event__occurrence__channel` only if none is there. Exactly one of two\n * racing callers wins the claim; the other returns `already`. The occurrence\n * is the shipment id for `shipped` and the refund id for `refunded`, so a\n * second parcel or a second partial refund is a new message, while a\n * redelivered one is not.\n *\n * A send that fails RELEASES its claim so the next attempt can try again. A\n * process that dies between claim and send leaves the marker in `sending` and\n * the message unsent — at most once, never twice, which is the right side to\n * fail on for mail a buyer reads as \"did they charge me again?\".\n *\n * ## NEVER THROWS\n *\n * Every caller is past its point of no return — money moved, an order\n * shipped — so a mail outage must not turn into a 500 that invites the\n * merchant to do it again. Failures are logged and returned as outcomes.\n *\n * Shared with the order-processing and POS lanes (AGL-3611, AGL-3607): call\n * {@link notifyOrderBuyer} from any new path that moves an order, and\n * {@link sendOrderReceipt} for an explicit, merchant-asked send.\n */\n\nexport type OrderBuyerEvent = CommerceModel.BuyerNotificationEvent\n\nexport interface OrderRef {\n hostId: string\n orderId: string\n}\n\nexport interface NotifyOrderBuyerOptions {\n /** `shipped`: which shipment. Absent means the order's latest one. */\n fulfillmentId?: string\n /** `refunded`: the refund's own id (Stripe's), the occurrence key. */\n refundId?: string\n /** `refunded`: what THIS refund moved, in cents. */\n refundCents?: number\n /** `refunded`: the line indexes refunded by name, when any were. */\n refundLineIndexes?: readonly number[]\n /** `refunded`: whether this refund closed the order. */\n fullyRefunded?: boolean\n /**\n * The buyer's address when the order document does not carry it — a POS\n * folio sale attributed to the reservation's guest, a payment link whose\n * payer Stripe named. The order's own `customerEmail` wins when present.\n */\n email?: string | null\n /** Injected in specs. */\n firestore?: FirebaseFirestore.Firestore\n}\n\nexport type OrderBuyerChannelOutcome =\n | { channel: CommerceModel.BuyerNotificationChannel; outcome: 'sent' }\n | { channel: CommerceModel.BuyerNotificationChannel; outcome: 'already' }\n | {\n channel: CommerceModel.BuyerNotificationChannel\n outcome: 'failed'\n error: string\n }\n\nexport type NotifyOrderBuyerOutcome =\n | { outcome: 'disabled' }\n | { outcome: 'no_such_order' }\n /** The moment does not apply to this order (a counter sale \"shipping\"). */\n | { outcome: 'not_applicable' }\n /** No channel could carry it: no address, no mail or text configured. */\n | { outcome: 'no_recipient' }\n | { outcome: 'handled'; channels: OrderBuyerChannelOutcome[] }\n\ninterface StoreContext {\n firestore: FirebaseFirestore.Firestore\n hostRef: FirebaseFirestore.DocumentReference\n host: Record<string, unknown>\n settings: Record<string, unknown>\n currency: string\n}\n\nasync function readStore(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<StoreContext> {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, settingsSnapshot] = await Promise.all([\n hostRef.get().catch(() => null),\n hostRef.collection('settings').doc('store').get().catch(() => null),\n ])\n const settings = (settingsSnapshot?.data?.() ?? {}) as Record<string, unknown>\n return {\n firestore,\n hostRef,\n host: (hostSnapshot?.data?.() ?? {}) as Record<string, unknown>,\n settings,\n currency: String(settings['currency'] ?? 'USD') || 'USD',\n }\n}\n\nfunction lineLabel(line: CommerceModel.OrderLineItem): string {\n return `${line.quantity}× ${line.name}${\n line.variantLabel ? ` (${line.variantLabel})` : ''\n }`\n}\n\n/** The status link, or the store's home page when no link can be minted. */\nfunction statusLink(store: StoreContext, ref: OrderRef): string {\n return (\n orderStatusUrl(store.host as never, ref.hostId, ref.orderId) ??\n Aglyn.hostPublicOrigin({\n cname: typeof store.host['cname'] === 'string' ? store.host['cname'] : null,\n subdomain:\n typeof store.host['subdomain'] === 'string'\n ? store.host['subdomain']\n : null,\n }) ??\n ''\n )\n}\n\ninterface ComposedMessage {\n emailKey: string\n /** Tokens for the designed email. */\n tokens: Record<string, string>\n /** Subject when the template renders none. */\n subject: string\n /** Plain-text body when the template renders none. */\n text: string\n /** The text message, kept short: one or two segments. */\n sms: string\n /** The order timeline line recording the send, channel appended. */\n timelineDetail: string\n}\n\n/** What the moment says. `null` when it does not apply to this order. */\nexport function composeOrderBuyerMessage(input: {\n event: OrderBuyerEvent\n order: CommerceModel.HostOrder\n orderId: string\n businessName: string\n currency: string\n statusUrl: string\n receiptFooter?: string\n /** Absolute digital-delivery links for a receipt, by product id. */\n downloadLinks?: Record<string, string>\n options?: NotifyOrderBuyerOptions\n}): (ComposedMessage & { occurrence: string }) | null {\n const {\n event,\n order,\n orderId,\n businessName,\n currency,\n statusUrl,\n options = {},\n } = input\n const lines = order.lineItems ?? []\n const number = CommerceModel.formatOrderNumber(order, orderId)\n const money = (cents: number) =>\n CommerceModel.formatOrderMoney(cents, currency)\n const store = businessName || 'The store'\n const base = {\n 'order.number': number,\n 'order.statusUrl': statusUrl,\n 'order.ref': orderId,\n }\n const link = statusUrl ? `\\n\\nView your order: ${statusUrl}` : ''\n const smsLink = statusUrl ? ` ${statusUrl}` : ''\n\n if (event === 'receipt') {\n const totalCents =\n order.totals?.totalCents ?? Number(order.amountCents ?? 0)\n const licenseText = Object.entries(\n ((order as { licenseKeys?: Record<string, string[]> }).licenseKeys ??\n {}) as Record<string, string[]>,\n )\n .flatMap(([productId, keys]) => {\n const line = lines.find((item) => item.productId === productId)\n return (keys ?? []).map(\n (key) => `License key (${line?.name ?? 'product'}): ${key}`,\n )\n })\n .join('\\n')\n const downloads = Object.entries(input.downloadLinks ?? {})\n .map(([productId, url]) => {\n const line = lines.find((item) => item.productId === productId)\n return `Download ${line?.name ?? 'your file'}: ${url}`\n })\n .join('\\n')\n const linesText = lines\n .map(\n (line) =>\n `${lineLabel(line)} — ${money(line.unitAmountCents * line.quantity)}`,\n )\n .join('\\n')\n const summary = [linesText, licenseText, downloads]\n .filter(Boolean)\n .join('\\n\\n')\n const footer = String(input.receiptFooter ?? '')\n return {\n occurrence: 'order',\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.receipt,\n tokens: {\n ...base,\n 'order.summary': summary,\n 'order.total': money(totalCents),\n 'store.receiptFooter': footer,\n },\n subject: 'Receipt for your order',\n text:\n `Thanks for your purchase!\\n\\n${summary}\\n\\nTotal: ${money(totalCents)}\\n` +\n `Order ${number}` +\n (footer ? `\\n\\n${footer}` : '') +\n link,\n sms: `${store}: receipt for order ${number}, ${money(totalCents)}.${smsLink}`,\n timelineDetail: 'Receipt sent',\n }\n }\n\n if (event === 'shipped') {\n const fulfillments = order.fulfillments ?? []\n const fulfillment = options.fulfillmentId\n ? fulfillments.find((entry) => entry.id === options.fulfillmentId)\n : [...fulfillments].sort((a, b) => (b.atMs ?? 0) - (a.atMs ?? 0))[0]\n if (!fulfillment) return null\n // The units THIS shipment carried (AGL-3611): a partial shipment of a\n // line names its own count, not the line's.\n const shipped = CommerceModel.fulfillmentLineQuantities(order, fulfillment)\n .map((entry) =>\n lines[entry.lineItemId] ? { ...lines[entry.lineItemId], quantity: entry.quantity } : null,\n )\n .filter(Boolean) as CommerceModel.OrderLineItem[]\n const tracking = CommerceModel.fulfillmentTrackingUrl(fulfillment)\n const hasTracking = Boolean(fulfillment.trackingNumber)\n // Nothing physical left the building: a register sale handed over the\n // counter, or a digital-only order the merchant marked fulfilled. A\n // \"your order is on its way\" email for either is noise at best.\n const physical = shipped.some(\n (line) =>\n line.productType !== 'digital' && line.productType !== 'service',\n )\n if (!hasTracking && (order.channel === 'pos' || !physical)) return null\n // Shippable units still out, over every active shipment (AGL-3611).\n const remainingUnits = CommerceModel.remainingFulfillmentLines(order).reduce(\n (sum, entry) => sum + entry.quantity,\n 0,\n )\n const carrier = CommerceModel.carrierLabelFor(fulfillment.carrier)\n const trackingSentence = hasTracking\n ? `Tracking: ${[carrier, fulfillment.trackingNumber]\n .filter(Boolean)\n .join(' ')}${tracking ? ` — ${tracking}` : ''}`\n : ''\n const remainingSentence =\n remainingUnits > 0\n ? `${remainingUnits} more item${remainingUnits === 1 ? '' : 's'} will ship separately.`\n : ''\n const summary = shipped.map(lineLabel).join('\\n')\n return {\n occurrence: fulfillment.id,\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.shipped,\n tokens: {\n ...base,\n 'shipment.summary': summary,\n 'shipment.carrier': carrier,\n 'shipment.trackingNumber': String(fulfillment.trackingNumber ?? ''),\n 'shipment.tracking': trackingSentence,\n 'shipment.remaining': remainingSentence,\n },\n subject: `Your order ${number} has shipped`,\n text:\n `${store} shipped items from order ${number}.\\n\\n${summary}` +\n (trackingSentence ? `\\n\\n${trackingSentence}` : '') +\n (remainingSentence ? `\\n\\n${remainingSentence}` : '') +\n link,\n sms:\n `${store}: order ${number} has shipped.` +\n (tracking ? ` Track it: ${tracking}` : smsLink),\n timelineDetail: 'Shipping confirmation sent',\n }\n }\n\n if (event === 'delivered') {\n // A counter sale is \"delivered\" the moment it is handed over.\n if (order.channel === 'pos') return null\n const summary = lines.map(lineLabel).join('\\n')\n return {\n occurrence: 'order',\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.delivered,\n tokens: { ...base, 'order.summary': summary },\n subject: `Your order ${number} was delivered`,\n text:\n `${store} marked order ${number} as delivered.\\n\\n${summary}` + link,\n sms: `${store}: order ${number} was delivered.${smsLink}`,\n timelineDetail: 'Delivery confirmation sent',\n }\n }\n\n if (event === 'refunded') {\n const refundCents = Math.max(0, Math.round(Number(options.refundCents ?? 0)))\n if (!(refundCents > 0)) return null\n const refundedLines = (options.refundLineIndexes ?? [])\n .map((index) => lines[index])\n .filter(Boolean) as CommerceModel.OrderLineItem[]\n const summary = refundedLines.map(lineLabel).join('\\n')\n const note = options.fullyRefunded\n ? 'Your order has been refunded in full.'\n : 'The rest of your order is unchanged.'\n return {\n // The refund's own id is the occurrence; without one, the running\n // total after this refund is (each settled refund raises it).\n occurrence:\n options.refundId ||\n `total-${Number(order.refundedCents ?? 0) || refundCents}`,\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.refunded,\n tokens: {\n ...base,\n 'refund.amount': money(refundCents),\n 'refund.summary': summary,\n 'refund.note': note,\n },\n subject: `Refund for your order ${number}`,\n text:\n `${store} refunded ${money(refundCents)} on order ${number}.` +\n (summary ? `\\n\\n${summary}` : '') +\n `\\n\\n${note}\\n\\nRefunds go back to the original payment method and ` +\n 'usually appear within 5 to 10 business days, depending on your bank.' +\n link,\n sms: `${store}: ${money(refundCents)} refunded on order ${number}.${smsLink}`,\n timelineDetail: `Refund confirmation sent (${money(refundCents)})`,\n }\n }\n\n // cancelled\n const summary = lines.map(lineLabel).join('\\n')\n const wasPaid = Boolean(\n order.paymentIntentId ||\n (order.channel === 'pos' && order.status !== 'pending') ||\n (order.timeline ?? []).some((entry) => entry?.event === 'paid'),\n )\n const note = wasPaid\n ? `Questions about a payment? Reply to this email or contact ${store}.`\n : ''\n return {\n occurrence: 'order',\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.cancelled,\n tokens: { ...base, 'order.summary': summary, 'cancel.note': note },\n subject: `Your order ${number} was canceled`,\n text:\n `${store} canceled order ${number}.\\n\\n${summary}` +\n (note ? `\\n\\n${note}` : '') +\n link,\n sms: `${store}: order ${number} was canceled.${smsLink}`,\n timelineDetail: 'Cancellation notice sent',\n }\n}\n\n/**\n * Claims one message in a transaction. `true` when this caller owns the send;\n * `false` when a marker is already there (sent, or being sent).\n */\nasync function claimMessage(\n firestore: FirebaseFirestore.Firestore,\n orderRef: FirebaseFirestore.DocumentReference,\n marker: string,\n channel: CommerceModel.BuyerNotificationChannel,\n): Promise<boolean> {\n return firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return false\n const sent = (snapshot.get('buyerNotifications') ?? {}) as Record<\n string,\n unknown\n >\n if (sent[marker]) return false\n transaction.update(orderRef, {\n [`buyerNotifications.${marker}`]: {\n state: 'sending',\n channel,\n atMs: Date.now(),\n },\n })\n return true\n })\n}\n\n/** Marks a claimed message sent and records it on the order timeline. */\nasync function settleMessage(\n firestore: FirebaseFirestore.Firestore,\n orderRef: FirebaseFirestore.DocumentReference,\n marker: string | null,\n channel: CommerceModel.BuyerNotificationChannel,\n detail: string,\n): Promise<void> {\n await firestore\n .runTransaction(async (transaction) => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return\n const order = CommerceModel.liftLegacyOrder(\n (snapshot.data() ?? {}) as never,\n )\n const atMs = Date.now()\n transaction.update(orderRef, {\n ...(marker\n ? {\n [`buyerNotifications.${marker}`]: { state: 'sent', channel, atMs },\n }\n : {}),\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'buyer-notified',\n `${detail} by ${channel === 'sms' ? 'text' : 'email'}`,\n atMs,\n ),\n })\n })\n .catch((error) => {\n console.error('[order-notifications] settle failed', marker, error)\n })\n}\n\nasync function releaseMessage(\n orderRef: FirebaseFirestore.DocumentReference,\n marker: string,\n): Promise<void> {\n await orderRef\n .update({\n [`buyerNotifications.${marker}`]:\n firebaseAdmin.firestore.FieldValue.delete(),\n })\n .catch(() => undefined)\n}\n\nasync function deliverEmail(\n store: StoreContext,\n ref: OrderRef,\n to: string,\n message: ComposedMessage,\n context: string,\n): Promise<void> {\n const designed = await renderHostEmailWithTokens(\n store.firestore,\n ref.hostId,\n message.emailKey,\n message.tokens,\n )\n const org = await getOrgForHost(ref.hostId).catch(() => null)\n await sendEmail({\n to,\n subject: designed?.subject ?? message.subject,\n text: designed?.text || message.text,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: Aglyn.resolveBrandingProfile(org?.org as never).fromName,\n sendingIdentity: await hostSendingIdentity(ref.hostId),\n audience: 'tenant',\n context,\n // Owed to the recipient by their own order: the phishing screen's soft\n // rules never hold it (AGL-3356).\n owedFor: 'order',\n })\n // Cost meter (AGL-1438), as every transactional store email.\n await meterHostEmail(ref.hostId)\n}\n\n/**\n * What a text attempt came to: `refused` names why nothing went (null when it\n * went), and `scheduledForMs` says it was held for the buyer's morning.\n */\ninterface SmsDelivery {\n refused: string | null\n scheduledForMs?: number\n}\n\n/**\n * Sends one order text. A receipt goes at once — the buyer is at the counter\n * or the checkout waiting for it. Every later moment (shipped, delivered,\n * refunded, canceled) can fire at any hour, from a carrier scan or a late\n * packing shift, so it asks the provider to keep quiet hours in the STORE's\n * zone: the best stand-in the platform has for a buyer whose own zone it was\n * never told, and right for the local and regional stores most texts come\n * from. Held texts arrive at 8 AM there.\n */\nasync function deliverSms(\n ref: OrderRef,\n to: string,\n message: ComposedMessage,\n context: string,\n quiet: { host: Record<string, unknown> } | null,\n): Promise<SmsDelivery> {\n const sms = pluginSmsMessaging()\n if (!sms?.isConfigured()) return { refused: 'Text messages are not configured' }\n let timeZone: string | null = null\n if (quiet) {\n const owner = await getOrgForHost(ref.hostId).catch(() => null)\n timeZone = resolveSiteTimeZone(\n (owner?.org ?? null) as { timeZone?: string } | null,\n quiet.host as { timeZone?: string },\n )\n }\n const outcome = await sms.send({\n to,\n body: message.sms,\n hostId: ref.hostId,\n purpose: 'transactional',\n context,\n ...(timeZone ? { quietHours: { timeZone } } : {}),\n })\n if (outcome.status !== 'sent') return { refused: outcome.status }\n return {\n refused: null,\n ...(outcome.scheduledForMs ? { scheduledForMs: outcome.scheduledForMs } : {}),\n }\n}\n\n/** Absolute download links for a receipt's digital lines. */\nfunction downloadLinksFor(\n store: StoreContext,\n ref: OrderRef,\n order: CommerceModel.HostOrder,\n): Record<string, string> {\n const origin = Aglyn.hostPublicOrigin({\n cname: typeof store.host['cname'] === 'string' ? store.host['cname'] : null,\n subdomain:\n typeof store.host['subdomain'] === 'string' ? store.host['subdomain'] : null,\n })\n const digital = (order.lineItems ?? []).filter(\n (line) => line.productType === 'digital',\n )\n if (!origin || !digital.length) return {}\n let token: string\n try {\n token = mintDownloadToken(ref.hostId, ref.orderId)\n } catch {\n return {}\n }\n return Object.fromEntries(\n digital.map((line) => [\n line.productId,\n `${origin}/api/commerce/download?hostId=${encodeURIComponent(ref.hostId)}` +\n `&orderId=${encodeURIComponent(ref.orderId)}` +\n `&productId=${encodeURIComponent(line.productId)}&token=${token}`,\n ]),\n )\n}\n\nasync function readOrder(\n firestore: FirebaseFirestore.Firestore,\n ref: OrderRef,\n): Promise<{\n orderRef: FirebaseFirestore.DocumentReference\n order: CommerceModel.HostOrder | null\n}> {\n const orderRef = firestore\n .collection('hosts')\n .doc(ref.hostId)\n .collection('orders')\n .doc(ref.orderId)\n const snapshot = await orderRef.get()\n return {\n orderRef,\n order: snapshot.exists\n ? CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n : null,\n }\n}\n\nfunction emailOf(value: unknown): string {\n const email = String(value ?? '').trim()\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) ? email : ''\n}\n\n/**\n * Tells the buyer about one moment in their order's life, once. See the\n * module header. Never throws.\n */\nexport async function notifyOrderBuyer(\n ref: OrderRef,\n event: OrderBuyerEvent,\n options: NotifyOrderBuyerOptions = {},\n): Promise<NotifyOrderBuyerOutcome> {\n try {\n if (!ref.hostId || !ref.orderId) return { outcome: 'no_such_order' }\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const store = await readStore(firestore, ref.hostId)\n const toggles = store.settings['buyerNotifications']\n if (!CommerceModel.buyerNotificationEnabled(toggles, event)) {\n return { outcome: 'disabled' }\n }\n const { orderRef, order } = await readOrder(firestore, ref)\n if (!order) return { outcome: 'no_such_order' }\n const message = composeOrderBuyerMessage({\n event,\n order,\n orderId: ref.orderId,\n businessName: String(\n resolveHostToken('businessName', store.host as HostTokenSource) ?? '',\n ),\n currency: store.currency,\n statusUrl: statusLink(store, ref),\n receiptFooter: String(store.settings['receiptFooter'] ?? ''),\n downloadLinks:\n event === 'receipt' ? downloadLinksFor(store, ref, order) : undefined,\n options,\n })\n if (!message) return { outcome: 'not_applicable' }\n\n const email = emailOf(order.customerEmail) || emailOf(options.email)\n const phone = String(order.customerPhone ?? '').trim()\n const targets: {\n channel: CommerceModel.BuyerNotificationChannel\n to: string\n }[] = []\n if (email && isEmailConfigured()) targets.push({ channel: 'email', to: email })\n if (\n phone &&\n CommerceModel.buyerNotificationEnabled(toggles, 'texts') &&\n pluginSmsMessaging()?.isConfigured()\n ) {\n targets.push({ channel: 'sms', to: phone })\n }\n if (!targets.length) return { outcome: 'no_recipient' }\n\n const channels: OrderBuyerChannelOutcome[] = []\n for (const target of targets) {\n const marker = CommerceModel.buyerNotificationMarker(\n event,\n message.occurrence,\n target.channel,\n )\n const claimed = await claimMessage(\n firestore,\n orderRef,\n marker,\n target.channel,\n ).catch((error) => {\n console.error('[order-notifications] claim failed', marker, error)\n return false\n })\n if (!claimed) {\n channels.push({ channel: target.channel, outcome: 'already' })\n continue\n }\n const context = message.emailKey\n let held: number | null = null\n try {\n if (target.channel === 'email') {\n await deliverEmail(store, ref, target.to, message, context)\n } else {\n const sent = await deliverSms(\n ref,\n target.to,\n message,\n context,\n event === 'receipt' ? null : { host: store.host },\n )\n if (sent.refused) throw new Error(sent.refused)\n held = sent.scheduledForMs ?? null\n }\n await settleMessage(\n firestore,\n orderRef,\n marker,\n target.channel,\n held\n ? `${message.timelineDetail} (held for the morning)`\n : message.timelineDetail,\n )\n channels.push({ channel: target.channel, outcome: 'sent' })\n } catch (error) {\n await releaseMessage(orderRef, marker)\n console.error('[order-notifications] send failed', {\n hostId: ref.hostId,\n orderId: ref.orderId,\n event,\n channel: target.channel,\n error,\n })\n channels.push({\n channel: target.channel,\n outcome: 'failed',\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n return { outcome: 'handled', channels }\n } catch (error) {\n console.error('[order-notifications] notify failed', ref, event, error)\n return { outcome: 'handled', channels: [] }\n }\n}\n\nexport type SendOrderReceiptOutcome =\n | { outcome: 'sent'; channel: CommerceModel.BuyerNotificationChannel }\n | { outcome: 'no_such_order' }\n | { outcome: 'not_configured' }\n | { outcome: 'invalid_recipient' }\n | { outcome: 'failed'; error: string }\n\n/**\n * Sends the receipt again, on the merchant's say-so, to the address or number\n * they gave (AGL-3610) — the order dialog's \"Resend receipt\".\n *\n * NOT idempotent, on purpose: a resend IS the second send. The route in front\n * of it is rate-limited instead, and the send is recorded on the timeline so\n * the merchant can see it went. It does not consult the store's moment\n * switches either — a merchant who turned automatic receipts off can still\n * hand one to a customer who asks.\n */\nexport async function sendOrderReceipt(\n ref: OrderRef,\n input: {\n channel: CommerceModel.BuyerNotificationChannel\n to: string\n firestore?: FirebaseFirestore.Firestore\n },\n): Promise<SendOrderReceiptOutcome> {\n try {\n const firestore = input.firestore ?? firebaseAdmin.app().firestore()\n const { orderRef, order } = await readOrder(firestore, ref)\n if (!order) return { outcome: 'no_such_order' }\n if (input.channel === 'email') {\n if (!isEmailConfigured()) return { outcome: 'not_configured' }\n if (!emailOf(input.to)) return { outcome: 'invalid_recipient' }\n } else {\n if (!pluginSmsMessaging()?.isConfigured()) {\n return { outcome: 'not_configured' }\n }\n if (!Aglyn.normalizePhone(input.to)) {\n return { outcome: 'invalid_recipient' }\n }\n }\n const store = await readStore(firestore, ref.hostId)\n const message = composeOrderBuyerMessage({\n event: 'receipt',\n order,\n orderId: ref.orderId,\n businessName: String(\n resolveHostToken('businessName', store.host as HostTokenSource) ?? '',\n ),\n currency: store.currency,\n statusUrl: statusLink(store, ref),\n receiptFooter: String(store.settings['receiptFooter'] ?? ''),\n downloadLinks: downloadLinksFor(store, ref, order),\n })\n if (!message) return { outcome: 'failed', error: 'Nothing to send' }\n if (input.channel === 'email') {\n await deliverEmail(store, ref, emailOf(input.to), message, 'order-receipt')\n } else {\n const sent = await deliverSms(ref, input.to, message, 'order-receipt', null)\n if (sent.refused) return { outcome: 'failed', error: sent.refused }\n }\n await settleMessage(firestore, orderRef, null, input.channel, 'Receipt re-sent')\n return { outcome: 'sent', channel: input.channel }\n } catch (error) {\n console.error('[order-notifications] receipt send failed', ref, error)\n return {\n outcome: 'failed',\n error: error instanceof Error ? error.message : String(error),\n }\n }\n}\n\n/**\n * What the online receipt sends needs from this module (AGL-3610): whether\n * the store left receipts on, and the `order.number` / `order.statusUrl`\n * tokens. The cart and buy-now receipts compose their own summary (license\n * keys and download links built from the Stripe session) inside the\n * webhook's redelivery guard, so they ask here rather than route through\n * {@link notifyOrderBuyer}.\n */\nexport async function onlineReceiptExtras(\n hostId: string,\n orderId: string,\n firestore: FirebaseFirestore.Firestore = firebaseAdmin.app().firestore(),\n): Promise<{ enabled: boolean; tokens: Record<string, string> }> {\n try {\n const store = await readStore(firestore, hostId)\n const { order } = await readOrder(firestore, { hostId, orderId })\n return {\n enabled: CommerceModel.buyerNotificationEnabled(\n store.settings['buyerNotifications'],\n 'receipt',\n ),\n tokens: {\n 'order.number': CommerceModel.formatOrderNumber(order ?? {}, orderId),\n 'order.statusUrl': statusLink(store, { hostId, orderId }),\n },\n }\n } catch (error) {\n console.error('[order-notifications] receipt extras failed', error)\n return { enabled: true, tokens: { 'order.number': '', 'order.statusUrl': '' } }\n }\n}\n"],"names":["Aglyn","pluginSmsMessaging","resolveSiteTimeZone","resolveHostToken","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","renderHostEmailWithTokens","isEmailConfigured","sendEmail","CommerceModel","mintDownloadToken","orderStatusUrl","readStore","firestore","hostId","settings","settingsSnapshot","hostSnapshot","hostRef","collection","doc","Promise","all","get","catch","data","host","currency","String","lineLabel","line","quantity","name","variantLabel","statusLink","store","ref","orderId","hostPublicOrigin","cname","subdomain","composeOrderBuyerMessage","input","order","event","businessName","statusUrl","options","lines","lineItems","number","formatOrderNumber","money","cents","formatOrderMoney","base","link","smsLink","totalCents","totals","Number","amountCents","licenseText","Object","entries","licenseKeys","flatMap","productId","keys","find","item","map","key","join","downloads","downloadLinks","url","linesText","unitAmountCents","summary","filter","Boolean","footer","receiptFooter","occurrence","emailKey","BUYER_NOTIFICATION_EMAIL_KEYS","receipt","tokens","subject","text","sms","timelineDetail","fulfillment","fulfillments","fulfillmentId","entry","id","sort","a","b","atMs","shipped","fulfillmentLineQuantities","lineItemId","tracking","fulfillmentTrackingUrl","hasTracking","trackingNumber","physical","some","productType","channel","remainingUnits","remainingFulfillmentLines","reduce","sum","carrier","carrierLabelFor","trackingSentence","remainingSentence","delivered","refundCents","Math","max","round","refundedLines","refundLineIndexes","index","note","fullyRefunded","refundId","refundedCents","refunded","wasPaid","paymentIntentId","status","timeline","cancelled","claimMessage","orderRef","marker","runTransaction","transaction","snapshot","exists","sent","update","state","Date","now","settleMessage","detail","liftLegacyOrder","appendOrderEvent","error","console","releaseMessage","FieldValue","delete","undefined","deliverEmail","to","message","context","designed","org","html","fromName","resolveBrandingProfile","sendingIdentity","audience","owedFor","deliverSms","quiet","isConfigured","refused","timeZone","owner","outcome","send","body","purpose","quietHours","scheduledForMs","downloadLinksFor","origin","digital","length","token","fromEntries","encodeURIComponent","readOrder","emailOf","value","email","trim","test","notifyOrderBuyer","app","toggles","buyerNotificationEnabled","customerEmail","phone","customerPhone","targets","push","channels","target","buyerNotificationMarker","claimed","held","Error","sendOrderReceipt","normalizePhone","onlineReceiptExtras","enabled"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-notifications.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { pluginSmsMessaging } from '@aglyn/aglyn/plugin-manager/plugin-sms-messaging'\nimport { resolveSiteTimeZone } from '@aglyn/aglyn/app-utils/collection-entry-date'\nimport {\n resolveHostToken,\n type HostTokenSource,\n} from '@aglyn/aglyn/app-utils/host-tokens'\nimport {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n renderHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport * as CommerceModel from '../model'\nimport { mintDownloadToken } from './download'\nimport { orderStatusUrl } from './order-status-token'\n\n/**\n * Every message a buyer gets about their order, through ONE door (AGL-3610).\n *\n * Before this, the buyer heard about their order once — the online receipt —\n * and never again: a register sale sent nothing, a paid payment link sent\n * nothing, and shipping, delivery, refunds and cancellations were silent\n * (`fulfill-order.ts` said so on purpose). Every path that moves an order now\n * makes one call here, and this decides what that moment owes the buyer:\n *\n * - whether the store turned the moment off (`buyerNotifications` on the\n * store settings; ON unless explicitly off — see the model),\n * - which channels can carry it (email when mail is configured and the\n * order has an address; a text when the platform's SMS provider is\n * configured and the order carries the buyer's phone),\n * - and whether it was already sent.\n *\n * ## ONCE PER MOMENT, PER OCCURRENCE, PER CHANNEL\n *\n * Stripe redelivers webhooks, admins click twice, a lost response is retried.\n * Each message is CLAIMED before it is sent: a transaction reads the order's\n * `buyerNotifications` map and writes a `sending` marker for\n * `event__occurrence__channel` only if none is there. Exactly one of two\n * racing callers wins the claim; the other returns `already`. The occurrence\n * is the shipment id for `shipped` and the refund id for `refunded`, so a\n * second parcel or a second partial refund is a new message, while a\n * redelivered one is not.\n *\n * A send that fails RELEASES its claim so the next attempt can try again. A\n * process that dies between claim and send leaves the marker in `sending` and\n * the message unsent — at most once, never twice, which is the right side to\n * fail on for mail a buyer reads as \"did they charge me again?\".\n *\n * ## NEVER THROWS\n *\n * Every caller is past its point of no return — money moved, an order\n * shipped — so a mail outage must not turn into a 500 that invites the\n * merchant to do it again. Failures are logged and returned as outcomes.\n *\n * Shared with the order-processing and POS lanes (AGL-3611, AGL-3607): call\n * {@link notifyOrderBuyer} from any new path that moves an order, and\n * {@link sendOrderReceipt} for an explicit, merchant-asked send.\n */\n\nexport type OrderBuyerEvent = CommerceModel.BuyerNotificationEvent\n\nexport interface OrderRef {\n hostId: string\n orderId: string\n}\n\nexport interface NotifyOrderBuyerOptions {\n /** `shipped`: which shipment. Absent means the order's latest one. */\n fulfillmentId?: string\n /** `refunded`: the refund's own id (Stripe's), the occurrence key. */\n refundId?: string\n /** `refunded`: what THIS refund moved, in cents. */\n refundCents?: number\n /** `refunded`: the line indexes refunded by name, when any were. */\n refundLineIndexes?: readonly number[]\n /** `refunded`: whether this refund closed the order. */\n fullyRefunded?: boolean\n /**\n * The buyer's address when the order document does not carry it — a POS\n * folio sale attributed to the reservation's guest, a payment link whose\n * payer Stripe named. The order's own `customerEmail` wins when present.\n */\n email?: string | null\n /** Injected in specs. */\n firestore?: FirebaseFirestore.Firestore\n}\n\nexport type OrderBuyerChannelOutcome =\n | { channel: CommerceModel.BuyerNotificationChannel; outcome: 'sent' }\n | { channel: CommerceModel.BuyerNotificationChannel; outcome: 'already' }\n | {\n channel: CommerceModel.BuyerNotificationChannel\n outcome: 'failed'\n error: string\n }\n\nexport type NotifyOrderBuyerOutcome =\n | { outcome: 'disabled' }\n | { outcome: 'no_such_order' }\n /** The moment does not apply to this order (a counter sale \"shipping\"). */\n | { outcome: 'not_applicable' }\n /** No channel could carry it: no address, no mail or text configured. */\n | { outcome: 'no_recipient' }\n | { outcome: 'handled'; channels: OrderBuyerChannelOutcome[] }\n\ninterface StoreContext {\n firestore: FirebaseFirestore.Firestore\n hostRef: FirebaseFirestore.DocumentReference\n host: Record<string, unknown>\n settings: Record<string, unknown>\n currency: string\n}\n\nasync function readStore(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<StoreContext> {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, settingsSnapshot] = await Promise.all([\n hostRef.get().catch(() => null),\n hostRef.collection('settings').doc('store').get().catch(() => null),\n ])\n const settings = (settingsSnapshot?.data?.() ?? {}) as Record<string, unknown>\n return {\n firestore,\n hostRef,\n host: (hostSnapshot?.data?.() ?? {}) as Record<string, unknown>,\n settings,\n currency: String(settings['currency'] ?? 'USD') || 'USD',\n }\n}\n\nfunction lineLabel(line: CommerceModel.OrderLineItem): string {\n return `${line.quantity}× ${line.name}${\n line.variantLabel ? ` (${line.variantLabel})` : ''\n }`\n}\n\n/** The status link, or the store's home page when no link can be minted. */\nfunction statusLink(store: StoreContext, ref: OrderRef): string {\n return (\n orderStatusUrl(store.host as never, ref.hostId, ref.orderId) ??\n Aglyn.hostPublicOrigin({\n cname: typeof store.host['cname'] === 'string' ? store.host['cname'] : null,\n subdomain:\n typeof store.host['subdomain'] === 'string'\n ? store.host['subdomain']\n : null,\n }) ??\n ''\n )\n}\n\ninterface ComposedMessage {\n emailKey: string\n /** Tokens for the designed email. */\n tokens: Record<string, string>\n /** Subject when the template renders none. */\n subject: string\n /** Plain-text body when the template renders none. */\n text: string\n /** The text message, kept short: one or two segments. */\n sms: string\n /** The order timeline line recording the send, channel appended. */\n timelineDetail: string\n}\n\n/** What the moment says. `null` when it does not apply to this order. */\nexport function composeOrderBuyerMessage(input: {\n event: OrderBuyerEvent\n order: CommerceModel.HostOrder\n orderId: string\n businessName: string\n currency: string\n statusUrl: string\n receiptFooter?: string\n /** Absolute digital-delivery links for a receipt, by product id. */\n downloadLinks?: Record<string, string>\n options?: NotifyOrderBuyerOptions\n}): (ComposedMessage & { occurrence: string }) | null {\n const {\n event,\n order,\n orderId,\n businessName,\n currency,\n statusUrl,\n options = {},\n } = input\n const lines = order.lineItems ?? []\n const number = CommerceModel.formatOrderNumber(order, orderId)\n const money = (cents: number) =>\n CommerceModel.formatOrderMoney(cents, currency)\n const store = businessName || 'The store'\n const base = {\n 'order.number': number,\n 'order.statusUrl': statusUrl,\n 'order.ref': orderId,\n }\n const link = statusUrl ? `\\n\\nView your order: ${statusUrl}` : ''\n const smsLink = statusUrl ? ` ${statusUrl}` : ''\n\n if (event === 'receipt') {\n const totalCents =\n order.totals?.totalCents ?? Number(order.amountCents ?? 0)\n const licenseText = Object.entries(\n ((order as { licenseKeys?: Record<string, string[]> }).licenseKeys ??\n {}) as Record<string, string[]>,\n )\n .flatMap(([productId, keys]) => {\n const line = lines.find((item) => item.productId === productId)\n return (keys ?? []).map(\n (key) => `License key (${line?.name ?? 'product'}): ${key}`,\n )\n })\n .join('\\n')\n const downloads = Object.entries(input.downloadLinks ?? {})\n .map(([productId, url]) => {\n const line = lines.find((item) => item.productId === productId)\n return `Download ${line?.name ?? 'your file'}: ${url}`\n })\n .join('\\n')\n const linesText = lines\n .map(\n (line) =>\n `${lineLabel(line)} — ${money(line.unitAmountCents * line.quantity)}`,\n )\n .join('\\n')\n const summary = [linesText, licenseText, downloads]\n .filter(Boolean)\n .join('\\n\\n')\n const footer = String(input.receiptFooter ?? '')\n return {\n occurrence: 'order',\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.receipt,\n tokens: {\n ...base,\n 'order.summary': summary,\n 'order.total': money(totalCents),\n 'store.receiptFooter': footer,\n },\n subject: 'Receipt for your order',\n text:\n `Thanks for your purchase!\\n\\n${summary}\\n\\nTotal: ${money(totalCents)}\\n` +\n `Order ${number}` +\n (footer ? `\\n\\n${footer}` : '') +\n link,\n sms: `${store}: receipt for order ${number}, ${money(totalCents)}.${smsLink}`,\n timelineDetail: 'Receipt sent',\n }\n }\n\n if (event === 'shipped') {\n const fulfillments = order.fulfillments ?? []\n const fulfillment = options.fulfillmentId\n ? fulfillments.find((entry) => entry.id === options.fulfillmentId)\n : [...fulfillments].sort((a, b) => (b.atMs ?? 0) - (a.atMs ?? 0))[0]\n if (!fulfillment) return null\n // Collected at the counter or handed over by the store's driver\n // (AGL-3624): \"picked up\" and \"delivered\" say so, never \"shipped\".\n if (fulfillment.handover) return null\n // The units THIS shipment carried (AGL-3611): a partial shipment of a\n // line names its own count, not the line's.\n const shipped = CommerceModel.fulfillmentLineQuantities(order, fulfillment)\n .map((entry) =>\n lines[entry.lineItemId] ? { ...lines[entry.lineItemId], quantity: entry.quantity } : null,\n )\n .filter(Boolean) as CommerceModel.OrderLineItem[]\n const tracking = CommerceModel.fulfillmentTrackingUrl(fulfillment)\n const hasTracking = Boolean(fulfillment.trackingNumber)\n // Nothing physical left the building: a register sale handed over the\n // counter, or a digital-only order the merchant marked fulfilled. A\n // \"your order is on its way\" email for either is noise at best.\n const physical = shipped.some(\n (line) =>\n line.productType !== 'digital' && line.productType !== 'service',\n )\n if (!hasTracking && (order.channel === 'pos' || !physical)) return null\n // Shippable units still out, over every active shipment (AGL-3611).\n const remainingUnits = CommerceModel.remainingFulfillmentLines(order).reduce(\n (sum, entry) => sum + entry.quantity,\n 0,\n )\n const carrier = CommerceModel.carrierLabelFor(fulfillment.carrier)\n const trackingSentence = hasTracking\n ? `Tracking: ${[carrier, fulfillment.trackingNumber]\n .filter(Boolean)\n .join(' ')}${tracking ? ` — ${tracking}` : ''}`\n : ''\n const remainingSentence =\n remainingUnits > 0\n ? `${remainingUnits} more item${remainingUnits === 1 ? '' : 's'} will ship separately.`\n : ''\n const summary = shipped.map(lineLabel).join('\\n')\n return {\n occurrence: fulfillment.id,\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.shipped,\n tokens: {\n ...base,\n 'shipment.summary': summary,\n 'shipment.carrier': carrier,\n 'shipment.trackingNumber': String(fulfillment.trackingNumber ?? ''),\n 'shipment.tracking': trackingSentence,\n 'shipment.remaining': remainingSentence,\n },\n subject: `Your order ${number} has shipped`,\n text:\n `${store} shipped items from order ${number}.\\n\\n${summary}` +\n (trackingSentence ? `\\n\\n${trackingSentence}` : '') +\n (remainingSentence ? `\\n\\n${remainingSentence}` : '') +\n link,\n sms:\n `${store}: order ${number} has shipped.` +\n (tracking ? ` Track it: ${tracking}` : smsLink),\n timelineDetail: 'Shipping confirmation sent',\n }\n }\n\n if (event === 'delivered') {\n // A counter sale is \"delivered\" the moment it is handed over.\n if (order.channel === 'pos') return null\n const summary = lines.map(lineLabel).join('\\n')\n return {\n occurrence: 'order',\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.delivered,\n tokens: { ...base, 'order.summary': summary },\n subject: `Your order ${number} was delivered`,\n text:\n `${store} marked order ${number} as delivered.\\n\\n${summary}` + link,\n sms: `${store}: order ${number} was delivered.${smsLink}`,\n timelineDetail: 'Delivery confirmation sent',\n }\n }\n\n if (event === 'ready_for_pickup' || event === 'picked_up') {\n const pickup = order.pickup\n if (order.fulfillmentMethod !== 'pickup' || !pickup) return null\n const summary = lines.map(lineLabel).join('\\n')\n const where = `${pickup.locationName}${pickup.address ? `, ${pickup.address}` : ''}`\n const hours = pickup.hours ? `Pickup hours:\\n${pickup.hours}` : ''\n const instructions = pickup.instructions ?? ''\n if (event === 'picked_up') {\n return {\n occurrence: 'order',\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.picked_up,\n tokens: { ...base, 'order.summary': summary, 'pickup.location': where },\n subject: `You picked up order ${number}`,\n text: `Order ${number} was picked up from ${where}. Thanks for shopping with ${store}.\\n\\n${summary}` + link,\n sms: `${store}: order ${number} was picked up. Thank you!${smsLink}`,\n timelineDetail: 'Pickup confirmation sent',\n }\n }\n return {\n occurrence: 'order',\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.ready_for_pickup,\n tokens: {\n ...base,\n 'order.summary': summary,\n 'pickup.location': where,\n 'pickup.hours': hours,\n 'pickup.instructions': instructions,\n },\n subject: `Your order ${number} is ready for pickup`,\n text:\n `Your order ${number} is ready for pickup at ${where}.\\n\\n${summary}` +\n (hours ? `\\n\\n${hours}` : '') +\n (instructions ? `\\n\\n${instructions}` : '') +\n `\\n\\nBring your order number, ${number}.` +\n link,\n sms: `${store}: order ${number} is ready for pickup at ${pickup.locationName}.${smsLink}`,\n timelineDetail: 'Ready-for-pickup notice sent',\n }\n }\n\n if (event === 'out_for_delivery') {\n const delivery = order.localDelivery\n if (order.fulfillmentMethod !== 'local_delivery' || !delivery) return null\n const summary = lines.map(lineLabel).join('\\n')\n const window = delivery.windowLabel ?? ''\n return {\n // Each run out the door is its own message: a failed drop sent out\n // again tells the buyer again.\n occurrence: String(delivery.outForDeliveryAtMs ?? 'order'),\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.out_for_delivery,\n tokens: { ...base, 'order.summary': summary, 'delivery.window': window },\n subject: `Your order ${number} is out for delivery`,\n text:\n `${store} is on the way with order ${number}` +\n (window ? `, in your window ${window}` : '') +\n `.\\n\\n${summary}` +\n link,\n sms: `${store}: order ${number} is out for delivery${window ? ` (${window})` : ''}.${smsLink}`,\n timelineDetail: 'Out-for-delivery notice sent',\n }\n }\n\n if (event === 'refunded') {\n const refundCents = Math.max(0, Math.round(Number(options.refundCents ?? 0)))\n if (!(refundCents > 0)) return null\n const refundedLines = (options.refundLineIndexes ?? [])\n .map((index) => lines[index])\n .filter(Boolean) as CommerceModel.OrderLineItem[]\n const summary = refundedLines.map(lineLabel).join('\\n')\n const note = options.fullyRefunded\n ? 'Your order has been refunded in full.'\n : 'The rest of your order is unchanged.'\n return {\n // The refund's own id is the occurrence; without one, the running\n // total after this refund is (each settled refund raises it).\n occurrence:\n options.refundId ||\n `total-${Number(order.refundedCents ?? 0) || refundCents}`,\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.refunded,\n tokens: {\n ...base,\n 'refund.amount': money(refundCents),\n 'refund.summary': summary,\n 'refund.note': note,\n },\n subject: `Refund for your order ${number}`,\n text:\n `${store} refunded ${money(refundCents)} on order ${number}.` +\n (summary ? `\\n\\n${summary}` : '') +\n `\\n\\n${note}\\n\\nRefunds go back to the original payment method and ` +\n 'usually appear within 5 to 10 business days, depending on your bank.' +\n link,\n sms: `${store}: ${money(refundCents)} refunded on order ${number}.${smsLink}`,\n timelineDetail: `Refund confirmation sent (${money(refundCents)})`,\n }\n }\n\n // cancelled\n const summary = lines.map(lineLabel).join('\\n')\n const wasPaid = Boolean(\n order.paymentIntentId ||\n (order.channel === 'pos' && order.status !== 'pending') ||\n (order.timeline ?? []).some((entry) => entry?.event === 'paid'),\n )\n const note = wasPaid\n ? `Questions about a payment? Reply to this email or contact ${store}.`\n : ''\n return {\n occurrence: 'order',\n emailKey: CommerceModel.BUYER_NOTIFICATION_EMAIL_KEYS.cancelled,\n tokens: { ...base, 'order.summary': summary, 'cancel.note': note },\n subject: `Your order ${number} was canceled`,\n text:\n `${store} canceled order ${number}.\\n\\n${summary}` +\n (note ? `\\n\\n${note}` : '') +\n link,\n sms: `${store}: order ${number} was canceled.${smsLink}`,\n timelineDetail: 'Cancellation notice sent',\n }\n}\n\n/**\n * Claims one message in a transaction. `true` when this caller owns the send;\n * `false` when a marker is already there (sent, or being sent).\n */\nasync function claimMessage(\n firestore: FirebaseFirestore.Firestore,\n orderRef: FirebaseFirestore.DocumentReference,\n marker: string,\n channel: CommerceModel.BuyerNotificationChannel,\n): Promise<boolean> {\n return firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return false\n const sent = (snapshot.get('buyerNotifications') ?? {}) as Record<\n string,\n unknown\n >\n if (sent[marker]) return false\n transaction.update(orderRef, {\n [`buyerNotifications.${marker}`]: {\n state: 'sending',\n channel,\n atMs: Date.now(),\n },\n })\n return true\n })\n}\n\n/** Marks a claimed message sent and records it on the order timeline. */\nasync function settleMessage(\n firestore: FirebaseFirestore.Firestore,\n orderRef: FirebaseFirestore.DocumentReference,\n marker: string | null,\n channel: CommerceModel.BuyerNotificationChannel,\n detail: string,\n): Promise<void> {\n await firestore\n .runTransaction(async (transaction) => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return\n const order = CommerceModel.liftLegacyOrder(\n (snapshot.data() ?? {}) as never,\n )\n const atMs = Date.now()\n transaction.update(orderRef, {\n ...(marker\n ? {\n [`buyerNotifications.${marker}`]: { state: 'sent', channel, atMs },\n }\n : {}),\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'buyer-notified',\n `${detail} by ${channel === 'sms' ? 'text' : 'email'}`,\n atMs,\n ),\n })\n })\n .catch((error) => {\n console.error('[order-notifications] settle failed', marker, error)\n })\n}\n\nasync function releaseMessage(\n orderRef: FirebaseFirestore.DocumentReference,\n marker: string,\n): Promise<void> {\n await orderRef\n .update({\n [`buyerNotifications.${marker}`]:\n firebaseAdmin.firestore.FieldValue.delete(),\n })\n .catch(() => undefined)\n}\n\nasync function deliverEmail(\n store: StoreContext,\n ref: OrderRef,\n to: string,\n message: ComposedMessage,\n context: string,\n): Promise<void> {\n const designed = await renderHostEmailWithTokens(\n store.firestore,\n ref.hostId,\n message.emailKey,\n message.tokens,\n )\n const org = await getOrgForHost(ref.hostId).catch(() => null)\n await sendEmail({\n to,\n subject: designed?.subject ?? message.subject,\n text: designed?.text || message.text,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: Aglyn.resolveBrandingProfile(org?.org as never).fromName,\n sendingIdentity: await hostSendingIdentity(ref.hostId),\n audience: 'tenant',\n context,\n // Owed to the recipient by their own order: the phishing screen's soft\n // rules never hold it (AGL-3356).\n owedFor: 'order',\n })\n // Cost meter (AGL-1438), as every transactional store email.\n await meterHostEmail(ref.hostId)\n}\n\n/**\n * What a text attempt came to: `refused` names why nothing went (null when it\n * went), and `scheduledForMs` says it was held for the buyer's morning.\n */\ninterface SmsDelivery {\n refused: string | null\n scheduledForMs?: number\n}\n\n/**\n * Sends one order text. A receipt goes at once — the buyer is at the counter\n * or the checkout waiting for it. Every later moment (shipped, delivered,\n * refunded, canceled) can fire at any hour, from a carrier scan or a late\n * packing shift, so it asks the provider to keep quiet hours in the STORE's\n * zone: the best stand-in the platform has for a buyer whose own zone it was\n * never told, and right for the local and regional stores most texts come\n * from. Held texts arrive at 8 AM there.\n */\nasync function deliverSms(\n ref: OrderRef,\n to: string,\n message: ComposedMessage,\n context: string,\n quiet: { host: Record<string, unknown> } | null,\n): Promise<SmsDelivery> {\n const sms = pluginSmsMessaging()\n if (!sms?.isConfigured()) return { refused: 'Text messages are not configured' }\n let timeZone: string | null = null\n if (quiet) {\n const owner = await getOrgForHost(ref.hostId).catch(() => null)\n timeZone = resolveSiteTimeZone(\n (owner?.org ?? null) as { timeZone?: string } | null,\n quiet.host as { timeZone?: string },\n )\n }\n const outcome = await sms.send({\n to,\n body: message.sms,\n hostId: ref.hostId,\n purpose: 'transactional',\n context,\n ...(timeZone ? { quietHours: { timeZone } } : {}),\n })\n if (outcome.status !== 'sent') return { refused: outcome.status }\n return {\n refused: null,\n ...(outcome.scheduledForMs ? { scheduledForMs: outcome.scheduledForMs } : {}),\n }\n}\n\n/** Absolute download links for a receipt's digital lines. */\nfunction downloadLinksFor(\n store: StoreContext,\n ref: OrderRef,\n order: CommerceModel.HostOrder,\n): Record<string, string> {\n const origin = Aglyn.hostPublicOrigin({\n cname: typeof store.host['cname'] === 'string' ? store.host['cname'] : null,\n subdomain:\n typeof store.host['subdomain'] === 'string' ? store.host['subdomain'] : null,\n })\n const digital = (order.lineItems ?? []).filter(\n (line) => line.productType === 'digital',\n )\n if (!origin || !digital.length) return {}\n let token: string\n try {\n token = mintDownloadToken(ref.hostId, ref.orderId)\n } catch {\n return {}\n }\n return Object.fromEntries(\n digital.map((line) => [\n line.productId,\n `${origin}/api/commerce/download?hostId=${encodeURIComponent(ref.hostId)}` +\n `&orderId=${encodeURIComponent(ref.orderId)}` +\n `&productId=${encodeURIComponent(line.productId)}&token=${token}`,\n ]),\n )\n}\n\nasync function readOrder(\n firestore: FirebaseFirestore.Firestore,\n ref: OrderRef,\n): Promise<{\n orderRef: FirebaseFirestore.DocumentReference\n order: CommerceModel.HostOrder | null\n}> {\n const orderRef = firestore\n .collection('hosts')\n .doc(ref.hostId)\n .collection('orders')\n .doc(ref.orderId)\n const snapshot = await orderRef.get()\n return {\n orderRef,\n order: snapshot.exists\n ? CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n : null,\n }\n}\n\nfunction emailOf(value: unknown): string {\n const email = String(value ?? '').trim()\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) ? email : ''\n}\n\n/**\n * Tells the buyer about one moment in their order's life, once. See the\n * module header. Never throws.\n */\nexport async function notifyOrderBuyer(\n ref: OrderRef,\n event: OrderBuyerEvent,\n options: NotifyOrderBuyerOptions = {},\n): Promise<NotifyOrderBuyerOutcome> {\n try {\n if (!ref.hostId || !ref.orderId) return { outcome: 'no_such_order' }\n const firestore = options.firestore ?? firebaseAdmin.app().firestore()\n const store = await readStore(firestore, ref.hostId)\n const toggles = store.settings['buyerNotifications']\n if (!CommerceModel.buyerNotificationEnabled(toggles, event)) {\n return { outcome: 'disabled' }\n }\n const { orderRef, order } = await readOrder(firestore, ref)\n if (!order) return { outcome: 'no_such_order' }\n const message = composeOrderBuyerMessage({\n event,\n order,\n orderId: ref.orderId,\n businessName: String(\n resolveHostToken('businessName', store.host as HostTokenSource) ?? '',\n ),\n currency: store.currency,\n statusUrl: statusLink(store, ref),\n receiptFooter: String(store.settings['receiptFooter'] ?? ''),\n downloadLinks:\n event === 'receipt' ? downloadLinksFor(store, ref, order) : undefined,\n options,\n })\n if (!message) return { outcome: 'not_applicable' }\n\n const email = emailOf(order.customerEmail) || emailOf(options.email)\n const phone = String(order.customerPhone ?? '').trim()\n const targets: {\n channel: CommerceModel.BuyerNotificationChannel\n to: string\n }[] = []\n if (email && isEmailConfigured()) targets.push({ channel: 'email', to: email })\n if (\n phone &&\n CommerceModel.buyerNotificationEnabled(toggles, 'texts') &&\n pluginSmsMessaging()?.isConfigured()\n ) {\n targets.push({ channel: 'sms', to: phone })\n }\n if (!targets.length) return { outcome: 'no_recipient' }\n\n const channels: OrderBuyerChannelOutcome[] = []\n for (const target of targets) {\n const marker = CommerceModel.buyerNotificationMarker(\n event,\n message.occurrence,\n target.channel,\n )\n const claimed = await claimMessage(\n firestore,\n orderRef,\n marker,\n target.channel,\n ).catch((error) => {\n console.error('[order-notifications] claim failed', marker, error)\n return false\n })\n if (!claimed) {\n channels.push({ channel: target.channel, outcome: 'already' })\n continue\n }\n const context = message.emailKey\n let held: number | null = null\n try {\n if (target.channel === 'email') {\n await deliverEmail(store, ref, target.to, message, context)\n } else {\n const sent = await deliverSms(\n ref,\n target.to,\n message,\n context,\n event === 'receipt' ? null : { host: store.host },\n )\n if (sent.refused) throw new Error(sent.refused)\n held = sent.scheduledForMs ?? null\n }\n await settleMessage(\n firestore,\n orderRef,\n marker,\n target.channel,\n held\n ? `${message.timelineDetail} (held for the morning)`\n : message.timelineDetail,\n )\n channels.push({ channel: target.channel, outcome: 'sent' })\n } catch (error) {\n await releaseMessage(orderRef, marker)\n console.error('[order-notifications] send failed', {\n hostId: ref.hostId,\n orderId: ref.orderId,\n event,\n channel: target.channel,\n error,\n })\n channels.push({\n channel: target.channel,\n outcome: 'failed',\n error: error instanceof Error ? error.message : String(error),\n })\n }\n }\n return { outcome: 'handled', channels }\n } catch (error) {\n console.error('[order-notifications] notify failed', ref, event, error)\n return { outcome: 'handled', channels: [] }\n }\n}\n\nexport type SendOrderReceiptOutcome =\n | { outcome: 'sent'; channel: CommerceModel.BuyerNotificationChannel }\n | { outcome: 'no_such_order' }\n | { outcome: 'not_configured' }\n | { outcome: 'invalid_recipient' }\n | { outcome: 'failed'; error: string }\n\n/**\n * Sends the receipt again, on the merchant's say-so, to the address or number\n * they gave (AGL-3610) — the order dialog's \"Resend receipt\".\n *\n * NOT idempotent, on purpose: a resend IS the second send. The route in front\n * of it is rate-limited instead, and the send is recorded on the timeline so\n * the merchant can see it went. It does not consult the store's moment\n * switches either — a merchant who turned automatic receipts off can still\n * hand one to a customer who asks.\n */\nexport async function sendOrderReceipt(\n ref: OrderRef,\n input: {\n channel: CommerceModel.BuyerNotificationChannel\n to: string\n firestore?: FirebaseFirestore.Firestore\n },\n): Promise<SendOrderReceiptOutcome> {\n try {\n const firestore = input.firestore ?? firebaseAdmin.app().firestore()\n const { orderRef, order } = await readOrder(firestore, ref)\n if (!order) return { outcome: 'no_such_order' }\n if (input.channel === 'email') {\n if (!isEmailConfigured()) return { outcome: 'not_configured' }\n if (!emailOf(input.to)) return { outcome: 'invalid_recipient' }\n } else {\n if (!pluginSmsMessaging()?.isConfigured()) {\n return { outcome: 'not_configured' }\n }\n if (!Aglyn.normalizePhone(input.to)) {\n return { outcome: 'invalid_recipient' }\n }\n }\n const store = await readStore(firestore, ref.hostId)\n const message = composeOrderBuyerMessage({\n event: 'receipt',\n order,\n orderId: ref.orderId,\n businessName: String(\n resolveHostToken('businessName', store.host as HostTokenSource) ?? '',\n ),\n currency: store.currency,\n statusUrl: statusLink(store, ref),\n receiptFooter: String(store.settings['receiptFooter'] ?? ''),\n downloadLinks: downloadLinksFor(store, ref, order),\n })\n if (!message) return { outcome: 'failed', error: 'Nothing to send' }\n if (input.channel === 'email') {\n await deliverEmail(store, ref, emailOf(input.to), message, 'order-receipt')\n } else {\n const sent = await deliverSms(ref, input.to, message, 'order-receipt', null)\n if (sent.refused) return { outcome: 'failed', error: sent.refused }\n }\n await settleMessage(firestore, orderRef, null, input.channel, 'Receipt re-sent')\n return { outcome: 'sent', channel: input.channel }\n } catch (error) {\n console.error('[order-notifications] receipt send failed', ref, error)\n return {\n outcome: 'failed',\n error: error instanceof Error ? error.message : String(error),\n }\n }\n}\n\n/**\n * What the online receipt sends needs from this module (AGL-3610): whether\n * the store left receipts on, and the `order.number` / `order.statusUrl`\n * tokens. The cart and buy-now receipts compose their own summary (license\n * keys and download links built from the Stripe session) inside the\n * webhook's redelivery guard, so they ask here rather than route through\n * {@link notifyOrderBuyer}.\n */\nexport async function onlineReceiptExtras(\n hostId: string,\n orderId: string,\n firestore: FirebaseFirestore.Firestore = firebaseAdmin.app().firestore(),\n): Promise<{ enabled: boolean; tokens: Record<string, string> }> {\n try {\n const store = await readStore(firestore, hostId)\n const { order } = await readOrder(firestore, { hostId, orderId })\n return {\n enabled: CommerceModel.buyerNotificationEnabled(\n store.settings['buyerNotifications'],\n 'receipt',\n ),\n tokens: {\n 'order.number': CommerceModel.formatOrderNumber(order ?? {}, orderId),\n 'order.statusUrl': statusLink(store, { hostId, orderId }),\n },\n }\n } catch (error) {\n console.error('[order-notifications] receipt extras failed', error)\n return { enabled: true, tokens: { 'order.number': '', 'order.statusUrl': '' } }\n }\n}\n"],"names":["Aglyn","pluginSmsMessaging","resolveSiteTimeZone","resolveHostToken","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","renderHostEmailWithTokens","isEmailConfigured","sendEmail","CommerceModel","mintDownloadToken","orderStatusUrl","readStore","firestore","hostId","settings","settingsSnapshot","hostSnapshot","hostRef","collection","doc","Promise","all","get","catch","data","host","currency","String","lineLabel","line","quantity","name","variantLabel","statusLink","store","ref","orderId","hostPublicOrigin","cname","subdomain","composeOrderBuyerMessage","input","order","event","businessName","statusUrl","options","lines","lineItems","number","formatOrderNumber","money","cents","formatOrderMoney","base","link","smsLink","totalCents","totals","Number","amountCents","licenseText","Object","entries","licenseKeys","flatMap","productId","keys","find","item","map","key","join","downloads","downloadLinks","url","linesText","unitAmountCents","summary","filter","Boolean","footer","receiptFooter","occurrence","emailKey","BUYER_NOTIFICATION_EMAIL_KEYS","receipt","tokens","subject","text","sms","timelineDetail","fulfillment","fulfillments","fulfillmentId","entry","id","sort","a","b","atMs","handover","shipped","fulfillmentLineQuantities","lineItemId","tracking","fulfillmentTrackingUrl","hasTracking","trackingNumber","physical","some","productType","channel","remainingUnits","remainingFulfillmentLines","reduce","sum","carrier","carrierLabelFor","trackingSentence","remainingSentence","delivered","pickup","fulfillmentMethod","where","locationName","address","hours","instructions","picked_up","ready_for_pickup","delivery","localDelivery","window","windowLabel","outForDeliveryAtMs","out_for_delivery","refundCents","Math","max","round","refundedLines","refundLineIndexes","index","note","fullyRefunded","refundId","refundedCents","refunded","wasPaid","paymentIntentId","status","timeline","cancelled","claimMessage","orderRef","marker","runTransaction","transaction","snapshot","exists","sent","update","state","Date","now","settleMessage","detail","liftLegacyOrder","appendOrderEvent","error","console","releaseMessage","FieldValue","delete","undefined","deliverEmail","to","message","context","designed","org","html","fromName","resolveBrandingProfile","sendingIdentity","audience","owedFor","deliverSms","quiet","isConfigured","refused","timeZone","owner","outcome","send","body","purpose","quietHours","scheduledForMs","downloadLinksFor","origin","digital","length","token","fromEntries","encodeURIComponent","readOrder","emailOf","value","email","trim","test","notifyOrderBuyer","app","toggles","buyerNotificationEnabled","customerEmail","phone","customerPhone","targets","push","channels","target","buyerNotificationMarker","claimed","held","Error","sendOrderReceipt","normalizePhone","onlineReceiptExtras","enabled"],"mappings":"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"}