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

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 (135) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/cart.js +3 -2
  3. package/src/lib/components/cart.js.map +1 -1
  4. package/src/lib/components/console/commerce-analytics-card.component.js +3 -6
  5. package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
  6. package/src/lib/components/console/commerce-glance-card.component.js +9 -3
  7. package/src/lib/components/console/commerce-glance-card.component.js.map +1 -1
  8. package/src/lib/components/console/customer-notifications-card.component.js +3 -1
  9. package/src/lib/components/console/customer-notifications-card.component.js.map +1 -1
  10. package/src/lib/components/console/discounts-card.component.js +2 -3
  11. package/src/lib/components/console/discounts-card.component.js.map +1 -1
  12. package/src/lib/components/console/host-coupons-card.component.js +3 -4
  13. package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
  14. package/src/lib/components/console/local-delivery-settings-card.component.js +2 -2
  15. package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -1
  16. package/src/lib/components/console/locations-card.component.js +1 -1
  17. package/src/lib/components/console/locations-card.component.js.map +1 -1
  18. package/src/lib/components/console/member-posts-card.component.js +2 -2
  19. package/src/lib/components/console/member-posts-card.component.js.map +1 -1
  20. package/src/lib/components/console/order-detail-dialog.component.js +35 -30
  21. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  22. package/src/lib/components/console/order-fulfillment-panel.component.d.ts +11 -1
  23. package/src/lib/components/console/order-fulfillment-panel.component.js +75 -4
  24. package/src/lib/components/console/order-fulfillment-panel.component.js.map +1 -1
  25. package/src/lib/components/console/order-zones.d.ts +45 -1
  26. package/src/lib/components/console/order-zones.js +8 -0
  27. package/src/lib/components/console/order-zones.js.map +1 -1
  28. package/src/lib/components/console/pickup-delivery-queue-card.component.js +15 -1
  29. package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -1
  30. package/src/lib/components/console/pos-page.component.js +3 -2
  31. package/src/lib/components/console/pos-page.component.js.map +1 -1
  32. package/src/lib/components/console/product-editor-dialog.component.d.ts +2 -0
  33. package/src/lib/components/console/product-editor-dialog.component.js +30 -12
  34. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  35. package/src/lib/components/console/products-hub-card.component.js +13 -4
  36. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  37. package/src/lib/components/console/products-hub-zone.component.d.ts +2 -1
  38. package/src/lib/components/console/products-hub-zone.component.js +1 -1
  39. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  40. package/src/lib/components/console/reviews-moderation-card.component.js +1 -2
  41. package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -1
  42. package/src/lib/components/console/shipping-settings-card.component.js +2 -2
  43. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
  44. package/src/lib/components/console/shippingeasy-card.component.js +1 -1
  45. package/src/lib/components/console/shippingeasy-card.component.js.map +1 -1
  46. package/src/lib/components/console/shipstation-card.component.js +1 -2
  47. package/src/lib/components/console/shipstation-card.component.js.map +1 -1
  48. package/src/lib/components/console/store-settings-card.component.js +3 -1
  49. package/src/lib/components/console/store-settings-card.component.js.map +1 -1
  50. package/src/lib/components/console/tax-settings-card.component.js +4 -1
  51. package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
  52. package/src/lib/components/order-status.js +24 -1
  53. package/src/lib/components/order-status.js.map +1 -1
  54. package/src/lib/components/product-detail.js +30 -16
  55. package/src/lib/components/product-detail.js.map +1 -1
  56. package/src/lib/components/product-grid.d.ts +7 -0
  57. package/src/lib/components/product-grid.js +50 -7
  58. package/src/lib/components/product-grid.js.map +1 -1
  59. package/src/lib/components/related-products.js +1 -1
  60. package/src/lib/components/related-products.js.map +1 -1
  61. package/src/lib/components/wishlist.js +1 -1
  62. package/src/lib/components/wishlist.js.map +1 -1
  63. package/src/lib/declarations.server.js +11 -0
  64. package/src/lib/declarations.server.js.map +1 -1
  65. package/src/lib/model/commerce-orders.d.ts +8 -0
  66. package/src/lib/model/commerce-orders.js.map +1 -1
  67. package/src/lib/model/commerce.d.ts +21 -0
  68. package/src/lib/model/commerce.js +39 -1
  69. package/src/lib/model/commerce.js.map +1 -1
  70. package/src/lib/model/index.d.ts +1 -0
  71. package/src/lib/model/index.js +1 -0
  72. package/src/lib/model/index.js.map +1 -1
  73. package/src/lib/model/order-annotations.d.ts +54 -0
  74. package/src/lib/model/order-annotations.js +76 -0
  75. package/src/lib/model/order-annotations.js.map +1 -0
  76. package/src/lib/model/order-events.d.ts +2 -0
  77. package/src/lib/model/order-events.js.map +1 -1
  78. package/src/lib/model/order-fulfillment.d.ts +29 -0
  79. package/src/lib/model/order-fulfillment.js +63 -0
  80. package/src/lib/model/order-fulfillment.js.map +1 -1
  81. package/src/lib/model/order-local-fulfillment.d.ts +36 -0
  82. package/src/lib/model/order-local-fulfillment.js +19 -1
  83. package/src/lib/model/order-local-fulfillment.js.map +1 -1
  84. package/src/lib/model/order-status-view.d.ts +11 -0
  85. package/src/lib/model/order-status-view.js +20 -3
  86. package/src/lib/model/order-status-view.js.map +1 -1
  87. package/src/lib/model/product-write.js +8 -4
  88. package/src/lib/model/product-write.js.map +1 -1
  89. package/src/lib/server/api-v1/openapi.js +1 -0
  90. package/src/lib/server/api-v1/openapi.js.map +1 -1
  91. package/src/lib/server/api-v1/order-view.d.ts +1 -0
  92. package/src/lib/server/api-v1/order-view.js +10 -0
  93. package/src/lib/server/api-v1/order-view.js.map +1 -1
  94. package/src/lib/server/cart-checkout.js +9 -1
  95. package/src/lib/server/cart-checkout.js.map +1 -1
  96. package/src/lib/server/catalog.d.ts +6 -0
  97. package/src/lib/server/catalog.js +3 -1
  98. package/src/lib/server/catalog.js.map +1 -1
  99. package/src/lib/server/checkout.js +11 -1
  100. package/src/lib/server/checkout.js.map +1 -1
  101. package/src/lib/server/fulfill-order.d.ts +12 -0
  102. package/src/lib/server/fulfill-order.js +13 -0
  103. package/src/lib/server/fulfill-order.js.map +1 -1
  104. package/src/lib/server/local-delivery-records.d.ts +30 -0
  105. package/src/lib/server/local-delivery-records.js +284 -0
  106. package/src/lib/server/local-delivery-records.js.map +1 -0
  107. package/src/lib/server/order-annotations.d.ts +28 -0
  108. package/src/lib/server/order-annotations.js +143 -0
  109. package/src/lib/server/order-annotations.js.map +1 -0
  110. package/src/lib/server/order-events.js +2 -1
  111. package/src/lib/server/order-events.js.map +1 -1
  112. package/src/lib/server/order-notifications.js +39 -8
  113. package/src/lib/server/order-notifications.js.map +1 -1
  114. package/src/lib/server/pos-payment.js +9 -3
  115. package/src/lib/server/pos-payment.js.map +1 -1
  116. package/src/lib/server/product-catalog.js +6 -2
  117. package/src/lib/server/product-catalog.js.map +1 -1
  118. package/src/lib/server/product-drafts.d.ts +36 -9
  119. package/src/lib/server/product-drafts.js +24 -5
  120. package/src/lib/server/product-drafts.js.map +1 -1
  121. package/src/lib/server/product.d.ts +6 -1
  122. package/src/lib/server/product.js +1 -1
  123. package/src/lib/server/product.js.map +1 -1
  124. package/src/lib/server/products-write.d.ts +29 -0
  125. package/src/lib/server/products-write.js +7 -7
  126. package/src/lib/server/products-write.js.map +1 -1
  127. package/src/lib/server/shipment-records.js +2 -0
  128. package/src/lib/server/shipment-records.js.map +1 -1
  129. package/src/lib/server/site-page-resolver.d.ts +13 -0
  130. package/src/lib/server/site-page-resolver.js +138 -1
  131. package/src/lib/server/site-page-resolver.js.map +1 -1
  132. package/src/lib/server.js +5 -0
  133. package/src/lib/server.js.map +1 -1
  134. package/src/lib/utils/use-storefront-purchase-event.js +8 -2
  135. package/src/lib/utils/use-storefront-purchase-event.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/fulfill-order.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as CommerceModel from '../model'\nimport { announcePluginShipment } from '@aglyn/aglyn/plugin-manager/plugin-shipment-records'\nimport { createResourceUid, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { notifyOrderBuyer } from './order-notifications'\nimport { ORDER_DELIVERED_EVENT, ORDER_FULFILLED_EVENT } from '../model/order-events'\nimport { fulfillmentEventView, stageOrderEvent } from './order-events'\nimport { createHash } from 'crypto'\n\n/**\n * The two fulfilment-side transitions, and ONLY those.\n *\n * `cancelled` releases stock under its own transaction and `refunded` moves\n * money under another, so neither belongs to a write whose contract is \"a\n * forward status flip plus a timeline entry, no stock moved, no money moved\".\n * Widening this union is how a caller gets a door around the specifics those\n * routes exist to enforce — the type is the door being shut.\n */\nexport type OrderFulfilmentTarget = 'fulfilled' | 'delivered'\n\n/** How long a carrier name and a tracking number may be, on every door. */\nexport const CARRIER_MAX = 40\nexport const TRACKING_NUMBER_MAX = 60\nconst URL_MAX = 2048\n\nexport interface RecordShipmentRequest {\n /** Site that owns the order. The CALLER has already proven ownership. */\n hostId: string\n orderId: string\n to: OrderFulfilmentTarget\n /** Free text, e.g. `'UPS'`. Bounded by the caller. */\n carrier?: string\n trackingNumber?: string\n /**\n * Which units this shipment covers (AGL-3611). Absent means everything\n * still to ship, which is what a fulfil meant before quantities existed.\n * Ignored for `delivered`.\n */\n lineItems?: ReadonlyArray<{ lineItemId: number; quantity: number }>\n /** An explicit tracking link; without one it is derived from the carrier. */\n trackingUrl?: string\n /** A label a shipping plugin bought for this parcel. */\n labelUrl?: string\n /** That plugin's id for the label (AGL-3612), kept on the fulfillment. */\n labelRef?: string\n /** Whether the buyer is to be told; recorded on the fulfillment. Default on. */\n notify?: boolean\n /**\n * One shipment attempt. A retry carrying the same key finds the fulfillment\n * it already wrote and answers `already`, so a partial shipment, which\n * leaves the order open to more, still cannot be recorded twice.\n */\n idempotencyKey?: string\n /**\n * A shipment whose tracking number an active fulfillment of this order\n * already carries is the same parcel, answered `already` (AGL-3613). For\n * the doors a shipping tool posts to — ShipStation's ShipNotice, a tracking\n * file — where the number is the parcel's identity and the same parcel may\n * have been recorded by hand first.\n */\n onceByTracking?: boolean\n}\n\n/**\n * What happened, as a domain fact rather than an HTTP status — each caller\n * maps it into its own error vocabulary (the console route into its JSON\n * shape, `/v1` into the published error envelope), because those two\n * vocabularies are contracts with different audiences and neither may leak\n * into the other.\n *\n * `already` is a SUCCESS and is distinct from `recorded` on purpose: a retried\n * request finds the order in the target status (or its keyed fulfillment\n * already written) and returns without writing, so a lost response can never\n * append a second copy of the same shipment.\n *\n * `recorded` carries the fulfillment it wrote (absent for `delivered`) and the\n * status the order is now in, so a caller can tell the buyer what shipped.\n */\nexport type RecordShipmentOutcome =\n | {\n outcome: 'recorded'\n status: CommerceModel.OrderStatus\n fulfillment?: CommerceModel.OrderFulfillment\n }\n | { outcome: 'already'; fulfillment?: CommerceModel.OrderFulfillment }\n | { outcome: 'no_such_order' }\n /** The transition rule refused. `from` is the status that refused it. */\n | { outcome: 'blocked'; from: string }\n /** The requested lines or quantities were refused; nothing was written. */\n | { outcome: 'invalid_lines'; problem: CommerceModel.FulfillmentLinesProblem; message: string }\n\n/** Trims a carrier and a tracking number to what every door stores. */\nexport function boundTracking(input: { carrier?: unknown; trackingNumber?: unknown }): {\n carrier: string\n trackingNumber: string\n} {\n return {\n carrier: String(input.carrier ?? '').trim().slice(0, CARRIER_MAX),\n trackingNumber: String(input.trackingNumber ?? '').trim().slice(0, TRACKING_NUMBER_MAX),\n }\n}\n\n/** An https link or nothing: a stored link is rendered to buyers as an anchor. */\nexport function safeHttpsUrl(value: unknown): string | undefined {\n const text = String(value ?? '').trim()\n if (!text || text.length > URL_MAX) return undefined\n try {\n const url = new URL(text)\n return url.protocol === 'https:' ? url.toString() : undefined\n } catch {\n return undefined\n }\n}\n\n/** The fulfillment id a keyed attempt writes, so a retry finds it. */\nexport function fulfillmentIdForKey(orderId: string, key: string): string {\n return `f-${createHash('sha256').update(`${orderId}:${key}`).digest('hex').slice(0, 20)}`\n}\n\n/** The tracking keys a fulfillment stores: the number, the carrier and the link. */\nfunction trackingFields(\n carrier: string,\n trackingNumber: string,\n explicitUrl: string | undefined,\n): Pick<CommerceModel.OrderFulfillment, 'carrier' | 'trackingNumber' | 'trackingUrl'> {\n const trackingUrl =\n explicitUrl ?? (trackingNumber ? CommerceModel.trackingUrlFor(carrier, trackingNumber) ?? undefined : undefined)\n return {\n ...(carrier ? { carrier } : {}),\n ...(trackingNumber ? { trackingNumber } : {}),\n ...(trackingUrl ? { trackingUrl } : {}),\n }\n}\n\n/** The timeline's words for a shipment. */\nfunction shipmentDetail(\n order: CommerceModel.HostOrder,\n lines: readonly CommerceModel.OrderFulfillmentLine[],\n partial: boolean,\n carrier: string,\n trackingNumber: string,\n): string {\n const tracking = trackingNumber ? `${carrier || 'Shipped'} ${trackingNumber}` : ''\n if (!partial) return tracking || 'Fulfilled'\n const what = CommerceModel.describeFulfillmentLines(order, lines)\n return tracking ? `${what} — ${tracking}` : `${what} shipped`\n}\n\nfunction orderRefFor(hostId: string, orderId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('orders')\n .doc(orderId)\n}\n\n/**\n * Record a shipment — the transaction, with NO authorization of any kind\n * (AGL-2461).\n *\n * This is the whole of what `fulfillOrderHandler` used to do below its auth\n * preamble, lifted out unchanged so a SECOND caller can have it: the customer\n * REST API's `PATCH /v1/sites/{id}/orders/{id}`\n * (`api-v1/orders-and-products.ts`), which authenticates an org API key and\n * so shares not one line of this function's former preamble.\n *\n * Lifted rather than copied, and that is the entire point: a second\n * `ORDER_TRANSITIONS` inside `/v1` would be two tables that drift into the\n * API writing a status the console forbids.\n *\n * **The caller authorizes.** Everything this function knows is the transition\n * rule. It does not know who is asking, and it will happily move any order on\n * any host — `hostId` is taken on trust because its two callers establish\n * trust in ways that have nothing in common (a Firebase uid in `memberRoles`\n * for the console; an org credential that owns the host for `/v1`).\n *\n * QUANTITIES (AGL-3611). A shipment names units, not just lines, and the\n * remaining count is computed from the transaction's own read, so two admins\n * shipping the last 3 mugs at once cannot ship 6: the second meets\n * `invalid_lines` with `over_fulfilled`. The resulting status is computed\n * from the fulfillments (`statusFromFulfillments`) — `partially_fulfilled`\n * until every shippable unit is covered — and the transition rule is re-asked\n * only when the status actually moves, since a second partial shipment on a\n * `partially_fulfilled` order changes nothing the table speaks to.\n *\n * Every guarantee AGL-1819 built is inside the transaction and therefore\n * inherited by both callers: `canTransitionOrder` is re-asked under the write\n * so a stale caller cannot skip a state, the already-in-target case returns\n * without writing so a retry cannot append a duplicate shipment, and the\n * lines and the timeline are computed from the transaction's OWN read so a\n * note landed from another tab survives.\n */\nexport async function recordOrderShipment(\n request: RecordShipmentRequest,\n): Promise<RecordShipmentOutcome> {\n const { hostId, orderId, to } = request\n const { carrier, trackingNumber } = boundTracking(request)\n const explicitUrl = safeHttpsUrl(request.trackingUrl)\n const labelUrl = safeHttpsUrl(request.labelUrl)\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = orderRefFor(hostId, orderId)\n const keyedId = request.idempotencyKey\n ? fulfillmentIdForKey(orderId, String(request.idempotencyKey).slice(0, 200))\n : null\n\n const result = await firestore.runTransaction(\n async (transaction): Promise<RecordShipmentOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder(\n (snapshot.data() ?? {}) as never,\n )\n const atMs = Date.now()\n\n if (to === 'delivered') {\n if (order.status === 'delivered') return { outcome: 'already' }\n if (!CommerceModel.canTransitionOrder(order.status, 'delivered')) {\n return { outcome: 'blocked', from: order.status }\n }\n const delivered = {\n status: 'delivered',\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(order, 'delivered', undefined, atMs),\n }\n transaction.update(orderRef, delivered)\n stageOrderEvent(transaction, ORDER_DELIVERED_EVENT, { hostId, orderId, key: 'delivered', order: { ...snapshot.data(), ...delivered } })\n return { outcome: 'recorded', status: 'delivered' }\n }\n\n // A redelivered keyed attempt: the fulfillment it wrote is there.\n if (keyedId) {\n const existing = (order.fulfillments ?? []).find((entry) => entry.id === keyedId)\n if (existing) return { outcome: 'already', fulfillment: existing }\n }\n if (request.onceByTracking && trackingNumber) {\n const same = CommerceModel.fulfillmentWithTracking(order, trackingNumber)\n if (same) return { outcome: 'already', fulfillment: same }\n }\n const named = (request.lineItems ?? []).length > 0\n // A redelivered click, or the second of two admins, on a \"fulfil\n // everything\". Success because it IS the state the caller asked for —\n // but nothing is written, so a retry cannot append a second copy.\n if (!named && order.status === 'fulfilled') return { outcome: 'already' }\n if (\n order.status !== 'partially_fulfilled' &&\n order.status !== 'fulfilled' &&\n !CommerceModel.canTransitionOrder(order.status, 'partially_fulfilled') &&\n !CommerceModel.canTransitionOrder(order.status, 'fulfilled')\n ) {\n return { outcome: 'blocked', from: order.status }\n }\n\n let lines: CommerceModel.OrderFulfillmentLine[]\n if (named) {\n const resolved = CommerceModel.resolveFulfillmentLines(order, request.lineItems ?? [])\n if (!('lines' in resolved)) {\n return {\n outcome: 'invalid_lines',\n problem: resolved,\n message: CommerceModel.describeFulfillmentLinesProblem(resolved),\n }\n }\n lines = resolved.lines\n } else {\n lines = CommerceModel.remainingFulfillmentLines(order)\n // Nothing physical left: a fulfil closes the digital and service\n // lines instead, which is what it did before quantities existed.\n if (lines.length === 0) {\n lines = CommerceModel.orderLineFulfillmentStates(order)\n .filter((state) => state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n }\n }\n\n const fulfillment: CommerceModel.OrderFulfillment = {\n id: keyedId ?? createResourceUid(),\n lineItemIds: lines.map((entry) => entry.lineItemId),\n lines,\n ...trackingFields(carrier, trackingNumber, explicitUrl),\n ...(labelUrl ? { labelUrl } : {}),\n ...(request.labelRef ? { labelRef: String(request.labelRef).slice(0, 80) } : {}),\n status: 'active',\n notify: request.notify !== false,\n atMs,\n }\n const fulfillments = [...(order.fulfillments ?? []), fulfillment]\n let status: CommerceModel.OrderStatus = CommerceModel.statusFromFulfillments({\n lineItems: order.lineItems,\n fulfillments,\n })\n // A fulfil with nothing left to cover still closes the order.\n if (status === 'paid') status = 'fulfilled'\n if (status !== order.status && !CommerceModel.canTransitionOrder(order.status, status)) {\n return { outcome: 'blocked', from: order.status }\n }\n const shipped = {\n status,\n fulfillments,\n // What a shipping tool's feed asks (AGL-3613): which orders changed.\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n status,\n shipmentDetail(order, lines, status === 'partially_fulfilled', carrier, trackingNumber),\n atMs,\n ),\n }\n transaction.update(orderRef, shipped)\n // Once per shipment (AGL-3611), in the same write, so the event exists\n // exactly when the shipment does.\n stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, { hostId, orderId, key: fulfillment.id, order: { ...snapshot.data(), ...shipped }, extra: { fulfillment: fulfillmentEventView(order, fulfillment) } })\n return { outcome: 'recorded', status, fulfillment }\n },\n )\n // The BUYER hears about it (AGL-3610): shipped with the tracking link, or\n // delivered. Once per shipment, after the write, and never fails the\n // shipment — see `order-notifications.ts`. A retry returns `already` above\n // and never reaches here. A merchant who unticked \"Notify customer\"\n // (AGL-3611) recorded the shipment with `notify: false`, and nobody is told.\n // Every shipment recorded, by any door, is announced (AGL-3612), so a\n // plugin that follows parcels can follow one typed in by hand too.\n if (result.outcome === 'recorded' && result.fulfillment?.trackingNumber) {\n await announcePluginShipment({\n hostId,\n recordId: orderId,\n shipmentId: result.fulfillment.id,\n ...(result.fulfillment.carrier ? { carrier: result.fulfillment.carrier } : {}),\n trackingNumber: result.fulfillment.trackingNumber,\n ...(request.labelRef ? { labelRef: request.labelRef } : {}),\n })\n }\n if (result.outcome === 'recorded' && result.fulfillment?.notify !== false) {\n await notifyOrderBuyer(\n { hostId, orderId },\n to === 'fulfilled' ? 'shipped' : 'delivered',\n result.fulfillment ? { fulfillmentId: result.fulfillment.id } : {},\n )\n }\n return result\n}\n\n/** What an edit or a cancel of one fulfillment came to. */\nexport type FulfillmentChangeOutcome =\n | { outcome: 'updated'; status: CommerceModel.OrderStatus; fulfillment: CommerceModel.OrderFulfillment }\n | { outcome: 'already'; fulfillment: CommerceModel.OrderFulfillment }\n | { outcome: 'no_such_order' }\n | { outcome: 'no_such_fulfillment' }\n /** The order is history (delivered, refunded, cancelled); `from` names it. */\n | { outcome: 'locked'; from: string }\n\n/**\n * Corrects the tracking on one fulfillment (AGL-3611): a mistyped number, a\n * carrier swapped, a label bought after the parcel was recorded. Moves no\n * status and no stock. A cancelled fulfillment is history and is refused.\n */\nexport async function updateFulfillmentTracking(request: {\n hostId: string\n orderId: string\n fulfillmentId: string\n carrier?: string\n trackingNumber?: string\n trackingUrl?: string\n}): Promise<FulfillmentChangeOutcome> {\n const { carrier, trackingNumber } = boundTracking(request)\n const explicitUrl = safeHttpsUrl(request.trackingUrl)\n const orderRef = orderRefFor(request.hostId, request.orderId)\n return firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction): Promise<FulfillmentChangeOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const index = (order.fulfillments ?? []).findIndex((entry) => entry.id === request.fulfillmentId)\n if (index < 0) return { outcome: 'no_such_fulfillment' }\n const current = (order.fulfillments ?? [])[index]\n if (!CommerceModel.fulfillmentIsActive(current) || !['paid', 'partially_fulfilled', 'fulfilled', 'delivered'].includes(order.status)) {\n return { outcome: 'locked', from: CommerceModel.fulfillmentIsActive(current) ? order.status : 'cancelled' }\n }\n const atMs = Date.now()\n const next: CommerceModel.OrderFulfillment = { ...current, updatedAtMs: atMs }\n delete next.carrier\n delete next.trackingNumber\n delete next.trackingUrl\n Object.assign(next, trackingFields(carrier, trackingNumber, explicitUrl))\n if (\n (current.carrier ?? '') === (next.carrier ?? '') &&\n (current.trackingNumber ?? '') === (next.trackingNumber ?? '') &&\n (current.trackingUrl ?? '') === (next.trackingUrl ?? '')\n ) {\n return { outcome: 'already', fulfillment: current }\n }\n const fulfillments = [...(order.fulfillments ?? [])]\n fulfillments[index] = next\n transaction.update(orderRef, {\n fulfillments,\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'tracking-updated',\n trackingNumber ? `${carrier || 'Tracking'} ${trackingNumber}` : 'Tracking removed',\n atMs,\n ),\n })\n return { outcome: 'updated', status: order.status, fulfillment: next }\n })\n}\n\n/**\n * Cancels one fulfillment (AGL-3611): the parcel was never handed over, or was\n * recorded against the wrong items. Its units go back to unfulfilled and the\n * order's status is recomputed from what remains — `fulfilled` can step back\n * to `partially_fulfilled` or `paid`.\n *\n * That step back is NOT an `ORDER_TRANSITIONS` edge, on purpose: the table\n * says what may happen to an order going forward, and admitting\n * `fulfilled → paid` there would open it to every other caller. This is the one writer\n * that may take it, and only from a status whose fulfillments are still open\n * (`orderFulfillmentsEditable`): a delivered, refunded or cancelled order's\n * shipments are what happened and stay that way.\n */\nexport async function cancelOrderFulfillment(request: {\n hostId: string\n orderId: string\n fulfillmentId: string\n}): Promise<FulfillmentChangeOutcome> {\n const orderRef = orderRefFor(request.hostId, request.orderId)\n return firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction): Promise<FulfillmentChangeOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const index = (order.fulfillments ?? []).findIndex((entry) => entry.id === request.fulfillmentId)\n if (index < 0) return { outcome: 'no_such_fulfillment' }\n const current = (order.fulfillments ?? [])[index]\n if (!CommerceModel.fulfillmentIsActive(current)) return { outcome: 'already', fulfillment: current }\n if (!CommerceModel.orderFulfillmentsEditable(order)) {\n return { outcome: 'locked', from: order.status }\n }\n const atMs = Date.now()\n const next: CommerceModel.OrderFulfillment = {\n ...current,\n status: 'cancelled',\n cancelledAtMs: atMs,\n updatedAtMs: atMs,\n }\n const fulfillments = [...(order.fulfillments ?? [])]\n fulfillments[index] = next\n const status = CommerceModel.statusFromFulfillments({ lineItems: order.lineItems, fulfillments })\n transaction.update(orderRef, {\n status,\n fulfillments,\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'fulfillment-cancelled',\n CommerceModel.describeFulfillmentLines(order, CommerceModel.fulfillmentLineQuantities(order, current)) +\n ' back to unfulfilled',\n atMs,\n ),\n })\n return { outcome: 'updated', status, fulfillment: next }\n })\n}\n\n/**\n * Fulfil and mark-delivered, with the transition re-asked under the write\n * (AGL-1819) — the last two order-status writes the console made with a client\n * `updateDoc`, moved behind the same shape as `cancel-order.ts`.\n *\n * THE STALE-DIALOG HOLE IS THE SAME ONE AGL-1818 CLOSED FOR CANCEL.\n * `canTransitionOrder` was consulted only to RENDER the buttons, so a dialog\n * opened on a `paid` order that was refunded (or cancelled) in another tab\n * could still write `fulfilled` straight onto it — `ORDER_TRANSITIONS` stayed\n * advisory because nothing consulted it on the way IN. Re-reading the order\n * inside the transaction that writes the status is what turns the table into\n * a guarantee; the race comes back as a 409 naming the state that refused.\n *\n * ONE ROUTE FOR THE TWO FULFILMENT-SIDE TRANSITIONS, and ONLY those. They are\n * the same act — a forward status flip plus a timeline entry, no stock moved,\n * no money moved — so they share a handler parameterized on `to`. It refuses\n * every other target with a 400 on purpose: `cancelled` releases stock under\n * its own transaction (`cancel-order.ts`) and `refunded` moves money\n * (`refund.ts`), so admitting them here would hand callers a door around\n * exactly the specifics those routes exist to enforce.\n *\n * NO STOCK MOVES HERE. The sale decremented inventory when the money landed\n * (see `cancel-order.ts`'s header); shipping the goods changes what is on the\n * shelf not at all. An open `restockCheck` — a partial refund's question,\n * flagged before the merchant ships the rest — survives untouched: the write\n * is an `update()` naming only status, fulfillments and timeline, and the\n * question is still the merchant's to answer.\n *\n * NO MANAGER NOTIFICATION, matching the client write this replaces: the\n * merchant fulfilled the order themselves, and `notifyHostManagers` would\n * tell them what they just did. (`supplier-update.ts` notifies because there\n * the SUPPLIER acted and the merchant is the one who needs to hear it.) The\n * BUYER is told, through `recordOrderShipment` (AGL-3610).\n *\n * ONCE. A retried request — a lost response, a second click — finds the order\n * already in the target status and returns success WITHOUT writing: appending\n * a second copy of the same fulfillment is the double this guard exists to\n * stop. It reports `already: true` so the console can say so. A fulfil retried\n * after the order moved on to `delivered` in another tab is a 409 instead,\n * which is the truth: that state cannot be fulfilled (again).\n *\n * THE SERVER'S ORDER, NOT THE DIALOG'S: `lineItemIds` and the timeline append\n * are computed from the transaction's own read, so a note landed from another\n * tab survives — the client write it replaces appended to the dialog's stale\n * copy and dropped it.\n *\n * Role gate: `admin` and `editor`, the role the client write already had\n * under the rules' host catch-all — same reasoning as `cancel-order.ts`.\n */\nexport const fulfillOrderHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const orderId = String(body.orderId ?? '')\n if (!hostId || !orderId) {\n return res.status(400).json({ error: 'Missing hostId or orderId' })\n }\n // Firestore reserves `__…__` ids and `.doc()` throws SYNCHRONOUSLY on one,\n // which would surface as a 500 on a caller's typo rather than the 400 it is.\n if (/^__.*__$/.test(hostId) || /^__.*__$/.test(orderId)) {\n return res.status(400).json({ error: 'Missing hostId or orderId' })\n }\n // Two more acts on one fulfillment (AGL-3611): correct its tracking, or\n // cancel it. Same route, same role gate; neither moves stock or money.\n const action = String(body.action ?? '')\n const fulfillmentId = String(body.fulfillmentId ?? '').slice(0, 100)\n if ((action === 'update-tracking' || action === 'cancel-fulfillment') && !fulfillmentId) {\n return res.status(400).json({ error: 'Missing fulfillmentId' })\n }\n const to = String(body.to ?? '') as OrderFulfilmentTarget\n if (!action && to !== 'fulfilled' && to !== 'delivered') {\n // Deliberately NOT a generic transition endpoint — see the header.\n return res.status(400).json({ error: 'to must be fulfilled or delivered' })\n }\n if (action && action !== 'update-tracking' && action !== 'cancel-fulfillment') {\n return res.status(400).json({ error: 'Unknown action' })\n }\n const { carrier, trackingNumber } = boundTracking(body)\n const lineItems = Array.isArray(body.lineItems)\n ? (body.lineItems as unknown[]).slice(0, 500).map((entry) => ({\n lineItemId: Number((entry as Record<string, unknown>)?.lineItemId),\n quantity: Number((entry as Record<string, unknown>)?.quantity),\n }))\n : undefined\n const idempotencyKey = String(req.headers['idempotency-key'] ?? body.idempotencyKey ?? '').slice(0, 200)\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not permitted' })\n }\n\n if (action) {\n const change =\n action === 'update-tracking'\n ? await updateFulfillmentTracking({\n hostId,\n orderId,\n fulfillmentId,\n carrier,\n trackingNumber,\n trackingUrl: body.trackingUrl,\n })\n : await cancelOrderFulfillment({ hostId, orderId, fulfillmentId })\n // A corrected tracking number is a parcel to follow (AGL-3612).\n if (change.outcome === 'updated' && action === 'update-tracking' && change.fulfillment.trackingNumber) {\n await announcePluginShipment({\n hostId,\n recordId: orderId,\n shipmentId: change.fulfillment.id,\n ...(change.fulfillment.carrier ? { carrier: change.fulfillment.carrier } : {}),\n trackingNumber: change.fulfillment.trackingNumber,\n })\n }\n if (change.outcome === 'no_such_order') return res.status(404).json({ error: 'Unknown order' })\n if (change.outcome === 'no_such_fulfillment') {\n return res.status(404).json({ error: 'That shipment is no longer on this order' })\n }\n if (change.outcome === 'locked') {\n return res.status(409).json({\n error:\n change.from === 'cancelled'\n ? 'That shipment was canceled and can no longer be edited'\n : `Shipments on an order in \"${change.from}\" can no longer be changed`,\n })\n }\n return res.status(200).json({\n ok: true,\n ...(change.outcome === 'already' ? { already: true } : { status: change.status }),\n fulfillment: change.fulfillment,\n })\n }\n\n // Authorized above; the shipment itself is the shared transaction. The\n // console dialog branches on `ok`, `already` and the 409's `error`\n // string, and this route's contract is with that dialog, not with the\n // registry's vocabulary.\n const outcome = await recordOrderShipment({\n hostId,\n orderId,\n to,\n carrier,\n trackingNumber,\n ...(lineItems ? { lineItems } : {}),\n ...(body.trackingUrl ? { trackingUrl: String(body.trackingUrl) } : {}),\n ...(body.labelUrl ? { labelUrl: String(body.labelUrl) } : {}),\n ...(body.notify === false ? { notify: false } : {}),\n ...(idempotencyKey ? { idempotencyKey } : {}),\n })\n if (outcome.outcome === 'no_such_order') {\n return res.status(404).json({ error: 'Unknown order' })\n }\n if (outcome.outcome === 'invalid_lines') {\n return res\n .status(outcome.problem.problem === 'over_fulfilled' ? 409 : 400)\n .json({ error: outcome.message })\n }\n if (outcome.outcome === 'blocked') {\n return res.status(409).json({\n error:\n to === 'delivered'\n ? `Orders in \"${outcome.from}\" cannot be marked delivered`\n : `Orders in \"${outcome.from}\" cannot be fulfilled`,\n })\n }\n if (outcome.outcome === 'already') {\n return res.status(200).json({\n ok: true,\n already: true,\n ...(outcome.fulfillment ? { fulfillment: outcome.fulfillment } : {}),\n })\n }\n return res.status(200).json({\n ok: true,\n status: outcome.status,\n ...(outcome.fulfillment ? { fulfillment: outcome.fulfillment } : {}),\n })\n } catch (error) {\n // Nothing landed — the transaction is all-or-nothing — so the order is\n // still where it was and the merchant can try again.\n console.error('fulfillOrder failed', error)\n return res.status(500).json({\n error: to === 'delivered' ? 'Mark delivered failed' : 'Fulfill failed',\n })\n }\n}\n"],"names":["CommerceModel","announcePluginShipment","createResourceUid","firebaseAdmin","notifyOrderBuyer","ORDER_DELIVERED_EVENT","ORDER_FULFILLED_EVENT","fulfillmentEventView","stageOrderEvent","createHash","CARRIER_MAX","TRACKING_NUMBER_MAX","URL_MAX","boundTracking","input","carrier","String","trim","slice","trackingNumber","safeHttpsUrl","value","text","length","undefined","url","URL","protocol","toString","fulfillmentIdForKey","orderId","key","update","digest","trackingFields","explicitUrl","trackingUrl","trackingUrlFor","shipmentDetail","order","lines","partial","tracking","what","describeFulfillmentLines","orderRefFor","hostId","app","firestore","collection","doc","recordOrderShipment","request","result","to","labelUrl","orderRef","keyedId","idempotencyKey","runTransaction","transaction","snapshot","get","exists","outcome","liftLegacyOrder","data","atMs","Date","now","status","canTransitionOrder","from","delivered","updatedAtMs","timeline","appendOrderEvent","existing","fulfillments","find","entry","id","fulfillment","onceByTracking","same","fulfillmentWithTracking","named","lineItems","resolved","resolveFulfillmentLines","problem","message","describeFulfillmentLinesProblem","remainingFulfillmentLines","orderLineFulfillmentStates","filter","state","remainingQuantity","map","lineItemId","quantity","lineItemIds","labelRef","notify","statusFromFulfillments","shipped","extra","recordId","shipmentId","fulfillmentId","updateFulfillmentTracking","current","next","index","findIndex","fulfillmentIsActive","includes","Object","assign","cancelOrderFulfillment","orderFulfillmentsEditable","cancelledAtMs","fulfillmentLineQuantities","fulfillOrderHandler","req","res","body","method","json","error","authorization","headers","idToken","startsWith","JSON","parse","test","action","Array","isArray","Number","hostSnapshot","decoded","auth","verifyIdToken","hostRef","memberRole","uid","change","ok","already","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/fulfill-order.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as CommerceModel from '../model'\nimport { announcePluginShipment } from '@aglyn/aglyn/plugin-manager/plugin-shipment-records'\nimport { createResourceUid, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { notifyOrderBuyer } from './order-notifications'\nimport { ORDER_DELIVERED_EVENT, ORDER_FULFILLED_EVENT } from '../model/order-events'\nimport { fulfillmentEventView, stageOrderEvent } from './order-events'\nimport { createHash } from 'crypto'\n\n/**\n * The two fulfilment-side transitions, and ONLY those.\n *\n * `cancelled` releases stock under its own transaction and `refunded` moves\n * money under another, so neither belongs to a write whose contract is \"a\n * forward status flip plus a timeline entry, no stock moved, no money moved\".\n * Widening this union is how a caller gets a door around the specifics those\n * routes exist to enforce — the type is the door being shut.\n */\nexport type OrderFulfilmentTarget = 'fulfilled' | 'delivered'\n\n/** How long a carrier name and a tracking number may be, on every door. */\nexport const CARRIER_MAX = 40\nexport const TRACKING_NUMBER_MAX = 60\nconst URL_MAX = 2048\n\nexport interface RecordShipmentRequest {\n /** Site that owns the order. The CALLER has already proven ownership. */\n hostId: string\n orderId: string\n to: OrderFulfilmentTarget\n /** Free text, e.g. `'UPS'`. Bounded by the caller. */\n carrier?: string\n trackingNumber?: string\n /**\n * Which units this shipment covers (AGL-3611). Absent means everything\n * still to ship, which is what a fulfil meant before quantities existed.\n * Ignored for `delivered`.\n */\n lineItems?: ReadonlyArray<{ lineItemId: number; quantity: number }>\n /** An explicit tracking link; without one it is derived from the carrier. */\n trackingUrl?: string\n /** A label a shipping plugin bought for this parcel. */\n labelUrl?: string\n /** That plugin's id for the label (AGL-3612), kept on the fulfillment. */\n labelRef?: string\n /** What that label cost, in integer cents of the order's currency (AGL-3693). */\n labelCostCents?: number\n /**\n * What the merchant says this shipment cost to send, typed by hand in the\n * fulfill dialog (AGL-3705): integer cents of the store's currency, 0 for\n * free shipping. Stored in the SAME field a label's cost is\n * (`labelCostCents`), so it rides `order.fulfilled` to a marketplace that\n * pays shipping back. A label's own cost wins when both are given. Absent\n * means not known — nothing is stored. The caller validates\n * (`shippingCostCentsProblem`); an invalid value here is dropped.\n */\n shippingCostCents?: number\n /** Whether the buyer is to be told; recorded on the fulfillment. Default on. */\n notify?: boolean\n /**\n * One shipment attempt. A retry carrying the same key finds the fulfillment\n * it already wrote and answers `already`, so a partial shipment, which\n * leaves the order open to more, still cannot be recorded twice.\n */\n idempotencyKey?: string\n /**\n * A shipment whose tracking number an active fulfillment of this order\n * already carries is the same parcel, answered `already` (AGL-3613). For\n * the doors a shipping tool posts to — ShipStation's ShipNotice, a tracking\n * file — where the number is the parcel's identity and the same parcel may\n * have been recorded by hand first.\n */\n onceByTracking?: boolean\n}\n\n/**\n * What happened, as a domain fact rather than an HTTP status — each caller\n * maps it into its own error vocabulary (the console route into its JSON\n * shape, `/v1` into the published error envelope), because those two\n * vocabularies are contracts with different audiences and neither may leak\n * into the other.\n *\n * `already` is a SUCCESS and is distinct from `recorded` on purpose: a retried\n * request finds the order in the target status (or its keyed fulfillment\n * already written) and returns without writing, so a lost response can never\n * append a second copy of the same shipment.\n *\n * `recorded` carries the fulfillment it wrote (absent for `delivered`) and the\n * status the order is now in, so a caller can tell the buyer what shipped.\n */\nexport type RecordShipmentOutcome =\n | {\n outcome: 'recorded'\n status: CommerceModel.OrderStatus\n fulfillment?: CommerceModel.OrderFulfillment\n }\n | { outcome: 'already'; fulfillment?: CommerceModel.OrderFulfillment }\n | { outcome: 'no_such_order' }\n /** The transition rule refused. `from` is the status that refused it. */\n | { outcome: 'blocked'; from: string }\n /** The requested lines or quantities were refused; nothing was written. */\n | { outcome: 'invalid_lines'; problem: CommerceModel.FulfillmentLinesProblem; message: string }\n\n/** Trims a carrier and a tracking number to what every door stores. */\nexport function boundTracking(input: { carrier?: unknown; trackingNumber?: unknown }): {\n carrier: string\n trackingNumber: string\n} {\n return {\n carrier: String(input.carrier ?? '').trim().slice(0, CARRIER_MAX),\n trackingNumber: String(input.trackingNumber ?? '').trim().slice(0, TRACKING_NUMBER_MAX),\n }\n}\n\n/** An https link or nothing: a stored link is rendered to buyers as an anchor. */\nexport function safeHttpsUrl(value: unknown): string | undefined {\n const text = String(value ?? '').trim()\n if (!text || text.length > URL_MAX) return undefined\n try {\n const url = new URL(text)\n return url.protocol === 'https:' ? url.toString() : undefined\n } catch {\n return undefined\n }\n}\n\n/** The fulfillment id a keyed attempt writes, so a retry finds it. */\nexport function fulfillmentIdForKey(orderId: string, key: string): string {\n return `f-${createHash('sha256').update(`${orderId}:${key}`).digest('hex').slice(0, 20)}`\n}\n\n/** The tracking keys a fulfillment stores: the number, the carrier and the link. */\nfunction trackingFields(\n carrier: string,\n trackingNumber: string,\n explicitUrl: string | undefined,\n): Pick<CommerceModel.OrderFulfillment, 'carrier' | 'trackingNumber' | 'trackingUrl'> {\n const trackingUrl =\n explicitUrl ?? (trackingNumber ? CommerceModel.trackingUrlFor(carrier, trackingNumber) ?? undefined : undefined)\n return {\n ...(carrier ? { carrier } : {}),\n ...(trackingNumber ? { trackingNumber } : {}),\n ...(trackingUrl ? { trackingUrl } : {}),\n }\n}\n\n/** The timeline's words for a shipment. */\nfunction shipmentDetail(\n order: CommerceModel.HostOrder,\n lines: readonly CommerceModel.OrderFulfillmentLine[],\n partial: boolean,\n carrier: string,\n trackingNumber: string,\n): string {\n const tracking = trackingNumber ? `${carrier || 'Shipped'} ${trackingNumber}` : ''\n if (!partial) return tracking || 'Fulfilled'\n const what = CommerceModel.describeFulfillmentLines(order, lines)\n return tracking ? `${what} — ${tracking}` : `${what} shipped`\n}\n\nfunction orderRefFor(hostId: string, orderId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('orders')\n .doc(orderId)\n}\n\n/**\n * Record a shipment — the transaction, with NO authorization of any kind\n * (AGL-2461).\n *\n * This is the whole of what `fulfillOrderHandler` used to do below its auth\n * preamble, lifted out unchanged so a SECOND caller can have it: the customer\n * REST API's `PATCH /v1/sites/{id}/orders/{id}`\n * (`api-v1/orders-and-products.ts`), which authenticates an org API key and\n * so shares not one line of this function's former preamble.\n *\n * Lifted rather than copied, and that is the entire point: a second\n * `ORDER_TRANSITIONS` inside `/v1` would be two tables that drift into the\n * API writing a status the console forbids.\n *\n * **The caller authorizes.** Everything this function knows is the transition\n * rule. It does not know who is asking, and it will happily move any order on\n * any host — `hostId` is taken on trust because its two callers establish\n * trust in ways that have nothing in common (a Firebase uid in `memberRoles`\n * for the console; an org credential that owns the host for `/v1`).\n *\n * QUANTITIES (AGL-3611). A shipment names units, not just lines, and the\n * remaining count is computed from the transaction's own read, so two admins\n * shipping the last 3 mugs at once cannot ship 6: the second meets\n * `invalid_lines` with `over_fulfilled`. The resulting status is computed\n * from the fulfillments (`statusFromFulfillments`) — `partially_fulfilled`\n * until every shippable unit is covered — and the transition rule is re-asked\n * only when the status actually moves, since a second partial shipment on a\n * `partially_fulfilled` order changes nothing the table speaks to.\n *\n * Every guarantee AGL-1819 built is inside the transaction and therefore\n * inherited by both callers: `canTransitionOrder` is re-asked under the write\n * so a stale caller cannot skip a state, the already-in-target case returns\n * without writing so a retry cannot append a duplicate shipment, and the\n * lines and the timeline are computed from the transaction's OWN read so a\n * note landed from another tab survives.\n */\nexport async function recordOrderShipment(\n request: RecordShipmentRequest,\n): Promise<RecordShipmentOutcome> {\n const { hostId, orderId, to } = request\n const { carrier, trackingNumber } = boundTracking(request)\n const explicitUrl = safeHttpsUrl(request.trackingUrl)\n const labelUrl = safeHttpsUrl(request.labelUrl)\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = orderRefFor(hostId, orderId)\n const keyedId = request.idempotencyKey\n ? fulfillmentIdForKey(orderId, String(request.idempotencyKey).slice(0, 200))\n : null\n\n const result = await firestore.runTransaction(\n async (transaction): Promise<RecordShipmentOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder(\n (snapshot.data() ?? {}) as never,\n )\n const atMs = Date.now()\n\n if (to === 'delivered') {\n if (order.status === 'delivered') return { outcome: 'already' }\n if (!CommerceModel.canTransitionOrder(order.status, 'delivered')) {\n return { outcome: 'blocked', from: order.status }\n }\n const delivered = {\n status: 'delivered',\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(order, 'delivered', undefined, atMs),\n }\n transaction.update(orderRef, delivered)\n stageOrderEvent(transaction, ORDER_DELIVERED_EVENT, { hostId, orderId, key: 'delivered', order: { ...snapshot.data(), ...delivered } })\n return { outcome: 'recorded', status: 'delivered' }\n }\n\n // A redelivered keyed attempt: the fulfillment it wrote is there.\n if (keyedId) {\n const existing = (order.fulfillments ?? []).find((entry) => entry.id === keyedId)\n if (existing) return { outcome: 'already', fulfillment: existing }\n }\n if (request.onceByTracking && trackingNumber) {\n const same = CommerceModel.fulfillmentWithTracking(order, trackingNumber)\n if (same) return { outcome: 'already', fulfillment: same }\n }\n const named = (request.lineItems ?? []).length > 0\n // A redelivered click, or the second of two admins, on a \"fulfil\n // everything\". Success because it IS the state the caller asked for —\n // but nothing is written, so a retry cannot append a second copy.\n if (!named && order.status === 'fulfilled') return { outcome: 'already' }\n if (\n order.status !== 'partially_fulfilled' &&\n order.status !== 'fulfilled' &&\n !CommerceModel.canTransitionOrder(order.status, 'partially_fulfilled') &&\n !CommerceModel.canTransitionOrder(order.status, 'fulfilled')\n ) {\n return { outcome: 'blocked', from: order.status }\n }\n\n let lines: CommerceModel.OrderFulfillmentLine[]\n if (named) {\n const resolved = CommerceModel.resolveFulfillmentLines(order, request.lineItems ?? [])\n if (!('lines' in resolved)) {\n return {\n outcome: 'invalid_lines',\n problem: resolved,\n message: CommerceModel.describeFulfillmentLinesProblem(resolved),\n }\n }\n lines = resolved.lines\n } else {\n lines = CommerceModel.remainingFulfillmentLines(order)\n // Nothing physical left: a fulfil closes the digital and service\n // lines instead, which is what it did before quantities existed.\n if (lines.length === 0) {\n lines = CommerceModel.orderLineFulfillmentStates(order)\n .filter((state) => state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n }\n }\n\n const fulfillment: CommerceModel.OrderFulfillment = {\n id: keyedId ?? createResourceUid(),\n lineItemIds: lines.map((entry) => entry.lineItemId),\n lines,\n ...trackingFields(carrier, trackingNumber, explicitUrl),\n ...(labelUrl ? { labelUrl } : {}),\n ...(request.labelRef ? { labelRef: String(request.labelRef).slice(0, 80) } : {}),\n ...(Number.isSafeInteger(request.labelCostCents) && Number(request.labelCostCents) > 0\n ? { labelCostCents: Number(request.labelCostCents) }\n : request.shippingCostCents !== undefined &&\n CommerceModel.shippingCostCentsProblem(request.shippingCostCents) === null\n ? { labelCostCents: request.shippingCostCents }\n : {}),\n status: 'active',\n notify: request.notify !== false,\n atMs,\n }\n const fulfillments = [...(order.fulfillments ?? []), fulfillment]\n let status: CommerceModel.OrderStatus = CommerceModel.statusFromFulfillments({\n lineItems: order.lineItems,\n fulfillments,\n })\n // A fulfil with nothing left to cover still closes the order.\n if (status === 'paid') status = 'fulfilled'\n if (status !== order.status && !CommerceModel.canTransitionOrder(order.status, status)) {\n return { outcome: 'blocked', from: order.status }\n }\n const shipped = {\n status,\n fulfillments,\n // What a shipping tool's feed asks (AGL-3613): which orders changed.\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n status,\n shipmentDetail(order, lines, status === 'partially_fulfilled', carrier, trackingNumber),\n atMs,\n ),\n }\n transaction.update(orderRef, shipped)\n // Once per shipment (AGL-3611), in the same write, so the event exists\n // exactly when the shipment does.\n stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, { hostId, orderId, key: fulfillment.id, order: { ...snapshot.data(), ...shipped }, extra: { fulfillment: fulfillmentEventView(order, fulfillment) } })\n return { outcome: 'recorded', status, fulfillment }\n },\n )\n // The BUYER hears about it (AGL-3610): shipped with the tracking link, or\n // delivered. Once per shipment, after the write, and never fails the\n // shipment — see `order-notifications.ts`. A retry returns `already` above\n // and never reaches here. A merchant who unticked \"Notify customer\"\n // (AGL-3611) recorded the shipment with `notify: false`, and nobody is told.\n // Every shipment recorded, by any door, is announced (AGL-3612), so a\n // plugin that follows parcels can follow one typed in by hand too.\n if (result.outcome === 'recorded' && result.fulfillment?.trackingNumber) {\n await announcePluginShipment({\n hostId,\n recordId: orderId,\n shipmentId: result.fulfillment.id,\n ...(result.fulfillment.carrier ? { carrier: result.fulfillment.carrier } : {}),\n trackingNumber: result.fulfillment.trackingNumber,\n ...(request.labelRef ? { labelRef: request.labelRef } : {}),\n })\n }\n if (result.outcome === 'recorded' && result.fulfillment?.notify !== false) {\n await notifyOrderBuyer(\n { hostId, orderId },\n to === 'fulfilled' ? 'shipped' : 'delivered',\n result.fulfillment ? { fulfillmentId: result.fulfillment.id } : {},\n )\n }\n return result\n}\n\n/** What an edit or a cancel of one fulfillment came to. */\nexport type FulfillmentChangeOutcome =\n | { outcome: 'updated'; status: CommerceModel.OrderStatus; fulfillment: CommerceModel.OrderFulfillment }\n | { outcome: 'already'; fulfillment: CommerceModel.OrderFulfillment }\n | { outcome: 'no_such_order' }\n | { outcome: 'no_such_fulfillment' }\n /** The order is history (delivered, refunded, cancelled); `from` names it. */\n | { outcome: 'locked'; from: string }\n\n/**\n * Corrects the tracking on one fulfillment (AGL-3611): a mistyped number, a\n * carrier swapped, a label bought after the parcel was recorded. Moves no\n * status and no stock. A cancelled fulfillment is history and is refused.\n */\nexport async function updateFulfillmentTracking(request: {\n hostId: string\n orderId: string\n fulfillmentId: string\n carrier?: string\n trackingNumber?: string\n trackingUrl?: string\n}): Promise<FulfillmentChangeOutcome> {\n const { carrier, trackingNumber } = boundTracking(request)\n const explicitUrl = safeHttpsUrl(request.trackingUrl)\n const orderRef = orderRefFor(request.hostId, request.orderId)\n return firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction): Promise<FulfillmentChangeOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const index = (order.fulfillments ?? []).findIndex((entry) => entry.id === request.fulfillmentId)\n if (index < 0) return { outcome: 'no_such_fulfillment' }\n const current = (order.fulfillments ?? [])[index]\n if (!CommerceModel.fulfillmentIsActive(current) || !['paid', 'partially_fulfilled', 'fulfilled', 'delivered'].includes(order.status)) {\n return { outcome: 'locked', from: CommerceModel.fulfillmentIsActive(current) ? order.status : 'cancelled' }\n }\n const atMs = Date.now()\n const next: CommerceModel.OrderFulfillment = { ...current, updatedAtMs: atMs }\n delete next.carrier\n delete next.trackingNumber\n delete next.trackingUrl\n Object.assign(next, trackingFields(carrier, trackingNumber, explicitUrl))\n if (\n (current.carrier ?? '') === (next.carrier ?? '') &&\n (current.trackingNumber ?? '') === (next.trackingNumber ?? '') &&\n (current.trackingUrl ?? '') === (next.trackingUrl ?? '')\n ) {\n return { outcome: 'already', fulfillment: current }\n }\n const fulfillments = [...(order.fulfillments ?? [])]\n fulfillments[index] = next\n transaction.update(orderRef, {\n fulfillments,\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'tracking-updated',\n trackingNumber ? `${carrier || 'Tracking'} ${trackingNumber}` : 'Tracking removed',\n atMs,\n ),\n })\n return { outcome: 'updated', status: order.status, fulfillment: next }\n })\n}\n\n/**\n * Cancels one fulfillment (AGL-3611): the parcel was never handed over, or was\n * recorded against the wrong items. Its units go back to unfulfilled and the\n * order's status is recomputed from what remains — `fulfilled` can step back\n * to `partially_fulfilled` or `paid`.\n *\n * That step back is NOT an `ORDER_TRANSITIONS` edge, on purpose: the table\n * says what may happen to an order going forward, and admitting\n * `fulfilled → paid` there would open it to every other caller. This is the one writer\n * that may take it, and only from a status whose fulfillments are still open\n * (`orderFulfillmentsEditable`): a delivered, refunded or cancelled order's\n * shipments are what happened and stay that way.\n */\nexport async function cancelOrderFulfillment(request: {\n hostId: string\n orderId: string\n fulfillmentId: string\n}): Promise<FulfillmentChangeOutcome> {\n const orderRef = orderRefFor(request.hostId, request.orderId)\n return firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction): Promise<FulfillmentChangeOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const index = (order.fulfillments ?? []).findIndex((entry) => entry.id === request.fulfillmentId)\n if (index < 0) return { outcome: 'no_such_fulfillment' }\n const current = (order.fulfillments ?? [])[index]\n if (!CommerceModel.fulfillmentIsActive(current)) return { outcome: 'already', fulfillment: current }\n if (!CommerceModel.orderFulfillmentsEditable(order)) {\n return { outcome: 'locked', from: order.status }\n }\n const atMs = Date.now()\n const next: CommerceModel.OrderFulfillment = {\n ...current,\n status: 'cancelled',\n cancelledAtMs: atMs,\n updatedAtMs: atMs,\n }\n const fulfillments = [...(order.fulfillments ?? [])]\n fulfillments[index] = next\n const status = CommerceModel.statusFromFulfillments({ lineItems: order.lineItems, fulfillments })\n transaction.update(orderRef, {\n status,\n fulfillments,\n updatedAtMs: atMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'fulfillment-cancelled',\n CommerceModel.describeFulfillmentLines(order, CommerceModel.fulfillmentLineQuantities(order, current)) +\n ' back to unfulfilled',\n atMs,\n ),\n })\n return { outcome: 'updated', status, fulfillment: next }\n })\n}\n\n/**\n * Fulfil and mark-delivered, with the transition re-asked under the write\n * (AGL-1819) — the last two order-status writes the console made with a client\n * `updateDoc`, moved behind the same shape as `cancel-order.ts`.\n *\n * THE STALE-DIALOG HOLE IS THE SAME ONE AGL-1818 CLOSED FOR CANCEL.\n * `canTransitionOrder` was consulted only to RENDER the buttons, so a dialog\n * opened on a `paid` order that was refunded (or cancelled) in another tab\n * could still write `fulfilled` straight onto it — `ORDER_TRANSITIONS` stayed\n * advisory because nothing consulted it on the way IN. Re-reading the order\n * inside the transaction that writes the status is what turns the table into\n * a guarantee; the race comes back as a 409 naming the state that refused.\n *\n * ONE ROUTE FOR THE TWO FULFILMENT-SIDE TRANSITIONS, and ONLY those. They are\n * the same act — a forward status flip plus a timeline entry, no stock moved,\n * no money moved — so they share a handler parameterized on `to`. It refuses\n * every other target with a 400 on purpose: `cancelled` releases stock under\n * its own transaction (`cancel-order.ts`) and `refunded` moves money\n * (`refund.ts`), so admitting them here would hand callers a door around\n * exactly the specifics those routes exist to enforce.\n *\n * NO STOCK MOVES HERE. The sale decremented inventory when the money landed\n * (see `cancel-order.ts`'s header); shipping the goods changes what is on the\n * shelf not at all. An open `restockCheck` — a partial refund's question,\n * flagged before the merchant ships the rest — survives untouched: the write\n * is an `update()` naming only status, fulfillments and timeline, and the\n * question is still the merchant's to answer.\n *\n * NO MANAGER NOTIFICATION, matching the client write this replaces: the\n * merchant fulfilled the order themselves, and `notifyHostManagers` would\n * tell them what they just did. (`supplier-update.ts` notifies because there\n * the SUPPLIER acted and the merchant is the one who needs to hear it.) The\n * BUYER is told, through `recordOrderShipment` (AGL-3610).\n *\n * ONCE. A retried request — a lost response, a second click — finds the order\n * already in the target status and returns success WITHOUT writing: appending\n * a second copy of the same fulfillment is the double this guard exists to\n * stop. It reports `already: true` so the console can say so. A fulfil retried\n * after the order moved on to `delivered` in another tab is a 409 instead,\n * which is the truth: that state cannot be fulfilled (again).\n *\n * THE SERVER'S ORDER, NOT THE DIALOG'S: `lineItemIds` and the timeline append\n * are computed from the transaction's own read, so a note landed from another\n * tab survives — the client write it replaces appended to the dialog's stale\n * copy and dropped it.\n *\n * Role gate: `admin` and `editor`, the role the client write already had\n * under the rules' host catch-all — same reasoning as `cancel-order.ts`.\n */\nexport const fulfillOrderHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const orderId = String(body.orderId ?? '')\n if (!hostId || !orderId) {\n return res.status(400).json({ error: 'Missing hostId or orderId' })\n }\n // Firestore reserves `__…__` ids and `.doc()` throws SYNCHRONOUSLY on one,\n // which would surface as a 500 on a caller's typo rather than the 400 it is.\n if (/^__.*__$/.test(hostId) || /^__.*__$/.test(orderId)) {\n return res.status(400).json({ error: 'Missing hostId or orderId' })\n }\n // Two more acts on one fulfillment (AGL-3611): correct its tracking, or\n // cancel it. Same route, same role gate; neither moves stock or money.\n const action = String(body.action ?? '')\n const fulfillmentId = String(body.fulfillmentId ?? '').slice(0, 100)\n if ((action === 'update-tracking' || action === 'cancel-fulfillment') && !fulfillmentId) {\n return res.status(400).json({ error: 'Missing fulfillmentId' })\n }\n const to = String(body.to ?? '') as OrderFulfilmentTarget\n if (!action && to !== 'fulfilled' && to !== 'delivered') {\n // Deliberately NOT a generic transition endpoint — see the header.\n return res.status(400).json({ error: 'to must be fulfilled or delivered' })\n }\n if (action && action !== 'update-tracking' && action !== 'cancel-fulfillment') {\n return res.status(400).json({ error: 'Unknown action' })\n }\n const { carrier, trackingNumber } = boundTracking(body)\n const lineItems = Array.isArray(body.lineItems)\n ? (body.lineItems as unknown[]).slice(0, 500).map((entry) => ({\n lineItemId: Number((entry as Record<string, unknown>)?.lineItemId),\n quantity: Number((entry as Record<string, unknown>)?.quantity),\n }))\n : undefined\n const idempotencyKey = String(req.headers['idempotency-key'] ?? body.idempotencyKey ?? '').slice(0, 200)\n // A hand-entered shipping cost (AGL-3705): whole cents, or absent. Refused\n // before anything is read, so a typo never half-records a shipment.\n const shippingCostCents = action || to !== 'fulfilled' ? undefined : body.shippingCostCents\n const shippingCostProblem = CommerceModel.shippingCostCentsProblem(shippingCostCents)\n if (shippingCostProblem) return res.status(400).json({ error: shippingCostProblem })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not permitted' })\n }\n\n if (action) {\n const change =\n action === 'update-tracking'\n ? await updateFulfillmentTracking({\n hostId,\n orderId,\n fulfillmentId,\n carrier,\n trackingNumber,\n trackingUrl: body.trackingUrl,\n })\n : await cancelOrderFulfillment({ hostId, orderId, fulfillmentId })\n // A corrected tracking number is a parcel to follow (AGL-3612).\n if (change.outcome === 'updated' && action === 'update-tracking' && change.fulfillment.trackingNumber) {\n await announcePluginShipment({\n hostId,\n recordId: orderId,\n shipmentId: change.fulfillment.id,\n ...(change.fulfillment.carrier ? { carrier: change.fulfillment.carrier } : {}),\n trackingNumber: change.fulfillment.trackingNumber,\n })\n }\n if (change.outcome === 'no_such_order') return res.status(404).json({ error: 'Unknown order' })\n if (change.outcome === 'no_such_fulfillment') {\n return res.status(404).json({ error: 'That shipment is no longer on this order' })\n }\n if (change.outcome === 'locked') {\n return res.status(409).json({\n error:\n change.from === 'cancelled'\n ? 'That shipment was canceled and can no longer be edited'\n : `Shipments on an order in \"${change.from}\" can no longer be changed`,\n })\n }\n return res.status(200).json({\n ok: true,\n ...(change.outcome === 'already' ? { already: true } : { status: change.status }),\n fulfillment: change.fulfillment,\n })\n }\n\n // Authorized above; the shipment itself is the shared transaction. The\n // console dialog branches on `ok`, `already` and the 409's `error`\n // string, and this route's contract is with that dialog, not with the\n // registry's vocabulary.\n const outcome = await recordOrderShipment({\n hostId,\n orderId,\n to,\n carrier,\n trackingNumber,\n ...(lineItems ? { lineItems } : {}),\n ...(body.trackingUrl ? { trackingUrl: String(body.trackingUrl) } : {}),\n ...(body.labelUrl ? { labelUrl: String(body.labelUrl) } : {}),\n ...(body.notify === false ? { notify: false } : {}),\n ...(typeof shippingCostCents === 'number' ? { shippingCostCents } : {}),\n ...(idempotencyKey ? { idempotencyKey } : {}),\n })\n if (outcome.outcome === 'no_such_order') {\n return res.status(404).json({ error: 'Unknown order' })\n }\n if (outcome.outcome === 'invalid_lines') {\n return res\n .status(outcome.problem.problem === 'over_fulfilled' ? 409 : 400)\n .json({ error: outcome.message })\n }\n if (outcome.outcome === 'blocked') {\n return res.status(409).json({\n error:\n to === 'delivered'\n ? `Orders in \"${outcome.from}\" cannot be marked delivered`\n : `Orders in \"${outcome.from}\" cannot be fulfilled`,\n })\n }\n if (outcome.outcome === 'already') {\n return res.status(200).json({\n ok: true,\n already: true,\n ...(outcome.fulfillment ? { fulfillment: outcome.fulfillment } : {}),\n })\n }\n return res.status(200).json({\n ok: true,\n status: outcome.status,\n ...(outcome.fulfillment ? { fulfillment: outcome.fulfillment } : {}),\n })\n } catch (error) {\n // Nothing landed — the transaction is all-or-nothing — so the order is\n // still where it was and the merchant can try again.\n console.error('fulfillOrder failed', error)\n return res.status(500).json({\n error: to === 'delivered' ? 'Mark delivered failed' : 'Fulfill failed',\n })\n }\n}\n"],"names":["CommerceModel","announcePluginShipment","createResourceUid","firebaseAdmin","notifyOrderBuyer","ORDER_DELIVERED_EVENT","ORDER_FULFILLED_EVENT","fulfillmentEventView","stageOrderEvent","createHash","CARRIER_MAX","TRACKING_NUMBER_MAX","URL_MAX","boundTracking","input","carrier","String","trim","slice","trackingNumber","safeHttpsUrl","value","text","length","undefined","url","URL","protocol","toString","fulfillmentIdForKey","orderId","key","update","digest","trackingFields","explicitUrl","trackingUrl","trackingUrlFor","shipmentDetail","order","lines","partial","tracking","what","describeFulfillmentLines","orderRefFor","hostId","app","firestore","collection","doc","recordOrderShipment","request","result","to","labelUrl","orderRef","keyedId","idempotencyKey","runTransaction","transaction","snapshot","get","exists","outcome","liftLegacyOrder","data","atMs","Date","now","status","canTransitionOrder","from","delivered","updatedAtMs","timeline","appendOrderEvent","existing","fulfillments","find","entry","id","fulfillment","onceByTracking","same","fulfillmentWithTracking","named","lineItems","resolved","resolveFulfillmentLines","problem","message","describeFulfillmentLinesProblem","remainingFulfillmentLines","orderLineFulfillmentStates","filter","state","remainingQuantity","map","lineItemId","quantity","lineItemIds","labelRef","Number","isSafeInteger","labelCostCents","shippingCostCents","shippingCostCentsProblem","notify","statusFromFulfillments","shipped","extra","recordId","shipmentId","fulfillmentId","updateFulfillmentTracking","current","next","index","findIndex","fulfillmentIsActive","includes","Object","assign","cancelOrderFulfillment","orderFulfillmentsEditable","cancelledAtMs","fulfillmentLineQuantities","fulfillOrderHandler","req","res","body","method","json","error","authorization","headers","idToken","startsWith","JSON","parse","test","action","Array","isArray","shippingCostProblem","hostSnapshot","decoded","auth","verifyIdToken","hostRef","memberRole","uid","change","ok","already","console"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginLocalDeliveryRecord, PluginLocalDeliveryRecords, PluginRecordCourier, PluginRecordCourierOutcome, PluginRecordCourierWrite } from '@aglyn/aglyn/plugin-manager/plugin-local-deliveries';
18
+ /** The stored run, read defensively; `null` when the order carries none. */
19
+ export declare function readOrderCourier(value: unknown): PluginRecordCourier | null;
20
+ /** One local delivery, or `null` when there is none by that id. */
21
+ export declare function readLocalDeliveryRecord(hostId: string, recordId: string): Promise<PluginLocalDeliveryRecord | null>;
22
+ /**
23
+ * Writes the run onto the order — one transaction that re-reads it, so two
24
+ * webhooks racing cannot interleave — then takes the step it brings through
25
+ * `moveLocalFulfillment`, which re-asks the transition itself. A repeat of a
26
+ * run the order already shows writes nothing, and a step the order already
27
+ * took answers `already` there, so a redelivered webhook is a no-op.
28
+ */
29
+ export declare function recordLocalDeliveryCourier(write: PluginRecordCourierWrite, nowMs?: number): Promise<PluginRecordCourierOutcome>;
30
+ export declare const commerceLocalDeliveryRecords: PluginLocalDeliveryRecords;
@@ -0,0 +1,284 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
18
+ import { normalizePhone } from "@aglyn/aglyn/foundation/definitions/contact.types";
19
+ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
20
+ import * as CommerceModel from "../model/index.js";
21
+ import { moveLocalFulfillment } from "./local-fulfillment-status.js";
22
+ import { notifyOrderBuyer } from "./order-notifications.js";
23
+ /**
24
+ * THIS PLUGIN'S LOCAL DELIVERIES, AS A COURIER SEES THEM (AGL-3695):
25
+ * commerce's implementation of core's `core.local-delivery-records`,
26
+ * registered from the server declarations so it is there wherever a courier
27
+ * is booked or a courier's webhook lands.
28
+ *
29
+ * A courier plugin never reads `hosts/{id}/orders`. It reads a drop here —
30
+ * the location it leaves from, the buyer's door, the window — and writes the
31
+ * run back here, and every step it brings goes through `moveLocalFulfillment`:
32
+ * the same transition rule, timeline, handover and buyer messages as the
33
+ * store's own driver, so "Delivered" by a courier fulfills the order exactly
34
+ * as "Delivered" from the queue does.
35
+ */ const firestore = ()=>firebaseAdmin.app().firestore();
36
+ const hostRef = (hostId)=>firestore().collection('hosts').doc(hostId);
37
+ const badId = (value)=>!value || value.includes('/') || /^__.*__$/.test(value);
38
+ /** The order statuses a courier may be sent from. */ const DISPATCHABLE_ORDER = new Set([
39
+ 'paid',
40
+ 'partially_fulfilled',
41
+ 'fulfilled'
42
+ ]);
43
+ /** The delivery statuses a courier may be sent from: not yet out, or a failed drop going out again. */ const DISPATCHABLE_DELIVERY = new Set([
44
+ 'scheduled',
45
+ 'failed'
46
+ ]);
47
+ const COURIER_STATES = new Set([
48
+ 'requested',
49
+ 'assigned',
50
+ 'at_pickup',
51
+ 'picked_up',
52
+ 'at_dropoff',
53
+ 'delivered',
54
+ 'cancelled',
55
+ 'returning',
56
+ 'returned'
57
+ ]);
58
+ const text = (value, max = 200)=>String(value != null ? value : '').trim().slice(0, max);
59
+ function place(name, address, fallbackCountry, phone, instructions) {
60
+ const line1 = text(address == null ? void 0 : address.line1, 120);
61
+ const postalCode = text(address == null ? void 0 : address.postalCode, 20);
62
+ const country = text((address == null ? void 0 : address.country) || fallbackCountry, 2).toUpperCase();
63
+ if (!line1 || !postalCode || !/^[A-Z]{2}$/.test(country)) return null;
64
+ const e164 = normalizePhone(text(phone, 40) || null, country);
65
+ return _extends({
66
+ name: text(name, 80) || 'Store',
67
+ address: _extends({
68
+ country,
69
+ line1,
70
+ postalCode
71
+ }, (address == null ? void 0 : address.line2) ? {
72
+ line2: text(address.line2, 120)
73
+ } : {}, (address == null ? void 0 : address.city) ? {
74
+ city: text(address.city, 80)
75
+ } : {}, (address == null ? void 0 : address.state) ? {
76
+ state: text(address.state, 40)
77
+ } : {})
78
+ }, e164 ? {
79
+ phone: e164
80
+ } : {}, instructions ? {
81
+ instructions: text(instructions, 300)
82
+ } : {});
83
+ }
84
+ /** The stored run, read defensively; `null` when the order carries none. */ export function readOrderCourier(value) {
85
+ const raw = value != null ? value : null;
86
+ if (!raw || !COURIER_STATES.has(String(raw['state'])) || !text(raw['provider'])) return null;
87
+ const number = (field)=>{
88
+ const parsed = Number(raw[field]);
89
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
90
+ };
91
+ const etaMs = number('etaMs');
92
+ const pickupEtaMs = number('pickupEtaMs');
93
+ return _extends({
94
+ provider: text(raw['provider'], 40),
95
+ providerLabel: text(raw['providerLabel'], 40) || text(raw['provider'], 40),
96
+ deliveryRef: text(raw['deliveryRef'], 120),
97
+ state: raw['state']
98
+ }, text(raw['trackingUrl'], 500) ? {
99
+ trackingUrl: text(raw['trackingUrl'], 500)
100
+ } : {}, etaMs ? {
101
+ etaMs
102
+ } : {}, pickupEtaMs ? {
103
+ pickupEtaMs
104
+ } : {}, text(raw['reason'], 300) ? {
105
+ reason: text(raw['reason'], 300)
106
+ } : {}, raw['testMode'] === true ? {
107
+ testMode: true
108
+ } : {}, {
109
+ updatedAtMs: Number(raw['updatedAtMs']) || 0
110
+ });
111
+ }
112
+ /** The run as it is stored on the order: only the fields the order keeps, nothing undefined. */ function storedCourier(courier) {
113
+ const read = readOrderCourier(courier);
114
+ if (!read) throw new Error('Not a courier run');
115
+ return read;
116
+ }
117
+ /** One local delivery, or `null` when there is none by that id. */ export async function readLocalDeliveryRecord(hostId, recordId) {
118
+ var _orderSnapshot_data, _resolveHostToken, _settingsSnapshot_get, _ref, _ref1;
119
+ var _location_pickup, _order_totals;
120
+ if (badId(hostId) || badId(recordId)) return null;
121
+ const [orderSnapshot, hostSnapshot, settingsSnapshot] = await Promise.all([
122
+ hostRef(hostId).collection('orders').doc(recordId).get(),
123
+ hostRef(hostId).get(),
124
+ hostRef(hostId).collection('settings').doc('store').get()
125
+ ]);
126
+ if (!orderSnapshot.exists) return null;
127
+ const order = CommerceModel.liftLegacyOrder((_orderSnapshot_data = orderSnapshot.data()) != null ? _orderSnapshot_data : {});
128
+ const delivery = order.localDelivery;
129
+ if (order.fulfillmentMethod !== 'local_delivery' || !delivery) return null;
130
+ const storeName = String((_resolveHostToken = resolveHostToken('businessName', hostSnapshot.data())) != null ? _resolveHostToken : '') || 'Store';
131
+ const settings = CommerceModel.normalizeLocalDeliverySettings(settingsSnapshot.get('localDelivery'));
132
+ // Where it leaves from: the location the order names, else the one the
133
+ // delivery settings name, else the store's default location.
134
+ const locationId = String(delivery.locationId || settings.locationId || '');
135
+ let location = null;
136
+ if (locationId && !badId(locationId)) {
137
+ const snapshot = await hostRef(hostId).collection('locations').doc(locationId).get();
138
+ location = snapshot.exists ? snapshot.data() : null;
139
+ }
140
+ if (!location) {
141
+ const fallback = await hostRef(hostId).collection('locations').where('isDefault', '==', true).limit(1).get();
142
+ location = fallback.empty ? null : fallback.docs[0].data();
143
+ }
144
+ const pickupAddress = CommerceModel.normalizePostalAddress(location == null ? void 0 : location.postalAddress);
145
+ const homeCountry = text(pickupAddress == null ? void 0 : pickupAddress.country, 2) || 'US';
146
+ const pickup = pickupAddress ? place((location == null ? void 0 : location.name) ? `${storeName} — ${location.name}` : storeName, pickupAddress, homeCountry, pickupAddress.phone, location == null ? void 0 : (_location_pickup = location.pickup) == null ? void 0 : _location_pickup.instructions) : null;
147
+ const shipTo = order.shippingAddress;
148
+ const dropoff = shipTo ? place(shipTo.name || order.customerName || 'Customer', shipTo, homeCountry, order.customerPhone || shipTo.phone) : null;
149
+ const status = CommerceModel.orderLocalDeliveryStatus(delivery.status);
150
+ const states = CommerceModel.orderLineFulfillmentStates(order);
151
+ const itemCount = states.reduce((sum, state)=>sum + Math.max(0, state.quantity), 0);
152
+ return _extends({
153
+ hostId,
154
+ recordId,
155
+ displayRef: CommerceModel.formatOrderNumber(order, recordId),
156
+ sellerStatus: order.status,
157
+ status,
158
+ dispatchable: DISPATCHABLE_ORDER.has(order.status) && DISPATCHABLE_DELIVERY.has(status),
159
+ currency: String((_settingsSnapshot_get = settingsSnapshot.get('currency')) != null ? _settingsSnapshot_get : 'usd').toLowerCase(),
160
+ valueCents: Math.max(0, Math.round(Number((_ref = (_ref1 = (_order_totals = order.totals) == null ? void 0 : _order_totals.itemsCents) != null ? _ref1 : order.amountCents) != null ? _ref : 0)) || 0),
161
+ itemCount,
162
+ pickup,
163
+ dropoff
164
+ }, delivery.windowStartMs ? {
165
+ windowStartMs: delivery.windowStartMs
166
+ } : {}, delivery.windowEndMs ? {
167
+ windowEndMs: delivery.windowEndMs
168
+ } : {}, {
169
+ testMode: CommerceModel.orderIsTestMode({
170
+ checkoutSessionId: order.checkoutSessionId,
171
+ livemode: order.livemode,
172
+ $id: recordId
173
+ }),
174
+ courier: readOrderCourier(delivery.courier)
175
+ });
176
+ }
177
+ const sameCourier = (a, b)=>{
178
+ var _a_trackingUrl, _b_trackingUrl, _a_etaMs, _b_etaMs, _a_pickupEtaMs, _b_pickupEtaMs, _a_reason, _b_reason;
179
+ if (!a || !b) return a === b;
180
+ return a.provider === b.provider && a.deliveryRef === b.deliveryRef && a.state === b.state && ((_a_trackingUrl = a.trackingUrl) != null ? _a_trackingUrl : '') === ((_b_trackingUrl = b.trackingUrl) != null ? _b_trackingUrl : '') && ((_a_etaMs = a.etaMs) != null ? _a_etaMs : 0) === ((_b_etaMs = b.etaMs) != null ? _b_etaMs : 0) && ((_a_pickupEtaMs = a.pickupEtaMs) != null ? _a_pickupEtaMs : 0) === ((_b_pickupEtaMs = b.pickupEtaMs) != null ? _b_pickupEtaMs : 0) && ((_a_reason = a.reason) != null ? _a_reason : '') === ((_b_reason = b.reason) != null ? _b_reason : '');
181
+ };
182
+ const MOVE_ACTION = {
183
+ out_for_delivery: 'out_for_delivery',
184
+ delivered: 'delivered',
185
+ failed: 'delivery_failed'
186
+ };
187
+ /**
188
+ * Writes the run onto the order — one transaction that re-reads it, so two
189
+ * webhooks racing cannot interleave — then takes the step it brings through
190
+ * `moveLocalFulfillment`, which re-asks the transition itself. A repeat of a
191
+ * run the order already shows writes nothing, and a step the order already
192
+ * took answers `already` there, so a redelivered webhook is a no-op.
193
+ */ export async function recordLocalDeliveryCourier(write, nowMs = Date.now()) {
194
+ const { hostId, recordId } = write;
195
+ if (badId(hostId) || badId(recordId)) return {
196
+ outcome: 'no_such_record'
197
+ };
198
+ const courier = write.courier ? storedCourier(write.courier) : null;
199
+ const orderRef = hostRef(hostId).collection('orders').doc(recordId);
200
+ const written = await firestore().runTransaction(async (transaction)=>{
201
+ var _snapshot_data, _ref, _ref1, _ref2;
202
+ const snapshot = await transaction.get(orderRef);
203
+ if (!snapshot.exists) return {
204
+ outcome: 'no_such_record'
205
+ };
206
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
207
+ const delivery = order.localDelivery;
208
+ if (order.fulfillmentMethod !== 'local_delivery' || !delivery) return {
209
+ outcome: 'not_local_delivery'
210
+ };
211
+ const before = readOrderCourier(delivery.courier);
212
+ const status = CommerceModel.orderLocalDeliveryStatus(delivery.status);
213
+ if (sameCourier(before, courier)) return {
214
+ outcome: 'unchanged',
215
+ status
216
+ };
217
+ const next = _extends({}, delivery, {
218
+ updatedAtMs: nowMs
219
+ });
220
+ if (courier) next.courier = courier;
221
+ else delete next.courier;
222
+ const stateChanged = ((_ref = before == null ? void 0 : before.state) != null ? _ref : null) !== ((_ref1 = courier == null ? void 0 : courier.state) != null ? _ref1 : null) || (before == null ? void 0 : before.deliveryRef) !== (courier == null ? void 0 : courier.deliveryRef);
223
+ const timeline = stateChanged ? CommerceModel.appendOrderEvent(order, 'courier', courier ? [
224
+ `${courier.providerLabel}: ${CommerceModel.ORDER_COURIER_STATE_LABELS[courier.state]}`,
225
+ courier.testMode ? '(test)' : '',
226
+ courier.reason ? `— ${courier.reason}` : ''
227
+ ].filter(Boolean).join(' ').slice(0, 300) : `${(_ref2 = before == null ? void 0 : before.providerLabel) != null ? _ref2 : 'Courier'} canceled; the store delivers`, nowMs) : order.timeline;
228
+ transaction.update(orderRef, _extends({
229
+ localDelivery: next
230
+ }, stateChanged ? {
231
+ timeline
232
+ } : {}, {
233
+ updatedAtMs: nowMs
234
+ }));
235
+ return {
236
+ outcome: 'recorded',
237
+ status
238
+ };
239
+ });
240
+ if (written.outcome === 'no_such_record' || written.outcome === 'not_local_delivery') return written;
241
+ if (!write.move) return written;
242
+ const moved = await moveLocalFulfillment(_extends({
243
+ hostId,
244
+ orderId: recordId,
245
+ action: MOVE_ACTION[write.move]
246
+ }, write.reason ? {
247
+ reason: write.reason
248
+ } : {}, {
249
+ nowMs
250
+ }));
251
+ if (moved.outcome === 'updated') {
252
+ // After the write, never inside it, and never failing it (AGL-3610).
253
+ if (moved.notify) await notifyOrderBuyer({
254
+ hostId,
255
+ orderId: recordId
256
+ }, moved.notify);
257
+ return {
258
+ outcome: 'recorded',
259
+ status: write.move
260
+ };
261
+ }
262
+ if (moved.outcome === 'already') {
263
+ return written.outcome === 'unchanged' ? written : {
264
+ outcome: 'recorded',
265
+ status: write.move
266
+ };
267
+ }
268
+ if (moved.outcome === 'blocked') return {
269
+ outcome: 'blocked',
270
+ from: moved.from
271
+ };
272
+ if (moved.outcome === 'no_such_order') return {
273
+ outcome: 'no_such_record'
274
+ };
275
+ return {
276
+ outcome: 'not_local_delivery'
277
+ };
278
+ }
279
+ export const commerceLocalDeliveryRecords = {
280
+ read: (hostId, recordId)=>readLocalDeliveryRecord(hostId, recordId),
281
+ recordCourier: (write)=>recordLocalDeliveryCourier(write)
282
+ };
283
+
284
+ //# sourceMappingURL=local-delivery-records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/local-delivery-records.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 { resolveHostToken, type HostTokenSource } from '@aglyn/aglyn/app-utils/host-tokens'\nimport { normalizePhone } from '@aglyn/aglyn/foundation/definitions/contact.types'\nimport type {\n PluginLocalDeliveryPlace,\n PluginLocalDeliveryRecord,\n PluginLocalDeliveryRecords,\n PluginRecordCourier,\n PluginRecordCourierOutcome,\n PluginRecordCourierWrite,\n} from '@aglyn/aglyn/plugin-manager/plugin-local-deliveries'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { moveLocalFulfillment, type LocalFulfillmentAction } from './local-fulfillment-status'\nimport { notifyOrderBuyer } from './order-notifications'\n\n/**\n * THIS PLUGIN'S LOCAL DELIVERIES, AS A COURIER SEES THEM (AGL-3695):\n * commerce's implementation of core's `core.local-delivery-records`,\n * registered from the server declarations so it is there wherever a courier\n * is booked or a courier's webhook lands.\n *\n * A courier plugin never reads `hosts/{id}/orders`. It reads a drop here —\n * the location it leaves from, the buyer's door, the window — and writes the\n * run back here, and every step it brings goes through `moveLocalFulfillment`:\n * the same transition rule, timeline, handover and buyer messages as the\n * store's own driver, so \"Delivered\" by a courier fulfills the order exactly\n * as \"Delivered\" from the queue does.\n */\n\nconst firestore = () => firebaseAdmin.app().firestore()\nconst hostRef = (hostId: string) => firestore().collection('hosts').doc(hostId)\n\nconst badId = (value: string) => !value || value.includes('/') || /^__.*__$/.test(value)\n\n/** The order statuses a courier may be sent from. */\nconst DISPATCHABLE_ORDER = new Set(['paid', 'partially_fulfilled', 'fulfilled'])\n/** The delivery statuses a courier may be sent from: not yet out, or a failed drop going out again. */\nconst DISPATCHABLE_DELIVERY = new Set(['scheduled', 'failed'])\n\nconst COURIER_STATES = new Set([\n 'requested',\n 'assigned',\n 'at_pickup',\n 'picked_up',\n 'at_dropoff',\n 'delivered',\n 'cancelled',\n 'returning',\n 'returned',\n])\n\nconst text = (value: unknown, max = 200): string => String(value ?? '').trim().slice(0, max)\n\nfunction place(\n name: string,\n address: CommerceModel.OrderAddress | CommerceModel.PostalAddress | undefined,\n fallbackCountry: string,\n phone: unknown,\n instructions?: string,\n): PluginLocalDeliveryPlace | null {\n const line1 = text(address?.line1, 120)\n const postalCode = text(address?.postalCode, 20)\n const country = text(address?.country || fallbackCountry, 2).toUpperCase()\n if (!line1 || !postalCode || !/^[A-Z]{2}$/.test(country)) return null\n const e164 = normalizePhone(text(phone, 40) || null, country)\n return {\n name: text(name, 80) || 'Store',\n address: {\n country,\n line1,\n postalCode,\n ...(address?.line2 ? { line2: text(address.line2, 120) } : {}),\n ...(address?.city ? { city: text(address.city, 80) } : {}),\n ...(address?.state ? { state: text(address.state, 40) } : {}),\n },\n ...(e164 ? { phone: e164 } : {}),\n ...(instructions ? { instructions: text(instructions, 300) } : {}),\n }\n}\n\n/** The stored run, read defensively; `null` when the order carries none. */\nexport function readOrderCourier(value: unknown): PluginRecordCourier | null {\n const raw = (value ?? null) as Record<string, unknown> | null\n if (!raw || !COURIER_STATES.has(String(raw['state'])) || !text(raw['provider'])) return null\n const number = (field: string) => {\n const parsed = Number(raw[field])\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined\n }\n const etaMs = number('etaMs')\n const pickupEtaMs = number('pickupEtaMs')\n return {\n provider: text(raw['provider'], 40),\n providerLabel: text(raw['providerLabel'], 40) || text(raw['provider'], 40),\n deliveryRef: text(raw['deliveryRef'], 120),\n state: raw['state'] as PluginRecordCourier['state'],\n ...(text(raw['trackingUrl'], 500) ? { trackingUrl: text(raw['trackingUrl'], 500) } : {}),\n ...(etaMs ? { etaMs } : {}),\n ...(pickupEtaMs ? { pickupEtaMs } : {}),\n ...(text(raw['reason'], 300) ? { reason: text(raw['reason'], 300) } : {}),\n ...(raw['testMode'] === true ? { testMode: true } : {}),\n updatedAtMs: Number(raw['updatedAtMs']) || 0,\n }\n}\n\n/** The run as it is stored on the order: only the fields the order keeps, nothing undefined. */\nfunction storedCourier(courier: PluginRecordCourier): CommerceModel.OrderLocalDeliveryCourier {\n const read = readOrderCourier(courier)\n if (!read) throw new Error('Not a courier run')\n return read as CommerceModel.OrderLocalDeliveryCourier\n}\n\n/** One local delivery, or `null` when there is none by that id. */\nexport async function readLocalDeliveryRecord(\n hostId: string,\n recordId: string,\n): Promise<PluginLocalDeliveryRecord | null> {\n if (badId(hostId) || badId(recordId)) return null\n const [orderSnapshot, hostSnapshot, settingsSnapshot] = await Promise.all([\n hostRef(hostId).collection('orders').doc(recordId).get(),\n hostRef(hostId).get(),\n hostRef(hostId).collection('settings').doc('store').get(),\n ])\n if (!orderSnapshot.exists) return null\n const order = CommerceModel.liftLegacyOrder((orderSnapshot.data() ?? {}) as never)\n const delivery = order.localDelivery\n if (order.fulfillmentMethod !== 'local_delivery' || !delivery) return null\n\n const storeName =\n String(resolveHostToken('businessName', hostSnapshot.data() as HostTokenSource) ?? '') || 'Store'\n const settings = CommerceModel.normalizeLocalDeliverySettings(settingsSnapshot.get('localDelivery'))\n // Where it leaves from: the location the order names, else the one the\n // delivery settings name, else the store's default location.\n const locationId = String(delivery.locationId || settings.locationId || '')\n let location: CommerceModel.InventoryLocation | null = null\n if (locationId && !badId(locationId)) {\n const snapshot = await hostRef(hostId).collection('locations').doc(locationId).get()\n location = snapshot.exists ? (snapshot.data() as CommerceModel.InventoryLocation) : null\n }\n if (!location) {\n const fallback = await hostRef(hostId).collection('locations').where('isDefault', '==', true).limit(1).get()\n location = fallback.empty ? null : (fallback.docs[0].data() as CommerceModel.InventoryLocation)\n }\n const pickupAddress = CommerceModel.normalizePostalAddress(location?.postalAddress)\n const homeCountry = text(pickupAddress?.country, 2) || 'US'\n const pickup = pickupAddress\n ? place(\n location?.name ? `${storeName} — ${location.name}` : storeName,\n pickupAddress,\n homeCountry,\n pickupAddress.phone,\n location?.pickup?.instructions,\n )\n : null\n const shipTo = order.shippingAddress\n const dropoff = shipTo\n ? place(\n shipTo.name || order.customerName || 'Customer',\n shipTo,\n homeCountry,\n order.customerPhone || shipTo.phone,\n )\n : null\n const status = CommerceModel.orderLocalDeliveryStatus(delivery.status)\n const states = CommerceModel.orderLineFulfillmentStates(order)\n const itemCount = states.reduce((sum, state) => sum + Math.max(0, state.quantity), 0)\n return {\n hostId,\n recordId,\n displayRef: CommerceModel.formatOrderNumber(order, recordId),\n sellerStatus: order.status,\n status,\n dispatchable: DISPATCHABLE_ORDER.has(order.status) && DISPATCHABLE_DELIVERY.has(status),\n currency: String(settingsSnapshot.get('currency') ?? 'usd').toLowerCase(),\n valueCents: Math.max(0, Math.round(Number(order.totals?.itemsCents ?? order.amountCents ?? 0)) || 0),\n itemCount,\n pickup,\n dropoff,\n ...(delivery.windowStartMs ? { windowStartMs: delivery.windowStartMs } : {}),\n ...(delivery.windowEndMs ? { windowEndMs: delivery.windowEndMs } : {}),\n testMode: CommerceModel.orderIsTestMode({\n checkoutSessionId: order.checkoutSessionId,\n livemode: (order as unknown as { livemode?: unknown }).livemode,\n $id: recordId,\n }),\n courier: readOrderCourier(delivery.courier),\n }\n}\n\nconst sameCourier = (a: PluginRecordCourier | null, b: PluginRecordCourier | null): boolean => {\n if (!a || !b) return a === b\n return (\n a.provider === b.provider &&\n a.deliveryRef === b.deliveryRef &&\n a.state === b.state &&\n (a.trackingUrl ?? '') === (b.trackingUrl ?? '') &&\n (a.etaMs ?? 0) === (b.etaMs ?? 0) &&\n (a.pickupEtaMs ?? 0) === (b.pickupEtaMs ?? 0) &&\n (a.reason ?? '') === (b.reason ?? '')\n )\n}\n\nconst MOVE_ACTION: Record<NonNullable<PluginRecordCourierWrite['move']>, LocalFulfillmentAction> = {\n out_for_delivery: 'out_for_delivery',\n delivered: 'delivered',\n failed: 'delivery_failed',\n}\n\n/**\n * Writes the run onto the order — one transaction that re-reads it, so two\n * webhooks racing cannot interleave — then takes the step it brings through\n * `moveLocalFulfillment`, which re-asks the transition itself. A repeat of a\n * run the order already shows writes nothing, and a step the order already\n * took answers `already` there, so a redelivered webhook is a no-op.\n */\nexport async function recordLocalDeliveryCourier(\n write: PluginRecordCourierWrite,\n nowMs: number = Date.now(),\n): Promise<PluginRecordCourierOutcome> {\n const { hostId, recordId } = write\n if (badId(hostId) || badId(recordId)) return { outcome: 'no_such_record' }\n const courier = write.courier ? storedCourier(write.courier) : null\n const orderRef = hostRef(hostId).collection('orders').doc(recordId)\n const written = await firestore().runTransaction(async (transaction) => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_record' as const }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const delivery = order.localDelivery\n if (order.fulfillmentMethod !== 'local_delivery' || !delivery) return { outcome: 'not_local_delivery' as const }\n const before = readOrderCourier(delivery.courier)\n const status = CommerceModel.orderLocalDeliveryStatus(delivery.status)\n if (sameCourier(before, courier as PluginRecordCourier | null)) return { outcome: 'unchanged' as const, status }\n const next: CommerceModel.OrderLocalDelivery = { ...delivery, updatedAtMs: nowMs }\n if (courier) next.courier = courier\n else delete next.courier\n const stateChanged = (before?.state ?? null) !== (courier?.state ?? null) || before?.deliveryRef !== courier?.deliveryRef\n const timeline = stateChanged\n ? CommerceModel.appendOrderEvent(\n order,\n 'courier',\n courier\n ? [\n `${courier.providerLabel}: ${CommerceModel.ORDER_COURIER_STATE_LABELS[courier.state]}`,\n courier.testMode ? '(test)' : '',\n courier.reason ? `— ${courier.reason}` : '',\n ]\n .filter(Boolean)\n .join(' ')\n .slice(0, 300)\n : `${before?.providerLabel ?? 'Courier'} canceled; the store delivers`,\n nowMs,\n )\n : order.timeline\n transaction.update(orderRef, {\n localDelivery: next,\n ...(stateChanged ? { timeline } : {}),\n updatedAtMs: nowMs,\n })\n return { outcome: 'recorded' as const, status }\n })\n if (written.outcome === 'no_such_record' || written.outcome === 'not_local_delivery') return written\n if (!write.move) return written\n const moved = await moveLocalFulfillment({\n hostId,\n orderId: recordId,\n action: MOVE_ACTION[write.move],\n ...(write.reason ? { reason: write.reason } : {}),\n nowMs,\n })\n if (moved.outcome === 'updated') {\n // After the write, never inside it, and never failing it (AGL-3610).\n if (moved.notify) await notifyOrderBuyer({ hostId, orderId: recordId }, moved.notify)\n return { outcome: 'recorded', status: write.move }\n }\n if (moved.outcome === 'already') {\n return written.outcome === 'unchanged' ? written : { outcome: 'recorded', status: write.move }\n }\n if (moved.outcome === 'blocked') return { outcome: 'blocked', from: moved.from }\n if (moved.outcome === 'no_such_order') return { outcome: 'no_such_record' }\n return { outcome: 'not_local_delivery' }\n}\n\nexport const commerceLocalDeliveryRecords: PluginLocalDeliveryRecords = {\n read: (hostId, recordId) => readLocalDeliveryRecord(hostId, recordId),\n recordCourier: (write) => recordLocalDeliveryCourier(write),\n}\n"],"names":["resolveHostToken","normalizePhone","firebaseAdmin","CommerceModel","moveLocalFulfillment","notifyOrderBuyer","firestore","app","hostRef","hostId","collection","doc","badId","value","includes","test","DISPATCHABLE_ORDER","Set","DISPATCHABLE_DELIVERY","COURIER_STATES","text","max","String","trim","slice","place","name","address","fallbackCountry","phone","instructions","line1","postalCode","country","toUpperCase","e164","line2","city","state","readOrderCourier","raw","has","number","field","parsed","Number","isFinite","undefined","etaMs","pickupEtaMs","provider","providerLabel","deliveryRef","trackingUrl","reason","testMode","updatedAtMs","storedCourier","courier","read","Error","readLocalDeliveryRecord","recordId","orderSnapshot","settingsSnapshot","order","location","hostSnapshot","Promise","all","get","exists","liftLegacyOrder","data","delivery","localDelivery","fulfillmentMethod","storeName","settings","normalizeLocalDeliverySettings","locationId","snapshot","fallback","where","limit","empty","docs","pickupAddress","normalizePostalAddress","postalAddress","homeCountry","pickup","shipTo","shippingAddress","dropoff","customerName","customerPhone","status","orderLocalDeliveryStatus","states","orderLineFulfillmentStates","itemCount","reduce","sum","Math","quantity","displayRef","formatOrderNumber","sellerStatus","dispatchable","currency","toLowerCase","valueCents","round","totals","itemsCents","amountCents","windowStartMs","windowEndMs","orderIsTestMode","checkoutSessionId","livemode","$id","sameCourier","a","b","MOVE_ACTION","out_for_delivery","delivered","failed","recordLocalDeliveryCourier","write","nowMs","Date","now","outcome","orderRef","written","runTransaction","transaction","before","next","stateChanged","timeline","appendOrderEvent","ORDER_COURIER_STATE_LABELS","filter","Boolean","join","update","move","moved","orderId","action","notify","from","commerceLocalDeliveryRecords","recordCourier"],"mappings":"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"}