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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) 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 +342 -170
  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 +1 -1
  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
@@ -19,6 +19,8 @@ import { findUserByUidAcrossPools, firebaseAdmin, getOrgForHost, meterHostEmail,
19
19
  import { paymentProvider } from "@aglyn/tenant-data-admin/server/payment-provider";
20
20
  import { captureHostContact } from "@aglyn/tenant-runtime";
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));
@@ -1891,16 +1943,17 @@ 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
1958
  // this file. `upsertHostContact` carries `purchaseCents`, so it is what
1906
1959
  // feeds `ltvCents` and the RFM ranking — a `void`ed one is a paying
@@ -2362,7 +2415,12 @@ async function findOrderForDispute(paymentIntentId) {
2362
2415
  // `renewalOrgUnreadable` and not `!renewalOrg` (AGL-2258): a null org
2363
2416
  // we successfully read still stops, which is AGL-2071's decision.
2364
2417
  if (!renewalEntitled && !renewalOrgUnreadable) {
2365
- await stopLapsedStorefrontSubscription(invoiceHostId, subscriptionId, subscriptionRef);
2418
+ var _ref25, _ref26;
2419
+ await stopLapsedStorefrontSubscription(invoiceHostId, subscriptionId, subscriptionRef, {
2420
+ subscriberEmail: (_ref25 = (_ref26 = object == null ? void 0 : object.customer_email) != null ? _ref26 : soldSnapshot.get('customerEmail')) != null ? _ref25 : null,
2421
+ productName: snapshot.name || 'a subscription product',
2422
+ org: renewalOrg != null ? renewalOrg : null
2423
+ });
2366
2424
  } else if (renewalEntitled) {
2367
2425
  // RE-PRICE THE PLATFORM FEE (AGL-2289). `application_fee_percent` was
2368
2426
  // set once at the sale and never revisited, and it lives on the
@@ -2411,8 +2469,8 @@ async function findOrderForDispute(paymentIntentId) {
2411
2469
  const stockVariantId = (_snapshot_variantId = snapshot.variantId) != null ? _snapshot_variantId : (_stocked_variants_ = stocked.variants[0]) == null ? void 0 : _stocked_variants_.id;
2412
2470
  const tracked = stocked.variants.some((variant)=>variant.id === stockVariantId && variant.inventory != null);
2413
2471
  if (stockVariantId && tracked) {
2414
- var _ref25;
2415
- const cycleQuantity = (_ref25 = cycleLine == null ? void 0 : cycleLine.quantity) != null ? _ref25 : 1;
2472
+ var _ref27;
2473
+ const cycleQuantity = (_ref27 = cycleLine == null ? void 0 : cycleLine.quantity) != null ? _ref27 : 1;
2416
2474
  // Atomic since AGL-2320, like every other decrement: a renewal
2417
2475
  // batch bills many subscribers at once, so two cycles of the same
2418
2476
  // physical box are exactly the concurrent pair that lost a
@@ -2437,20 +2495,20 @@ async function findOrderForDispute(paymentIntentId) {
2437
2495
  }
2438
2496
  }
2439
2497
  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;
2498
+ var _ref28, _ref29;
2499
+ const renewalEmail = (_ref28 = (_ref29 = object == null ? void 0 : object.customer_email) != null ? _ref29 : soldSnapshot.get('customerEmail')) != null ? _ref28 : null;
2442
2500
  // A physical cycle now lands in Orders (AGL-1750), but the console
2443
2501
  // still has no Subscriptions tab, so for a digital or service
2444
2502
  // subscription this notification remains the only place a merchant
2445
2503
  // learns the money arrived at all.
2446
- void notifyHostManagers(invoiceHostId, _extends({
2504
+ const renewalPrice = `$${(paidCents / 100).toFixed(2)}${interval ? `/${interval}` : ''}`;
2505
+ void notifyHostManagers(invoiceHostId, {
2447
2506
  type: 'content.order',
2448
- title: `Subscription renewed — $${(paidCents / 100).toFixed(2)}${interval ? `/${interval}` : ''}`
2449
- }, renewalEmail ? {
2450
- body: String(renewalEmail)
2451
- } : {}, {
2507
+ title: `Subscription renewed on {site} — ${renewalPrice}`,
2508
+ // Whose subscription, to what and where (AGL-3432).
2509
+ body: `${renewalEmail ? `${String(renewalEmail)}'s` : 'A'} ` + `subscription to ${snapshot.name || 'a product'} on {site} ` + `renewed for ${renewalPrice}.`,
2452
2510
  link: `/${invoiceHostId}/products`
2453
- }));
2511
+ });
2454
2512
  // RFM (AGL-328): a subscriber in month 12 has paid twelve times, and
2455
2513
  // counting only the first charge ranks them as a one-purchase
2456
2514
  // customer forever. Keyed to the invoice so the guard above is what
@@ -2477,7 +2535,7 @@ async function findOrderForDispute(paymentIntentId) {
2477
2535
  var _object_metadata17;
2478
2536
  const { hostId, reservationId } = (_object_metadata17 = object.metadata) != null ? _object_metadata17 : {};
2479
2537
  if (hostId && reservationId) {
2480
- var _ref28, _ref29;
2538
+ var _ref30, _ref31;
2481
2539
  var _object_metadata18;
2482
2540
  const firestore = firebaseAdmin.app().firestore();
2483
2541
  const reservationRef = firestore.collection('hosts').doc(String(hostId)).collection('reservations').doc(String(reservationId));
@@ -2490,7 +2548,7 @@ async function findOrderForDispute(paymentIntentId) {
2490
2548
  // `amount_total` is stay-plus-tax and Stripe's own tax fields read
2491
2549
  // zero; the session's metadata is the only witness, exactly as it is
2492
2550
  // 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)));
2551
+ const taxCents = Math.max(0, Math.round(Number((_ref30 = object == null ? void 0 : (_object_metadata18 = object.metadata) == null ? void 0 : _object_metadata18.taxCents) != null ? _ref30 : 0)));
2494
2552
  // What the guest actually handed over, MINUS that tax. This is the
2495
2553
  // DEPOSIT when the resource has one (`reserve.ts` charges
2496
2554
  // `depositCents || totalCents`), never the stay's `totalCents` — see
@@ -2504,7 +2562,7 @@ async function findOrderForDispute(paymentIntentId) {
2504
2562
  // are and under-state what the merchant collects at the register. The
2505
2563
  // same figure feeds the guest's lifetime value, which tax is no part
2506
2564
  // 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);
2565
+ const paidCents = Math.max(0, Math.round(Number((_ref31 = object == null ? void 0 : object.amount_total) != null ? _ref31 : 0)) - taxCents);
2508
2566
  // Redelivery guard (AGL-1755), the AGL-1748 shape. The `pending` ->
2509
2567
  // `confirmed` transition was always the guard, but it was a
2510
2568
  // read-then-write with every side effect hanging off it, so two
@@ -2575,17 +2633,44 @@ async function findOrderForDispute(paymentIntentId) {
2575
2633
  return true;
2576
2634
  });
2577
2635
  if (confirmedNow) {
2578
- var _ref30, _ref31;
2636
+ var _reservation_nights, _reservation_totalCents, _ref32, _ref33, _ref34, _ref35, _ref36;
2579
2637
  var _object_customer_details3, _object_customer_details4, _object_customer_details5;
2580
2638
  const reservation = reserved != null ? reserved : {};
2581
- void notifyHostManagers(String(hostId), _extends({
2639
+ // A day key, so its UTC date IS the check-in date.
2640
+ const checkInShort = new Date(Number(reservation['checkInDayMs'])).toUTCString().slice(0, 16);
2641
+ const nights = Number((_reservation_nights = reservation['nights']) != null ? _reservation_nights : 0);
2642
+ const nightsText = `${nights} night${nights === 1 ? '' : 's'}`;
2643
+ /*
2644
+ * WHAT THE GUEST WAS CHARGED TODAY, tax included (AGL-3442).
2645
+ *
2646
+ * `paidCents` is the money applied to the STAY and leaves out the
2647
+ * merchant's lodging tax, so a guest told only that figure is told
2648
+ * less than their card was charged. The two together are Stripe's
2649
+ * session total, and the tax is the figure `reserve.ts` charged as
2650
+ * its own line, named so the guest can match it to their receipt.
2651
+ * The balance below is the stay's alone: the tax is charged on what
2652
+ * is collected at booking, and nothing computes one on what is
2653
+ * collected at the property.
2654
+ */ const charged = `$${((paidCents + taxCents) / 100).toFixed(2)}`;
2655
+ const taxIncluded = taxCents > 0 ? `, including $${(taxCents / 100).toFixed(2)} lodging tax` : '';
2656
+ const paid = `${charged}${taxIncluded}`;
2657
+ // What the guest still owes for the stay: the console's
2658
+ // reservations card collects `totalCents - paidCents` at the
2659
+ // property, so the guest is told it too (AGL-3432).
2660
+ const balanceCents = Math.max(0, Math.round(Number((_reservation_totalCents = reservation['totalCents']) != null ? _reservation_totalCents : 0)) - paidCents);
2661
+ const balance = `$${(balanceCents / 100).toFixed(2)}`;
2662
+ const reservedBy = (_ref32 = (_ref33 = object == null ? void 0 : (_object_customer_details3 = object.customer_details) == null ? void 0 : _object_customer_details3.email) != null ? _ref33 : reservation['guestEmail']) != null ? _ref32 : 'A guest';
2663
+ // Which room or unit was reserved (AGL-3432): on a property with
2664
+ // several, "a stay" does not say which one to get ready.
2665
+ const resourceName = reservation['resourceId'] ? String((_ref34 = await firestore.collection('hosts').doc(String(hostId)).collection('resources').doc(String(reservation['resourceId'])).get().then((snapshot)=>snapshot.get('name')).catch(()=>'')) != null ? _ref34 : '').trim() : '';
2666
+ void notifyHostManagers(String(hostId), {
2582
2667
  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
- } : {}, {
2668
+ title: 'New reservation on {site}',
2669
+ // Who, what, when, what was paid and what is still owed
2670
+ // (AGL-3432).
2671
+ body: `${reservedBy} reserved ${resourceName || 'a stay'} on {site}: check-in ` + `${checkInShort}, ${nightsText}. ${charged} was paid${taxIncluded}` + (balanceCents > 0 ? `; ${balance} is still to collect at the property.` : '.'),
2587
2672
  link: `/${hostId}/products`
2588
- }));
2673
+ });
2589
2674
  // Contacts ingestion (AGL-1755): this branch stored `paidCents` from
2590
2675
  // `amount_total` and then called `upsertHostContact` with no amount,
2591
2676
  // so a guest who paid for a stay showed a lifetime value of zero. A
@@ -2615,14 +2700,14 @@ async function findOrderForDispute(paymentIntentId) {
2615
2700
  // exactly once. Taking `totalCents` here is what WOULD double-count:
2616
2701
  // it would claim money the guest has not paid yet and will pay again
2617
2702
  // 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;
2703
+ const guestContactEmail = (_ref35 = (_ref36 = object == null ? void 0 : (_object_customer_details4 = object.customer_details) == null ? void 0 : _object_customer_details4.email) != null ? _ref36 : reservation['guestEmail']) != null ? _ref35 : null;
2619
2704
  if (guestContactEmail) {
2620
- var _ref32, _ref33;
2705
+ var _ref37, _ref38;
2621
2706
  var _object_customer_details6;
2622
2707
  await captureHostContact(_extends({
2623
2708
  hostId: String(hostId),
2624
2709
  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,
2710
+ name: (_ref37 = (_ref38 = object == null ? void 0 : (_object_customer_details6 = object.customer_details) == null ? void 0 : _object_customer_details6.name) != null ? _ref38 : reservation['guestName']) != null ? _ref37 : undefined,
2626
2711
  source: 'booking',
2627
2712
  // A paid stay is a sale (AGL-2612), whatever the source says
2628
2713
  // about the kind of thing that was bought.
@@ -2638,21 +2723,22 @@ async function findOrderForDispute(paymentIntentId) {
2638
2723
  }
2639
2724
  const guestEmail = object == null ? void 0 : (_object_customer_details5 = object.customer_details) == null ? void 0 : _object_customer_details5.email;
2640
2725
  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}`;
2726
+ var _reservation_nights1, _ref39;
2727
+ // The balance as a whole line, empty when the stay is paid in
2728
+ // full so the built-in copy leaves it out.
2729
+ const balanceNote = balanceCents > 0 ? `Still to pay: ${balance}, at the property. It has not been charged.` : '';
2730
+ const fallbackText = `Your stay is confirmed!\n\nCheck-in: ${checkInShort}\n` + `Nights: ${reservation['nights']}\n` + `Paid today: ${paid}\n` + (balanceNote ? `${balanceNote}\n` : '') + `Reference: ${reservationId}`;
2646
2731
  // Site-owner-designed template when published (AGL-771).
2647
2732
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'reservation-confirmed', {
2648
2733
  'reservation.checkIn': checkInShort,
2649
- 'reservation.nights': String((_reservation_nights = reservation['nights']) != null ? _reservation_nights : ''),
2734
+ 'reservation.nights': String((_reservation_nights1 = reservation['nights']) != null ? _reservation_nights1 : ''),
2650
2735
  'reservation.paid': paid,
2736
+ 'reservation.balance': balanceNote,
2651
2737
  'reservation.ref': String(reservationId)
2652
2738
  });
2653
2739
  await sendEmail(_extends({
2654
2740
  to: guestEmail,
2655
- subject: (_ref34 = designed == null ? void 0 : designed.subject) != null ? _ref34 : 'Reservation confirmed',
2741
+ subject: (_ref39 = designed == null ? void 0 : designed.subject) != null ? _ref39 : 'Reservation confirmed',
2656
2742
  text: (designed == null ? void 0 : designed.text) || fallbackText
2657
2743
  }, (designed == null ? void 0 : designed.html) ? {
2658
2744
  html: designed.html
@@ -2678,7 +2764,7 @@ async function findOrderForDispute(paymentIntentId) {
2678
2764
  var _object_metadata19;
2679
2765
  const { hostId, cartId, feeCents, couponCode } = (_object_metadata19 = object.metadata) != null ? _object_metadata19 : {};
2680
2766
  if (hostId && cartId) {
2681
- var _cartSnapshot_data, _ref35, _ref36, _ref37, _ref38, _ref39, _ref40;
2767
+ var _cartSnapshot_data, _ref40, _ref41, _ref42, _ref43, _ref44, _ref45;
2682
2768
  var _object_customer_details7, _object_metadata20, _object_customer_details8, _object_customer_details9, _object_customer_details10, _object_metadata21, _this2, _object_metadata22;
2683
2769
  const firestore = firebaseAdmin.app().firestore();
2684
2770
  const hostRef = firestore.collection('hosts').doc(String(hostId));
@@ -2744,7 +2830,11 @@ async function findOrderForDispute(paymentIntentId) {
2744
2830
  unitAmountCents: Math.round(Number((_ref = variant == null ? void 0 : variant.priceUsd) != null ? _ref : 0) * 100)
2745
2831
  });
2746
2832
  }).filter(Boolean);
2747
- const shipping = (_ref35 = object == null ? void 0 : object.shipping_details) != null ? _ref35 : object == null ? void 0 : object.customer_details;
2833
+ const shipping = (_ref40 = object == null ? void 0 : object.shipping_details) != null ? _ref40 : object == null ? void 0 : object.customer_details;
2834
+ // The number the merchant knows this order by, carried out of the
2835
+ // transaction for the notices below (AGL-3432). The document id is
2836
+ // the Stripe session id, which appears nowhere in the console.
2837
+ let cartOrderNumber;
2748
2838
  const created = await firestore.runTransaction(async (transaction)=>{
2749
2839
  var _counter_get, _ref, _ref1, _ref2, _ref3, _ref4, _shipping_address_line1, _shipping_address_line2, _shipping_address_city, _shipping_address_state, _shipping_address_postal_code, _shipping_address_country, _ref5;
2750
2840
  var _object_metadata, _object_customer_details, _object_customer_details1;
@@ -2754,6 +2844,7 @@ async function findOrderForDispute(paymentIntentId) {
2754
2844
  ]);
2755
2845
  if (existing.exists) return false;
2756
2846
  const number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
2847
+ cartOrderNumber = number;
2757
2848
  transaction.set(counterRef, {
2758
2849
  next: number + 1
2759
2850
  }, {
@@ -2828,6 +2919,9 @@ async function findOrderForDispute(paymentIntentId) {
2828
2919
  // the non-idempotent effects below (inventory / coupon / gift-card
2829
2920
  // decrements) that would otherwise double-apply.
2830
2921
  if (!created) return;
2922
+ const cartOrderLabel = CommerceModel.formatOrderNumber({
2923
+ number: cartOrderNumber
2924
+ }, String(object.id));
2831
2925
  // AGL-2149: loud, not silent. Logged for the platform and pushed to the
2832
2926
  // merchant, once, behind the same `created` guard as every other
2833
2927
  // non-idempotent effect so a redelivery does not re-nag.
@@ -2840,7 +2934,7 @@ async function findOrderForDispute(paymentIntentId) {
2840
2934
  void notifyHostManagers(String(hostId), {
2841
2935
  type: 'content.order',
2842
2936
  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.',
2937
+ body: `Order ${cartOrderLabel} on {site} was charged in full, but ` + `${unresolvedLines.length} of its lines could not be recorded ` + 'because the product was deleted during ' + 'checkout. Refund the difference or fulfill it by hand.',
2844
2938
  link: `/${hostId}/products`
2845
2939
  });
2846
2940
  }
@@ -2882,10 +2976,10 @@ async function findOrderForDispute(paymentIntentId) {
2882
2976
  // matters, and the order carries the buyer identity anyway.
2883
2977
  const licenseKeysByProduct = {};
2884
2978
  for (const line of lineItems){
2885
- var _ref41;
2979
+ var _ref46;
2886
2980
  var _object_customer_details11;
2887
2981
  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);
2982
+ const keys = await assignLicenseKeys(firestore, hostRef, String(hostId), line.productId, String(object.id), (_ref46 = object == null ? void 0 : (_object_customer_details11 = object.customer_details) == null ? void 0 : _object_customer_details11.email) != null ? _ref46 : null, line.quantity);
2889
2983
  if (keys.length) licenseKeysByProduct[line.productId] = keys;
2890
2984
  }
2891
2985
  if (Object.keys(licenseKeysByProduct).length) {
@@ -2898,9 +2992,9 @@ async function findOrderForDispute(paymentIntentId) {
2898
2992
  // Branded receipt (AGL-296): env-gated like every outbound email.
2899
2993
  const buyerEmailForReceipt = object == null ? void 0 : (_object_customer_details7 = object.customer_details) == null ? void 0 : _object_customer_details7.email;
2900
2994
  if (isEmailConfigured() && buyerEmailForReceipt) {
2901
- var _ref42, _ref43, _ref44, _ref45;
2995
+ var _ref47, _ref48, _ref49, _ref50;
2902
2996
  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 : '');
2997
+ const receiptFooter = String((_ref47 = receiptSettings == null ? void 0 : receiptSettings.get('receiptFooter')) != null ? _ref47 : '');
2904
2998
  const linesText = lineItems.map((line)=>`${line.quantity}× ${line.name}${line.variantLabel ? ` (${line.variantLabel})` : ''} — $${(line.unitAmountCents * line.quantity / 100).toFixed(2)}`).join('\n');
2905
2999
  // The keys assigned above the gate (AGL-2149); the receipt only
2906
3000
  // reports them.
@@ -2914,9 +3008,9 @@ async function findOrderForDispute(paymentIntentId) {
2914
3008
  // Digital delivery links (AGL-302); reuse the canonical mint the
2915
3009
  // tenant download endpoint verifies so the secret can never drift.
2916
3010
  const downloadToken = mintDownloadToken(hostId, String(object.id));
2917
- const siteOrigin = String((_ref43 = object == null ? void 0 : object.success_url) != null ? _ref43 : '').replace(/\/\?.*$|\?.*$/, '');
3011
+ const siteOrigin = String((_ref48 = object == null ? void 0 : object.success_url) != null ? _ref48 : '').replace(/\/\?.*$|\?.*$/, '');
2918
3012
  const downloadLines = lineItems.filter((line)=>line.productType === 'digital').map((line)=>`Download ${line.name}: ${siteOrigin}/api/commerce/download` + `?hostId=${hostId}&orderId=${object.id}` + `&productId=${line.productId}&token=${downloadToken}`).join('\n');
2919
- const orderTotal = `$${(Number((_ref44 = object == null ? void 0 : object.amount_total) != null ? _ref44 : 0) / 100).toFixed(2)}`;
3013
+ const orderTotal = `$${(Number((_ref49 = object == null ? void 0 : object.amount_total) != null ? _ref49 : 0) / 100).toFixed(2)}`;
2920
3014
  const orderSummary = [
2921
3015
  linesText,
2922
3016
  licenseText,
@@ -2928,11 +3022,15 @@ async function findOrderForDispute(paymentIntentId) {
2928
3022
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'order-receipt', {
2929
3023
  'order.summary': orderSummary,
2930
3024
  'order.total': orderTotal,
2931
- 'order.ref': String(object.id)
3025
+ 'order.ref': String(object.id),
3026
+ // The store's own Receipt footer (AGL-3432), as a token: the
3027
+ // built-in copy always renders (AGL-3370), so a footer that is
3028
+ // only in the fallback text above never reaches a buyer.
3029
+ 'store.receiptFooter': receiptFooter
2932
3030
  });
2933
3031
  await sendEmail(_extends({
2934
3032
  to: buyerEmailForReceipt,
2935
- subject: (_ref45 = designed == null ? void 0 : designed.subject) != null ? _ref45 : `Receipt for your order`,
3033
+ subject: (_ref50 = designed == null ? void 0 : designed.subject) != null ? _ref50 : `Receipt for your order`,
2936
3034
  text: (designed == null ? void 0 : designed.text) || fallbackText
2937
3035
  }, (designed == null ? void 0 : designed.html) ? {
2938
3036
  html: designed.html
@@ -3009,37 +3107,38 @@ async function findOrderForDispute(paymentIntentId) {
3009
3107
  // is byte-identical to what it was before this issue for exactly that
3010
3108
  // reason. The hold refuses the SECOND shopper; it never gates the
3011
3109
  // 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({
3110
+ await releaseStockHold(hostRef, String((_ref41 = (_object_metadata20 = object.metadata) == null ? void 0 : _object_metadata20.stockHoldKey) != null ? _ref41 : ''));
3111
+ const cartOrderTotal = `$${(Number((_ref42 = object == null ? void 0 : object.amount_total) != null ? _ref42 : 0) / 100).toFixed(2)}`;
3112
+ const cartUnits = lineItems.reduce((sum, line)=>sum + line.quantity, 0);
3113
+ void notifyHostManagers(String(hostId), {
3014
3114
  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
- } : {}, {
3115
+ title: `New order on {site} — ${cartOrderTotal}`,
3116
+ // What came in, from whom and where (AGL-3432).
3117
+ body: `Order ${cartOrderLabel} came in on {site}: ${cartUnits} ` + `item${cartUnits === 1 ? '' : 's'}, ${cartOrderTotal}` + ((object == null ? void 0 : (_object_customer_details8 = object.customer_details) == null ? void 0 : _object_customer_details8.email) ? `, from ${object.customer_details.email}.` : '.'),
3019
3118
  link: `/${hostId}/products`
3020
- }));
3119
+ });
3021
3120
  await captureHostContact(_extends({
3022
3121
  hostId: String(hostId),
3023
3122
  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,
3123
+ name: (_ref43 = object == null ? void 0 : (_object_customer_details10 = object.customer_details) == null ? void 0 : _object_customer_details10.name) != null ? _ref43 : undefined,
3025
3124
  source: 'order',
3026
3125
  initialLifecycleStage: 'customer'
3027
3126
  }, marketingOptIn ? {
3028
3127
  marketingConsent: true
3029
3128
  } : {}, {
3030
- purchaseCents: Number((_ref39 = object == null ? void 0 : object.amount_total) != null ? _ref39 : 0),
3129
+ purchaseCents: Number((_ref44 = object == null ? void 0 : object.amount_total) != null ? _ref44 : 0),
3031
3130
  interaction: {
3032
3131
  refId: String(object.id),
3033
- summary: `Placed an order ($${(Number((_ref40 = object == null ? void 0 : object.amount_total) != null ? _ref40 : 0) / 100).toFixed(2)})`
3132
+ summary: `Placed an order ($${(Number((_ref45 = object == null ? void 0 : object.amount_total) != null ? _ref45 : 0) / 100).toFixed(2)})`
3034
3133
  }
3035
3134
  }));
3036
3135
  if (couponCode) {
3037
- var _ref46;
3136
+ var _ref51;
3038
3137
  var _object_metadata23;
3039
3138
  await settleRedemption({
3040
3139
  firestore,
3041
3140
  ref: hostRef.collection('coupons').doc(String(couponCode)),
3042
- holdKey: String((_ref46 = (_object_metadata23 = object.metadata) == null ? void 0 : _object_metadata23.couponHoldKey) != null ? _ref46 : ''),
3141
+ holdKey: String((_ref51 = (_object_metadata23 = object.metadata) == null ? void 0 : _object_metadata23.couponHoldKey) != null ? _ref51 : ''),
3043
3142
  orderRef,
3044
3143
  label: `coupon ${couponCode}`,
3045
3144
  detail: `Coupon ${couponCode} was applied to this order but no longer ` + 'exists, so the redemption is uncounted against its limit.'
@@ -3047,10 +3146,10 @@ async function findOrderForDispute(paymentIntentId) {
3047
3146
  }
3048
3147
  // Gift card balance settlement (AGL-322, made a settlement by AGL-2449).
3049
3148
  if ((_object_metadata21 = object.metadata) == null ? void 0 : _object_metadata21.giftCardCode) {
3050
- var _ref47, _ref48;
3149
+ var _ref52, _ref53;
3051
3150
  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 : '');
3151
+ const giftCardCents = Number((_ref52 = (_object_metadata24 = object.metadata) == null ? void 0 : _object_metadata24.giftCardCents) != null ? _ref52 : 0);
3152
+ const holdKey = String((_ref53 = (_object_metadata25 = object.metadata) == null ? void 0 : _object_metadata25.giftCardHoldKey) != null ? _ref53 : '');
3054
3153
  const cardRef = hostRef.collection('giftCards').doc(String(object.metadata.giftCardCode));
3055
3154
  const orphanNote = `$${(giftCardCents / 100).toFixed(2)} was applied from gift card ` + `${object.metadata.giftCardCode}, which no longer exists. The ` + 'balance was not deducted from any card.';
3056
3155
  if (!holdKey) {
@@ -3130,7 +3229,7 @@ async function findOrderForDispute(paymentIntentId) {
3130
3229
  const lineProduct = productsById.get(line.productId);
3131
3230
  if (!(lineProduct == null ? void 0 : lineProduct.giftCard)) continue;
3132
3231
  for(let unit = 0; unit < line.quantity; unit += 1){
3133
- var _ref49, _ref50;
3232
+ var _ref54, _ref55;
3134
3233
  var _object_customer_details12, _object_customer_details13, _object_customer_details14;
3135
3234
  const code = `GC-${createHmac('sha256', String(object.id)).update(`${line.productId}:${unit}:${Date.now()}`).digest('hex').slice(0, 12).toUpperCase()}`;
3136
3235
  // THE EMAIL IS GATED ON THE WRITE (AGL-2161). This `.set()` used
@@ -3149,13 +3248,13 @@ async function findOrderForDispute(paymentIntentId) {
3149
3248
  const issued = await hostRef.collection('giftCards').doc(code).set({
3150
3249
  initialCents: line.unitAmountCents,
3151
3250
  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,
3251
+ recipientEmail: (_ref54 = object == null ? void 0 : (_object_customer_details12 = object.customer_details) == null ? void 0 : _object_customer_details12.email) != null ? _ref54 : null,
3153
3252
  orderId: String(object.id),
3154
3253
  // Which line bought it, so a refund that withdraws that line
3155
3254
  // voids this card and no other (AGL-3363).
3156
3255
  productId: line.productId,
3157
3256
  // 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),
3257
+ searchTokens: giftCardSearchTokens(code, (_ref55 = object == null ? void 0 : (_object_customer_details13 = object.customer_details) == null ? void 0 : _object_customer_details13.email) != null ? _ref55 : null),
3159
3258
  createdAtMs: Date.now()
3160
3259
  }).then(()=>true).catch((error)=>{
3161
3260
  console.error('Gift card issuance failed', hostId, code, error);
@@ -3176,24 +3275,27 @@ async function findOrderForDispute(paymentIntentId) {
3176
3275
  void notifyHostManagers(String(hostId), {
3177
3276
  type: 'content.order',
3178
3277
  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.',
3278
+ body: `A $${(line.unitAmountCents / 100).toFixed(2)} gift card ` + `bought on {site} (order ${cartOrderLabel}) could not be ` + 'written, so no code was emailed to the buyer. Issue one by ' + 'hand from Gift cards.',
3180
3279
  link: `/${hostId}/products`
3181
3280
  });
3182
3281
  continue;
3183
3282
  }
3184
3283
  const giftTo = object == null ? void 0 : (_object_customer_details14 = object.customer_details) == null ? void 0 : _object_customer_details14.email;
3185
3284
  if (giftTo) {
3186
- var _ref51;
3285
+ var _ref56;
3187
3286
  const giftValue = `$${(line.unitAmountCents / 100).toFixed(2)}`;
3188
3287
  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).
3288
+ // Site-owner-designed template when published (AGL-771). A
3289
+ // bought card carries no note; empty, so the built-in copy
3290
+ // leaves that line out.
3190
3291
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'gift-card', {
3191
3292
  'giftcard.code': code,
3192
- 'giftcard.value': giftValue
3293
+ 'giftcard.value': giftValue,
3294
+ 'giftcard.note': ''
3193
3295
  });
3194
3296
  await sendEmail(_extends({
3195
3297
  to: giftTo,
3196
- subject: (_ref51 = designed == null ? void 0 : designed.subject) != null ? _ref51 : 'Your gift card',
3298
+ subject: (_ref56 = designed == null ? void 0 : designed.subject) != null ? _ref56 : 'Your gift card',
3197
3299
  text: (designed == null ? void 0 : designed.text) || fallbackText
3198
3300
  }, (designed == null ? void 0 : designed.html) ? {
3199
3301
  html: designed.html
@@ -3214,12 +3316,12 @@ async function findOrderForDispute(paymentIntentId) {
3214
3316
  }
3215
3317
  // Discounts engine redemptions (AGL-305).
3216
3318
  if ((_object_metadata22 = object.metadata) == null ? void 0 : _object_metadata22.discountId) {
3217
- var _ref52;
3319
+ var _ref57;
3218
3320
  var _object_metadata26;
3219
3321
  await settleRedemption({
3220
3322
  firestore,
3221
3323
  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 : ''),
3324
+ holdKey: String((_ref57 = (_object_metadata26 = object.metadata) == null ? void 0 : _object_metadata26.discountHoldKey) != null ? _ref57 : ''),
3223
3325
  orderRef,
3224
3326
  label: `discount ${object.metadata.discountId}`,
3225
3327
  detail: `Discount ${object.metadata.discountId} was applied to this ` + 'order but no longer exists, so the redemption is uncounted ' + 'against its limit.'
@@ -3233,7 +3335,7 @@ async function findOrderForDispute(paymentIntentId) {
3233
3335
  var _object_metadata27;
3234
3336
  const { hostId, orderId, productId } = (_object_metadata27 = object.metadata) != null ? _object_metadata27 : {};
3235
3337
  if (hostId && orderId) {
3236
- var _ref53, _ref54, _ref55;
3338
+ var _ref58, _ref59, _ref60;
3237
3339
  var _object_total_details, _object_total_details1;
3238
3340
  const firestore = firebaseAdmin.app().firestore();
3239
3341
  const hostRef = firestore.collection('hosts').doc(String(hostId));
@@ -3272,7 +3374,7 @@ async function findOrderForDispute(paymentIntentId) {
3272
3374
  //
3273
3375
  // The whole map is written rather than one field of it, so a
3274
3376
  // `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));
3377
+ const shippingCents = Math.max(0, Math.round(Number((_ref58 = object == null ? void 0 : (_object_total_details = object.total_details) == null ? void 0 : _object_total_details.amount_shipping) != null ? _ref58 : 0) || 0));
3276
3378
  // Tax joins shipping in the fold (AGL-1953). `draft-order.ts` now
3277
3379
  // declares tax — a Stripe Tax Rate for a `manual` store, whose figure
3278
3380
  // it already froze into `totals`, or `automatic_tax` for a `stripe`
@@ -3285,8 +3387,8 @@ async function findOrderForDispute(paymentIntentId) {
3285
3387
  // `amount_tax` 0 here (its own figure stays on the order, and its
3286
3388
  // AGL-1953 witness is metadata, not this field), and a draft composed
3287
3389
  // 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);
3390
+ const stripeTaxCents = Math.max(0, Math.round(Number((_ref59 = object == null ? void 0 : (_object_total_details1 = object.total_details) == null ? void 0 : _object_total_details1.amount_tax) != null ? _ref59 : 0) || 0));
3391
+ const chargedTotalCents = Number((_ref60 = object == null ? void 0 : object.amount_total) != null ? _ref60 : NaN);
3290
3392
  // `shipping_details` ONLY, unlike the cart branch's
3291
3393
  // `?? customer_details` fallback: that address is the BILLING one, and
3292
3394
  // a card sale rung up at a register completes here too — it must not
@@ -3298,6 +3400,8 @@ async function findOrderForDispute(paymentIntentId) {
3298
3400
  // transaction below. Carried out so the money can be reported after
3299
3401
  // the transaction commits.
3300
3402
  let paidAfterCancel = false;
3403
+ // The canceled order's number, for the notice below (AGL-3432).
3404
+ let canceledOrderLabel = '';
3301
3405
  const flipped = await firestore.runTransaction(async (transaction)=>{
3302
3406
  var _snapshot_data, _snapshot_data1, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _lifted_totals, _ref6, _shipTo_name, _shipTo_address_line1, _shipTo_address_line2, _shipTo_address_city, _shipTo_address_state, _shipTo_address_postal_code, _shipTo_address_country;
3303
3407
  var _object_customer_details, _object_metadata, _object_customer_details1, _lifted_totals1;
@@ -3323,6 +3427,7 @@ async function findOrderForDispute(paymentIntentId) {
3323
3427
  const alreadyNoted = ((_lifted_timeline = lifted.timeline) != null ? _lifted_timeline : []).some((entry)=>(entry == null ? void 0 : entry.event) === 'paid-after-cancel');
3324
3428
  if (lifted.status === 'cancelled' && !alreadyNoted) {
3325
3429
  paidAfterCancel = true;
3430
+ canceledOrderLabel = CommerceModel.formatOrderNumber(lifted, String(orderId));
3326
3431
  // `set(..., { merge: true })`, the same call the paying branch
3327
3432
  // below makes on this document. Nothing here is a nested map, so
3328
3433
  // merge and replace agree, and the read above has just proven
@@ -3395,13 +3500,13 @@ async function findOrderForDispute(paymentIntentId) {
3395
3500
  await notifyHostManagers(String(hostId), {
3396
3501
  type: 'content.order',
3397
3502
  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.',
3503
+ body: `Order ${canceledOrderLabel || orderId} on {site} was canceled, ` + 'but its payment link was still live and has been paid. The ' + 'money is in Stripe and no sale was recorded — refund it in ' + 'Stripe or reconcile it by hand.',
3399
3504
  link: `/${hostId}/orders`
3400
3505
  });
3401
3506
  }
3402
3507
  if (flipped) {
3403
- var _ref56, _ref57;
3404
- var _object_metadata28, _object_customer_details15;
3508
+ var _ref61, _ref62, _ref63, _ref64, _ref65, _ref66;
3509
+ var _object_metadata28, _object_customer_details15, _order_totals, _object_customer_details16;
3405
3510
  const order = paidOrder;
3406
3511
  // Discounts engine redemptions (AGL-305), for this branch's BOTH
3407
3512
  // tenants: a console draft order and a POS card sale, which carry the
@@ -3416,20 +3521,25 @@ async function findOrderForDispute(paymentIntentId) {
3416
3521
  // the hold existed, which Stripe's at-least-once delivery would run
3417
3522
  // twice. The transition is what makes it once.
3418
3523
  if ((_object_metadata28 = object.metadata) == null ? void 0 : _object_metadata28.discountId) {
3419
- var _ref58;
3524
+ var _ref67;
3420
3525
  var _object_metadata29;
3421
3526
  await settleRedemption({
3422
3527
  firestore,
3423
3528
  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 : ''),
3529
+ holdKey: String((_ref67 = (_object_metadata29 = object.metadata) == null ? void 0 : _object_metadata29.discountHoldKey) != null ? _ref67 : ''),
3425
3530
  orderRef,
3426
3531
  label: `discount ${object.metadata.discountId}`,
3427
3532
  detail: `Discount ${object.metadata.discountId} was applied to this ` + 'order but no longer exists, so the redemption is uncounted ' + 'against its limit.'
3428
3533
  });
3429
3534
  }
3535
+ const draftLabel = CommerceModel.formatOrderNumber(order, String(orderId));
3536
+ const draftPayer = (_ref61 = (_ref62 = object == null ? void 0 : (_object_customer_details15 = object.customer_details) == null ? void 0 : _object_customer_details15.email) != null ? _ref62 : order.customerEmail) != null ? _ref61 : null;
3537
+ const draftPaidCents = Number((_ref63 = object == null ? void 0 : object.amount_total) != null ? _ref63 : 0) || Number((_ref64 = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref64 : 0);
3430
3538
  void notifyHostManagers(String(hostId), {
3431
3539
  type: 'content.order',
3432
- title: `Draft order paid — ${CommerceModel.formatOrderNumber(order, String(orderId))}`,
3540
+ title: `Draft order paid — ${draftLabel}`,
3541
+ // Who paid what, and where (AGL-3432).
3542
+ body: `${draftPayer != null ? draftPayer : 'The buyer'} paid draft order ${draftLabel} ` + `on {site}: $${(draftPaidCents / 100).toFixed(2)}.`,
3433
3543
  link: `/${hostId}/products`
3434
3544
  });
3435
3545
  // Contacts ingestion (AGL-1748): this branch flipped the order,
@@ -3444,15 +3554,15 @@ async function findOrderForDispute(paymentIntentId) {
3444
3554
  // figure the draft was priced at, and the two agree by construction,
3445
3555
  // but only one of them is the money that moved. Stored totals remain
3446
3556
  // 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;
3557
+ const draftEmail = (_ref65 = (_ref66 = object == null ? void 0 : (_object_customer_details16 = object.customer_details) == null ? void 0 : _object_customer_details16.email) != null ? _ref66 : order.customerEmail) != null ? _ref65 : null;
3448
3558
  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);
3559
+ var _ref68, _ref69, _ref70;
3560
+ var _order_totals1, _object_customer_details17;
3561
+ const chargedCents = Number((_ref68 = object == null ? void 0 : object.amount_total) != null ? _ref68 : 0) || Number((_ref69 = (_order_totals1 = order.totals) == null ? void 0 : _order_totals1.totalCents) != null ? _ref69 : 0);
3452
3562
  await captureHostContact(_extends({
3453
3563
  hostId: String(hostId),
3454
3564
  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,
3565
+ name: (_ref70 = object == null ? void 0 : (_object_customer_details17 = object.customer_details) == null ? void 0 : _object_customer_details17.name) != null ? _ref70 : undefined,
3456
3566
  source: 'order',
3457
3567
  initialLifecycleStage: 'customer'
3458
3568
  }, chargedCents > 0 ? {
@@ -3465,15 +3575,15 @@ async function findOrderForDispute(paymentIntentId) {
3465
3575
  }));
3466
3576
  }
3467
3577
  if (productId) {
3468
- var _productSnapshot_data, _ref62, _ref63;
3578
+ var _productSnapshot_data, _ref71, _ref72;
3469
3579
  var _object_metadata30, _lifted_variants_, _order_lineItems_, _order_lineItems;
3470
3580
  const productRef = hostRef.collection('products').doc(String(productId));
3471
3581
  const productSnapshot = await productRef.get();
3472
3582
  const lifted = CommerceModel.liftLegacyProduct((_productSnapshot_data = productSnapshot.data()) != null ? _productSnapshot_data : {
3473
3583
  name: 'Product'
3474
3584
  });
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;
3585
+ const soldVariantId = String((_ref71 = (_object_metadata30 = object.metadata) == null ? void 0 : _object_metadata30.variantId) != null ? _ref71 : '') || ((_lifted_variants_ = lifted.variants[0]) == null ? void 0 : _lifted_variants_.id);
3586
+ const quantity = (_ref72 = (_order_lineItems = order.lineItems) == null ? void 0 : (_order_lineItems_ = _order_lineItems[0]) == null ? void 0 : _order_lineItems_.quantity) != null ? _ref72 : 1;
3477
3587
  if (soldVariantId) {
3478
3588
  // Atomic since AGL-2320.
3479
3589
  const moved = await decrementVariantStock({
@@ -3525,10 +3635,10 @@ async function findOrderForDispute(paymentIntentId) {
3525
3635
  // would decrement it twice.
3526
3636
  const paidProducts = new Map();
3527
3637
  for (const line of (_order_lineItems1 = order.lineItems) != null ? _order_lineItems1 : []){
3528
- var _ref64, _ref65, _line_variantId1;
3638
+ var _ref73, _ref74, _line_variantId1;
3529
3639
  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));
3640
+ const lineProductId = String((_ref73 = line == null ? void 0 : line.productId) != null ? _ref73 : '');
3641
+ const soldQty = Math.round(Number((_ref74 = line == null ? void 0 : line.quantity) != null ? _ref74 : 0));
3532
3642
  if (!lineProductId || !(soldQty > 0)) continue;
3533
3643
  // Firestore reserves `__…__` ids and `.doc()` throws
3534
3644
  // synchronously on one; a corrupt line must not fail the
@@ -3591,8 +3701,8 @@ async function findOrderForDispute(paymentIntentId) {
3591
3701
  var _object_metadata31;
3592
3702
  const { hostId, productId, feeCents, couponCode } = (_object_metadata31 = object.metadata) != null ? _object_metadata31 : {};
3593
3703
  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;
3704
+ var _ref75, _productForSnapshot_get2, _productForSnapshot_data1, _ref76, _ref77, _ref78, _ref79, _ref80, _ref81, _ref82;
3705
+ var _object_metadata32, _buyNowLineItems_, _object_customer_details18, _object_customer_details19, _object_customer_details20, _object_metadata33;
3596
3706
  const firestore = firebaseAdmin.app().firestore();
3597
3707
  const hostRef = firestore.collection('hosts').doc(String(hostId));
3598
3708
  // Orders v1 (AGL-283): line-item snapshot + totals + timeline with
@@ -3602,9 +3712,9 @@ async function findOrderForDispute(paymentIntentId) {
3602
3712
  // is bounded to Stripe's at-least-once edge).
3603
3713
  const orderRef = hostRef.collection('orders').doc(String(object.id));
3604
3714
  const counterRef = hostRef.collection('counters').doc('orders');
3605
- const amountCents = Number((_ref66 = object == null ? void 0 : object.amount_total) != null ? _ref66 : 0);
3715
+ const amountCents = Number((_ref75 = object == null ? void 0 : object.amount_total) != null ? _ref75 : 0);
3606
3716
  const productForSnapshot = await hostRef.collection('products').doc(String(productId)).get();
3607
- const snapshotName = String((_productForSnapshot_get1 = productForSnapshot.get('name')) != null ? _productForSnapshot_get1 : 'Product');
3717
+ const snapshotName = String((_productForSnapshot_get2 = productForSnapshot.get('name')) != null ? _productForSnapshot_get2 : 'Product');
3608
3718
  // AGL-1711: the line item and totals used to be fabricated from
3609
3719
  // `amount_total` alone — one unit, priced at the whole charge, with tax
3610
3720
  // and discount recorded as 0. `computeBuyNowOrder` rebuilds the real
@@ -3615,7 +3725,7 @@ async function findOrderForDispute(paymentIntentId) {
3615
3725
  name: snapshotName
3616
3726
  });
3617
3727
  const soldVariant = ((_object_metadata32 = object.metadata) == null ? void 0 : _object_metadata32.variantId) ? liftedForSnapshot.variants.find((item)=>item.id === String(object.metadata.variantId)) : liftedForSnapshot.variants[0];
3618
- const variantOptions = Object.values((_ref67 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref67 : {});
3728
+ const variantOptions = Object.values((_ref76 = soldVariant == null ? void 0 : soldVariant.options) != null ? _ref76 : {});
3619
3729
  const { lineItems: buyNowLineItems, totals: buyNowTotals } = CommerceModel.computeBuyNowOrder(object, _extends({
3620
3730
  name: snapshotName
3621
3731
  }, variantOptions.length ? {
@@ -3627,7 +3737,10 @@ async function findOrderForDispute(paymentIntentId) {
3627
3737
  } : {}, liftedForSnapshot.supplierId ? {
3628
3738
  supplierId: liftedForSnapshot.supplierId
3629
3739
  } : {}));
3630
- const soldQuantity = (_ref68 = (_buyNowLineItems_ = buyNowLineItems[0]) == null ? void 0 : _buyNowLineItems_.quantity) != null ? _ref68 : 1;
3740
+ const soldQuantity = (_ref77 = (_buyNowLineItems_ = buyNowLineItems[0]) == null ? void 0 : _buyNowLineItems_.quantity) != null ? _ref77 : 1;
3741
+ // The number the merchant knows this order by, for the notices below
3742
+ // (AGL-3432).
3743
+ let buyNowOrderNumber;
3631
3744
  const created = await firestore.runTransaction(async (transaction)=>{
3632
3745
  var _counter_get, _ref, _ref1, _ref2, _ref3;
3633
3746
  var _object_metadata, _object_customer_details, _object_customer_details1;
@@ -3637,6 +3750,7 @@ async function findOrderForDispute(paymentIntentId) {
3637
3750
  ]);
3638
3751
  if (existing.exists) return false;
3639
3752
  const number = Number((_counter_get = counter.get('next')) != null ? _counter_get : 1);
3753
+ buyNowOrderNumber = number;
3640
3754
  transaction.set(counterRef, {
3641
3755
  next: number + 1
3642
3756
  }, {
@@ -3683,16 +3797,19 @@ async function findOrderForDispute(paymentIntentId) {
3683
3797
  // Redelivery guard (AGL-498): skip the notification + fulfilment side
3684
3798
  // effects when this order already existed.
3685
3799
  if (!created) return;
3800
+ const buyNowOrderLabel = CommerceModel.formatOrderNumber({
3801
+ number: buyNowOrderNumber
3802
+ }, String(object.id));
3686
3803
  // 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({
3804
+ // in the bell, not just the owner's email: what came in, from whom
3805
+ // and where (AGL-3432).
3806
+ const buyNowTotal = `$${(Number((_ref78 = object == null ? void 0 : object.amount_total) != null ? _ref78 : 0) / 100).toFixed(2)}`;
3807
+ void notifyHostManagers(String(hostId), {
3689
3808
  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
- } : {}, {
3809
+ title: `New order on {site} — ${buyNowTotal}`,
3810
+ body: `Order ${buyNowOrderLabel} came in on {site}: ${soldQuantity}× ` + `${snapshotName}, ${buyNowTotal}` + ((object == null ? void 0 : (_object_customer_details18 = object.customer_details) == null ? void 0 : _object_customer_details18.email) ? `, from ${object.customer_details.email}.` : '.'),
3694
3811
  link: `/${hostId}/products`
3695
- }));
3812
+ });
3696
3813
  // Dropship routing (AGL-289): paid lines with a supplier notify
3697
3814
  // it (signed webhook and/or email) and stash a callback token so
3698
3815
  // the supplier can post tracking back. Plan-gated; failures never
@@ -3713,8 +3830,8 @@ async function findOrderForDispute(paymentIntentId) {
3713
3830
  // `supplier-outbox.ts` retries out of band.
3714
3831
  await (async ()=>{
3715
3832
  try {
3716
- var _routedProduct_get, _ref, _ref1;
3717
- var _object_customer_details, _object_customer_details1;
3833
+ var _routedProduct_get, _ref, _ref1, _ref2, _shipTo_address_line1, _shipTo_address_line2, _shipTo_address_city, _shipTo_address_state, _shipTo_address_postal_code, _shipTo_address_country;
3834
+ var _object_shipping_details, _object_customer_details, _object_customer_details1;
3718
3835
  const routedOrg = await getOrgForHost(String(hostId));
3719
3836
  if (!Aglyn.checkEntitlement(routedOrg == null ? void 0 : routedOrg.org, 'dropshipRouting')) {
3720
3837
  return;
@@ -3737,6 +3854,11 @@ async function findOrderForDispute(paymentIntentId) {
3737
3854
  }, {
3738
3855
  merge: true
3739
3856
  });
3857
+ // Where the parcel goes (AGL-3432): the address the shopper gave
3858
+ // at checkout. Buy-now asks for one only when the store ships, so
3859
+ // it can be absent, and a billing address is never offered in its
3860
+ // place.
3861
+ const shipTo = (object == null ? void 0 : (_object_shipping_details = object.shipping_details) == null ? void 0 : _object_shipping_details.address) ? object.shipping_details : null;
3740
3862
  const payload = {
3741
3863
  hostId: String(hostId),
3742
3864
  orderId: String(object.id),
@@ -3746,7 +3868,15 @@ async function findOrderForDispute(paymentIntentId) {
3746
3868
  // the buyer paid for.
3747
3869
  quantity: soldQuantity,
3748
3870
  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,
3871
+ shippingName: (_ref1 = (_ref2 = shipTo == null ? void 0 : shipTo.name) != null ? _ref2 : object == null ? void 0 : (_object_customer_details1 = object.customer_details) == null ? void 0 : _object_customer_details1.name) != null ? _ref1 : null,
3872
+ shippingAddress: shipTo ? {
3873
+ line1: (_shipTo_address_line1 = shipTo.address.line1) != null ? _shipTo_address_line1 : null,
3874
+ line2: (_shipTo_address_line2 = shipTo.address.line2) != null ? _shipTo_address_line2 : null,
3875
+ city: (_shipTo_address_city = shipTo.address.city) != null ? _shipTo_address_city : null,
3876
+ state: (_shipTo_address_state = shipTo.address.state) != null ? _shipTo_address_state : null,
3877
+ postalCode: (_shipTo_address_postal_code = shipTo.address.postal_code) != null ? _shipTo_address_postal_code : null,
3878
+ country: (_shipTo_address_country = shipTo.address.country) != null ? _shipTo_address_country : null
3879
+ } : null,
3750
3880
  updateUrl: `https://${requestHost}/api/commerce/supplier-update` + `?hostId=${hostId}&orderId=${object.id}&token=${supplierToken}`
3751
3881
  };
3752
3882
  if (supplier.webhookUrl) {
@@ -3771,11 +3901,31 @@ async function findOrderForDispute(paymentIntentId) {
3771
3901
  });
3772
3902
  }
3773
3903
  if (supplier.email) {
3774
- var _ref2, _payload_shippingName;
3904
+ var _resolveHostToken, _ref3, _payload_shippingName;
3905
+ // Which store is asking (AGL-3432): a supplier serves several,
3906
+ // and this mail goes out with no site header to say so.
3907
+ const storeName = (_resolveHostToken = resolveHostToken('businessName', await hostRef.get().then((snapshot)=>snapshot.data()).catch(()=>undefined))) != null ? _resolveHostToken : 'A store';
3908
+ const recipient = (_ref3 = (_payload_shippingName = payload.shippingName) != null ? _payload_shippingName : payload.customerEmail) != null ? _ref3 : 'the buyer';
3909
+ const address = payload.shippingAddress ? [
3910
+ payload.shippingAddress.line1,
3911
+ payload.shippingAddress.line2,
3912
+ [
3913
+ payload.shippingAddress.city,
3914
+ [
3915
+ payload.shippingAddress.state,
3916
+ payload.shippingAddress.postalCode
3917
+ ].filter(Boolean).join(' ')
3918
+ ].filter(Boolean).join(', '),
3919
+ payload.shippingAddress.country
3920
+ ].filter(Boolean).join(', ') : '';
3775
3921
  await sendEmail({
3776
3922
  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`,
3923
+ subject: `New order to fulfill for ${storeName}: ${payload.productName}`,
3924
+ // The tracking link opens a form for the carrier and the
3925
+ // number, so it carries no placeholders: a link with
3926
+ // `trackingNumber=TRACKING` in it, opened unedited, records
3927
+ // that word as the tracking.
3928
+ text: `${storeName} has a new order for you to ship: ` + `${payload.quantity}× ${payload.productName} ` + `(order ${buyNowOrderLabel}).\n\n` + (address ? `Ship to: ${recipient}, ${address}\n\n` : `Ship to: ${recipient}. No shipping address was ` + `collected at checkout; ask ${storeName} for it.\n\n`) + 'When it ships, open this link to enter the carrier and ' + `tracking number:\n${payload.updateUrl}`,
3779
3929
  fromName: (await brandFor(hostId)).fromName,
3780
3930
  sendingIdentity: await hostSendingIdentity(String(hostId)),
3781
3931
  audience: 'tenant',
@@ -3795,14 +3945,14 @@ async function findOrderForDispute(paymentIntentId) {
3795
3945
  // Contacts ingestion (AGL-197): buyers become contacts.
3796
3946
  await captureHostContact({
3797
3947
  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,
3948
+ email: object == null ? void 0 : (_object_customer_details19 = object.customer_details) == null ? void 0 : _object_customer_details19.email,
3949
+ name: (_ref79 = object == null ? void 0 : (_object_customer_details20 = object.customer_details) == null ? void 0 : _object_customer_details20.name) != null ? _ref79 : undefined,
3800
3950
  source: 'order',
3801
3951
  initialLifecycleStage: 'customer',
3802
- purchaseCents: Number((_ref71 = object == null ? void 0 : object.amount_total) != null ? _ref71 : 0),
3952
+ purchaseCents: Number((_ref80 = object == null ? void 0 : object.amount_total) != null ? _ref80 : 0),
3803
3953
  interaction: {
3804
3954
  refId: String(object.id),
3805
- summary: `Placed an order ($${(Number((_ref72 = object == null ? void 0 : object.amount_total) != null ? _ref72 : 0) / 100).toFixed(2)})`
3955
+ summary: `Placed an order ($${(Number((_ref81 = object == null ? void 0 : object.amount_total) != null ? _ref81 : 0) / 100).toFixed(2)})`
3806
3956
  }
3807
3957
  });
3808
3958
  const productRef = hostRef.collection('products').doc(String(productId));
@@ -3820,12 +3970,12 @@ async function findOrderForDispute(paymentIntentId) {
3820
3970
  // serialize; the snapshot above is still read, for the product NAME on
3821
3971
  // the receipt below, and no longer decides any count.
3822
3972
  {
3823
- var _productSnapshot_data2, _ref74;
3973
+ var _productSnapshot_data2, _ref83;
3824
3974
  var _object_metadata34, _lifted_variants_1;
3825
3975
  const lifted = CommerceModel.liftLegacyProduct((_productSnapshot_data2 = productSnapshot.data()) != null ? _productSnapshot_data2 : {
3826
3976
  name: 'Product'
3827
3977
  });
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);
3978
+ const soldVariantId = String((_ref83 = (_object_metadata34 = object.metadata) == null ? void 0 : _object_metadata34.variantId) != null ? _ref83 : '') || ((_lifted_variants_1 = lifted.variants[0]) == null ? void 0 : _lifted_variants_1.id);
3829
3979
  if (soldVariantId) {
3830
3980
  // AGL-1711: `-1` regardless of how many units were bought, so a
3831
3981
  // 3-unit buy-now sale decremented stock by one and the difference
@@ -3856,14 +4006,14 @@ async function findOrderForDispute(paymentIntentId) {
3856
4006
  // Settlement, the same way round and for the same reason as the cart
3857
4007
  // branch above (AGL-2356): the decrement lands first, then the
3858
4008
  // reservation is dropped.
3859
- await releaseStockHold(hostRef, String((_ref73 = (_object_metadata33 = object.metadata) == null ? void 0 : _object_metadata33.stockHoldKey) != null ? _ref73 : ''));
4009
+ await releaseStockHold(hostRef, String((_ref82 = (_object_metadata33 = object.metadata) == null ? void 0 : _object_metadata33.stockHoldKey) != null ? _ref82 : ''));
3860
4010
  if (couponCode) {
3861
- var _ref75;
4011
+ var _ref84;
3862
4012
  var _object_metadata35;
3863
4013
  await settleRedemption({
3864
4014
  firestore,
3865
4015
  ref: hostRef.collection('coupons').doc(String(couponCode)),
3866
- holdKey: String((_ref75 = (_object_metadata35 = object.metadata) == null ? void 0 : _object_metadata35.couponHoldKey) != null ? _ref75 : ''),
4016
+ holdKey: String((_ref84 = (_object_metadata35 = object.metadata) == null ? void 0 : _object_metadata35.couponHoldKey) != null ? _ref84 : ''),
3867
4017
  orderRef,
3868
4018
  label: `coupon ${couponCode}`,
3869
4019
  detail: `Coupon ${couponCode} was applied to this order but no longer ` + 'exists, so the redemption is uncounted against its limit.'
@@ -3872,24 +4022,31 @@ async function findOrderForDispute(paymentIntentId) {
3872
4022
  // Receipt + seller notification (AGL-96): env-gated like every
3873
4023
  // other outbound email; failures never fail the webhook.
3874
4024
  if (isEmailConfigured()) {
3875
- var _productSnapshot_get, _ref76;
3876
- var _object_customer_details20, _org, _this3;
4025
+ var _productSnapshot_get, _ref85;
4026
+ var _object_customer_details21, _org, _this3;
3877
4027
  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;
4028
+ const amount = (Number((_ref85 = object == null ? void 0 : object.amount_total) != null ? _ref85 : 0) / 100).toFixed(2);
4029
+ const buyerEmail = object == null ? void 0 : (_object_customer_details21 = object.customer_details) == null ? void 0 : _object_customer_details21.email;
3880
4030
  const orderTotal = `$${amount}`;
3881
4031
  if (buyerEmail) {
3882
- var _ref77;
3883
- const fallbackText = `Thanks for your purchase!\n\n${productName} — $${amount}` + `\nOrder reference: ${object.id}`;
4032
+ var _ref86, _ref87;
4033
+ // The store's own Receipt footer, as the cart receipt carries it
4034
+ // (AGL-3432).
4035
+ const receiptFooter = String((_ref86 = await hostRef.collection('settings').doc('store').get().then((snapshot)=>snapshot.get('receiptFooter')).catch(()=>'')) != null ? _ref86 : '');
4036
+ // The quantity with the name (AGL-3432), so a three-unit sale
4037
+ // does not read as one item at the whole total.
4038
+ const receiptLine = `${soldQuantity}× ${productName}`;
4039
+ const fallbackText = `Thanks for your purchase!\n\n${receiptLine} — $${amount}` + `\nOrder reference: ${object.id}` + (receiptFooter ? `\n\n${receiptFooter}` : '');
3884
4040
  // Site-owner-designed template when published (AGL-771).
3885
4041
  const designed = await renderHostEmailWithTokens(firebaseAdmin.app().firestore(), String(hostId), 'order-receipt', {
3886
- 'order.summary': productName,
4042
+ 'order.summary': receiptLine,
3887
4043
  'order.total': orderTotal,
3888
- 'order.ref': String(object.id)
4044
+ 'order.ref': String(object.id),
4045
+ 'store.receiptFooter': receiptFooter
3889
4046
  });
3890
4047
  await sendEmail(_extends({
3891
4048
  to: String(buyerEmail),
3892
- subject: (_ref77 = designed == null ? void 0 : designed.subject) != null ? _ref77 : `Receipt: ${productName}`,
4049
+ subject: (_ref87 = designed == null ? void 0 : designed.subject) != null ? _ref87 : `Receipt: ${productName}`,
3893
4050
  text: (designed == null ? void 0 : designed.text) || fallbackText
3894
4051
  }, (designed == null ? void 0 : designed.html) ? {
3895
4052
  html: designed.html
@@ -3923,7 +4080,7 @@ async function findOrderForDispute(paymentIntentId) {
3923
4080
  // SSO.
3924
4081
  const seller = (_this4 = await findUserByUidAcrossPools(sellerUid).catch(()=>null)) == null ? void 0 : _this4.record;
3925
4082
  if (seller == null ? void 0 : seller.email) {
3926
- var _hostSnapshot_get, _ref78;
4083
+ var _hostSnapshot_get, _ref88;
3927
4084
  const siteName = String((_hostSnapshot_get = hostSnapshot.get('displayName')) != null ? _hostSnapshot_get : hostId);
3928
4085
  const fallbackText = `You made a sale on ${siteName}!\n\n${productName} — $${amount}` + (buyerEmail ? `\nBuyer: ${buyerEmail}` : '') + `\nOrder reference: ${object.id}`;
3929
4086
  // Site-owner-designed template when published (AGL-771).
@@ -3936,7 +4093,7 @@ async function findOrderForDispute(paymentIntentId) {
3936
4093
  });
3937
4094
  await sendEmail(_extends({
3938
4095
  to: seller.email,
3939
- subject: (_ref78 = designed == null ? void 0 : designed.subject) != null ? _ref78 : `New order: ${productName}`,
4096
+ subject: (_ref88 = designed == null ? void 0 : designed.subject) != null ? _ref88 : `New order: ${productName}`,
3940
4097
  text: (designed == null ? void 0 : designed.text) || fallbackText
3941
4098
  }, (designed == null ? void 0 : designed.html) ? {
3942
4099
  html: designed.html
@@ -3966,15 +4123,15 @@ async function findOrderForDispute(paymentIntentId) {
3966
4123
  // the refund is written on the order's timeline. Nothing else is moved: a
3967
4124
  // Dashboard refund is staff's act, and they chose its transfer reversal.
3968
4125
  if (type === 'charge.refunded') {
3969
- var _ref79, _ref80;
4126
+ var _ref89, _ref90;
3970
4127
  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 : '');
4128
+ const paymentIntentId = typeof (object == null ? void 0 : object.payment_intent) === 'string' ? object.payment_intent : String((_ref89 = object == null ? void 0 : (_object_payment_intent = object.payment_intent) == null ? void 0 : _object_payment_intent.id) != null ? _ref89 : '');
3972
4129
  const lookup = paymentIntentId ? await findOrderForDispute(paymentIntentId) : {
3973
4130
  kind: 'not-ours'
3974
4131
  };
3975
4132
  if (lookup.kind !== 'order') return;
3976
4133
  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 : '');
4134
+ const hostId = String((_ref90 = (_snapshot_ref_parent_parent = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent.id) != null ? _ref90 : '');
3978
4135
  const external = await recordExternalRefund(snapshot.ref, object);
3979
4136
  if (external && hostId && snapshot.ref.parent.parent) {
3980
4137
  const outcome = await applyExternalRefundToGiftCards({
@@ -3986,9 +4143,12 @@ async function findOrderForDispute(paymentIntentId) {
3986
4143
  fullyRefunded: external.fullyRefunded
3987
4144
  });
3988
4145
  if (outcome.kind === 'frozen-for-review') {
4146
+ var _snapshot_data;
4147
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
3989
4148
  await notifyGiftCardHold({
3990
4149
  hostId,
3991
- orderLabel: `order ${snapshot.id}`,
4150
+ // The order number the merchant knows it by, never its document id.
4151
+ orderLabel: `order ${CommerceModel.formatOrderNumber(order, snapshot.id)}`,
3992
4152
  orderPath: `/${hostId}/products/orders?order=${encodeURIComponent(snapshot.id)}`,
3993
4153
  orderId: snapshot.id,
3994
4154
  cards: outcome.cards,
@@ -4012,20 +4172,23 @@ async function findOrderForDispute(paymentIntentId) {
4012
4172
  // merchant; both report the site so the seller's fraud ledger can name it.
4013
4173
  const risk = paymentRiskEventFrom(type, object);
4014
4174
  if (risk && risk.signal.kind !== 'dispute') {
4015
- var _ref81, _snapshot_data;
4016
- var _snapshot_ref_parent_parent1;
4175
+ var _ref91, _snapshot_data1, _ref92, _ref93;
4176
+ var _snapshot_ref_parent_parent1, _order_totals2;
4017
4177
  const lookup = risk.paymentIntentId ? await findOrderForDispute(risk.paymentIntentId) : {
4018
4178
  kind: 'not-ours'
4019
4179
  };
4020
4180
  if (lookup.kind !== 'order') return;
4021
4181
  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 : {});
4182
+ const hostId = String((_ref91 = (_snapshot_ref_parent_parent1 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent1.id) != null ? _ref91 : '');
4183
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data1 = snapshot.data()) != null ? _snapshot_data1 : {});
4184
+ const orderCents = Number((_ref92 = (_ref93 = (_order_totals2 = order.totals) == null ? void 0 : _order_totals2.totalCents) != null ? _ref93 : order.amountCents) != null ? _ref92 : 0);
4024
4185
  await recordPaymentRiskOnRecord({
4025
4186
  ref: snapshot.ref,
4026
4187
  signal: risk.signal,
4027
4188
  hostId,
4028
4189
  subjectLabel: `order ${CommerceModel.formatOrderNumber(order, snapshot.id)}`,
4190
+ // The notice names the sum in question, as the dispute's does.
4191
+ amount: orderCents > 0 ? `$${(orderCents / 100).toFixed(2)}` : null,
4029
4192
  // The Orders section of the Products hub, with this order's dialog
4030
4193
  // open — where Refund lives (`siteRecordLinks().order`).
4031
4194
  link: `/${hostId}/products/orders?order=${encodeURIComponent(snapshot.id)}`,
@@ -4081,7 +4244,7 @@ async function findOrderForDispute(paymentIntentId) {
4081
4244
  // finds). AGL-1554 will add its own `closed` branch for the marketplace
4082
4245
  // side; the two coexist because each self-selects by finding its own record.
4083
4246
  if (type === 'charge.dispute.created' || type === 'charge.dispute.closed') {
4084
- var _dispute_payment_intent, _ref82;
4247
+ var _dispute_payment_intent, _ref94;
4085
4248
  var _snapshot_ref_parent_parent2;
4086
4249
  const dispute = object != null ? object : {};
4087
4250
  const paymentIntentId = String((_dispute_payment_intent = dispute.payment_intent) != null ? _dispute_payment_intent : '');
@@ -4107,14 +4270,16 @@ async function findOrderForDispute(paymentIntentId) {
4107
4270
  const claimed = lookup.kind === 'order' || lookup.kind === 'unresolved';
4108
4271
  const snapshot = lookup.kind === 'order' ? lookup.snapshot : null;
4109
4272
  if (snapshot) {
4110
- var _ref83;
4273
+ var _ref95;
4111
4274
  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 : '');
4275
+ const hostId = String((_ref95 = (_snapshot_ref_parent_parent3 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent3.id) != null ? _ref95 : '');
4113
4276
  if (type === 'charge.dispute.created') {
4277
+ var _snapshot_data2;
4114
4278
  // A chargeback's gift cards are frozen while it is open (AGL-3363).
4279
+ const disputed = CommerceModel.liftLegacyOrder((_snapshot_data2 = snapshot.data()) != null ? _snapshot_data2 : {});
4115
4280
  await notifyGiftCardHold({
4116
4281
  hostId,
4117
- orderLabel: `order ${snapshot.id}`,
4282
+ orderLabel: `order ${CommerceModel.formatOrderNumber(disputed, snapshot.id)}`,
4118
4283
  orderPath: `/${hostId}/products/orders?order=${encodeURIComponent(snapshot.id)}`,
4119
4284
  orderId: snapshot.id,
4120
4285
  cards: await applyOrderGiftCardRisk(snapshot, {
@@ -4125,7 +4290,7 @@ async function findOrderForDispute(paymentIntentId) {
4125
4290
  }, notifyRiskEvent);
4126
4291
  const { opened, record } = await recordDisputeOpened(snapshot, dispute);
4127
4292
  if (opened && hostId) {
4128
- var _snapshot_data1, _record_evidenceDueByMs, _ref84;
4293
+ var _snapshot_data3, _record_evidenceDueByMs, _ref96;
4129
4294
  // Time-critical — Stripe's evidence window is days, not weeks — so
4130
4295
  // this is awaited rather than fired off with `void`: the handler is
4131
4296
  // serverless and work left running past the response is work the
@@ -4133,7 +4298,7 @@ async function findOrderForDispute(paymentIntentId) {
4133
4298
  // (AGL-3368) tells the site's managers and the workspace's owners
4134
4299
  // what a dispute means and when the evidence is due; it never
4135
4300
  // throws.
4136
- const order = CommerceModel.liftLegacyOrder((_snapshot_data1 = snapshot.data()) != null ? _snapshot_data1 : {});
4301
+ const order = CommerceModel.liftLegacyOrder((_snapshot_data3 = snapshot.data()) != null ? _snapshot_data3 : {});
4137
4302
  await notifyRiskEvent({
4138
4303
  kind: 'sale-dispute',
4139
4304
  orgId: null,
@@ -4144,16 +4309,16 @@ async function findOrderForDispute(paymentIntentId) {
4144
4309
  },
4145
4310
  amount: `$${(record.amountCents / 100).toFixed(2)}`,
4146
4311
  evidenceDueByMs: (_record_evidenceDueByMs = record.evidenceDueByMs) != null ? _record_evidenceDueByMs : null,
4147
- dedupeKey: `sale:${String((_ref84 = dispute == null ? void 0 : dispute.id) != null ? _ref84 : snapshot.id)}`,
4312
+ dedupeKey: `sale:${String((_ref96 = dispute == null ? void 0 : dispute.id) != null ? _ref96 : snapshot.id)}`,
4148
4313
  staffEvidence: record.reason ? `Reason: ${record.reason.replace(/_/g, ' ')}.` : null
4149
4314
  });
4150
4315
  }
4151
4316
  } else {
4152
- var _ref85, _ref86;
4317
+ var _ref97, _snapshot_data4, _ref98;
4153
4318
  // Lost: the money went back, so the cards it bought are voided. Won
4154
4319
  // (or closed on a warning): the dispute's freeze lifts; a freeze a
4155
4320
  // 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' ? {
4321
+ await applyOrderGiftCardRisk(snapshot, String((_ref97 = dispute == null ? void 0 : dispute.status) != null ? _ref97 : '') === 'lost' ? {
4157
4322
  kind: 'void',
4158
4323
  reason: 'dispute-lost'
4159
4324
  } : {
@@ -4161,11 +4326,18 @@ async function findOrderForDispute(paymentIntentId) {
4161
4326
  reason: 'dispute'
4162
4327
  });
4163
4328
  const settled = await recordDisputeClosed(snapshot, dispute);
4329
+ // The number the merchant knows the order by (AGL-3432), for both
4330
+ // notices below; the Stripe session id is nowhere in the console.
4331
+ const disputedOrderLabel = CommerceModel.formatOrderNumber(CommerceModel.liftLegacyOrder((_snapshot_data4 = snapshot.data()) != null ? _snapshot_data4 : {}), snapshot.id);
4164
4332
  if (settled.recorded && hostId) {
4333
+ // What the shopper's bank took back left the PLATFORM's balance on
4334
+ // a destination charge, not the merchant's: their own loss is the
4335
+ // seller share in the "Payout adjusted" notice below. Told that way
4336
+ // (AGL-3432), so the two are not read as two losses.
4165
4337
  await notifyHostManagers(hostId, {
4166
4338
  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}`,
4339
+ title: settled.lost ? `Chargeback lost — order ${disputedOrderLabel}` : 'Chargeback resolved in your favor',
4340
+ body: settled.lost ? `The chargeback on order ${disputedOrderLabel} on {site} was ` + 'decided in the shopper\'s favor, and their bank took back ' + `$${(settled.reversedCents / 100).toFixed(2)}. If part of the ` + 'sale had been paid out to you, a separate notice says what ' + 'is taken back from your balance.' : `The chargeback on order ${disputedOrderLabel} on {site} ` + 'closed in your favor. Nothing is taken back from you for it.',
4169
4341
  link: `/${hostId}/orders`
4170
4342
  });
4171
4343
  // The customer's side of the ledger (AGL-1754), through the same
@@ -4217,7 +4389,7 @@ async function findOrderForDispute(paymentIntentId) {
4217
4389
  // transiently-failed reversal gets its retry — and gated inside on
4218
4390
  // the order's own `reversedTransferCents` marker, so it runs at most
4219
4391
  // once per dispute however many times Stripe delivers.
4220
- if (String((_ref86 = dispute == null ? void 0 : dispute.status) != null ? _ref86 : '') === 'lost') {
4392
+ if (String((_ref98 = dispute == null ? void 0 : dispute.status) != null ? _ref98 : '') === 'lost') {
4221
4393
  const pulledBackCents = await reverseSellerShare(snapshot.ref, dispute);
4222
4394
  // The merchant-experience half of the AGL-1794 decision, and not an
4223
4395
  // optional extra: the reversal takes money out of the connected
@@ -4237,7 +4409,7 @@ async function findOrderForDispute(paymentIntentId) {
4237
4409
  await notifyHostManagers(hostId, {
4238
4410
  type: 'content.order',
4239
4411
  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.`,
4412
+ body: `Because the chargeback on order ${disputedOrderLabel} on ` + `{site} was lost, the $${(pulledBackCents / 100).toFixed(2)} ` + 'transferred to you for that sale has been taken back. If ' + 'your balance does not cover it, Stripe recovers the rest ' + 'from your future payouts.',
4241
4413
  link: `/${hostId}/orders`
4242
4414
  });
4243
4415
  }
@@ -4247,7 +4419,7 @@ async function findOrderForDispute(paymentIntentId) {
4247
4419
  return _extends({
4248
4420
  claimed
4249
4421
  }, snapshot ? {
4250
- hostId: String((_ref82 = (_snapshot_ref_parent_parent2 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent2.id) != null ? _ref82 : '')
4422
+ hostId: String((_ref94 = (_snapshot_ref_parent_parent2 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent2.id) != null ? _ref94 : '')
4251
4423
  } : {});
4252
4424
  }
4253
4425
  };