@aglyn/plugins-commerce 1.0.0-beta.290 → 1.0.0-beta.292

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 (183) hide show
  1. package/README.md +4 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/cart-fulfillment.d.ts +3 -0
  4. package/src/lib/components/cart-fulfillment.js.map +1 -1
  5. package/src/lib/components/cart.js +14 -4
  6. package/src/lib/components/cart.js.map +1 -1
  7. package/src/lib/components/commerce-console-page.js +2 -1
  8. package/src/lib/components/commerce-console-page.js.map +1 -1
  9. package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
  10. package/src/lib/components/console/host-orders-card.component.js +3 -2
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/invoice-send.component.d.ts +44 -0
  13. package/src/lib/components/console/invoice-send.component.js +237 -0
  14. package/src/lib/components/console/invoice-send.component.js.map +1 -0
  15. package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
  16. package/src/lib/components/console/order-detail-dialog.component.js +9 -2
  17. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  18. package/src/lib/components/console/order-invoice.component.d.ts +9 -0
  19. package/src/lib/components/console/order-invoice.component.js +27 -2
  20. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  21. package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
  22. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  23. package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
  24. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
  25. package/src/lib/components/console/pos-page.component.js +1 -1
  26. package/src/lib/components/console/pos-page.component.js.map +1 -1
  27. package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
  28. package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
  29. package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
  30. package/src/lib/components/console/product-editor-dialog.component.js +31 -5
  31. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  32. package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
  33. package/src/lib/components/console/product-register-fields.component.js +8 -197
  34. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  35. package/src/lib/components/console/products-hub-zone.component.js +6 -0
  36. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  37. package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
  38. package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
  39. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  40. package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
  41. package/src/lib/components/console/quote-pay-link.component.js +29 -3
  42. package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
  43. package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
  44. package/src/lib/components/console/quote-proofs.component.js +68 -0
  45. package/src/lib/components/console/quote-proofs.component.js.map +1 -1
  46. package/src/lib/components/console/quote-send.component.d.ts +42 -0
  47. package/src/lib/components/console/quote-send.component.js +304 -0
  48. package/src/lib/components/console/quote-send.component.js.map +1 -0
  49. package/src/lib/components/console/quote-start.component.d.ts +45 -0
  50. package/src/lib/components/console/quote-start.component.js +182 -0
  51. package/src/lib/components/console/quote-start.component.js.map +1 -0
  52. package/src/lib/components/product-add-ons.d.ts +28 -0
  53. package/src/lib/components/product-add-ons.js +136 -0
  54. package/src/lib/components/product-add-ons.js.map +1 -0
  55. package/src/lib/components/product-detail.d.ts +16 -0
  56. package/src/lib/components/product-detail.js +112 -19
  57. package/src/lib/components/product-detail.js.map +1 -1
  58. package/src/lib/components/product-grid.d.ts +8 -2
  59. package/src/lib/components/product-grid.js +6 -1
  60. package/src/lib/components/product-grid.js.map +1 -1
  61. package/src/lib/components/product-options-field-body.d.ts +44 -0
  62. package/src/lib/components/product-options-field-body.js +260 -0
  63. package/src/lib/components/product-options-field-body.js.map +1 -0
  64. package/src/lib/components/product-options-field.d.ts +32 -0
  65. package/src/lib/components/product-options-field.js +128 -0
  66. package/src/lib/components/product-options-field.js.map +1 -0
  67. package/src/lib/components/quote-page-body.js +39 -20
  68. package/src/lib/components/quote-page-body.js.map +1 -1
  69. package/src/lib/model/commerce-cart.d.ts +15 -3
  70. package/src/lib/model/commerce-cart.js +10 -3
  71. package/src/lib/model/commerce-cart.js.map +1 -1
  72. package/src/lib/model/commerce-invoices.d.ts +139 -0
  73. package/src/lib/model/commerce-invoices.js +343 -0
  74. package/src/lib/model/commerce-invoices.js.map +1 -0
  75. package/src/lib/model/commerce-orders.d.ts +35 -5
  76. package/src/lib/model/commerce-orders.js +3 -0
  77. package/src/lib/model/commerce-orders.js.map +1 -1
  78. package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
  79. package/src/lib/model/commerce-pos-kiosk.js +3 -2
  80. package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
  81. package/src/lib/model/commerce-product-field.d.ts +23 -0
  82. package/src/lib/model/commerce-product-field.js +26 -0
  83. package/src/lib/model/commerce-product-field.js.map +1 -0
  84. package/src/lib/model/commerce-quote-email.d.ts +81 -0
  85. package/src/lib/model/commerce-quote-email.js +170 -0
  86. package/src/lib/model/commerce-quote-email.js.map +1 -0
  87. package/src/lib/model/commerce-quote-pay.d.ts +49 -6
  88. package/src/lib/model/commerce-quote-pay.js +69 -36
  89. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  90. package/src/lib/model/commerce-quote-request.d.ts +93 -0
  91. package/src/lib/model/commerce-quote-request.js +207 -0
  92. package/src/lib/model/commerce-quote-request.js.map +1 -0
  93. package/src/lib/model/commerce-quotes.d.ts +81 -0
  94. package/src/lib/model/commerce-quotes.js +1 -1
  95. package/src/lib/model/commerce-quotes.js.map +1 -1
  96. package/src/lib/model/commerce.d.ts +18 -0
  97. package/src/lib/model/commerce.js +26 -0
  98. package/src/lib/model/commerce.js.map +1 -1
  99. package/src/lib/model/index.d.ts +3 -0
  100. package/src/lib/model/index.js +3 -0
  101. package/src/lib/model/index.js.map +1 -1
  102. package/src/lib/model/product-modifiers.d.ts +74 -4
  103. package/src/lib/model/product-modifiers.js +131 -10
  104. package/src/lib/model/product-modifiers.js.map +1 -1
  105. package/src/lib/plugin.js +19 -0
  106. package/src/lib/plugin.js.map +1 -1
  107. package/src/lib/server/billing-webhook.js +164 -98
  108. package/src/lib/server/billing-webhook.js.map +1 -1
  109. package/src/lib/server/cart-checkout.js +14 -2
  110. package/src/lib/server/cart-checkout.js.map +1 -1
  111. package/src/lib/server/cart.d.ts +5 -0
  112. package/src/lib/server/cart.js +41 -4
  113. package/src/lib/server/cart.js.map +1 -1
  114. package/src/lib/server/catalog.d.ts +6 -0
  115. package/src/lib/server/catalog.js +2 -0
  116. package/src/lib/server/catalog.js.map +1 -1
  117. package/src/lib/server/checkout-extras.js +3 -1
  118. package/src/lib/server/checkout-extras.js.map +1 -1
  119. package/src/lib/server/checkout.js +28 -3
  120. package/src/lib/server/checkout.js.map +1 -1
  121. package/src/lib/server/commerce-pdf.d.ts +5 -1
  122. package/src/lib/server/commerce-pdf.js +37 -10
  123. package/src/lib/server/commerce-pdf.js.map +1 -1
  124. package/src/lib/server/draft-order.js +23 -3
  125. package/src/lib/server/draft-order.js.map +1 -1
  126. package/src/lib/server/invoice-email.d.ts +76 -0
  127. package/src/lib/server/invoice-email.js +183 -0
  128. package/src/lib/server/invoice-email.js.map +1 -0
  129. package/src/lib/server/local-fulfillment.js +5 -1
  130. package/src/lib/server/local-fulfillment.js.map +1 -1
  131. package/src/lib/server/order-invoice-send.d.ts +43 -0
  132. package/src/lib/server/order-invoice-send.js +218 -0
  133. package/src/lib/server/order-invoice-send.js.map +1 -0
  134. package/src/lib/server/order-status.d.ts +11 -0
  135. package/src/lib/server/order-status.js +44 -8
  136. package/src/lib/server/order-status.js.map +1 -1
  137. package/src/lib/server/person-eraser.d.ts +3 -2
  138. package/src/lib/server/person-eraser.js +25 -6
  139. package/src/lib/server/person-eraser.js.map +1 -1
  140. package/src/lib/server/pos-offline-sync.js +1 -1
  141. package/src/lib/server/pos-offline-sync.js.map +1 -1
  142. package/src/lib/server/pos-order.js +1 -1
  143. package/src/lib/server/pos-order.js.map +1 -1
  144. package/src/lib/server/product.d.ts +26 -0
  145. package/src/lib/server/product.js +18 -0
  146. package/src/lib/server/product.js.map +1 -1
  147. package/src/lib/server/products-write.js +5 -2
  148. package/src/lib/server/products-write.js.map +1 -1
  149. package/src/lib/server/quote-email.d.ts +85 -0
  150. package/src/lib/server/quote-email.js +211 -0
  151. package/src/lib/server/quote-email.js.map +1 -0
  152. package/src/lib/server/quote-link-token.d.ts +11 -0
  153. package/src/lib/server/quote-link-token.js +21 -0
  154. package/src/lib/server/quote-link-token.js.map +1 -1
  155. package/src/lib/server/quote-pay.d.ts +8 -3
  156. package/src/lib/server/quote-pay.js +105 -43
  157. package/src/lib/server/quote-pay.js.map +1 -1
  158. package/src/lib/server/quote.d.ts +11 -0
  159. package/src/lib/server/quote.js +309 -12
  160. package/src/lib/server/quote.js.map +1 -1
  161. package/src/lib/server/site-page-resolver.js +2 -1
  162. package/src/lib/server/site-page-resolver.js.map +1 -1
  163. package/src/lib/server.js +5 -0
  164. package/src/lib/server.js.map +1 -1
  165. package/src/lib/site.js +8 -0
  166. package/src/lib/site.js.map +1 -1
  167. package/src/lib/tenant-emails.js +243 -0
  168. package/src/lib/tenant-emails.js.map +1 -1
  169. package/src/lib/utils/add-to-cart.d.ts +8 -0
  170. package/src/lib/utils/add-to-cart.js +10 -2
  171. package/src/lib/utils/add-to-cart.js.map +1 -1
  172. package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
  173. package/src/lib/utils/commerce-document-pdf.js +6 -1
  174. package/src/lib/utils/commerce-document-pdf.js.map +1 -1
  175. package/src/lib/utils/invoice-email-content.d.ts +45 -0
  176. package/src/lib/utils/invoice-email-content.js +126 -0
  177. package/src/lib/utils/invoice-email-content.js.map +1 -0
  178. package/src/lib/utils/order-invoice.d.ts +1 -1
  179. package/src/lib/utils/order-invoice.js +14 -6
  180. package/src/lib/utils/order-invoice.js.map +1 -1
  181. package/src/lib/utils/quote-email-content.d.ts +63 -0
  182. package/src/lib/utils/quote-email-content.js +157 -0
  183. package/src/lib/utils/quote-email-content.js.map +1 -0
@@ -18,6 +18,8 @@ import { readClientIp } from "@aglyn/aglyn/app-utils/request-ip";
18
18
  import { consumeRateLimit, firebaseAdmin } from "@aglyn/tenant-data-admin";
19
19
  import * as CommerceModel from "../model/index.js";
20
20
  import { verifyOrderStatusToken } from "./order-status-token.js";
21
+ import { mintQuoteLinkToken } from "./quote-link-token.js";
22
+ import { quotePageHref } from "../constants/quote-page.js";
21
23
  import { resolvePluginTrackingPages } from "@aglyn/aglyn/plugin-manager/plugin-tracking-pages";
22
24
  /** How long the page waits for a tracking service's own page link (AGL-3635). */ export const TRACKING_PAGE_TIMEOUT_MS = 1500;
23
25
  /** Status lookups one address may make per site, per ten minutes. */ export const ORDER_STATUS_LOOKUPS_PER_WINDOW = 60;
@@ -41,6 +43,28 @@ async function actionsFor(input) {
41
43
  }));
42
44
  return answers.flat().filter((action)=>action && action.id && action.label && /^(https:\/\/|\/)/.test(String(action.url)));
43
45
  }
46
+ /**
47
+ * Pay, on the status page of an order still to be paid that its lasting pay
48
+ * link can take payment for (AGL-4099): a draft order or a quote, opening the
49
+ * same `/pay-quote` page its invoice links to. None for anything else.
50
+ */ export function orderStatusPayAction(input) {
51
+ const { hostId, orderId, order } = input;
52
+ if (order.status !== 'pending' || !CommerceModel.orderPayLinkKind(order)) return null;
53
+ if (CommerceModel.invoicePayRefusal(order, orderId)) return null;
54
+ const token = mintQuoteLinkToken(hostId, orderId, CommerceModel.orderPayLinkVersion(order));
55
+ if (!token) return null;
56
+ const charge = CommerceModel.invoiceCheckoutCharge(order, orderId);
57
+ const later = [
58
+ charge.shippingOptions.length > 0 ? 'shipping' : '',
59
+ charge.taxAtPayment ? 'tax' : ''
60
+ ].filter(Boolean).join(' and ');
61
+ const amount = CommerceModel.formatOrderMoney(charge.totalCents, input.currency);
62
+ return {
63
+ id: 'pay',
64
+ label: later ? `Pay ${amount} plus ${later}` : `Pay ${amount}`,
65
+ url: quotePageHref(orderId, token)
66
+ };
67
+ }
44
68
  /**
45
69
  * The guest order-status page's data (AGL-3610): `GET
46
70
  * /api/commerce/order-status?hostId=…&o=<orderId>&t=<token>` on the store.
@@ -92,7 +116,7 @@ async function actionsFor(input) {
92
116
  });
93
117
  if (!verifyOrderStatusToken(hostId, orderId, token)) return notFound();
94
118
  try {
95
- var _orderSnapshot_data, _resolveHostToken, _settingsSnapshot_get;
119
+ var _orderSnapshot_data, _settingsSnapshot_get, _resolveHostToken;
96
120
  const firestore = firebaseAdmin.app().firestore();
97
121
  const hostRef = firestore.collection('hosts').doc(hostId);
98
122
  const [hostSnapshot, settingsSnapshot, orderSnapshot] = await Promise.all([
@@ -102,18 +126,30 @@ async function actionsFor(input) {
102
126
  ]);
103
127
  if (!hostSnapshot.exists || !orderSnapshot.exists) return notFound();
104
128
  const order = CommerceModel.liftLegacyOrder((_orderSnapshot_data = orderSnapshot.data()) != null ? _orderSnapshot_data : {});
129
+ const currency = String((_settingsSnapshot_get = settingsSnapshot.get('currency')) != null ? _settingsSnapshot_get : 'USD');
130
+ const pay = orderStatusPayAction({
131
+ hostId,
132
+ orderId,
133
+ order,
134
+ currency
135
+ });
105
136
  const view = CommerceModel.buildOrderStatusView({
106
137
  order,
107
138
  orderId,
108
139
  storeName: String((_resolveHostToken = resolveHostToken('businessName', hostSnapshot.data())) != null ? _resolveHostToken : ''),
109
- currency: String((_settingsSnapshot_get = settingsSnapshot.get('currency')) != null ? _settingsSnapshot_get : 'USD'),
140
+ currency,
110
141
  number: CommerceModel.formatOrderNumber(order, orderId),
111
- actions: await actionsFor({
112
- hostId,
113
- orderId,
114
- order,
115
- token
116
- })
142
+ actions: [
143
+ ...pay ? [
144
+ pay
145
+ ] : [],
146
+ ...await actionsFor({
147
+ hostId,
148
+ orderId,
149
+ order,
150
+ token
151
+ })
152
+ ]
117
153
  });
118
154
  // A tracking service's own page for each parcel, where the merchant
119
155
  // follows them through one (AGL-3635); the carrier's link otherwise.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-status.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n resolveHostToken,\n type HostTokenSource,\n} from '@aglyn/aglyn/app-utils/host-tokens'\nimport { readClientIp } from '@aglyn/aglyn/app-utils/request-ip'\nimport { consumeRateLimit, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { verifyOrderStatusToken } from './order-status-token'\nimport { resolvePluginTrackingPages } from '@aglyn/aglyn/plugin-manager/plugin-tracking-pages'\n\n/** How long the page waits for a tracking service's own page link (AGL-3635). */\nexport const TRACKING_PAGE_TIMEOUT_MS = 1_500\n\n/** Status lookups one address may make per site, per ten minutes. */\nexport const ORDER_STATUS_LOOKUPS_PER_WINDOW = 60\nexport const ORDER_STATUS_WINDOW_MS = 10 * 60_000\n\n/**\n * Adds buyer actions to the status page (AGL-3610) — the typed extension\n * point for \"Request a return\", which the order-processing work (AGL-3611)\n * owns. A provider sees the verified order and answers the actions that apply\n * to it (none, usually); the page draws each as a button to its `url`. Kept\n * inside commerce: returns are this plugin's, so no cross-plugin seam is\n * needed, and a provider that throws costs the page its buttons, not the page.\n */\nexport type OrderStatusActionsProvider = (input: {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n /** The verified status token, for a provider building a signed follow-up link. */\n token: string\n}) => CommerceModel.OrderStatusAction[] | Promise<CommerceModel.OrderStatusAction[]>\n\nconst actionProviders: OrderStatusActionsProvider[] = []\n\nexport function registerOrderStatusActions(provider: OrderStatusActionsProvider): void {\n if (!actionProviders.includes(provider)) actionProviders.push(provider)\n}\n\n/** Specs only. */\nexport function clearOrderStatusActions(): void {\n actionProviders.length = 0\n}\n\nasync function actionsFor(\n input: Parameters<OrderStatusActionsProvider>[0],\n): Promise<CommerceModel.OrderStatusAction[]> {\n const answers = await Promise.all(\n actionProviders.map(async (provider) => {\n try {\n return (await provider(input)) ?? []\n } catch (error) {\n console.error('[order-status] actions provider failed', error)\n return []\n }\n }),\n )\n return answers\n .flat()\n .filter(\n (action) =>\n action && action.id && action.label && /^(https:\\/\\/|\\/)/.test(String(action.url)),\n )\n}\n\n/**\n * The guest order-status page's data (AGL-3610): `GET\n * /api/commerce/order-status?hostId=…&o=<orderId>&t=<token>` on the store.\n *\n * The token is the whole credential — `verifyOrderStatusToken`, constant\n * time — and a wrong one answers exactly as a missing order does (404), so\n * the route cannot be used to learn which order ids exist. Rate-limited per\n * address per site, durable, BEFORE the token check so guessing costs the\n * same as looking. The answer is `buildOrderStatusView`, an allow-listed\n * projection, never the order document; `Cache-Control: private, no-store`\n * and `X-Robots-Tag: noindex` keep it out of shared caches and search.\n *\n * Registered as a `recipientLink` route: an emailed link has to keep working\n * for the buyer it was sent to, so the per-site enablement gates do not\n * stand between them and their order.\n */\nexport const orderStatusHandler: PluginApiHandler = async (req, res) => {\n res.setHeader('Cache-Control', 'private, no-store')\n res.setHeader('X-Robots-Tag', 'noindex, nofollow')\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query.hostId ?? '')\n const orderId = String(req.query.o ?? '').slice(0, 200)\n const token = String(req.query.t ?? '').slice(0, 200)\n if (!hostId || !orderId || !token || /^__.*__$/.test(hostId) || /^__.*__$/.test(orderId) || orderId.includes('/')) {\n return res.status(400).json({ error: 'This link is incomplete.' })\n }\n const ip =\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n 'no-address'\n const rate = await consumeRateLimit(`commerce-order-status:${hostId}:${ip}`, {\n limit: ORDER_STATUS_LOOKUPS_PER_WINDOW,\n windowMs: ORDER_STATUS_WINDOW_MS,\n })\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - Date.now()) / 1000))),\n )\n return res.status(429).json({ error: 'Too many lookups. Try again in a few minutes.' })\n }\n const notFound = () =>\n res.status(404).json({ error: 'We could not find that order. Check the link in your email.' })\n if (!verifyOrderStatusToken(hostId, orderId, token)) return notFound()\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, settingsSnapshot, orderSnapshot] = await Promise.all([\n hostRef.get(),\n hostRef.collection('settings').doc('store').get(),\n hostRef.collection('orders').doc(orderId).get(),\n ])\n if (!hostSnapshot.exists || !orderSnapshot.exists) return notFound()\n const order = CommerceModel.liftLegacyOrder((orderSnapshot.data() ?? {}) as never)\n const view = CommerceModel.buildOrderStatusView({\n order,\n orderId,\n storeName: String(\n resolveHostToken('businessName', hostSnapshot.data() as HostTokenSource) ?? '',\n ),\n currency: String(settingsSnapshot.get('currency') ?? 'USD'),\n number: CommerceModel.formatOrderNumber(order, orderId),\n actions: await actionsFor({ hostId, orderId, order, token }),\n })\n // A tracking service's own page for each parcel, where the merchant\n // follows them through one (AGL-3635); the carrier's link otherwise.\n const pages = await resolvePluginTrackingPages(\n view.shipments\n .filter((shipment) => shipment.trackingNumber)\n .map((shipment) => ({\n hostId,\n recordId: orderId,\n carrier: shipment.carrier,\n trackingNumber: String(shipment.trackingNumber),\n })),\n { timeoutMs: TRACKING_PAGE_TIMEOUT_MS },\n )\n for (const shipment of view.shipments) {\n const page = shipment.trackingNumber ? pages.get(shipment.trackingNumber) : undefined\n if (page) shipment.trackingUrl = page\n }\n return res.status(200).json(view)\n } catch (error) {\n console.error('orderStatus failed', error)\n return res.status(500).json({ error: 'Your order could not be loaded. Try again.' })\n }\n}\n"],"names":["resolveHostToken","readClientIp","consumeRateLimit","firebaseAdmin","CommerceModel","verifyOrderStatusToken","resolvePluginTrackingPages","TRACKING_PAGE_TIMEOUT_MS","ORDER_STATUS_LOOKUPS_PER_WINDOW","ORDER_STATUS_WINDOW_MS","actionProviders","registerOrderStatusActions","provider","includes","push","clearOrderStatusActions","length","actionsFor","input","answers","Promise","all","map","error","console","flat","filter","action","id","label","test","String","url","orderStatusHandler","req","res","setHeader","method","status","json","hostId","query","orderId","o","slice","token","t","ip","headers","remoteAddress","socket","rate","limit","windowMs","allowed","Math","max","ceil","resetMs","Date","now","notFound","orderSnapshot","settingsSnapshot","firestore","app","hostRef","collection","doc","hostSnapshot","get","exists","order","liftLegacyOrder","data","view","buildOrderStatusView","storeName","currency","number","formatOrderNumber","actions","pages","shipments","shipment","trackingNumber","recordId","carrier","timeoutMs","page","undefined","trackingUrl"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-status.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n resolveHostToken,\n type HostTokenSource,\n} from '@aglyn/aglyn/app-utils/host-tokens'\nimport { readClientIp } from '@aglyn/aglyn/app-utils/request-ip'\nimport { consumeRateLimit, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { verifyOrderStatusToken } from './order-status-token'\nimport { mintQuoteLinkToken } from './quote-link-token'\nimport { quotePageHref } from '../constants/quote-page'\nimport { resolvePluginTrackingPages } from '@aglyn/aglyn/plugin-manager/plugin-tracking-pages'\n\n/** How long the page waits for a tracking service's own page link (AGL-3635). */\nexport const TRACKING_PAGE_TIMEOUT_MS = 1_500\n\n/** Status lookups one address may make per site, per ten minutes. */\nexport const ORDER_STATUS_LOOKUPS_PER_WINDOW = 60\nexport const ORDER_STATUS_WINDOW_MS = 10 * 60_000\n\n/**\n * Adds buyer actions to the status page (AGL-3610) — the typed extension\n * point for \"Request a return\", which the order-processing work (AGL-3611)\n * owns. A provider sees the verified order and answers the actions that apply\n * to it (none, usually); the page draws each as a button to its `url`. Kept\n * inside commerce: returns are this plugin's, so no cross-plugin seam is\n * needed, and a provider that throws costs the page its buttons, not the page.\n */\nexport type OrderStatusActionsProvider = (input: {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n /** The verified status token, for a provider building a signed follow-up link. */\n token: string\n}) => CommerceModel.OrderStatusAction[] | Promise<CommerceModel.OrderStatusAction[]>\n\nconst actionProviders: OrderStatusActionsProvider[] = []\n\nexport function registerOrderStatusActions(provider: OrderStatusActionsProvider): void {\n if (!actionProviders.includes(provider)) actionProviders.push(provider)\n}\n\n/** Specs only. */\nexport function clearOrderStatusActions(): void {\n actionProviders.length = 0\n}\n\nasync function actionsFor(\n input: Parameters<OrderStatusActionsProvider>[0],\n): Promise<CommerceModel.OrderStatusAction[]> {\n const answers = await Promise.all(\n actionProviders.map(async (provider) => {\n try {\n return (await provider(input)) ?? []\n } catch (error) {\n console.error('[order-status] actions provider failed', error)\n return []\n }\n }),\n )\n return answers\n .flat()\n .filter(\n (action) =>\n action && action.id && action.label && /^(https:\\/\\/|\\/)/.test(String(action.url)),\n )\n}\n\n/**\n * Pay, on the status page of an order still to be paid that its lasting pay\n * link can take payment for (AGL-4099): a draft order or a quote, opening the\n * same `/pay-quote` page its invoice links to. None for anything else.\n */\nexport function orderStatusPayAction(input: {\n hostId: string\n orderId: string\n order: CommerceModel.HostOrder\n currency: string\n}): CommerceModel.OrderStatusAction | null {\n const { hostId, orderId, order } = input\n if (order.status !== 'pending' || !CommerceModel.orderPayLinkKind(order)) return null\n if (CommerceModel.invoicePayRefusal(order, orderId)) return null\n const token = mintQuoteLinkToken(hostId, orderId, CommerceModel.orderPayLinkVersion(order))\n if (!token) return null\n const charge = CommerceModel.invoiceCheckoutCharge(order, orderId)\n const later = [charge.shippingOptions.length > 0 ? 'shipping' : '', charge.taxAtPayment ? 'tax' : '']\n .filter(Boolean)\n .join(' and ')\n const amount = CommerceModel.formatOrderMoney(charge.totalCents, input.currency)\n return { id: 'pay', label: later ? `Pay ${amount} plus ${later}` : `Pay ${amount}`, url: quotePageHref(orderId, token) }\n}\n\n/**\n * The guest order-status page's data (AGL-3610): `GET\n * /api/commerce/order-status?hostId=…&o=<orderId>&t=<token>` on the store.\n *\n * The token is the whole credential — `verifyOrderStatusToken`, constant\n * time — and a wrong one answers exactly as a missing order does (404), so\n * the route cannot be used to learn which order ids exist. Rate-limited per\n * address per site, durable, BEFORE the token check so guessing costs the\n * same as looking. The answer is `buildOrderStatusView`, an allow-listed\n * projection, never the order document; `Cache-Control: private, no-store`\n * and `X-Robots-Tag: noindex` keep it out of shared caches and search.\n *\n * Registered as a `recipientLink` route: an emailed link has to keep working\n * for the buyer it was sent to, so the per-site enablement gates do not\n * stand between them and their order.\n */\nexport const orderStatusHandler: PluginApiHandler = async (req, res) => {\n res.setHeader('Cache-Control', 'private, no-store')\n res.setHeader('X-Robots-Tag', 'noindex, nofollow')\n if (req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.query.hostId ?? '')\n const orderId = String(req.query.o ?? '').slice(0, 200)\n const token = String(req.query.t ?? '').slice(0, 200)\n if (!hostId || !orderId || !token || /^__.*__$/.test(hostId) || /^__.*__$/.test(orderId) || orderId.includes('/')) {\n return res.status(400).json({ error: 'This link is incomplete.' })\n }\n const ip =\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n 'no-address'\n const rate = await consumeRateLimit(`commerce-order-status:${hostId}:${ip}`, {\n limit: ORDER_STATUS_LOOKUPS_PER_WINDOW,\n windowMs: ORDER_STATUS_WINDOW_MS,\n })\n if (!rate.allowed) {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((rate.resetMs - Date.now()) / 1000))),\n )\n return res.status(429).json({ error: 'Too many lookups. Try again in a few minutes.' })\n }\n const notFound = () =>\n res.status(404).json({ error: 'We could not find that order. Check the link in your email.' })\n if (!verifyOrderStatusToken(hostId, orderId, token)) return notFound()\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, settingsSnapshot, orderSnapshot] = await Promise.all([\n hostRef.get(),\n hostRef.collection('settings').doc('store').get(),\n hostRef.collection('orders').doc(orderId).get(),\n ])\n if (!hostSnapshot.exists || !orderSnapshot.exists) return notFound()\n const order = CommerceModel.liftLegacyOrder((orderSnapshot.data() ?? {}) as never)\n const currency = String(settingsSnapshot.get('currency') ?? 'USD')\n const pay = orderStatusPayAction({ hostId, orderId, order, currency })\n const view = CommerceModel.buildOrderStatusView({\n order,\n orderId,\n storeName: String(\n resolveHostToken('businessName', hostSnapshot.data() as HostTokenSource) ?? '',\n ),\n currency,\n number: CommerceModel.formatOrderNumber(order, orderId),\n actions: [...(pay ? [pay] : []), ...(await actionsFor({ hostId, orderId, order, token }))],\n })\n // A tracking service's own page for each parcel, where the merchant\n // follows them through one (AGL-3635); the carrier's link otherwise.\n const pages = await resolvePluginTrackingPages(\n view.shipments\n .filter((shipment) => shipment.trackingNumber)\n .map((shipment) => ({\n hostId,\n recordId: orderId,\n carrier: shipment.carrier,\n trackingNumber: String(shipment.trackingNumber),\n })),\n { timeoutMs: TRACKING_PAGE_TIMEOUT_MS },\n )\n for (const shipment of view.shipments) {\n const page = shipment.trackingNumber ? pages.get(shipment.trackingNumber) : undefined\n if (page) shipment.trackingUrl = page\n }\n return res.status(200).json(view)\n } catch (error) {\n console.error('orderStatus failed', error)\n return res.status(500).json({ error: 'Your order could not be loaded. Try again.' })\n }\n}\n"],"names":["resolveHostToken","readClientIp","consumeRateLimit","firebaseAdmin","CommerceModel","verifyOrderStatusToken","mintQuoteLinkToken","quotePageHref","resolvePluginTrackingPages","TRACKING_PAGE_TIMEOUT_MS","ORDER_STATUS_LOOKUPS_PER_WINDOW","ORDER_STATUS_WINDOW_MS","actionProviders","registerOrderStatusActions","provider","includes","push","clearOrderStatusActions","length","actionsFor","input","answers","Promise","all","map","error","console","flat","filter","action","id","label","test","String","url","orderStatusPayAction","hostId","orderId","order","status","orderPayLinkKind","invoicePayRefusal","token","orderPayLinkVersion","charge","invoiceCheckoutCharge","later","shippingOptions","taxAtPayment","Boolean","join","amount","formatOrderMoney","totalCents","currency","orderStatusHandler","req","res","setHeader","method","json","query","o","slice","t","ip","headers","remoteAddress","socket","rate","limit","windowMs","allowed","Math","max","ceil","resetMs","Date","now","notFound","orderSnapshot","settingsSnapshot","firestore","app","hostRef","collection","doc","hostSnapshot","get","exists","liftLegacyOrder","data","pay","view","buildOrderStatusView","storeName","number","formatOrderNumber","actions","pages","shipments","shipment","trackingNumber","recordId","carrier","timeoutMs","page","undefined","trackingUrl"],"mappings":"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"}
@@ -48,8 +48,9 @@ export declare function erasedOrderPatch(data: FirebaseFirestore.DocumentData |
48
48
  /**
49
49
  * A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer
50
50
  * in its `quote` section rather than on the order's own fields, so it is
51
- * found by `quote.customer.emailLower` and loses the person and both
52
- * addresses; its lines and totals stay, as an order's do.
51
+ * found by `quote.customer.emailLower` and loses the person, both
52
+ * addresses and the address each email went to; its lines and totals stay,
53
+ * as an order's do.
53
54
  */
54
55
  export declare function erasedQuotePatch(data: FirebaseFirestore.DocumentData | undefined, now: number): Record<string, unknown>;
55
56
  export declare function erasedCheckInPatch(data: FirebaseFirestore.DocumentData | undefined, now: number): Record<string, unknown>;
@@ -19,9 +19,19 @@ import { FieldValue } from "firebase-admin/firestore";
19
19
  import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
20
20
  const LABEL = 'commerce person eraser';
21
21
  /** Where a quote keeps its customer's address (`commerce-quotes.ts`). */ const QUOTE_EMAIL_FIELD = 'quote.customer.emailLower';
22
+ /**
23
+ * Each time the order's invoice was emailed names where it went (AGL-4099);
24
+ * the record of the send stays, without the address.
25
+ */ function erasedInvoiceSends(data) {
26
+ return Array.isArray(data == null ? void 0 : data['invoiceSends']) ? {
27
+ invoiceSends: data['invoiceSends'].map((send)=>_extends({}, send, {
28
+ to: ''
29
+ }))
30
+ } : {};
31
+ }
22
32
  /** The patch that takes the buyer off one order, as it stands. */ export function erasedOrderPatch(data, now) {
23
33
  const addressTokens = new Set(Array.isArray(data == null ? void 0 : data['customerEmailTokens']) ? data['customerEmailTokens'] : []);
24
- return _extends({
34
+ return _extends({}, erasedInvoiceSends(data), {
25
35
  customerEmail: null,
26
36
  customerName: null,
27
37
  shippingAddress: FieldValue.delete(),
@@ -36,19 +46,28 @@ const LABEL = 'commerce person eraser';
36
46
  /** The patch that takes the customer off one check-in at the window (AGL-4043). */ /**
37
47
  * A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer
38
48
  * in its `quote` section rather than on the order's own fields, so it is
39
- * found by `quote.customer.emailLower` and loses the person and both
40
- * addresses; its lines and totals stay, as an order's do.
49
+ * found by `quote.customer.emailLower` and loses the person, both
50
+ * addresses and the address each email went to; its lines and totals stay,
51
+ * as an order's do.
41
52
  */ export function erasedQuotePatch(data, now) {
42
53
  var _ref;
43
54
  const quote = (_ref = data == null ? void 0 : data['quote']) != null ? _ref : {};
44
- return {
55
+ // Each time it was emailed names where it went (AGL-4097); the record of
56
+ // the send stays, without the address.
57
+ const sends = Array.isArray(quote['sends']) ? {
58
+ sends: quote['sends'].map((send)=>_extends({}, send, {
59
+ to: ''
60
+ }))
61
+ } : {};
62
+ return _extends({
45
63
  quote: _extends({}, quote, {
46
64
  customer: {},
47
65
  billTo: null,
48
66
  shipTo: null
49
- }),
67
+ }, sends)
68
+ }, erasedInvoiceSends(data), {
50
69
  customerErasedAtMs: now
51
- };
70
+ });
52
71
  }
53
72
  export function erasedCheckInPatch(data, now) {
54
73
  var _ref, _ref1;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/person-eraser.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n PluginPersonEraser,\n PluginPersonErasureReport,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { updateWhereEquals } from '@aglyn/tenant-data-admin/server/paged-sweeps'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\n\n/**\n * COMMERCE'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080).\n *\n * An order is the merchant's record of a sale: the amounts, the line items\n * and the tax are theirs to keep and the law expects them kept. So the person\n * is taken OFF every order they placed on a site of the workspace — name,\n * email, both addresses — and a stamp says when; the order itself stays.\n *\n * The address also lives in what the orders list queries by (AGL-3321): its\n * lower-cased key, its search prefixes, and the quick search's token array,\n * which holds those prefixes beside the order number's and the item names'.\n * The first two go; the prefixes are taken back out of the third. A prefix\n * the number or an item name shares with the address goes with it — a search\n * for that prefix misses this order until the orders backfill restamps it —\n * and nothing left on the order answers to the address.\n *\n * A check-in at the window (AGL-4043) copies the buyer's name, address and\n * phone, so the same person is taken off each of theirs, by the stored\n * lower-cased address; the check-in itself — who was due, who checked them\n * in, the wristband — stays, as the order does.\n *\n * A DRY RUN counts the orders and check-ins and writes nothing.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CommercePersonEraserDeps {\n firestore(): Firestore\n}\n\nconst LABEL = 'commerce person eraser'\n\n/** Where a quote keeps its customer's address (`commerce-quotes.ts`). */\nconst QUOTE_EMAIL_FIELD = 'quote.customer.emailLower'\n\n/** The patch that takes the buyer off one order, as it stands. */\nexport function erasedOrderPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const addressTokens = new Set(\n Array.isArray(data?.['customerEmailTokens']) ? data['customerEmailTokens'] : [],\n )\n return {\n customerEmail: null,\n customerName: null,\n shippingAddress: FieldValue.delete(),\n billingAddress: FieldValue.delete(),\n customerErasedAtMs: now,\n customerEmailLower: null,\n customerEmailTokens: [],\n ...(Array.isArray(data?.['searchTokens'])\n ? {\n searchTokens: (data['searchTokens'] as unknown[]).filter(\n (token) => !addressTokens.has(token),\n ),\n }\n : {}),\n }\n}\n\n/** The patch that takes the customer off one check-in at the window (AGL-4043). */\n/**\n * A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer\n * in its `quote` section rather than on the order's own fields, so it is\n * found by `quote.customer.emailLower` and loses the person and both\n * addresses; its lines and totals stay, as an order's do.\n */\nexport function erasedQuotePatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const quote = (data?.['quote'] ?? {}) as Record<string, unknown>\n return {\n quote: { ...quote, customer: {}, billTo: null, shipTo: null },\n customerErasedAtMs: now,\n }\n}\n\nexport function erasedCheckInPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n // One key per order or stay, so the people one order brought still read\n // as one party, and as nobody's name.\n const source = String(data?.['orderId'] ?? data?.['reservationId'] ?? '')\n return {\n customerKey: `erased:${source}`,\n customerName: null,\n customerEmail: null,\n customerEmailLower: null,\n customerPhone: null,\n contactId: null,\n searchTokens: [],\n customerErasedAtMs: now,\n }\n}\n\nexport function createCommercePersonEraser(deps: CommercePersonEraserDeps): PluginPersonEraser {\n return async ({ orgId, email, dryRun, atMs }): Promise<PluginPersonErasureReport> => {\n const db = deps.firestore()\n const hosts = await db.collection('hosts').where('orgId', '==', orgId).get()\n let orders = 0\n let checkIns = 0\n let quotes = 0\n const emailLower = email.trim().toLowerCase()\n for (const host of hosts.docs) {\n const placed = host.ref.collection('orders')\n const checked = host.ref.collection('checkIns')\n if (dryRun) {\n orders += (await placed.where('customerEmail', '==', email).get()).size\n checkIns += (await checked.where('customerEmailLower', '==', emailLower).get()).size\n quotes += (await placed.where(QUOTE_EMAIL_FIELD, '==', emailLower).get()).size\n continue\n }\n checkIns += await updateWhereEquals(\n db,\n checked,\n 'customerEmailLower',\n emailLower,\n (data) => erasedCheckInPatch(data, atMs),\n LABEL,\n )\n orders += await updateWhereEquals(\n db,\n placed,\n 'customerEmail',\n email,\n (data) => erasedOrderPatch(data, atMs),\n LABEL,\n )\n quotes += await updateWhereEquals(\n db,\n placed,\n QUOTE_EMAIL_FIELD,\n emailLower,\n (data) => erasedQuotePatch(data, atMs),\n LABEL,\n )\n }\n return { orders, checkIns, quotes }\n }\n}\n\n/** The eraser over the platform's own Firestore, as the declarations register it. */\nexport const commercePersonEraser: PluginPersonEraser = (request) =>\n createCommercePersonEraser({ firestore: () => firebaseAdmin.app().firestore() })(request)\n"],"names":["updateWhereEquals","FieldValue","firebaseAdmin","LABEL","QUOTE_EMAIL_FIELD","erasedOrderPatch","data","now","addressTokens","Set","Array","isArray","customerEmail","customerName","shippingAddress","delete","billingAddress","customerErasedAtMs","customerEmailLower","customerEmailTokens","searchTokens","filter","token","has","erasedQuotePatch","quote","customer","billTo","shipTo","erasedCheckInPatch","source","String","customerKey","customerPhone","contactId","createCommercePersonEraser","deps","orgId","email","dryRun","atMs","db","firestore","hosts","collection","where","get","orders","checkIns","quotes","emailLower","trim","toLowerCase","host","docs","placed","ref","checked","size","commercePersonEraser","request","app"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/person-eraser.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n PluginPersonEraser,\n PluginPersonErasureReport,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { updateWhereEquals } from '@aglyn/tenant-data-admin/server/paged-sweeps'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\n\n/**\n * COMMERCE'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080).\n *\n * An order is the merchant's record of a sale: the amounts, the line items\n * and the tax are theirs to keep and the law expects them kept. So the person\n * is taken OFF every order they placed on a site of the workspace — name,\n * email, both addresses — and a stamp says when; the order itself stays.\n *\n * The address also lives in what the orders list queries by (AGL-3321): its\n * lower-cased key, its search prefixes, and the quick search's token array,\n * which holds those prefixes beside the order number's and the item names'.\n * The first two go; the prefixes are taken back out of the third. A prefix\n * the number or an item name shares with the address goes with it — a search\n * for that prefix misses this order until the orders backfill restamps it —\n * and nothing left on the order answers to the address.\n *\n * A check-in at the window (AGL-4043) copies the buyer's name, address and\n * phone, so the same person is taken off each of theirs, by the stored\n * lower-cased address; the check-in itself — who was due, who checked them\n * in, the wristband — stays, as the order does.\n *\n * A DRY RUN counts the orders and check-ins and writes nothing.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CommercePersonEraserDeps {\n firestore(): Firestore\n}\n\nconst LABEL = 'commerce person eraser'\n\n/** Where a quote keeps its customer's address (`commerce-quotes.ts`). */\nconst QUOTE_EMAIL_FIELD = 'quote.customer.emailLower'\n\n/**\n * Each time the order's invoice was emailed names where it went (AGL-4099);\n * the record of the send stays, without the address.\n */\nfunction erasedInvoiceSends(data: FirebaseFirestore.DocumentData | undefined): Record<string, unknown> {\n return Array.isArray(data?.['invoiceSends'])\n ? { invoiceSends: (data['invoiceSends'] as Array<Record<string, unknown>>).map((send) => ({ ...send, to: '' })) }\n : {}\n}\n\n/** The patch that takes the buyer off one order, as it stands. */\nexport function erasedOrderPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const addressTokens = new Set(\n Array.isArray(data?.['customerEmailTokens']) ? data['customerEmailTokens'] : [],\n )\n return {\n ...erasedInvoiceSends(data),\n customerEmail: null,\n customerName: null,\n shippingAddress: FieldValue.delete(),\n billingAddress: FieldValue.delete(),\n customerErasedAtMs: now,\n customerEmailLower: null,\n customerEmailTokens: [],\n ...(Array.isArray(data?.['searchTokens'])\n ? {\n searchTokens: (data['searchTokens'] as unknown[]).filter(\n (token) => !addressTokens.has(token),\n ),\n }\n : {}),\n }\n}\n\n/** The patch that takes the customer off one check-in at the window (AGL-4043). */\n/**\n * A quote's customer, erased (AGL-4093). An unpaid quote keeps its customer\n * in its `quote` section rather than on the order's own fields, so it is\n * found by `quote.customer.emailLower` and loses the person, both\n * addresses and the address each email went to; its lines and totals stay,\n * as an order's do.\n */\nexport function erasedQuotePatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n const quote = (data?.['quote'] ?? {}) as Record<string, unknown>\n // Each time it was emailed names where it went (AGL-4097); the record of\n // the send stays, without the address.\n const sends = Array.isArray(quote['sends'])\n ? { sends: (quote['sends'] as Array<Record<string, unknown>>).map((send) => ({ ...send, to: '' })) }\n : {}\n return {\n quote: { ...quote, customer: {}, billTo: null, shipTo: null, ...sends },\n ...erasedInvoiceSends(data),\n customerErasedAtMs: now,\n }\n}\n\nexport function erasedCheckInPatch(\n data: FirebaseFirestore.DocumentData | undefined,\n now: number,\n): Record<string, unknown> {\n // One key per order or stay, so the people one order brought still read\n // as one party, and as nobody's name.\n const source = String(data?.['orderId'] ?? data?.['reservationId'] ?? '')\n return {\n customerKey: `erased:${source}`,\n customerName: null,\n customerEmail: null,\n customerEmailLower: null,\n customerPhone: null,\n contactId: null,\n searchTokens: [],\n customerErasedAtMs: now,\n }\n}\n\nexport function createCommercePersonEraser(deps: CommercePersonEraserDeps): PluginPersonEraser {\n return async ({ orgId, email, dryRun, atMs }): Promise<PluginPersonErasureReport> => {\n const db = deps.firestore()\n const hosts = await db.collection('hosts').where('orgId', '==', orgId).get()\n let orders = 0\n let checkIns = 0\n let quotes = 0\n const emailLower = email.trim().toLowerCase()\n for (const host of hosts.docs) {\n const placed = host.ref.collection('orders')\n const checked = host.ref.collection('checkIns')\n if (dryRun) {\n orders += (await placed.where('customerEmail', '==', email).get()).size\n checkIns += (await checked.where('customerEmailLower', '==', emailLower).get()).size\n quotes += (await placed.where(QUOTE_EMAIL_FIELD, '==', emailLower).get()).size\n continue\n }\n checkIns += await updateWhereEquals(\n db,\n checked,\n 'customerEmailLower',\n emailLower,\n (data) => erasedCheckInPatch(data, atMs),\n LABEL,\n )\n orders += await updateWhereEquals(\n db,\n placed,\n 'customerEmail',\n email,\n (data) => erasedOrderPatch(data, atMs),\n LABEL,\n )\n quotes += await updateWhereEquals(\n db,\n placed,\n QUOTE_EMAIL_FIELD,\n emailLower,\n (data) => erasedQuotePatch(data, atMs),\n LABEL,\n )\n }\n return { orders, checkIns, quotes }\n }\n}\n\n/** The eraser over the platform's own Firestore, as the declarations register it. */\nexport const commercePersonEraser: PluginPersonEraser = (request) =>\n createCommercePersonEraser({ firestore: () => firebaseAdmin.app().firestore() })(request)\n"],"names":["updateWhereEquals","FieldValue","firebaseAdmin","LABEL","QUOTE_EMAIL_FIELD","erasedInvoiceSends","data","Array","isArray","invoiceSends","map","send","to","erasedOrderPatch","now","addressTokens","Set","customerEmail","customerName","shippingAddress","delete","billingAddress","customerErasedAtMs","customerEmailLower","customerEmailTokens","searchTokens","filter","token","has","erasedQuotePatch","quote","sends","customer","billTo","shipTo","erasedCheckInPatch","source","String","customerKey","customerPhone","contactId","createCommercePersonEraser","deps","orgId","email","dryRun","atMs","db","firestore","hosts","collection","where","get","orders","checkIns","quotes","emailLower","trim","toLowerCase","host","docs","placed","ref","checked","size","commercePersonEraser","request","app"],"mappings":"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"}
@@ -657,7 +657,7 @@ async function syncOne(context, raw) {
657
657
  });
658
658
  }
659
659
  const variant = (_product_variants_find = product.variants.find((item)=>item.id === line.variantId)) != null ? _product_variants_find : product.variants[0];
660
- const chosen = CommerceModel.resolveLineModifiers(product, line.modifiers);
660
+ const chosen = CommerceModel.resolveLineModifiers(product, line.modifiers, variant == null ? void 0 : variant.id);
661
661
  const currentCents = variant && CommerceModel.variantHasPrice(variant) && chosen.ok ? Math.round(Number(variant.priceUsd) * 100) + chosen.extraCents : null;
662
662
  if (currentCents !== line.unitAmountCents) {
663
663
  flags.add('price-changed');