@aglyn/plugins-commerce 1.0.0-beta.219 → 1.0.0-beta.221

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 (83) hide show
  1. package/README.md +5 -0
  2. package/package.json +14 -14
  3. package/src/lib/components/console/catalog-organization-card.component.js +626 -446
  4. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  5. package/src/lib/components/console/payments-settings-card.component.js +1 -1
  6. package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
  7. package/src/lib/components/console/product-editor-dialog.component.js +1127 -846
  8. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  9. package/src/lib/components/console/site-member-purchases.component.d.ts +24 -0
  10. package/src/lib/components/console/site-member-purchases.component.js +339 -0
  11. package/src/lib/components/console/site-member-purchases.component.js.map +1 -0
  12. package/src/lib/components/member-signup.js +2 -2
  13. package/src/lib/components/member-signup.js.map +1 -1
  14. package/src/lib/components/newsletter-signup.js +2 -2
  15. package/src/lib/components/newsletter-signup.js.map +1 -1
  16. package/src/lib/constants/operator-alerts.js +5 -3
  17. package/src/lib/constants/operator-alerts.js.map +1 -1
  18. package/src/lib/model/commerce-io.js +1 -1
  19. package/src/lib/model/commerce-io.js.map +1 -1
  20. package/src/lib/model/commerce-record-lists.d.ts +31 -0
  21. package/src/lib/model/commerce-record-lists.js +45 -0
  22. package/src/lib/model/commerce-record-lists.js.map +1 -0
  23. package/src/lib/model/commerce-record-routes.d.ts +18 -0
  24. package/src/lib/model/commerce-record-routes.js +1 -1
  25. package/src/lib/model/commerce-record-routes.js.map +1 -1
  26. package/src/lib/model/commerce-search-sources.js +4 -2
  27. package/src/lib/model/commerce-search-sources.js.map +1 -1
  28. package/src/lib/model/commerce-topics.d.ts +20 -0
  29. package/src/lib/model/commerce-topics.js +33 -0
  30. package/src/lib/model/commerce-topics.js.map +1 -0
  31. package/src/lib/model/index.d.ts +1 -0
  32. package/src/lib/model/index.js +1 -0
  33. package/src/lib/model/index.js.map +1 -1
  34. package/src/lib/model/product-record.d.ts +32 -0
  35. package/src/lib/model/product-record.js +82 -0
  36. package/src/lib/model/product-record.js.map +1 -0
  37. package/src/lib/model/site-member-purchases.d.ts +48 -0
  38. package/src/lib/model/site-member-purchases.js +50 -0
  39. package/src/lib/model/site-member-purchases.js.map +1 -0
  40. package/src/lib/plugin.js +12 -0
  41. package/src/lib/plugin.js.map +1 -1
  42. package/src/lib/server/billing-webhook.js +410 -208
  43. package/src/lib/server/billing-webhook.js.map +1 -1
  44. package/src/lib/server/cart-checkout.js +1 -1
  45. package/src/lib/server/cart-checkout.js.map +1 -1
  46. package/src/lib/server/gift-cards.js +7 -3
  47. package/src/lib/server/gift-cards.js.map +1 -1
  48. package/src/lib/server/low-stock.js +3 -1
  49. package/src/lib/server/low-stock.js.map +1 -1
  50. package/src/lib/server/member-post.js +2 -2
  51. package/src/lib/server/member-post.js.map +1 -1
  52. package/src/lib/server/membership-admin-password.js +4 -1
  53. package/src/lib/server/membership-admin-password.js.map +1 -1
  54. package/src/lib/server/membership-register.js +13 -2
  55. package/src/lib/server/membership-register.js.map +1 -1
  56. package/src/lib/server/newsletter.js +32 -19
  57. package/src/lib/server/newsletter.js.map +1 -1
  58. package/src/lib/server/pos-order.js +18 -10
  59. package/src/lib/server/pos-order.js.map +1 -1
  60. package/src/lib/server/process-abandoned.js +4 -2
  61. package/src/lib/server/process-abandoned.js.map +1 -1
  62. package/src/lib/server/process-restock.js +7 -5
  63. package/src/lib/server/process-restock.js.map +1 -1
  64. package/src/lib/server/product-record-index.js +5 -37
  65. package/src/lib/server/product-record-index.js.map +1 -1
  66. package/src/lib/server/reconcile-stock.js +1 -1
  67. package/src/lib/server/reconcile-stock.js.map +1 -1
  68. package/src/lib/server/reserve-stock.js +6 -1
  69. package/src/lib/server/reserve-stock.js.map +1 -1
  70. package/src/lib/server/revenue-source.js +3 -3
  71. package/src/lib/server/revenue-source.js.map +1 -1
  72. package/src/lib/server/supplier-outbox.js +16 -1
  73. package/src/lib/server/supplier-outbox.js.map +1 -1
  74. package/src/lib/server/supplier-update.js +19 -9
  75. package/src/lib/server/supplier-update.js.map +1 -1
  76. package/src/lib/starter-templates.d.ts +19 -0
  77. package/src/lib/starter-templates.js +157 -0
  78. package/src/lib/starter-templates.js.map +1 -0
  79. package/src/lib/tenant-emails.js +54 -14
  80. package/src/lib/tenant-emails.js.map +1 -1
  81. package/src/lib/testing/stand-in-record-system.d.ts +11 -2
  82. package/src/lib/testing/stand-in-record-system.js +5 -3
  83. package/src/lib/testing/stand-in-record-system.js.map +1 -1
@@ -17,8 +17,10 @@
17
17
  import * as Aglyn from "@aglyn/aglyn/server";
18
18
  import { findUserByUidAcrossPools, firebaseAdmin, getOrgForHost, meterHostEmail, notifyHostManagers, notifyRiskEvent, raiseOperatorAlert, renderHostEmailWithTokens, updateExisting, hostSendingIdentity } from "@aglyn/tenant-data-admin";
19
19
  import { paymentProvider } from "@aglyn/tenant-data-admin/server/payment-provider";
20
- import { captureHostContact } from "@aglyn/tenant-runtime";
20
+ import recordCapturedContact from "@aglyn/aglyn/plugin-manager/record-captured-contact";
21
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";
22
24
  import { COMMERCE_CHARGEBACK_UNROUTABLE, COMMERCE_FEE_REPRICE_REFUSED, COMMERCE_SELLER_SHARE_NOT_REVERSED, COMMERCE_TAX_NOT_REVERSED } from "../constants/operator-alerts.js";
23
25
  import { createHmac } from "crypto";
24
26
  import { isEmailConfigured, sendEmail } from "@aglyn/shared-util-email";
@@ -105,11 +107,17 @@ import { enqueueSupplierDelivery } from "./supplier-outbox.js";
105
107
  }
106
108
  if (keys.length) {
107
109
  const remaining = await hostRef.collection('licenseKeys').where('productId', '==', productId).where('assignedAtMs', '==', null).limit(6).get();
108
- 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');
109
117
  void notifyHostManagers(hostId, {
110
118
  type: 'content.lowStock',
111
- title: 'License key pool running low',
112
- 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.',
113
121
  link: `/${hostId}/products`
114
122
  });
115
123
  }
@@ -294,6 +302,25 @@ async function findOrderForDispute(paymentIntentId) {
294
302
  }
295
303
  });
296
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
+ }
297
324
  /**
298
325
  * Tells the operator that a subscription cycle's sales tax stayed with the
299
326
  * merchant (AGL-3377): the platform files and remits it, so every one of
@@ -305,6 +332,7 @@ async function findOrderForDispute(paymentIntentId) {
305
332
  context: {
306
333
  invoiceId: input.invoiceId,
307
334
  hostId: input.hostId,
335
+ site: await operatorSiteLabel(input.hostId),
308
336
  amount: input.taxCents === undefined ? 'An unknown amount' : formatOperatorAlertAmount(input.taxCents),
309
337
  reason: input.reason
310
338
  }
@@ -574,11 +602,11 @@ async function findOrderForDispute(paymentIntentId) {
574
602
  * fulfilment path are untouched; what changes is that no FURTHER cycle is
575
603
  * billed.
576
604
  *
577
- * `cancel_at_period_end` rather than an outright cancel, for two reasons that
578
- * both matter: the subscriber keeps the period they just paid for, and the
579
- * flag is REVERSIBLE — the merchant has a full cycle to restore their Aglyn
580
- * plan before anything is actually lost. An immediate cancel would take
581
- * 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).
582
610
  *
583
611
  * ## Once-only
584
612
  *
@@ -588,7 +616,8 @@ async function findOrderForDispute(paymentIntentId) {
588
616
  * subscription billing forever with the books saying it had been stopped. The
589
617
  * `Idempotency-Key` covers the window where the claim is written and the
590
618
  * response never arrives.
591
- */ 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;
592
621
  const stripeKey = process.env.STRIPE_SECRET_KEY;
593
622
  if (!stripeKey) {
594
623
  console.error('Lapsed storefront subscription not stopped: STRIPE_SECRET_KEY is not set (AGL-2071)', {
@@ -649,10 +678,29 @@ async function findOrderForDispute(paymentIntentId) {
649
678
  }, {
650
679
  merge: true
651
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';
652
697
  void notifyHostManagers(hostId, {
653
698
  type: 'content.order',
654
- title: 'A subscription stopped renewing — your plan no longer includes storefront subscriptions',
655
- 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.',
656
704
  link: `/${hostId}/products`
657
705
  });
658
706
  }
@@ -739,11 +787,13 @@ async function findOrderForDispute(paymentIntentId) {
739
787
  }
740
788
  const refusal = await response.json().catch(()=>null);
741
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 : '';
742
791
  await raiseOperatorAlert(COMMERCE_FEE_REPRICE_REFUSED, {
743
792
  dedupeKey: `${subscriptionId}:${desiredFeePct}`,
744
793
  context: {
745
794
  subject: `subscription ${subscriptionId}`,
746
- 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),
747
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}`})`
748
798
  }
749
799
  });
@@ -765,6 +815,7 @@ async function findOrderForDispute(paymentIntentId) {
765
815
  context: {
766
816
  subject: `invoice ${invoiceId}`,
767
817
  hostId,
818
+ site: await operatorSiteLabel(hostId),
768
819
  reason
769
820
  }
770
821
  });
@@ -1604,14 +1655,15 @@ async function findOrderForDispute(paymentIntentId) {
1604
1655
  * Tells the operator the platform is out a lost dispute's principal because
1605
1656
  * the merchant's share could not be pulled back (AGL-3377). Once per dispute.
1606
1657
  */ async function reportSellerShareNotReversed(orderRef, disputeId, principalCents, reason) {
1607
- var _ref;
1608
- var _orderRef_parent_parent;
1658
+ var _ref, _ref1;
1659
+ var _orderRef_parent_parent, _orderRef_parent_parent1;
1609
1660
  await raiseOperatorAlert(COMMERCE_SELLER_SHARE_NOT_REVERSED, {
1610
1661
  dedupeKey: disputeId,
1611
1662
  context: {
1612
1663
  disputeId,
1613
1664
  orderId: orderRef.id,
1614
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 : ''),
1615
1667
  amount: formatOperatorAlertAmount(principalCents),
1616
1668
  reason
1617
1669
  }
@@ -1774,7 +1826,7 @@ async function findOrderForDispute(paymentIntentId) {
1774
1826
  var _object_metadata15;
1775
1827
  const { hostId, productId } = (_object_metadata15 = object.metadata) != null ? _object_metadata15 : {};
1776
1828
  if (hostId && productId) {
1777
- var _productForSnapshot_data, _ref9, _productForSnapshot_get, _subTotals_totalCents, _ref10;
1829
+ var _productForSnapshot_data, _ref9, _productForSnapshot_get, _subTotals_totalCents, _productForSnapshot_get1, _ref10;
1778
1830
  var _object_metadata16, _liftedForSnapshot_subscription, _object_customer_details, _object_customer_details1, _object_customer_details2;
1779
1831
  const firestore = firebaseAdmin.app().firestore();
1780
1832
  const hostRef = firestore.collection('hosts').doc(String(hostId));
@@ -1818,9 +1870,9 @@ async function findOrderForDispute(paymentIntentId) {
1818
1870
  const subscriptionRef = hostRef.collection('subscriptions').doc(String(object.subscription));
1819
1871
  // Redelivery guard (AGL-1732, the AGL-498 shape). Stripe delivers at
1820
1872
  // least once, and the effects below this write are NOT idempotent —
1821
- // `upsertHostContact`'s `purchaseCents` is a `FieldValue.increment`, so
1822
- // a replay would inflate the subscriber's lifetime value and their
1823
- // order count on every retry.
1873
+ // the capture's `purchaseCents` is added to the person's lifetime
1874
+ // value, so a replay would inflate the subscriber's lifetime value and
1875
+ // their order count on every retry.
1824
1876
  //
1825
1877
  // Keyed on `checkoutSessionId` rather than on the document existing:
1826
1878
  // `customer.subscription.created` writes the SAME doc path (status and
@@ -1891,35 +1943,47 @@ async function findOrderForDispute(paymentIntentId) {
1891
1943
  });
1892
1944
  if (!recorded) return;
1893
1945
  const subscriptionCents = Number((_subTotals_totalCents = subTotals.totalCents) != null ? _subTotals_totalCents : 0);
1894
- 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), {
1895
1949
  type: 'content.order',
1896
1950
  // The amount rides the title exactly as the order notification's
1897
1951
  // does (AGL-1732) — "New subscriber" alone never said what for.
1898
- title: `New subscriber — $${(subscriptionCents / 100).toFixed(2)}${((_liftedForSnapshot_subscription = liftedForSnapshot.subscription) == null ? void 0 : _liftedForSnapshot_subscription.interval) ? `/${liftedForSnapshot.subscription.interval}` : ''}`
1899
- }, (object == null ? void 0 : (_object_customer_details = object.customer_details) == null ? void 0 : _object_customer_details.email) ? {
1900
- body: object.customer_details.email
1901
- } : {}, {
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}.`,
1902
1955
  link: `/${hostId}/products`
1903
- }));
1956
+ });
1904
1957
  // AWAITED SINCE AGL-2473, here and at the five sibling call sites in
1905
- // this file. `upsertHostContact` carries `purchaseCents`, so it is what
1906
- // feeds `ltvCents` and the RFM ranking — a `void`ed one is a paying
1907
- // customer quietly missing from the segment the merchant emails. It
1908
- // touches only our own Firestore, which is the whole line AGL-2473
1909
- // drew: Aglyn's own storage is awaited, a stranger's server is queued.
1910
- // `refund.ts` already awaited its sibling for exactly this reason.
1911
- await captureHostContact(_extends({
1958
+ // this file. The capture carries `purchaseCents`, which is what the
1959
+ // record system adds to a person's lifetime value and the RFM ranking
1960
+ // — a `void`ed one is a paying customer quietly missing from the
1961
+ // segment the merchant emails. It touches only our own Firestore,
1962
+ // which is the whole line AGL-2473 drew: Aglyn's own storage is
1963
+ // awaited, a stranger's server is queued. `refund.ts` already awaited
1964
+ // its sibling for exactly this reason.
1965
+ //
1966
+ // Through the contact-capture contract (AGL-3080), like every other
1967
+ // door commerce owns: the plugin that keeps people decides what a
1968
+ // purchase makes of them, and commerce imports none of it.
1969
+ await recordCapturedContact(_extends({
1970
+ orgId: '',
1912
1971
  hostId: String(hostId),
1913
- email: object == null ? void 0 : (_object_customer_details1 = object.customer_details) == null ? void 0 : _object_customer_details1.email,
1914
- name: (_ref10 = object == null ? void 0 : (_object_customer_details2 = object.customer_details) == null ? void 0 : _object_customer_details2.name) != null ? _ref10 : undefined,
1915
- source: 'order',
1972
+ identity: {
1973
+ email: object == null ? void 0 : (_object_customer_details1 = object.customer_details) == null ? void 0 : _object_customer_details1.email,
1974
+ name: (_ref10 = object == null ? void 0 : (_object_customer_details2 = object.customer_details) == null ? void 0 : _object_customer_details2.name) != null ? _ref10 : null
1975
+ },
1976
+ // A purchase makes the person a relationship (AGL-3232): a contact,
1977
+ // with an open lead for the address closed onto it.
1978
+ surface: 'relationship',
1916
1979
  // Every order door names `customer` (AGL-2612): a floor, so the
1917
1980
  // stage fills or advances and never moves anybody back.
1918
- initialLifecycleStage: 'customer'
1981
+ lifecycleFloor: 'customer'
1919
1982
  }, subscriptionCents > 0 ? {
1920
1983
  purchaseCents: subscriptionCents
1921
1984
  } : {}, {
1922
1985
  interaction: {
1986
+ source: 'order',
1923
1987
  refId: String(object.subscription),
1924
1988
  summary: `Started a subscription ($${(subscriptionCents / 100).toFixed(2)})`
1925
1989
  }
@@ -2362,7 +2426,12 @@ async function findOrderForDispute(paymentIntentId) {
2362
2426
  // `renewalOrgUnreadable` and not `!renewalOrg` (AGL-2258): a null org
2363
2427
  // we successfully read still stops, which is AGL-2071's decision.
2364
2428
  if (!renewalEntitled && !renewalOrgUnreadable) {
2365
- await stopLapsedStorefrontSubscription(invoiceHostId, subscriptionId, subscriptionRef);
2429
+ var _ref25, _ref26;
2430
+ await stopLapsedStorefrontSubscription(invoiceHostId, subscriptionId, subscriptionRef, {
2431
+ subscriberEmail: (_ref25 = (_ref26 = object == null ? void 0 : object.customer_email) != null ? _ref26 : soldSnapshot.get('customerEmail')) != null ? _ref25 : null,
2432
+ productName: snapshot.name || 'a subscription product',
2433
+ org: renewalOrg != null ? renewalOrg : null
2434
+ });
2366
2435
  } else if (renewalEntitled) {
2367
2436
  // RE-PRICE THE PLATFORM FEE (AGL-2289). `application_fee_percent` was
2368
2437
  // set once at the sale and never revisited, and it lives on the
@@ -2411,8 +2480,8 @@ async function findOrderForDispute(paymentIntentId) {
2411
2480
  const stockVariantId = (_snapshot_variantId = snapshot.variantId) != null ? _snapshot_variantId : (_stocked_variants_ = stocked.variants[0]) == null ? void 0 : _stocked_variants_.id;
2412
2481
  const tracked = stocked.variants.some((variant)=>variant.id === stockVariantId && variant.inventory != null);
2413
2482
  if (stockVariantId && tracked) {
2414
- var _ref25;
2415
- const cycleQuantity = (_ref25 = cycleLine == null ? void 0 : cycleLine.quantity) != null ? _ref25 : 1;
2483
+ var _ref27;
2484
+ const cycleQuantity = (_ref27 = cycleLine == null ? void 0 : cycleLine.quantity) != null ? _ref27 : 1;
2416
2485
  // Atomic since AGL-2320, like every other decrement: a renewal
2417
2486
  // batch bills many subscribers at once, so two cycles of the same
2418
2487
  // physical box are exactly the concurrent pair that lost a
@@ -2437,38 +2506,41 @@ async function findOrderForDispute(paymentIntentId) {
2437
2506
  }
2438
2507
  }
2439
2508
  if (!isOpeningInvoice && paidCents > 0) {
2440
- var _ref26, _ref27;
2441
- const renewalEmail = (_ref26 = (_ref27 = object == null ? void 0 : object.customer_email) != null ? _ref27 : soldSnapshot.get('customerEmail')) != null ? _ref26 : null;
2509
+ var _ref28, _ref29;
2510
+ const renewalEmail = (_ref28 = (_ref29 = object == null ? void 0 : object.customer_email) != null ? _ref29 : soldSnapshot.get('customerEmail')) != null ? _ref28 : null;
2442
2511
  // A physical cycle now lands in Orders (AGL-1750), but the console
2443
2512
  // still has no Subscriptions tab, so for a digital or service
2444
2513
  // subscription this notification remains the only place a merchant
2445
2514
  // learns the money arrived at all.
2446
- void notifyHostManagers(invoiceHostId, _extends({
2515
+ const renewalPrice = `$${(paidCents / 100).toFixed(2)}${interval ? `/${interval}` : ''}`;
2516
+ void notifyHostManagers(invoiceHostId, {
2447
2517
  type: 'content.order',
2448
- title: `Subscription renewed — $${(paidCents / 100).toFixed(2)}${interval ? `/${interval}` : ''}`
2449
- }, renewalEmail ? {
2450
- body: String(renewalEmail)
2451
- } : {}, {
2518
+ title: `Subscription renewed on {site} — ${renewalPrice}`,
2519
+ // Whose subscription, to what and where (AGL-3432).
2520
+ body: `${renewalEmail ? `${String(renewalEmail)}'s` : 'A'} ` + `subscription to ${snapshot.name || 'a product'} on {site} ` + `renewed for ${renewalPrice}.`,
2452
2521
  link: `/${invoiceHostId}/products`
2453
- }));
2522
+ });
2454
2523
  // RFM (AGL-328): a subscriber in month 12 has paid twelve times, and
2455
2524
  // counting only the first charge ranks them as a one-purchase
2456
2525
  // customer forever. Keyed to the invoice so the guard above is what
2457
2526
  // stops a redelivery inflating it.
2458
- await captureHostContact(_extends({
2527
+ await recordCapturedContact({
2528
+ orgId: '',
2459
2529
  hostId: invoiceHostId,
2460
- email: renewalEmail
2461
- }, soldSnapshot.get('customerName') ? {
2462
- name: String(soldSnapshot.get('customerName'))
2463
- } : {}, {
2464
- source: 'order',
2465
- initialLifecycleStage: 'customer',
2530
+ identity: _extends({
2531
+ email: renewalEmail
2532
+ }, soldSnapshot.get('customerName') ? {
2533
+ name: String(soldSnapshot.get('customerName'))
2534
+ } : {}),
2535
+ surface: 'relationship',
2536
+ lifecycleFloor: 'customer',
2466
2537
  purchaseCents: paidCents,
2467
2538
  interaction: {
2539
+ source: 'order',
2468
2540
  refId: invoiceId,
2469
2541
  summary: `Subscription renewed ($${(paidCents / 100).toFixed(2)})`
2470
2542
  }
2471
- }));
2543
+ });
2472
2544
  }
2473
2545
  }
2474
2546
  }
@@ -2477,7 +2549,7 @@ async function findOrderForDispute(paymentIntentId) {
2477
2549
  var _object_metadata17;
2478
2550
  const { hostId, reservationId } = (_object_metadata17 = object.metadata) != null ? _object_metadata17 : {};
2479
2551
  if (hostId && reservationId) {
2480
- var _ref28, _ref29;
2552
+ var _ref30, _ref31;
2481
2553
  var _object_metadata18;
2482
2554
  const firestore = firebaseAdmin.app().firestore();
2483
2555
  const reservationRef = firestore.collection('hosts').doc(String(hostId)).collection('reservations').doc(String(reservationId));
@@ -2490,7 +2562,7 @@ async function findOrderForDispute(paymentIntentId) {
2490
2562
  // `amount_total` is stay-plus-tax and Stripe's own tax fields read
2491
2563
  // zero; the session's metadata is the only witness, exactly as it is
2492
2564
  // for a buy-now sale.
2493
- const taxCents = Math.max(0, Math.round(Number((_ref28 = object == null ? void 0 : (_object_metadata18 = object.metadata) == null ? void 0 : _object_metadata18.taxCents) != null ? _ref28 : 0)));
2565
+ 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)));
2494
2566
  // What the guest actually handed over, MINUS that tax. This is the
2495
2567
  // DEPOSIT when the resource has one (`reserve.ts` charges
2496
2568
  // `depositCents || totalCents`), never the stay's `totalCents` — see
@@ -2504,7 +2576,7 @@ async function findOrderForDispute(paymentIntentId) {
2504
2576
  // are and under-state what the merchant collects at the register. The
2505
2577
  // same figure feeds the guest's lifetime value, which tax is no part
2506
2578
  // of either (AGL-1755).
2507
- const paidCents = Math.max(0, Math.round(Number((_ref29 = object == null ? void 0 : object.amount_total) != null ? _ref29 : 0)) - taxCents);
2579
+ const paidCents = Math.max(0, Math.round(Number((_ref31 = object == null ? void 0 : object.amount_total) != null ? _ref31 : 0)) - taxCents);
2508
2580
  // Redelivery guard (AGL-1755), the AGL-1748 shape. The `pending` ->
2509
2581
  // `confirmed` transition was always the guard, but it was a
2510
2582
  // read-then-write with every side effect hanging off it, so two
@@ -2575,17 +2647,44 @@ async function findOrderForDispute(paymentIntentId) {
2575
2647
  return true;
2576
2648
  });
2577
2649
  if (confirmedNow) {
2578
- var _ref30, _ref31;
2650
+ var _reservation_nights, _reservation_totalCents, _ref32, _ref33, _ref34, _ref35, _ref36;
2579
2651
  var _object_customer_details3, _object_customer_details4, _object_customer_details5;
2580
2652
  const reservation = reserved != null ? reserved : {};
2581
- void notifyHostManagers(String(hostId), _extends({
2653
+ // A day key, so its UTC date IS the check-in date.
2654
+ const checkInShort = new Date(Number(reservation['checkInDayMs'])).toUTCString().slice(0, 16);
2655
+ const nights = Number((_reservation_nights = reservation['nights']) != null ? _reservation_nights : 0);
2656
+ const nightsText = `${nights} night${nights === 1 ? '' : 's'}`;
2657
+ /*
2658
+ * WHAT THE GUEST WAS CHARGED TODAY, tax included (AGL-3442).
2659
+ *
2660
+ * `paidCents` is the money applied to the STAY and leaves out the
2661
+ * merchant's lodging tax, so a guest told only that figure is told
2662
+ * less than their card was charged. The two together are Stripe's
2663
+ * session total, and the tax is the figure `reserve.ts` charged as
2664
+ * its own line, named so the guest can match it to their receipt.
2665
+ * The balance below is the stay's alone: the tax is charged on what
2666
+ * is collected at booking, and nothing computes one on what is
2667
+ * collected at the property.
2668
+ */ const charged = `$${((paidCents + taxCents) / 100).toFixed(2)}`;
2669
+ const taxIncluded = taxCents > 0 ? `, including $${(taxCents / 100).toFixed(2)} lodging tax` : '';
2670
+ const paid = `${charged}${taxIncluded}`;
2671
+ // What the guest still owes for the stay: the console's
2672
+ // reservations card collects `totalCents - paidCents` at the
2673
+ // property, so the guest is told it too (AGL-3432).
2674
+ const balanceCents = Math.max(0, Math.round(Number((_reservation_totalCents = reservation['totalCents']) != null ? _reservation_totalCents : 0)) - paidCents);
2675
+ const balance = `$${(balanceCents / 100).toFixed(2)}`;
2676
+ 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';
2677
+ // Which room or unit was reserved (AGL-3432): on a property with
2678
+ // several, "a stay" does not say which one to get ready.
2679
+ 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() : '';
2680
+ void notifyHostManagers(String(hostId), {
2582
2681
  type: 'content.booking',
2583
- title: 'New reservation'
2584
- }, (object == null ? void 0 : (_object_customer_details3 = object.customer_details) == null ? void 0 : _object_customer_details3.email) ? {
2585
- body: object.customer_details.email
2586
- } : {}, {
2682
+ title: 'New reservation on {site}',
2683
+ // Who, what, when, what was paid and what is still owed
2684
+ // (AGL-3432).
2685
+ 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.` : '.'),
2587
2686
  link: `/${hostId}/products`
2588
- }));
2687
+ });
2589
2688
  // Contacts ingestion (AGL-1755): this branch stored `paidCents` from
2590
2689
  // `amount_total` and then called `upsertHostContact` with no amount,
2591
2690
  // so a guest who paid for a stay showed a lifetime value of zero. A
@@ -2615,22 +2714,26 @@ async function findOrderForDispute(paymentIntentId) {
2615
2714
  // exactly once. Taking `totalCents` here is what WOULD double-count:
2616
2715
  // it would claim money the guest has not paid yet and will pay again
2617
2716
  // through the register.
2618
- const guestContactEmail = (_ref30 = (_ref31 = object == null ? void 0 : (_object_customer_details4 = object.customer_details) == null ? void 0 : _object_customer_details4.email) != null ? _ref31 : reservation['guestEmail']) != null ? _ref30 : null;
2717
+ 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;
2619
2718
  if (guestContactEmail) {
2620
- var _ref32, _ref33;
2719
+ var _ref37, _ref38;
2621
2720
  var _object_customer_details6;
2622
- await captureHostContact(_extends({
2721
+ await recordCapturedContact(_extends({
2722
+ orgId: '',
2623
2723
  hostId: String(hostId),
2624
- email: guestContactEmail,
2625
- name: (_ref32 = (_ref33 = object == null ? void 0 : (_object_customer_details6 = object.customer_details) == null ? void 0 : _object_customer_details6.name) != null ? _ref33 : reservation['guestName']) != null ? _ref32 : undefined,
2626
- source: 'booking',
2724
+ identity: {
2725
+ email: guestContactEmail,
2726
+ 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 : null
2727
+ },
2728
+ surface: 'relationship',
2627
2729
  // A paid stay is a sale (AGL-2612), whatever the source says
2628
2730
  // about the kind of thing that was bought.
2629
- initialLifecycleStage: 'customer'
2731
+ lifecycleFloor: 'customer'
2630
2732
  }, paidCents > 0 ? {
2631
2733
  purchaseCents: paidCents
2632
2734
  } : {}, {
2633
2735
  interaction: {
2736
+ source: 'booking',
2634
2737
  refId: String(reservationId),
2635
2738
  summary: `Reserved a stay ($${(paidCents / 100).toFixed(2)})`
2636
2739
  }
@@ -2638,21 +2741,22 @@ async function findOrderForDispute(paymentIntentId) {
2638
2741
  }
2639
2742
  const guestEmail = object == null ? void 0 : (_object_customer_details5 = object.customer_details) == null ? void 0 : _object_customer_details5.email;
2640
2743
  if (guestEmail) {
2641
- var _reservation_nights, _ref34;
2642
- const checkIn = new Date(Number(reservation['checkInDayMs'])).toUTCString();
2643
- const paid = `$${(paidCents / 100).toFixed(2)}`;
2644
- const checkInShort = checkIn.slice(0, 16);
2645
- const fallbackText = `Your stay is confirmed!\n\nCheck-in: ${checkInShort}\n` + `Nights: ${reservation['nights']}\n` + `Paid today: ${paid}\n` + `Reference: ${reservationId}`;
2744
+ var _reservation_nights1, _ref39;
2745
+ // The balance as a whole line, empty when the stay is paid in
2746
+ // full so the built-in copy leaves it out.
2747
+ const balanceNote = balanceCents > 0 ? `Still to pay: ${balance}, at the property. It has not been charged.` : '';
2748
+ const fallbackText = `Your stay is confirmed!\n\nCheck-in: ${checkInShort}\n` + `Nights: ${reservation['nights']}\n` + `Paid today: ${paid}\n` + (balanceNote ? `${balanceNote}\n` : '') + `Reference: ${reservationId}`;
2646
2749
  // Site-owner-designed template when published (AGL-771).
2647
2750
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'reservation-confirmed', {
2648
2751
  'reservation.checkIn': checkInShort,
2649
- 'reservation.nights': String((_reservation_nights = reservation['nights']) != null ? _reservation_nights : ''),
2752
+ 'reservation.nights': String((_reservation_nights1 = reservation['nights']) != null ? _reservation_nights1 : ''),
2650
2753
  'reservation.paid': paid,
2754
+ 'reservation.balance': balanceNote,
2651
2755
  'reservation.ref': String(reservationId)
2652
2756
  });
2653
2757
  await sendEmail(_extends({
2654
2758
  to: guestEmail,
2655
- subject: (_ref34 = designed == null ? void 0 : designed.subject) != null ? _ref34 : 'Reservation confirmed',
2759
+ subject: (_ref39 = designed == null ? void 0 : designed.subject) != null ? _ref39 : 'Reservation confirmed',
2656
2760
  text: (designed == null ? void 0 : designed.text) || fallbackText
2657
2761
  }, (designed == null ? void 0 : designed.html) ? {
2658
2762
  html: designed.html
@@ -2678,7 +2782,7 @@ async function findOrderForDispute(paymentIntentId) {
2678
2782
  var _object_metadata19;
2679
2783
  const { hostId, cartId, feeCents, couponCode } = (_object_metadata19 = object.metadata) != null ? _object_metadata19 : {};
2680
2784
  if (hostId && cartId) {
2681
- var _cartSnapshot_data, _ref35, _ref36, _ref37, _ref38, _ref39, _ref40;
2785
+ var _cartSnapshot_data, _ref40, _ref41, _ref42, _ref43, _ref44, _ref45;
2682
2786
  var _object_customer_details7, _object_metadata20, _object_customer_details8, _object_customer_details9, _object_customer_details10, _object_metadata21, _this2, _object_metadata22;
2683
2787
  const firestore = firebaseAdmin.app().firestore();
2684
2788
  const hostRef = firestore.collection('hosts').doc(String(hostId));
@@ -2744,7 +2848,11 @@ async function findOrderForDispute(paymentIntentId) {
2744
2848
  unitAmountCents: Math.round(Number((_ref = variant == null ? void 0 : variant.priceUsd) != null ? _ref : 0) * 100)
2745
2849
  });
2746
2850
  }).filter(Boolean);
2747
- const shipping = (_ref35 = object == null ? void 0 : object.shipping_details) != null ? _ref35 : object == null ? void 0 : object.customer_details;
2851
+ const shipping = (_ref40 = object == null ? void 0 : object.shipping_details) != null ? _ref40 : object == null ? void 0 : object.customer_details;
2852
+ // The number the merchant knows this order by, carried out of the
2853
+ // transaction for the notices below (AGL-3432). The document id is
2854
+ // the Stripe session id, which appears nowhere in the console.
2855
+ let cartOrderNumber;
2748
2856
  const created = await firestore.runTransaction(async (transaction)=>{
2749
2857
  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;
2750
2858
  var _object_metadata, _object_customer_details, _object_customer_details1;
@@ -2754,6 +2862,7 @@ async function findOrderForDispute(paymentIntentId) {
2754
2862
  ]);
2755
2863
  if (existing.exists) return false;
2756
2864
  const number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
2865
+ cartOrderNumber = number;
2757
2866
  transaction.set(counterRef, {
2758
2867
  next: number + 1
2759
2868
  }, {
@@ -2828,6 +2937,9 @@ async function findOrderForDispute(paymentIntentId) {
2828
2937
  // the non-idempotent effects below (inventory / coupon / gift-card
2829
2938
  // decrements) that would otherwise double-apply.
2830
2939
  if (!created) return;
2940
+ const cartOrderLabel = CommerceModel.formatOrderNumber({
2941
+ number: cartOrderNumber
2942
+ }, String(object.id));
2831
2943
  // AGL-2149: loud, not silent. Logged for the platform and pushed to the
2832
2944
  // merchant, once, behind the same `created` guard as every other
2833
2945
  // non-idempotent effect so a redelivery does not re-nag.
@@ -2840,7 +2952,7 @@ async function findOrderForDispute(paymentIntentId) {
2840
2952
  void notifyHostManagers(String(hostId), {
2841
2953
  type: 'content.order',
2842
2954
  title: 'A paid order is missing items',
2843
- 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.',
2955
+ 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.',
2844
2956
  link: `/${hostId}/products`
2845
2957
  });
2846
2958
  }
@@ -2882,10 +2994,10 @@ async function findOrderForDispute(paymentIntentId) {
2882
2994
  // matters, and the order carries the buyer identity anyway.
2883
2995
  const licenseKeysByProduct = {};
2884
2996
  for (const line of lineItems){
2885
- var _ref41;
2997
+ var _ref46;
2886
2998
  var _object_customer_details11;
2887
2999
  if (line.productType !== 'digital') continue;
2888
- const keys = await assignLicenseKeys(firestore, hostRef, String(hostId), line.productId, String(object.id), (_ref41 = object == null ? void 0 : (_object_customer_details11 = object.customer_details) == null ? void 0 : _object_customer_details11.email) != null ? _ref41 : null, line.quantity);
3000
+ 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);
2889
3001
  if (keys.length) licenseKeysByProduct[line.productId] = keys;
2890
3002
  }
2891
3003
  if (Object.keys(licenseKeysByProduct).length) {
@@ -2898,9 +3010,9 @@ async function findOrderForDispute(paymentIntentId) {
2898
3010
  // Branded receipt (AGL-296): env-gated like every outbound email.
2899
3011
  const buyerEmailForReceipt = object == null ? void 0 : (_object_customer_details7 = object.customer_details) == null ? void 0 : _object_customer_details7.email;
2900
3012
  if (isEmailConfigured() && buyerEmailForReceipt) {
2901
- var _ref42, _ref43, _ref44, _ref45;
3013
+ var _ref47, _ref48, _ref49, _ref50;
2902
3014
  const receiptSettings = await hostRef.collection('settings').doc('store').get().catch(()=>null);
2903
- const receiptFooter = String((_ref42 = receiptSettings == null ? void 0 : receiptSettings.get('receiptFooter')) != null ? _ref42 : '');
3015
+ const receiptFooter = String((_ref47 = receiptSettings == null ? void 0 : receiptSettings.get('receiptFooter')) != null ? _ref47 : '');
2904
3016
  const linesText = lineItems.map((line)=>`${line.quantity}× ${line.name}${line.variantLabel ? ` (${line.variantLabel})` : ''} — $${(line.unitAmountCents * line.quantity / 100).toFixed(2)}`).join('\n');
2905
3017
  // The keys assigned above the gate (AGL-2149); the receipt only
2906
3018
  // reports them.
@@ -2914,9 +3026,9 @@ async function findOrderForDispute(paymentIntentId) {
2914
3026
  // Digital delivery links (AGL-302); reuse the canonical mint the
2915
3027
  // tenant download endpoint verifies so the secret can never drift.
2916
3028
  const downloadToken = mintDownloadToken(hostId, String(object.id));
2917
- const siteOrigin = String((_ref43 = object == null ? void 0 : object.success_url) != null ? _ref43 : '').replace(/\/\?.*$|\?.*$/, '');
3029
+ const siteOrigin = String((_ref48 = object == null ? void 0 : object.success_url) != null ? _ref48 : '').replace(/\/\?.*$|\?.*$/, '');
2918
3030
  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');
2919
- const orderTotal = `$${(Number((_ref44 = object == null ? void 0 : object.amount_total) != null ? _ref44 : 0) / 100).toFixed(2)}`;
3031
+ const orderTotal = `$${(Number((_ref49 = object == null ? void 0 : object.amount_total) != null ? _ref49 : 0) / 100).toFixed(2)}`;
2920
3032
  const orderSummary = [
2921
3033
  linesText,
2922
3034
  licenseText,
@@ -2928,11 +3040,15 @@ async function findOrderForDispute(paymentIntentId) {
2928
3040
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'order-receipt', {
2929
3041
  'order.summary': orderSummary,
2930
3042
  'order.total': orderTotal,
2931
- 'order.ref': String(object.id)
3043
+ 'order.ref': String(object.id),
3044
+ // The store's own Receipt footer (AGL-3432), as a token: the
3045
+ // built-in copy always renders (AGL-3370), so a footer that is
3046
+ // only in the fallback text above never reaches a buyer.
3047
+ 'store.receiptFooter': receiptFooter
2932
3048
  });
2933
3049
  await sendEmail(_extends({
2934
3050
  to: buyerEmailForReceipt,
2935
- subject: (_ref45 = designed == null ? void 0 : designed.subject) != null ? _ref45 : `Receipt for your order`,
3051
+ subject: (_ref50 = designed == null ? void 0 : designed.subject) != null ? _ref50 : `Receipt for your order`,
2936
3052
  text: (designed == null ? void 0 : designed.text) || fallbackText
2937
3053
  }, (designed == null ? void 0 : designed.html) ? {
2938
3054
  html: designed.html
@@ -3009,37 +3125,42 @@ async function findOrderForDispute(paymentIntentId) {
3009
3125
  // is byte-identical to what it was before this issue for exactly that
3010
3126
  // reason. The hold refuses the SECOND shopper; it never gates the
3011
3127
  // first one's goods.
3012
- await releaseStockHold(hostRef, String((_ref36 = (_object_metadata20 = object.metadata) == null ? void 0 : _object_metadata20.stockHoldKey) != null ? _ref36 : ''));
3013
- void notifyHostManagers(String(hostId), _extends({
3128
+ await releaseStockHold(hostRef, String((_ref41 = (_object_metadata20 = object.metadata) == null ? void 0 : _object_metadata20.stockHoldKey) != null ? _ref41 : ''));
3129
+ const cartOrderTotal = `$${(Number((_ref42 = object == null ? void 0 : object.amount_total) != null ? _ref42 : 0) / 100).toFixed(2)}`;
3130
+ const cartUnits = lineItems.reduce((sum, line)=>sum + line.quantity, 0);
3131
+ void notifyHostManagers(String(hostId), {
3014
3132
  type: 'content.order',
3015
- title: `New order — $${(Number((_ref37 = object == null ? void 0 : object.amount_total) != null ? _ref37 : 0) / 100).toFixed(2)}`
3016
- }, (object == null ? void 0 : (_object_customer_details8 = object.customer_details) == null ? void 0 : _object_customer_details8.email) ? {
3017
- body: `From ${object.customer_details.email}`
3018
- } : {}, {
3133
+ title: `New order on {site} — ${cartOrderTotal}`,
3134
+ // What came in, from whom and where (AGL-3432).
3135
+ 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}.` : '.'),
3019
3136
  link: `/${hostId}/products`
3020
- }));
3021
- await captureHostContact(_extends({
3137
+ });
3138
+ await recordCapturedContact(_extends({
3139
+ orgId: '',
3022
3140
  hostId: String(hostId),
3023
- email: object == null ? void 0 : (_object_customer_details9 = object.customer_details) == null ? void 0 : _object_customer_details9.email,
3024
- name: (_ref38 = object == null ? void 0 : (_object_customer_details10 = object.customer_details) == null ? void 0 : _object_customer_details10.name) != null ? _ref38 : undefined,
3025
- source: 'order',
3026
- initialLifecycleStage: 'customer'
3141
+ identity: {
3142
+ email: object == null ? void 0 : (_object_customer_details9 = object.customer_details) == null ? void 0 : _object_customer_details9.email,
3143
+ name: (_ref43 = object == null ? void 0 : (_object_customer_details10 = object.customer_details) == null ? void 0 : _object_customer_details10.name) != null ? _ref43 : null
3144
+ },
3145
+ surface: 'relationship',
3146
+ lifecycleFloor: 'customer'
3027
3147
  }, marketingOptIn ? {
3028
3148
  marketingConsent: true
3029
3149
  } : {}, {
3030
- purchaseCents: Number((_ref39 = object == null ? void 0 : object.amount_total) != null ? _ref39 : 0),
3150
+ purchaseCents: Number((_ref44 = object == null ? void 0 : object.amount_total) != null ? _ref44 : 0),
3031
3151
  interaction: {
3152
+ source: 'order',
3032
3153
  refId: String(object.id),
3033
- summary: `Placed an order ($${(Number((_ref40 = object == null ? void 0 : object.amount_total) != null ? _ref40 : 0) / 100).toFixed(2)})`
3154
+ summary: `Placed an order ($${(Number((_ref45 = object == null ? void 0 : object.amount_total) != null ? _ref45 : 0) / 100).toFixed(2)})`
3034
3155
  }
3035
3156
  }));
3036
3157
  if (couponCode) {
3037
- var _ref46;
3158
+ var _ref51;
3038
3159
  var _object_metadata23;
3039
3160
  await settleRedemption({
3040
3161
  firestore,
3041
3162
  ref: hostRef.collection('coupons').doc(String(couponCode)),
3042
- holdKey: String((_ref46 = (_object_metadata23 = object.metadata) == null ? void 0 : _object_metadata23.couponHoldKey) != null ? _ref46 : ''),
3163
+ holdKey: String((_ref51 = (_object_metadata23 = object.metadata) == null ? void 0 : _object_metadata23.couponHoldKey) != null ? _ref51 : ''),
3043
3164
  orderRef,
3044
3165
  label: `coupon ${couponCode}`,
3045
3166
  detail: `Coupon ${couponCode} was applied to this order but no longer ` + 'exists, so the redemption is uncounted against its limit.'
@@ -3047,10 +3168,10 @@ async function findOrderForDispute(paymentIntentId) {
3047
3168
  }
3048
3169
  // Gift card balance settlement (AGL-322, made a settlement by AGL-2449).
3049
3170
  if ((_object_metadata21 = object.metadata) == null ? void 0 : _object_metadata21.giftCardCode) {
3050
- var _ref47, _ref48;
3171
+ var _ref52, _ref53;
3051
3172
  var _object_metadata24, _object_metadata25;
3052
- const giftCardCents = Number((_ref47 = (_object_metadata24 = object.metadata) == null ? void 0 : _object_metadata24.giftCardCents) != null ? _ref47 : 0);
3053
- const holdKey = String((_ref48 = (_object_metadata25 = object.metadata) == null ? void 0 : _object_metadata25.giftCardHoldKey) != null ? _ref48 : '');
3173
+ const giftCardCents = Number((_ref52 = (_object_metadata24 = object.metadata) == null ? void 0 : _object_metadata24.giftCardCents) != null ? _ref52 : 0);
3174
+ const holdKey = String((_ref53 = (_object_metadata25 = object.metadata) == null ? void 0 : _object_metadata25.giftCardHoldKey) != null ? _ref53 : '');
3054
3175
  const cardRef = hostRef.collection('giftCards').doc(String(object.metadata.giftCardCode));
3055
3176
  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.';
3056
3177
  if (!holdKey) {
@@ -3130,7 +3251,7 @@ async function findOrderForDispute(paymentIntentId) {
3130
3251
  const lineProduct = productsById.get(line.productId);
3131
3252
  if (!(lineProduct == null ? void 0 : lineProduct.giftCard)) continue;
3132
3253
  for(let unit = 0; unit < line.quantity; unit += 1){
3133
- var _ref49, _ref50;
3254
+ var _ref54, _ref55;
3134
3255
  var _object_customer_details12, _object_customer_details13, _object_customer_details14;
3135
3256
  const code = `GC-${createHmac('sha256', String(object.id)).update(`${line.productId}:${unit}:${Date.now()}`).digest('hex').slice(0, 12).toUpperCase()}`;
3136
3257
  // THE EMAIL IS GATED ON THE WRITE (AGL-2161). This `.set()` used
@@ -3149,13 +3270,13 @@ async function findOrderForDispute(paymentIntentId) {
3149
3270
  const issued = await hostRef.collection('giftCards').doc(code).set({
3150
3271
  initialCents: line.unitAmountCents,
3151
3272
  balanceCents: line.unitAmountCents,
3152
- recipientEmail: (_ref49 = object == null ? void 0 : (_object_customer_details12 = object.customer_details) == null ? void 0 : _object_customer_details12.email) != null ? _ref49 : null,
3273
+ recipientEmail: (_ref54 = object == null ? void 0 : (_object_customer_details12 = object.customer_details) == null ? void 0 : _object_customer_details12.email) != null ? _ref54 : null,
3153
3274
  orderId: String(object.id),
3154
3275
  // Which line bought it, so a refund that withdraws that line
3155
3276
  // voids this card and no other (AGL-3363).
3156
3277
  productId: line.productId,
3157
3278
  // What the console's Gift cards search asks (AGL-3321).
3158
- searchTokens: giftCardSearchTokens(code, (_ref50 = object == null ? void 0 : (_object_customer_details13 = object.customer_details) == null ? void 0 : _object_customer_details13.email) != null ? _ref50 : null),
3279
+ searchTokens: giftCardSearchTokens(code, (_ref55 = object == null ? void 0 : (_object_customer_details13 = object.customer_details) == null ? void 0 : _object_customer_details13.email) != null ? _ref55 : null),
3159
3280
  createdAtMs: Date.now()
3160
3281
  }).then(()=>true).catch((error)=>{
3161
3282
  console.error('Gift card issuance failed', hostId, code, error);
@@ -3176,24 +3297,27 @@ async function findOrderForDispute(paymentIntentId) {
3176
3297
  void notifyHostManagers(String(hostId), {
3177
3298
  type: 'content.order',
3178
3299
  title: 'Gift card paid for but not issued',
3179
- 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.',
3300
+ 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.',
3180
3301
  link: `/${hostId}/products`
3181
3302
  });
3182
3303
  continue;
3183
3304
  }
3184
3305
  const giftTo = object == null ? void 0 : (_object_customer_details14 = object.customer_details) == null ? void 0 : _object_customer_details14.email;
3185
3306
  if (giftTo) {
3186
- var _ref51;
3307
+ var _ref56;
3187
3308
  const giftValue = `$${(line.unitAmountCents / 100).toFixed(2)}`;
3188
3309
  const fallbackText = `Gift card code: ${code}\n` + `Value: ${giftValue}\n\n` + 'Enter it at checkout to apply the balance.';
3189
- // Site-owner-designed template when published (AGL-771).
3310
+ // Site-owner-designed template when published (AGL-771). A
3311
+ // bought card carries no note; empty, so the built-in copy
3312
+ // leaves that line out.
3190
3313
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'gift-card', {
3191
3314
  'giftcard.code': code,
3192
- 'giftcard.value': giftValue
3315
+ 'giftcard.value': giftValue,
3316
+ 'giftcard.note': ''
3193
3317
  });
3194
3318
  await sendEmail(_extends({
3195
3319
  to: giftTo,
3196
- subject: (_ref51 = designed == null ? void 0 : designed.subject) != null ? _ref51 : 'Your gift card',
3320
+ subject: (_ref56 = designed == null ? void 0 : designed.subject) != null ? _ref56 : 'Your gift card',
3197
3321
  text: (designed == null ? void 0 : designed.text) || fallbackText
3198
3322
  }, (designed == null ? void 0 : designed.html) ? {
3199
3323
  html: designed.html
@@ -3214,12 +3338,12 @@ async function findOrderForDispute(paymentIntentId) {
3214
3338
  }
3215
3339
  // Discounts engine redemptions (AGL-305).
3216
3340
  if ((_object_metadata22 = object.metadata) == null ? void 0 : _object_metadata22.discountId) {
3217
- var _ref52;
3341
+ var _ref57;
3218
3342
  var _object_metadata26;
3219
3343
  await settleRedemption({
3220
3344
  firestore,
3221
3345
  ref: hostRef.collection('discounts').doc(String(object.metadata.discountId)),
3222
- holdKey: String((_ref52 = (_object_metadata26 = object.metadata) == null ? void 0 : _object_metadata26.discountHoldKey) != null ? _ref52 : ''),
3346
+ holdKey: String((_ref57 = (_object_metadata26 = object.metadata) == null ? void 0 : _object_metadata26.discountHoldKey) != null ? _ref57 : ''),
3223
3347
  orderRef,
3224
3348
  label: `discount ${object.metadata.discountId}`,
3225
3349
  detail: `Discount ${object.metadata.discountId} was applied to this ` + 'order but no longer exists, so the redemption is uncounted ' + 'against its limit.'
@@ -3233,7 +3357,7 @@ async function findOrderForDispute(paymentIntentId) {
3233
3357
  var _object_metadata27;
3234
3358
  const { hostId, orderId, productId } = (_object_metadata27 = object.metadata) != null ? _object_metadata27 : {};
3235
3359
  if (hostId && orderId) {
3236
- var _ref53, _ref54, _ref55;
3360
+ var _ref58, _ref59, _ref60;
3237
3361
  var _object_total_details, _object_total_details1;
3238
3362
  const firestore = firebaseAdmin.app().firestore();
3239
3363
  const hostRef = firestore.collection('hosts').doc(String(hostId));
@@ -3272,7 +3396,7 @@ async function findOrderForDispute(paymentIntentId) {
3272
3396
  //
3273
3397
  // The whole map is written rather than one field of it, so a
3274
3398
  // `merge`-set's nested-map merge and a plain replace agree.
3275
- const shippingCents = Math.max(0, Math.round(Number((_ref53 = object == null ? void 0 : (_object_total_details = object.total_details) == null ? void 0 : _object_total_details.amount_shipping) != null ? _ref53 : 0) || 0));
3399
+ 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));
3276
3400
  // Tax joins shipping in the fold (AGL-1953). `draft-order.ts` now
3277
3401
  // declares tax — a Stripe Tax Rate for a `manual` store, whose figure
3278
3402
  // it already froze into `totals`, or `automatic_tax` for a `stripe`
@@ -3285,8 +3409,8 @@ async function findOrderForDispute(paymentIntentId) {
3285
3409
  // `amount_tax` 0 here (its own figure stays on the order, and its
3286
3410
  // AGL-1953 witness is metadata, not this field), and a draft composed
3287
3411
  // by a store with no tax reports 0 too.
3288
- const stripeTaxCents = Math.max(0, Math.round(Number((_ref54 = object == null ? void 0 : (_object_total_details1 = object.total_details) == null ? void 0 : _object_total_details1.amount_tax) != null ? _ref54 : 0) || 0));
3289
- const chargedTotalCents = Number((_ref55 = object == null ? void 0 : object.amount_total) != null ? _ref55 : NaN);
3412
+ 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));
3413
+ const chargedTotalCents = Number((_ref60 = object == null ? void 0 : object.amount_total) != null ? _ref60 : NaN);
3290
3414
  // `shipping_details` ONLY, unlike the cart branch's
3291
3415
  // `?? customer_details` fallback: that address is the BILLING one, and
3292
3416
  // a card sale rung up at a register completes here too — it must not
@@ -3298,6 +3422,8 @@ async function findOrderForDispute(paymentIntentId) {
3298
3422
  // transaction below. Carried out so the money can be reported after
3299
3423
  // the transaction commits.
3300
3424
  let paidAfterCancel = false;
3425
+ // The canceled order's number, for the notice below (AGL-3432).
3426
+ let canceledOrderLabel = '';
3301
3427
  const flipped = await firestore.runTransaction(async (transaction)=>{
3302
3428
  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;
3303
3429
  var _object_customer_details, _object_metadata, _object_customer_details1, _lifted_totals1;
@@ -3323,6 +3449,7 @@ async function findOrderForDispute(paymentIntentId) {
3323
3449
  const alreadyNoted = ((_lifted_timeline = lifted.timeline) != null ? _lifted_timeline : []).some((entry)=>(entry == null ? void 0 : entry.event) === 'paid-after-cancel');
3324
3450
  if (lifted.status === 'cancelled' && !alreadyNoted) {
3325
3451
  paidAfterCancel = true;
3452
+ canceledOrderLabel = CommerceModel.formatOrderNumber(lifted, String(orderId));
3326
3453
  // `set(..., { merge: true })`, the same call the paying branch
3327
3454
  // below makes on this document. Nothing here is a nested map, so
3328
3455
  // merge and replace agree, and the read above has just proven
@@ -3395,13 +3522,13 @@ async function findOrderForDispute(paymentIntentId) {
3395
3522
  await notifyHostManagers(String(hostId), {
3396
3523
  type: 'content.order',
3397
3524
  title: 'A canceled order was paid',
3398
- 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.',
3525
+ 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.',
3399
3526
  link: `/${hostId}/orders`
3400
3527
  });
3401
3528
  }
3402
3529
  if (flipped) {
3403
- var _ref56, _ref57;
3404
- var _object_metadata28, _object_customer_details15;
3530
+ var _ref61, _ref62, _ref63, _ref64, _ref65, _ref66;
3531
+ var _object_metadata28, _object_customer_details15, _order_totals, _object_customer_details16;
3405
3532
  const order = paidOrder;
3406
3533
  // Discounts engine redemptions (AGL-305), for this branch's BOTH
3407
3534
  // tenants: a console draft order and a POS card sale, which carry the
@@ -3416,20 +3543,25 @@ async function findOrderForDispute(paymentIntentId) {
3416
3543
  // the hold existed, which Stripe's at-least-once delivery would run
3417
3544
  // twice. The transition is what makes it once.
3418
3545
  if ((_object_metadata28 = object.metadata) == null ? void 0 : _object_metadata28.discountId) {
3419
- var _ref58;
3546
+ var _ref67;
3420
3547
  var _object_metadata29;
3421
3548
  await settleRedemption({
3422
3549
  firestore,
3423
3550
  ref: hostRef.collection('discounts').doc(String(object.metadata.discountId)),
3424
- holdKey: String((_ref58 = (_object_metadata29 = object.metadata) == null ? void 0 : _object_metadata29.discountHoldKey) != null ? _ref58 : ''),
3551
+ holdKey: String((_ref67 = (_object_metadata29 = object.metadata) == null ? void 0 : _object_metadata29.discountHoldKey) != null ? _ref67 : ''),
3425
3552
  orderRef,
3426
3553
  label: `discount ${object.metadata.discountId}`,
3427
3554
  detail: `Discount ${object.metadata.discountId} was applied to this ` + 'order but no longer exists, so the redemption is uncounted ' + 'against its limit.'
3428
3555
  });
3429
3556
  }
3557
+ const draftLabel = CommerceModel.formatOrderNumber(order, String(orderId));
3558
+ 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;
3559
+ 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);
3430
3560
  void notifyHostManagers(String(hostId), {
3431
3561
  type: 'content.order',
3432
- title: `Draft order paid — ${CommerceModel.formatOrderNumber(order, String(orderId))}`,
3562
+ title: `Draft order paid — ${draftLabel}`,
3563
+ // Who paid what, and where (AGL-3432).
3564
+ body: `${draftPayer != null ? draftPayer : 'The buyer'} paid draft order ${draftLabel} ` + `on {site}: $${(draftPaidCents / 100).toFixed(2)}.`,
3433
3565
  link: `/${hostId}/products`
3434
3566
  });
3435
3567
  // Contacts ingestion (AGL-1748): this branch flipped the order,
@@ -3444,36 +3576,40 @@ async function findOrderForDispute(paymentIntentId) {
3444
3576
  // figure the draft was priced at, and the two agree by construction,
3445
3577
  // but only one of them is the money that moved. Stored totals remain
3446
3578
  // the fallback for a session shape that reports no total.
3447
- const draftEmail = (_ref56 = (_ref57 = object == null ? void 0 : (_object_customer_details15 = object.customer_details) == null ? void 0 : _object_customer_details15.email) != null ? _ref57 : order.customerEmail) != null ? _ref56 : null;
3579
+ 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;
3448
3580
  if (draftEmail) {
3449
- var _ref59, _ref60, _ref61;
3450
- var _order_totals, _object_customer_details16;
3451
- const chargedCents = Number((_ref59 = object == null ? void 0 : object.amount_total) != null ? _ref59 : 0) || Number((_ref60 = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref60 : 0);
3452
- await captureHostContact(_extends({
3581
+ var _ref68, _ref69, _ref70;
3582
+ var _order_totals1, _object_customer_details17;
3583
+ 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);
3584
+ await recordCapturedContact(_extends({
3585
+ orgId: '',
3453
3586
  hostId: String(hostId),
3454
- email: draftEmail,
3455
- name: (_ref61 = object == null ? void 0 : (_object_customer_details16 = object.customer_details) == null ? void 0 : _object_customer_details16.name) != null ? _ref61 : undefined,
3456
- source: 'order',
3457
- initialLifecycleStage: 'customer'
3587
+ identity: {
3588
+ email: draftEmail,
3589
+ name: (_ref70 = object == null ? void 0 : (_object_customer_details17 = object.customer_details) == null ? void 0 : _object_customer_details17.name) != null ? _ref70 : null
3590
+ },
3591
+ surface: 'relationship',
3592
+ lifecycleFloor: 'customer'
3458
3593
  }, chargedCents > 0 ? {
3459
3594
  purchaseCents: chargedCents
3460
3595
  } : {}, {
3461
3596
  interaction: {
3597
+ source: 'order',
3462
3598
  refId: String(orderId),
3463
3599
  summary: `Paid ${CommerceModel.formatOrderNumber(order, String(orderId))} ($${(chargedCents / 100).toFixed(2)})`
3464
3600
  }
3465
3601
  }));
3466
3602
  }
3467
3603
  if (productId) {
3468
- var _productSnapshot_data, _ref62, _ref63;
3604
+ var _productSnapshot_data, _ref71, _ref72;
3469
3605
  var _object_metadata30, _lifted_variants_, _order_lineItems_, _order_lineItems;
3470
3606
  const productRef = hostRef.collection('products').doc(String(productId));
3471
3607
  const productSnapshot = await productRef.get();
3472
3608
  const lifted = CommerceModel.liftLegacyProduct((_productSnapshot_data = productSnapshot.data()) != null ? _productSnapshot_data : {
3473
3609
  name: 'Product'
3474
3610
  });
3475
- const soldVariantId = String((_ref62 = (_object_metadata30 = object.metadata) == null ? void 0 : _object_metadata30.variantId) != null ? _ref62 : '') || ((_lifted_variants_ = lifted.variants[0]) == null ? void 0 : _lifted_variants_.id);
3476
- const quantity = (_ref63 = (_order_lineItems = order.lineItems) == null ? void 0 : (_order_lineItems_ = _order_lineItems[0]) == null ? void 0 : _order_lineItems_.quantity) != null ? _ref63 : 1;
3611
+ 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);
3612
+ const quantity = (_ref72 = (_order_lineItems = order.lineItems) == null ? void 0 : (_order_lineItems_ = _order_lineItems[0]) == null ? void 0 : _order_lineItems_.quantity) != null ? _ref72 : 1;
3477
3613
  if (soldVariantId) {
3478
3614
  // Atomic since AGL-2320.
3479
3615
  const moved = await decrementVariantStock({
@@ -3525,10 +3661,10 @@ async function findOrderForDispute(paymentIntentId) {
3525
3661
  // would decrement it twice.
3526
3662
  const paidProducts = new Map();
3527
3663
  for (const line of (_order_lineItems1 = order.lineItems) != null ? _order_lineItems1 : []){
3528
- var _ref64, _ref65, _line_variantId1;
3664
+ var _ref73, _ref74, _line_variantId1;
3529
3665
  var _lineProduct_variants_;
3530
- const lineProductId = String((_ref64 = line == null ? void 0 : line.productId) != null ? _ref64 : '');
3531
- const soldQty = Math.round(Number((_ref65 = line == null ? void 0 : line.quantity) != null ? _ref65 : 0));
3666
+ const lineProductId = String((_ref73 = line == null ? void 0 : line.productId) != null ? _ref73 : '');
3667
+ const soldQty = Math.round(Number((_ref74 = line == null ? void 0 : line.quantity) != null ? _ref74 : 0));
3532
3668
  if (!lineProductId || !(soldQty > 0)) continue;
3533
3669
  // Firestore reserves `__…__` ids and `.doc()` throws
3534
3670
  // synchronously on one; a corrupt line must not fail the
@@ -3591,8 +3727,8 @@ async function findOrderForDispute(paymentIntentId) {
3591
3727
  var _object_metadata31;
3592
3728
  const { hostId, productId, feeCents, couponCode } = (_object_metadata31 = object.metadata) != null ? _object_metadata31 : {};
3593
3729
  if (hostId && productId) {
3594
- var _ref66, _productForSnapshot_get1, _productForSnapshot_data1, _ref67, _ref68, _ref69, _ref70, _ref71, _ref72, _ref73;
3595
- var _object_metadata32, _buyNowLineItems_, _object_customer_details17, _object_customer_details18, _object_customer_details19, _object_metadata33;
3730
+ var _ref75, _productForSnapshot_get2, _productForSnapshot_data1, _ref76, _ref77, _ref78, _ref79, _ref80, _ref81, _ref82;
3731
+ var _object_metadata32, _buyNowLineItems_, _object_customer_details18, _object_customer_details19, _object_customer_details20, _object_metadata33;
3596
3732
  const firestore = firebaseAdmin.app().firestore();
3597
3733
  const hostRef = firestore.collection('hosts').doc(String(hostId));
3598
3734
  // Orders v1 (AGL-283): line-item snapshot + totals + timeline with
@@ -3602,9 +3738,9 @@ async function findOrderForDispute(paymentIntentId) {
3602
3738
  // is bounded to Stripe's at-least-once edge).
3603
3739
  const orderRef = hostRef.collection('orders').doc(String(object.id));
3604
3740
  const counterRef = hostRef.collection('counters').doc('orders');
3605
- const amountCents = Number((_ref66 = object == null ? void 0 : object.amount_total) != null ? _ref66 : 0);
3741
+ const amountCents = Number((_ref75 = object == null ? void 0 : object.amount_total) != null ? _ref75 : 0);
3606
3742
  const productForSnapshot = await hostRef.collection('products').doc(String(productId)).get();
3607
- const snapshotName = String((_productForSnapshot_get1 = productForSnapshot.get('name')) != null ? _productForSnapshot_get1 : 'Product');
3743
+ const snapshotName = String((_productForSnapshot_get2 = productForSnapshot.get('name')) != null ? _productForSnapshot_get2 : 'Product');
3608
3744
  // AGL-1711: the line item and totals used to be fabricated from
3609
3745
  // `amount_total` alone — one unit, priced at the whole charge, with tax
3610
3746
  // and discount recorded as 0. `computeBuyNowOrder` rebuilds the real
@@ -3615,7 +3751,7 @@ async function findOrderForDispute(paymentIntentId) {
3615
3751
  name: snapshotName
3616
3752
  });
3617
3753
  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];
3618
- const variantOptions = Object.values((_ref67 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref67 : {});
3754
+ const variantOptions = Object.values((_ref76 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref76 : {});
3619
3755
  const { lineItems: buyNowLineItems, totals: buyNowTotals } = CommerceModel.computeBuyNowOrder(object, _extends({
3620
3756
  name: snapshotName
3621
3757
  }, variantOptions.length ? {
@@ -3627,7 +3763,10 @@ async function findOrderForDispute(paymentIntentId) {
3627
3763
  } : {}, liftedForSnapshot.supplierId ? {
3628
3764
  supplierId: liftedForSnapshot.supplierId
3629
3765
  } : {}));
3630
- const soldQuantity = (_ref68 = (_buyNowLineItems_ = buyNowLineItems[0]) == null ? void 0 : _buyNowLineItems_.quantity) != null ? _ref68 : 1;
3766
+ const soldQuantity = (_ref77 = (_buyNowLineItems_ = buyNowLineItems[0]) == null ? void 0 : _buyNowLineItems_.quantity) != null ? _ref77 : 1;
3767
+ // The number the merchant knows this order by, for the notices below
3768
+ // (AGL-3432).
3769
+ let buyNowOrderNumber;
3631
3770
  const created = await firestore.runTransaction(async (transaction)=>{
3632
3771
  var _counter_get, _ref, _ref1, _ref2, _ref3;
3633
3772
  var _object_metadata, _object_customer_details, _object_customer_details1;
@@ -3637,6 +3776,7 @@ async function findOrderForDispute(paymentIntentId) {
3637
3776
  ]);
3638
3777
  if (existing.exists) return false;
3639
3778
  const number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
3779
+ buyNowOrderNumber = number;
3640
3780
  transaction.set(counterRef, {
3641
3781
  next: number + 1
3642
3782
  }, {
@@ -3683,16 +3823,19 @@ async function findOrderForDispute(paymentIntentId) {
3683
3823
  // Redelivery guard (AGL-498): skip the notification + fulfilment side
3684
3824
  // effects when this order already existed.
3685
3825
  if (!created) return;
3826
+ const buyNowOrderLabel = CommerceModel.formatOrderNumber({
3827
+ number: buyNowOrderNumber
3828
+ }, String(object.id));
3686
3829
  // In-app order notification (wave v6): host managers see sales
3687
- // in the bell, not just the owner's email.
3688
- void notifyHostManagers(String(hostId), _extends({
3830
+ // in the bell, not just the owner's email: what came in, from whom
3831
+ // and where (AGL-3432).
3832
+ const buyNowTotal = `$${(Number((_ref78 = object == null ? void 0 : object.amount_total) != null ? _ref78 : 0) / 100).toFixed(2)}`;
3833
+ void notifyHostManagers(String(hostId), {
3689
3834
  type: 'content.order',
3690
- title: `New order — $${(Number((_ref69 = object == null ? void 0 : object.amount_total) != null ? _ref69 : 0) / 100).toFixed(2)}`
3691
- }, (object == null ? void 0 : (_object_customer_details17 = object.customer_details) == null ? void 0 : _object_customer_details17.email) ? {
3692
- body: `From ${object.customer_details.email}`
3693
- } : {}, {
3835
+ title: `New order on {site} — ${buyNowTotal}`,
3836
+ 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}.` : '.'),
3694
3837
  link: `/${hostId}/products`
3695
- }));
3838
+ });
3696
3839
  // Dropship routing (AGL-289): paid lines with a supplier notify
3697
3840
  // it (signed webhook and/or email) and stash a callback token so
3698
3841
  // the supplier can post tracking back. Plan-gated; failures never
@@ -3713,8 +3856,8 @@ async function findOrderForDispute(paymentIntentId) {
3713
3856
  // `supplier-outbox.ts` retries out of band.
3714
3857
  await (async ()=>{
3715
3858
  try {
3716
- var _routedProduct_get, _ref, _ref1;
3717
- var _object_customer_details, _object_customer_details1;
3859
+ 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;
3860
+ var _object_shipping_details, _object_customer_details, _object_customer_details1;
3718
3861
  const routedOrg = await getOrgForHost(String(hostId));
3719
3862
  if (!Aglyn.checkEntitlement(routedOrg == null ? void 0 : routedOrg.org, 'dropshipRouting')) {
3720
3863
  return;
@@ -3737,6 +3880,11 @@ async function findOrderForDispute(paymentIntentId) {
3737
3880
  }, {
3738
3881
  merge: true
3739
3882
  });
3883
+ // Where the parcel goes (AGL-3432): the address the shopper gave
3884
+ // at checkout. Buy-now asks for one only when the store ships, so
3885
+ // it can be absent, and a billing address is never offered in its
3886
+ // place.
3887
+ const shipTo = (object == null ? void 0 : (_object_shipping_details = object.shipping_details) == null ? void 0 : _object_shipping_details.address) ? object.shipping_details : null;
3740
3888
  const payload = {
3741
3889
  hostId: String(hostId),
3742
3890
  orderId: String(object.id),
@@ -3746,7 +3894,15 @@ async function findOrderForDispute(paymentIntentId) {
3746
3894
  // the buyer paid for.
3747
3895
  quantity: soldQuantity,
3748
3896
  customerEmail: (_ref = object == null ? void 0 : (_object_customer_details = object.customer_details) == null ? void 0 : _object_customer_details.email) != null ? _ref : null,
3749
- shippingName: (_ref1 = object == null ? void 0 : (_object_customer_details1 = object.customer_details) == null ? void 0 : _object_customer_details1.name) != null ? _ref1 : null,
3897
+ 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,
3898
+ shippingAddress: shipTo ? {
3899
+ line1: (_shipTo_address_line1 = shipTo.address.line1) != null ? _shipTo_address_line1 : null,
3900
+ line2: (_shipTo_address_line2 = shipTo.address.line2) != null ? _shipTo_address_line2 : null,
3901
+ city: (_shipTo_address_city = shipTo.address.city) != null ? _shipTo_address_city : null,
3902
+ state: (_shipTo_address_state = shipTo.address.state) != null ? _shipTo_address_state : null,
3903
+ postalCode: (_shipTo_address_postal_code = shipTo.address.postal_code) != null ? _shipTo_address_postal_code : null,
3904
+ country: (_shipTo_address_country = shipTo.address.country) != null ? _shipTo_address_country : null
3905
+ } : null,
3750
3906
  updateUrl: `https://${requestHost}/api/commerce/supplier-update` + `?hostId=${hostId}&orderId=${object.id}&token=${supplierToken}`
3751
3907
  };
3752
3908
  if (supplier.webhookUrl) {
@@ -3771,11 +3927,31 @@ async function findOrderForDispute(paymentIntentId) {
3771
3927
  });
3772
3928
  }
3773
3929
  if (supplier.email) {
3774
- var _ref2, _payload_shippingName;
3930
+ var _resolveHostToken, _ref3, _payload_shippingName;
3931
+ // Which store is asking (AGL-3432): a supplier serves several,
3932
+ // and this mail goes out with no site header to say so.
3933
+ const storeName = (_resolveHostToken = resolveHostToken('businessName', await hostRef.get().then((snapshot)=>snapshot.data()).catch(()=>undefined))) != null ? _resolveHostToken : 'A store';
3934
+ const recipient = (_ref3 = (_payload_shippingName = payload.shippingName) != null ? _payload_shippingName : payload.customerEmail) != null ? _ref3 : 'the buyer';
3935
+ const address = payload.shippingAddress ? [
3936
+ payload.shippingAddress.line1,
3937
+ payload.shippingAddress.line2,
3938
+ [
3939
+ payload.shippingAddress.city,
3940
+ [
3941
+ payload.shippingAddress.state,
3942
+ payload.shippingAddress.postalCode
3943
+ ].filter(Boolean).join(' ')
3944
+ ].filter(Boolean).join(', '),
3945
+ payload.shippingAddress.country
3946
+ ].filter(Boolean).join(', ') : '';
3775
3947
  await sendEmail({
3776
3948
  to: supplier.email,
3777
- subject: `New order to fulfill: ${payload.productName}`,
3778
- 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`,
3949
+ subject: `New order to fulfill for ${storeName}: ${payload.productName}`,
3950
+ // The tracking link opens a form for the carrier and the
3951
+ // number, so it carries no placeholders: a link with
3952
+ // `trackingNumber=TRACKING` in it, opened unedited, records
3953
+ // that word as the tracking.
3954
+ 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}`,
3779
3955
  fromName: (await brandFor(hostId)).fromName,
3780
3956
  sendingIdentity: await hostSendingIdentity(String(hostId)),
3781
3957
  audience: 'tenant',
@@ -3793,16 +3969,20 @@ async function findOrderForDispute(paymentIntentId) {
3793
3969
  }
3794
3970
  })();
3795
3971
  // Contacts ingestion (AGL-197): buyers become contacts.
3796
- await captureHostContact({
3972
+ await recordCapturedContact({
3973
+ orgId: '',
3797
3974
  hostId: String(hostId),
3798
- email: object == null ? void 0 : (_object_customer_details18 = object.customer_details) == null ? void 0 : _object_customer_details18.email,
3799
- name: (_ref70 = object == null ? void 0 : (_object_customer_details19 = object.customer_details) == null ? void 0 : _object_customer_details19.name) != null ? _ref70 : undefined,
3800
- source: 'order',
3801
- initialLifecycleStage: 'customer',
3802
- purchaseCents: Number((_ref71 = object == null ? void 0 : object.amount_total) != null ? _ref71 : 0),
3975
+ identity: {
3976
+ email: object == null ? void 0 : (_object_customer_details19 = object.customer_details) == null ? void 0 : _object_customer_details19.email,
3977
+ name: (_ref79 = object == null ? void 0 : (_object_customer_details20 = object.customer_details) == null ? void 0 : _object_customer_details20.name) != null ? _ref79 : null
3978
+ },
3979
+ surface: 'relationship',
3980
+ lifecycleFloor: 'customer',
3981
+ purchaseCents: Number((_ref80 = object == null ? void 0 : object.amount_total) != null ? _ref80 : 0),
3803
3982
  interaction: {
3983
+ source: 'order',
3804
3984
  refId: String(object.id),
3805
- summary: `Placed an order ($${(Number((_ref72 = object == null ? void 0 : object.amount_total) != null ? _ref72 : 0) / 100).toFixed(2)})`
3985
+ summary: `Placed an order ($${(Number((_ref81 = object == null ? void 0 : object.amount_total) != null ? _ref81 : 0) / 100).toFixed(2)})`
3806
3986
  }
3807
3987
  });
3808
3988
  const productRef = hostRef.collection('products').doc(String(productId));
@@ -3820,12 +4000,12 @@ async function findOrderForDispute(paymentIntentId) {
3820
4000
  // serialize; the snapshot above is still read, for the product NAME on
3821
4001
  // the receipt below, and no longer decides any count.
3822
4002
  {
3823
- var _productSnapshot_data2, _ref74;
4003
+ var _productSnapshot_data2, _ref83;
3824
4004
  var _object_metadata34, _lifted_variants_1;
3825
4005
  const lifted = CommerceModel.liftLegacyProduct((_productSnapshot_data2 = productSnapshot.data()) != null ? _productSnapshot_data2 : {
3826
4006
  name: 'Product'
3827
4007
  });
3828
- const soldVariantId = String((_ref74 = (_object_metadata34 = object.metadata) == null ? void 0 : _object_metadata34.variantId) != null ? _ref74 : '') || ((_lifted_variants_1 = lifted.variants[0]) == null ? void 0 : _lifted_variants_1.id);
4008
+ 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);
3829
4009
  if (soldVariantId) {
3830
4010
  // AGL-1711: `-1` regardless of how many units were bought, so a
3831
4011
  // 3-unit buy-now sale decremented stock by one and the difference
@@ -3856,14 +4036,14 @@ async function findOrderForDispute(paymentIntentId) {
3856
4036
  // Settlement, the same way round and for the same reason as the cart
3857
4037
  // branch above (AGL-2356): the decrement lands first, then the
3858
4038
  // reservation is dropped.
3859
- await releaseStockHold(hostRef, String((_ref73 = (_object_metadata33 = object.metadata) == null ? void 0 : _object_metadata33.stockHoldKey) != null ? _ref73 : ''));
4039
+ await releaseStockHold(hostRef, String((_ref82 = (_object_metadata33 = object.metadata) == null ? void 0 : _object_metadata33.stockHoldKey) != null ? _ref82 : ''));
3860
4040
  if (couponCode) {
3861
- var _ref75;
4041
+ var _ref84;
3862
4042
  var _object_metadata35;
3863
4043
  await settleRedemption({
3864
4044
  firestore,
3865
4045
  ref: hostRef.collection('coupons').doc(String(couponCode)),
3866
- holdKey: String((_ref75 = (_object_metadata35 = object.metadata) == null ? void 0 : _object_metadata35.couponHoldKey) != null ? _ref75 : ''),
4046
+ holdKey: String((_ref84 = (_object_metadata35 = object.metadata) == null ? void 0 : _object_metadata35.couponHoldKey) != null ? _ref84 : ''),
3867
4047
  orderRef,
3868
4048
  label: `coupon ${couponCode}`,
3869
4049
  detail: `Coupon ${couponCode} was applied to this order but no longer ` + 'exists, so the redemption is uncounted against its limit.'
@@ -3872,24 +4052,31 @@ async function findOrderForDispute(paymentIntentId) {
3872
4052
  // Receipt + seller notification (AGL-96): env-gated like every
3873
4053
  // other outbound email; failures never fail the webhook.
3874
4054
  if (isEmailConfigured()) {
3875
- var _productSnapshot_get, _ref76;
3876
- var _object_customer_details20, _org, _this3;
4055
+ var _productSnapshot_get, _ref85;
4056
+ var _object_customer_details21, _org, _this3;
3877
4057
  const productName = String((_productSnapshot_get = productSnapshot.get('name')) != null ? _productSnapshot_get : 'your purchase');
3878
- const amount = (Number((_ref76 = object == null ? void 0 : object.amount_total) != null ? _ref76 : 0) / 100).toFixed(2);
3879
- const buyerEmail = object == null ? void 0 : (_object_customer_details20 = object.customer_details) == null ? void 0 : _object_customer_details20.email;
4058
+ const amount = (Number((_ref85 = object == null ? void 0 : object.amount_total) != null ? _ref85 : 0) / 100).toFixed(2);
4059
+ const buyerEmail = object == null ? void 0 : (_object_customer_details21 = object.customer_details) == null ? void 0 : _object_customer_details21.email;
3880
4060
  const orderTotal = `$${amount}`;
3881
4061
  if (buyerEmail) {
3882
- var _ref77;
3883
- const fallbackText = `Thanks for your purchase!\n\n${productName} — $${amount}` + `\nOrder reference: ${object.id}`;
4062
+ var _ref86, _ref87;
4063
+ // The store's own Receipt footer, as the cart receipt carries it
4064
+ // (AGL-3432).
4065
+ const receiptFooter = String((_ref86 = await hostRef.collection('settings').doc('store').get().then((snapshot)=>snapshot.get('receiptFooter')).catch(()=>'')) != null ? _ref86 : '');
4066
+ // The quantity with the name (AGL-3432), so a three-unit sale
4067
+ // does not read as one item at the whole total.
4068
+ const receiptLine = `${soldQuantity}× ${productName}`;
4069
+ const fallbackText = `Thanks for your purchase!\n\n${receiptLine} — $${amount}` + `\nOrder reference: ${object.id}` + (receiptFooter ? `\n\n${receiptFooter}` : '');
3884
4070
  // Site-owner-designed template when published (AGL-771).
3885
4071
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'order-receipt', {
3886
- 'order.summary': productName,
4072
+ 'order.summary': receiptLine,
3887
4073
  'order.total': orderTotal,
3888
- 'order.ref': String(object.id)
4074
+ 'order.ref': String(object.id),
4075
+ 'store.receiptFooter': receiptFooter
3889
4076
  });
3890
4077
  await sendEmail(_extends({
3891
4078
  to: String(buyerEmail),
3892
- subject: (_ref77 = designed == null ? void 0 : designed.subject) != null ? _ref77 : `Receipt: ${productName}`,
4079
+ subject: (_ref87 = designed == null ? void 0 : designed.subject) != null ? _ref87 : `Receipt: ${productName}`,
3893
4080
  text: (designed == null ? void 0 : designed.text) || fallbackText
3894
4081
  }, (designed == null ? void 0 : designed.html) ? {
3895
4082
  html: designed.html
@@ -3923,7 +4110,7 @@ async function findOrderForDispute(paymentIntentId) {
3923
4110
  // SSO.
3924
4111
  const seller = (_this4 = await findUserByUidAcrossPools(sellerUid).catch(()=>null)) == null ? void 0 : _this4.record;
3925
4112
  if (seller == null ? void 0 : seller.email) {
3926
- var _hostSnapshot_get, _ref78;
4113
+ var _hostSnapshot_get, _ref88;
3927
4114
  const siteName = String((_hostSnapshot_get = hostSnapshot.get('displayName')) != null ? _hostSnapshot_get : hostId);
3928
4115
  const fallbackText = `You made a sale on ${siteName}!\n\n${productName} — $${amount}` + (buyerEmail ? `\nBuyer: ${buyerEmail}` : '') + `\nOrder reference: ${object.id}`;
3929
4116
  // Site-owner-designed template when published (AGL-771).
@@ -3936,7 +4123,7 @@ async function findOrderForDispute(paymentIntentId) {
3936
4123
  });
3937
4124
  await sendEmail(_extends({
3938
4125
  to: seller.email,
3939
- subject: (_ref78 = designed == null ? void 0 : designed.subject) != null ? _ref78 : `New order: ${productName}`,
4126
+ subject: (_ref88 = designed == null ? void 0 : designed.subject) != null ? _ref88 : `New order: ${productName}`,
3940
4127
  text: (designed == null ? void 0 : designed.text) || fallbackText
3941
4128
  }, (designed == null ? void 0 : designed.html) ? {
3942
4129
  html: designed.html
@@ -3966,15 +4153,15 @@ async function findOrderForDispute(paymentIntentId) {
3966
4153
  // the refund is written on the order's timeline. Nothing else is moved: a
3967
4154
  // Dashboard refund is staff's act, and they chose its transfer reversal.
3968
4155
  if (type === 'charge.refunded') {
3969
- var _ref79, _ref80;
4156
+ var _ref89, _ref90;
3970
4157
  var _object_payment_intent, _snapshot_ref_parent_parent;
3971
- const paymentIntentId = typeof (object == null ? void 0 : object.payment_intent) === 'string' ? object.payment_intent : String((_ref79 = object == null ? void 0 : (_object_payment_intent = object.payment_intent) == null ? void 0 : _object_payment_intent.id) != null ? _ref79 : '');
4158
+ 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 : '');
3972
4159
  const lookup = paymentIntentId ? await findOrderForDispute(paymentIntentId) : {
3973
4160
  kind: 'not-ours'
3974
4161
  };
3975
4162
  if (lookup.kind !== 'order') return;
3976
4163
  const snapshot = lookup.snapshot;
3977
- const hostId = String((_ref80 = (_snapshot_ref_parent_parent = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent.id) != null ? _ref80 : '');
4164
+ const hostId = String((_ref90 = (_snapshot_ref_parent_parent = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent.id) != null ? _ref90 : '');
3978
4165
  const external = await recordExternalRefund(snapshot.ref, object);
3979
4166
  if (external && hostId && snapshot.ref.parent.parent) {
3980
4167
  const outcome = await applyExternalRefundToGiftCards({
@@ -3986,9 +4173,12 @@ async function findOrderForDispute(paymentIntentId) {
3986
4173
  fullyRefunded: external.fullyRefunded
3987
4174
  });
3988
4175
  if (outcome.kind === 'frozen-for-review') {
4176
+ var _snapshot_data;
4177
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
3989
4178
  await notifyGiftCardHold({
3990
4179
  hostId,
3991
- orderLabel: `order ${snapshot.id}`,
4180
+ // The order number the merchant knows it by, never its document id.
4181
+ orderLabel: `order ${CommerceModel.formatOrderNumber(order, snapshot.id)}`,
3992
4182
  orderPath: `/${hostId}/products/orders?order=${encodeURIComponent(snapshot.id)}`,
3993
4183
  orderId: snapshot.id,
3994
4184
  cards: outcome.cards,
@@ -4012,20 +4202,23 @@ async function findOrderForDispute(paymentIntentId) {
4012
4202
  // merchant; both report the site so the seller's fraud ledger can name it.
4013
4203
  const risk = paymentRiskEventFrom(type, object);
4014
4204
  if (risk && risk.signal.kind !== 'dispute') {
4015
- var _ref81, _snapshot_data;
4016
- var _snapshot_ref_parent_parent1;
4205
+ var _ref91, _snapshot_data1, _ref92, _ref93;
4206
+ var _snapshot_ref_parent_parent1, _order_totals2;
4017
4207
  const lookup = risk.paymentIntentId ? await findOrderForDispute(risk.paymentIntentId) : {
4018
4208
  kind: 'not-ours'
4019
4209
  };
4020
4210
  if (lookup.kind !== 'order') return;
4021
4211
  const snapshot = lookup.snapshot;
4022
- const hostId = String((_ref81 = (_snapshot_ref_parent_parent1 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent1.id) != null ? _ref81 : '');
4023
- const order = CommerceModel.liftLegacyOrder((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
4212
+ const hostId = String((_ref91 = (_snapshot_ref_parent_parent1 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent1.id) != null ? _ref91 : '');
4213
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data1 = snapshot.data()) != null ? _snapshot_data1 : {});
4214
+ const orderCents = Number((_ref92 = (_ref93 = (_order_totals2 = order.totals) == null ? void 0 : _order_totals2.totalCents) != null ? _ref93 : order.amountCents) != null ? _ref92 : 0);
4024
4215
  await recordPaymentRiskOnRecord({
4025
4216
  ref: snapshot.ref,
4026
4217
  signal: risk.signal,
4027
4218
  hostId,
4028
4219
  subjectLabel: `order ${CommerceModel.formatOrderNumber(order, snapshot.id)}`,
4220
+ // The notice names the sum in question, as the dispute's does.
4221
+ amount: orderCents > 0 ? `$${(orderCents / 100).toFixed(2)}` : null,
4029
4222
  // The Orders section of the Products hub, with this order's dialog
4030
4223
  // open — where Refund lives (`siteRecordLinks().order`).
4031
4224
  link: `/${hostId}/products/orders?order=${encodeURIComponent(snapshot.id)}`,
@@ -4081,7 +4274,7 @@ async function findOrderForDispute(paymentIntentId) {
4081
4274
  // finds). AGL-1554 will add its own `closed` branch for the marketplace
4082
4275
  // side; the two coexist because each self-selects by finding its own record.
4083
4276
  if (type === 'charge.dispute.created' || type === 'charge.dispute.closed') {
4084
- var _dispute_payment_intent, _ref82;
4277
+ var _dispute_payment_intent, _ref94;
4085
4278
  var _snapshot_ref_parent_parent2;
4086
4279
  const dispute = object != null ? object : {};
4087
4280
  const paymentIntentId = String((_dispute_payment_intent = dispute.payment_intent) != null ? _dispute_payment_intent : '');
@@ -4107,14 +4300,16 @@ async function findOrderForDispute(paymentIntentId) {
4107
4300
  const claimed = lookup.kind === 'order' || lookup.kind === 'unresolved';
4108
4301
  const snapshot = lookup.kind === 'order' ? lookup.snapshot : null;
4109
4302
  if (snapshot) {
4110
- var _ref83;
4303
+ var _ref95;
4111
4304
  var _snapshot_ref_parent_parent3;
4112
- const hostId = String((_ref83 = (_snapshot_ref_parent_parent3 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent3.id) != null ? _ref83 : '');
4305
+ const hostId = String((_ref95 = (_snapshot_ref_parent_parent3 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent3.id) != null ? _ref95 : '');
4113
4306
  if (type === 'charge.dispute.created') {
4307
+ var _snapshot_data2;
4114
4308
  // A chargeback's gift cards are frozen while it is open (AGL-3363).
4309
+ const disputed = CommerceModel.liftLegacyOrder((_snapshot_data2 = snapshot.data()) != null ? _snapshot_data2 : {});
4115
4310
  await notifyGiftCardHold({
4116
4311
  hostId,
4117
- orderLabel: `order ${snapshot.id}`,
4312
+ orderLabel: `order ${CommerceModel.formatOrderNumber(disputed, snapshot.id)}`,
4118
4313
  orderPath: `/${hostId}/products/orders?order=${encodeURIComponent(snapshot.id)}`,
4119
4314
  orderId: snapshot.id,
4120
4315
  cards: await applyOrderGiftCardRisk(snapshot, {
@@ -4125,7 +4320,7 @@ async function findOrderForDispute(paymentIntentId) {
4125
4320
  }, notifyRiskEvent);
4126
4321
  const { opened, record } = await recordDisputeOpened(snapshot, dispute);
4127
4322
  if (opened && hostId) {
4128
- var _snapshot_data1, _record_evidenceDueByMs, _ref84;
4323
+ var _snapshot_data3, _record_evidenceDueByMs, _ref96;
4129
4324
  // Time-critical — Stripe's evidence window is days, not weeks — so
4130
4325
  // this is awaited rather than fired off with `void`: the handler is
4131
4326
  // serverless and work left running past the response is work the
@@ -4133,7 +4328,7 @@ async function findOrderForDispute(paymentIntentId) {
4133
4328
  // (AGL-3368) tells the site's managers and the workspace's owners
4134
4329
  // what a dispute means and when the evidence is due; it never
4135
4330
  // throws.
4136
- const order = CommerceModel.liftLegacyOrder((_snapshot_data1 = snapshot.data()) != null ? _snapshot_data1 : {});
4331
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data3 = snapshot.data()) != null ? _snapshot_data3 : {});
4137
4332
  await notifyRiskEvent({
4138
4333
  kind: 'sale-dispute',
4139
4334
  orgId: null,
@@ -4144,16 +4339,16 @@ async function findOrderForDispute(paymentIntentId) {
4144
4339
  },
4145
4340
  amount: `$${(record.amountCents / 100).toFixed(2)}`,
4146
4341
  evidenceDueByMs: (_record_evidenceDueByMs = record.evidenceDueByMs) != null ? _record_evidenceDueByMs : null,
4147
- dedupeKey: `sale:${String((_ref84 = dispute == null ? void 0 : dispute.id) != null ? _ref84 : snapshot.id)}`,
4342
+ dedupeKey: `sale:${String((_ref96 = dispute == null ? void 0 : dispute.id) != null ? _ref96 : snapshot.id)}`,
4148
4343
  staffEvidence: record.reason ? `Reason: ${record.reason.replace(/_/g, ' ')}.` : null
4149
4344
  });
4150
4345
  }
4151
4346
  } else {
4152
- var _ref85, _ref86;
4347
+ var _ref97, _snapshot_data4, _ref98;
4153
4348
  // Lost: the money went back, so the cards it bought are voided. Won
4154
4349
  // (or closed on a warning): the dispute's freeze lifts; a freeze a
4155
4350
  // fraud warning or review put on stays for the merchant (AGL-3363).
4156
- await applyOrderGiftCardRisk(snapshot, String((_ref85 = dispute == null ? void 0 : dispute.status) != null ? _ref85 : '') === 'lost' ? {
4351
+ await applyOrderGiftCardRisk(snapshot, String((_ref97 = dispute == null ? void 0 : dispute.status) != null ? _ref97 : '') === 'lost' ? {
4157
4352
  kind: 'void',
4158
4353
  reason: 'dispute-lost'
4159
4354
  } : {
@@ -4161,11 +4356,18 @@ async function findOrderForDispute(paymentIntentId) {
4161
4356
  reason: 'dispute'
4162
4357
  });
4163
4358
  const settled = await recordDisputeClosed(snapshot, dispute);
4359
+ // The number the merchant knows the order by (AGL-3432), for both
4360
+ // notices below; the Stripe session id is nowhere in the console.
4361
+ const disputedOrderLabel = CommerceModel.formatOrderNumber(CommerceModel.liftLegacyOrder((_snapshot_data4 = snapshot.data()) != null ? _snapshot_data4 : {}), snapshot.id);
4164
4362
  if (settled.recorded && hostId) {
4363
+ // What the shopper's bank took back left the PLATFORM's balance on
4364
+ // a destination charge, not the merchant's: their own loss is the
4365
+ // seller share in the "Payout adjusted" notice below. Told that way
4366
+ // (AGL-3432), so the two are not read as two losses.
4165
4367
  await notifyHostManagers(hostId, {
4166
4368
  type: 'content.order',
4167
- title: settled.lost ? `Chargeback lost — $${(settled.reversedCents / 100).toFixed(2)} reversed` : 'Chargeback resolved in your favor',
4168
- body: `Order ${snapshot.id}`,
4369
+ title: settled.lost ? `Chargeback lost — order ${disputedOrderLabel}` : 'Chargeback resolved in your favor',
4370
+ 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.',
4169
4371
  link: `/${hostId}/orders`
4170
4372
  });
4171
4373
  // The customer's side of the ledger (AGL-1754), through the same
@@ -4217,7 +4419,7 @@ async function findOrderForDispute(paymentIntentId) {
4217
4419
  // transiently-failed reversal gets its retry — and gated inside on
4218
4420
  // the order's own `reversedTransferCents` marker, so it runs at most
4219
4421
  // once per dispute however many times Stripe delivers.
4220
- if (String((_ref86 = dispute == null ? void 0 : dispute.status) != null ? _ref86 : '') === 'lost') {
4422
+ if (String((_ref98 = dispute == null ? void 0 : dispute.status) != null ? _ref98 : '') === 'lost') {
4221
4423
  const pulledBackCents = await reverseSellerShare(snapshot.ref, dispute);
4222
4424
  // The merchant-experience half of the AGL-1794 decision, and not an
4223
4425
  // optional extra: the reversal takes money out of the connected
@@ -4237,7 +4439,7 @@ async function findOrderForDispute(paymentIntentId) {
4237
4439
  await notifyHostManagers(hostId, {
4238
4440
  type: 'content.order',
4239
4441
  title: `Payout adjusted — $${(pulledBackCents / 100).toFixed(2)} recovered for a lost chargeback`,
4240
- 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.`,
4442
+ 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.',
4241
4443
  link: `/${hostId}/orders`
4242
4444
  });
4243
4445
  }
@@ -4247,7 +4449,7 @@ async function findOrderForDispute(paymentIntentId) {
4247
4449
  return _extends({
4248
4450
  claimed
4249
4451
  }, snapshot ? {
4250
- hostId: String((_ref82 = (_snapshot_ref_parent_parent2 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent2.id) != null ? _ref82 : '')
4452
+ hostId: String((_ref94 = (_snapshot_ref_parent_parent2 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent2.id) != null ? _ref94 : '')
4251
4453
  } : {});
4252
4454
  }
4253
4455
  };