@aglyn/plugins-commerce 1.0.0-beta.218 → 1.0.0-beta.220

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 (148) hide show
  1. package/README.md +6 -0
  2. package/package.json +14 -14
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js +1 -19
  5. package/src/index.js.map +1 -1
  6. package/src/lib/components/cart.js +2 -1
  7. package/src/lib/components/cart.js.map +1 -1
  8. package/src/lib/components/console/catalog-organization-card.component.js +665 -459
  9. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  10. package/src/lib/components/console/host-orders-card.component.js +2 -1
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/member-posts-card.component.js +1 -1
  13. package/src/lib/components/console/member-posts-card.component.js.map +1 -1
  14. package/src/lib/components/console/order-detail-dialog.component.js +8 -7
  15. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  16. package/src/lib/components/console/payments-settings-card.component.js +2 -2
  17. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  18. package/src/lib/components/console/product-editor-dialog.component.js +1221 -889
  19. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  20. package/src/lib/components/console/product-zones.d.ts +184 -0
  21. package/src/lib/components/console/product-zones.js +54 -0
  22. package/src/lib/components/console/product-zones.js.map +1 -0
  23. package/src/lib/components/console/products-hub-card.component.js +3 -2
  24. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  25. package/src/lib/components/console/products-hub-zone.component.d.ts +1 -1
  26. package/src/lib/components/console/products-hub-zone.component.js +2 -1
  27. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  28. package/src/lib/components/console/site-member-purchases.component.d.ts +24 -0
  29. package/src/lib/components/console/site-member-purchases.component.js +339 -0
  30. package/src/lib/components/console/site-member-purchases.component.js.map +1 -0
  31. package/src/lib/components/console/store-settings-card.component.js +2 -2
  32. package/src/lib/components/console/store-settings-card.component.js.map +1 -1
  33. package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -1
  34. package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
  35. package/src/lib/components/member-signup.js +2 -2
  36. package/src/lib/components/member-signup.js.map +1 -1
  37. package/src/lib/components/newsletter-signup.js +2 -2
  38. package/src/lib/components/newsletter-signup.js.map +1 -1
  39. package/src/lib/components/product-detail.js +2 -1
  40. package/src/lib/components/product-detail.js.map +1 -1
  41. package/src/lib/components/product-grid.js +1 -1
  42. package/src/lib/components/product-grid.js.map +1 -1
  43. package/src/lib/constants/operator-alerts.js +5 -3
  44. package/src/lib/constants/operator-alerts.js.map +1 -1
  45. package/src/lib/constants/storefront-payments.d.ts +33 -0
  46. package/src/lib/constants/storefront-payments.js +35 -0
  47. package/src/lib/constants/storefront-payments.js.map +1 -0
  48. package/src/lib/declarations.server.d.ts +3 -1
  49. package/src/lib/declarations.server.js +19 -1
  50. package/src/lib/declarations.server.js.map +1 -1
  51. package/src/lib/model/commerce-io.js +1 -1
  52. package/src/lib/model/commerce-io.js.map +1 -1
  53. package/src/lib/model/commerce-record-lists.d.ts +31 -0
  54. package/src/lib/model/commerce-record-lists.js +45 -0
  55. package/src/lib/model/commerce-record-lists.js.map +1 -0
  56. package/src/lib/model/commerce-record-routes.d.ts +45 -0
  57. package/src/lib/model/commerce-record-routes.js +72 -0
  58. package/src/lib/model/commerce-record-routes.js.map +1 -0
  59. package/src/lib/model/commerce-search-sources.d.ts +21 -0
  60. package/src/lib/model/commerce-search-sources.js +42 -0
  61. package/src/lib/model/commerce-search-sources.js.map +1 -0
  62. package/src/lib/model/commerce-topics.d.ts +20 -0
  63. package/src/lib/model/commerce-topics.js +33 -0
  64. package/src/lib/model/commerce-topics.js.map +1 -0
  65. package/src/lib/model/index.d.ts +1 -0
  66. package/src/lib/model/index.js +1 -0
  67. package/src/lib/model/index.js.map +1 -1
  68. package/src/lib/model/product-record.d.ts +32 -0
  69. package/src/lib/model/product-record.js +82 -0
  70. package/src/lib/model/product-record.js.map +1 -0
  71. package/src/lib/model/site-member-purchases.d.ts +48 -0
  72. package/src/lib/model/site-member-purchases.js +50 -0
  73. package/src/lib/model/site-member-purchases.js.map +1 -0
  74. package/src/lib/model/storefront-tax-summary.d.ts +121 -0
  75. package/src/lib/model/storefront-tax-summary.js +148 -0
  76. package/src/lib/model/storefront-tax-summary.js.map +1 -0
  77. package/src/lib/plan-entitlements.d.ts +35 -0
  78. package/src/lib/plan-entitlements.js +54 -0
  79. package/src/lib/plan-entitlements.js.map +1 -0
  80. package/src/lib/plugin.js +21 -0
  81. package/src/lib/plugin.js.map +1 -1
  82. package/src/lib/server/billing-webhook.js +387 -248
  83. package/src/lib/server/billing-webhook.js.map +1 -1
  84. package/src/lib/server/cart-checkout.js +3 -3
  85. package/src/lib/server/cart-checkout.js.map +1 -1
  86. package/src/lib/server/checkout.js +2 -2
  87. package/src/lib/server/checkout.js.map +1 -1
  88. package/src/lib/server/connect.js +5 -5
  89. package/src/lib/server/connect.js.map +1 -1
  90. package/src/lib/server/draft-order.js +2 -2
  91. package/src/lib/server/draft-order.js.map +1 -1
  92. package/src/lib/server/gift-cards.js +7 -3
  93. package/src/lib/server/gift-cards.js.map +1 -1
  94. package/src/lib/server/low-stock.js +3 -1
  95. package/src/lib/server/low-stock.js.map +1 -1
  96. package/src/lib/server/member-post.js +2 -2
  97. package/src/lib/server/member-post.js.map +1 -1
  98. package/src/lib/server/membership-admin-password.js +4 -1
  99. package/src/lib/server/membership-admin-password.js.map +1 -1
  100. package/src/lib/server/membership-content.js +1 -1
  101. package/src/lib/server/membership-content.js.map +1 -1
  102. package/src/lib/server/membership-register.js +13 -2
  103. package/src/lib/server/membership-register.js.map +1 -1
  104. package/src/lib/server/newsletter.js +32 -19
  105. package/src/lib/server/newsletter.js.map +1 -1
  106. package/src/lib/server/pos-order.js +3 -3
  107. package/src/lib/server/pos-order.js.map +1 -1
  108. package/src/lib/server/process-abandoned.js +4 -2
  109. package/src/lib/server/process-abandoned.js.map +1 -1
  110. package/src/lib/server/process-restock.js +7 -5
  111. package/src/lib/server/process-restock.js.map +1 -1
  112. package/src/lib/server/product-record-index.d.ts +19 -0
  113. package/src/lib/server/product-record-index.js +83 -0
  114. package/src/lib/server/product-record-index.js.map +1 -0
  115. package/src/lib/server/reconcile-stock.js +1 -1
  116. package/src/lib/server/reconcile-stock.js.map +1 -1
  117. package/src/lib/server/reserve-stock.js +6 -1
  118. package/src/lib/server/reserve-stock.js.map +1 -1
  119. package/src/lib/server/reserve.js +2 -2
  120. package/src/lib/server/reserve.js.map +1 -1
  121. package/src/lib/server/revenue-source.d.ts +130 -0
  122. package/src/lib/server/revenue-source.js +286 -0
  123. package/src/lib/server/revenue-source.js.map +1 -0
  124. package/src/lib/server/supplier-outbox.js +16 -1
  125. package/src/lib/server/supplier-outbox.js.map +1 -1
  126. package/src/lib/server/supplier-update.js +19 -9
  127. package/src/lib/server/supplier-update.js.map +1 -1
  128. package/src/lib/server/tax-profile.d.ts +8 -5
  129. package/src/lib/server/tax-profile.js +22 -5
  130. package/src/lib/server/tax-profile.js.map +1 -1
  131. package/src/lib/server/tax-return-source.d.ts +125 -0
  132. package/src/lib/server/tax-return-source.js +524 -0
  133. package/src/lib/server/tax-return-source.js.map +1 -0
  134. package/src/lib/server/tax-summary.d.ts +92 -0
  135. package/src/lib/server/tax-summary.js +258 -0
  136. package/src/lib/server/tax-summary.js.map +1 -0
  137. package/src/lib/server.d.ts +1 -0
  138. package/src/lib/server.js +23 -0
  139. package/src/lib/server.js.map +1 -1
  140. package/src/lib/starter-templates.d.ts +19 -0
  141. package/src/lib/starter-templates.js +157 -0
  142. package/src/lib/starter-templates.js.map +1 -0
  143. package/src/lib/tenant-emails.d.ts +33 -0
  144. package/src/lib/tenant-emails.js +551 -0
  145. package/src/lib/tenant-emails.js.map +1 -0
  146. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  147. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  148. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
@@ -15,9 +15,12 @@
15
15
  * limitations under the License.
16
16
  */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
17
  import * as Aglyn from "@aglyn/aglyn/server";
18
- import { findUserByUidAcrossPools, firebaseAdmin, getOrgForHost, meterHostEmail, notifyHostManagers, notifyRiskEvent, raiseOperatorAlert, renderHostEmailWithTokens, clearConnectPayoutFailure, recordConnectPayoutFailure, syncConnectAccountStatus, updateExisting, hostSendingIdentity } from "@aglyn/tenant-data-admin";
18
+ import { findUserByUidAcrossPools, firebaseAdmin, getOrgForHost, meterHostEmail, notifyHostManagers, notifyRiskEvent, raiseOperatorAlert, renderHostEmailWithTokens, updateExisting, hostSendingIdentity } from "@aglyn/tenant-data-admin";
19
+ import { paymentProvider } from "@aglyn/tenant-data-admin/server/payment-provider";
19
20
  import { captureHostContact } from "@aglyn/tenant-runtime";
20
21
  import { formatOperatorAlertAmount } from "@aglyn/aglyn/app-utils/operator-alerts";
22
+ import { resolveSiteTimeZone } from "@aglyn/aglyn/app-utils/collection-entry-date";
23
+ import { resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
21
24
  import { COMMERCE_CHARGEBACK_UNROUTABLE, COMMERCE_FEE_REPRICE_REFUSED, COMMERCE_SELLER_SHARE_NOT_REVERSED, COMMERCE_TAX_NOT_REVERSED } from "../constants/operator-alerts.js";
22
25
  import { createHmac } from "crypto";
23
26
  import { isEmailConfigured, sendEmail } from "@aglyn/shared-util-email";
@@ -104,11 +107,17 @@ import { enqueueSupplierDelivery } from "./supplier-outbox.js";
104
107
  }
105
108
  if (keys.length) {
106
109
  const remaining = await hostRef.collection('licenseKeys').where('productId', '==', productId).where('assignedAtMs', '==', null).limit(6).get();
107
- if (remaining.size < 5) {
110
+ // Once per CROSSING (AGL-3432), as the low-stock alert is: this sale
111
+ // took the pool below five, or took its last key. Every later sale in a
112
+ // low pool stays quiet rather than repeating the same notice.
113
+ const left = remaining.size;
114
+ const before = left + keys.length;
115
+ if (left < 5 && before >= 5 || left === 0) {
116
+ const productName = String(await hostRef.collection('products').doc(productId).get().then((snapshot)=>snapshot.get('name')).catch(()=>'') || 'a product');
108
117
  void notifyHostManagers(hostId, {
109
118
  type: 'content.lowStock',
110
- title: 'License key pool running low',
111
- body: `${remaining.size} keys left`,
119
+ title: left === 0 ? `License keys used up — ${productName}` : `License keys running low — ${productName}`,
120
+ body: left === 0 ? `The last license key for ${productName} on {site} was just ` + 'assigned. A buyer who orders it now gets no key; add keys to ' + 'its pool under Products.' : `Only ${left} license key${left === 1 ? ' is' : 's are'} left ` + `for ${productName} on {site}. Every sale of it takes its ` + 'keys from this pool; add more under Products before it runs out.',
112
121
  link: `/${hostId}/products`
113
122
  });
114
123
  }
@@ -293,6 +302,25 @@ async function findOrderForDispute(paymentIntentId) {
293
302
  }
294
303
  });
295
304
  }
305
+ /**
306
+ * A site as an operator alert names it: its name and id, else the id alone
307
+ * (AGL-3432) — staff see faults from every org, and a bare host id has to be
308
+ * looked up before anyone can act. One host read, taken only when a money
309
+ * fault is being reported; a read that fails answers the id.
310
+ */ async function operatorSiteLabel(hostId) {
311
+ if (!hostId) return 'unknown';
312
+ try {
313
+ var _data;
314
+ const host = (_data = (await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get()).data()) != null ? _data : {};
315
+ const name = [
316
+ host['displayName'],
317
+ host['subdomain']
318
+ ].find((value)=>typeof value === 'string' && value.trim() !== '');
319
+ return name ? `${name.trim()} (${hostId})` : hostId;
320
+ } catch (unused) {
321
+ return hostId;
322
+ }
323
+ }
296
324
  /**
297
325
  * Tells the operator that a subscription cycle's sales tax stayed with the
298
326
  * merchant (AGL-3377): the platform files and remits it, so every one of
@@ -304,6 +332,7 @@ async function findOrderForDispute(paymentIntentId) {
304
332
  context: {
305
333
  invoiceId: input.invoiceId,
306
334
  hostId: input.hostId,
335
+ site: await operatorSiteLabel(input.hostId),
307
336
  amount: input.taxCents === undefined ? 'An unknown amount' : formatOperatorAlertAmount(input.taxCents),
308
337
  reason: input.reason
309
338
  }
@@ -573,11 +602,11 @@ async function findOrderForDispute(paymentIntentId) {
573
602
  * fulfilment path are untouched; what changes is that no FURTHER cycle is
574
603
  * billed.
575
604
  *
576
- * `cancel_at_period_end` rather than an outright cancel, for two reasons that
577
- * both matter: the subscriber keeps the period they just paid for, and the
578
- * flag is REVERSIBLE — the merchant has a full cycle to restore their Aglyn
579
- * plan before anything is actually lost. An immediate cancel would take
580
- * service away from a shopper who did nothing wrong.
605
+ * `cancel_at_period_end` rather than an outright cancel: the subscriber keeps
606
+ * the period they just paid for. An immediate cancel would take service away
607
+ * from a shopper who did nothing wrong. The flag is reversible in Stripe, but
608
+ * nothing here clears it when the merchant's plan comes back, so the notice
609
+ * says that restoring the plan does not restart the subscription (AGL-3432).
581
610
  *
582
611
  * ## Once-only
583
612
  *
@@ -587,7 +616,8 @@ async function findOrderForDispute(paymentIntentId) {
587
616
  * subscription billing forever with the books saying it had been stopped. The
588
617
  * `Idempotency-Key` covers the window where the claim is written and the
589
618
  * response never arrives.
590
- */ async function stopLapsedStorefrontSubscription(hostId, subscriptionId, subscriptionRef) {
619
+ */ async function stopLapsedStorefrontSubscription(hostId, subscriptionId, subscriptionRef, /** Who subscribed to what, and the workspace, for the merchant's notice. */ notice) {
620
+ var _ref, _ref1;
591
621
  const stripeKey = process.env.STRIPE_SECRET_KEY;
592
622
  if (!stripeKey) {
593
623
  console.error('Lapsed storefront subscription not stopped: STRIPE_SECRET_KEY is not set (AGL-2071)', {
@@ -648,10 +678,29 @@ async function findOrderForDispute(paymentIntentId) {
648
678
  }, {
649
679
  merge: true
650
680
  }).catch(()=>undefined);
681
+ // When it ends, from Stripe's own answer: `cancel_at` is the period end the
682
+ // flag just scheduled. Told as a date in the site's zone, and left out when
683
+ // Stripe did not say.
684
+ const stopped = await response.json().catch(()=>null);
685
+ const endsAtSeconds = Number((_ref = (_ref1 = stopped == null ? void 0 : stopped.cancel_at) != null ? _ref1 : stopped == null ? void 0 : stopped.current_period_end) != null ? _ref : 0);
686
+ let endsOn = '';
687
+ if (endsAtSeconds > 0) {
688
+ const host = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get().then((snapshot)=>snapshot.data()).catch(()=>undefined);
689
+ endsOn = new Date(endsAtSeconds * 1000).toLocaleDateString('en-US', {
690
+ month: 'long',
691
+ day: 'numeric',
692
+ year: 'numeric',
693
+ timeZone: resolveSiteTimeZone(notice.org, host)
694
+ });
695
+ }
696
+ const subscriber = notice.subscriberEmail ? `${notice.subscriberEmail}'s subscription` : 'A subscription';
651
697
  void notifyHostManagers(hostId, {
652
698
  type: 'content.order',
653
- title: 'A subscription stopped renewing — your plan no longer includes storefront subscriptions',
654
- body: 'This subscriber keeps the period they have already paid for. ' + 'Restore your plan before it ends to keep billing them.',
699
+ title: 'A subscription on {site} stopped renewing',
700
+ // Restoring the plan does NOT restart it (AGL-3432): nothing clears
701
+ // Stripe's `cancel_at_period_end` when the plan comes back, so the notice
702
+ // promises no such remedy.
703
+ body: `${subscriber} to ${notice.productName} on {site} will end` + `${endsOn ? ` on ${endsOn}` : ' at the end of its paid period'} ` + 'instead of renewing, because your plan no longer includes ' + 'storefront subscriptions. The subscriber keeps the period they ' + 'already paid for. Restoring your plan does not restart this ' + 'subscription.',
655
704
  link: `/${hostId}/products`
656
705
  });
657
706
  }
@@ -738,11 +787,13 @@ async function findOrderForDispute(paymentIntentId) {
738
787
  }
739
788
  const refusal = await response.json().catch(()=>null);
740
789
  console.error(`Stripe refused the fee re-price for ${subscriptionId} (AGL-2289)`, refusal);
790
+ const repriceHostId = (_ref = (_subscriptionRef_parent_parent = subscriptionRef.parent.parent) == null ? void 0 : _subscriptionRef_parent_parent.id) != null ? _ref : '';
741
791
  await raiseOperatorAlert(COMMERCE_FEE_REPRICE_REFUSED, {
742
792
  dedupeKey: `${subscriptionId}:${desiredFeePct}`,
743
793
  context: {
744
794
  subject: `subscription ${subscriptionId}`,
745
- hostId: (_ref = (_subscriptionRef_parent_parent = subscriptionRef.parent.parent) == null ? void 0 : _subscriptionRef_parent_parent.id) != null ? _ref : '',
795
+ hostId: repriceHostId,
796
+ site: await operatorSiteLabel(repriceHostId),
746
797
  reason: `Stripe refused to set the fee to ${desiredFeePct}% (${(_ref1 = refusal == null ? void 0 : (_refusal_error = refusal.error) == null ? void 0 : _refusal_error.message) != null ? _ref1 : `HTTP ${response.status}`})`
747
798
  }
748
799
  });
@@ -764,6 +815,7 @@ async function findOrderForDispute(paymentIntentId) {
764
815
  context: {
765
816
  subject: `invoice ${invoiceId}`,
766
817
  hostId,
818
+ site: await operatorSiteLabel(hostId),
767
819
  reason
768
820
  }
769
821
  });
@@ -1603,14 +1655,15 @@ async function findOrderForDispute(paymentIntentId) {
1603
1655
  * Tells the operator the platform is out a lost dispute's principal because
1604
1656
  * the merchant's share could not be pulled back (AGL-3377). Once per dispute.
1605
1657
  */ async function reportSellerShareNotReversed(orderRef, disputeId, principalCents, reason) {
1606
- var _ref;
1607
- var _orderRef_parent_parent;
1658
+ var _ref, _ref1;
1659
+ var _orderRef_parent_parent, _orderRef_parent_parent1;
1608
1660
  await raiseOperatorAlert(COMMERCE_SELLER_SHARE_NOT_REVERSED, {
1609
1661
  dedupeKey: disputeId,
1610
1662
  context: {
1611
1663
  disputeId,
1612
1664
  orderId: orderRef.id,
1613
1665
  hostId: (_ref = (_orderRef_parent_parent = orderRef.parent.parent) == null ? void 0 : _orderRef_parent_parent.id) != null ? _ref : '',
1666
+ site: await operatorSiteLabel((_ref1 = (_orderRef_parent_parent1 = orderRef.parent.parent) == null ? void 0 : _orderRef_parent_parent1.id) != null ? _ref1 : ''),
1614
1667
  amount: formatOperatorAlertAmount(principalCents),
1615
1668
  reason
1616
1669
  }
@@ -1633,59 +1686,25 @@ async function findOrderForDispute(paymentIntentId) {
1633
1686
  * `object.metadata.type`, exactly as the inline route sections did.
1634
1687
  */ export const commerceBillingWebhookHandler = async ({ type, object, event, requestHost })=>{
1635
1688
  var _object_metadata, _object_metadata1, _object_metadata2, _object_metadata3, _object_metadata4;
1636
- // Connect readiness, kept fresh (AGL-1997). Every commerce money route —
1689
+ // THE MERCHANT'S PAYMENT ACCOUNT CHANGED. Every commerce money route —
1637
1690
  // checkout, cart checkout, draft orders, reservations, POS — gates the sale
1638
- // on the CACHED `stripeChargesEnabled` written by the connect route. Nothing
1639
- // refreshed it but the merchant reopening that route, so a merchant Stripe
1640
- // later restricted kept selling on a stale `true` and the shopper met the
1641
- // failure at payment time.
1691
+ // on the readiness the connect route CACHED on the merchant's profile
1692
+ // (AGL-1997), and nothing refreshed it but the merchant reopening that
1693
+ // route: a merchant the provider later restricted kept selling on a stale
1694
+ // `true`, and the shopper met the failure at payment time. A payout or
1695
+ // transfer that never landed is recorded against the same profile, and a
1696
+ // later one that did retires the warning.
1642
1697
  //
1643
- // FIRST and with an early return: this event shares nothing with the order
1644
- // sections below, and returning here keeps it out of every `metadata.type`
1645
- // test. `syncConnectAccountStatus` mirrors current state, so a redelivery is
1646
- // harmless.
1647
- // `event.livemode`, not `object.livemode` (AGL-2471): the Stripe Account
1648
- // object carries no `livemode` field, but the event announcing it does, and
1649
- // that is what lets a linkage whose mode was never recorded heal itself
1650
- // instead of staying refused forever.
1651
- if (type === 'account.updated') {
1652
- await syncConnectAccountStatus('profiles', object, event == null ? void 0 : event.livemode);
1653
- return;
1654
- }
1655
- // A PAYOUT OR TRANSFER THAT NEVER LANDED.
1656
- //
1657
- // Placed beside `account.updated` because it is the same kind of event —
1658
- // account-level, nothing to do with the `metadata.type` order sections
1659
- // below — and returns for the same reason.
1660
- //
1661
- // `payout.failed` is the CONNECTED account's balance failing to reach its
1662
- // bank, so the account id is `event.account`: the Payout object's own
1663
- // `destination` names the bank, not the Connect account. `transfer.failed`
1664
- // is the platform's balance failing to reach the connected account, a
1665
- // platform event whose `destination` IS the account.
1666
- //
1667
- // Recorded and surfaced, never retried: Stripe runs its own retry schedule
1668
- // and a second transfer against an account that just refused one is how a
1669
- // duplicate lands.
1670
- if (type === 'payout.failed' || type === 'transfer.failed') {
1671
- var _ref, _ref1, _ref2;
1672
- var _object_destination;
1673
- const failedAccountId = type === 'payout.failed' ? String((_ref = event == null ? void 0 : event.account) != null ? _ref : '') : String((_ref1 = (_ref2 = object == null ? void 0 : (_object_destination = object.destination) == null ? void 0 : _object_destination.id) != null ? _ref2 : object == null ? void 0 : object.destination) != null ? _ref1 : '');
1674
- await recordConnectPayoutFailure('profiles', {
1675
- kind: type === 'payout.failed' ? 'payout' : 'transfer',
1676
- object,
1677
- accountId: failedAccountId,
1678
- livemode: event == null ? void 0 : event.livemode
1679
- });
1680
- return;
1681
- }
1682
- // A later success retires the warning the card shows. The history in
1683
- // `connectPayoutFailures` is kept — "has this account failed before" is what
1684
- // that record exists to answer — but a stale warning on a resolved problem
1685
- // trains people to ignore the surface.
1686
- if (type === 'payout.paid') {
1687
- var _ref3;
1688
- await clearConnectPayoutFailure('profiles', String((_ref3 = event == null ? void 0 : event.account) != null ? _ref3 : ''));
1698
+ // The payment provider reads the event — which ones describe an account,
1699
+ // and where each keeps its id — and mirrors current state, so a redelivery
1700
+ // is harmless. FIRST and with an early return: these events share nothing
1701
+ // with the order sections below, and returning here keeps them out of
1702
+ // every `metadata.type` test.
1703
+ if (await paymentProvider().applyAccountEvent('profiles', {
1704
+ type,
1705
+ object,
1706
+ event
1707
+ })) {
1689
1708
  return;
1690
1709
  }
1691
1710
  // A DEAD SESSION GIVES ITS RESERVATIONS BACK (AGL-2453).
@@ -1706,9 +1725,9 @@ async function findOrderForDispute(paymentIntentId) {
1706
1725
  // Same early return as `account.updated`: this event carries no order and
1707
1726
  // shares nothing with the `checkout.session.completed` sections below.
1708
1727
  if (type === 'checkout.session.expired') {
1709
- var _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11;
1728
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7;
1710
1729
  var _object_metadata5, _object_metadata6, _object_metadata7, _object_metadata8, _object_metadata9, _object_metadata10, _object_metadata11, _object_metadata12;
1711
- const expiredHostId = String((_ref4 = object == null ? void 0 : (_object_metadata5 = object.metadata) == null ? void 0 : _object_metadata5.hostId) != null ? _ref4 : '');
1730
+ const expiredHostId = String((_ref = object == null ? void 0 : (_object_metadata5 = object.metadata) == null ? void 0 : _object_metadata5.hostId) != null ? _ref : '');
1712
1731
  if (!expiredHostId) return;
1713
1732
  const expiredHostRef = firebaseAdmin.app().firestore().collection('hosts').doc(expiredHostId);
1714
1733
  // THE UNITS COME BACK FIRST (AGL-2356), because they are the reservation a
@@ -1722,14 +1741,14 @@ async function findOrderForDispute(paymentIntentId) {
1722
1741
  // fire at all unless the endpoint subscribes to the event. What it buys is
1723
1742
  // that the shelf and the storefront agree PROMPTLY, which is the difference
1724
1743
  // between a hold a merchant can reason about and one they cannot.
1725
- await releaseStockHold(expiredHostRef, String((_ref5 = object == null ? void 0 : (_object_metadata6 = object.metadata) == null ? void 0 : _object_metadata6.stockHoldKey) != null ? _ref5 : ''));
1726
- const couponHoldKey = String((_ref6 = object == null ? void 0 : (_object_metadata7 = object.metadata) == null ? void 0 : _object_metadata7.couponHoldKey) != null ? _ref6 : '');
1727
- const couponCode = String((_ref7 = object == null ? void 0 : (_object_metadata8 = object.metadata) == null ? void 0 : _object_metadata8.couponCode) != null ? _ref7 : '');
1744
+ await releaseStockHold(expiredHostRef, String((_ref1 = object == null ? void 0 : (_object_metadata6 = object.metadata) == null ? void 0 : _object_metadata6.stockHoldKey) != null ? _ref1 : ''));
1745
+ const couponHoldKey = String((_ref2 = object == null ? void 0 : (_object_metadata7 = object.metadata) == null ? void 0 : _object_metadata7.couponHoldKey) != null ? _ref2 : '');
1746
+ const couponCode = String((_ref3 = object == null ? void 0 : (_object_metadata8 = object.metadata) == null ? void 0 : _object_metadata8.couponCode) != null ? _ref3 : '');
1728
1747
  if (couponHoldKey && couponCode) {
1729
1748
  await releasePromotionHold(expiredHostRef.collection('coupons').doc(couponCode), couponHoldKey);
1730
1749
  }
1731
- const discountHoldKey = String((_ref8 = object == null ? void 0 : (_object_metadata9 = object.metadata) == null ? void 0 : _object_metadata9.discountHoldKey) != null ? _ref8 : '');
1732
- const discountId = String((_ref9 = object == null ? void 0 : (_object_metadata10 = object.metadata) == null ? void 0 : _object_metadata10.discountId) != null ? _ref9 : '');
1750
+ const discountHoldKey = String((_ref4 = object == null ? void 0 : (_object_metadata9 = object.metadata) == null ? void 0 : _object_metadata9.discountHoldKey) != null ? _ref4 : '');
1751
+ const discountId = String((_ref5 = object == null ? void 0 : (_object_metadata10 = object.metadata) == null ? void 0 : _object_metadata10.discountId) != null ? _ref5 : '');
1733
1752
  if (discountHoldKey && discountId) {
1734
1753
  await releasePromotionHold(expiredHostRef.collection('discounts').doc(discountId), discountHoldKey);
1735
1754
  }
@@ -1737,8 +1756,8 @@ async function findOrderForDispute(paymentIntentId) {
1737
1756
  // event and AGL-2449 left it to the TTL alone. A shopper whose abandoned
1738
1757
  // checkout stands their own balance off for a day cannot be told why
1739
1758
  // either, and the release is the identical delete sentinel.
1740
- const giftCardHoldKey = String((_ref10 = object == null ? void 0 : (_object_metadata11 = object.metadata) == null ? void 0 : _object_metadata11.giftCardHoldKey) != null ? _ref10 : '');
1741
- const giftCardCode = String((_ref11 = object == null ? void 0 : (_object_metadata12 = object.metadata) == null ? void 0 : _object_metadata12.giftCardCode) != null ? _ref11 : '');
1759
+ const giftCardHoldKey = String((_ref6 = object == null ? void 0 : (_object_metadata11 = object.metadata) == null ? void 0 : _object_metadata11.giftCardHoldKey) != null ? _ref6 : '');
1760
+ const giftCardCode = String((_ref7 = object == null ? void 0 : (_object_metadata12 = object.metadata) == null ? void 0 : _object_metadata12.giftCardCode) != null ? _ref7 : '');
1742
1761
  if (giftCardHoldKey && giftCardCode) {
1743
1762
  await expiredHostRef.collection('giftCards').doc(giftCardCode).set({
1744
1763
  holds: {
@@ -1791,9 +1810,9 @@ async function findOrderForDispute(paymentIntentId) {
1791
1810
  var _object_metadata14;
1792
1811
  const subHostId = object == null ? void 0 : (_object_metadata14 = object.metadata) == null ? void 0 : _object_metadata14.hostId;
1793
1812
  if (subHostId) {
1794
- var _ref12;
1813
+ var _ref8;
1795
1814
  await firebaseAdmin.app().firestore().collection('hosts').doc(String(subHostId)).collection('subscriptions').doc(String(object.id)).set({
1796
- status: type === 'customer.subscription.deleted' ? 'canceled' : String((_ref12 = object == null ? void 0 : object.status) != null ? _ref12 : 'active'),
1815
+ status: type === 'customer.subscription.deleted' ? 'canceled' : String((_ref8 = object == null ? void 0 : object.status) != null ? _ref8 : 'active'),
1797
1816
  currentPeriodEndMs: (object == null ? void 0 : object.current_period_end) ? object.current_period_end * 1000 : null
1798
1817
  }, {
1799
1818
  merge: true
@@ -1807,7 +1826,7 @@ async function findOrderForDispute(paymentIntentId) {
1807
1826
  var _object_metadata15;
1808
1827
  const { hostId, productId } = (_object_metadata15 = object.metadata) != null ? _object_metadata15 : {};
1809
1828
  if (hostId && productId) {
1810
- var _productForSnapshot_data, _ref13, _productForSnapshot_get, _subTotals_totalCents, _ref14;
1829
+ var _productForSnapshot_data, _ref9, _productForSnapshot_get, _subTotals_totalCents, _productForSnapshot_get1, _ref10;
1811
1830
  var _object_metadata16, _liftedForSnapshot_subscription, _object_customer_details, _object_customer_details1, _object_customer_details2;
1812
1831
  const firestore = firebaseAdmin.app().firestore();
1813
1832
  const hostRef = firestore.collection('hosts').doc(String(hostId));
@@ -1838,7 +1857,7 @@ async function findOrderForDispute(paymentIntentId) {
1838
1857
  name: 'Product'
1839
1858
  });
1840
1859
  const soldVariant = ((_object_metadata16 = object.metadata) == null ? void 0 : _object_metadata16.variantId) ? liftedForSnapshot.variants.find((item)=>item.id === String(object.metadata.variantId)) : liftedForSnapshot.variants[0];
1841
- const variantOptions = Object.values((_ref13 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref13 : {});
1860
+ const variantOptions = Object.values((_ref9 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref9 : {});
1842
1861
  const { lineItems: subLineItems, totals: subTotals } = CommerceModel.computeBuyNowOrder(object, _extends({
1843
1862
  name: String((_productForSnapshot_get = productForSnapshot.get('name')) != null ? _productForSnapshot_get : 'Product')
1844
1863
  }, variantOptions.length ? {
@@ -1924,16 +1943,17 @@ async function findOrderForDispute(paymentIntentId) {
1924
1943
  });
1925
1944
  if (!recorded) return;
1926
1945
  const subscriptionCents = Number((_subTotals_totalCents = subTotals.totalCents) != null ? _subTotals_totalCents : 0);
1927
- void notifyHostManagers(String(hostId), _extends({
1946
+ const subscriptionPrice = `$${(subscriptionCents / 100).toFixed(2)}${((_liftedForSnapshot_subscription = liftedForSnapshot.subscription) == null ? void 0 : _liftedForSnapshot_subscription.interval) ? `/${liftedForSnapshot.subscription.interval}` : ''}`;
1947
+ const subscriberEmail = object == null ? void 0 : (_object_customer_details = object.customer_details) == null ? void 0 : _object_customer_details.email;
1948
+ void notifyHostManagers(String(hostId), {
1928
1949
  type: 'content.order',
1929
1950
  // The amount rides the title exactly as the order notification's
1930
1951
  // does (AGL-1732) — "New subscriber" alone never said what for.
1931
- title: `New subscriber — $${(subscriptionCents / 100).toFixed(2)}${((_liftedForSnapshot_subscription = liftedForSnapshot.subscription) == null ? void 0 : _liftedForSnapshot_subscription.interval) ? `/${liftedForSnapshot.subscription.interval}` : ''}`
1932
- }, (object == null ? void 0 : (_object_customer_details = object.customer_details) == null ? void 0 : _object_customer_details.email) ? {
1933
- body: object.customer_details.email
1934
- } : {}, {
1952
+ title: `New subscriber on {site} — ${subscriptionPrice}`,
1953
+ // Who subscribed, to what and where (AGL-3432).
1954
+ body: `${subscriberEmail || 'A shopper'} subscribed to ` + `${String((_productForSnapshot_get1 = productForSnapshot.get('name')) != null ? _productForSnapshot_get1 : 'a product')} on ` + `{site} for ${subscriptionPrice}.`,
1935
1955
  link: `/${hostId}/products`
1936
- }));
1956
+ });
1937
1957
  // AWAITED SINCE AGL-2473, here and at the five sibling call sites in
1938
1958
  // this file. `upsertHostContact` carries `purchaseCents`, so it is what
1939
1959
  // feeds `ltvCents` and the RFM ranking — a `void`ed one is a paying
@@ -1944,7 +1964,7 @@ async function findOrderForDispute(paymentIntentId) {
1944
1964
  await captureHostContact(_extends({
1945
1965
  hostId: String(hostId),
1946
1966
  email: object == null ? void 0 : (_object_customer_details1 = object.customer_details) == null ? void 0 : _object_customer_details1.email,
1947
- name: (_ref14 = object == null ? void 0 : (_object_customer_details2 = object.customer_details) == null ? void 0 : _object_customer_details2.name) != null ? _ref14 : undefined,
1967
+ name: (_ref10 = object == null ? void 0 : (_object_customer_details2 = object.customer_details) == null ? void 0 : _object_customer_details2.name) != null ? _ref10 : undefined,
1948
1968
  source: 'order',
1949
1969
  // Every order door names `customer` (AGL-2612): a floor, so the
1950
1970
  // stage fills or advances and never moves anybody back.
@@ -1993,23 +2013,23 @@ async function findOrderForDispute(paymentIntentId) {
1993
2013
  // is enabled). Handling both means the branch fires whichever is on; the
1994
2014
  // invoice-id guard means having both on records the cycle once.
1995
2015
  if (type === 'invoice.payment_succeeded' || type === 'invoice.paid') {
1996
- var _ref15, _ref16, _ref17, _ref18, _ref19, _ref20;
2016
+ var _ref11, _ref12, _ref13, _ref14, _ref15, _ref16;
1997
2017
  var _object_subscription_details, _object_parent_subscription_details, _object_parent, _object_parent_subscription_details1, _object_parent1;
1998
2018
  // The SUBSCRIPTION's metadata, which is what `checkout.ts` sets
1999
2019
  // (`subscription_data[metadata]`) — the invoice's own `metadata` is a
2000
2020
  // different, empty bag. `parent.subscription_details` is where newer API
2001
2021
  // versions moved it; both are read because the endpoint's version is not
2002
2022
  // visible from here.
2003
- const subscriptionMeta = (_ref15 = (_ref16 = object == null ? void 0 : (_object_subscription_details = object.subscription_details) == null ? void 0 : _object_subscription_details.metadata) != null ? _ref16 : object == null ? void 0 : (_object_parent = object.parent) == null ? void 0 : (_object_parent_subscription_details = _object_parent.subscription_details) == null ? void 0 : _object_parent_subscription_details.metadata) != null ? _ref15 : null;
2004
- const invoiceId = String((_ref17 = object == null ? void 0 : object.id) != null ? _ref17 : '');
2005
- const subscriptionId = String((_ref18 = (_ref19 = object == null ? void 0 : object.subscription) != null ? _ref19 : object == null ? void 0 : (_object_parent1 = object.parent) == null ? void 0 : (_object_parent_subscription_details1 = _object_parent1.subscription_details) == null ? void 0 : _object_parent_subscription_details1.subscription) != null ? _ref18 : '');
2006
- const invoiceHostId = String((_ref20 = subscriptionMeta == null ? void 0 : subscriptionMeta.hostId) != null ? _ref20 : '');
2023
+ const subscriptionMeta = (_ref11 = (_ref12 = object == null ? void 0 : (_object_subscription_details = object.subscription_details) == null ? void 0 : _object_subscription_details.metadata) != null ? _ref12 : object == null ? void 0 : (_object_parent = object.parent) == null ? void 0 : (_object_parent_subscription_details = _object_parent.subscription_details) == null ? void 0 : _object_parent_subscription_details.metadata) != null ? _ref11 : null;
2024
+ const invoiceId = String((_ref13 = object == null ? void 0 : object.id) != null ? _ref13 : '');
2025
+ const subscriptionId = String((_ref14 = (_ref15 = object == null ? void 0 : object.subscription) != null ? _ref15 : object == null ? void 0 : (_object_parent1 = object.parent) == null ? void 0 : (_object_parent_subscription_details1 = _object_parent1.subscription_details) == null ? void 0 : _object_parent_subscription_details1.subscription) != null ? _ref14 : '');
2026
+ const invoiceHostId = String((_ref16 = subscriptionMeta == null ? void 0 : subscriptionMeta.hostId) != null ? _ref16 : '');
2007
2027
  // Platform billing — Aglyn charging its own customers — runs through the
2008
2028
  // same endpoint and the same fan-out, and its invoices carry `orgId`
2009
2029
  // instead. Self-selection is on the same discriminator every other
2010
2030
  // section of this file uses.
2011
2031
  if ((subscriptionMeta == null ? void 0 : subscriptionMeta.type) === 'commerce-subscription' && invoiceHostId && subscriptionId && invoiceId) {
2012
- var _soldSnapshot_get, _ref21, _ref22, _ref23, _ref24, _ref25;
2032
+ var _soldSnapshot_get, _ref17, _ref18, _ref19, _ref20, _ref21;
2013
2033
  var _this, _this1, _object_status_transitions, _object_customer_shipping;
2014
2034
  const firestore = firebaseAdmin.app().firestore();
2015
2035
  const hostRef = firestore.collection('hosts').doc(invoiceHostId);
@@ -2060,7 +2080,7 @@ async function findOrderForDispute(paymentIntentId) {
2060
2080
  // and only for them.
2061
2081
  const soldSnapshot = await subscriptionRef.get();
2062
2082
  const soldLine = (_this1 = (_soldSnapshot_get = soldSnapshot.get('lineItems')) != null ? _soldSnapshot_get : []) == null ? void 0 : _this1[0];
2063
- const productId = String((_ref21 = (_ref22 = (_ref23 = soldLine == null ? void 0 : soldLine.productId) != null ? _ref23 : soldSnapshot.get('productId')) != null ? _ref22 : subscriptionMeta == null ? void 0 : subscriptionMeta.productId) != null ? _ref21 : '');
2083
+ const productId = String((_ref17 = (_ref18 = (_ref19 = soldLine == null ? void 0 : soldLine.productId) != null ? _ref19 : soldSnapshot.get('productId')) != null ? _ref18 : subscriptionMeta == null ? void 0 : subscriptionMeta.productId) != null ? _ref17 : '');
2064
2084
  let snapshot;
2065
2085
  if (soldLine) {
2066
2086
  snapshot = _extends({
@@ -2077,7 +2097,7 @@ async function findOrderForDispute(paymentIntentId) {
2077
2097
  productType: soldLine.productType
2078
2098
  } : {});
2079
2099
  } else {
2080
- var _ref26, _soldSnapshot_get1, _ref27, _ref28;
2100
+ var _ref22, _soldSnapshot_get1, _ref23, _ref24;
2081
2101
  // AGL-1763: no product id, no read. `doc('__missing__')` was a
2082
2102
  // deliberate miss that Firestore does not permit — a document id
2083
2103
  // matching `__.*__` is RESERVED, so the backend rejects the path with
@@ -2090,18 +2110,18 @@ async function findOrderForDispute(paymentIntentId) {
2090
2110
  // exactly the population this branch now serves: a subscription
2091
2111
  // Aglyn has no record of, whose metadata nothing of ours wrote.
2092
2112
  const productSnapshot = productId ? await hostRef.collection('products').doc(productId).get() : null;
2093
- const lifted = CommerceModel.liftLegacyProduct((_ref26 = productSnapshot == null ? void 0 : productSnapshot.data()) != null ? _ref26 : {
2113
+ const lifted = CommerceModel.liftLegacyProduct((_ref22 = productSnapshot == null ? void 0 : productSnapshot.data()) != null ? _ref22 : {
2094
2114
  name: 'Subscription'
2095
2115
  });
2096
2116
  const variantId = String((_soldSnapshot_get1 = soldSnapshot.get('variantId')) != null ? _soldSnapshot_get1 : '');
2097
2117
  const variant = variantId ? lifted.variants.find((item)=>item.id === variantId) : lifted.variants[0];
2098
- const variantOptions = Object.values((_ref27 = variant == null ? void 0 : variant.options) != null ? _ref27 : {});
2118
+ const variantOptions = Object.values((_ref23 = variant == null ? void 0 : variant.options) != null ? _ref23 : {});
2099
2119
  snapshot = _extends({
2100
2120
  productId
2101
2121
  }, variantId ? {
2102
2122
  variantId
2103
2123
  } : {}, {
2104
- name: String((_ref28 = productSnapshot == null ? void 0 : productSnapshot.get('name')) != null ? _ref28 : 'Subscription')
2124
+ name: String((_ref24 = productSnapshot == null ? void 0 : productSnapshot.get('name')) != null ? _ref24 : 'Subscription')
2105
2125
  }, variantOptions.length ? {
2106
2126
  variantLabel: variantOptions.join(' / ')
2107
2127
  } : {}, (variant == null ? void 0 : variant.sku) ? {
@@ -2123,8 +2143,8 @@ async function findOrderForDispute(paymentIntentId) {
2123
2143
  const invoiceTotals = _extends({}, computedInvoiceTotals, {
2124
2144
  feeCents: feeCentsInForce
2125
2145
  });
2126
- const paidCents = Math.max(0, Math.round(Number((_ref24 = object == null ? void 0 : object.amount_paid) != null ? _ref24 : 0)));
2127
- const billingReason = String((_ref25 = object == null ? void 0 : object.billing_reason) != null ? _ref25 : '');
2146
+ const paidCents = Math.max(0, Math.round(Number((_ref20 = object == null ? void 0 : object.amount_paid) != null ? _ref20 : 0)));
2147
+ const billingReason = String((_ref21 = object == null ? void 0 : object.billing_reason) != null ? _ref21 : '');
2128
2148
  // The subscription's FIRST invoice is also a paid invoice, and
2129
2149
  // `checkout.session.completed` has already counted that money into the
2130
2150
  // contact's lifetime value and already told the managers about it
@@ -2395,7 +2415,12 @@ async function findOrderForDispute(paymentIntentId) {
2395
2415
  // `renewalOrgUnreadable` and not `!renewalOrg` (AGL-2258): a null org
2396
2416
  // we successfully read still stops, which is AGL-2071's decision.
2397
2417
  if (!renewalEntitled && !renewalOrgUnreadable) {
2398
- await stopLapsedStorefrontSubscription(invoiceHostId, subscriptionId, subscriptionRef);
2418
+ var _ref25, _ref26;
2419
+ await stopLapsedStorefrontSubscription(invoiceHostId, subscriptionId, subscriptionRef, {
2420
+ subscriberEmail: (_ref25 = (_ref26 = object == null ? void 0 : object.customer_email) != null ? _ref26 : soldSnapshot.get('customerEmail')) != null ? _ref25 : null,
2421
+ productName: snapshot.name || 'a subscription product',
2422
+ org: renewalOrg != null ? renewalOrg : null
2423
+ });
2399
2424
  } else if (renewalEntitled) {
2400
2425
  // RE-PRICE THE PLATFORM FEE (AGL-2289). `application_fee_percent` was
2401
2426
  // set once at the sale and never revisited, and it lives on the
@@ -2444,8 +2469,8 @@ async function findOrderForDispute(paymentIntentId) {
2444
2469
  const stockVariantId = (_snapshot_variantId = snapshot.variantId) != null ? _snapshot_variantId : (_stocked_variants_ = stocked.variants[0]) == null ? void 0 : _stocked_variants_.id;
2445
2470
  const tracked = stocked.variants.some((variant)=>variant.id === stockVariantId && variant.inventory != null);
2446
2471
  if (stockVariantId && tracked) {
2447
- var _ref29;
2448
- const cycleQuantity = (_ref29 = cycleLine == null ? void 0 : cycleLine.quantity) != null ? _ref29 : 1;
2472
+ var _ref27;
2473
+ const cycleQuantity = (_ref27 = cycleLine == null ? void 0 : cycleLine.quantity) != null ? _ref27 : 1;
2449
2474
  // Atomic since AGL-2320, like every other decrement: a renewal
2450
2475
  // batch bills many subscribers at once, so two cycles of the same
2451
2476
  // physical box are exactly the concurrent pair that lost a
@@ -2470,20 +2495,20 @@ async function findOrderForDispute(paymentIntentId) {
2470
2495
  }
2471
2496
  }
2472
2497
  if (!isOpeningInvoice && paidCents > 0) {
2473
- var _ref30, _ref31;
2474
- const renewalEmail = (_ref30 = (_ref31 = object == null ? void 0 : object.customer_email) != null ? _ref31 : soldSnapshot.get('customerEmail')) != null ? _ref30 : null;
2498
+ var _ref28, _ref29;
2499
+ const renewalEmail = (_ref28 = (_ref29 = object == null ? void 0 : object.customer_email) != null ? _ref29 : soldSnapshot.get('customerEmail')) != null ? _ref28 : null;
2475
2500
  // A physical cycle now lands in Orders (AGL-1750), but the console
2476
2501
  // still has no Subscriptions tab, so for a digital or service
2477
2502
  // subscription this notification remains the only place a merchant
2478
2503
  // learns the money arrived at all.
2479
- void notifyHostManagers(invoiceHostId, _extends({
2504
+ const renewalPrice = `$${(paidCents / 100).toFixed(2)}${interval ? `/${interval}` : ''}`;
2505
+ void notifyHostManagers(invoiceHostId, {
2480
2506
  type: 'content.order',
2481
- title: `Subscription renewed — $${(paidCents / 100).toFixed(2)}${interval ? `/${interval}` : ''}`
2482
- }, renewalEmail ? {
2483
- body: String(renewalEmail)
2484
- } : {}, {
2507
+ title: `Subscription renewed on {site} — ${renewalPrice}`,
2508
+ // Whose subscription, to what and where (AGL-3432).
2509
+ body: `${renewalEmail ? `${String(renewalEmail)}'s` : 'A'} ` + `subscription to ${snapshot.name || 'a product'} on {site} ` + `renewed for ${renewalPrice}.`,
2485
2510
  link: `/${invoiceHostId}/products`
2486
- }));
2511
+ });
2487
2512
  // RFM (AGL-328): a subscriber in month 12 has paid twelve times, and
2488
2513
  // counting only the first charge ranks them as a one-purchase
2489
2514
  // customer forever. Keyed to the invoice so the guard above is what
@@ -2510,7 +2535,7 @@ async function findOrderForDispute(paymentIntentId) {
2510
2535
  var _object_metadata17;
2511
2536
  const { hostId, reservationId } = (_object_metadata17 = object.metadata) != null ? _object_metadata17 : {};
2512
2537
  if (hostId && reservationId) {
2513
- var _ref32, _ref33;
2538
+ var _ref30, _ref31;
2514
2539
  var _object_metadata18;
2515
2540
  const firestore = firebaseAdmin.app().firestore();
2516
2541
  const reservationRef = firestore.collection('hosts').doc(String(hostId)).collection('reservations').doc(String(reservationId));
@@ -2523,7 +2548,7 @@ async function findOrderForDispute(paymentIntentId) {
2523
2548
  // `amount_total` is stay-plus-tax and Stripe's own tax fields read
2524
2549
  // zero; the session's metadata is the only witness, exactly as it is
2525
2550
  // for a buy-now sale.
2526
- const taxCents = Math.max(0, Math.round(Number((_ref32 = object == null ? void 0 : (_object_metadata18 = object.metadata) == null ? void 0 : _object_metadata18.taxCents) != null ? _ref32 : 0)));
2551
+ const taxCents = Math.max(0, Math.round(Number((_ref30 = object == null ? void 0 : (_object_metadata18 = object.metadata) == null ? void 0 : _object_metadata18.taxCents) != null ? _ref30 : 0)));
2527
2552
  // What the guest actually handed over, MINUS that tax. This is the
2528
2553
  // DEPOSIT when the resource has one (`reserve.ts` charges
2529
2554
  // `depositCents || totalCents`), never the stay's `totalCents` — see
@@ -2537,7 +2562,7 @@ async function findOrderForDispute(paymentIntentId) {
2537
2562
  // are and under-state what the merchant collects at the register. The
2538
2563
  // same figure feeds the guest's lifetime value, which tax is no part
2539
2564
  // of either (AGL-1755).
2540
- const paidCents = Math.max(0, Math.round(Number((_ref33 = object == null ? void 0 : object.amount_total) != null ? _ref33 : 0)) - taxCents);
2565
+ const paidCents = Math.max(0, Math.round(Number((_ref31 = object == null ? void 0 : object.amount_total) != null ? _ref31 : 0)) - taxCents);
2541
2566
  // Redelivery guard (AGL-1755), the AGL-1748 shape. The `pending` ->
2542
2567
  // `confirmed` transition was always the guard, but it was a
2543
2568
  // read-then-write with every side effect hanging off it, so two
@@ -2608,17 +2633,44 @@ async function findOrderForDispute(paymentIntentId) {
2608
2633
  return true;
2609
2634
  });
2610
2635
  if (confirmedNow) {
2611
- var _ref34, _ref35;
2636
+ var _reservation_nights, _reservation_totalCents, _ref32, _ref33, _ref34, _ref35, _ref36;
2612
2637
  var _object_customer_details3, _object_customer_details4, _object_customer_details5;
2613
2638
  const reservation = reserved != null ? reserved : {};
2614
- void notifyHostManagers(String(hostId), _extends({
2639
+ // A day key, so its UTC date IS the check-in date.
2640
+ const checkInShort = new Date(Number(reservation['checkInDayMs'])).toUTCString().slice(0, 16);
2641
+ const nights = Number((_reservation_nights = reservation['nights']) != null ? _reservation_nights : 0);
2642
+ const nightsText = `${nights} night${nights === 1 ? '' : 's'}`;
2643
+ /*
2644
+ * WHAT THE GUEST WAS CHARGED TODAY, tax included (AGL-3442).
2645
+ *
2646
+ * `paidCents` is the money applied to the STAY and leaves out the
2647
+ * merchant's lodging tax, so a guest told only that figure is told
2648
+ * less than their card was charged. The two together are Stripe's
2649
+ * session total, and the tax is the figure `reserve.ts` charged as
2650
+ * its own line, named so the guest can match it to their receipt.
2651
+ * The balance below is the stay's alone: the tax is charged on what
2652
+ * is collected at booking, and nothing computes one on what is
2653
+ * collected at the property.
2654
+ */ const charged = `$${((paidCents + taxCents) / 100).toFixed(2)}`;
2655
+ const taxIncluded = taxCents > 0 ? `, including $${(taxCents / 100).toFixed(2)} lodging tax` : '';
2656
+ const paid = `${charged}${taxIncluded}`;
2657
+ // What the guest still owes for the stay: the console's
2658
+ // reservations card collects `totalCents - paidCents` at the
2659
+ // property, so the guest is told it too (AGL-3432).
2660
+ const balanceCents = Math.max(0, Math.round(Number((_reservation_totalCents = reservation['totalCents']) != null ? _reservation_totalCents : 0)) - paidCents);
2661
+ const balance = `$${(balanceCents / 100).toFixed(2)}`;
2662
+ const reservedBy = (_ref32 = (_ref33 = object == null ? void 0 : (_object_customer_details3 = object.customer_details) == null ? void 0 : _object_customer_details3.email) != null ? _ref33 : reservation['guestEmail']) != null ? _ref32 : 'A guest';
2663
+ // Which room or unit was reserved (AGL-3432): on a property with
2664
+ // several, "a stay" does not say which one to get ready.
2665
+ const resourceName = reservation['resourceId'] ? String((_ref34 = await firestore.collection('hosts').doc(String(hostId)).collection('resources').doc(String(reservation['resourceId'])).get().then((snapshot)=>snapshot.get('name')).catch(()=>'')) != null ? _ref34 : '').trim() : '';
2666
+ void notifyHostManagers(String(hostId), {
2615
2667
  type: 'content.booking',
2616
- title: 'New reservation'
2617
- }, (object == null ? void 0 : (_object_customer_details3 = object.customer_details) == null ? void 0 : _object_customer_details3.email) ? {
2618
- body: object.customer_details.email
2619
- } : {}, {
2668
+ title: 'New reservation on {site}',
2669
+ // Who, what, when, what was paid and what is still owed
2670
+ // (AGL-3432).
2671
+ body: `${reservedBy} reserved ${resourceName || 'a stay'} on {site}: check-in ` + `${checkInShort}, ${nightsText}. ${charged} was paid${taxIncluded}` + (balanceCents > 0 ? `; ${balance} is still to collect at the property.` : '.'),
2620
2672
  link: `/${hostId}/products`
2621
- }));
2673
+ });
2622
2674
  // Contacts ingestion (AGL-1755): this branch stored `paidCents` from
2623
2675
  // `amount_total` and then called `upsertHostContact` with no amount,
2624
2676
  // so a guest who paid for a stay showed a lifetime value of zero. A
@@ -2648,14 +2700,14 @@ async function findOrderForDispute(paymentIntentId) {
2648
2700
  // exactly once. Taking `totalCents` here is what WOULD double-count:
2649
2701
  // it would claim money the guest has not paid yet and will pay again
2650
2702
  // through the register.
2651
- const guestContactEmail = (_ref34 = (_ref35 = object == null ? void 0 : (_object_customer_details4 = object.customer_details) == null ? void 0 : _object_customer_details4.email) != null ? _ref35 : reservation['guestEmail']) != null ? _ref34 : null;
2703
+ const guestContactEmail = (_ref35 = (_ref36 = object == null ? void 0 : (_object_customer_details4 = object.customer_details) == null ? void 0 : _object_customer_details4.email) != null ? _ref36 : reservation['guestEmail']) != null ? _ref35 : null;
2652
2704
  if (guestContactEmail) {
2653
- var _ref36, _ref37;
2705
+ var _ref37, _ref38;
2654
2706
  var _object_customer_details6;
2655
2707
  await captureHostContact(_extends({
2656
2708
  hostId: String(hostId),
2657
2709
  email: guestContactEmail,
2658
- name: (_ref36 = (_ref37 = object == null ? void 0 : (_object_customer_details6 = object.customer_details) == null ? void 0 : _object_customer_details6.name) != null ? _ref37 : reservation['guestName']) != null ? _ref36 : undefined,
2710
+ name: (_ref37 = (_ref38 = object == null ? void 0 : (_object_customer_details6 = object.customer_details) == null ? void 0 : _object_customer_details6.name) != null ? _ref38 : reservation['guestName']) != null ? _ref37 : undefined,
2659
2711
  source: 'booking',
2660
2712
  // A paid stay is a sale (AGL-2612), whatever the source says
2661
2713
  // about the kind of thing that was bought.
@@ -2671,21 +2723,22 @@ async function findOrderForDispute(paymentIntentId) {
2671
2723
  }
2672
2724
  const guestEmail = object == null ? void 0 : (_object_customer_details5 = object.customer_details) == null ? void 0 : _object_customer_details5.email;
2673
2725
  if (guestEmail) {
2674
- var _reservation_nights, _ref38;
2675
- const checkIn = new Date(Number(reservation['checkInDayMs'])).toUTCString();
2676
- const paid = `$${(paidCents / 100).toFixed(2)}`;
2677
- const checkInShort = checkIn.slice(0, 16);
2678
- const fallbackText = `Your stay is confirmed!\n\nCheck-in: ${checkInShort}\n` + `Nights: ${reservation['nights']}\n` + `Paid today: ${paid}\n` + `Reference: ${reservationId}`;
2726
+ var _reservation_nights1, _ref39;
2727
+ // The balance as a whole line, empty when the stay is paid in
2728
+ // full so the built-in copy leaves it out.
2729
+ const balanceNote = balanceCents > 0 ? `Still to pay: ${balance}, at the property. It has not been charged.` : '';
2730
+ const fallbackText = `Your stay is confirmed!\n\nCheck-in: ${checkInShort}\n` + `Nights: ${reservation['nights']}\n` + `Paid today: ${paid}\n` + (balanceNote ? `${balanceNote}\n` : '') + `Reference: ${reservationId}`;
2679
2731
  // Site-owner-designed template when published (AGL-771).
2680
2732
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'reservation-confirmed', {
2681
2733
  'reservation.checkIn': checkInShort,
2682
- 'reservation.nights': String((_reservation_nights = reservation['nights']) != null ? _reservation_nights : ''),
2734
+ 'reservation.nights': String((_reservation_nights1 = reservation['nights']) != null ? _reservation_nights1 : ''),
2683
2735
  'reservation.paid': paid,
2736
+ 'reservation.balance': balanceNote,
2684
2737
  'reservation.ref': String(reservationId)
2685
2738
  });
2686
2739
  await sendEmail(_extends({
2687
2740
  to: guestEmail,
2688
- subject: (_ref38 = designed == null ? void 0 : designed.subject) != null ? _ref38 : 'Reservation confirmed',
2741
+ subject: (_ref39 = designed == null ? void 0 : designed.subject) != null ? _ref39 : 'Reservation confirmed',
2689
2742
  text: (designed == null ? void 0 : designed.text) || fallbackText
2690
2743
  }, (designed == null ? void 0 : designed.html) ? {
2691
2744
  html: designed.html
@@ -2711,7 +2764,7 @@ async function findOrderForDispute(paymentIntentId) {
2711
2764
  var _object_metadata19;
2712
2765
  const { hostId, cartId, feeCents, couponCode } = (_object_metadata19 = object.metadata) != null ? _object_metadata19 : {};
2713
2766
  if (hostId && cartId) {
2714
- var _cartSnapshot_data, _ref39, _ref40, _ref41, _ref42, _ref43, _ref44;
2767
+ var _cartSnapshot_data, _ref40, _ref41, _ref42, _ref43, _ref44, _ref45;
2715
2768
  var _object_customer_details7, _object_metadata20, _object_customer_details8, _object_customer_details9, _object_customer_details10, _object_metadata21, _this2, _object_metadata22;
2716
2769
  const firestore = firebaseAdmin.app().firestore();
2717
2770
  const hostRef = firestore.collection('hosts').doc(String(hostId));
@@ -2777,7 +2830,11 @@ async function findOrderForDispute(paymentIntentId) {
2777
2830
  unitAmountCents: Math.round(Number((_ref = variant == null ? void 0 : variant.priceUsd) != null ? _ref : 0) * 100)
2778
2831
  });
2779
2832
  }).filter(Boolean);
2780
- const shipping = (_ref39 = object == null ? void 0 : object.shipping_details) != null ? _ref39 : object == null ? void 0 : object.customer_details;
2833
+ const shipping = (_ref40 = object == null ? void 0 : object.shipping_details) != null ? _ref40 : object == null ? void 0 : object.customer_details;
2834
+ // The number the merchant knows this order by, carried out of the
2835
+ // transaction for the notices below (AGL-3432). The document id is
2836
+ // the Stripe session id, which appears nowhere in the console.
2837
+ let cartOrderNumber;
2781
2838
  const created = await firestore.runTransaction(async (transaction)=>{
2782
2839
  var _counter_get, _ref, _ref1, _ref2, _ref3, _ref4, _shipping_address_line1, _shipping_address_line2, _shipping_address_city, _shipping_address_state, _shipping_address_postal_code, _shipping_address_country, _ref5;
2783
2840
  var _object_metadata, _object_customer_details, _object_customer_details1;
@@ -2787,6 +2844,7 @@ async function findOrderForDispute(paymentIntentId) {
2787
2844
  ]);
2788
2845
  if (existing.exists) return false;
2789
2846
  const number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
2847
+ cartOrderNumber = number;
2790
2848
  transaction.set(counterRef, {
2791
2849
  next: number + 1
2792
2850
  }, {
@@ -2861,6 +2919,9 @@ async function findOrderForDispute(paymentIntentId) {
2861
2919
  // the non-idempotent effects below (inventory / coupon / gift-card
2862
2920
  // decrements) that would otherwise double-apply.
2863
2921
  if (!created) return;
2922
+ const cartOrderLabel = CommerceModel.formatOrderNumber({
2923
+ number: cartOrderNumber
2924
+ }, String(object.id));
2864
2925
  // AGL-2149: loud, not silent. Logged for the platform and pushed to the
2865
2926
  // merchant, once, behind the same `created` guard as every other
2866
2927
  // non-idempotent effect so a redelivery does not re-nag.
@@ -2873,7 +2934,7 @@ async function findOrderForDispute(paymentIntentId) {
2873
2934
  void notifyHostManagers(String(hostId), {
2874
2935
  type: 'content.order',
2875
2936
  title: 'A paid order is missing items',
2876
- body: `Order ${object.id} was charged in full but ` + `${unresolvedLines.length} ${unresolvedLines.length === 1 ? 'line' : 'lines'} could not be recorded — the product was deleted during ` + 'checkout. Refund the difference or fulfill it by hand.',
2937
+ body: `Order ${cartOrderLabel} on {site} was charged in full, but ` + `${unresolvedLines.length} of its lines could not be recorded ` + 'because the product was deleted during ' + 'checkout. Refund the difference or fulfill it by hand.',
2877
2938
  link: `/${hostId}/products`
2878
2939
  });
2879
2940
  }
@@ -2915,10 +2976,10 @@ async function findOrderForDispute(paymentIntentId) {
2915
2976
  // matters, and the order carries the buyer identity anyway.
2916
2977
  const licenseKeysByProduct = {};
2917
2978
  for (const line of lineItems){
2918
- var _ref45;
2979
+ var _ref46;
2919
2980
  var _object_customer_details11;
2920
2981
  if (line.productType !== 'digital') continue;
2921
- const keys = await assignLicenseKeys(firestore, hostRef, String(hostId), line.productId, String(object.id), (_ref45 = object == null ? void 0 : (_object_customer_details11 = object.customer_details) == null ? void 0 : _object_customer_details11.email) != null ? _ref45 : null, line.quantity);
2982
+ const keys = await assignLicenseKeys(firestore, hostRef, String(hostId), line.productId, String(object.id), (_ref46 = object == null ? void 0 : (_object_customer_details11 = object.customer_details) == null ? void 0 : _object_customer_details11.email) != null ? _ref46 : null, line.quantity);
2922
2983
  if (keys.length) licenseKeysByProduct[line.productId] = keys;
2923
2984
  }
2924
2985
  if (Object.keys(licenseKeysByProduct).length) {
@@ -2931,9 +2992,9 @@ async function findOrderForDispute(paymentIntentId) {
2931
2992
  // Branded receipt (AGL-296): env-gated like every outbound email.
2932
2993
  const buyerEmailForReceipt = object == null ? void 0 : (_object_customer_details7 = object.customer_details) == null ? void 0 : _object_customer_details7.email;
2933
2994
  if (isEmailConfigured() && buyerEmailForReceipt) {
2934
- var _ref46, _ref47, _ref48, _ref49;
2995
+ var _ref47, _ref48, _ref49, _ref50;
2935
2996
  const receiptSettings = await hostRef.collection('settings').doc('store').get().catch(()=>null);
2936
- const receiptFooter = String((_ref46 = receiptSettings == null ? void 0 : receiptSettings.get('receiptFooter')) != null ? _ref46 : '');
2997
+ const receiptFooter = String((_ref47 = receiptSettings == null ? void 0 : receiptSettings.get('receiptFooter')) != null ? _ref47 : '');
2937
2998
  const linesText = lineItems.map((line)=>`${line.quantity}× ${line.name}${line.variantLabel ? ` (${line.variantLabel})` : ''} — $${(line.unitAmountCents * line.quantity / 100).toFixed(2)}`).join('\n');
2938
2999
  // The keys assigned above the gate (AGL-2149); the receipt only
2939
3000
  // reports them.
@@ -2947,9 +3008,9 @@ async function findOrderForDispute(paymentIntentId) {
2947
3008
  // Digital delivery links (AGL-302); reuse the canonical mint the
2948
3009
  // tenant download endpoint verifies so the secret can never drift.
2949
3010
  const downloadToken = mintDownloadToken(hostId, String(object.id));
2950
- const siteOrigin = String((_ref47 = object == null ? void 0 : object.success_url) != null ? _ref47 : '').replace(/\/\?.*$|\?.*$/, '');
3011
+ const siteOrigin = String((_ref48 = object == null ? void 0 : object.success_url) != null ? _ref48 : '').replace(/\/\?.*$|\?.*$/, '');
2951
3012
  const downloadLines = lineItems.filter((line)=>line.productType === 'digital').map((line)=>`Download ${line.name}: ${siteOrigin}/api/commerce/download` + `?hostId=${hostId}&orderId=${object.id}` + `&productId=${line.productId}&token=${downloadToken}`).join('\n');
2952
- const orderTotal = `$${(Number((_ref48 = object == null ? void 0 : object.amount_total) != null ? _ref48 : 0) / 100).toFixed(2)}`;
3013
+ const orderTotal = `$${(Number((_ref49 = object == null ? void 0 : object.amount_total) != null ? _ref49 : 0) / 100).toFixed(2)}`;
2953
3014
  const orderSummary = [
2954
3015
  linesText,
2955
3016
  licenseText,
@@ -2961,11 +3022,15 @@ async function findOrderForDispute(paymentIntentId) {
2961
3022
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'order-receipt', {
2962
3023
  'order.summary': orderSummary,
2963
3024
  'order.total': orderTotal,
2964
- 'order.ref': String(object.id)
3025
+ 'order.ref': String(object.id),
3026
+ // The store's own Receipt footer (AGL-3432), as a token: the
3027
+ // built-in copy always renders (AGL-3370), so a footer that is
3028
+ // only in the fallback text above never reaches a buyer.
3029
+ 'store.receiptFooter': receiptFooter
2965
3030
  });
2966
3031
  await sendEmail(_extends({
2967
3032
  to: buyerEmailForReceipt,
2968
- subject: (_ref49 = designed == null ? void 0 : designed.subject) != null ? _ref49 : `Receipt for your order`,
3033
+ subject: (_ref50 = designed == null ? void 0 : designed.subject) != null ? _ref50 : `Receipt for your order`,
2969
3034
  text: (designed == null ? void 0 : designed.text) || fallbackText
2970
3035
  }, (designed == null ? void 0 : designed.html) ? {
2971
3036
  html: designed.html
@@ -3042,37 +3107,38 @@ async function findOrderForDispute(paymentIntentId) {
3042
3107
  // is byte-identical to what it was before this issue for exactly that
3043
3108
  // reason. The hold refuses the SECOND shopper; it never gates the
3044
3109
  // first one's goods.
3045
- await releaseStockHold(hostRef, String((_ref40 = (_object_metadata20 = object.metadata) == null ? void 0 : _object_metadata20.stockHoldKey) != null ? _ref40 : ''));
3046
- void notifyHostManagers(String(hostId), _extends({
3110
+ await releaseStockHold(hostRef, String((_ref41 = (_object_metadata20 = object.metadata) == null ? void 0 : _object_metadata20.stockHoldKey) != null ? _ref41 : ''));
3111
+ const cartOrderTotal = `$${(Number((_ref42 = object == null ? void 0 : object.amount_total) != null ? _ref42 : 0) / 100).toFixed(2)}`;
3112
+ const cartUnits = lineItems.reduce((sum, line)=>sum + line.quantity, 0);
3113
+ void notifyHostManagers(String(hostId), {
3047
3114
  type: 'content.order',
3048
- title: `New order — $${(Number((_ref41 = object == null ? void 0 : object.amount_total) != null ? _ref41 : 0) / 100).toFixed(2)}`
3049
- }, (object == null ? void 0 : (_object_customer_details8 = object.customer_details) == null ? void 0 : _object_customer_details8.email) ? {
3050
- body: `From ${object.customer_details.email}`
3051
- } : {}, {
3115
+ title: `New order on {site} — ${cartOrderTotal}`,
3116
+ // What came in, from whom and where (AGL-3432).
3117
+ body: `Order ${cartOrderLabel} came in on {site}: ${cartUnits} ` + `item${cartUnits === 1 ? '' : 's'}, ${cartOrderTotal}` + ((object == null ? void 0 : (_object_customer_details8 = object.customer_details) == null ? void 0 : _object_customer_details8.email) ? `, from ${object.customer_details.email}.` : '.'),
3052
3118
  link: `/${hostId}/products`
3053
- }));
3119
+ });
3054
3120
  await captureHostContact(_extends({
3055
3121
  hostId: String(hostId),
3056
3122
  email: object == null ? void 0 : (_object_customer_details9 = object.customer_details) == null ? void 0 : _object_customer_details9.email,
3057
- name: (_ref42 = object == null ? void 0 : (_object_customer_details10 = object.customer_details) == null ? void 0 : _object_customer_details10.name) != null ? _ref42 : undefined,
3123
+ name: (_ref43 = object == null ? void 0 : (_object_customer_details10 = object.customer_details) == null ? void 0 : _object_customer_details10.name) != null ? _ref43 : undefined,
3058
3124
  source: 'order',
3059
3125
  initialLifecycleStage: 'customer'
3060
3126
  }, marketingOptIn ? {
3061
3127
  marketingConsent: true
3062
3128
  } : {}, {
3063
- purchaseCents: Number((_ref43 = object == null ? void 0 : object.amount_total) != null ? _ref43 : 0),
3129
+ purchaseCents: Number((_ref44 = object == null ? void 0 : object.amount_total) != null ? _ref44 : 0),
3064
3130
  interaction: {
3065
3131
  refId: String(object.id),
3066
- summary: `Placed an order ($${(Number((_ref44 = object == null ? void 0 : object.amount_total) != null ? _ref44 : 0) / 100).toFixed(2)})`
3132
+ summary: `Placed an order ($${(Number((_ref45 = object == null ? void 0 : object.amount_total) != null ? _ref45 : 0) / 100).toFixed(2)})`
3067
3133
  }
3068
3134
  }));
3069
3135
  if (couponCode) {
3070
- var _ref50;
3136
+ var _ref51;
3071
3137
  var _object_metadata23;
3072
3138
  await settleRedemption({
3073
3139
  firestore,
3074
3140
  ref: hostRef.collection('coupons').doc(String(couponCode)),
3075
- holdKey: String((_ref50 = (_object_metadata23 = object.metadata) == null ? void 0 : _object_metadata23.couponHoldKey) != null ? _ref50 : ''),
3141
+ holdKey: String((_ref51 = (_object_metadata23 = object.metadata) == null ? void 0 : _object_metadata23.couponHoldKey) != null ? _ref51 : ''),
3076
3142
  orderRef,
3077
3143
  label: `coupon ${couponCode}`,
3078
3144
  detail: `Coupon ${couponCode} was applied to this order but no longer ` + 'exists, so the redemption is uncounted against its limit.'
@@ -3080,10 +3146,10 @@ async function findOrderForDispute(paymentIntentId) {
3080
3146
  }
3081
3147
  // Gift card balance settlement (AGL-322, made a settlement by AGL-2449).
3082
3148
  if ((_object_metadata21 = object.metadata) == null ? void 0 : _object_metadata21.giftCardCode) {
3083
- var _ref51, _ref52;
3149
+ var _ref52, _ref53;
3084
3150
  var _object_metadata24, _object_metadata25;
3085
- const giftCardCents = Number((_ref51 = (_object_metadata24 = object.metadata) == null ? void 0 : _object_metadata24.giftCardCents) != null ? _ref51 : 0);
3086
- const holdKey = String((_ref52 = (_object_metadata25 = object.metadata) == null ? void 0 : _object_metadata25.giftCardHoldKey) != null ? _ref52 : '');
3151
+ const giftCardCents = Number((_ref52 = (_object_metadata24 = object.metadata) == null ? void 0 : _object_metadata24.giftCardCents) != null ? _ref52 : 0);
3152
+ const holdKey = String((_ref53 = (_object_metadata25 = object.metadata) == null ? void 0 : _object_metadata25.giftCardHoldKey) != null ? _ref53 : '');
3087
3153
  const cardRef = hostRef.collection('giftCards').doc(String(object.metadata.giftCardCode));
3088
3154
  const orphanNote = `$${(giftCardCents / 100).toFixed(2)} was applied from gift card ` + `${object.metadata.giftCardCode}, which no longer exists. The ` + 'balance was not deducted from any card.';
3089
3155
  if (!holdKey) {
@@ -3163,7 +3229,7 @@ async function findOrderForDispute(paymentIntentId) {
3163
3229
  const lineProduct = productsById.get(line.productId);
3164
3230
  if (!(lineProduct == null ? void 0 : lineProduct.giftCard)) continue;
3165
3231
  for(let unit = 0; unit < line.quantity; unit += 1){
3166
- var _ref53, _ref54;
3232
+ var _ref54, _ref55;
3167
3233
  var _object_customer_details12, _object_customer_details13, _object_customer_details14;
3168
3234
  const code = `GC-${createHmac('sha256', String(object.id)).update(`${line.productId}:${unit}:${Date.now()}`).digest('hex').slice(0, 12).toUpperCase()}`;
3169
3235
  // THE EMAIL IS GATED ON THE WRITE (AGL-2161). This `.set()` used
@@ -3182,13 +3248,13 @@ async function findOrderForDispute(paymentIntentId) {
3182
3248
  const issued = await hostRef.collection('giftCards').doc(code).set({
3183
3249
  initialCents: line.unitAmountCents,
3184
3250
  balanceCents: line.unitAmountCents,
3185
- recipientEmail: (_ref53 = object == null ? void 0 : (_object_customer_details12 = object.customer_details) == null ? void 0 : _object_customer_details12.email) != null ? _ref53 : null,
3251
+ recipientEmail: (_ref54 = object == null ? void 0 : (_object_customer_details12 = object.customer_details) == null ? void 0 : _object_customer_details12.email) != null ? _ref54 : null,
3186
3252
  orderId: String(object.id),
3187
3253
  // Which line bought it, so a refund that withdraws that line
3188
3254
  // voids this card and no other (AGL-3363).
3189
3255
  productId: line.productId,
3190
3256
  // What the console's Gift cards search asks (AGL-3321).
3191
- searchTokens: giftCardSearchTokens(code, (_ref54 = object == null ? void 0 : (_object_customer_details13 = object.customer_details) == null ? void 0 : _object_customer_details13.email) != null ? _ref54 : null),
3257
+ searchTokens: giftCardSearchTokens(code, (_ref55 = object == null ? void 0 : (_object_customer_details13 = object.customer_details) == null ? void 0 : _object_customer_details13.email) != null ? _ref55 : null),
3192
3258
  createdAtMs: Date.now()
3193
3259
  }).then(()=>true).catch((error)=>{
3194
3260
  console.error('Gift card issuance failed', hostId, code, error);
@@ -3209,24 +3275,27 @@ async function findOrderForDispute(paymentIntentId) {
3209
3275
  void notifyHostManagers(String(hostId), {
3210
3276
  type: 'content.order',
3211
3277
  title: 'Gift card paid for but not issued',
3212
- body: `A $${(line.unitAmountCents / 100).toFixed(2)} gift card on ` + `order ${String(object.id)} could not be written, so no code ` + 'was emailed to the buyer. Issue one by hand from Gift cards.',
3278
+ body: `A $${(line.unitAmountCents / 100).toFixed(2)} gift card ` + `bought on {site} (order ${cartOrderLabel}) could not be ` + 'written, so no code was emailed to the buyer. Issue one by ' + 'hand from Gift cards.',
3213
3279
  link: `/${hostId}/products`
3214
3280
  });
3215
3281
  continue;
3216
3282
  }
3217
3283
  const giftTo = object == null ? void 0 : (_object_customer_details14 = object.customer_details) == null ? void 0 : _object_customer_details14.email;
3218
3284
  if (giftTo) {
3219
- var _ref55;
3285
+ var _ref56;
3220
3286
  const giftValue = `$${(line.unitAmountCents / 100).toFixed(2)}`;
3221
3287
  const fallbackText = `Gift card code: ${code}\n` + `Value: ${giftValue}\n\n` + 'Enter it at checkout to apply the balance.';
3222
- // Site-owner-designed template when published (AGL-771).
3288
+ // Site-owner-designed template when published (AGL-771). A
3289
+ // bought card carries no note; empty, so the built-in copy
3290
+ // leaves that line out.
3223
3291
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'gift-card', {
3224
3292
  'giftcard.code': code,
3225
- 'giftcard.value': giftValue
3293
+ 'giftcard.value': giftValue,
3294
+ 'giftcard.note': ''
3226
3295
  });
3227
3296
  await sendEmail(_extends({
3228
3297
  to: giftTo,
3229
- subject: (_ref55 = designed == null ? void 0 : designed.subject) != null ? _ref55 : 'Your gift card',
3298
+ subject: (_ref56 = designed == null ? void 0 : designed.subject) != null ? _ref56 : 'Your gift card',
3230
3299
  text: (designed == null ? void 0 : designed.text) || fallbackText
3231
3300
  }, (designed == null ? void 0 : designed.html) ? {
3232
3301
  html: designed.html
@@ -3247,12 +3316,12 @@ async function findOrderForDispute(paymentIntentId) {
3247
3316
  }
3248
3317
  // Discounts engine redemptions (AGL-305).
3249
3318
  if ((_object_metadata22 = object.metadata) == null ? void 0 : _object_metadata22.discountId) {
3250
- var _ref56;
3319
+ var _ref57;
3251
3320
  var _object_metadata26;
3252
3321
  await settleRedemption({
3253
3322
  firestore,
3254
3323
  ref: hostRef.collection('discounts').doc(String(object.metadata.discountId)),
3255
- holdKey: String((_ref56 = (_object_metadata26 = object.metadata) == null ? void 0 : _object_metadata26.discountHoldKey) != null ? _ref56 : ''),
3324
+ holdKey: String((_ref57 = (_object_metadata26 = object.metadata) == null ? void 0 : _object_metadata26.discountHoldKey) != null ? _ref57 : ''),
3256
3325
  orderRef,
3257
3326
  label: `discount ${object.metadata.discountId}`,
3258
3327
  detail: `Discount ${object.metadata.discountId} was applied to this ` + 'order but no longer exists, so the redemption is uncounted ' + 'against its limit.'
@@ -3266,7 +3335,7 @@ async function findOrderForDispute(paymentIntentId) {
3266
3335
  var _object_metadata27;
3267
3336
  const { hostId, orderId, productId } = (_object_metadata27 = object.metadata) != null ? _object_metadata27 : {};
3268
3337
  if (hostId && orderId) {
3269
- var _ref57, _ref58, _ref59;
3338
+ var _ref58, _ref59, _ref60;
3270
3339
  var _object_total_details, _object_total_details1;
3271
3340
  const firestore = firebaseAdmin.app().firestore();
3272
3341
  const hostRef = firestore.collection('hosts').doc(String(hostId));
@@ -3305,7 +3374,7 @@ async function findOrderForDispute(paymentIntentId) {
3305
3374
  //
3306
3375
  // The whole map is written rather than one field of it, so a
3307
3376
  // `merge`-set's nested-map merge and a plain replace agree.
3308
- const shippingCents = Math.max(0, Math.round(Number((_ref57 = object == null ? void 0 : (_object_total_details = object.total_details) == null ? void 0 : _object_total_details.amount_shipping) != null ? _ref57 : 0) || 0));
3377
+ const shippingCents = Math.max(0, Math.round(Number((_ref58 = object == null ? void 0 : (_object_total_details = object.total_details) == null ? void 0 : _object_total_details.amount_shipping) != null ? _ref58 : 0) || 0));
3309
3378
  // Tax joins shipping in the fold (AGL-1953). `draft-order.ts` now
3310
3379
  // declares tax — a Stripe Tax Rate for a `manual` store, whose figure
3311
3380
  // it already froze into `totals`, or `automatic_tax` for a `stripe`
@@ -3318,8 +3387,8 @@ async function findOrderForDispute(paymentIntentId) {
3318
3387
  // `amount_tax` 0 here (its own figure stays on the order, and its
3319
3388
  // AGL-1953 witness is metadata, not this field), and a draft composed
3320
3389
  // by a store with no tax reports 0 too.
3321
- const stripeTaxCents = Math.max(0, Math.round(Number((_ref58 = object == null ? void 0 : (_object_total_details1 = object.total_details) == null ? void 0 : _object_total_details1.amount_tax) != null ? _ref58 : 0) || 0));
3322
- const chargedTotalCents = Number((_ref59 = object == null ? void 0 : object.amount_total) != null ? _ref59 : NaN);
3390
+ const stripeTaxCents = Math.max(0, Math.round(Number((_ref59 = object == null ? void 0 : (_object_total_details1 = object.total_details) == null ? void 0 : _object_total_details1.amount_tax) != null ? _ref59 : 0) || 0));
3391
+ const chargedTotalCents = Number((_ref60 = object == null ? void 0 : object.amount_total) != null ? _ref60 : NaN);
3323
3392
  // `shipping_details` ONLY, unlike the cart branch's
3324
3393
  // `?? customer_details` fallback: that address is the BILLING one, and
3325
3394
  // a card sale rung up at a register completes here too — it must not
@@ -3331,6 +3400,8 @@ async function findOrderForDispute(paymentIntentId) {
3331
3400
  // transaction below. Carried out so the money can be reported after
3332
3401
  // the transaction commits.
3333
3402
  let paidAfterCancel = false;
3403
+ // The canceled order's number, for the notice below (AGL-3432).
3404
+ let canceledOrderLabel = '';
3334
3405
  const flipped = await firestore.runTransaction(async (transaction)=>{
3335
3406
  var _snapshot_data, _snapshot_data1, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _lifted_totals, _ref6, _shipTo_name, _shipTo_address_line1, _shipTo_address_line2, _shipTo_address_city, _shipTo_address_state, _shipTo_address_postal_code, _shipTo_address_country;
3336
3407
  var _object_customer_details, _object_metadata, _object_customer_details1, _lifted_totals1;
@@ -3356,6 +3427,7 @@ async function findOrderForDispute(paymentIntentId) {
3356
3427
  const alreadyNoted = ((_lifted_timeline = lifted.timeline) != null ? _lifted_timeline : []).some((entry)=>(entry == null ? void 0 : entry.event) === 'paid-after-cancel');
3357
3428
  if (lifted.status === 'cancelled' && !alreadyNoted) {
3358
3429
  paidAfterCancel = true;
3430
+ canceledOrderLabel = CommerceModel.formatOrderNumber(lifted, String(orderId));
3359
3431
  // `set(..., { merge: true })`, the same call the paying branch
3360
3432
  // below makes on this document. Nothing here is a nested map, so
3361
3433
  // merge and replace agree, and the read above has just proven
@@ -3428,13 +3500,13 @@ async function findOrderForDispute(paymentIntentId) {
3428
3500
  await notifyHostManagers(String(hostId), {
3429
3501
  type: 'content.order',
3430
3502
  title: 'A canceled order was paid',
3431
- body: `Order ${orderId} was canceled, but its payment link was still ` + 'live and has been paid. The money is in Stripe and no sale was ' + 'recorded — refund it in Stripe or reconcile it by hand.',
3503
+ body: `Order ${canceledOrderLabel || orderId} on {site} was canceled, ` + 'but its payment link was still live and has been paid. The ' + 'money is in Stripe and no sale was recorded — refund it in ' + 'Stripe or reconcile it by hand.',
3432
3504
  link: `/${hostId}/orders`
3433
3505
  });
3434
3506
  }
3435
3507
  if (flipped) {
3436
- var _ref60, _ref61;
3437
- var _object_metadata28, _object_customer_details15;
3508
+ var _ref61, _ref62, _ref63, _ref64, _ref65, _ref66;
3509
+ var _object_metadata28, _object_customer_details15, _order_totals, _object_customer_details16;
3438
3510
  const order = paidOrder;
3439
3511
  // Discounts engine redemptions (AGL-305), for this branch's BOTH
3440
3512
  // tenants: a console draft order and a POS card sale, which carry the
@@ -3449,20 +3521,25 @@ async function findOrderForDispute(paymentIntentId) {
3449
3521
  // the hold existed, which Stripe's at-least-once delivery would run
3450
3522
  // twice. The transition is what makes it once.
3451
3523
  if ((_object_metadata28 = object.metadata) == null ? void 0 : _object_metadata28.discountId) {
3452
- var _ref62;
3524
+ var _ref67;
3453
3525
  var _object_metadata29;
3454
3526
  await settleRedemption({
3455
3527
  firestore,
3456
3528
  ref: hostRef.collection('discounts').doc(String(object.metadata.discountId)),
3457
- holdKey: String((_ref62 = (_object_metadata29 = object.metadata) == null ? void 0 : _object_metadata29.discountHoldKey) != null ? _ref62 : ''),
3529
+ holdKey: String((_ref67 = (_object_metadata29 = object.metadata) == null ? void 0 : _object_metadata29.discountHoldKey) != null ? _ref67 : ''),
3458
3530
  orderRef,
3459
3531
  label: `discount ${object.metadata.discountId}`,
3460
3532
  detail: `Discount ${object.metadata.discountId} was applied to this ` + 'order but no longer exists, so the redemption is uncounted ' + 'against its limit.'
3461
3533
  });
3462
3534
  }
3535
+ const draftLabel = CommerceModel.formatOrderNumber(order, String(orderId));
3536
+ const draftPayer = (_ref61 = (_ref62 = object == null ? void 0 : (_object_customer_details15 = object.customer_details) == null ? void 0 : _object_customer_details15.email) != null ? _ref62 : order.customerEmail) != null ? _ref61 : null;
3537
+ const draftPaidCents = Number((_ref63 = object == null ? void 0 : object.amount_total) != null ? _ref63 : 0) || Number((_ref64 = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref64 : 0);
3463
3538
  void notifyHostManagers(String(hostId), {
3464
3539
  type: 'content.order',
3465
- title: `Draft order paid — ${CommerceModel.formatOrderNumber(order, String(orderId))}`,
3540
+ title: `Draft order paid — ${draftLabel}`,
3541
+ // Who paid what, and where (AGL-3432).
3542
+ body: `${draftPayer != null ? draftPayer : 'The buyer'} paid draft order ${draftLabel} ` + `on {site}: $${(draftPaidCents / 100).toFixed(2)}.`,
3466
3543
  link: `/${hostId}/products`
3467
3544
  });
3468
3545
  // Contacts ingestion (AGL-1748): this branch flipped the order,
@@ -3477,15 +3554,15 @@ async function findOrderForDispute(paymentIntentId) {
3477
3554
  // figure the draft was priced at, and the two agree by construction,
3478
3555
  // but only one of them is the money that moved. Stored totals remain
3479
3556
  // the fallback for a session shape that reports no total.
3480
- const draftEmail = (_ref60 = (_ref61 = object == null ? void 0 : (_object_customer_details15 = object.customer_details) == null ? void 0 : _object_customer_details15.email) != null ? _ref61 : order.customerEmail) != null ? _ref60 : null;
3557
+ const draftEmail = (_ref65 = (_ref66 = object == null ? void 0 : (_object_customer_details16 = object.customer_details) == null ? void 0 : _object_customer_details16.email) != null ? _ref66 : order.customerEmail) != null ? _ref65 : null;
3481
3558
  if (draftEmail) {
3482
- var _ref63, _ref64, _ref65;
3483
- var _order_totals, _object_customer_details16;
3484
- const chargedCents = Number((_ref63 = object == null ? void 0 : object.amount_total) != null ? _ref63 : 0) || Number((_ref64 = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref64 : 0);
3559
+ var _ref68, _ref69, _ref70;
3560
+ var _order_totals1, _object_customer_details17;
3561
+ const chargedCents = Number((_ref68 = object == null ? void 0 : object.amount_total) != null ? _ref68 : 0) || Number((_ref69 = (_order_totals1 = order.totals) == null ? void 0 : _order_totals1.totalCents) != null ? _ref69 : 0);
3485
3562
  await captureHostContact(_extends({
3486
3563
  hostId: String(hostId),
3487
3564
  email: draftEmail,
3488
- name: (_ref65 = object == null ? void 0 : (_object_customer_details16 = object.customer_details) == null ? void 0 : _object_customer_details16.name) != null ? _ref65 : undefined,
3565
+ name: (_ref70 = object == null ? void 0 : (_object_customer_details17 = object.customer_details) == null ? void 0 : _object_customer_details17.name) != null ? _ref70 : undefined,
3489
3566
  source: 'order',
3490
3567
  initialLifecycleStage: 'customer'
3491
3568
  }, chargedCents > 0 ? {
@@ -3498,15 +3575,15 @@ async function findOrderForDispute(paymentIntentId) {
3498
3575
  }));
3499
3576
  }
3500
3577
  if (productId) {
3501
- var _productSnapshot_data, _ref66, _ref67;
3578
+ var _productSnapshot_data, _ref71, _ref72;
3502
3579
  var _object_metadata30, _lifted_variants_, _order_lineItems_, _order_lineItems;
3503
3580
  const productRef = hostRef.collection('products').doc(String(productId));
3504
3581
  const productSnapshot = await productRef.get();
3505
3582
  const lifted = CommerceModel.liftLegacyProduct((_productSnapshot_data = productSnapshot.data()) != null ? _productSnapshot_data : {
3506
3583
  name: 'Product'
3507
3584
  });
3508
- const soldVariantId = String((_ref66 = (_object_metadata30 = object.metadata) == null ? void 0 : _object_metadata30.variantId) != null ? _ref66 : '') || ((_lifted_variants_ = lifted.variants[0]) == null ? void 0 : _lifted_variants_.id);
3509
- const quantity = (_ref67 = (_order_lineItems = order.lineItems) == null ? void 0 : (_order_lineItems_ = _order_lineItems[0]) == null ? void 0 : _order_lineItems_.quantity) != null ? _ref67 : 1;
3585
+ const soldVariantId = String((_ref71 = (_object_metadata30 = object.metadata) == null ? void 0 : _object_metadata30.variantId) != null ? _ref71 : '') || ((_lifted_variants_ = lifted.variants[0]) == null ? void 0 : _lifted_variants_.id);
3586
+ const quantity = (_ref72 = (_order_lineItems = order.lineItems) == null ? void 0 : (_order_lineItems_ = _order_lineItems[0]) == null ? void 0 : _order_lineItems_.quantity) != null ? _ref72 : 1;
3510
3587
  if (soldVariantId) {
3511
3588
  // Atomic since AGL-2320.
3512
3589
  const moved = await decrementVariantStock({
@@ -3558,10 +3635,10 @@ async function findOrderForDispute(paymentIntentId) {
3558
3635
  // would decrement it twice.
3559
3636
  const paidProducts = new Map();
3560
3637
  for (const line of (_order_lineItems1 = order.lineItems) != null ? _order_lineItems1 : []){
3561
- var _ref68, _ref69, _line_variantId1;
3638
+ var _ref73, _ref74, _line_variantId1;
3562
3639
  var _lineProduct_variants_;
3563
- const lineProductId = String((_ref68 = line == null ? void 0 : line.productId) != null ? _ref68 : '');
3564
- const soldQty = Math.round(Number((_ref69 = line == null ? void 0 : line.quantity) != null ? _ref69 : 0));
3640
+ const lineProductId = String((_ref73 = line == null ? void 0 : line.productId) != null ? _ref73 : '');
3641
+ const soldQty = Math.round(Number((_ref74 = line == null ? void 0 : line.quantity) != null ? _ref74 : 0));
3565
3642
  if (!lineProductId || !(soldQty > 0)) continue;
3566
3643
  // Firestore reserves `__…__` ids and `.doc()` throws
3567
3644
  // synchronously on one; a corrupt line must not fail the
@@ -3624,8 +3701,8 @@ async function findOrderForDispute(paymentIntentId) {
3624
3701
  var _object_metadata31;
3625
3702
  const { hostId, productId, feeCents, couponCode } = (_object_metadata31 = object.metadata) != null ? _object_metadata31 : {};
3626
3703
  if (hostId && productId) {
3627
- var _ref70, _productForSnapshot_get1, _productForSnapshot_data1, _ref71, _ref72, _ref73, _ref74, _ref75, _ref76, _ref77;
3628
- var _object_metadata32, _buyNowLineItems_, _object_customer_details17, _object_customer_details18, _object_customer_details19, _object_metadata33;
3704
+ var _ref75, _productForSnapshot_get2, _productForSnapshot_data1, _ref76, _ref77, _ref78, _ref79, _ref80, _ref81, _ref82;
3705
+ var _object_metadata32, _buyNowLineItems_, _object_customer_details18, _object_customer_details19, _object_customer_details20, _object_metadata33;
3629
3706
  const firestore = firebaseAdmin.app().firestore();
3630
3707
  const hostRef = firestore.collection('hosts').doc(String(hostId));
3631
3708
  // Orders v1 (AGL-283): line-item snapshot + totals + timeline with
@@ -3635,9 +3712,9 @@ async function findOrderForDispute(paymentIntentId) {
3635
3712
  // is bounded to Stripe's at-least-once edge).
3636
3713
  const orderRef = hostRef.collection('orders').doc(String(object.id));
3637
3714
  const counterRef = hostRef.collection('counters').doc('orders');
3638
- const amountCents = Number((_ref70 = object == null ? void 0 : object.amount_total) != null ? _ref70 : 0);
3715
+ const amountCents = Number((_ref75 = object == null ? void 0 : object.amount_total) != null ? _ref75 : 0);
3639
3716
  const productForSnapshot = await hostRef.collection('products').doc(String(productId)).get();
3640
- const snapshotName = String((_productForSnapshot_get1 = productForSnapshot.get('name')) != null ? _productForSnapshot_get1 : 'Product');
3717
+ const snapshotName = String((_productForSnapshot_get2 = productForSnapshot.get('name')) != null ? _productForSnapshot_get2 : 'Product');
3641
3718
  // AGL-1711: the line item and totals used to be fabricated from
3642
3719
  // `amount_total` alone — one unit, priced at the whole charge, with tax
3643
3720
  // and discount recorded as 0. `computeBuyNowOrder` rebuilds the real
@@ -3648,7 +3725,7 @@ async function findOrderForDispute(paymentIntentId) {
3648
3725
  name: snapshotName
3649
3726
  });
3650
3727
  const soldVariant = ((_object_metadata32 = object.metadata) == null ? void 0 : _object_metadata32.variantId) ? liftedForSnapshot.variants.find((item)=>item.id === String(object.metadata.variantId)) : liftedForSnapshot.variants[0];
3651
- const variantOptions = Object.values((_ref71 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref71 : {});
3728
+ const variantOptions = Object.values((_ref76 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref76 : {});
3652
3729
  const { lineItems: buyNowLineItems, totals: buyNowTotals } = CommerceModel.computeBuyNowOrder(object, _extends({
3653
3730
  name: snapshotName
3654
3731
  }, variantOptions.length ? {
@@ -3660,7 +3737,10 @@ async function findOrderForDispute(paymentIntentId) {
3660
3737
  } : {}, liftedForSnapshot.supplierId ? {
3661
3738
  supplierId: liftedForSnapshot.supplierId
3662
3739
  } : {}));
3663
- const soldQuantity = (_ref72 = (_buyNowLineItems_ = buyNowLineItems[0]) == null ? void 0 : _buyNowLineItems_.quantity) != null ? _ref72 : 1;
3740
+ const soldQuantity = (_ref77 = (_buyNowLineItems_ = buyNowLineItems[0]) == null ? void 0 : _buyNowLineItems_.quantity) != null ? _ref77 : 1;
3741
+ // The number the merchant knows this order by, for the notices below
3742
+ // (AGL-3432).
3743
+ let buyNowOrderNumber;
3664
3744
  const created = await firestore.runTransaction(async (transaction)=>{
3665
3745
  var _counter_get, _ref, _ref1, _ref2, _ref3;
3666
3746
  var _object_metadata, _object_customer_details, _object_customer_details1;
@@ -3670,6 +3750,7 @@ async function findOrderForDispute(paymentIntentId) {
3670
3750
  ]);
3671
3751
  if (existing.exists) return false;
3672
3752
  const number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
3753
+ buyNowOrderNumber = number;
3673
3754
  transaction.set(counterRef, {
3674
3755
  next: number + 1
3675
3756
  }, {
@@ -3716,16 +3797,19 @@ async function findOrderForDispute(paymentIntentId) {
3716
3797
  // Redelivery guard (AGL-498): skip the notification + fulfilment side
3717
3798
  // effects when this order already existed.
3718
3799
  if (!created) return;
3800
+ const buyNowOrderLabel = CommerceModel.formatOrderNumber({
3801
+ number: buyNowOrderNumber
3802
+ }, String(object.id));
3719
3803
  // In-app order notification (wave v6): host managers see sales
3720
- // in the bell, not just the owner's email.
3721
- void notifyHostManagers(String(hostId), _extends({
3804
+ // in the bell, not just the owner's email: what came in, from whom
3805
+ // and where (AGL-3432).
3806
+ const buyNowTotal = `$${(Number((_ref78 = object == null ? void 0 : object.amount_total) != null ? _ref78 : 0) / 100).toFixed(2)}`;
3807
+ void notifyHostManagers(String(hostId), {
3722
3808
  type: 'content.order',
3723
- title: `New order — $${(Number((_ref73 = object == null ? void 0 : object.amount_total) != null ? _ref73 : 0) / 100).toFixed(2)}`
3724
- }, (object == null ? void 0 : (_object_customer_details17 = object.customer_details) == null ? void 0 : _object_customer_details17.email) ? {
3725
- body: `From ${object.customer_details.email}`
3726
- } : {}, {
3809
+ title: `New order on {site} — ${buyNowTotal}`,
3810
+ body: `Order ${buyNowOrderLabel} came in on {site}: ${soldQuantity}× ` + `${snapshotName}, ${buyNowTotal}` + ((object == null ? void 0 : (_object_customer_details18 = object.customer_details) == null ? void 0 : _object_customer_details18.email) ? `, from ${object.customer_details.email}.` : '.'),
3727
3811
  link: `/${hostId}/products`
3728
- }));
3812
+ });
3729
3813
  // Dropship routing (AGL-289): paid lines with a supplier notify
3730
3814
  // it (signed webhook and/or email) and stash a callback token so
3731
3815
  // the supplier can post tracking back. Plan-gated; failures never
@@ -3746,8 +3830,8 @@ async function findOrderForDispute(paymentIntentId) {
3746
3830
  // `supplier-outbox.ts` retries out of band.
3747
3831
  await (async ()=>{
3748
3832
  try {
3749
- var _routedProduct_get, _ref, _ref1;
3750
- var _object_customer_details, _object_customer_details1;
3833
+ var _routedProduct_get, _ref, _ref1, _ref2, _shipTo_address_line1, _shipTo_address_line2, _shipTo_address_city, _shipTo_address_state, _shipTo_address_postal_code, _shipTo_address_country;
3834
+ var _object_shipping_details, _object_customer_details, _object_customer_details1;
3751
3835
  const routedOrg = await getOrgForHost(String(hostId));
3752
3836
  if (!Aglyn.checkEntitlement(routedOrg == null ? void 0 : routedOrg.org, 'dropshipRouting')) {
3753
3837
  return;
@@ -3770,6 +3854,11 @@ async function findOrderForDispute(paymentIntentId) {
3770
3854
  }, {
3771
3855
  merge: true
3772
3856
  });
3857
+ // Where the parcel goes (AGL-3432): the address the shopper gave
3858
+ // at checkout. Buy-now asks for one only when the store ships, so
3859
+ // it can be absent, and a billing address is never offered in its
3860
+ // place.
3861
+ const shipTo = (object == null ? void 0 : (_object_shipping_details = object.shipping_details) == null ? void 0 : _object_shipping_details.address) ? object.shipping_details : null;
3773
3862
  const payload = {
3774
3863
  hostId: String(hostId),
3775
3864
  orderId: String(object.id),
@@ -3779,7 +3868,15 @@ async function findOrderForDispute(paymentIntentId) {
3779
3868
  // the buyer paid for.
3780
3869
  quantity: soldQuantity,
3781
3870
  customerEmail: (_ref = object == null ? void 0 : (_object_customer_details = object.customer_details) == null ? void 0 : _object_customer_details.email) != null ? _ref : null,
3782
- shippingName: (_ref1 = object == null ? void 0 : (_object_customer_details1 = object.customer_details) == null ? void 0 : _object_customer_details1.name) != null ? _ref1 : null,
3871
+ shippingName: (_ref1 = (_ref2 = shipTo == null ? void 0 : shipTo.name) != null ? _ref2 : object == null ? void 0 : (_object_customer_details1 = object.customer_details) == null ? void 0 : _object_customer_details1.name) != null ? _ref1 : null,
3872
+ shippingAddress: shipTo ? {
3873
+ line1: (_shipTo_address_line1 = shipTo.address.line1) != null ? _shipTo_address_line1 : null,
3874
+ line2: (_shipTo_address_line2 = shipTo.address.line2) != null ? _shipTo_address_line2 : null,
3875
+ city: (_shipTo_address_city = shipTo.address.city) != null ? _shipTo_address_city : null,
3876
+ state: (_shipTo_address_state = shipTo.address.state) != null ? _shipTo_address_state : null,
3877
+ postalCode: (_shipTo_address_postal_code = shipTo.address.postal_code) != null ? _shipTo_address_postal_code : null,
3878
+ country: (_shipTo_address_country = shipTo.address.country) != null ? _shipTo_address_country : null
3879
+ } : null,
3783
3880
  updateUrl: `https://${requestHost}/api/commerce/supplier-update` + `?hostId=${hostId}&orderId=${object.id}&token=${supplierToken}`
3784
3881
  };
3785
3882
  if (supplier.webhookUrl) {
@@ -3804,11 +3901,31 @@ async function findOrderForDispute(paymentIntentId) {
3804
3901
  });
3805
3902
  }
3806
3903
  if (supplier.email) {
3807
- var _ref2, _payload_shippingName;
3904
+ var _resolveHostToken, _ref3, _payload_shippingName;
3905
+ // Which store is asking (AGL-3432): a supplier serves several,
3906
+ // and this mail goes out with no site header to say so.
3907
+ const storeName = (_resolveHostToken = resolveHostToken('businessName', await hostRef.get().then((snapshot)=>snapshot.data()).catch(()=>undefined))) != null ? _resolveHostToken : 'A store';
3908
+ const recipient = (_ref3 = (_payload_shippingName = payload.shippingName) != null ? _payload_shippingName : payload.customerEmail) != null ? _ref3 : 'the buyer';
3909
+ const address = payload.shippingAddress ? [
3910
+ payload.shippingAddress.line1,
3911
+ payload.shippingAddress.line2,
3912
+ [
3913
+ payload.shippingAddress.city,
3914
+ [
3915
+ payload.shippingAddress.state,
3916
+ payload.shippingAddress.postalCode
3917
+ ].filter(Boolean).join(' ')
3918
+ ].filter(Boolean).join(', '),
3919
+ payload.shippingAddress.country
3920
+ ].filter(Boolean).join(', ') : '';
3808
3921
  await sendEmail({
3809
3922
  to: supplier.email,
3810
- subject: `New order to fulfill: ${payload.productName}`,
3811
- text: `${payload.quantity}× ${payload.productName}\n` + `Ship to: ${(_ref2 = (_payload_shippingName = payload.shippingName) != null ? _payload_shippingName : payload.customerEmail) != null ? _ref2 : 'see order'}\n\n` + `Add tracking: ${payload.updateUrl}&trackingNumber=TRACKING&carrier=CARRIER`,
3923
+ subject: `New order to fulfill for ${storeName}: ${payload.productName}`,
3924
+ // The tracking link opens a form for the carrier and the
3925
+ // number, so it carries no placeholders: a link with
3926
+ // `trackingNumber=TRACKING` in it, opened unedited, records
3927
+ // that word as the tracking.
3928
+ text: `${storeName} has a new order for you to ship: ` + `${payload.quantity}× ${payload.productName} ` + `(order ${buyNowOrderLabel}).\n\n` + (address ? `Ship to: ${recipient}, ${address}\n\n` : `Ship to: ${recipient}. No shipping address was ` + `collected at checkout; ask ${storeName} for it.\n\n`) + 'When it ships, open this link to enter the carrier and ' + `tracking number:\n${payload.updateUrl}`,
3812
3929
  fromName: (await brandFor(hostId)).fromName,
3813
3930
  sendingIdentity: await hostSendingIdentity(String(hostId)),
3814
3931
  audience: 'tenant',
@@ -3828,14 +3945,14 @@ async function findOrderForDispute(paymentIntentId) {
3828
3945
  // Contacts ingestion (AGL-197): buyers become contacts.
3829
3946
  await captureHostContact({
3830
3947
  hostId: String(hostId),
3831
- email: object == null ? void 0 : (_object_customer_details18 = object.customer_details) == null ? void 0 : _object_customer_details18.email,
3832
- name: (_ref74 = object == null ? void 0 : (_object_customer_details19 = object.customer_details) == null ? void 0 : _object_customer_details19.name) != null ? _ref74 : undefined,
3948
+ email: object == null ? void 0 : (_object_customer_details19 = object.customer_details) == null ? void 0 : _object_customer_details19.email,
3949
+ name: (_ref79 = object == null ? void 0 : (_object_customer_details20 = object.customer_details) == null ? void 0 : _object_customer_details20.name) != null ? _ref79 : undefined,
3833
3950
  source: 'order',
3834
3951
  initialLifecycleStage: 'customer',
3835
- purchaseCents: Number((_ref75 = object == null ? void 0 : object.amount_total) != null ? _ref75 : 0),
3952
+ purchaseCents: Number((_ref80 = object == null ? void 0 : object.amount_total) != null ? _ref80 : 0),
3836
3953
  interaction: {
3837
3954
  refId: String(object.id),
3838
- summary: `Placed an order ($${(Number((_ref76 = object == null ? void 0 : object.amount_total) != null ? _ref76 : 0) / 100).toFixed(2)})`
3955
+ summary: `Placed an order ($${(Number((_ref81 = object == null ? void 0 : object.amount_total) != null ? _ref81 : 0) / 100).toFixed(2)})`
3839
3956
  }
3840
3957
  });
3841
3958
  const productRef = hostRef.collection('products').doc(String(productId));
@@ -3853,12 +3970,12 @@ async function findOrderForDispute(paymentIntentId) {
3853
3970
  // serialize; the snapshot above is still read, for the product NAME on
3854
3971
  // the receipt below, and no longer decides any count.
3855
3972
  {
3856
- var _productSnapshot_data2, _ref78;
3973
+ var _productSnapshot_data2, _ref83;
3857
3974
  var _object_metadata34, _lifted_variants_1;
3858
3975
  const lifted = CommerceModel.liftLegacyProduct((_productSnapshot_data2 = productSnapshot.data()) != null ? _productSnapshot_data2 : {
3859
3976
  name: 'Product'
3860
3977
  });
3861
- const soldVariantId = String((_ref78 = (_object_metadata34 = object.metadata) == null ? void 0 : _object_metadata34.variantId) != null ? _ref78 : '') || ((_lifted_variants_1 = lifted.variants[0]) == null ? void 0 : _lifted_variants_1.id);
3978
+ const soldVariantId = String((_ref83 = (_object_metadata34 = object.metadata) == null ? void 0 : _object_metadata34.variantId) != null ? _ref83 : '') || ((_lifted_variants_1 = lifted.variants[0]) == null ? void 0 : _lifted_variants_1.id);
3862
3979
  if (soldVariantId) {
3863
3980
  // AGL-1711: `-1` regardless of how many units were bought, so a
3864
3981
  // 3-unit buy-now sale decremented stock by one and the difference
@@ -3889,14 +4006,14 @@ async function findOrderForDispute(paymentIntentId) {
3889
4006
  // Settlement, the same way round and for the same reason as the cart
3890
4007
  // branch above (AGL-2356): the decrement lands first, then the
3891
4008
  // reservation is dropped.
3892
- await releaseStockHold(hostRef, String((_ref77 = (_object_metadata33 = object.metadata) == null ? void 0 : _object_metadata33.stockHoldKey) != null ? _ref77 : ''));
4009
+ await releaseStockHold(hostRef, String((_ref82 = (_object_metadata33 = object.metadata) == null ? void 0 : _object_metadata33.stockHoldKey) != null ? _ref82 : ''));
3893
4010
  if (couponCode) {
3894
- var _ref79;
4011
+ var _ref84;
3895
4012
  var _object_metadata35;
3896
4013
  await settleRedemption({
3897
4014
  firestore,
3898
4015
  ref: hostRef.collection('coupons').doc(String(couponCode)),
3899
- holdKey: String((_ref79 = (_object_metadata35 = object.metadata) == null ? void 0 : _object_metadata35.couponHoldKey) != null ? _ref79 : ''),
4016
+ holdKey: String((_ref84 = (_object_metadata35 = object.metadata) == null ? void 0 : _object_metadata35.couponHoldKey) != null ? _ref84 : ''),
3900
4017
  orderRef,
3901
4018
  label: `coupon ${couponCode}`,
3902
4019
  detail: `Coupon ${couponCode} was applied to this order but no longer ` + 'exists, so the redemption is uncounted against its limit.'
@@ -3905,24 +4022,31 @@ async function findOrderForDispute(paymentIntentId) {
3905
4022
  // Receipt + seller notification (AGL-96): env-gated like every
3906
4023
  // other outbound email; failures never fail the webhook.
3907
4024
  if (isEmailConfigured()) {
3908
- var _productSnapshot_get, _ref80;
3909
- var _object_customer_details20, _org, _this3;
4025
+ var _productSnapshot_get, _ref85;
4026
+ var _object_customer_details21, _org, _this3;
3910
4027
  const productName = String((_productSnapshot_get = productSnapshot.get('name')) != null ? _productSnapshot_get : 'your purchase');
3911
- const amount = (Number((_ref80 = object == null ? void 0 : object.amount_total) != null ? _ref80 : 0) / 100).toFixed(2);
3912
- const buyerEmail = object == null ? void 0 : (_object_customer_details20 = object.customer_details) == null ? void 0 : _object_customer_details20.email;
4028
+ const amount = (Number((_ref85 = object == null ? void 0 : object.amount_total) != null ? _ref85 : 0) / 100).toFixed(2);
4029
+ const buyerEmail = object == null ? void 0 : (_object_customer_details21 = object.customer_details) == null ? void 0 : _object_customer_details21.email;
3913
4030
  const orderTotal = `$${amount}`;
3914
4031
  if (buyerEmail) {
3915
- var _ref81;
3916
- const fallbackText = `Thanks for your purchase!\n\n${productName} — $${amount}` + `\nOrder reference: ${object.id}`;
4032
+ var _ref86, _ref87;
4033
+ // The store's own Receipt footer, as the cart receipt carries it
4034
+ // (AGL-3432).
4035
+ const receiptFooter = String((_ref86 = await hostRef.collection('settings').doc('store').get().then((snapshot)=>snapshot.get('receiptFooter')).catch(()=>'')) != null ? _ref86 : '');
4036
+ // The quantity with the name (AGL-3432), so a three-unit sale
4037
+ // does not read as one item at the whole total.
4038
+ const receiptLine = `${soldQuantity}× ${productName}`;
4039
+ const fallbackText = `Thanks for your purchase!\n\n${receiptLine} — $${amount}` + `\nOrder reference: ${object.id}` + (receiptFooter ? `\n\n${receiptFooter}` : '');
3917
4040
  // Site-owner-designed template when published (AGL-771).
3918
4041
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'order-receipt', {
3919
- 'order.summary': productName,
4042
+ 'order.summary': receiptLine,
3920
4043
  'order.total': orderTotal,
3921
- 'order.ref': String(object.id)
4044
+ 'order.ref': String(object.id),
4045
+ 'store.receiptFooter': receiptFooter
3922
4046
  });
3923
4047
  await sendEmail(_extends({
3924
4048
  to: String(buyerEmail),
3925
- subject: (_ref81 = designed == null ? void 0 : designed.subject) != null ? _ref81 : `Receipt: ${productName}`,
4049
+ subject: (_ref87 = designed == null ? void 0 : designed.subject) != null ? _ref87 : `Receipt: ${productName}`,
3926
4050
  text: (designed == null ? void 0 : designed.text) || fallbackText
3927
4051
  }, (designed == null ? void 0 : designed.html) ? {
3928
4052
  html: designed.html
@@ -3956,7 +4080,7 @@ async function findOrderForDispute(paymentIntentId) {
3956
4080
  // SSO.
3957
4081
  const seller = (_this4 = await findUserByUidAcrossPools(sellerUid).catch(()=>null)) == null ? void 0 : _this4.record;
3958
4082
  if (seller == null ? void 0 : seller.email) {
3959
- var _hostSnapshot_get, _ref82;
4083
+ var _hostSnapshot_get, _ref88;
3960
4084
  const siteName = String((_hostSnapshot_get = hostSnapshot.get('displayName')) != null ? _hostSnapshot_get : hostId);
3961
4085
  const fallbackText = `You made a sale on ${siteName}!\n\n${productName} — $${amount}` + (buyerEmail ? `\nBuyer: ${buyerEmail}` : '') + `\nOrder reference: ${object.id}`;
3962
4086
  // Site-owner-designed template when published (AGL-771).
@@ -3969,7 +4093,7 @@ async function findOrderForDispute(paymentIntentId) {
3969
4093
  });
3970
4094
  await sendEmail(_extends({
3971
4095
  to: seller.email,
3972
- subject: (_ref82 = designed == null ? void 0 : designed.subject) != null ? _ref82 : `New order: ${productName}`,
4096
+ subject: (_ref88 = designed == null ? void 0 : designed.subject) != null ? _ref88 : `New order: ${productName}`,
3973
4097
  text: (designed == null ? void 0 : designed.text) || fallbackText
3974
4098
  }, (designed == null ? void 0 : designed.html) ? {
3975
4099
  html: designed.html
@@ -3999,15 +4123,15 @@ async function findOrderForDispute(paymentIntentId) {
3999
4123
  // the refund is written on the order's timeline. Nothing else is moved: a
4000
4124
  // Dashboard refund is staff's act, and they chose its transfer reversal.
4001
4125
  if (type === 'charge.refunded') {
4002
- var _ref83, _ref84;
4126
+ var _ref89, _ref90;
4003
4127
  var _object_payment_intent, _snapshot_ref_parent_parent;
4004
- const paymentIntentId = typeof (object == null ? void 0 : object.payment_intent) === 'string' ? object.payment_intent : String((_ref83 = object == null ? void 0 : (_object_payment_intent = object.payment_intent) == null ? void 0 : _object_payment_intent.id) != null ? _ref83 : '');
4128
+ const paymentIntentId = typeof (object == null ? void 0 : object.payment_intent) === 'string' ? object.payment_intent : String((_ref89 = object == null ? void 0 : (_object_payment_intent = object.payment_intent) == null ? void 0 : _object_payment_intent.id) != null ? _ref89 : '');
4005
4129
  const lookup = paymentIntentId ? await findOrderForDispute(paymentIntentId) : {
4006
4130
  kind: 'not-ours'
4007
4131
  };
4008
4132
  if (lookup.kind !== 'order') return;
4009
4133
  const snapshot = lookup.snapshot;
4010
- const hostId = String((_ref84 = (_snapshot_ref_parent_parent = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent.id) != null ? _ref84 : '');
4134
+ const hostId = String((_ref90 = (_snapshot_ref_parent_parent = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent.id) != null ? _ref90 : '');
4011
4135
  const external = await recordExternalRefund(snapshot.ref, object);
4012
4136
  if (external && hostId && snapshot.ref.parent.parent) {
4013
4137
  const outcome = await applyExternalRefundToGiftCards({
@@ -4019,9 +4143,12 @@ async function findOrderForDispute(paymentIntentId) {
4019
4143
  fullyRefunded: external.fullyRefunded
4020
4144
  });
4021
4145
  if (outcome.kind === 'frozen-for-review') {
4146
+ var _snapshot_data;
4147
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
4022
4148
  await notifyGiftCardHold({
4023
4149
  hostId,
4024
- orderLabel: `order ${snapshot.id}`,
4150
+ // The order number the merchant knows it by, never its document id.
4151
+ orderLabel: `order ${CommerceModel.formatOrderNumber(order, snapshot.id)}`,
4025
4152
  orderPath: `/${hostId}/products/orders?order=${encodeURIComponent(snapshot.id)}`,
4026
4153
  orderId: snapshot.id,
4027
4154
  cards: outcome.cards,
@@ -4045,20 +4172,23 @@ async function findOrderForDispute(paymentIntentId) {
4045
4172
  // merchant; both report the site so the seller's fraud ledger can name it.
4046
4173
  const risk = paymentRiskEventFrom(type, object);
4047
4174
  if (risk && risk.signal.kind !== 'dispute') {
4048
- var _ref85, _snapshot_data;
4049
- var _snapshot_ref_parent_parent1;
4175
+ var _ref91, _snapshot_data1, _ref92, _ref93;
4176
+ var _snapshot_ref_parent_parent1, _order_totals2;
4050
4177
  const lookup = risk.paymentIntentId ? await findOrderForDispute(risk.paymentIntentId) : {
4051
4178
  kind: 'not-ours'
4052
4179
  };
4053
4180
  if (lookup.kind !== 'order') return;
4054
4181
  const snapshot = lookup.snapshot;
4055
- const hostId = String((_ref85 = (_snapshot_ref_parent_parent1 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent1.id) != null ? _ref85 : '');
4056
- const order = CommerceModel.liftLegacyOrder((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
4182
+ const hostId = String((_ref91 = (_snapshot_ref_parent_parent1 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent1.id) != null ? _ref91 : '');
4183
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data1 = snapshot.data()) != null ? _snapshot_data1 : {});
4184
+ const orderCents = Number((_ref92 = (_ref93 = (_order_totals2 = order.totals) == null ? void 0 : _order_totals2.totalCents) != null ? _ref93 : order.amountCents) != null ? _ref92 : 0);
4057
4185
  await recordPaymentRiskOnRecord({
4058
4186
  ref: snapshot.ref,
4059
4187
  signal: risk.signal,
4060
4188
  hostId,
4061
4189
  subjectLabel: `order ${CommerceModel.formatOrderNumber(order, snapshot.id)}`,
4190
+ // The notice names the sum in question, as the dispute's does.
4191
+ amount: orderCents > 0 ? `$${(orderCents / 100).toFixed(2)}` : null,
4062
4192
  // The Orders section of the Products hub, with this order's dialog
4063
4193
  // open — where Refund lives (`siteRecordLinks().order`).
4064
4194
  link: `/${hostId}/products/orders?order=${encodeURIComponent(snapshot.id)}`,
@@ -4114,7 +4244,7 @@ async function findOrderForDispute(paymentIntentId) {
4114
4244
  // finds). AGL-1554 will add its own `closed` branch for the marketplace
4115
4245
  // side; the two coexist because each self-selects by finding its own record.
4116
4246
  if (type === 'charge.dispute.created' || type === 'charge.dispute.closed') {
4117
- var _dispute_payment_intent, _ref86;
4247
+ var _dispute_payment_intent, _ref94;
4118
4248
  var _snapshot_ref_parent_parent2;
4119
4249
  const dispute = object != null ? object : {};
4120
4250
  const paymentIntentId = String((_dispute_payment_intent = dispute.payment_intent) != null ? _dispute_payment_intent : '');
@@ -4140,14 +4270,16 @@ async function findOrderForDispute(paymentIntentId) {
4140
4270
  const claimed = lookup.kind === 'order' || lookup.kind === 'unresolved';
4141
4271
  const snapshot = lookup.kind === 'order' ? lookup.snapshot : null;
4142
4272
  if (snapshot) {
4143
- var _ref87;
4273
+ var _ref95;
4144
4274
  var _snapshot_ref_parent_parent3;
4145
- const hostId = String((_ref87 = (_snapshot_ref_parent_parent3 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent3.id) != null ? _ref87 : '');
4275
+ const hostId = String((_ref95 = (_snapshot_ref_parent_parent3 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent3.id) != null ? _ref95 : '');
4146
4276
  if (type === 'charge.dispute.created') {
4277
+ var _snapshot_data2;
4147
4278
  // A chargeback's gift cards are frozen while it is open (AGL-3363).
4279
+ const disputed = CommerceModel.liftLegacyOrder((_snapshot_data2 = snapshot.data()) != null ? _snapshot_data2 : {});
4148
4280
  await notifyGiftCardHold({
4149
4281
  hostId,
4150
- orderLabel: `order ${snapshot.id}`,
4282
+ orderLabel: `order ${CommerceModel.formatOrderNumber(disputed, snapshot.id)}`,
4151
4283
  orderPath: `/${hostId}/products/orders?order=${encodeURIComponent(snapshot.id)}`,
4152
4284
  orderId: snapshot.id,
4153
4285
  cards: await applyOrderGiftCardRisk(snapshot, {
@@ -4158,7 +4290,7 @@ async function findOrderForDispute(paymentIntentId) {
4158
4290
  }, notifyRiskEvent);
4159
4291
  const { opened, record } = await recordDisputeOpened(snapshot, dispute);
4160
4292
  if (opened && hostId) {
4161
- var _snapshot_data1, _record_evidenceDueByMs, _ref88;
4293
+ var _snapshot_data3, _record_evidenceDueByMs, _ref96;
4162
4294
  // Time-critical — Stripe's evidence window is days, not weeks — so
4163
4295
  // this is awaited rather than fired off with `void`: the handler is
4164
4296
  // serverless and work left running past the response is work the
@@ -4166,7 +4298,7 @@ async function findOrderForDispute(paymentIntentId) {
4166
4298
  // (AGL-3368) tells the site's managers and the workspace's owners
4167
4299
  // what a dispute means and when the evidence is due; it never
4168
4300
  // throws.
4169
- const order = CommerceModel.liftLegacyOrder((_snapshot_data1 = snapshot.data()) != null ? _snapshot_data1 : {});
4301
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data3 = snapshot.data()) != null ? _snapshot_data3 : {});
4170
4302
  await notifyRiskEvent({
4171
4303
  kind: 'sale-dispute',
4172
4304
  orgId: null,
@@ -4177,16 +4309,16 @@ async function findOrderForDispute(paymentIntentId) {
4177
4309
  },
4178
4310
  amount: `$${(record.amountCents / 100).toFixed(2)}`,
4179
4311
  evidenceDueByMs: (_record_evidenceDueByMs = record.evidenceDueByMs) != null ? _record_evidenceDueByMs : null,
4180
- dedupeKey: `sale:${String((_ref88 = dispute == null ? void 0 : dispute.id) != null ? _ref88 : snapshot.id)}`,
4312
+ dedupeKey: `sale:${String((_ref96 = dispute == null ? void 0 : dispute.id) != null ? _ref96 : snapshot.id)}`,
4181
4313
  staffEvidence: record.reason ? `Reason: ${record.reason.replace(/_/g, ' ')}.` : null
4182
4314
  });
4183
4315
  }
4184
4316
  } else {
4185
- var _ref89, _ref90;
4317
+ var _ref97, _snapshot_data4, _ref98;
4186
4318
  // Lost: the money went back, so the cards it bought are voided. Won
4187
4319
  // (or closed on a warning): the dispute's freeze lifts; a freeze a
4188
4320
  // fraud warning or review put on stays for the merchant (AGL-3363).
4189
- await applyOrderGiftCardRisk(snapshot, String((_ref89 = dispute == null ? void 0 : dispute.status) != null ? _ref89 : '') === 'lost' ? {
4321
+ await applyOrderGiftCardRisk(snapshot, String((_ref97 = dispute == null ? void 0 : dispute.status) != null ? _ref97 : '') === 'lost' ? {
4190
4322
  kind: 'void',
4191
4323
  reason: 'dispute-lost'
4192
4324
  } : {
@@ -4194,11 +4326,18 @@ async function findOrderForDispute(paymentIntentId) {
4194
4326
  reason: 'dispute'
4195
4327
  });
4196
4328
  const settled = await recordDisputeClosed(snapshot, dispute);
4329
+ // The number the merchant knows the order by (AGL-3432), for both
4330
+ // notices below; the Stripe session id is nowhere in the console.
4331
+ const disputedOrderLabel = CommerceModel.formatOrderNumber(CommerceModel.liftLegacyOrder((_snapshot_data4 = snapshot.data()) != null ? _snapshot_data4 : {}), snapshot.id);
4197
4332
  if (settled.recorded && hostId) {
4333
+ // What the shopper's bank took back left the PLATFORM's balance on
4334
+ // a destination charge, not the merchant's: their own loss is the
4335
+ // seller share in the "Payout adjusted" notice below. Told that way
4336
+ // (AGL-3432), so the two are not read as two losses.
4198
4337
  await notifyHostManagers(hostId, {
4199
4338
  type: 'content.order',
4200
- title: settled.lost ? `Chargeback lost — $${(settled.reversedCents / 100).toFixed(2)} reversed` : 'Chargeback resolved in your favor',
4201
- body: `Order ${snapshot.id}`,
4339
+ title: settled.lost ? `Chargeback lost — order ${disputedOrderLabel}` : 'Chargeback resolved in your favor',
4340
+ body: settled.lost ? `The chargeback on order ${disputedOrderLabel} on {site} was ` + 'decided in the shopper\'s favor, and their bank took back ' + `$${(settled.reversedCents / 100).toFixed(2)}. If part of the ` + 'sale had been paid out to you, a separate notice says what ' + 'is taken back from your balance.' : `The chargeback on order ${disputedOrderLabel} on {site} ` + 'closed in your favor. Nothing is taken back from you for it.',
4202
4341
  link: `/${hostId}/orders`
4203
4342
  });
4204
4343
  // The customer's side of the ledger (AGL-1754), through the same
@@ -4250,7 +4389,7 @@ async function findOrderForDispute(paymentIntentId) {
4250
4389
  // transiently-failed reversal gets its retry — and gated inside on
4251
4390
  // the order's own `reversedTransferCents` marker, so it runs at most
4252
4391
  // once per dispute however many times Stripe delivers.
4253
- if (String((_ref90 = dispute == null ? void 0 : dispute.status) != null ? _ref90 : '') === 'lost') {
4392
+ if (String((_ref98 = dispute == null ? void 0 : dispute.status) != null ? _ref98 : '') === 'lost') {
4254
4393
  const pulledBackCents = await reverseSellerShare(snapshot.ref, dispute);
4255
4394
  // The merchant-experience half of the AGL-1794 decision, and not an
4256
4395
  // optional extra: the reversal takes money out of the connected
@@ -4270,7 +4409,7 @@ async function findOrderForDispute(paymentIntentId) {
4270
4409
  await notifyHostManagers(hostId, {
4271
4410
  type: 'content.order',
4272
4411
  title: `Payout adjusted — $${(pulledBackCents / 100).toFixed(2)} recovered for a lost chargeback`,
4273
- body: `Order ${snapshot.id}: the amount transferred to you for this sale has been ` + `reversed. If your balance does not cover it, Stripe recovers the remainder ` + `from your future payouts.`,
4412
+ body: `Because the chargeback on order ${disputedOrderLabel} on ` + `{site} was lost, the $${(pulledBackCents / 100).toFixed(2)} ` + 'transferred to you for that sale has been taken back. If ' + 'your balance does not cover it, Stripe recovers the rest ' + 'from your future payouts.',
4274
4413
  link: `/${hostId}/orders`
4275
4414
  });
4276
4415
  }
@@ -4280,7 +4419,7 @@ async function findOrderForDispute(paymentIntentId) {
4280
4419
  return _extends({
4281
4420
  claimed
4282
4421
  }, snapshot ? {
4283
- hostId: String((_ref86 = (_snapshot_ref_parent_parent2 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent2.id) != null ? _ref86 : '')
4422
+ hostId: String((_ref94 = (_snapshot_ref_parent_parent2 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent2.id) != null ? _ref94 : '')
4284
4423
  } : {});
4285
4424
  }
4286
4425
  };