@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/model/local-fulfillment-settings.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 {\n parseOpeningHours,\n type OpeningHoursDay,\n type OpeningHoursRow,\n} from '@aglyn/aglyn/app-utils/local-business'\nimport { zonedDateTime, zonedWallTimeToInstant } from '@aglyn/shared-util-timestamp/zoned-time'\nimport type { InventoryLocation, PostalAddress } from './commerce'\nimport type { PickupLocationSettings } from './order-local-fulfillment'\n\n/**\n * Buy online, pick up in store, and the store's own local delivery (AGL-3624):\n * what a merchant configures, and the pure decisions checkout takes from it.\n *\n * PICKUP is per location. A location of `hosts/{hostId}/locations` (AGL-286,\n * the same stock buckets the register sells from) offers pickup when its\n * `pickup.enabled` is on, with its own hours and arrival instructions. The\n * buyer picks one at the cart; the order is routed there and its stock comes\n * off that location's bucket.\n *\n * LOCAL DELIVERY is per store, on `settings/store.localDelivery`: zones\n * matched by postal code (no outside service needed) or by distance from the\n * store (only where the store's address check returns map coordinates — see\n * `server/local-fulfillment.ts`), each with a fee, an order minimum and a\n * free-over threshold, and delivery windows written in the same\n * `Mo-Fr 09:00-12:00` syntax as a business's opening hours.\n */\n\n/** A pickup location as the cart and the order show it. Public facts only. */\nexport interface PickupLocationOption {\n id: string\n name: string\n address?: string\n hours?: string\n instructions?: string\n readyWithinMinutes?: number\n}\n\nexport const PICKUP_INSTRUCTIONS_MAX = 400\nexport const PICKUP_HOURS_MAX = 600\nexport const PICKUP_LOCATIONS_MAX = 25\n\nconst text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\nconst wholeMinutes = (value: unknown, max: number): number | undefined => {\n const minutes = Math.round(Number(value))\n return Number.isFinite(minutes) && minutes > 0 ? Math.min(minutes, max) : undefined\n}\n\n/** A stored pickup block made safe; never throws. */\nexport function normalizePickupSettings(value: unknown): PickupLocationSettings {\n if (!value || typeof value !== 'object') return {}\n const raw = value as Record<string, unknown>\n const settings: PickupLocationSettings = {}\n if (raw['enabled'] === true) settings.enabled = true\n const hours = text(raw['hours'], PICKUP_HOURS_MAX)\n if (hours) settings.hours = hours\n const instructions = text(raw['instructions'], PICKUP_INSTRUCTIONS_MAX)\n if (instructions) settings.instructions = instructions\n const ready = wholeMinutes(raw['readyWithinMinutes'], 14 * 24 * 60)\n if (ready) settings.readyWithinMinutes = ready\n return settings\n}\n\n/** A postal address on one line, or the location's free-text address. */\nexport function formatPostalAddressLine(address?: PostalAddress | null, fallback?: string | null): string {\n const parts = [\n address?.line1,\n address?.line2,\n [address?.city, address?.state].filter(Boolean).join(', '),\n address?.postalCode,\n ]\n .map((part) => String(part ?? '').trim())\n .filter(Boolean)\n return parts.length ? parts.join(', ') : String(fallback ?? '').trim()\n}\n\n/**\n * The locations a buyer may pick up from, default location first, then by\n * name. A location without pickup switched on is never offered, however\n * complete the rest of it is.\n */\nexport function pickupLocationOptions(\n locations: ReadonlyArray<(InventoryLocation & { id: string }) | null | undefined>,\n): PickupLocationOption[] {\n return locations\n .filter((location): location is InventoryLocation & { id: string } =>\n Boolean(location?.id && normalizePickupSettings(location.pickup).enabled),\n )\n .sort(\n (a, b) =>\n Number(Boolean(b.isDefault)) - Number(Boolean(a.isDefault)) ||\n String(a.name ?? '').localeCompare(String(b.name ?? '')),\n )\n .slice(0, PICKUP_LOCATIONS_MAX)\n .map((location) => {\n const pickup = normalizePickupSettings(location.pickup)\n const address = formatPostalAddressLine(location.postalAddress, location.address)\n return {\n id: location.id,\n name: String(location.name || 'Store').slice(0, 80),\n ...(address ? { address } : {}),\n ...(pickup.hours ? { hours: pickup.hours } : {}),\n ...(pickup.instructions ? { instructions: pickup.instructions } : {}),\n ...(pickup.readyWithinMinutes ? { readyWithinMinutes: pickup.readyWithinMinutes } : {}),\n }\n })\n}\n\n/** \"Usually ready in 2 hours\" from a preparation time, or `''`. */\nexport function pickupReadyWithinLabel(minutes: number | undefined): string {\n if (!minutes || minutes <= 0) return ''\n if (minutes < 60) return `Usually ready in ${minutes} minutes`\n const hours = Math.round(minutes / 60)\n if (hours < 48) return `Usually ready in ${hours} hour${hours === 1 ? '' : 's'}`\n const days = Math.round(hours / 24)\n return `Usually ready in ${days} days`\n}\n\n// ---------------------------------------------------------------------------\n// Local delivery\n// ---------------------------------------------------------------------------\n\nexport interface LocalDeliveryCoordinates {\n lat: number\n lng: number\n}\n\nexport interface LocalDeliveryZone {\n id: string\n name: string\n /**\n * `postcode` matches the buyer's postal code against `postcodes`;\n * `radius` measures the distance from the store's `origin`.\n */\n kind: 'postcode' | 'radius'\n /**\n * Codes this zone delivers to: `10001` exactly, `100*` by prefix, or\n * `10001-10099` as a numeric range.\n */\n postcodes?: string[]\n /** Kilometers from the store's origin, for a `radius` zone. */\n radiusKm?: number\n /** The delivery fee, in cents. */\n feeCents: number\n /** The smallest order (items, in cents) this zone delivers. */\n minimumCents?: number\n /** Items total (cents) at or above which delivery is free. */\n freeOverCents?: number\n}\n\n/** `settings/store.localDelivery`. */\nexport interface LocalDeliverySettings {\n enabled?: boolean\n /** The one country delivered within, ISO-3166 alpha-2. */\n country?: string\n /** The location orders leave from, and whose stock they take. */\n locationId?: string\n /** Where radius zones measure from, as the store's address check placed it. */\n origin?: LocalDeliveryCoordinates\n zones?: LocalDeliveryZone[]\n /** Delivery windows, one line per set of days: `Mo-Fr 09:00-12:00`. */\n windows?: string\n /** How long before a window opens an order must be placed, in minutes. */\n leadTimeMinutes?: number\n /** How many days ahead a window can be booked. */\n daysAhead?: number\n /** What the buyer is told about delivery: \"We call when we're 10 minutes away\". */\n instructions?: string\n}\n\nexport const LOCAL_DELIVERY_ZONES_MAX = 20\nexport const LOCAL_DELIVERY_POSTCODES_MAX = 200\nexport const LOCAL_DELIVERY_DEFAULT_LEAD_MINUTES = 120\nexport const LOCAL_DELIVERY_DEFAULT_DAYS_AHEAD = 7\nexport const LOCAL_DELIVERY_MAX_DAYS_AHEAD = 30\n/** A cart offers at most this many windows; the earliest are kept. */\nexport const LOCAL_DELIVERY_WINDOWS_OFFERED_MAX = 40\nexport const LOCAL_DELIVERY_RADIUS_MAX_KM = 200\n\nconst wholeCents = (value: unknown): number => {\n const cents = Math.round(Number(value ?? 0))\n return Number.isFinite(cents) && cents > 0 ? cents : 0\n}\n\nconst coordinates = (value: unknown): LocalDeliveryCoordinates | undefined => {\n if (!value || typeof value !== 'object') return undefined\n const lat = Number((value as Record<string, unknown>)['lat'])\n const lng = Number((value as Record<string, unknown>)['lng'])\n if (!Number.isFinite(lat) || !Number.isFinite(lng)) return undefined\n if (Math.abs(lat) > 90 || Math.abs(lng) > 180) return undefined\n return { lat, lng }\n}\n\n/**\n * One postal code as zones compare it: upper-cased with spaces removed, and a\n * US ZIP+4 cut to its five digits so `10001-1234` is in the `10001` zone.\n */\nexport function normalizePostcode(value: unknown, country?: string): string {\n const code = String(value ?? '').toUpperCase().replace(/\\s+/g, '').slice(0, 16)\n if (String(country ?? '').toUpperCase() === 'US') {\n const zip = /^(\\d{5})(?:-?\\d{4})?$/.exec(code)\n if (zip) return zip[1]\n }\n return code\n}\n\n/** One zone entry as the console saves it: trimmed, upper-cased, spaces gone. */\nfunction normalizePostcodePattern(value: unknown): string {\n return String(value ?? '').toUpperCase().replace(/\\s+/g, '').slice(0, 24)\n}\n\n/** Whether a postal code is one a zone entry names. */\nexport function postcodePatternMatches(pattern: string, postcode: string): boolean {\n const entry = normalizePostcodePattern(pattern)\n const code = String(postcode ?? '').toUpperCase().replace(/\\s+/g, '')\n if (!entry || !code) return false\n if (entry.endsWith('*')) {\n const prefix = entry.slice(0, -1)\n return prefix.length > 0 && code.startsWith(prefix)\n }\n const range = /^(\\d+)-(\\d+)$/.exec(entry)\n if (range && range[1].length === range[2].length && /^\\d+$/.test(code)) {\n if (code.length !== range[1].length) return false\n const value = Number(code)\n const low = Math.min(Number(range[1]), Number(range[2]))\n const high = Math.max(Number(range[1]), Number(range[2]))\n return value >= low && value <= high\n }\n return entry === code\n}\n\n/** A stored zone made safe, or `null` when it cannot deliver anywhere. */\nfunction normalizeZone(value: unknown): LocalDeliveryZone | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n const id = text(raw['id'], 60)\n if (!id) return null\n const kind = raw['kind'] === 'radius' ? 'radius' : 'postcode'\n const zone: LocalDeliveryZone = {\n id,\n name: text(raw['name'], 80) || 'Local delivery',\n kind,\n feeCents: wholeCents(raw['feeCents']),\n }\n if (kind === 'postcode') {\n const codes = Array.isArray(raw['postcodes']) ? (raw['postcodes'] as unknown[]) : []\n zone.postcodes = [...new Set(codes.map(normalizePostcodePattern).filter(Boolean))].slice(\n 0,\n LOCAL_DELIVERY_POSTCODES_MAX,\n )\n } else {\n const radius = Number(raw['radiusKm'])\n zone.radiusKm =\n Number.isFinite(radius) && radius > 0\n ? Math.min(Math.round(radius * 10) / 10, LOCAL_DELIVERY_RADIUS_MAX_KM)\n : 0\n }\n const minimum = wholeCents(raw['minimumCents'])\n if (minimum) zone.minimumCents = minimum\n const freeOver = wholeCents(raw['freeOverCents'])\n if (freeOver) zone.freeOverCents = freeOver\n return zone\n}\n\n/** The stored settings made safe; never throws. */\nexport function normalizeLocalDeliverySettings(value: unknown): LocalDeliverySettings {\n if (!value || typeof value !== 'object') return {}\n const raw = value as Record<string, unknown>\n const settings: LocalDeliverySettings = {}\n if (raw['enabled'] === true) settings.enabled = true\n const country = String(raw['country'] ?? '').trim().toUpperCase()\n if (/^[A-Z]{2}$/.test(country)) settings.country = country\n const locationId = text(raw['locationId'], 120)\n if (locationId) settings.locationId = locationId\n const origin = coordinates(raw['origin'])\n if (origin) settings.origin = origin\n const zones = (Array.isArray(raw['zones']) ? (raw['zones'] as unknown[]) : [])\n .map(normalizeZone)\n .filter((zone): zone is LocalDeliveryZone => Boolean(zone))\n .slice(0, LOCAL_DELIVERY_ZONES_MAX)\n if (zones.length) settings.zones = zones\n const windows = text(raw['windows'], PICKUP_HOURS_MAX)\n if (windows) settings.windows = windows\n const lead = Math.round(Number(raw['leadTimeMinutes']))\n if (Number.isFinite(lead) && lead >= 0) settings.leadTimeMinutes = Math.min(lead, 14 * 24 * 60)\n const days = Math.round(Number(raw['daysAhead']))\n if (Number.isFinite(days) && days >= 1) settings.daysAhead = Math.min(days, LOCAL_DELIVERY_MAX_DAYS_AHEAD)\n const instructions = text(raw['instructions'], PICKUP_INSTRUCTIONS_MAX)\n if (instructions) settings.instructions = instructions\n return settings\n}\n\n/** Great-circle distance in kilometers (haversine). */\nexport function distanceKm(a: LocalDeliveryCoordinates, b: LocalDeliveryCoordinates): number {\n const radians = (degrees: number) => (degrees * Math.PI) / 180\n const dLat = radians(b.lat - a.lat)\n const dLng = radians(b.lng - a.lng)\n const h =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(radians(a.lat)) * Math.cos(radians(b.lat)) * Math.sin(dLng / 2) ** 2\n return 2 * 6371 * Math.asin(Math.min(1, Math.sqrt(h)))\n}\n\n/** Whether any zone measures distance, so checkout needs the full address placed on a map. */\nexport function localDeliveryHasRadiusZones(settings: LocalDeliverySettings | undefined): boolean {\n return Boolean(settings?.origin) && (settings?.zones ?? []).some((zone) => zone.kind === 'radius' && (zone.radiusKm ?? 0) > 0)\n}\n\n/**\n * The zone a destination falls in, or `null`. Postal-code zones are asked\n * first, in the order the store listed them, so a store can carve a cheaper\n * neighbourhood out of a wider one by listing it above. Radius zones answer\n * only with both ends on a map — the store's origin and the destination's\n * coordinates — and the tightest radius that reaches wins.\n */\nexport function matchLocalDeliveryZone(\n settings: LocalDeliverySettings | undefined,\n destination: { country?: string; postalCode?: string; coordinates?: LocalDeliveryCoordinates | null },\n): LocalDeliveryZone | null {\n if (!settings?.country) return null\n const country = String(destination.country ?? settings.country).toUpperCase()\n if (country !== settings.country) return null\n const postcode = normalizePostcode(destination.postalCode, settings.country)\n const zones = settings.zones ?? []\n if (postcode) {\n for (const zone of zones) {\n if (zone.kind !== 'postcode') continue\n if ((zone.postcodes ?? []).some((pattern) => postcodePatternMatches(pattern, postcode))) return zone\n }\n }\n const at = destination.coordinates ? coordinates(destination.coordinates) : undefined\n if (!at || !settings.origin) return null\n const distance = distanceKm(settings.origin, at)\n return (\n zones\n .filter((zone) => zone.kind === 'radius' && (zone.radiusKm ?? 0) > 0 && distance <= (zone.radiusKm ?? 0))\n .sort((a, b) => (a.radiusKm ?? 0) - (b.radiusKm ?? 0))[0] ?? null\n )\n}\n\n/** What a zone charges for an order of `itemsCents`. */\nexport function localDeliveryFeeCents(zone: LocalDeliveryZone, itemsCents: number): number {\n if (zone.freeOverCents && itemsCents >= zone.freeOverCents) return 0\n return Math.max(0, Math.round(zone.feeCents || 0))\n}\n\n/** How far an order of `itemsCents` is below the zone's minimum; 0 when it meets it. */\nexport function localDeliveryMinimumShortfall(zone: LocalDeliveryZone, itemsCents: number): number {\n return Math.max(0, (zone.minimumCents ?? 0) - Math.max(0, Math.round(itemsCents || 0)))\n}\n\n/** One bookable delivery window. `id` is its start instant, which is what the buyer sends back. */\nexport interface LocalDeliveryWindow {\n id: string\n startMs: number\n endMs: number\n}\n\nconst DAY_NAMES: readonly OpeningHoursDay[] = [\n 'Sunday',\n 'Monday',\n 'Tuesday',\n 'Wednesday',\n 'Thursday',\n 'Friday',\n 'Saturday',\n]\n\n/** The windows rows a store's settings parse to. Lines that do not read are dropped. */\nexport function localDeliveryWindowRows(settings: LocalDeliverySettings | undefined): OpeningHoursRow[] {\n return parseOpeningHours(settings?.windows ?? '')\n}\n\n/**\n * The windows a buyer may book now, soonest first: every row on every day\n * from today to `daysAhead`, in the store's time zone, that opens at least\n * the lead time from now. Pure in `nowMs`, so a spec can stand at any hour.\n */\nexport function upcomingLocalDeliveryWindows(\n settings: LocalDeliverySettings | undefined,\n context: { nowMs: number; timeZone: string },\n): LocalDeliveryWindow[] {\n const rows = localDeliveryWindowRows(settings)\n if (!rows.length) return []\n const lead = (settings?.leadTimeMinutes ?? LOCAL_DELIVERY_DEFAULT_LEAD_MINUTES) * 60_000\n const daysAhead = settings?.daysAhead ?? LOCAL_DELIVERY_DEFAULT_DAYS_AHEAD\n const today = zonedDateTime(context.nowMs, context.timeZone)\n const windows: LocalDeliveryWindow[] = []\n const seen = new Set<number>()\n for (let offset = 0; offset <= daysAhead; offset++) {\n // Noon of the day, so a daylight-saving change never lands it on the\n // wrong calendar date.\n const noon = zonedWallTimeToInstant(\n { year: today.year, month: today.month, day: today.day + offset, hour: 12 },\n context.timeZone,\n )\n const day = zonedDateTime(noon, context.timeZone)\n const name = DAY_NAMES[day.weekday]\n for (const row of rows) {\n if (!row.days.includes(name)) continue\n const [openHour, openMinute] = row.opens.split(':').map(Number)\n const [closeHour, closeMinute] = row.closes.split(':').map(Number)\n const startMs = zonedWallTimeToInstant(\n { year: day.year, month: day.month, day: day.day, hour: openHour, minute: openMinute },\n context.timeZone,\n )\n const overnight = closeHour * 60 + closeMinute <= openHour * 60 + openMinute\n const endMs = zonedWallTimeToInstant(\n {\n year: day.year,\n month: day.month,\n day: day.day + (overnight ? 1 : 0),\n hour: closeHour,\n minute: closeMinute,\n },\n context.timeZone,\n )\n if (startMs < context.nowMs + lead || seen.has(startMs)) continue\n seen.add(startMs)\n windows.push({ id: String(startMs), startMs, endMs })\n }\n }\n return windows.sort((a, b) => a.startMs - b.startMs).slice(0, LOCAL_DELIVERY_WINDOWS_OFFERED_MAX)\n}\n\n/** The bookable window starting at `startMs`, or `null` when no such window is open now. */\nexport function findLocalDeliveryWindow(\n settings: LocalDeliverySettings | undefined,\n startMs: unknown,\n context: { nowMs: number; timeZone: string },\n): LocalDeliveryWindow | null {\n const wanted = Math.round(Number(startMs))\n if (!Number.isFinite(wanted)) return null\n return upcomingLocalDeliveryWindows(settings, context).find((window) => window.startMs === wanted) ?? null\n}\n\n/** Whether checkout can offer local delivery at all. */\nexport function localDeliveryOffered(settings: LocalDeliverySettings | undefined): boolean {\n return Boolean(\n settings?.enabled &&\n settings.country &&\n (settings.zones ?? []).length > 0 &&\n localDeliveryWindowRows(settings).length > 0,\n )\n}\n\n/**\n * What stops saved settings from offering delivery, in the console's words.\n * Empty when checkout will offer it (or when it is switched off).\n */\nexport function localDeliveryProblems(settings: LocalDeliverySettings | undefined): string[] {\n if (!settings?.enabled) return []\n const problems: string[] = []\n if (!settings.country) problems.push('Choose the country you deliver in.')\n const zones = settings.zones ?? []\n if (!zones.length) problems.push('Add at least one delivery zone.')\n for (const zone of zones) {\n if (zone.kind === 'postcode' && !(zone.postcodes ?? []).length) {\n problems.push(`\"${zone.name}\" lists no postal codes.`)\n }\n if (zone.kind === 'radius' && !settings.origin) {\n problems.push(`\"${zone.name}\" measures distance, but your store has no map position yet.`)\n }\n }\n if (!localDeliveryWindowRows(settings).length) problems.push('Add at least one delivery window.')\n return problems\n}\n\n/** \"Tue, Oct 14, 9:00 AM – 12:00 PM\" in the store's zone. */\nexport function formatLocalDeliveryWindow(\n window: { startMs: number; endMs: number },\n timeZone: string,\n): string {\n try {\n const day = new Intl.DateTimeFormat('en-US', {\n timeZone,\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n }).format(new Date(window.startMs))\n const time = new Intl.DateTimeFormat('en-US', { timeZone, hour: 'numeric', minute: '2-digit' })\n return `${day}, ${time.format(new Date(window.startMs))} – ${time.format(new Date(window.endMs))}`\n } catch {\n return new Date(window.startMs).toISOString()\n }\n}\n"],"names":["parseOpeningHours","zonedDateTime","zonedWallTimeToInstant","PICKUP_INSTRUCTIONS_MAX","PICKUP_HOURS_MAX","PICKUP_LOCATIONS_MAX","text","value","max","trim","slice","wholeMinutes","minutes","Math","round","Number","isFinite","min","undefined","normalizePickupSettings","raw","settings","enabled","hours","instructions","ready","readyWithinMinutes","formatPostalAddressLine","address","fallback","parts","line1","line2","city","state","filter","Boolean","join","postalCode","map","part","String","length","pickupLocationOptions","locations","location","id","pickup","sort","a","b","isDefault","name","localeCompare","postalAddress","pickupReadyWithinLabel","days","LOCAL_DELIVERY_ZONES_MAX","LOCAL_DELIVERY_POSTCODES_MAX","LOCAL_DELIVERY_DEFAULT_LEAD_MINUTES","LOCAL_DELIVERY_DEFAULT_DAYS_AHEAD","LOCAL_DELIVERY_MAX_DAYS_AHEAD","LOCAL_DELIVERY_WINDOWS_OFFERED_MAX","LOCAL_DELIVERY_RADIUS_MAX_KM","wholeCents","cents","coordinates","lat","lng","abs","normalizePostcode","country","code","toUpperCase","replace","zip","exec","normalizePostcodePattern","postcodePatternMatches","pattern","postcode","entry","endsWith","prefix","startsWith","range","test","low","high","normalizeZone","kind","zone","feeCents","codes","Array","isArray","postcodes","Set","radius","radiusKm","minimum","minimumCents","freeOver","freeOverCents","normalizeLocalDeliverySettings","locationId","origin","zones","windows","lead","leadTimeMinutes","daysAhead","distanceKm","radians","degrees","PI","dLat","dLng","h","sin","cos","asin","sqrt","localDeliveryHasRadiusZones","some","matchLocalDeliveryZone","destination","at","distance","localDeliveryFeeCents","itemsCents","localDeliveryMinimumShortfall","DAY_NAMES","localDeliveryWindowRows","upcomingLocalDeliveryWindows","context","rows","today","nowMs","timeZone","seen","offset","noon","year","month","day","hour","weekday","row","includes","openHour","openMinute","opens","split","closeHour","closeMinute","closes","startMs","minute","overnight","endMs","has","add","push","findLocalDeliveryWindow","wanted","find","window","localDeliveryOffered","localDeliveryProblems","problems","formatLocalDeliveryWindow","Intl","DateTimeFormat","format","Date","time","toISOString"],"mappings":"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"}
@@ -40,7 +40,16 @@ export interface OrderListFields {
40
40
  * open-dispute banner ask.
41
41
  */
42
42
  disputeKey: OrderDisputeTone | null;
43
- /** The buyer's address, lower-cased — the Customer filter's `equals`. */
43
+ /**
44
+ * The order number, or null on an order that has none (POS, draft and
45
+ * channel orders are unnumbered) — the Order header's sort (AGL-3680),
46
+ * which an absent field would drop the order from.
47
+ */
48
+ number: number | null;
49
+ /**
50
+ * The buyer's address, lower-cased — the Customer filter's `equals` and the
51
+ * Customer header's sort; null with no address, never absent.
52
+ */
44
53
  customerEmailLower: string | null;
45
54
  /** The address's search prefixes (`emailSearchTokens`) — its `contains`. */
46
55
  customerEmailTokens: string[];
@@ -96,6 +96,7 @@ const text = (value)=>typeof value === 'string' ? value : '';
96
96
  channel: text(source.channel) || 'online',
97
97
  productIds,
98
98
  disputeKey: orderDisputeKey(source.dispute),
99
+ number: typeof source.number === 'number' && Number.isFinite(source.number) ? source.number : null,
99
100
  customerEmailLower: nameSearchKey(email) || null,
100
101
  customerEmailTokens: emailTokens,
101
102
  orderLabelTokens: labelTokens,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-list-fields.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 { emailSearchTokens } from '@aglyn/aglyn/app-utils/email-search'\nimport {\n nameSearchKey,\n nameSearchTokens,\n} from '@aglyn/aglyn/app-utils/name-search'\nimport { describeOrderDispute, type OrderDisputeTone } from './commerce-dispute'\nimport { orderSyncFields } from './order-shipping-export'\nimport {\n formatOrderNumber,\n type HostOrder,\n type OrderChannel,\n type OrderDispute,\n type OrderStatus,\n} from './commerce-orders'\n\n/*\n * WHAT THE ORDERS LIST ASKS ITS QUERY, WRITTEN ON THE ORDER (AGL-3321).\n *\n * The orders list puts every filter and its search on the Firestore query,\n * and a query can only ask about a field a document stores. The list used to\n * derive five of its columns in the browser — a legacy order's status and\n * channel, the products in a cart, the dispute badge, the order number — and\n * match them over the two hundred rows it had read, which answered \"none\"\n * for an order on the next page.\n *\n * So every writer of `hosts/{hostId}/orders` spreads `orderListFields` over\n * the document it writes, computed from the whole document as it will stand:\n * the creators (cart, buy-now, subscription renewal, POS, draft) and every\n * write that moves an input — the dispute record, the address a paid draft\n * learns. A write that moves none of them (a status flip, a fulfillment, a\n * refund, a timeline note) moves none of these either: `status` is its own\n * field and is what the Status filter reads.\n *\n * `tools/scripts/backfill-orders-list-fields.mjs` restates this for the\n * orders written before it; the two answer the same worked examples,\n * `tools/scripts/lib/order-list-fields.fixtures.json`.\n */\n\n/** Tokens past this many are dropped from the quick search's array. */\nexport const ORDER_SEARCH_TOKEN_LIMIT = 200\n\n/** The fields the orders list's query reads, as every writer stamps them. */\nexport interface OrderListFields {\n /**\n * Always present. A Commerce Starter order (AGL-90) carried none and was\n * read as `paid` (`liftLegacyOrder`); the lift is written down so the\n * Status filter finds it.\n */\n status: OrderStatus\n /** Always present; absent was always read as `online`. */\n channel: OrderChannel\n /**\n * Every product the order holds, line items first and the legacy flat\n * `productId` after — the Product filter's `array-contains-any`.\n */\n productIds: string[]\n /**\n * The dispute badge's tone (`describeOrderDispute`), or null with no\n * dispute. `open` is a case with a deadline; `lost` a chargeback; `won` and\n * `settled` closed with no money reversed. What the Disputes filter and the\n * open-dispute banner ask.\n */\n disputeKey: OrderDisputeTone | null\n /** The buyer's address, lower-cased — the Customer filter's `equals`. */\n customerEmailLower: string | null\n /** The address's search prefixes (`emailSearchTokens`) — its `contains`. */\n customerEmailTokens: string[]\n /** The order number's prefixes, with and without its `#` — Order `contains`. */\n orderLabelTokens: string[]\n /**\n * The quick search: the order number's, the buyer's address's and every\n * line item name's word prefixes, capped at {@link ORDER_SEARCH_TOKEN_LIMIT}\n * with the item names cut first.\n */\n searchTokens: string[]\n}\n\n/** The dispute badge's tone, stored — see {@link OrderListFields.disputeKey}. */\nexport function orderDisputeKey(\n dispute: OrderDispute | null | undefined,\n): OrderDisputeTone | null {\n if (!dispute || typeof dispute !== 'object') return null\n return describeOrderDispute({ dispute })?.tone ?? null\n}\n\n/**\n * The order number's prefixes, as `#1042` and as `1042`, so a reader who\n * types either finds it. An order with no number is labelled by its document\n * id (`formatOrderNumber`), and is found by that label the same way.\n */\nexport function orderLabelTokens(\n order: Pick<Partial<HostOrder>, 'number'>,\n docId: string,\n): string[] {\n const label = nameSearchKey(formatOrderNumber({ number: order.number }, docId))\n const bare = label.replace(/^#+/, '')\n return nameSearchTokens(bare && bare !== label ? `${label} ${bare}` : label)\n}\n\nconst text = (value: unknown): string =>\n typeof value === 'string' ? value : ''\n\n/**\n * The fields the orders list's query reads, from an order as it will be\n * stored — see {@link OrderListFields}. Pure: the same answer on the server,\n * in a spec and (restated) in the backfill.\n *\n * `legacyProductName` names the product of a Commerce Starter order, which\n * stored an id and no line items; only the backfill has it to give.\n */\nexport function orderListFields(\n order: object,\n docId: string,\n options: { legacyProductName?: string } = {},\n): OrderListFields {\n const source = order as Partial<HostOrder> & Record<string, unknown>\n const lineItems = Array.isArray(source.lineItems) ? source.lineItems : []\n const productIds = [\n ...new Set(\n [...lineItems.map((line) => line?.productId), source.productId].filter(\n (id): id is string => typeof id === 'string' && id !== '',\n ),\n ),\n ]\n const email = text(source.customerEmail)\n const emailTokens = emailSearchTokens(email)\n const labelTokens = orderLabelTokens(source, docId)\n const names = lineItems.map((line) => text(line?.name)).filter(Boolean)\n if (!names.length && options.legacyProductName) {\n names.push(options.legacyProductName)\n }\n const searchTokens = [\n ...new Set([\n ...labelTokens,\n ...emailTokens,\n ...names.flatMap((name) => nameSearchTokens(name)),\n ]),\n ].slice(0, ORDER_SEARCH_TOKEN_LIMIT)\n return {\n status: (text(source.status) || 'paid') as OrderStatus,\n channel: (text(source.channel) || 'online') as OrderChannel,\n productIds,\n disputeKey: orderDisputeKey(source.dispute),\n customerEmailLower: nameSearchKey(email) || null,\n customerEmailTokens: emailTokens,\n orderLabelTokens: labelTokens,\n searchTokens,\n }\n}\n\n/**\n * An order document with its list fields spread over it — what a creator\n * writes. The document's own `status` and `channel` win where it names them,\n * which every creator does; the rest is derived from it.\n *\n * A creator also stamps whether the order ships and when it last changed\n * (`orderSyncFields`, AGL-3613): the query the shipping export and the\n * ShipStation feed ask. Those stay OFF `orderListFields`, which the orders\n * backfill restates field for field — they are not list columns.\n */\nexport function withOrderListFields<T extends object>(\n docId: string,\n doc: T,\n): T & OrderListFields & { requiresShipping: boolean; updatedAtMs?: number } {\n return { ...doc, ...orderListFields(doc, docId), ...orderSyncFields(doc) }\n}\n"],"names":["emailSearchTokens","nameSearchKey","nameSearchTokens","describeOrderDispute","orderSyncFields","formatOrderNumber","ORDER_SEARCH_TOKEN_LIMIT","orderDisputeKey","dispute","tone","orderLabelTokens","order","docId","label","number","bare","replace","text","value","orderListFields","options","source","lineItems","Array","isArray","productIds","Set","map","line","productId","filter","id","email","customerEmail","emailTokens","labelTokens","names","name","Boolean","length","legacyProductName","push","searchTokens","flatMap","slice","status","channel","disputeKey","customerEmailLower","customerEmailTokens","withOrderListFields","doc"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,sCAAqC;AACvE,SACEC,aAAa,EACbC,gBAAgB,QACX,qCAAoC;AAC3C,SAASC,oBAAoB,QAA+B,wBAAoB;AAChF,SAASC,eAAe,QAAQ,6BAAyB;AACzD,SACEC,iBAAiB,QAKZ,uBAAmB;AAE1B;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,qEAAqE,GACrE,OAAO,MAAMC,2BAA2B,IAAG;AAsC3C,+EAA+E,GAC/E,OAAO,SAASC,gBACdC,OAAwC;;QAGjCL;IADP,IAAI,CAACK,WAAW,OAAOA,YAAY,UAAU,OAAO;IACpD,gBAAOL,wBAAAA,qBAAqB;QAAEK;IAAQ,uBAA/BL,sBAAmCM,IAAI,mBAAI;AACpD;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBACdC,KAAyC,EACzCC,KAAa;IAEb,MAAMC,QAAQZ,cAAcI,kBAAkB;QAAES,QAAQH,MAAMG,MAAM;IAAC,GAAGF;IACxE,MAAMG,OAAOF,MAAMG,OAAO,CAAC,OAAO;IAClC,OAAOd,iBAAiBa,QAAQA,SAASF,QAAQ,GAAGA,MAAM,CAAC,EAAEE,MAAM,GAAGF;AACxE;AAEA,MAAMI,OAAO,CAACC,QACZ,OAAOA,UAAU,WAAWA,QAAQ;AAEtC;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdR,KAAa,EACbC,KAAa,EACbQ,UAA0C,CAAC,CAAC;IAE5C,MAAMC,SAASV;IACf,MAAMW,YAAYC,MAAMC,OAAO,CAACH,OAAOC,SAAS,IAAID,OAAOC,SAAS,GAAG,EAAE;IACzE,MAAMG,aAAa;WACd,IAAIC,IACL;eAAIJ,UAAUK,GAAG,CAAC,CAACC,OAASA,wBAAAA,KAAMC,SAAS;YAAGR,OAAOQ,SAAS;SAAC,CAACC,MAAM,CACpE,CAACC,KAAqB,OAAOA,OAAO,YAAYA,OAAO;KAG5D;IACD,MAAMC,QAAQf,KAAKI,OAAOY,aAAa;IACvC,MAAMC,cAAclC,kBAAkBgC;IACtC,MAAMG,cAAczB,iBAAiBW,QAAQT;IAC7C,MAAMwB,QAAQd,UAAUK,GAAG,CAAC,CAACC,OAASX,KAAKW,wBAAAA,KAAMS,IAAI,GAAGP,MAAM,CAACQ;IAC/D,IAAI,CAACF,MAAMG,MAAM,IAAInB,QAAQoB,iBAAiB,EAAE;QAC9CJ,MAAMK,IAAI,CAACrB,QAAQoB,iBAAiB;IACtC;IACA,MAAME,eAAe;WAChB,IAAIhB,IAAI;eACNS;eACAD;eACAE,MAAMO,OAAO,CAAC,CAACN,OAASnC,iBAAiBmC;SAC7C;KACF,CAACO,KAAK,CAAC,GAAGtC;IACX,OAAO;QACLuC,QAAS5B,KAAKI,OAAOwB,MAAM,KAAK;QAChCC,SAAU7B,KAAKI,OAAOyB,OAAO,KAAK;QAClCrB;QACAsB,YAAYxC,gBAAgBc,OAAOb,OAAO;QAC1CwC,oBAAoB/C,cAAc+B,UAAU;QAC5CiB,qBAAqBf;QACrBxB,kBAAkByB;QAClBO;IACF;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASQ,oBACdtC,KAAa,EACbuC,GAAM;IAEN,OAAO,aAAKA,KAAQhC,gBAAgBgC,KAAKvC,QAAWR,gBAAgB+C;AACtE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-list-fields.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 { emailSearchTokens } from '@aglyn/aglyn/app-utils/email-search'\nimport {\n nameSearchKey,\n nameSearchTokens,\n} from '@aglyn/aglyn/app-utils/name-search'\nimport { describeOrderDispute, type OrderDisputeTone } from './commerce-dispute'\nimport { orderSyncFields } from './order-shipping-export'\nimport {\n formatOrderNumber,\n type HostOrder,\n type OrderChannel,\n type OrderDispute,\n type OrderStatus,\n} from './commerce-orders'\n\n/*\n * WHAT THE ORDERS LIST ASKS ITS QUERY, WRITTEN ON THE ORDER (AGL-3321).\n *\n * The orders list puts every filter and its search on the Firestore query,\n * and a query can only ask about a field a document stores. The list used to\n * derive five of its columns in the browser — a legacy order's status and\n * channel, the products in a cart, the dispute badge, the order number — and\n * match them over the two hundred rows it had read, which answered \"none\"\n * for an order on the next page.\n *\n * So every writer of `hosts/{hostId}/orders` spreads `orderListFields` over\n * the document it writes, computed from the whole document as it will stand:\n * the creators (cart, buy-now, subscription renewal, POS, draft) and every\n * write that moves an input — the dispute record, the address a paid draft\n * learns. A write that moves none of them (a status flip, a fulfillment, a\n * refund, a timeline note) moves none of these either: `status` is its own\n * field and is what the Status filter reads.\n *\n * `tools/scripts/backfill-orders-list-fields.mjs` restates this for the\n * orders written before it; the two answer the same worked examples,\n * `tools/scripts/lib/order-list-fields.fixtures.json`.\n */\n\n/** Tokens past this many are dropped from the quick search's array. */\nexport const ORDER_SEARCH_TOKEN_LIMIT = 200\n\n/** The fields the orders list's query reads, as every writer stamps them. */\nexport interface OrderListFields {\n /**\n * Always present. A Commerce Starter order (AGL-90) carried none and was\n * read as `paid` (`liftLegacyOrder`); the lift is written down so the\n * Status filter finds it.\n */\n status: OrderStatus\n /** Always present; absent was always read as `online`. */\n channel: OrderChannel\n /**\n * Every product the order holds, line items first and the legacy flat\n * `productId` after — the Product filter's `array-contains-any`.\n */\n productIds: string[]\n /**\n * The dispute badge's tone (`describeOrderDispute`), or null with no\n * dispute. `open` is a case with a deadline; `lost` a chargeback; `won` and\n * `settled` closed with no money reversed. What the Disputes filter and the\n * open-dispute banner ask.\n */\n disputeKey: OrderDisputeTone | null\n /**\n * The order number, or null on an order that has none (POS, draft and\n * channel orders are unnumbered) — the Order header's sort (AGL-3680),\n * which an absent field would drop the order from.\n */\n number: number | null\n /**\n * The buyer's address, lower-cased — the Customer filter's `equals` and the\n * Customer header's sort; null with no address, never absent.\n */\n customerEmailLower: string | null\n /** The address's search prefixes (`emailSearchTokens`) — its `contains`. */\n customerEmailTokens: string[]\n /** The order number's prefixes, with and without its `#` — Order `contains`. */\n orderLabelTokens: string[]\n /**\n * The quick search: the order number's, the buyer's address's and every\n * line item name's word prefixes, capped at {@link ORDER_SEARCH_TOKEN_LIMIT}\n * with the item names cut first.\n */\n searchTokens: string[]\n}\n\n/** The dispute badge's tone, stored — see {@link OrderListFields.disputeKey}. */\nexport function orderDisputeKey(\n dispute: OrderDispute | null | undefined,\n): OrderDisputeTone | null {\n if (!dispute || typeof dispute !== 'object') return null\n return describeOrderDispute({ dispute })?.tone ?? null\n}\n\n/**\n * The order number's prefixes, as `#1042` and as `1042`, so a reader who\n * types either finds it. An order with no number is labelled by its document\n * id (`formatOrderNumber`), and is found by that label the same way.\n */\nexport function orderLabelTokens(\n order: Pick<Partial<HostOrder>, 'number'>,\n docId: string,\n): string[] {\n const label = nameSearchKey(formatOrderNumber({ number: order.number }, docId))\n const bare = label.replace(/^#+/, '')\n return nameSearchTokens(bare && bare !== label ? `${label} ${bare}` : label)\n}\n\nconst text = (value: unknown): string =>\n typeof value === 'string' ? value : ''\n\n/**\n * The fields the orders list's query reads, from an order as it will be\n * stored — see {@link OrderListFields}. Pure: the same answer on the server,\n * in a spec and (restated) in the backfill.\n *\n * `legacyProductName` names the product of a Commerce Starter order, which\n * stored an id and no line items; only the backfill has it to give.\n */\nexport function orderListFields(\n order: object,\n docId: string,\n options: { legacyProductName?: string } = {},\n): OrderListFields {\n const source = order as Partial<HostOrder> & Record<string, unknown>\n const lineItems = Array.isArray(source.lineItems) ? source.lineItems : []\n const productIds = [\n ...new Set(\n [...lineItems.map((line) => line?.productId), source.productId].filter(\n (id): id is string => typeof id === 'string' && id !== '',\n ),\n ),\n ]\n const email = text(source.customerEmail)\n const emailTokens = emailSearchTokens(email)\n const labelTokens = orderLabelTokens(source, docId)\n const names = lineItems.map((line) => text(line?.name)).filter(Boolean)\n if (!names.length && options.legacyProductName) {\n names.push(options.legacyProductName)\n }\n const searchTokens = [\n ...new Set([\n ...labelTokens,\n ...emailTokens,\n ...names.flatMap((name) => nameSearchTokens(name)),\n ]),\n ].slice(0, ORDER_SEARCH_TOKEN_LIMIT)\n return {\n status: (text(source.status) || 'paid') as OrderStatus,\n channel: (text(source.channel) || 'online') as OrderChannel,\n productIds,\n disputeKey: orderDisputeKey(source.dispute),\n number:\n typeof source.number === 'number' && Number.isFinite(source.number)\n ? source.number\n : null,\n customerEmailLower: nameSearchKey(email) || null,\n customerEmailTokens: emailTokens,\n orderLabelTokens: labelTokens,\n searchTokens,\n }\n}\n\n/**\n * An order document with its list fields spread over it — what a creator\n * writes. The document's own `status` and `channel` win where it names them,\n * which every creator does; the rest is derived from it.\n *\n * A creator also stamps whether the order ships and when it last changed\n * (`orderSyncFields`, AGL-3613): the query the shipping export and the\n * ShipStation feed ask. Those stay OFF `orderListFields`, which the orders\n * backfill restates field for field — they are not list columns.\n */\nexport function withOrderListFields<T extends object>(\n docId: string,\n doc: T,\n): T & OrderListFields & { requiresShipping: boolean; updatedAtMs?: number } {\n return { ...doc, ...orderListFields(doc, docId), ...orderSyncFields(doc) }\n}\n"],"names":["emailSearchTokens","nameSearchKey","nameSearchTokens","describeOrderDispute","orderSyncFields","formatOrderNumber","ORDER_SEARCH_TOKEN_LIMIT","orderDisputeKey","dispute","tone","orderLabelTokens","order","docId","label","number","bare","replace","text","value","orderListFields","options","source","lineItems","Array","isArray","productIds","Set","map","line","productId","filter","id","email","customerEmail","emailTokens","labelTokens","names","name","Boolean","length","legacyProductName","push","searchTokens","flatMap","slice","status","channel","disputeKey","Number","isFinite","customerEmailLower","customerEmailTokens","withOrderListFields","doc"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,iBAAiB,QAAQ,sCAAqC;AACvE,SACEC,aAAa,EACbC,gBAAgB,QACX,qCAAoC;AAC3C,SAASC,oBAAoB,QAA+B,wBAAoB;AAChF,SAASC,eAAe,QAAQ,6BAAyB;AACzD,SACEC,iBAAiB,QAKZ,uBAAmB;AAE1B;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,qEAAqE,GACrE,OAAO,MAAMC,2BAA2B,IAAG;AA+C3C,+EAA+E,GAC/E,OAAO,SAASC,gBACdC,OAAwC;;QAGjCL;IADP,IAAI,CAACK,WAAW,OAAOA,YAAY,UAAU,OAAO;IACpD,gBAAOL,wBAAAA,qBAAqB;QAAEK;IAAQ,uBAA/BL,sBAAmCM,IAAI,mBAAI;AACpD;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBACdC,KAAyC,EACzCC,KAAa;IAEb,MAAMC,QAAQZ,cAAcI,kBAAkB;QAAES,QAAQH,MAAMG,MAAM;IAAC,GAAGF;IACxE,MAAMG,OAAOF,MAAMG,OAAO,CAAC,OAAO;IAClC,OAAOd,iBAAiBa,QAAQA,SAASF,QAAQ,GAAGA,MAAM,CAAC,EAAEE,MAAM,GAAGF;AACxE;AAEA,MAAMI,OAAO,CAACC,QACZ,OAAOA,UAAU,WAAWA,QAAQ;AAEtC;;;;;;;CAOC,GACD,OAAO,SAASC,gBACdR,KAAa,EACbC,KAAa,EACbQ,UAA0C,CAAC,CAAC;IAE5C,MAAMC,SAASV;IACf,MAAMW,YAAYC,MAAMC,OAAO,CAACH,OAAOC,SAAS,IAAID,OAAOC,SAAS,GAAG,EAAE;IACzE,MAAMG,aAAa;WACd,IAAIC,IACL;eAAIJ,UAAUK,GAAG,CAAC,CAACC,OAASA,wBAAAA,KAAMC,SAAS;YAAGR,OAAOQ,SAAS;SAAC,CAACC,MAAM,CACpE,CAACC,KAAqB,OAAOA,OAAO,YAAYA,OAAO;KAG5D;IACD,MAAMC,QAAQf,KAAKI,OAAOY,aAAa;IACvC,MAAMC,cAAclC,kBAAkBgC;IACtC,MAAMG,cAAczB,iBAAiBW,QAAQT;IAC7C,MAAMwB,QAAQd,UAAUK,GAAG,CAAC,CAACC,OAASX,KAAKW,wBAAAA,KAAMS,IAAI,GAAGP,MAAM,CAACQ;IAC/D,IAAI,CAACF,MAAMG,MAAM,IAAInB,QAAQoB,iBAAiB,EAAE;QAC9CJ,MAAMK,IAAI,CAACrB,QAAQoB,iBAAiB;IACtC;IACA,MAAME,eAAe;WAChB,IAAIhB,IAAI;eACNS;eACAD;eACAE,MAAMO,OAAO,CAAC,CAACN,OAASnC,iBAAiBmC;SAC7C;KACF,CAACO,KAAK,CAAC,GAAGtC;IACX,OAAO;QACLuC,QAAS5B,KAAKI,OAAOwB,MAAM,KAAK;QAChCC,SAAU7B,KAAKI,OAAOyB,OAAO,KAAK;QAClCrB;QACAsB,YAAYxC,gBAAgBc,OAAOb,OAAO;QAC1CM,QACE,OAAOO,OAAOP,MAAM,KAAK,YAAYkC,OAAOC,QAAQ,CAAC5B,OAAOP,MAAM,IAC9DO,OAAOP,MAAM,GACb;QACNoC,oBAAoBjD,cAAc+B,UAAU;QAC5CmB,qBAAqBjB;QACrBxB,kBAAkByB;QAClBO;IACF;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASU,oBACdxC,KAAa,EACbyC,GAAM;IAEN,OAAO,aAAKA,KAAQlC,gBAAgBkC,KAAKzC,QAAWR,gBAAgBiD;AACtE"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * How an order reaches its buyer when it does not travel by carrier
19
+ * (AGL-3624): picked up at one of the store's locations, or taken there by the
20
+ * store's OWN driver. Marketplace delivery apps (AGL-3644) are a different
21
+ * thing — an outside courier the delivery-apps plugin books — and never write
22
+ * these fields.
23
+ *
24
+ * Pure and import-free, because the native apps read the same order document
25
+ * (`tools/scripts/mobile-pure-modules.json`): the statuses, their labels, the
26
+ * transitions and the fields the pickup and delivery lists query by are one
27
+ * answer on every platform.
28
+ */
29
+ /** A location's pickup offer (`hosts/{hostId}/locations/{id}.pickup`). */
30
+ export interface PickupLocationSettings {
31
+ enabled?: boolean;
32
+ /** Pickup hours, one line per set of days: `Mo-Fr 09:00-17:00`. */
33
+ hours?: string;
34
+ /** What the buyer does on arrival: "Ring the bell at the side door". */
35
+ instructions?: string;
36
+ /** How long an order usually takes to prepare, shown to the buyer. */
37
+ readyWithinMinutes?: number;
38
+ }
39
+ /** How the buyer chose to receive the order. Absent on every order before AGL-3624: shipped. */
40
+ export type OrderFulfillmentMethod = 'shipping' | 'pickup' | 'local_delivery';
41
+ /** Where a pickup order stands: being put together, waiting at the counter, collected. */
42
+ export type OrderPickupStatus = 'preparing' | 'ready' | 'picked_up';
43
+ /** Where the store's own delivery stands. `failed` is a missed drop the store can send out again. */
44
+ export type OrderLocalDeliveryStatus = 'scheduled' | 'out_for_delivery' | 'delivered' | 'failed';
45
+ /**
46
+ * The pickup the buyer chose, copied from the location at checkout so the
47
+ * order says where to go even after the location is renamed or removed.
48
+ */
49
+ export interface OrderPickup {
50
+ locationId: string;
51
+ locationName: string;
52
+ /** The location's address on one line, when it has one. */
53
+ address?: string;
54
+ /** What to do on arrival, in the store's words. */
55
+ instructions?: string;
56
+ /** The location's hours as the store typed them (`Mo-Fr 09:00-17:00`). */
57
+ hours?: string;
58
+ status: OrderPickupStatus;
59
+ readyAtMs?: number;
60
+ pickedUpAtMs?: number;
61
+ /** Who collected it, when the person at the counter noted a name. */
62
+ pickedUpBy?: string;
63
+ updatedAtMs?: number;
64
+ }
65
+ /** The local delivery the buyer chose and paid for. */
66
+ export interface OrderLocalDelivery {
67
+ zoneId: string;
68
+ zoneName: string;
69
+ /** The fee charged, in cents (the shipping line of the order). */
70
+ feeCents: number;
71
+ /** The window the buyer picked, as instants. */
72
+ windowStartMs: number;
73
+ windowEndMs: number;
74
+ /** The window as the buyer was shown it, in the store's time zone. */
75
+ windowLabel?: string;
76
+ /** The postal code the buyer declared at checkout and the zone was matched on. */
77
+ postalCode?: string;
78
+ /** The location the order leaves from, when the store named one. */
79
+ locationId?: string;
80
+ status: OrderLocalDeliveryStatus;
81
+ /**
82
+ * The address the buyer then entered at payment is OUTSIDE the zone the
83
+ * fee was charged for: the store is told, and decides.
84
+ */
85
+ addressOutsideZone?: boolean;
86
+ outForDeliveryAtMs?: number;
87
+ deliveredAtMs?: number;
88
+ failedAtMs?: number;
89
+ /** Why a drop failed, in the driver's words. */
90
+ failedReason?: string;
91
+ updatedAtMs?: number;
92
+ }
93
+ export declare const ORDER_PICKUP_STATUS_LABELS: Readonly<Record<OrderPickupStatus, string>>;
94
+ export declare const ORDER_LOCAL_DELIVERY_STATUS_LABELS: Readonly<Record<OrderLocalDeliveryStatus, string>>;
95
+ export declare const ORDER_FULFILLMENT_METHOD_LABELS: Readonly<Record<OrderFulfillmentMethod, string>>;
96
+ export declare function canTransitionPickup(from: OrderPickupStatus | undefined, to: OrderPickupStatus): boolean;
97
+ export declare function canTransitionLocalDelivery(from: OrderLocalDeliveryStatus | undefined, to: OrderLocalDeliveryStatus): boolean;
98
+ /**
99
+ * The key the pickup and delivery lists query on (`fulfillmentKey`): the
100
+ * method and its status in one field, so "orders ready for pickup" is ONE
101
+ * equality and every tab of the list is one composite index.
102
+ */
103
+ export type OrderFulfillmentKey = 'pickup_preparing' | 'pickup_ready' | 'pickup_picked_up' | 'delivery_scheduled' | 'delivery_out_for_delivery' | 'delivery_delivered' | 'delivery_failed';
104
+ /** The fields the pickup and delivery lists query, stamped by every writer of either. */
105
+ export interface OrderLocalFulfillmentListFields {
106
+ fulfillmentKey: OrderFulfillmentKey;
107
+ /** The location to prepare it at or send it from; `''` when the store named none. */
108
+ fulfillmentLocationId: string;
109
+ /**
110
+ * What the list sorts by, soonest first: a pickup by when it was ordered
111
+ * (first in, first prepared), a delivery by the start of its window.
112
+ */
113
+ fulfillmentDueMs: number;
114
+ }
115
+ /** Reads a stored pickup status, defaulting a malformed one to where every pickup starts. */
116
+ export declare function orderPickupStatus(value: unknown): OrderPickupStatus;
117
+ export declare function orderLocalDeliveryStatus(value: unknown): OrderLocalDeliveryStatus;
118
+ /**
119
+ * The list fields for an order, or `null` for one that is shipped: those
120
+ * orders carry none of the three, so neither list ever reads them.
121
+ */
122
+ export declare function orderLocalFulfillmentListFields(order: {
123
+ fulfillmentMethod?: OrderFulfillmentMethod | string;
124
+ pickup?: Partial<OrderPickup> | null;
125
+ localDelivery?: Partial<OrderLocalDelivery> | null;
126
+ createdAtMs?: number;
127
+ }): OrderLocalFulfillmentListFields | null;
128
+ /** Whether an order is collected or brought by the store, so no carrier ever ships it. */
129
+ export declare function orderIsLocallyFulfilled(order: {
130
+ fulfillmentMethod?: string;
131
+ } | null | undefined): boolean;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * How an order reaches its buyer when it does not travel by carrier
18
+ * (AGL-3624): picked up at one of the store's locations, or taken there by the
19
+ * store's OWN driver. Marketplace delivery apps (AGL-3644) are a different
20
+ * thing — an outside courier the delivery-apps plugin books — and never write
21
+ * these fields.
22
+ *
23
+ * Pure and import-free, because the native apps read the same order document
24
+ * (`tools/scripts/mobile-pure-modules.json`): the statuses, their labels, the
25
+ * transitions and the fields the pickup and delivery lists query by are one
26
+ * answer on every platform.
27
+ */ /** A location's pickup offer (`hosts/{hostId}/locations/{id}.pickup`). */ export const ORDER_PICKUP_STATUS_LABELS = {
28
+ preparing: 'Preparing',
29
+ ready: 'Ready for pickup',
30
+ picked_up: 'Picked up'
31
+ };
32
+ export const ORDER_LOCAL_DELIVERY_STATUS_LABELS = {
33
+ scheduled: 'Scheduled',
34
+ out_for_delivery: 'Out for delivery',
35
+ delivered: 'Delivered',
36
+ failed: 'Delivery failed'
37
+ };
38
+ export const ORDER_FULFILLMENT_METHOD_LABELS = {
39
+ shipping: 'Shipping',
40
+ pickup: 'Pickup',
41
+ local_delivery: 'Local delivery'
42
+ };
43
+ /**
44
+ * The forward moves of a pickup. `ready` → `preparing` is allowed: the
45
+ * merchant marked the wrong order ready and takes it back before anyone is
46
+ * told twice. Nothing leaves `picked_up`: the goods are gone.
47
+ */ const PICKUP_TRANSITIONS = {
48
+ preparing: [
49
+ 'ready',
50
+ 'picked_up'
51
+ ],
52
+ ready: [
53
+ 'picked_up',
54
+ 'preparing'
55
+ ],
56
+ picked_up: []
57
+ };
58
+ /** A failed drop goes out again; a delivered one is history. */ const LOCAL_DELIVERY_TRANSITIONS = {
59
+ scheduled: [
60
+ 'out_for_delivery',
61
+ 'delivered',
62
+ 'failed'
63
+ ],
64
+ out_for_delivery: [
65
+ 'delivered',
66
+ 'failed'
67
+ ],
68
+ failed: [
69
+ 'out_for_delivery',
70
+ 'delivered'
71
+ ],
72
+ delivered: []
73
+ };
74
+ export function canTransitionPickup(from, to) {
75
+ var _PICKUP_TRANSITIONS_;
76
+ return ((_PICKUP_TRANSITIONS_ = PICKUP_TRANSITIONS[from != null ? from : 'preparing']) != null ? _PICKUP_TRANSITIONS_ : []).includes(to);
77
+ }
78
+ export function canTransitionLocalDelivery(from, to) {
79
+ var _LOCAL_DELIVERY_TRANSITIONS_;
80
+ return ((_LOCAL_DELIVERY_TRANSITIONS_ = LOCAL_DELIVERY_TRANSITIONS[from != null ? from : 'scheduled']) != null ? _LOCAL_DELIVERY_TRANSITIONS_ : []).includes(to);
81
+ }
82
+ const PICKUP_STATUSES = [
83
+ 'preparing',
84
+ 'ready',
85
+ 'picked_up'
86
+ ];
87
+ const DELIVERY_STATUSES = [
88
+ 'scheduled',
89
+ 'out_for_delivery',
90
+ 'delivered',
91
+ 'failed'
92
+ ];
93
+ /** Reads a stored pickup status, defaulting a malformed one to where every pickup starts. */ export function orderPickupStatus(value) {
94
+ return PICKUP_STATUSES.includes(value) ? value : 'preparing';
95
+ }
96
+ export function orderLocalDeliveryStatus(value) {
97
+ return DELIVERY_STATUSES.includes(value) ? value : 'scheduled';
98
+ }
99
+ /**
100
+ * The list fields for an order, or `null` for one that is shipped: those
101
+ * orders carry none of the three, so neither list ever reads them.
102
+ */ export function orderLocalFulfillmentListFields(order) {
103
+ if (order.fulfillmentMethod === 'pickup' && order.pickup) {
104
+ var _order_pickup_locationId;
105
+ return {
106
+ fulfillmentKey: `pickup_${orderPickupStatus(order.pickup.status)}`,
107
+ fulfillmentLocationId: String((_order_pickup_locationId = order.pickup.locationId) != null ? _order_pickup_locationId : ''),
108
+ fulfillmentDueMs: Number(order.createdAtMs) || 0
109
+ };
110
+ }
111
+ if (order.fulfillmentMethod === 'local_delivery' && order.localDelivery) {
112
+ var _order_localDelivery_locationId;
113
+ return {
114
+ fulfillmentKey: `delivery_${orderLocalDeliveryStatus(order.localDelivery.status)}`,
115
+ fulfillmentLocationId: String((_order_localDelivery_locationId = order.localDelivery.locationId) != null ? _order_localDelivery_locationId : ''),
116
+ fulfillmentDueMs: Number(order.localDelivery.windowStartMs) || Number(order.createdAtMs) || 0
117
+ };
118
+ }
119
+ return null;
120
+ }
121
+ /** Whether an order is collected or brought by the store, so no carrier ever ships it. */ export function orderIsLocallyFulfilled(order) {
122
+ return (order == null ? void 0 : order.fulfillmentMethod) === 'pickup' || (order == null ? void 0 : order.fulfillmentMethod) === 'local_delivery';
123
+ }
124
+
125
+ //# sourceMappingURL=order-local-fulfillment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-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\n/**\n * How an order reaches its buyer when it does not travel by carrier\n * (AGL-3624): picked up at one of the store's locations, or taken there by the\n * store's OWN driver. Marketplace delivery apps (AGL-3644) are a different\n * thing — an outside courier the delivery-apps plugin books — and never write\n * these fields.\n *\n * Pure and import-free, because the native apps read the same order document\n * (`tools/scripts/mobile-pure-modules.json`): the statuses, their labels, the\n * transitions and the fields the pickup and delivery lists query by are one\n * answer on every platform.\n */\n\n/** A location's pickup offer (`hosts/{hostId}/locations/{id}.pickup`). */\nexport interface PickupLocationSettings {\n enabled?: boolean\n /** Pickup hours, one line per set of days: `Mo-Fr 09:00-17:00`. */\n hours?: string\n /** What the buyer does on arrival: \"Ring the bell at the side door\". */\n instructions?: string\n /** How long an order usually takes to prepare, shown to the buyer. */\n readyWithinMinutes?: number\n}\n\n/** How the buyer chose to receive the order. Absent on every order before AGL-3624: shipped. */\nexport type OrderFulfillmentMethod = 'shipping' | 'pickup' | 'local_delivery'\n\n/** Where a pickup order stands: being put together, waiting at the counter, collected. */\nexport type OrderPickupStatus = 'preparing' | 'ready' | 'picked_up'\n\n/** Where the store's own delivery stands. `failed` is a missed drop the store can send out again. */\nexport type OrderLocalDeliveryStatus = 'scheduled' | 'out_for_delivery' | 'delivered' | 'failed'\n\n/**\n * The pickup the buyer chose, copied from the location at checkout so the\n * order says where to go even after the location is renamed or removed.\n */\nexport interface OrderPickup {\n locationId: string\n locationName: string\n /** The location's address on one line, when it has one. */\n address?: string\n /** What to do on arrival, in the store's words. */\n instructions?: string\n /** The location's hours as the store typed them (`Mo-Fr 09:00-17:00`). */\n hours?: string\n status: OrderPickupStatus\n readyAtMs?: number\n pickedUpAtMs?: number\n /** Who collected it, when the person at the counter noted a name. */\n pickedUpBy?: string\n updatedAtMs?: number\n}\n\n/** The local delivery the buyer chose and paid for. */\nexport interface OrderLocalDelivery {\n zoneId: string\n zoneName: string\n /** The fee charged, in cents (the shipping line of the order). */\n feeCents: number\n /** The window the buyer picked, as instants. */\n windowStartMs: number\n windowEndMs: number\n /** The window as the buyer was shown it, in the store's time zone. */\n windowLabel?: string\n /** The postal code the buyer declared at checkout and the zone was matched on. */\n postalCode?: string\n /** The location the order leaves from, when the store named one. */\n locationId?: string\n status: OrderLocalDeliveryStatus\n /**\n * The address the buyer then entered at payment is OUTSIDE the zone the\n * fee was charged for: the store is told, and decides.\n */\n addressOutsideZone?: boolean\n outForDeliveryAtMs?: number\n deliveredAtMs?: number\n failedAtMs?: number\n /** Why a drop failed, in the driver's words. */\n failedReason?: string\n updatedAtMs?: number\n}\n\nexport const ORDER_PICKUP_STATUS_LABELS: Readonly<Record<OrderPickupStatus, string>> = {\n preparing: 'Preparing',\n ready: 'Ready for pickup',\n picked_up: 'Picked up',\n}\n\nexport const ORDER_LOCAL_DELIVERY_STATUS_LABELS: Readonly<Record<OrderLocalDeliveryStatus, string>> = {\n scheduled: 'Scheduled',\n out_for_delivery: 'Out for delivery',\n delivered: 'Delivered',\n failed: 'Delivery failed',\n}\n\nexport const ORDER_FULFILLMENT_METHOD_LABELS: Readonly<Record<OrderFulfillmentMethod, string>> = {\n shipping: 'Shipping',\n pickup: 'Pickup',\n local_delivery: 'Local delivery',\n}\n\n/**\n * The forward moves of a pickup. `ready` → `preparing` is allowed: the\n * merchant marked the wrong order ready and takes it back before anyone is\n * told twice. Nothing leaves `picked_up`: the goods are gone.\n */\nconst PICKUP_TRANSITIONS: Readonly<Record<OrderPickupStatus, readonly OrderPickupStatus[]>> = {\n preparing: ['ready', 'picked_up'],\n ready: ['picked_up', 'preparing'],\n picked_up: [],\n}\n\n/** A failed drop goes out again; a delivered one is history. */\nconst LOCAL_DELIVERY_TRANSITIONS: Readonly<\n Record<OrderLocalDeliveryStatus, readonly OrderLocalDeliveryStatus[]>\n> = {\n scheduled: ['out_for_delivery', 'delivered', 'failed'],\n out_for_delivery: ['delivered', 'failed'],\n failed: ['out_for_delivery', 'delivered'],\n delivered: [],\n}\n\nexport function canTransitionPickup(from: OrderPickupStatus | undefined, to: OrderPickupStatus): boolean {\n return (PICKUP_TRANSITIONS[from ?? 'preparing'] ?? []).includes(to)\n}\n\nexport function canTransitionLocalDelivery(\n from: OrderLocalDeliveryStatus | undefined,\n to: OrderLocalDeliveryStatus,\n): boolean {\n return (LOCAL_DELIVERY_TRANSITIONS[from ?? 'scheduled'] ?? []).includes(to)\n}\n\n/**\n * The key the pickup and delivery lists query on (`fulfillmentKey`): the\n * method and its status in one field, so \"orders ready for pickup\" is ONE\n * equality and every tab of the list is one composite index.\n */\nexport type OrderFulfillmentKey =\n | 'pickup_preparing'\n | 'pickup_ready'\n | 'pickup_picked_up'\n | 'delivery_scheduled'\n | 'delivery_out_for_delivery'\n | 'delivery_delivered'\n | 'delivery_failed'\n\n/** The fields the pickup and delivery lists query, stamped by every writer of either. */\nexport interface OrderLocalFulfillmentListFields {\n fulfillmentKey: OrderFulfillmentKey\n /** The location to prepare it at or send it from; `''` when the store named none. */\n fulfillmentLocationId: string\n /**\n * What the list sorts by, soonest first: a pickup by when it was ordered\n * (first in, first prepared), a delivery by the start of its window.\n */\n fulfillmentDueMs: number\n}\n\nconst PICKUP_STATUSES: readonly OrderPickupStatus[] = ['preparing', 'ready', 'picked_up']\nconst DELIVERY_STATUSES: readonly OrderLocalDeliveryStatus[] = [\n 'scheduled',\n 'out_for_delivery',\n 'delivered',\n 'failed',\n]\n\n/** Reads a stored pickup status, defaulting a malformed one to where every pickup starts. */\nexport function orderPickupStatus(value: unknown): OrderPickupStatus {\n return PICKUP_STATUSES.includes(value as OrderPickupStatus) ? (value as OrderPickupStatus) : 'preparing'\n}\n\nexport function orderLocalDeliveryStatus(value: unknown): OrderLocalDeliveryStatus {\n return DELIVERY_STATUSES.includes(value as OrderLocalDeliveryStatus)\n ? (value as OrderLocalDeliveryStatus)\n : 'scheduled'\n}\n\n/**\n * The list fields for an order, or `null` for one that is shipped: those\n * orders carry none of the three, so neither list ever reads them.\n */\nexport function orderLocalFulfillmentListFields(order: {\n fulfillmentMethod?: OrderFulfillmentMethod | string\n pickup?: Partial<OrderPickup> | null\n localDelivery?: Partial<OrderLocalDelivery> | null\n createdAtMs?: number\n}): OrderLocalFulfillmentListFields | null {\n if (order.fulfillmentMethod === 'pickup' && order.pickup) {\n return {\n fulfillmentKey: `pickup_${orderPickupStatus(order.pickup.status)}` as OrderFulfillmentKey,\n fulfillmentLocationId: String(order.pickup.locationId ?? ''),\n fulfillmentDueMs: Number(order.createdAtMs) || 0,\n }\n }\n if (order.fulfillmentMethod === 'local_delivery' && order.localDelivery) {\n return {\n fulfillmentKey: `delivery_${orderLocalDeliveryStatus(order.localDelivery.status)}` as OrderFulfillmentKey,\n fulfillmentLocationId: String(order.localDelivery.locationId ?? ''),\n fulfillmentDueMs: Number(order.localDelivery.windowStartMs) || Number(order.createdAtMs) || 0,\n }\n }\n return null\n}\n\n/** Whether an order is collected or brought by the store, so no carrier ever ships it. */\nexport function orderIsLocallyFulfilled(order: { fulfillmentMethod?: string } | null | undefined): boolean {\n return order?.fulfillmentMethod === 'pickup' || order?.fulfillmentMethod === 'local_delivery'\n}\n"],"names":["ORDER_PICKUP_STATUS_LABELS","preparing","ready","picked_up","ORDER_LOCAL_DELIVERY_STATUS_LABELS","scheduled","out_for_delivery","delivered","failed","ORDER_FULFILLMENT_METHOD_LABELS","shipping","pickup","local_delivery","PICKUP_TRANSITIONS","LOCAL_DELIVERY_TRANSITIONS","canTransitionPickup","from","to","includes","canTransitionLocalDelivery","PICKUP_STATUSES","DELIVERY_STATUSES","orderPickupStatus","value","orderLocalDeliveryStatus","orderLocalFulfillmentListFields","order","fulfillmentMethod","fulfillmentKey","status","fulfillmentLocationId","String","locationId","fulfillmentDueMs","Number","createdAtMs","localDelivery","windowStartMs","orderIsLocallyFulfilled"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,wEAAwE,GAsExE,OAAO,MAAMA,6BAA0E;IACrFC,WAAW;IACXC,OAAO;IACPC,WAAW;AACb,EAAC;AAED,OAAO,MAAMC,qCAAyF;IACpGC,WAAW;IACXC,kBAAkB;IAClBC,WAAW;IACXC,QAAQ;AACV,EAAC;AAED,OAAO,MAAMC,kCAAoF;IAC/FC,UAAU;IACVC,QAAQ;IACRC,gBAAgB;AAClB,EAAC;AAED;;;;CAIC,GACD,MAAMC,qBAAwF;IAC5FZ,WAAW;QAAC;QAAS;KAAY;IACjCC,OAAO;QAAC;QAAa;KAAY;IACjCC,WAAW,EAAE;AACf;AAEA,8DAA8D,GAC9D,MAAMW,6BAEF;IACFT,WAAW;QAAC;QAAoB;QAAa;KAAS;IACtDC,kBAAkB;QAAC;QAAa;KAAS;IACzCE,QAAQ;QAAC;QAAoB;KAAY;IACzCD,WAAW,EAAE;AACf;AAEA,OAAO,SAASQ,oBAAoBC,IAAmC,EAAEC,EAAqB;QACpFJ;IAAR,OAAO,EAACA,uBAAAA,kBAAkB,CAACG,eAAAA,OAAQ,YAAY,YAAvCH,uBAA2C,EAAE,EAAEK,QAAQ,CAACD;AAClE;AAEA,OAAO,SAASE,2BACdH,IAA0C,EAC1CC,EAA4B;QAEpBH;IAAR,OAAO,EAACA,+BAAAA,0BAA0B,CAACE,eAAAA,OAAQ,YAAY,YAA/CF,+BAAmD,EAAE,EAAEI,QAAQ,CAACD;AAC1E;AA4BA,MAAMG,kBAAgD;IAAC;IAAa;IAAS;CAAY;AACzF,MAAMC,oBAAyD;IAC7D;IACA;IACA;IACA;CACD;AAED,2FAA2F,GAC3F,OAAO,SAASC,kBAAkBC,KAAc;IAC9C,OAAOH,gBAAgBF,QAAQ,CAACK,SAA+BA,QAA8B;AAC/F;AAEA,OAAO,SAASC,yBAAyBD,KAAc;IACrD,OAAOF,kBAAkBH,QAAQ,CAACK,SAC7BA,QACD;AACN;AAEA;;;CAGC,GACD,OAAO,SAASE,gCAAgCC,KAK/C;IACC,IAAIA,MAAMC,iBAAiB,KAAK,YAAYD,MAAMf,MAAM,EAAE;YAGxBe;QAFhC,OAAO;YACLE,gBAAgB,CAAC,OAAO,EAAEN,kBAAkBI,MAAMf,MAAM,CAACkB,MAAM,GAAG;YAClEC,uBAAuBC,QAAOL,2BAAAA,MAAMf,MAAM,CAACqB,UAAU,YAAvBN,2BAA2B;YACzDO,kBAAkBC,OAAOR,MAAMS,WAAW,KAAK;QACjD;IACF;IACA,IAAIT,MAAMC,iBAAiB,KAAK,oBAAoBD,MAAMU,aAAa,EAAE;YAGvCV;QAFhC,OAAO;YACLE,gBAAgB,CAAC,SAAS,EAAEJ,yBAAyBE,MAAMU,aAAa,CAACP,MAAM,GAAG;YAClFC,uBAAuBC,QAAOL,kCAAAA,MAAMU,aAAa,CAACJ,UAAU,YAA9BN,kCAAkC;YAChEO,kBAAkBC,OAAOR,MAAMU,aAAa,CAACC,aAAa,KAAKH,OAAOR,MAAMS,WAAW,KAAK;QAC9F;IACF;IACA,OAAO;AACT;AAEA,wFAAwF,GACxF,OAAO,SAASG,wBAAwBZ,KAAwD;IAC9F,OAAOA,CAAAA,yBAAAA,MAAOC,iBAAiB,MAAK,YAAYD,CAAAA,yBAAAA,MAAOC,iBAAiB,MAAK;AAC/E"}
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  import { fulfillmentIsActive, lineRequiresShipping, remainingFulfillmentLines } from "./order-fulfillment.js";
18
+ import { orderIsLocallyFulfilled } from "./order-local-fulfillment.js";
18
19
  /*
19
20
  * AN ORDER AS A SHIPPING TOOL READS IT (AGL-3613).
20
21
  *
@@ -50,6 +51,9 @@ import { fulfillmentIsActive, lineRequiresShipping, remainingFulfillmentLines }
50
51
  * given a shipping address. A legacy Commerce Starter order with no lines
51
52
  * ships when it carries an address.
52
53
  */ export function orderRequiresShipping(order) {
54
+ // Collected at a location or brought by the store's own driver (AGL-3624):
55
+ // nothing for a shipping tool to label, whatever is in it.
56
+ if (orderIsLocallyFulfilled(order)) return false;
53
57
  const lines = Array.isArray(order.lineItems) ? order.lineItems : [];
54
58
  const address = hasShippingAddress(order.shippingAddress);
55
59
  if (!lines.length) return address;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-shipping-export.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 { HostOrder, OrderAddress, OrderFulfillment, OrderLineItem } from './commerce-orders'\nimport { fulfillmentIsActive, lineRequiresShipping, remainingFulfillmentLines } from './order-fulfillment'\n\n/*\n * AN ORDER AS A SHIPPING TOOL READS IT (AGL-3613).\n *\n * ShipStation's Custom Store feed, Pirate Ship's spreadsheet upload and the\n * Shippo and EasyPost CSVs all want the same facts about an order: who it\n * goes to, what is still to be packed, and what that weighs. This module is\n * that one reading, pure, so the XML feed and the CSV export cannot disagree\n * about an order's weight or which units are still owed.\n *\n * WHAT IS STILL TO SHIP, NOT WHAT WAS BOUGHT. A partially fulfilled order\n * exports its remaining units (`remainingFulfillmentLines`), so a label tool\n * that imports it again after the first parcel left asks for the rest and\n * not the whole order a second time. Digital and service lines never ship.\n *\n * WHICH ORDERS SHIP AT ALL is stamped on the order as `requiresShipping`\n * when it is written (`withOrderListFields`), so the export and the\n * ShipStation feed ask it of their Firestore query instead of reading every\n * order and dropping the downloads afterwards.\n */\n\n/** Grams in one avoirdupois ounce. */\nexport const GRAMS_PER_OUNCE = 28.349523125\n\n/** Ounces in one pound. */\nexport const OUNCES_PER_POUND = 16\n\n/** A weight in grams as ounces, to two places. */\nexport function gramsToOunces(grams: number): number {\n return Math.round((grams / GRAMS_PER_OUNCE) * 100) / 100\n}\n\n/** A weight in ounces as pounds, to two places. */\nexport function ouncesToPounds(ounces: number): number {\n return Math.round((ounces / OUNCES_PER_POUND) * 100) / 100\n}\n\n/**\n * Whether an order has anything to put in a box.\n *\n * A line with no recorded type predates types and counts as physical, the\n * reading that never hides an order a merchant has to ship. A register sale\n * left the shop in the buyer's hands, so a POS order ships only when it was\n * given a shipping address. A legacy Commerce Starter order with no lines\n * ships when it carries an address.\n */\nexport function orderRequiresShipping(order: Partial<HostOrder>): boolean {\n const lines = Array.isArray(order.lineItems) ? order.lineItems : []\n const address = hasShippingAddress(order.shippingAddress)\n if (!lines.length) return address\n if (!lines.some((line) => line && lineRequiresShipping(line))) return false\n return order.channel === 'pos' ? address : true\n}\n\nfunction hasShippingAddress(address: OrderAddress | undefined | null): boolean {\n return Boolean(address && String(address.line1 ?? '').trim())\n}\n\n/**\n * The fields a CREATOR of an order stamps beside its list fields: whether it\n * ships, and when it last changed — its creation, for a new order. Writers\n * that change an order afterwards stamp `updatedAtMs` themselves; nothing\n * else here moves after creation, because an order's lines never do.\n */\nexport function orderSyncFields(order: object): { requiresShipping: boolean; updatedAtMs?: number } {\n const source = order as Partial<HostOrder> & { updatedAtMs?: unknown; createdAtMs?: unknown }\n const at = Number(source.updatedAtMs ?? source.createdAtMs)\n return {\n requiresShipping: orderRequiresShipping(source),\n ...(Number.isFinite(at) && at > 0 ? { updatedAtMs: at } : {}),\n }\n}\n\n/** The number a shipping tool shows and sends back: `1042`, or the document id for an unnumbered order. */\nexport function shippingOrderNumber(order: Pick<HostOrder, 'number'>, docId: string): string {\n return typeof order.number === 'number' && Number.isFinite(order.number) ? String(order.number) : docId\n}\n\n/**\n * Every spelling of an order a file or a ShipNotice may name it by: `1042`,\n * `#1042` and the document id. What `parseShippingOrderRef` reads back.\n */\nexport function shippingOrderRefs(order: Pick<HostOrder, 'number'>, docId: string): string[] {\n const refs = [docId]\n if (typeof order.number === 'number' && Number.isFinite(order.number)) {\n refs.unshift(String(order.number), `#${order.number}`)\n }\n return refs\n}\n\n/** What a reference a tool sent back names: an order number, or a document id. */\nexport function parseShippingOrderRef(value: unknown): { number?: number; id?: string } | null {\n const text = String(value ?? '').trim()\n if (!text) return null\n const numbered = /^#?\\s*(\\d{1,12})$/.exec(text)\n if (numbered) return { number: Number(numbered[1]), id: text.replace(/^#\\s*/, '') }\n // Document ids are what Firestore accepts: no slash, not reserved.\n if (/[/]/.test(text) || /^__.*__$/.test(text) || text.length > 200) return null\n return { id: text }\n}\n\n/** A product's weights, keyed by variant id (`''` for a product with no variant named). */\nexport type ProductWeights = Readonly<Record<string, Readonly<Record<string, number>>>>\n\n/** One unit of a line's weight in grams, or `null` when the product records none. */\nexport function lineUnitGrams(line: Pick<OrderLineItem, 'productId' | 'variantId'>, weights: ProductWeights): number | null {\n const byVariant = weights[line.productId]\n if (!byVariant) return null\n const grams = line.variantId ? byVariant[line.variantId] : undefined\n const value = grams ?? byVariant['']\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null\n}\n\n/** One line still to ship: which line, how many units, and what one weighs. */\nexport interface ShippingLine {\n lineItemId: number\n line: OrderLineItem\n quantity: number\n unitGrams: number | null\n}\n\n/** The lines still to ship, with their remaining units. */\nexport function shippingLines(order: Pick<HostOrder, 'lineItems' | 'fulfillments'>, weights: ProductWeights = {}): ShippingLine[] {\n const lines = order.lineItems ?? []\n return remainingFulfillmentLines(order)\n .map((entry) => ({ entry, line: lines[entry.lineItemId] }))\n .filter((pair): pair is { entry: { lineItemId: number; quantity: number }; line: OrderLineItem } => Boolean(pair.line))\n .map(({ entry, line }) => ({\n lineItemId: entry.lineItemId,\n line,\n quantity: entry.quantity,\n unitGrams: lineUnitGrams(line, weights),\n }))\n}\n\n/**\n * The parcel's weight in ounces: every remaining unit's recorded weight.\n * `null` when no line records one, so a tool falls back to the default\n * package the merchant set there rather than a weight of zero.\n */\nexport function shippingWeightOunces(lines: readonly ShippingLine[]): number | null {\n let grams = 0\n let known = false\n for (const entry of lines) {\n if (entry.unitGrams === null) continue\n known = true\n grams += entry.unitGrams * entry.quantity\n }\n return known ? gramsToOunces(grams) : null\n}\n\n/** A line as a packing list names it: `2 × Mug (Blue), SKU MUG-B`. */\nexport function describeShippingLine(entry: Pick<ShippingLine, 'line' | 'quantity'>): string {\n const { line } = entry\n return `${entry.quantity} × ${line.name}${line.variantLabel ? ` (${line.variantLabel})` : ''}${line.sku ? `, SKU ${line.sku}` : ''}`\n}\n\nconst text = (value: unknown): string | null => {\n const trimmed = value === null || value === undefined ? '' : String(value).trim()\n return trimmed || null\n}\n\n/**\n * The shipping columns of one order, as the orders export writes them\n * (`ORDER_SHIPPING_FIELDS`). Every value is what the order holds; nothing is\n * invented — an order with no phone exports no phone.\n */\nexport function orderShippingRecord(\n docId: string,\n order: Partial<HostOrder>,\n weights: ProductWeights = {},\n): Record<string, unknown> {\n const address = order.shippingAddress ?? {}\n const lines = shippingLines({ lineItems: order.lineItems ?? [], fulfillments: order.fulfillments ?? [] }, weights)\n const ounces = shippingWeightOunces(lines)\n return {\n orderRef: shippingOrderNumber(order, docId),\n shipName: text(address.name) ?? text(order.customerName),\n shipLine1: text(address.line1),\n shipLine2: text(address.line2),\n shipCity: text(address.city),\n shipState: text(address.state),\n shipPostalCode: text(address.postalCode),\n shipCountry: text(address.country),\n shipPhone: text(address.phone) ?? text(order.customerPhone),\n shipEmail: text(order.customerEmail),\n itemsToShip: lines.length ? lines.map(describeShippingLine).join('; ') : null,\n unitsToShip: lines.reduce((sum, entry) => sum + entry.quantity, 0),\n weightOz: ounces,\n weightLb: ounces === null ? null : ouncesToPounds(ounces),\n weightUnit: ounces === null ? null : 'oz',\n }\n}\n\n/**\n * A tracking number as a parcel's identity: spaces and dashes dropped,\n * upper-cased. `9400 1000 0000` and `94001000000` are one parcel.\n */\nexport function normalizeTrackingNumber(value: unknown): string {\n return String(value ?? '')\n .replace(/[\\s-]+/g, '')\n .toUpperCase()\n}\n\n/** The active fulfillment already carrying this tracking number, if any. */\nexport function fulfillmentWithTracking(\n order: Pick<HostOrder, 'fulfillments'>,\n trackingNumber: unknown,\n): OrderFulfillment | undefined {\n const wanted = normalizeTrackingNumber(trackingNumber)\n if (!wanted) return undefined\n return (order.fulfillments ?? []).find(\n (entry) => fulfillmentIsActive(entry) && normalizeTrackingNumber(entry.trackingNumber) === wanted,\n )\n}\n"],"names":["fulfillmentIsActive","lineRequiresShipping","remainingFulfillmentLines","GRAMS_PER_OUNCE","OUNCES_PER_POUND","gramsToOunces","grams","Math","round","ouncesToPounds","ounces","orderRequiresShipping","order","lines","Array","isArray","lineItems","address","hasShippingAddress","shippingAddress","length","some","line","channel","Boolean","String","line1","trim","orderSyncFields","source","at","Number","updatedAtMs","createdAtMs","requiresShipping","isFinite","shippingOrderNumber","docId","number","shippingOrderRefs","refs","unshift","parseShippingOrderRef","value","text","numbered","exec","id","replace","test","lineUnitGrams","weights","byVariant","productId","variantId","undefined","shippingLines","map","entry","lineItemId","filter","pair","quantity","unitGrams","shippingWeightOunces","known","describeShippingLine","name","variantLabel","sku","trimmed","orderShippingRecord","fulfillments","orderRef","shipName","customerName","shipLine1","shipLine2","line2","shipCity","city","shipState","state","shipPostalCode","postalCode","shipCountry","country","shipPhone","phone","customerPhone","shipEmail","customerEmail","itemsToShip","join","unitsToShip","reduce","sum","weightOz","weightLb","weightUnit","normalizeTrackingNumber","toUpperCase","fulfillmentWithTracking","trackingNumber","wanted","find"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-shipping-export.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 { HostOrder, OrderAddress, OrderFulfillment, OrderLineItem } from './commerce-orders'\nimport { fulfillmentIsActive, lineRequiresShipping, remainingFulfillmentLines } from './order-fulfillment'\nimport { orderIsLocallyFulfilled } from './order-local-fulfillment'\n\n/*\n * AN ORDER AS A SHIPPING TOOL READS IT (AGL-3613).\n *\n * ShipStation's Custom Store feed, Pirate Ship's spreadsheet upload and the\n * Shippo and EasyPost CSVs all want the same facts about an order: who it\n * goes to, what is still to be packed, and what that weighs. This module is\n * that one reading, pure, so the XML feed and the CSV export cannot disagree\n * about an order's weight or which units are still owed.\n *\n * WHAT IS STILL TO SHIP, NOT WHAT WAS BOUGHT. A partially fulfilled order\n * exports its remaining units (`remainingFulfillmentLines`), so a label tool\n * that imports it again after the first parcel left asks for the rest and\n * not the whole order a second time. Digital and service lines never ship.\n *\n * WHICH ORDERS SHIP AT ALL is stamped on the order as `requiresShipping`\n * when it is written (`withOrderListFields`), so the export and the\n * ShipStation feed ask it of their Firestore query instead of reading every\n * order and dropping the downloads afterwards.\n */\n\n/** Grams in one avoirdupois ounce. */\nexport const GRAMS_PER_OUNCE = 28.349523125\n\n/** Ounces in one pound. */\nexport const OUNCES_PER_POUND = 16\n\n/** A weight in grams as ounces, to two places. */\nexport function gramsToOunces(grams: number): number {\n return Math.round((grams / GRAMS_PER_OUNCE) * 100) / 100\n}\n\n/** A weight in ounces as pounds, to two places. */\nexport function ouncesToPounds(ounces: number): number {\n return Math.round((ounces / OUNCES_PER_POUND) * 100) / 100\n}\n\n/**\n * Whether an order has anything to put in a box.\n *\n * A line with no recorded type predates types and counts as physical, the\n * reading that never hides an order a merchant has to ship. A register sale\n * left the shop in the buyer's hands, so a POS order ships only when it was\n * given a shipping address. A legacy Commerce Starter order with no lines\n * ships when it carries an address.\n */\nexport function orderRequiresShipping(order: Partial<HostOrder>): boolean {\n // Collected at a location or brought by the store's own driver (AGL-3624):\n // nothing for a shipping tool to label, whatever is in it.\n if (orderIsLocallyFulfilled(order)) return false\n const lines = Array.isArray(order.lineItems) ? order.lineItems : []\n const address = hasShippingAddress(order.shippingAddress)\n if (!lines.length) return address\n if (!lines.some((line) => line && lineRequiresShipping(line))) return false\n return order.channel === 'pos' ? address : true\n}\n\nfunction hasShippingAddress(address: OrderAddress | undefined | null): boolean {\n return Boolean(address && String(address.line1 ?? '').trim())\n}\n\n/**\n * The fields a CREATOR of an order stamps beside its list fields: whether it\n * ships, and when it last changed — its creation, for a new order. Writers\n * that change an order afterwards stamp `updatedAtMs` themselves; nothing\n * else here moves after creation, because an order's lines never do.\n */\nexport function orderSyncFields(order: object): { requiresShipping: boolean; updatedAtMs?: number } {\n const source = order as Partial<HostOrder> & { updatedAtMs?: unknown; createdAtMs?: unknown }\n const at = Number(source.updatedAtMs ?? source.createdAtMs)\n return {\n requiresShipping: orderRequiresShipping(source),\n ...(Number.isFinite(at) && at > 0 ? { updatedAtMs: at } : {}),\n }\n}\n\n/** The number a shipping tool shows and sends back: `1042`, or the document id for an unnumbered order. */\nexport function shippingOrderNumber(order: Pick<HostOrder, 'number'>, docId: string): string {\n return typeof order.number === 'number' && Number.isFinite(order.number) ? String(order.number) : docId\n}\n\n/**\n * Every spelling of an order a file or a ShipNotice may name it by: `1042`,\n * `#1042` and the document id. What `parseShippingOrderRef` reads back.\n */\nexport function shippingOrderRefs(order: Pick<HostOrder, 'number'>, docId: string): string[] {\n const refs = [docId]\n if (typeof order.number === 'number' && Number.isFinite(order.number)) {\n refs.unshift(String(order.number), `#${order.number}`)\n }\n return refs\n}\n\n/** What a reference a tool sent back names: an order number, or a document id. */\nexport function parseShippingOrderRef(value: unknown): { number?: number; id?: string } | null {\n const text = String(value ?? '').trim()\n if (!text) return null\n const numbered = /^#?\\s*(\\d{1,12})$/.exec(text)\n if (numbered) return { number: Number(numbered[1]), id: text.replace(/^#\\s*/, '') }\n // Document ids are what Firestore accepts: no slash, not reserved.\n if (/[/]/.test(text) || /^__.*__$/.test(text) || text.length > 200) return null\n return { id: text }\n}\n\n/** A product's weights, keyed by variant id (`''` for a product with no variant named). */\nexport type ProductWeights = Readonly<Record<string, Readonly<Record<string, number>>>>\n\n/** One unit of a line's weight in grams, or `null` when the product records none. */\nexport function lineUnitGrams(line: Pick<OrderLineItem, 'productId' | 'variantId'>, weights: ProductWeights): number | null {\n const byVariant = weights[line.productId]\n if (!byVariant) return null\n const grams = line.variantId ? byVariant[line.variantId] : undefined\n const value = grams ?? byVariant['']\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null\n}\n\n/** One line still to ship: which line, how many units, and what one weighs. */\nexport interface ShippingLine {\n lineItemId: number\n line: OrderLineItem\n quantity: number\n unitGrams: number | null\n}\n\n/** The lines still to ship, with their remaining units. */\nexport function shippingLines(order: Pick<HostOrder, 'lineItems' | 'fulfillments'>, weights: ProductWeights = {}): ShippingLine[] {\n const lines = order.lineItems ?? []\n return remainingFulfillmentLines(order)\n .map((entry) => ({ entry, line: lines[entry.lineItemId] }))\n .filter((pair): pair is { entry: { lineItemId: number; quantity: number }; line: OrderLineItem } => Boolean(pair.line))\n .map(({ entry, line }) => ({\n lineItemId: entry.lineItemId,\n line,\n quantity: entry.quantity,\n unitGrams: lineUnitGrams(line, weights),\n }))\n}\n\n/**\n * The parcel's weight in ounces: every remaining unit's recorded weight.\n * `null` when no line records one, so a tool falls back to the default\n * package the merchant set there rather than a weight of zero.\n */\nexport function shippingWeightOunces(lines: readonly ShippingLine[]): number | null {\n let grams = 0\n let known = false\n for (const entry of lines) {\n if (entry.unitGrams === null) continue\n known = true\n grams += entry.unitGrams * entry.quantity\n }\n return known ? gramsToOunces(grams) : null\n}\n\n/** A line as a packing list names it: `2 × Mug (Blue), SKU MUG-B`. */\nexport function describeShippingLine(entry: Pick<ShippingLine, 'line' | 'quantity'>): string {\n const { line } = entry\n return `${entry.quantity} × ${line.name}${line.variantLabel ? ` (${line.variantLabel})` : ''}${line.sku ? `, SKU ${line.sku}` : ''}`\n}\n\nconst text = (value: unknown): string | null => {\n const trimmed = value === null || value === undefined ? '' : String(value).trim()\n return trimmed || null\n}\n\n/**\n * The shipping columns of one order, as the orders export writes them\n * (`ORDER_SHIPPING_FIELDS`). Every value is what the order holds; nothing is\n * invented — an order with no phone exports no phone.\n */\nexport function orderShippingRecord(\n docId: string,\n order: Partial<HostOrder>,\n weights: ProductWeights = {},\n): Record<string, unknown> {\n const address = order.shippingAddress ?? {}\n const lines = shippingLines({ lineItems: order.lineItems ?? [], fulfillments: order.fulfillments ?? [] }, weights)\n const ounces = shippingWeightOunces(lines)\n return {\n orderRef: shippingOrderNumber(order, docId),\n shipName: text(address.name) ?? text(order.customerName),\n shipLine1: text(address.line1),\n shipLine2: text(address.line2),\n shipCity: text(address.city),\n shipState: text(address.state),\n shipPostalCode: text(address.postalCode),\n shipCountry: text(address.country),\n shipPhone: text(address.phone) ?? text(order.customerPhone),\n shipEmail: text(order.customerEmail),\n itemsToShip: lines.length ? lines.map(describeShippingLine).join('; ') : null,\n unitsToShip: lines.reduce((sum, entry) => sum + entry.quantity, 0),\n weightOz: ounces,\n weightLb: ounces === null ? null : ouncesToPounds(ounces),\n weightUnit: ounces === null ? null : 'oz',\n }\n}\n\n/**\n * A tracking number as a parcel's identity: spaces and dashes dropped,\n * upper-cased. `9400 1000 0000` and `94001000000` are one parcel.\n */\nexport function normalizeTrackingNumber(value: unknown): string {\n return String(value ?? '')\n .replace(/[\\s-]+/g, '')\n .toUpperCase()\n}\n\n/** The active fulfillment already carrying this tracking number, if any. */\nexport function fulfillmentWithTracking(\n order: Pick<HostOrder, 'fulfillments'>,\n trackingNumber: unknown,\n): OrderFulfillment | undefined {\n const wanted = normalizeTrackingNumber(trackingNumber)\n if (!wanted) return undefined\n return (order.fulfillments ?? []).find(\n (entry) => fulfillmentIsActive(entry) && normalizeTrackingNumber(entry.trackingNumber) === wanted,\n )\n}\n"],"names":["fulfillmentIsActive","lineRequiresShipping","remainingFulfillmentLines","orderIsLocallyFulfilled","GRAMS_PER_OUNCE","OUNCES_PER_POUND","gramsToOunces","grams","Math","round","ouncesToPounds","ounces","orderRequiresShipping","order","lines","Array","isArray","lineItems","address","hasShippingAddress","shippingAddress","length","some","line","channel","Boolean","String","line1","trim","orderSyncFields","source","at","Number","updatedAtMs","createdAtMs","requiresShipping","isFinite","shippingOrderNumber","docId","number","shippingOrderRefs","refs","unshift","parseShippingOrderRef","value","text","numbered","exec","id","replace","test","lineUnitGrams","weights","byVariant","productId","variantId","undefined","shippingLines","map","entry","lineItemId","filter","pair","quantity","unitGrams","shippingWeightOunces","known","describeShippingLine","name","variantLabel","sku","trimmed","orderShippingRecord","fulfillments","orderRef","shipName","customerName","shipLine1","shipLine2","line2","shipCity","city","shipState","state","shipPostalCode","postalCode","shipCountry","country","shipPhone","phone","customerPhone","shipEmail","customerEmail","itemsToShip","join","unitsToShip","reduce","sum","weightOz","weightLb","weightUnit","normalizeTrackingNumber","toUpperCase","fulfillmentWithTracking","trackingNumber","wanted","find"],"mappings":"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"}
@@ -15,6 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { HostOrder, OrderStatus } from './commerce-orders';
18
+ import { type OrderLocalDeliveryStatus, type OrderPickupStatus } from './order-local-fulfillment';
18
19
  /**
19
20
  * What the guest order-status page shows (AGL-3610): the ONE projection of an
20
21
  * order that leaves the server for a visitor holding a signed link.
@@ -46,7 +47,7 @@ export interface OrderStatusShipment {
46
47
  }>;
47
48
  }
48
49
  export interface OrderStatusStep {
49
- key: 'placed' | 'paid' | 'shipped' | 'delivered' | 'cancelled' | 'refunded';
50
+ key: 'placed' | 'paid' | 'shipped' | 'delivered' | 'cancelled' | 'refunded' | 'ready_for_pickup' | 'picked_up' | 'out_for_delivery';
50
51
  label: string;
51
52
  atMs: number | null;
52
53
  done: boolean;
@@ -91,6 +92,24 @@ export interface OrderStatusView {
91
92
  shipments: OrderStatusShipment[];
92
93
  steps: OrderStatusStep[];
93
94
  actions: OrderStatusAction[];
95
+ /**
96
+ * Where to collect a pickup order (AGL-3624) — the location the buyer chose
97
+ * and its hours, as the ready-for-pickup email states them.
98
+ */
99
+ pickup?: {
100
+ locationName: string;
101
+ address: string | null;
102
+ hours: string | null;
103
+ instructions: string | null;
104
+ status: OrderPickupStatus;
105
+ statusLabel: string;
106
+ };
107
+ /** When the store's own driver brings it (AGL-3624). */
108
+ localDelivery?: {
109
+ windowLabel: string | null;
110
+ status: OrderLocalDeliveryStatus;
111
+ statusLabel: string;
112
+ };
94
113
  }
95
114
  /** Builds the public projection. Pure; the caller has verified the link. */
96
115
  export declare function buildOrderStatusView(input: {