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

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
@@ -0,0 +1,76 @@
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 { appendOrderEvent, orderLineRefunded } from "./commerce-orders.js";
18
+ /*
19
+ * What a person writes on an order by hand (AGL-3651, AGL-3652): a note on
20
+ * its timeline and the answer to a restock question. The console's order
21
+ * dialog and the server routes the native apps call both build the write
22
+ * here, so the two cannot disagree about what a note or an answer is.
23
+ */ /** The longest note the timeline keeps. */ export const ORDER_NOTE_MAX_LENGTH = 500;
24
+ /** The words a timeline line carries for an answered restock question. */ export const RESTOCK_ANSWER_DETAIL = {
25
+ restocked: 'answered — restocked',
26
+ dismissed: 'answered — no restock'
27
+ };
28
+ /** The fields a note updates, or null for a note with nothing in it. */ export function orderNoteUpdate(order, text, atMs = Date.now()) {
29
+ const note = String(text != null ? text : '').trim().slice(0, ORDER_NOTE_MAX_LENGTH);
30
+ if (!note) return null;
31
+ return {
32
+ timeline: appendOrderEvent(order, 'note', note, atMs)
33
+ };
34
+ }
35
+ /**
36
+ * Answers the order's open restock question. `askedFlaggedAtMs` names the
37
+ * question the person saw, so an answer never lands on a newer one. The
38
+ * timeline is the stored order's, so a note that landed meanwhile survives.
39
+ */ export function restockAnswer(current, askedFlaggedAtMs, resolution, uid, atMs = Date.now()) {
40
+ const fresh = current.restockCheck;
41
+ if (!fresh || fresh.resolution) return {
42
+ verdict: 'answered'
43
+ };
44
+ if (fresh.flaggedAtMs !== askedFlaggedAtMs) return {
45
+ verdict: 'changed'
46
+ };
47
+ return {
48
+ verdict: 'recorded',
49
+ update: {
50
+ restockCheck: _extends({}, fresh, {
51
+ resolution,
52
+ resolvedAtMs: atMs
53
+ }, uid ? {
54
+ resolvedBy: uid
55
+ } : {}),
56
+ timeline: appendOrderEvent(current, 'restock-check', RESTOCK_ANSWER_DETAIL[resolution], atMs)
57
+ }
58
+ };
59
+ }
60
+ /**
61
+ * What the order dialog's restock card says about the open question, word for
62
+ * word: how many units may need restocking after which door, and how far to
63
+ * trust that number. The native apps print this same sentence.
64
+ */ export function describeRestockCheck(restock, order) {
65
+ var _restock_lines;
66
+ // On a partial the flagged units are the MOST it could be — UNLESS the
67
+ // refund named its lines (AGL-2325), in which case the question is scoped
68
+ // to exactly those and the merchant is only being asked whether the goods
69
+ // came back.
70
+ const linesNamed = Boolean(((_restock_lines = restock.lines) == null ? void 0 : _restock_lines.length) && restock.lines.every((line)=>line.lineIndex != null && orderLineRefunded(order, line.lineIndex)));
71
+ return `${restock.units} ${restock.units === 1 ? 'unit' : 'units'} may need restocking after this ${restock.kind === 'chargeback' ? 'chargeback' : 'refund'}.` + // The chargeback default is NO, same as the timeline's wording: the
72
+ // shopper kept the item and took the money.
73
+ (restock.kind === 'chargeback' ? ' The shopper kept the goods unless they actually came back.' : '') + (restock.fullyReversed ? '' : linesNamed ? ' Only part of the money came back: these are the lines withdrawn by this refund, so the units are theirs — only you know whether the goods came back.' : ' Only part of the money came back, so these units are an upper bound — only you know which goods returned.');
74
+ }
75
+
76
+ //# sourceMappingURL=order-annotations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-annotations.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n appendOrderEvent,\n orderLineRefunded,\n type HostOrder,\n type OrderRestockCheck,\n type OrderTimelineEvent,\n} from './commerce-orders'\n\n/*\n * What a person writes on an order by hand (AGL-3651, AGL-3652): a note on\n * its timeline and the answer to a restock question. The console's order\n * dialog and the server routes the native apps call both build the write\n * here, so the two cannot disagree about what a note or an answer is.\n */\n\n/** The longest note the timeline keeps. */\nexport const ORDER_NOTE_MAX_LENGTH = 500\n\n/** What the restock question can be answered with. */\nexport type RestockResolution = NonNullable<OrderRestockCheck['resolution']>\n\n/** The words a timeline line carries for an answered restock question. */\nexport const RESTOCK_ANSWER_DETAIL: Readonly<Record<RestockResolution, string>> = {\n restocked: 'answered — restocked',\n dismissed: 'answered — no restock',\n}\n\n/** The fields a note updates, or null for a note with nothing in it. */\nexport function orderNoteUpdate(\n order: Pick<HostOrder, 'timeline'>,\n text: string,\n atMs = Date.now(),\n): { timeline: OrderTimelineEvent[] } | null {\n const note = String(text ?? '').trim().slice(0, ORDER_NOTE_MAX_LENGTH)\n if (!note) return null\n return { timeline: appendOrderEvent(order, 'note', note, atMs) }\n}\n\nexport type RestockAnswer =\n | { verdict: 'recorded'; update: { restockCheck: OrderRestockCheck; timeline: OrderTimelineEvent[] } }\n /** The question was already answered (by the cancel route, or another admin). */\n | { verdict: 'answered' }\n /** A later reversal asked again: the caller answered an older question. */\n | { verdict: 'changed' }\n\n/**\n * Answers the order's open restock question. `askedFlaggedAtMs` names the\n * question the person saw, so an answer never lands on a newer one. The\n * timeline is the stored order's, so a note that landed meanwhile survives.\n */\nexport function restockAnswer(\n current: Pick<HostOrder, 'restockCheck' | 'timeline'>,\n askedFlaggedAtMs: number,\n resolution: RestockResolution,\n uid: string,\n atMs = Date.now(),\n): RestockAnswer {\n const fresh = current.restockCheck\n if (!fresh || fresh.resolution) return { verdict: 'answered' }\n if (fresh.flaggedAtMs !== askedFlaggedAtMs) return { verdict: 'changed' }\n return {\n verdict: 'recorded',\n update: {\n restockCheck: { ...fresh, resolution, resolvedAtMs: atMs, ...(uid ? { resolvedBy: uid } : {}) },\n timeline: appendOrderEvent(current, 'restock-check', RESTOCK_ANSWER_DETAIL[resolution], atMs),\n },\n }\n}\n\n/**\n * What the order dialog's restock card says about the open question, word for\n * word: how many units may need restocking after which door, and how far to\n * trust that number. The native apps print this same sentence.\n */\nexport function describeRestockCheck(\n restock: Pick<OrderRestockCheck, 'units' | 'kind' | 'fullyReversed' | 'lines'>,\n order: Partial<HostOrder>,\n): string {\n // On a partial the flagged units are the MOST it could be — UNLESS the\n // refund named its lines (AGL-2325), in which case the question is scoped\n // to exactly those and the merchant is only being asked whether the goods\n // came back.\n const linesNamed = Boolean(\n restock.lines?.length &&\n restock.lines.every((line) => line.lineIndex != null && orderLineRefunded(order, line.lineIndex)),\n )\n return (\n `${restock.units} ${restock.units === 1 ? 'unit' : 'units'} may need restocking after this ${\n restock.kind === 'chargeback' ? 'chargeback' : 'refund'\n }.` +\n // The chargeback default is NO, same as the timeline's wording: the\n // shopper kept the item and took the money.\n (restock.kind === 'chargeback' ? ' The shopper kept the goods unless they actually came back.' : '') +\n (restock.fullyReversed\n ? ''\n : linesNamed\n ? ' Only part of the money came back: these are the lines withdrawn by this refund, so the units are theirs — only you know whether the goods came back.'\n : ' Only part of the money came back, so these units are an upper bound — only you know which goods returned.')\n )\n}\n"],"names":["appendOrderEvent","orderLineRefunded","ORDER_NOTE_MAX_LENGTH","RESTOCK_ANSWER_DETAIL","restocked","dismissed","orderNoteUpdate","order","text","atMs","Date","now","note","String","trim","slice","timeline","restockAnswer","current","askedFlaggedAtMs","resolution","uid","fresh","restockCheck","verdict","flaggedAtMs","update","resolvedAtMs","resolvedBy","describeRestockCheck","restock","linesNamed","Boolean","lines","length","every","line","lineIndex","units","kind","fullyReversed"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAChBC,iBAAiB,QAIZ,uBAAmB;AAE1B;;;;;CAKC,GAED,yCAAyC,GACzC,OAAO,MAAMC,wBAAwB,IAAG;AAKxC,wEAAwE,GACxE,OAAO,MAAMC,wBAAqE;IAChFC,WAAW;IACXC,WAAW;AACb,EAAC;AAED,sEAAsE,GACtE,OAAO,SAASC,gBACdC,KAAkC,EAClCC,IAAY,EACZC,OAAOC,KAAKC,GAAG,EAAE;IAEjB,MAAMC,OAAOC,OAAOL,eAAAA,OAAQ,IAAIM,IAAI,GAAGC,KAAK,CAAC,GAAGb;IAChD,IAAI,CAACU,MAAM,OAAO;IAClB,OAAO;QAAEI,UAAUhB,iBAAiBO,OAAO,QAAQK,MAAMH;IAAM;AACjE;AASA;;;;CAIC,GACD,OAAO,SAASQ,cACdC,OAAqD,EACrDC,gBAAwB,EACxBC,UAA6B,EAC7BC,GAAW,EACXZ,OAAOC,KAAKC,GAAG,EAAE;IAEjB,MAAMW,QAAQJ,QAAQK,YAAY;IAClC,IAAI,CAACD,SAASA,MAAMF,UAAU,EAAE,OAAO;QAAEI,SAAS;IAAW;IAC7D,IAAIF,MAAMG,WAAW,KAAKN,kBAAkB,OAAO;QAAEK,SAAS;IAAU;IACxE,OAAO;QACLA,SAAS;QACTE,QAAQ;YACNH,cAAc,aAAKD;gBAAOF;gBAAYO,cAAclB;eAAUY,MAAM;gBAAEO,YAAYP;YAAI,IAAI,CAAC;YAC3FL,UAAUhB,iBAAiBkB,SAAS,iBAAiBf,qBAAqB,CAACiB,WAAW,EAAEX;QAC1F;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASoB,qBACdC,OAA8E,EAC9EvB,KAAyB;QAOvBuB;IALF,uEAAuE;IACvE,0EAA0E;IAC1E,0EAA0E;IAC1E,aAAa;IACb,MAAMC,aAAaC,QACjBF,EAAAA,iBAAAA,QAAQG,KAAK,qBAAbH,eAAeI,MAAM,KACnBJ,QAAQG,KAAK,CAACE,KAAK,CAAC,CAACC,OAASA,KAAKC,SAAS,IAAI,QAAQpC,kBAAkBM,OAAO6B,KAAKC,SAAS;IAEnG,OACE,GAAGP,QAAQQ,KAAK,CAAC,CAAC,EAAER,QAAQQ,KAAK,KAAK,IAAI,SAAS,QAAQ,gCAAgC,EACzFR,QAAQS,IAAI,KAAK,eAAe,eAAe,SAChD,CAAC,CAAC,GACH,oEAAoE;IACpE,4CAA4C;IAC3CT,CAAAA,QAAQS,IAAI,KAAK,eAAe,gEAAgE,EAAC,IACjGT,CAAAA,QAAQU,aAAa,GAClB,KACAT,aACE,0JACA,4GAA2G;AAErH"}
@@ -60,6 +60,8 @@ export interface OrderEventFulfillment {
60
60
  trackingNumber: string | null;
61
61
  trackingUrl: string | null;
62
62
  labelUrl: string | null;
63
+ /** What the shipment cost to send — its label's cost, or the merchant's hand-entered one (AGL-3705) — in integer cents of the order's currency; `null` when unknown (AGL-3693). */
64
+ labelCostCents: number | null;
63
65
  at: string;
64
66
  }
65
67
  /** A refund as an event carries it: what THIS refund moved. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-events.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginDomainEvent,\n type PluginDomainEvent,\n type PluginDomainEventDeclaration,\n} from '@aglyn/aglyn/plugin-manager/plugin-domain-events'\n\n/**\n * The order and return events commerce raises (AGL-3611), for other plugins\n * — accounting, shipping, workflows — and for the merchant's own webhooks.\n *\n * Each payload carries `order` in the public API's shape (`GET\n * /v1/sites/{siteId}/orders/{orderId}`), as it stood once the fact was\n * written, so a subscriber reads one documented object rather than the\n * plugin's storage. A subscriber in another plugin restates the fields it\n * reads; these types are this plugin's own.\n */\n\n/** An order as the public API returns it. Money is integer cents. */\nexport interface OrderEventOrder {\n id: string\n object: 'order'\n number: number | null\n status: string | null\n channel: string\n currency: string\n customerEmail: string | null\n customerName: string | null\n lineItems: unknown\n totals: {\n itemsCents: number\n shippingCents: number\n taxCents: number\n discountCents: number\n totalCents: number | null\n feeCents: number\n }\n refundedCents: number\n /** The tax regime: `stripe-automatic`, `manual`, `none`, or `null` when not recorded. */\n taxMode: string | null\n [key: string]: unknown\n}\n\n/** A shipment as an event carries it. */\nexport interface OrderEventFulfillment {\n id: string\n lines: Array<{ lineItemId: number; quantity: number }>\n carrier: string | null\n trackingNumber: string | null\n trackingUrl: string | null\n labelUrl: string | null\n at: string\n}\n\n/** A refund as an event carries it: what THIS refund moved. */\nexport interface OrderEventRefund {\n /** Stripe's refund id, when there is one. */\n id: string | null\n amountCents: number\n /** The line indexes refunded by name; empty for an amount-only refund. */\n lineItemIds: number[]\n /** Whether this refund left nothing more to refund. */\n full: boolean\n}\n\n/** A return as an event carries it. */\nexport interface OrderEventReturn {\n id: string\n status: string\n lines: Array<{ lineItemId: number; quantity: number; reason: string }>\n refundCents: number | null\n /** What went back in stock when the parcel arrived; `null` before then. */\n restock?: Array<{ lineItemId: number; quantity: number }> | null\n}\n\nexport interface OrderEventPayload {\n order: OrderEventOrder\n}\nexport interface OrderFulfilledEventPayload extends OrderEventPayload {\n fulfillment: OrderEventFulfillment\n}\nexport interface OrderRefundedEventPayload extends OrderEventPayload {\n refund: OrderEventRefund\n}\nexport interface ReturnEventPayload extends OrderEventPayload {\n return: OrderEventReturn\n}\n\nexport const ORDER_PAID_EVENT = definePluginDomainEvent<OrderEventPayload>('order.paid')\nexport const ORDER_FULFILLED_EVENT = definePluginDomainEvent<OrderFulfilledEventPayload>('order.fulfilled')\nexport const ORDER_DELIVERED_EVENT = definePluginDomainEvent<OrderEventPayload>('order.delivered')\nexport const ORDER_REFUNDED_EVENT = definePluginDomainEvent<OrderRefundedEventPayload>('order.refunded')\nexport const ORDER_CANCELLED_EVENT = definePluginDomainEvent<OrderEventPayload>('order.cancelled')\nexport const RETURN_REQUESTED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.requested')\nexport const RETURN_APPROVED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.approved')\nexport const RETURN_DECLINED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.declined')\nexport const RETURN_RECEIVED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.received')\nexport const RETURN_REFUNDED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.refunded')\n\n/**\n * A checkout a shopper reached with an address (AGL-3639): the fact an\n * abandoned-cart flow in a merchant's own email platform starts from. Not an\n * order event — nothing was bought — so it is raised beside them rather than\n * among them: it is not a workflow trigger and not offered to the merchant's\n * outbound webhooks, whose events are about orders.\n */\nexport interface CheckoutStartedEventPayload {\n checkout: {\n /** The checkout session's id: one event per checkout. */\n id: string\n email: string\n /** Whether the shopper ticked the store's marketing box at checkout. */\n marketingOptIn: boolean\n currency: string\n /** The basket's list value, integer cents, before discounts and shipping. */\n itemsCents: number\n /** Where the shopper can pick the checkout up again. */\n resumeUrl: string | null\n items: Array<{\n productId: string\n variantId: string | null\n name: string\n sku: string | null\n quantity: number\n unitCents: number\n }>\n }\n}\n\nexport const CHECKOUT_STARTED_EVENT = definePluginDomainEvent<CheckoutStartedEventPayload>('checkout.started')\n\nexport const CHECKOUT_STARTED_EVENT_DECLARATION: PluginDomainEventDeclaration = {\n event: CHECKOUT_STARTED_EVENT as PluginDomainEvent<unknown>,\n label: 'Checkout started',\n description:\n 'A shopper reached checkout with an email address: the basket, its value and the link back to it. Raised once per checkout, whether or not it is paid.',\n payloadKeys: ['checkout'],\n}\n\n/** Every event commerce raises, by name. */\nexport type CommerceEventName =\n | 'order.paid'\n | 'order.fulfilled'\n | 'order.delivered'\n | 'order.refunded'\n | 'order.cancelled'\n | 'return.requested'\n | 'return.approved'\n | 'return.declined'\n | 'return.received'\n | 'return.refunded'\n\n/** The events, in the order pickers list them, with their words. */\nexport const COMMERCE_EVENT_DECLARATIONS: ReadonlyArray<\n PluginDomainEventDeclaration & { event: PluginDomainEvent<unknown>; hostEvent: string }\n> = [\n {\n event: ORDER_PAID_EVENT,\n hostEvent: 'orderPaid',\n label: 'Order paid',\n description:\n 'An order was paid: a storefront checkout, a buy-now, a paid payment link, a POS sale or a subscription renewal.',\n payloadKeys: ['order'],\n },\n {\n event: ORDER_FULFILLED_EVENT,\n hostEvent: 'orderFulfilled',\n label: 'Order fulfilled',\n description: 'A shipment was recorded on an order, once per shipment, with the units it carried.',\n payloadKeys: ['order', 'fulfillment'],\n },\n {\n event: ORDER_DELIVERED_EVENT,\n hostEvent: 'orderDelivered',\n label: 'Order delivered',\n description: 'An order was marked delivered.',\n payloadKeys: ['order'],\n },\n {\n event: ORDER_REFUNDED_EVENT,\n hostEvent: 'orderRefunded',\n label: 'Order refunded',\n description: 'Money went back to the buyer, in part or in full, once per refund.',\n payloadKeys: ['order', 'refund'],\n },\n {\n event: ORDER_CANCELLED_EVENT,\n hostEvent: 'orderCancelled',\n label: 'Order canceled',\n description: 'An order was canceled and its stock returned.',\n payloadKeys: ['order'],\n },\n {\n event: RETURN_REQUESTED_EVENT,\n hostEvent: 'returnRequested',\n label: 'Return requested',\n description: 'A buyer asked to return items, or the merchant opened a return for them.',\n payloadKeys: ['order', 'return'],\n },\n {\n event: RETURN_APPROVED_EVENT,\n hostEvent: 'returnApproved',\n label: 'Return approved',\n description: 'The store approved a return, or opened one itself.',\n payloadKeys: ['order', 'return'],\n },\n {\n event: RETURN_DECLINED_EVENT,\n hostEvent: 'returnDeclined',\n label: 'Return declined',\n description: 'The store declined a return request.',\n payloadKeys: ['order', 'return'],\n },\n {\n event: RETURN_RECEIVED_EVENT,\n hostEvent: 'returnReceived',\n label: 'Return received',\n description: 'The returned items arrived, with what went back in stock.',\n payloadKeys: ['order', 'return'],\n },\n {\n event: RETURN_REFUNDED_EVENT,\n hostEvent: 'returnRefunded',\n label: 'Return refunded',\n description: 'A return was refunded.',\n payloadKeys: ['order', 'return'],\n },\n]\n\n/** The workflow trigger an event is offered as. */\nexport function commerceHostEventFor(event: string): string | null {\n return COMMERCE_EVENT_DECLARATIONS.find((entry) => entry.event.id === event)?.hostEvent ?? null\n}\n\n/** The event names, for a webhook's event picker. */\nexport const COMMERCE_EVENT_NAMES: readonly CommerceEventName[] = [\n 'order.paid',\n 'order.fulfilled',\n 'order.delivered',\n 'order.refunded',\n 'order.cancelled',\n 'return.requested',\n 'return.approved',\n 'return.declined',\n 'return.received',\n 'return.refunded',\n]\n"],"names":["definePluginDomainEvent","ORDER_PAID_EVENT","ORDER_FULFILLED_EVENT","ORDER_DELIVERED_EVENT","ORDER_REFUNDED_EVENT","ORDER_CANCELLED_EVENT","RETURN_REQUESTED_EVENT","RETURN_APPROVED_EVENT","RETURN_DECLINED_EVENT","RETURN_RECEIVED_EVENT","RETURN_REFUNDED_EVENT","CHECKOUT_STARTED_EVENT","CHECKOUT_STARTED_EVENT_DECLARATION","event","label","description","payloadKeys","COMMERCE_EVENT_DECLARATIONS","hostEvent","commerceHostEventFor","find","entry","id","COMMERCE_EVENT_NAMES"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,uBAAuB,QAGlB,mDAAkD;AAmFzD,OAAO,MAAMC,mBAAmBD,wBAA2C,cAAa;AACxF,OAAO,MAAME,wBAAwBF,wBAAoD,mBAAkB;AAC3G,OAAO,MAAMG,wBAAwBH,wBAA2C,mBAAkB;AAClG,OAAO,MAAMI,uBAAuBJ,wBAAmD,kBAAiB;AACxG,OAAO,MAAMK,wBAAwBL,wBAA2C,mBAAkB;AAClG,OAAO,MAAMM,yBAAyBN,wBAA4C,oBAAmB;AACrG,OAAO,MAAMO,wBAAwBP,wBAA4C,mBAAkB;AACnG,OAAO,MAAMQ,wBAAwBR,wBAA4C,mBAAkB;AACnG,OAAO,MAAMS,wBAAwBT,wBAA4C,mBAAkB;AACnG,OAAO,MAAMU,wBAAwBV,wBAA4C,mBAAkB;AAgCnG,OAAO,MAAMW,yBAAyBX,wBAAqD,oBAAmB;AAE9G,OAAO,MAAMY,qCAAmE;IAC9EC,OAAOF;IACPG,OAAO;IACPC,aACE;IACFC,aAAa;QAAC;KAAW;AAC3B,EAAC;AAeD,kEAAkE,GAClE,OAAO,MAAMC,8BAET;IACF;QACEJ,OAAOZ;QACPiB,WAAW;QACXJ,OAAO;QACPC,aACE;QACFC,aAAa;YAAC;SAAQ;IACxB;IACA;QACEH,OAAOX;QACPgB,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAc;IACvC;IACA;QACEH,OAAOV;QACPe,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAQ;IACxB;IACA;QACEH,OAAOT;QACPc,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAOR;QACPa,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAQ;IACxB;IACA;QACEH,OAAOP;QACPY,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAON;QACPW,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAOL;QACPU,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAOJ;QACPS,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAOH;QACPQ,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;CACD,CAAA;AAED,iDAAiD,GACjD,OAAO,SAASG,qBAAqBN,KAAa;;QACzCI;IAAP,gBAAOA,oCAAAA,4BAA4BG,IAAI,CAAC,CAACC,QAAUA,MAAMR,KAAK,CAACS,EAAE,KAAKT,2BAA/DI,kCAAuEC,SAAS,mBAAI;AAC7F;AAEA,mDAAmD,GACnD,OAAO,MAAMK,uBAAqD;IAChE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-events.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginDomainEvent,\n type PluginDomainEvent,\n type PluginDomainEventDeclaration,\n} from '@aglyn/aglyn/plugin-manager/plugin-domain-events'\n\n/**\n * The order and return events commerce raises (AGL-3611), for other plugins\n * — accounting, shipping, workflows — and for the merchant's own webhooks.\n *\n * Each payload carries `order` in the public API's shape (`GET\n * /v1/sites/{siteId}/orders/{orderId}`), as it stood once the fact was\n * written, so a subscriber reads one documented object rather than the\n * plugin's storage. A subscriber in another plugin restates the fields it\n * reads; these types are this plugin's own.\n */\n\n/** An order as the public API returns it. Money is integer cents. */\nexport interface OrderEventOrder {\n id: string\n object: 'order'\n number: number | null\n status: string | null\n channel: string\n currency: string\n customerEmail: string | null\n customerName: string | null\n lineItems: unknown\n totals: {\n itemsCents: number\n shippingCents: number\n taxCents: number\n discountCents: number\n totalCents: number | null\n feeCents: number\n }\n refundedCents: number\n /** The tax regime: `stripe-automatic`, `manual`, `none`, or `null` when not recorded. */\n taxMode: string | null\n [key: string]: unknown\n}\n\n/** A shipment as an event carries it. */\nexport interface OrderEventFulfillment {\n id: string\n lines: Array<{ lineItemId: number; quantity: number }>\n carrier: string | null\n trackingNumber: string | null\n trackingUrl: string | null\n labelUrl: string | null\n /** What the shipment cost to send — its label's cost, or the merchant's hand-entered one (AGL-3705) — in integer cents of the order's currency; `null` when unknown (AGL-3693). */\n labelCostCents: number | null\n at: string\n}\n\n/** A refund as an event carries it: what THIS refund moved. */\nexport interface OrderEventRefund {\n /** Stripe's refund id, when there is one. */\n id: string | null\n amountCents: number\n /** The line indexes refunded by name; empty for an amount-only refund. */\n lineItemIds: number[]\n /** Whether this refund left nothing more to refund. */\n full: boolean\n}\n\n/** A return as an event carries it. */\nexport interface OrderEventReturn {\n id: string\n status: string\n lines: Array<{ lineItemId: number; quantity: number; reason: string }>\n refundCents: number | null\n /** What went back in stock when the parcel arrived; `null` before then. */\n restock?: Array<{ lineItemId: number; quantity: number }> | null\n}\n\nexport interface OrderEventPayload {\n order: OrderEventOrder\n}\nexport interface OrderFulfilledEventPayload extends OrderEventPayload {\n fulfillment: OrderEventFulfillment\n}\nexport interface OrderRefundedEventPayload extends OrderEventPayload {\n refund: OrderEventRefund\n}\nexport interface ReturnEventPayload extends OrderEventPayload {\n return: OrderEventReturn\n}\n\nexport const ORDER_PAID_EVENT = definePluginDomainEvent<OrderEventPayload>('order.paid')\nexport const ORDER_FULFILLED_EVENT = definePluginDomainEvent<OrderFulfilledEventPayload>('order.fulfilled')\nexport const ORDER_DELIVERED_EVENT = definePluginDomainEvent<OrderEventPayload>('order.delivered')\nexport const ORDER_REFUNDED_EVENT = definePluginDomainEvent<OrderRefundedEventPayload>('order.refunded')\nexport const ORDER_CANCELLED_EVENT = definePluginDomainEvent<OrderEventPayload>('order.cancelled')\nexport const RETURN_REQUESTED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.requested')\nexport const RETURN_APPROVED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.approved')\nexport const RETURN_DECLINED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.declined')\nexport const RETURN_RECEIVED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.received')\nexport const RETURN_REFUNDED_EVENT = definePluginDomainEvent<ReturnEventPayload>('return.refunded')\n\n/**\n * A checkout a shopper reached with an address (AGL-3639): the fact an\n * abandoned-cart flow in a merchant's own email platform starts from. Not an\n * order event — nothing was bought — so it is raised beside them rather than\n * among them: it is not a workflow trigger and not offered to the merchant's\n * outbound webhooks, whose events are about orders.\n */\nexport interface CheckoutStartedEventPayload {\n checkout: {\n /** The checkout session's id: one event per checkout. */\n id: string\n email: string\n /** Whether the shopper ticked the store's marketing box at checkout. */\n marketingOptIn: boolean\n currency: string\n /** The basket's list value, integer cents, before discounts and shipping. */\n itemsCents: number\n /** Where the shopper can pick the checkout up again. */\n resumeUrl: string | null\n items: Array<{\n productId: string\n variantId: string | null\n name: string\n sku: string | null\n quantity: number\n unitCents: number\n }>\n }\n}\n\nexport const CHECKOUT_STARTED_EVENT = definePluginDomainEvent<CheckoutStartedEventPayload>('checkout.started')\n\nexport const CHECKOUT_STARTED_EVENT_DECLARATION: PluginDomainEventDeclaration = {\n event: CHECKOUT_STARTED_EVENT as PluginDomainEvent<unknown>,\n label: 'Checkout started',\n description:\n 'A shopper reached checkout with an email address: the basket, its value and the link back to it. Raised once per checkout, whether or not it is paid.',\n payloadKeys: ['checkout'],\n}\n\n/** Every event commerce raises, by name. */\nexport type CommerceEventName =\n | 'order.paid'\n | 'order.fulfilled'\n | 'order.delivered'\n | 'order.refunded'\n | 'order.cancelled'\n | 'return.requested'\n | 'return.approved'\n | 'return.declined'\n | 'return.received'\n | 'return.refunded'\n\n/** The events, in the order pickers list them, with their words. */\nexport const COMMERCE_EVENT_DECLARATIONS: ReadonlyArray<\n PluginDomainEventDeclaration & { event: PluginDomainEvent<unknown>; hostEvent: string }\n> = [\n {\n event: ORDER_PAID_EVENT,\n hostEvent: 'orderPaid',\n label: 'Order paid',\n description:\n 'An order was paid: a storefront checkout, a buy-now, a paid payment link, a POS sale or a subscription renewal.',\n payloadKeys: ['order'],\n },\n {\n event: ORDER_FULFILLED_EVENT,\n hostEvent: 'orderFulfilled',\n label: 'Order fulfilled',\n description: 'A shipment was recorded on an order, once per shipment, with the units it carried.',\n payloadKeys: ['order', 'fulfillment'],\n },\n {\n event: ORDER_DELIVERED_EVENT,\n hostEvent: 'orderDelivered',\n label: 'Order delivered',\n description: 'An order was marked delivered.',\n payloadKeys: ['order'],\n },\n {\n event: ORDER_REFUNDED_EVENT,\n hostEvent: 'orderRefunded',\n label: 'Order refunded',\n description: 'Money went back to the buyer, in part or in full, once per refund.',\n payloadKeys: ['order', 'refund'],\n },\n {\n event: ORDER_CANCELLED_EVENT,\n hostEvent: 'orderCancelled',\n label: 'Order canceled',\n description: 'An order was canceled and its stock returned.',\n payloadKeys: ['order'],\n },\n {\n event: RETURN_REQUESTED_EVENT,\n hostEvent: 'returnRequested',\n label: 'Return requested',\n description: 'A buyer asked to return items, or the merchant opened a return for them.',\n payloadKeys: ['order', 'return'],\n },\n {\n event: RETURN_APPROVED_EVENT,\n hostEvent: 'returnApproved',\n label: 'Return approved',\n description: 'The store approved a return, or opened one itself.',\n payloadKeys: ['order', 'return'],\n },\n {\n event: RETURN_DECLINED_EVENT,\n hostEvent: 'returnDeclined',\n label: 'Return declined',\n description: 'The store declined a return request.',\n payloadKeys: ['order', 'return'],\n },\n {\n event: RETURN_RECEIVED_EVENT,\n hostEvent: 'returnReceived',\n label: 'Return received',\n description: 'The returned items arrived, with what went back in stock.',\n payloadKeys: ['order', 'return'],\n },\n {\n event: RETURN_REFUNDED_EVENT,\n hostEvent: 'returnRefunded',\n label: 'Return refunded',\n description: 'A return was refunded.',\n payloadKeys: ['order', 'return'],\n },\n]\n\n/** The workflow trigger an event is offered as. */\nexport function commerceHostEventFor(event: string): string | null {\n return COMMERCE_EVENT_DECLARATIONS.find((entry) => entry.event.id === event)?.hostEvent ?? null\n}\n\n/** The event names, for a webhook's event picker. */\nexport const COMMERCE_EVENT_NAMES: readonly CommerceEventName[] = [\n 'order.paid',\n 'order.fulfilled',\n 'order.delivered',\n 'order.refunded',\n 'order.cancelled',\n 'return.requested',\n 'return.approved',\n 'return.declined',\n 'return.received',\n 'return.refunded',\n]\n"],"names":["definePluginDomainEvent","ORDER_PAID_EVENT","ORDER_FULFILLED_EVENT","ORDER_DELIVERED_EVENT","ORDER_REFUNDED_EVENT","ORDER_CANCELLED_EVENT","RETURN_REQUESTED_EVENT","RETURN_APPROVED_EVENT","RETURN_DECLINED_EVENT","RETURN_RECEIVED_EVENT","RETURN_REFUNDED_EVENT","CHECKOUT_STARTED_EVENT","CHECKOUT_STARTED_EVENT_DECLARATION","event","label","description","payloadKeys","COMMERCE_EVENT_DECLARATIONS","hostEvent","commerceHostEventFor","find","entry","id","COMMERCE_EVENT_NAMES"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,uBAAuB,QAGlB,mDAAkD;AAqFzD,OAAO,MAAMC,mBAAmBD,wBAA2C,cAAa;AACxF,OAAO,MAAME,wBAAwBF,wBAAoD,mBAAkB;AAC3G,OAAO,MAAMG,wBAAwBH,wBAA2C,mBAAkB;AAClG,OAAO,MAAMI,uBAAuBJ,wBAAmD,kBAAiB;AACxG,OAAO,MAAMK,wBAAwBL,wBAA2C,mBAAkB;AAClG,OAAO,MAAMM,yBAAyBN,wBAA4C,oBAAmB;AACrG,OAAO,MAAMO,wBAAwBP,wBAA4C,mBAAkB;AACnG,OAAO,MAAMQ,wBAAwBR,wBAA4C,mBAAkB;AACnG,OAAO,MAAMS,wBAAwBT,wBAA4C,mBAAkB;AACnG,OAAO,MAAMU,wBAAwBV,wBAA4C,mBAAkB;AAgCnG,OAAO,MAAMW,yBAAyBX,wBAAqD,oBAAmB;AAE9G,OAAO,MAAMY,qCAAmE;IAC9EC,OAAOF;IACPG,OAAO;IACPC,aACE;IACFC,aAAa;QAAC;KAAW;AAC3B,EAAC;AAeD,kEAAkE,GAClE,OAAO,MAAMC,8BAET;IACF;QACEJ,OAAOZ;QACPiB,WAAW;QACXJ,OAAO;QACPC,aACE;QACFC,aAAa;YAAC;SAAQ;IACxB;IACA;QACEH,OAAOX;QACPgB,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAc;IACvC;IACA;QACEH,OAAOV;QACPe,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAQ;IACxB;IACA;QACEH,OAAOT;QACPc,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAOR;QACPa,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAQ;IACxB;IACA;QACEH,OAAOP;QACPY,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAON;QACPW,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAOL;QACPU,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAOJ;QACPS,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;IACA;QACEH,OAAOH;QACPQ,WAAW;QACXJ,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;YAAS;SAAS;IAClC;CACD,CAAA;AAED,iDAAiD,GACjD,OAAO,SAASG,qBAAqBN,KAAa;;QACzCI;IAAP,gBAAOA,oCAAAA,4BAA4BG,IAAI,CAAC,CAACC,QAAUA,MAAMR,KAAK,CAACS,EAAE,KAAKT,2BAA/DI,kCAAuEC,SAAS,mBAAI;AAC7F;AAEA,mDAAmD,GACnD,OAAO,MAAMK,uBAAqD;IAChE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA"}
@@ -115,3 +115,32 @@ export declare function describeFulfillmentLines(order: Pick<HostOrder, 'lineIte
115
115
  export declare const FULFILLMENT_CARRIER_CHOICES: readonly string[];
116
116
  /** The words for a fulfillment's `trackingStatus`, or the status itself. */
117
117
  export declare function trackingStatusLabel(status: string | undefined | null): string;
118
+ /**
119
+ * The most a shipment's hand-entered shipping cost may be, in minor units of
120
+ * the store's currency (AGL-3705): $10,000.00, or ¥1,000,000. A typo guard,
121
+ * not a policy — a parcel that costs more is freight, not a shipment.
122
+ */
123
+ export declare const SHIPPING_COST_MAX_CENTS = 1000000;
124
+ /**
125
+ * How many minor units a currency has (2 for USD, 0 for JPY), from the
126
+ * platform's own currency data; 2 for a code it does not know.
127
+ */
128
+ export declare function currencyMinorDigits(currency: string | null | undefined): number;
129
+ /**
130
+ * Why a shipping cost a door was handed cannot be stored (AGL-3705), or null
131
+ * when it can. Absent (`undefined`, `null`, `''`) is fine — it means "not
132
+ * known", which is not zero. A present cost is whole minor units, from zero
133
+ * (free shipping, which is said) to {@link SHIPPING_COST_MAX_CENTS}.
134
+ */
135
+ export declare function shippingCostCentsProblem(value: unknown): string | null;
136
+ /**
137
+ * What a merchant typed in the fulfill dialog's Shipping cost field, read in
138
+ * the store's currency (AGL-3705): `12`, `12.5`, `$12.50` and `1,250.00` are
139
+ * amounts; an empty field is no cost at all (`cents: null`), which is not 0.
140
+ * More decimals than the currency has, a negative or a word is an error.
141
+ */
142
+ export declare function parseShippingCostInput(input: string, currency: string | null | undefined): {
143
+ cents: number | null;
144
+ } | {
145
+ error: string;
146
+ };
@@ -213,5 +213,68 @@ const wholeUnits = (value)=>{
213
213
  var _TRACKING_STATUS_LABELS_status;
214
214
  return status ? (_TRACKING_STATUS_LABELS_status = TRACKING_STATUS_LABELS[status]) != null ? _TRACKING_STATUS_LABELS_status : status : '';
215
215
  }
216
+ /**
217
+ * The most a shipment's hand-entered shipping cost may be, in minor units of
218
+ * the store's currency (AGL-3705): $10,000.00, or ¥1,000,000. A typo guard,
219
+ * not a policy — a parcel that costs more is freight, not a shipment.
220
+ */ export const SHIPPING_COST_MAX_CENTS = 1000000;
221
+ /**
222
+ * How many minor units a currency has (2 for USD, 0 for JPY), from the
223
+ * platform's own currency data; 2 for a code it does not know.
224
+ */ export function currencyMinorDigits(currency) {
225
+ try {
226
+ var _format_resolvedOptions_maximumFractionDigits;
227
+ const format = new Intl.NumberFormat('en-US', {
228
+ style: 'currency',
229
+ currency: String(currency || 'USD').toUpperCase()
230
+ });
231
+ return (_format_resolvedOptions_maximumFractionDigits = format.resolvedOptions().maximumFractionDigits) != null ? _format_resolvedOptions_maximumFractionDigits : 2;
232
+ } catch (unused) {
233
+ return 2;
234
+ }
235
+ }
236
+ /**
237
+ * Why a shipping cost a door was handed cannot be stored (AGL-3705), or null
238
+ * when it can. Absent (`undefined`, `null`, `''`) is fine — it means "not
239
+ * known", which is not zero. A present cost is whole minor units, from zero
240
+ * (free shipping, which is said) to {@link SHIPPING_COST_MAX_CENTS}.
241
+ */ export function shippingCostCentsProblem(value) {
242
+ if (value === undefined || value === null || value === '') return null;
243
+ if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
244
+ return 'Shipping cost must be a whole number of cents';
245
+ }
246
+ if (value < 0) return 'Shipping cost cannot be negative';
247
+ if (value > SHIPPING_COST_MAX_CENTS) return 'Shipping cost is too large';
248
+ return null;
249
+ }
250
+ /**
251
+ * What a merchant typed in the fulfill dialog's Shipping cost field, read in
252
+ * the store's currency (AGL-3705): `12`, `12.5`, `$12.50` and `1,250.00` are
253
+ * amounts; an empty field is no cost at all (`cents: null`), which is not 0.
254
+ * More decimals than the currency has, a negative or a word is an error.
255
+ */ export function parseShippingCostInput(input, currency) {
256
+ const digits = currencyMinorDigits(currency);
257
+ const cleaned = String(input != null ? input : '').trim().replace(/^[^\d.-]+/, '').replace(/,/g, '');
258
+ if (!String(input != null ? input : '').trim()) return {
259
+ cents: null
260
+ };
261
+ if (cleaned.startsWith('-')) return {
262
+ error: 'Shipping cost cannot be negative'
263
+ };
264
+ const pattern = digits > 0 ? new RegExp(`^\\d*(\\.\\d{0,${digits}})?$`) : /^\d+$/;
265
+ if (!cleaned || cleaned === '.' || !pattern.test(cleaned)) {
266
+ return {
267
+ error: digits > 0 ? `Enter an amount like 8.45` : 'Enter a whole amount, like 850'
268
+ };
269
+ }
270
+ const [whole, fraction = ''] = cleaned.split('.');
271
+ const cents = Number(whole || '0') * 10 ** digits + Number(fraction.padEnd(digits, '0') || '0');
272
+ const problem = shippingCostCentsProblem(cents);
273
+ return problem ? {
274
+ error: problem
275
+ } : {
276
+ cents
277
+ };
278
+ }
216
279
 
217
280
  //# sourceMappingURL=order-fulfillment.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-fulfillment.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Quantity-level fulfillment (AGL-3611).\n *\n * A fulfillment used to name whole lines (`lineItemIds`), so \"ship 2 of the 5\n * mugs now, the rest next week\" could not be said. A fulfillment now carries\n * `lines: [{ lineItemId, quantity }]`; one written before that reads as every\n * unit of each line it named, which is what it meant when it was written.\n *\n * Every figure here is computed from the order's own fulfillments, so the\n * server's transaction and the console's panel ask the same question of the\n * same record and cannot disagree about what is left to ship.\n */\n\nimport type {\n HostOrder,\n OrderFulfillment,\n OrderLineItem,\n OrderStatus,\n} from './commerce-orders'\n\n/** One line of a fulfillment: which order line, and how many of its units. */\nexport interface OrderFulfillmentLine {\n /** The line's index in `order.lineItems`. */\n lineItemId: number\n quantity: number\n}\n\n/** A fulfillment's lifecycle; absent reads as `active`. */\nexport type OrderFulfillmentStatus = 'active' | 'cancelled'\n\n/**\n * Whether a line has to be shipped at all. Digital goods and services are\n * delivered by other means, so they never hold an order in\n * `partially_fulfilled`. A line with no recorded type predates types and is\n * treated as physical, the only reading that cannot close an order early.\n */\nexport function lineRequiresShipping(line: Pick<OrderLineItem, 'productType'>): boolean {\n return line.productType !== 'digital' && line.productType !== 'service'\n}\n\nconst wholeUnits = (value: unknown): number => {\n const units = Math.floor(Number(value ?? 0))\n return Number.isFinite(units) && units > 0 ? units : 0\n}\n\n/** Whether a fulfillment still counts: a cancelled one ships nothing. */\nexport function fulfillmentIsActive(fulfillment: Partial<OrderFulfillment> | null | undefined): boolean {\n return Boolean(fulfillment) && fulfillment?.status !== 'cancelled'\n}\n\n/**\n * The units one fulfillment covers, per line. A legacy fulfillment (no\n * `lines`) covers every unit of each line it names.\n */\nexport function fulfillmentLineQuantities(\n order: Pick<HostOrder, 'lineItems'>,\n fulfillment: Partial<OrderFulfillment>,\n): OrderFulfillmentLine[] {\n const lines = order.lineItems ?? []\n if (Array.isArray(fulfillment.lines) && fulfillment.lines.length > 0) {\n return fulfillment.lines\n .map((entry) => ({\n lineItemId: Math.round(Number(entry?.lineItemId)),\n quantity: wholeUnits(entry?.quantity),\n }))\n .filter(\n (entry) =>\n Number.isInteger(entry.lineItemId) &&\n entry.lineItemId >= 0 &&\n entry.lineItemId < lines.length &&\n entry.quantity > 0,\n )\n }\n return (fulfillment.lineItemIds ?? [])\n .map((index) => Math.round(Number(index)))\n .filter((index) => Number.isInteger(index) && index >= 0 && index < lines.length)\n .map((index) => ({ lineItemId: index, quantity: wholeUnits(lines[index]?.quantity) }))\n .filter((entry) => entry.quantity > 0)\n}\n\n/** Units already fulfilled per line index, over every active fulfillment. */\nexport function fulfilledQuantities(order: Pick<HostOrder, 'lineItems' | 'fulfillments'>): Map<number, number> {\n const totals = new Map<number, number>()\n for (const fulfillment of order.fulfillments ?? []) {\n if (!fulfillmentIsActive(fulfillment)) continue\n for (const entry of fulfillmentLineQuantities(order, fulfillment)) {\n totals.set(entry.lineItemId, (totals.get(entry.lineItemId) ?? 0) + entry.quantity)\n }\n }\n return totals\n}\n\n/** A line as the fulfillment panel and the server both reason about it. */\nexport interface OrderLineFulfillmentState {\n lineItemId: number\n quantity: number\n fulfilledQuantity: number\n remainingQuantity: number\n requiresShipping: boolean\n}\n\n/** Every line's fulfilled and remaining units. */\nexport function orderLineFulfillmentStates(\n order: Pick<HostOrder, 'lineItems' | 'fulfillments'>,\n): OrderLineFulfillmentState[] {\n const fulfilled = fulfilledQuantities(order)\n return (order.lineItems ?? []).map((line, index) => {\n const quantity = wholeUnits(line.quantity)\n const done = Math.min(quantity, fulfilled.get(index) ?? 0)\n return {\n lineItemId: index,\n quantity,\n fulfilledQuantity: done,\n remainingQuantity: quantity - done,\n requiresShipping: lineRequiresShipping(line),\n }\n })\n}\n\n/**\n * What is left to ship: every shippable line's remaining units. This is what\n * a \"fulfill everything\" request covers.\n */\nexport function remainingFulfillmentLines(\n order: Pick<HostOrder, 'lineItems' | 'fulfillments'>,\n): OrderFulfillmentLine[] {\n return orderLineFulfillmentStates(order)\n .filter((state) => state.requiresShipping && state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n}\n\n/** Why a requested set of fulfillment lines was refused. */\nexport type FulfillmentLinesProblem =\n | { problem: 'empty' }\n | { problem: 'unknown_line'; lineItemId: number }\n | { problem: 'bad_quantity'; lineItemId: number }\n | { problem: 'over_fulfilled'; lineItemId: number; remaining: number; requested: number }\n\n/**\n * Validates and merges a requested set of lines against what is left.\n *\n * Duplicate entries for one line are summed before the check, so a request\n * cannot slip past the remaining count by naming the line twice. A digital\n * or service line may be fulfilled (a merchant marking a service done), but\n * never beyond its quantity.\n */\nexport function resolveFulfillmentLines(\n order: Pick<HostOrder, 'lineItems' | 'fulfillments'>,\n requested: ReadonlyArray<{ lineItemId: unknown; quantity: unknown }>,\n): { lines: OrderFulfillmentLine[] } | FulfillmentLinesProblem {\n const states = orderLineFulfillmentStates(order)\n const merged = new Map<number, number>()\n for (const entry of requested) {\n const lineItemId = Number(entry?.lineItemId)\n if (!Number.isInteger(lineItemId) || lineItemId < 0 || lineItemId >= states.length) {\n return { problem: 'unknown_line', lineItemId: Number.isFinite(lineItemId) ? lineItemId : -1 }\n }\n const quantity = Number(entry?.quantity)\n if (!Number.isInteger(quantity) || quantity <= 0) {\n return { problem: 'bad_quantity', lineItemId }\n }\n merged.set(lineItemId, (merged.get(lineItemId) ?? 0) + quantity)\n }\n if (merged.size === 0) return { problem: 'empty' }\n const lines: OrderFulfillmentLine[] = []\n for (const [lineItemId, quantity] of [...merged.entries()].sort((a, b) => a[0] - b[0])) {\n const remaining = states[lineItemId].remainingQuantity\n if (quantity > remaining) {\n return { problem: 'over_fulfilled', lineItemId, remaining, requested: quantity }\n }\n lines.push({ lineItemId, quantity })\n }\n return { lines }\n}\n\n/** Words for a refused request, for the console and the API. */\nexport function describeFulfillmentLinesProblem(problem: FulfillmentLinesProblem): string {\n switch (problem.problem) {\n case 'empty':\n return 'Choose at least one item to fulfill'\n case 'unknown_line':\n return `Line ${problem.lineItemId} is not on this order`\n case 'bad_quantity':\n return `The quantity for line ${problem.lineItemId} must be a whole number above zero`\n case 'over_fulfilled':\n return problem.remaining === 0\n ? `Line ${problem.lineItemId} is already fully fulfilled`\n : `Line ${problem.lineItemId} has only ${problem.remaining} left to fulfill, not ${problem.requested}`\n }\n}\n\n/**\n * The status an order's fulfillments put it in: `fulfilled` once every\n * shippable unit is covered, `partially_fulfilled` while some are, and\n * `paid` when none are. An order with nothing to ship reads as `fulfilled`\n * once anything at all was recorded against it, and as `paid` before.\n */\nexport function statusFromFulfillments(\n order: Pick<HostOrder, 'lineItems' | 'fulfillments'>,\n): Extract<OrderStatus, 'paid' | 'partially_fulfilled' | 'fulfilled'> {\n const states = orderLineFulfillmentStates(order)\n const shippable = states.filter((state) => state.requiresShipping)\n const anything = states.some((state) => state.fulfilledQuantity > 0)\n if (shippable.length === 0) return anything ? 'fulfilled' : 'paid'\n if (shippable.every((state) => state.remainingQuantity === 0)) return 'fulfilled'\n return shippable.some((state) => state.fulfilledQuantity > 0) || anything\n ? 'partially_fulfilled'\n : 'paid'\n}\n\n/** Statuses whose fulfillments may still be added to, edited or cancelled. */\nconst FULFILLMENT_OPEN_STATUSES: readonly OrderStatus[] = ['paid', 'partially_fulfilled', 'fulfilled']\n\n/**\n * Whether a fulfillment on this order may be edited or cancelled. A delivered,\n * refunded or cancelled order is history: its fulfillments are what happened.\n */\nexport function orderFulfillmentsEditable(order: Pick<HostOrder, 'status'>): boolean {\n return FULFILLMENT_OPEN_STATUSES.includes(order.status)\n}\n\n/** A short \"2× Mug, 1× Shirt\" for a fulfillment, for timelines and emails. */\nexport function describeFulfillmentLines(\n order: Pick<HostOrder, 'lineItems'>,\n lines: readonly OrderFulfillmentLine[],\n): string {\n return lines\n .map((entry) => `${entry.quantity}× ${order.lineItems?.[entry.lineItemId]?.name ?? `line ${entry.lineItemId}`}`)\n .join(', ')\n}\n\n/** The carriers the console offers by name; anything else is typed under Other. */\nexport const FULFILLMENT_CARRIER_CHOICES: readonly string[] = [\n 'USPS',\n 'UPS',\n 'FedEx',\n 'DHL',\n 'Canada Post',\n 'Royal Mail',\n 'Australia Post',\n]\n\n/** What a merchant reads for a carrier's tracking status (AGL-3612). */\nconst TRACKING_STATUS_LABELS: Readonly<Record<string, string>> = {\n pre_transit: 'Label created',\n in_transit: 'In transit',\n out_for_delivery: 'Out for delivery',\n delivered: 'Delivered',\n exception: 'Delivery problem',\n returned: 'Returned to sender',\n}\n\n/** The words for a fulfillment's `trackingStatus`, or the status itself. */\nexport function trackingStatusLabel(status: string | undefined | null): string {\n return status ? TRACKING_STATUS_LABELS[status] ?? status : ''\n}\n"],"names":["lineRequiresShipping","line","productType","wholeUnits","value","units","Math","floor","Number","isFinite","fulfillmentIsActive","fulfillment","Boolean","status","fulfillmentLineQuantities","order","lines","lineItems","Array","isArray","length","map","entry","lineItemId","round","quantity","filter","isInteger","lineItemIds","index","fulfilledQuantities","totals","Map","fulfillments","set","get","orderLineFulfillmentStates","fulfilled","done","min","fulfilledQuantity","remainingQuantity","requiresShipping","remainingFulfillmentLines","state","resolveFulfillmentLines","requested","states","merged","problem","size","entries","sort","a","b","remaining","push","describeFulfillmentLinesProblem","statusFromFulfillments","shippable","anything","some","every","FULFILLMENT_OPEN_STATUSES","orderFulfillmentsEditable","includes","describeFulfillmentLines","name","join","FULFILLMENT_CARRIER_CHOICES","TRACKING_STATUS_LABELS","pre_transit","in_transit","out_for_delivery","delivered","exception","returned","trackingStatusLabel"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-fulfillment.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Quantity-level fulfillment (AGL-3611).\n *\n * A fulfillment used to name whole lines (`lineItemIds`), so \"ship 2 of the 5\n * mugs now, the rest next week\" could not be said. A fulfillment now carries\n * `lines: [{ lineItemId, quantity }]`; one written before that reads as every\n * unit of each line it named, which is what it meant when it was written.\n *\n * Every figure here is computed from the order's own fulfillments, so the\n * server's transaction and the console's panel ask the same question of the\n * same record and cannot disagree about what is left to ship.\n */\n\nimport type {\n HostOrder,\n OrderFulfillment,\n OrderLineItem,\n OrderStatus,\n} from './commerce-orders'\n\n/** One line of a fulfillment: which order line, and how many of its units. */\nexport interface OrderFulfillmentLine {\n /** The line's index in `order.lineItems`. */\n lineItemId: number\n quantity: number\n}\n\n/** A fulfillment's lifecycle; absent reads as `active`. */\nexport type OrderFulfillmentStatus = 'active' | 'cancelled'\n\n/**\n * Whether a line has to be shipped at all. Digital goods and services are\n * delivered by other means, so they never hold an order in\n * `partially_fulfilled`. A line with no recorded type predates types and is\n * treated as physical, the only reading that cannot close an order early.\n */\nexport function lineRequiresShipping(line: Pick<OrderLineItem, 'productType'>): boolean {\n return line.productType !== 'digital' && line.productType !== 'service'\n}\n\nconst wholeUnits = (value: unknown): number => {\n const units = Math.floor(Number(value ?? 0))\n return Number.isFinite(units) && units > 0 ? units : 0\n}\n\n/** Whether a fulfillment still counts: a cancelled one ships nothing. */\nexport function fulfillmentIsActive(fulfillment: Partial<OrderFulfillment> | null | undefined): boolean {\n return Boolean(fulfillment) && fulfillment?.status !== 'cancelled'\n}\n\n/**\n * The units one fulfillment covers, per line. A legacy fulfillment (no\n * `lines`) covers every unit of each line it names.\n */\nexport function fulfillmentLineQuantities(\n order: Pick<HostOrder, 'lineItems'>,\n fulfillment: Partial<OrderFulfillment>,\n): OrderFulfillmentLine[] {\n const lines = order.lineItems ?? []\n if (Array.isArray(fulfillment.lines) && fulfillment.lines.length > 0) {\n return fulfillment.lines\n .map((entry) => ({\n lineItemId: Math.round(Number(entry?.lineItemId)),\n quantity: wholeUnits(entry?.quantity),\n }))\n .filter(\n (entry) =>\n Number.isInteger(entry.lineItemId) &&\n entry.lineItemId >= 0 &&\n entry.lineItemId < lines.length &&\n entry.quantity > 0,\n )\n }\n return (fulfillment.lineItemIds ?? [])\n .map((index) => Math.round(Number(index)))\n .filter((index) => Number.isInteger(index) && index >= 0 && index < lines.length)\n .map((index) => ({ lineItemId: index, quantity: wholeUnits(lines[index]?.quantity) }))\n .filter((entry) => entry.quantity > 0)\n}\n\n/** Units already fulfilled per line index, over every active fulfillment. */\nexport function fulfilledQuantities(order: Pick<HostOrder, 'lineItems' | 'fulfillments'>): Map<number, number> {\n const totals = new Map<number, number>()\n for (const fulfillment of order.fulfillments ?? []) {\n if (!fulfillmentIsActive(fulfillment)) continue\n for (const entry of fulfillmentLineQuantities(order, fulfillment)) {\n totals.set(entry.lineItemId, (totals.get(entry.lineItemId) ?? 0) + entry.quantity)\n }\n }\n return totals\n}\n\n/** A line as the fulfillment panel and the server both reason about it. */\nexport interface OrderLineFulfillmentState {\n lineItemId: number\n quantity: number\n fulfilledQuantity: number\n remainingQuantity: number\n requiresShipping: boolean\n}\n\n/** Every line's fulfilled and remaining units. */\nexport function orderLineFulfillmentStates(\n order: Pick<HostOrder, 'lineItems' | 'fulfillments'>,\n): OrderLineFulfillmentState[] {\n const fulfilled = fulfilledQuantities(order)\n return (order.lineItems ?? []).map((line, index) => {\n const quantity = wholeUnits(line.quantity)\n const done = Math.min(quantity, fulfilled.get(index) ?? 0)\n return {\n lineItemId: index,\n quantity,\n fulfilledQuantity: done,\n remainingQuantity: quantity - done,\n requiresShipping: lineRequiresShipping(line),\n }\n })\n}\n\n/**\n * What is left to ship: every shippable line's remaining units. This is what\n * a \"fulfill everything\" request covers.\n */\nexport function remainingFulfillmentLines(\n order: Pick<HostOrder, 'lineItems' | 'fulfillments'>,\n): OrderFulfillmentLine[] {\n return orderLineFulfillmentStates(order)\n .filter((state) => state.requiresShipping && state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n}\n\n/** Why a requested set of fulfillment lines was refused. */\nexport type FulfillmentLinesProblem =\n | { problem: 'empty' }\n | { problem: 'unknown_line'; lineItemId: number }\n | { problem: 'bad_quantity'; lineItemId: number }\n | { problem: 'over_fulfilled'; lineItemId: number; remaining: number; requested: number }\n\n/**\n * Validates and merges a requested set of lines against what is left.\n *\n * Duplicate entries for one line are summed before the check, so a request\n * cannot slip past the remaining count by naming the line twice. A digital\n * or service line may be fulfilled (a merchant marking a service done), but\n * never beyond its quantity.\n */\nexport function resolveFulfillmentLines(\n order: Pick<HostOrder, 'lineItems' | 'fulfillments'>,\n requested: ReadonlyArray<{ lineItemId: unknown; quantity: unknown }>,\n): { lines: OrderFulfillmentLine[] } | FulfillmentLinesProblem {\n const states = orderLineFulfillmentStates(order)\n const merged = new Map<number, number>()\n for (const entry of requested) {\n const lineItemId = Number(entry?.lineItemId)\n if (!Number.isInteger(lineItemId) || lineItemId < 0 || lineItemId >= states.length) {\n return { problem: 'unknown_line', lineItemId: Number.isFinite(lineItemId) ? lineItemId : -1 }\n }\n const quantity = Number(entry?.quantity)\n if (!Number.isInteger(quantity) || quantity <= 0) {\n return { problem: 'bad_quantity', lineItemId }\n }\n merged.set(lineItemId, (merged.get(lineItemId) ?? 0) + quantity)\n }\n if (merged.size === 0) return { problem: 'empty' }\n const lines: OrderFulfillmentLine[] = []\n for (const [lineItemId, quantity] of [...merged.entries()].sort((a, b) => a[0] - b[0])) {\n const remaining = states[lineItemId].remainingQuantity\n if (quantity > remaining) {\n return { problem: 'over_fulfilled', lineItemId, remaining, requested: quantity }\n }\n lines.push({ lineItemId, quantity })\n }\n return { lines }\n}\n\n/** Words for a refused request, for the console and the API. */\nexport function describeFulfillmentLinesProblem(problem: FulfillmentLinesProblem): string {\n switch (problem.problem) {\n case 'empty':\n return 'Choose at least one item to fulfill'\n case 'unknown_line':\n return `Line ${problem.lineItemId} is not on this order`\n case 'bad_quantity':\n return `The quantity for line ${problem.lineItemId} must be a whole number above zero`\n case 'over_fulfilled':\n return problem.remaining === 0\n ? `Line ${problem.lineItemId} is already fully fulfilled`\n : `Line ${problem.lineItemId} has only ${problem.remaining} left to fulfill, not ${problem.requested}`\n }\n}\n\n/**\n * The status an order's fulfillments put it in: `fulfilled` once every\n * shippable unit is covered, `partially_fulfilled` while some are, and\n * `paid` when none are. An order with nothing to ship reads as `fulfilled`\n * once anything at all was recorded against it, and as `paid` before.\n */\nexport function statusFromFulfillments(\n order: Pick<HostOrder, 'lineItems' | 'fulfillments'>,\n): Extract<OrderStatus, 'paid' | 'partially_fulfilled' | 'fulfilled'> {\n const states = orderLineFulfillmentStates(order)\n const shippable = states.filter((state) => state.requiresShipping)\n const anything = states.some((state) => state.fulfilledQuantity > 0)\n if (shippable.length === 0) return anything ? 'fulfilled' : 'paid'\n if (shippable.every((state) => state.remainingQuantity === 0)) return 'fulfilled'\n return shippable.some((state) => state.fulfilledQuantity > 0) || anything\n ? 'partially_fulfilled'\n : 'paid'\n}\n\n/** Statuses whose fulfillments may still be added to, edited or cancelled. */\nconst FULFILLMENT_OPEN_STATUSES: readonly OrderStatus[] = ['paid', 'partially_fulfilled', 'fulfilled']\n\n/**\n * Whether a fulfillment on this order may be edited or cancelled. A delivered,\n * refunded or cancelled order is history: its fulfillments are what happened.\n */\nexport function orderFulfillmentsEditable(order: Pick<HostOrder, 'status'>): boolean {\n return FULFILLMENT_OPEN_STATUSES.includes(order.status)\n}\n\n/** A short \"2× Mug, 1× Shirt\" for a fulfillment, for timelines and emails. */\nexport function describeFulfillmentLines(\n order: Pick<HostOrder, 'lineItems'>,\n lines: readonly OrderFulfillmentLine[],\n): string {\n return lines\n .map((entry) => `${entry.quantity}× ${order.lineItems?.[entry.lineItemId]?.name ?? `line ${entry.lineItemId}`}`)\n .join(', ')\n}\n\n/** The carriers the console offers by name; anything else is typed under Other. */\nexport const FULFILLMENT_CARRIER_CHOICES: readonly string[] = [\n 'USPS',\n 'UPS',\n 'FedEx',\n 'DHL',\n 'Canada Post',\n 'Royal Mail',\n 'Australia Post',\n]\n\n/** What a merchant reads for a carrier's tracking status (AGL-3612). */\nconst TRACKING_STATUS_LABELS: Readonly<Record<string, string>> = {\n pre_transit: 'Label created',\n in_transit: 'In transit',\n out_for_delivery: 'Out for delivery',\n delivered: 'Delivered',\n exception: 'Delivery problem',\n returned: 'Returned to sender',\n}\n\n/** The words for a fulfillment's `trackingStatus`, or the status itself. */\nexport function trackingStatusLabel(status: string | undefined | null): string {\n return status ? TRACKING_STATUS_LABELS[status] ?? status : ''\n}\n\n/**\n * The most a shipment's hand-entered shipping cost may be, in minor units of\n * the store's currency (AGL-3705): $10,000.00, or ¥1,000,000. A typo guard,\n * not a policy — a parcel that costs more is freight, not a shipment.\n */\nexport const SHIPPING_COST_MAX_CENTS = 1_000_000\n\n/**\n * How many minor units a currency has (2 for USD, 0 for JPY), from the\n * platform's own currency data; 2 for a code it does not know.\n */\nexport function currencyMinorDigits(currency: string | null | undefined): number {\n try {\n const format = new Intl.NumberFormat('en-US', { style: 'currency', currency: String(currency || 'USD').toUpperCase() })\n return format.resolvedOptions().maximumFractionDigits ?? 2\n } catch {\n return 2\n }\n}\n\n/**\n * Why a shipping cost a door was handed cannot be stored (AGL-3705), or null\n * when it can. Absent (`undefined`, `null`, `''`) is fine — it means \"not\n * known\", which is not zero. A present cost is whole minor units, from zero\n * (free shipping, which is said) to {@link SHIPPING_COST_MAX_CENTS}.\n */\nexport function shippingCostCentsProblem(value: unknown): string | null {\n if (value === undefined || value === null || value === '') return null\n if (typeof value !== 'number' || !Number.isSafeInteger(value)) {\n return 'Shipping cost must be a whole number of cents'\n }\n if (value < 0) return 'Shipping cost cannot be negative'\n if (value > SHIPPING_COST_MAX_CENTS) return 'Shipping cost is too large'\n return null\n}\n\n/**\n * What a merchant typed in the fulfill dialog's Shipping cost field, read in\n * the store's currency (AGL-3705): `12`, `12.5`, `$12.50` and `1,250.00` are\n * amounts; an empty field is no cost at all (`cents: null`), which is not 0.\n * More decimals than the currency has, a negative or a word is an error.\n */\nexport function parseShippingCostInput(\n input: string,\n currency: string | null | undefined,\n): { cents: number | null } | { error: string } {\n const digits = currencyMinorDigits(currency)\n const cleaned = String(input ?? '')\n .trim()\n .replace(/^[^\\d.-]+/, '')\n .replace(/,/g, '')\n if (!String(input ?? '').trim()) return { cents: null }\n if (cleaned.startsWith('-')) return { error: 'Shipping cost cannot be negative' }\n const pattern = digits > 0 ? new RegExp(`^\\\\d*(\\\\.\\\\d{0,${digits}})?$`) : /^\\d+$/\n if (!cleaned || cleaned === '.' || !pattern.test(cleaned)) {\n return {\n error: digits > 0 ? `Enter an amount like 8.45` : 'Enter a whole amount, like 850',\n }\n }\n const [whole, fraction = ''] = cleaned.split('.')\n const cents = Number(whole || '0') * 10 ** digits + Number(fraction.padEnd(digits, '0') || '0')\n const problem = shippingCostCentsProblem(cents)\n return problem ? { error: problem } : { cents }\n}\n"],"names":["lineRequiresShipping","line","productType","wholeUnits","value","units","Math","floor","Number","isFinite","fulfillmentIsActive","fulfillment","Boolean","status","fulfillmentLineQuantities","order","lines","lineItems","Array","isArray","length","map","entry","lineItemId","round","quantity","filter","isInteger","lineItemIds","index","fulfilledQuantities","totals","Map","fulfillments","set","get","orderLineFulfillmentStates","fulfilled","done","min","fulfilledQuantity","remainingQuantity","requiresShipping","remainingFulfillmentLines","state","resolveFulfillmentLines","requested","states","merged","problem","size","entries","sort","a","b","remaining","push","describeFulfillmentLinesProblem","statusFromFulfillments","shippable","anything","some","every","FULFILLMENT_OPEN_STATUSES","orderFulfillmentsEditable","includes","describeFulfillmentLines","name","join","FULFILLMENT_CARRIER_CHOICES","TRACKING_STATUS_LABELS","pre_transit","in_transit","out_for_delivery","delivered","exception","returned","trackingStatusLabel","SHIPPING_COST_MAX_CENTS","currencyMinorDigits","currency","format","Intl","NumberFormat","style","String","toUpperCase","resolvedOptions","maximumFractionDigits","shippingCostCentsProblem","undefined","isSafeInteger","parseShippingCostInput","input","digits","cleaned","trim","replace","cents","startsWith","error","pattern","RegExp","test","whole","fraction","split","padEnd"],"mappings":"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"}
@@ -88,8 +88,44 @@ export interface OrderLocalDelivery {
88
88
  failedAtMs?: number;
89
89
  /** Why a drop failed, in the driver's words. */
90
90
  failedReason?: string;
91
+ /**
92
+ * An outside courier the store sent for this drop (AGL-3695), as the
93
+ * courier plugin last reported it through core's
94
+ * `core.local-delivery-records`. Absent when the store's own driver takes it.
95
+ */
96
+ courier?: OrderLocalDeliveryCourier;
91
97
  updatedAtMs?: number;
92
98
  }
99
+ /** Where an outside courier's run stands. `returning`/`returned`: it is coming back to the store. */
100
+ export type OrderCourierState = 'requested' | 'assigned' | 'at_pickup' | 'picked_up' | 'at_dropoff' | 'delivered' | 'cancelled' | 'returning' | 'returned';
101
+ /**
102
+ * An outside courier's run on a local delivery (AGL-3695): the merchant's own
103
+ * DoorDash Drive account, booked from the console. The courier bills the
104
+ * merchant's account directly, so no fee is kept here.
105
+ */
106
+ export interface OrderLocalDeliveryCourier {
107
+ /** The courier plugin's provider id: `doordash`. */
108
+ provider: string;
109
+ /** `DoorDash`. */
110
+ providerLabel: string;
111
+ /** The courier's reference for the run. */
112
+ deliveryRef: string;
113
+ state: OrderCourierState;
114
+ /** The courier's live tracking page. */
115
+ trackingUrl?: string;
116
+ /** When the courier expects to reach the door. */
117
+ etaMs?: number;
118
+ /** When the courier expects to collect. */
119
+ pickupEtaMs?: number;
120
+ /** Why the run was canceled or came back, in the courier's words. */
121
+ reason?: string;
122
+ /** Booked in the courier's test environment: nobody is coming. */
123
+ testMode?: boolean;
124
+ updatedAtMs: number;
125
+ }
126
+ export declare const ORDER_COURIER_STATE_LABELS: Readonly<Record<OrderCourierState, string>>;
127
+ /** Whether a courier is still working the drop: requested, and neither done nor called off. */
128
+ export declare function orderCourierIsActive(courier: Pick<OrderLocalDeliveryCourier, 'state'> | null | undefined): boolean;
93
129
  export declare const ORDER_PICKUP_STATUS_LABELS: Readonly<Record<OrderPickupStatus, string>>;
94
130
  export declare const ORDER_LOCAL_DELIVERY_STATUS_LABELS: Readonly<Record<OrderLocalDeliveryStatus, string>>;
95
131
  export declare const ORDER_FULFILLMENT_METHOD_LABELS: Readonly<Record<OrderFulfillmentMethod, string>>;
@@ -24,7 +24,25 @@
24
24
  * (`tools/scripts/mobile-pure-modules.json`): the statuses, their labels, the
25
25
  * transitions and the fields the pickup and delivery lists query by are one
26
26
  * answer on every platform.
27
- */ /** A location's pickup offer (`hosts/{hostId}/locations/{id}.pickup`). */ export const ORDER_PICKUP_STATUS_LABELS = {
27
+ */ /** A location's pickup offer (`hosts/{hostId}/locations/{id}.pickup`). */ export const ORDER_COURIER_STATE_LABELS = {
28
+ requested: 'Courier requested',
29
+ assigned: 'Courier on the way to the store',
30
+ at_pickup: 'Courier at the store',
31
+ picked_up: 'On its way',
32
+ at_dropoff: 'Courier arriving',
33
+ delivered: 'Delivered',
34
+ cancelled: 'Courier canceled',
35
+ returning: 'Coming back to the store',
36
+ returned: 'Returned to the store'
37
+ };
38
+ /** Whether a courier is still working the drop: requested, and neither done nor called off. */ export function orderCourierIsActive(courier) {
39
+ return Boolean(courier) && ![
40
+ 'delivered',
41
+ 'cancelled',
42
+ 'returned'
43
+ ].includes(String(courier == null ? void 0 : courier.state));
44
+ }
45
+ export const ORDER_PICKUP_STATUS_LABELS = {
28
46
  preparing: 'Preparing',
29
47
  ready: 'Ready for pickup',
30
48
  picked_up: 'Picked up'
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-local-fulfillment.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * How an order reaches its buyer when it does not travel by carrier\n * (AGL-3624): picked up at one of the store's locations, or taken there by the\n * store's OWN driver. Marketplace delivery apps (AGL-3644) are a different\n * thing — an outside courier the delivery-apps plugin books — and never write\n * these fields.\n *\n * Pure and import-free, because the native apps read the same order document\n * (`tools/scripts/mobile-pure-modules.json`): the statuses, their labels, the\n * transitions and the fields the pickup and delivery lists query by are one\n * answer on every platform.\n */\n\n/** A location's pickup offer (`hosts/{hostId}/locations/{id}.pickup`). */\nexport interface PickupLocationSettings {\n enabled?: boolean\n /** Pickup hours, one line per set of days: `Mo-Fr 09:00-17:00`. */\n hours?: string\n /** What the buyer does on arrival: \"Ring the bell at the side door\". */\n instructions?: string\n /** How long an order usually takes to prepare, shown to the buyer. */\n readyWithinMinutes?: number\n}\n\n/** How the buyer chose to receive the order. Absent on every order before AGL-3624: shipped. */\nexport type OrderFulfillmentMethod = 'shipping' | 'pickup' | 'local_delivery'\n\n/** Where a pickup order stands: being put together, waiting at the counter, collected. */\nexport type OrderPickupStatus = 'preparing' | 'ready' | 'picked_up'\n\n/** Where the store's own delivery stands. `failed` is a missed drop the store can send out again. */\nexport type OrderLocalDeliveryStatus = 'scheduled' | 'out_for_delivery' | 'delivered' | 'failed'\n\n/**\n * The pickup the buyer chose, copied from the location at checkout so the\n * order says where to go even after the location is renamed or removed.\n */\nexport interface OrderPickup {\n locationId: string\n locationName: string\n /** The location's address on one line, when it has one. */\n address?: string\n /** What to do on arrival, in the store's words. */\n instructions?: string\n /** The location's hours as the store typed them (`Mo-Fr 09:00-17:00`). */\n hours?: string\n status: OrderPickupStatus\n readyAtMs?: number\n pickedUpAtMs?: number\n /** Who collected it, when the person at the counter noted a name. */\n pickedUpBy?: string\n updatedAtMs?: number\n}\n\n/** The local delivery the buyer chose and paid for. */\nexport interface OrderLocalDelivery {\n zoneId: string\n zoneName: string\n /** The fee charged, in cents (the shipping line of the order). */\n feeCents: number\n /** The window the buyer picked, as instants. */\n windowStartMs: number\n windowEndMs: number\n /** The window as the buyer was shown it, in the store's time zone. */\n windowLabel?: string\n /** The postal code the buyer declared at checkout and the zone was matched on. */\n postalCode?: string\n /** The location the order leaves from, when the store named one. */\n locationId?: string\n status: OrderLocalDeliveryStatus\n /**\n * The address the buyer then entered at payment is OUTSIDE the zone the\n * fee was charged for: the store is told, and decides.\n */\n addressOutsideZone?: boolean\n outForDeliveryAtMs?: number\n deliveredAtMs?: number\n failedAtMs?: number\n /** Why a drop failed, in the driver's words. */\n failedReason?: string\n updatedAtMs?: number\n}\n\nexport const ORDER_PICKUP_STATUS_LABELS: Readonly<Record<OrderPickupStatus, string>> = {\n preparing: 'Preparing',\n ready: 'Ready for pickup',\n picked_up: 'Picked up',\n}\n\nexport const ORDER_LOCAL_DELIVERY_STATUS_LABELS: Readonly<Record<OrderLocalDeliveryStatus, string>> = {\n scheduled: 'Scheduled',\n out_for_delivery: 'Out for delivery',\n delivered: 'Delivered',\n failed: 'Delivery failed',\n}\n\nexport const ORDER_FULFILLMENT_METHOD_LABELS: Readonly<Record<OrderFulfillmentMethod, string>> = {\n shipping: 'Shipping',\n pickup: 'Pickup',\n local_delivery: 'Local delivery',\n}\n\n/**\n * The forward moves of a pickup. `ready` → `preparing` is allowed: the\n * merchant marked the wrong order ready and takes it back before anyone is\n * told twice. Nothing leaves `picked_up`: the goods are gone.\n */\nconst PICKUP_TRANSITIONS: Readonly<Record<OrderPickupStatus, readonly OrderPickupStatus[]>> = {\n preparing: ['ready', 'picked_up'],\n ready: ['picked_up', 'preparing'],\n picked_up: [],\n}\n\n/** A failed drop goes out again; a delivered one is history. */\nconst LOCAL_DELIVERY_TRANSITIONS: Readonly<\n Record<OrderLocalDeliveryStatus, readonly OrderLocalDeliveryStatus[]>\n> = {\n scheduled: ['out_for_delivery', 'delivered', 'failed'],\n out_for_delivery: ['delivered', 'failed'],\n failed: ['out_for_delivery', 'delivered'],\n delivered: [],\n}\n\nexport function canTransitionPickup(from: OrderPickupStatus | undefined, to: OrderPickupStatus): boolean {\n return (PICKUP_TRANSITIONS[from ?? 'preparing'] ?? []).includes(to)\n}\n\nexport function canTransitionLocalDelivery(\n from: OrderLocalDeliveryStatus | undefined,\n to: OrderLocalDeliveryStatus,\n): boolean {\n return (LOCAL_DELIVERY_TRANSITIONS[from ?? 'scheduled'] ?? []).includes(to)\n}\n\n/**\n * The key the pickup and delivery lists query on (`fulfillmentKey`): the\n * method and its status in one field, so \"orders ready for pickup\" is ONE\n * equality and every tab of the list is one composite index.\n */\nexport type OrderFulfillmentKey =\n | 'pickup_preparing'\n | 'pickup_ready'\n | 'pickup_picked_up'\n | 'delivery_scheduled'\n | 'delivery_out_for_delivery'\n | 'delivery_delivered'\n | 'delivery_failed'\n\n/** The fields the pickup and delivery lists query, stamped by every writer of either. */\nexport interface OrderLocalFulfillmentListFields {\n fulfillmentKey: OrderFulfillmentKey\n /** The location to prepare it at or send it from; `''` when the store named none. */\n fulfillmentLocationId: string\n /**\n * What the list sorts by, soonest first: a pickup by when it was ordered\n * (first in, first prepared), a delivery by the start of its window.\n */\n fulfillmentDueMs: number\n}\n\nconst PICKUP_STATUSES: readonly OrderPickupStatus[] = ['preparing', 'ready', 'picked_up']\nconst DELIVERY_STATUSES: readonly OrderLocalDeliveryStatus[] = [\n 'scheduled',\n 'out_for_delivery',\n 'delivered',\n 'failed',\n]\n\n/** Reads a stored pickup status, defaulting a malformed one to where every pickup starts. */\nexport function orderPickupStatus(value: unknown): OrderPickupStatus {\n return PICKUP_STATUSES.includes(value as OrderPickupStatus) ? (value as OrderPickupStatus) : 'preparing'\n}\n\nexport function orderLocalDeliveryStatus(value: unknown): OrderLocalDeliveryStatus {\n return DELIVERY_STATUSES.includes(value as OrderLocalDeliveryStatus)\n ? (value as OrderLocalDeliveryStatus)\n : 'scheduled'\n}\n\n/**\n * The list fields for an order, or `null` for one that is shipped: those\n * orders carry none of the three, so neither list ever reads them.\n */\nexport function orderLocalFulfillmentListFields(order: {\n fulfillmentMethod?: OrderFulfillmentMethod | string\n pickup?: Partial<OrderPickup> | null\n localDelivery?: Partial<OrderLocalDelivery> | null\n createdAtMs?: number\n}): OrderLocalFulfillmentListFields | null {\n if (order.fulfillmentMethod === 'pickup' && order.pickup) {\n return {\n fulfillmentKey: `pickup_${orderPickupStatus(order.pickup.status)}` as OrderFulfillmentKey,\n fulfillmentLocationId: String(order.pickup.locationId ?? ''),\n fulfillmentDueMs: Number(order.createdAtMs) || 0,\n }\n }\n if (order.fulfillmentMethod === 'local_delivery' && order.localDelivery) {\n return {\n fulfillmentKey: `delivery_${orderLocalDeliveryStatus(order.localDelivery.status)}` as OrderFulfillmentKey,\n fulfillmentLocationId: String(order.localDelivery.locationId ?? ''),\n fulfillmentDueMs: Number(order.localDelivery.windowStartMs) || Number(order.createdAtMs) || 0,\n }\n }\n return null\n}\n\n/** Whether an order is collected or brought by the store, so no carrier ever ships it. */\nexport function orderIsLocallyFulfilled(order: { fulfillmentMethod?: string } | null | undefined): boolean {\n return order?.fulfillmentMethod === 'pickup' || order?.fulfillmentMethod === 'local_delivery'\n}\n"],"names":["ORDER_PICKUP_STATUS_LABELS","preparing","ready","picked_up","ORDER_LOCAL_DELIVERY_STATUS_LABELS","scheduled","out_for_delivery","delivered","failed","ORDER_FULFILLMENT_METHOD_LABELS","shipping","pickup","local_delivery","PICKUP_TRANSITIONS","LOCAL_DELIVERY_TRANSITIONS","canTransitionPickup","from","to","includes","canTransitionLocalDelivery","PICKUP_STATUSES","DELIVERY_STATUSES","orderPickupStatus","value","orderLocalDeliveryStatus","orderLocalFulfillmentListFields","order","fulfillmentMethod","fulfillmentKey","status","fulfillmentLocationId","String","locationId","fulfillmentDueMs","Number","createdAtMs","localDelivery","windowStartMs","orderIsLocallyFulfilled"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,wEAAwE,GAsExE,OAAO,MAAMA,6BAA0E;IACrFC,WAAW;IACXC,OAAO;IACPC,WAAW;AACb,EAAC;AAED,OAAO,MAAMC,qCAAyF;IACpGC,WAAW;IACXC,kBAAkB;IAClBC,WAAW;IACXC,QAAQ;AACV,EAAC;AAED,OAAO,MAAMC,kCAAoF;IAC/FC,UAAU;IACVC,QAAQ;IACRC,gBAAgB;AAClB,EAAC;AAED;;;;CAIC,GACD,MAAMC,qBAAwF;IAC5FZ,WAAW;QAAC;QAAS;KAAY;IACjCC,OAAO;QAAC;QAAa;KAAY;IACjCC,WAAW,EAAE;AACf;AAEA,8DAA8D,GAC9D,MAAMW,6BAEF;IACFT,WAAW;QAAC;QAAoB;QAAa;KAAS;IACtDC,kBAAkB;QAAC;QAAa;KAAS;IACzCE,QAAQ;QAAC;QAAoB;KAAY;IACzCD,WAAW,EAAE;AACf;AAEA,OAAO,SAASQ,oBAAoBC,IAAmC,EAAEC,EAAqB;QACpFJ;IAAR,OAAO,EAACA,uBAAAA,kBAAkB,CAACG,eAAAA,OAAQ,YAAY,YAAvCH,uBAA2C,EAAE,EAAEK,QAAQ,CAACD;AAClE;AAEA,OAAO,SAASE,2BACdH,IAA0C,EAC1CC,EAA4B;QAEpBH;IAAR,OAAO,EAACA,+BAAAA,0BAA0B,CAACE,eAAAA,OAAQ,YAAY,YAA/CF,+BAAmD,EAAE,EAAEI,QAAQ,CAACD;AAC1E;AA4BA,MAAMG,kBAAgD;IAAC;IAAa;IAAS;CAAY;AACzF,MAAMC,oBAAyD;IAC7D;IACA;IACA;IACA;CACD;AAED,2FAA2F,GAC3F,OAAO,SAASC,kBAAkBC,KAAc;IAC9C,OAAOH,gBAAgBF,QAAQ,CAACK,SAA+BA,QAA8B;AAC/F;AAEA,OAAO,SAASC,yBAAyBD,KAAc;IACrD,OAAOF,kBAAkBH,QAAQ,CAACK,SAC7BA,QACD;AACN;AAEA;;;CAGC,GACD,OAAO,SAASE,gCAAgCC,KAK/C;IACC,IAAIA,MAAMC,iBAAiB,KAAK,YAAYD,MAAMf,MAAM,EAAE;YAGxBe;QAFhC,OAAO;YACLE,gBAAgB,CAAC,OAAO,EAAEN,kBAAkBI,MAAMf,MAAM,CAACkB,MAAM,GAAG;YAClEC,uBAAuBC,QAAOL,2BAAAA,MAAMf,MAAM,CAACqB,UAAU,YAAvBN,2BAA2B;YACzDO,kBAAkBC,OAAOR,MAAMS,WAAW,KAAK;QACjD;IACF;IACA,IAAIT,MAAMC,iBAAiB,KAAK,oBAAoBD,MAAMU,aAAa,EAAE;YAGvCV;QAFhC,OAAO;YACLE,gBAAgB,CAAC,SAAS,EAAEJ,yBAAyBE,MAAMU,aAAa,CAACP,MAAM,GAAG;YAClFC,uBAAuBC,QAAOL,kCAAAA,MAAMU,aAAa,CAACJ,UAAU,YAA9BN,kCAAkC;YAChEO,kBAAkBC,OAAOR,MAAMU,aAAa,CAACC,aAAa,KAAKH,OAAOR,MAAMS,WAAW,KAAK;QAC9F;IACF;IACA,OAAO;AACT;AAEA,wFAAwF,GACxF,OAAO,SAASG,wBAAwBZ,KAAwD;IAC9F,OAAOA,CAAAA,yBAAAA,MAAOC,iBAAiB,MAAK,YAAYD,CAAAA,yBAAAA,MAAOC,iBAAiB,MAAK;AAC/E"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/order-local-fulfillment.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * How an order reaches its buyer when it does not travel by carrier\n * (AGL-3624): picked up at one of the store's locations, or taken there by the\n * store's OWN driver. Marketplace delivery apps (AGL-3644) are a different\n * thing — an outside courier the delivery-apps plugin books — and never write\n * these fields.\n *\n * Pure and import-free, because the native apps read the same order document\n * (`tools/scripts/mobile-pure-modules.json`): the statuses, their labels, the\n * transitions and the fields the pickup and delivery lists query by are one\n * answer on every platform.\n */\n\n/** A location's pickup offer (`hosts/{hostId}/locations/{id}.pickup`). */\nexport interface PickupLocationSettings {\n enabled?: boolean\n /** Pickup hours, one line per set of days: `Mo-Fr 09:00-17:00`. */\n hours?: string\n /** What the buyer does on arrival: \"Ring the bell at the side door\". */\n instructions?: string\n /** How long an order usually takes to prepare, shown to the buyer. */\n readyWithinMinutes?: number\n}\n\n/** How the buyer chose to receive the order. Absent on every order before AGL-3624: shipped. */\nexport type OrderFulfillmentMethod = 'shipping' | 'pickup' | 'local_delivery'\n\n/** Where a pickup order stands: being put together, waiting at the counter, collected. */\nexport type OrderPickupStatus = 'preparing' | 'ready' | 'picked_up'\n\n/** Where the store's own delivery stands. `failed` is a missed drop the store can send out again. */\nexport type OrderLocalDeliveryStatus = 'scheduled' | 'out_for_delivery' | 'delivered' | 'failed'\n\n/**\n * The pickup the buyer chose, copied from the location at checkout so the\n * order says where to go even after the location is renamed or removed.\n */\nexport interface OrderPickup {\n locationId: string\n locationName: string\n /** The location's address on one line, when it has one. */\n address?: string\n /** What to do on arrival, in the store's words. */\n instructions?: string\n /** The location's hours as the store typed them (`Mo-Fr 09:00-17:00`). */\n hours?: string\n status: OrderPickupStatus\n readyAtMs?: number\n pickedUpAtMs?: number\n /** Who collected it, when the person at the counter noted a name. */\n pickedUpBy?: string\n updatedAtMs?: number\n}\n\n/** The local delivery the buyer chose and paid for. */\nexport interface OrderLocalDelivery {\n zoneId: string\n zoneName: string\n /** The fee charged, in cents (the shipping line of the order). */\n feeCents: number\n /** The window the buyer picked, as instants. */\n windowStartMs: number\n windowEndMs: number\n /** The window as the buyer was shown it, in the store's time zone. */\n windowLabel?: string\n /** The postal code the buyer declared at checkout and the zone was matched on. */\n postalCode?: string\n /** The location the order leaves from, when the store named one. */\n locationId?: string\n status: OrderLocalDeliveryStatus\n /**\n * The address the buyer then entered at payment is OUTSIDE the zone the\n * fee was charged for: the store is told, and decides.\n */\n addressOutsideZone?: boolean\n outForDeliveryAtMs?: number\n deliveredAtMs?: number\n failedAtMs?: number\n /** Why a drop failed, in the driver's words. */\n failedReason?: string\n /**\n * An outside courier the store sent for this drop (AGL-3695), as the\n * courier plugin last reported it through core's\n * `core.local-delivery-records`. Absent when the store's own driver takes it.\n */\n courier?: OrderLocalDeliveryCourier\n updatedAtMs?: number\n}\n\n/** Where an outside courier's run stands. `returning`/`returned`: it is coming back to the store. */\nexport type OrderCourierState =\n | 'requested'\n | 'assigned'\n | 'at_pickup'\n | 'picked_up'\n | 'at_dropoff'\n | 'delivered'\n | 'cancelled'\n | 'returning'\n | 'returned'\n\n/**\n * An outside courier's run on a local delivery (AGL-3695): the merchant's own\n * DoorDash Drive account, booked from the console. The courier bills the\n * merchant's account directly, so no fee is kept here.\n */\nexport interface OrderLocalDeliveryCourier {\n /** The courier plugin's provider id: `doordash`. */\n provider: string\n /** `DoorDash`. */\n providerLabel: string\n /** The courier's reference for the run. */\n deliveryRef: string\n state: OrderCourierState\n /** The courier's live tracking page. */\n trackingUrl?: string\n /** When the courier expects to reach the door. */\n etaMs?: number\n /** When the courier expects to collect. */\n pickupEtaMs?: number\n /** Why the run was canceled or came back, in the courier's words. */\n reason?: string\n /** Booked in the courier's test environment: nobody is coming. */\n testMode?: boolean\n updatedAtMs: number\n}\n\nexport const ORDER_COURIER_STATE_LABELS: Readonly<Record<OrderCourierState, string>> = {\n requested: 'Courier requested',\n assigned: 'Courier on the way to the store',\n at_pickup: 'Courier at the store',\n picked_up: 'On its way',\n at_dropoff: 'Courier arriving',\n delivered: 'Delivered',\n cancelled: 'Courier canceled',\n returning: 'Coming back to the store',\n returned: 'Returned to the store',\n}\n\n/** Whether a courier is still working the drop: requested, and neither done nor called off. */\nexport function orderCourierIsActive(courier: Pick<OrderLocalDeliveryCourier, 'state'> | null | undefined): boolean {\n return Boolean(courier) && !['delivered', 'cancelled', 'returned'].includes(String(courier?.state))\n}\n\nexport const ORDER_PICKUP_STATUS_LABELS: Readonly<Record<OrderPickupStatus, string>> = {\n preparing: 'Preparing',\n ready: 'Ready for pickup',\n picked_up: 'Picked up',\n}\n\nexport const ORDER_LOCAL_DELIVERY_STATUS_LABELS: Readonly<Record<OrderLocalDeliveryStatus, string>> = {\n scheduled: 'Scheduled',\n out_for_delivery: 'Out for delivery',\n delivered: 'Delivered',\n failed: 'Delivery failed',\n}\n\nexport const ORDER_FULFILLMENT_METHOD_LABELS: Readonly<Record<OrderFulfillmentMethod, string>> = {\n shipping: 'Shipping',\n pickup: 'Pickup',\n local_delivery: 'Local delivery',\n}\n\n/**\n * The forward moves of a pickup. `ready` → `preparing` is allowed: the\n * merchant marked the wrong order ready and takes it back before anyone is\n * told twice. Nothing leaves `picked_up`: the goods are gone.\n */\nconst PICKUP_TRANSITIONS: Readonly<Record<OrderPickupStatus, readonly OrderPickupStatus[]>> = {\n preparing: ['ready', 'picked_up'],\n ready: ['picked_up', 'preparing'],\n picked_up: [],\n}\n\n/** A failed drop goes out again; a delivered one is history. */\nconst LOCAL_DELIVERY_TRANSITIONS: Readonly<\n Record<OrderLocalDeliveryStatus, readonly OrderLocalDeliveryStatus[]>\n> = {\n scheduled: ['out_for_delivery', 'delivered', 'failed'],\n out_for_delivery: ['delivered', 'failed'],\n failed: ['out_for_delivery', 'delivered'],\n delivered: [],\n}\n\nexport function canTransitionPickup(from: OrderPickupStatus | undefined, to: OrderPickupStatus): boolean {\n return (PICKUP_TRANSITIONS[from ?? 'preparing'] ?? []).includes(to)\n}\n\nexport function canTransitionLocalDelivery(\n from: OrderLocalDeliveryStatus | undefined,\n to: OrderLocalDeliveryStatus,\n): boolean {\n return (LOCAL_DELIVERY_TRANSITIONS[from ?? 'scheduled'] ?? []).includes(to)\n}\n\n/**\n * The key the pickup and delivery lists query on (`fulfillmentKey`): the\n * method and its status in one field, so \"orders ready for pickup\" is ONE\n * equality and every tab of the list is one composite index.\n */\nexport type OrderFulfillmentKey =\n | 'pickup_preparing'\n | 'pickup_ready'\n | 'pickup_picked_up'\n | 'delivery_scheduled'\n | 'delivery_out_for_delivery'\n | 'delivery_delivered'\n | 'delivery_failed'\n\n/** The fields the pickup and delivery lists query, stamped by every writer of either. */\nexport interface OrderLocalFulfillmentListFields {\n fulfillmentKey: OrderFulfillmentKey\n /** The location to prepare it at or send it from; `''` when the store named none. */\n fulfillmentLocationId: string\n /**\n * What the list sorts by, soonest first: a pickup by when it was ordered\n * (first in, first prepared), a delivery by the start of its window.\n */\n fulfillmentDueMs: number\n}\n\nconst PICKUP_STATUSES: readonly OrderPickupStatus[] = ['preparing', 'ready', 'picked_up']\nconst DELIVERY_STATUSES: readonly OrderLocalDeliveryStatus[] = [\n 'scheduled',\n 'out_for_delivery',\n 'delivered',\n 'failed',\n]\n\n/** Reads a stored pickup status, defaulting a malformed one to where every pickup starts. */\nexport function orderPickupStatus(value: unknown): OrderPickupStatus {\n return PICKUP_STATUSES.includes(value as OrderPickupStatus) ? (value as OrderPickupStatus) : 'preparing'\n}\n\nexport function orderLocalDeliveryStatus(value: unknown): OrderLocalDeliveryStatus {\n return DELIVERY_STATUSES.includes(value as OrderLocalDeliveryStatus)\n ? (value as OrderLocalDeliveryStatus)\n : 'scheduled'\n}\n\n/**\n * The list fields for an order, or `null` for one that is shipped: those\n * orders carry none of the three, so neither list ever reads them.\n */\nexport function orderLocalFulfillmentListFields(order: {\n fulfillmentMethod?: OrderFulfillmentMethod | string\n pickup?: Partial<OrderPickup> | null\n localDelivery?: Partial<OrderLocalDelivery> | null\n createdAtMs?: number\n}): OrderLocalFulfillmentListFields | null {\n if (order.fulfillmentMethod === 'pickup' && order.pickup) {\n return {\n fulfillmentKey: `pickup_${orderPickupStatus(order.pickup.status)}` as OrderFulfillmentKey,\n fulfillmentLocationId: String(order.pickup.locationId ?? ''),\n fulfillmentDueMs: Number(order.createdAtMs) || 0,\n }\n }\n if (order.fulfillmentMethod === 'local_delivery' && order.localDelivery) {\n return {\n fulfillmentKey: `delivery_${orderLocalDeliveryStatus(order.localDelivery.status)}` as OrderFulfillmentKey,\n fulfillmentLocationId: String(order.localDelivery.locationId ?? ''),\n fulfillmentDueMs: Number(order.localDelivery.windowStartMs) || Number(order.createdAtMs) || 0,\n }\n }\n return null\n}\n\n/** Whether an order is collected or brought by the store, so no carrier ever ships it. */\nexport function orderIsLocallyFulfilled(order: { fulfillmentMethod?: string } | null | undefined): boolean {\n return order?.fulfillmentMethod === 'pickup' || order?.fulfillmentMethod === 'local_delivery'\n}\n"],"names":["ORDER_COURIER_STATE_LABELS","requested","assigned","at_pickup","picked_up","at_dropoff","delivered","cancelled","returning","returned","orderCourierIsActive","courier","Boolean","includes","String","state","ORDER_PICKUP_STATUS_LABELS","preparing","ready","ORDER_LOCAL_DELIVERY_STATUS_LABELS","scheduled","out_for_delivery","failed","ORDER_FULFILLMENT_METHOD_LABELS","shipping","pickup","local_delivery","PICKUP_TRANSITIONS","LOCAL_DELIVERY_TRANSITIONS","canTransitionPickup","from","to","canTransitionLocalDelivery","PICKUP_STATUSES","DELIVERY_STATUSES","orderPickupStatus","value","orderLocalDeliveryStatus","orderLocalFulfillmentListFields","order","fulfillmentMethod","fulfillmentKey","status","fulfillmentLocationId","locationId","fulfillmentDueMs","Number","createdAtMs","localDelivery","windowStartMs","orderIsLocallyFulfilled"],"mappings":"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"}
@@ -109,6 +109,17 @@ export interface OrderStatusView {
109
109
  windowLabel: string | null;
110
110
  status: OrderLocalDeliveryStatus;
111
111
  statusLabel: string;
112
+ /**
113
+ * The outside courier bringing it (AGL-3695), while one is: who, where
114
+ * the run stands, the courier's own tracking page and the arrival
115
+ * estimate. Optional so an answer from before this field reads as none.
116
+ */
117
+ courier?: {
118
+ providerLabel: string;
119
+ stateLabel: string;
120
+ trackingUrl: string | null;
121
+ etaMs: number | null;
122
+ } | null;
112
123
  };
113
124
  }
114
125
  /** Builds the public projection. Pure; the caller has verified the link. */
@@ -16,7 +16,7 @@
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  import { fulfillmentIsActive, fulfillmentLineQuantities } from "./order-fulfillment.js";
18
18
  import { carrierLabelFor, fulfillmentTrackingUrl } from "./tracking-url.js";
19
- import { ORDER_LOCAL_DELIVERY_STATUS_LABELS, ORDER_PICKUP_STATUS_LABELS, orderLocalDeliveryStatus, orderPickupStatus } from "./order-local-fulfillment.js";
19
+ import { ORDER_COURIER_STATE_LABELS, ORDER_LOCAL_DELIVERY_STATUS_LABELS, orderCourierIsActive, ORDER_PICKUP_STATUS_LABELS, orderLocalDeliveryStatus, orderPickupStatus } from "./order-local-fulfillment.js";
20
20
  const STATUS_LABELS = {
21
21
  pending: 'Awaiting payment',
22
22
  paid: 'Confirmed',
@@ -199,12 +199,29 @@ function eventAt(order, ...events) {
199
199
  statusLabel: ORDER_PICKUP_STATUS_LABELS[orderPickupStatus(pickup.status)]
200
200
  }
201
201
  } : {}, delivery ? {
202
- localDelivery: {
202
+ localDelivery: _extends({
203
203
  windowLabel: (_delivery_windowLabel = delivery.windowLabel) != null ? _delivery_windowLabel : null,
204
204
  status: orderLocalDeliveryStatus(delivery.status),
205
205
  statusLabel: ORDER_LOCAL_DELIVERY_STATUS_LABELS[orderLocalDeliveryStatus(delivery.status)]
206
- }
206
+ }, courierView(delivery.courier) ? {
207
+ courier: courierView(delivery.courier)
208
+ } : {})
207
209
  } : {});
208
210
  }
211
+ /**
212
+ * The courier as a buyer reads it: only a run still under way, only an https
213
+ * tracking page, and no reference, fee or test flag (AGL-3695).
214
+ */ function courierView(courier) {
215
+ var _courier_trackingUrl, _ORDER_COURIER_STATE_LABELS_courier_state;
216
+ if (!courier || !orderCourierIsActive(courier)) return null;
217
+ const trackingUrl = /^https:\/\/[^\s]+$/.test(String((_courier_trackingUrl = courier.trackingUrl) != null ? _courier_trackingUrl : '')) ? String(courier.trackingUrl) : null;
218
+ const etaMs = Number(courier.etaMs);
219
+ return {
220
+ providerLabel: String(courier.providerLabel || 'Courier'),
221
+ stateLabel: (_ORDER_COURIER_STATE_LABELS_courier_state = ORDER_COURIER_STATE_LABELS[courier.state]) != null ? _ORDER_COURIER_STATE_LABELS_courier_state : 'Courier requested',
222
+ trackingUrl,
223
+ etaMs: Number.isFinite(etaMs) && etaMs > 0 ? etaMs : null
224
+ };
225
+ }
209
226
 
210
227
  //# sourceMappingURL=order-status-view.js.map