@aglyn/plugins-commerce 1.0.0-beta.201 → 1.0.0-beta.203

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 (41) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/console/gift-cards-card.component.js +34 -2
  3. package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
  4. package/src/lib/model/commerce-gift-cards.d.ts +30 -0
  5. package/src/lib/model/commerce-gift-cards.js +39 -1
  6. package/src/lib/model/commerce-gift-cards.js.map +1 -1
  7. package/src/lib/server/billing-webhook.js +180 -106
  8. package/src/lib/server/billing-webhook.js.map +1 -1
  9. package/src/lib/server/cart-checkout.js +46 -8
  10. package/src/lib/server/cart-checkout.js.map +1 -1
  11. package/src/lib/server/checkout.js +23 -5
  12. package/src/lib/server/checkout.js.map +1 -1
  13. package/src/lib/server/download.js +12 -1
  14. package/src/lib/server/download.js.map +1 -1
  15. package/src/lib/server/draft-order.js +5 -2
  16. package/src/lib/server/draft-order.js.map +1 -1
  17. package/src/lib/server/feed.js +12 -2
  18. package/src/lib/server/feed.js.map +1 -1
  19. package/src/lib/server/gift-card-risk.d.ts +112 -0
  20. package/src/lib/server/gift-card-risk.js +220 -0
  21. package/src/lib/server/gift-card-risk.js.map +1 -0
  22. package/src/lib/server/gift-cards.js +37 -2
  23. package/src/lib/server/gift-cards.js.map +1 -1
  24. package/src/lib/server/paid-media-delivery.d.ts +26 -4
  25. package/src/lib/server/paid-media-delivery.js +36 -9
  26. package/src/lib/server/paid-media-delivery.js.map +1 -1
  27. package/src/lib/server/recurring-charges.d.ts +44 -0
  28. package/src/lib/server/recurring-charges.js +65 -0
  29. package/src/lib/server/recurring-charges.js.map +1 -0
  30. package/src/lib/server/refund.js +24 -0
  31. package/src/lib/server/refund.js.map +1 -1
  32. package/src/lib/server/reserve.js +14 -5
  33. package/src/lib/server/reserve.js.map +1 -1
  34. package/src/lib/server/stream.js +12 -1
  35. package/src/lib/server/stream.js.map +1 -1
  36. package/src/lib/server/subscription-portal.js +13 -5
  37. package/src/lib/server/subscription-portal.js.map +1 -1
  38. package/src/lib/server/supplier-outbox.js +22 -7
  39. package/src/lib/server/supplier-outbox.js.map +1 -1
  40. package/src/lib/server.js +15 -3
  41. package/src/lib/server.js.map +1 -1
@@ -27,6 +27,8 @@ import { recordContactRefund } from "./contact-refund.js";
27
27
  import { reverseEmailAttributedRevenue } from "@aglyn/tenant-data-admin/server/email-revenue-attribution";
28
28
  import { paymentRiskEventFrom } from "@aglyn/aglyn/app-utils/payment-risk";
29
29
  import { recordPaymentRiskOnRecord } from "@aglyn/tenant-data-admin/server/payment-risk-record";
30
+ import { isTransientStripeStatus, reverseDestinationTransfer } from "@aglyn/tenant-data-admin/server/stripe-transfer-reversal";
31
+ import { applyExternalRefundToGiftCards, applyGiftCardRiskToOrder, notifyGiftCardHold } from "./gift-card-risk.js";
30
32
  import { mintDownloadToken, tokenSigningSecret } from "./download.js";
31
33
  import { alertLowStockCrossing } from "./low-stock.js";
32
34
  import { decrementVariantStock } from "./reserve-stock.js";
@@ -305,6 +307,66 @@ async function findOrderForDispute(paymentIntentId) {
305
307
  evidenceDueByMs: dueBy * 1000
306
308
  } : {});
307
309
  }
310
+ /**
311
+ * Record, once, the part of `charge.amount_refunded` the order does not
312
+ * already account for (AGL-3363): the console's own refunds are reserved in
313
+ * `refundedCents` before Stripe is asked, and earlier Dashboard refunds are
314
+ * in `externalRefundedCents`. Returns the new amount and the order's lines,
315
+ * or null when there is nothing new — a console refund's own event, or a
316
+ * redelivery. One transaction, so two deliveries cannot both count it.
317
+ */ async function recordExternalRefund(orderRef, charge) {
318
+ var _ref;
319
+ const stripeRefundedCents = Math.max(0, Math.round(Number((_ref = charge == null ? void 0 : charge.amount_refunded) != null ? _ref : 0)));
320
+ if (!(stripeRefundedCents > 0)) return null;
321
+ return firebaseAdmin.app().firestore().runTransaction(async (transaction)=>{
322
+ var _fresh_data, _order_refundedCents, _data_externalRefundedCents, _order_lineItems;
323
+ const fresh = await transaction.get(orderRef);
324
+ if (!fresh.exists) return null;
325
+ const data = (_fresh_data = fresh.data()) != null ? _fresh_data : {};
326
+ const order = CommerceModel.liftLegacyOrder(data);
327
+ const consoleCents = Math.max(0, Number((_order_refundedCents = order.refundedCents) != null ? _order_refundedCents : 0));
328
+ const externalCents = Math.max(0, Number((_data_externalRefundedCents = data['externalRefundedCents']) != null ? _data_externalRefundedCents : 0));
329
+ const newCents = stripeRefundedCents - consoleCents - externalCents;
330
+ if (!(newCents > 0)) return null;
331
+ transaction.set(orderRef, {
332
+ externalRefundedCents: externalCents + newCents,
333
+ timeline: CommerceModel.appendOrderEvent(order, 'refund', `$${(newCents / 100).toFixed(2)} refunded outside the dashboard (in Stripe)`)
334
+ }, {
335
+ merge: true
336
+ });
337
+ return {
338
+ newCents,
339
+ fullyRefunded: (charge == null ? void 0 : charge.refunded) === true,
340
+ lines: ((_order_lineItems = order.lineItems) != null ? _order_lineItems : []).map((line)=>{
341
+ var _line_productId, _line_unitAmountCents, _line_quantity;
342
+ return {
343
+ productId: String((_line_productId = line.productId) != null ? _line_productId : ''),
344
+ totalCents: Math.round(Number((_line_unitAmountCents = line.unitAmountCents) != null ? _line_unitAmountCents : 0)) * Math.max(1, Math.round(Number((_line_quantity = line.quantity) != null ? _line_quantity : 1)))
345
+ };
346
+ })
347
+ };
348
+ });
349
+ }
350
+ /**
351
+ * Apply a gift-card risk action to the cards an order issued (AGL-3363). The
352
+ * order doc id is the Checkout Session id every issued card carries as its
353
+ * `orderId`. Never throws.
354
+ */ async function applyOrderGiftCardRisk(snapshot, action) {
355
+ try {
356
+ var _snapshot_ref_parent, _snapshot_ref;
357
+ const hostRef = (_snapshot_ref = snapshot.ref) == null ? void 0 : (_snapshot_ref_parent = _snapshot_ref.parent) == null ? void 0 : _snapshot_ref_parent.parent;
358
+ if (!hostRef) return 0;
359
+ return await applyGiftCardRiskToOrder({
360
+ firestore: firebaseAdmin.app().firestore(),
361
+ hostRef,
362
+ orderId: snapshot.id,
363
+ action
364
+ });
365
+ } catch (error) {
366
+ console.error('[gift-card-risk] skipped', snapshot.id, error);
367
+ return 0;
368
+ }
369
+ }
308
370
  /**
309
371
  * `charge.dispute.created`: FLAG the order, reverse nothing (AGL-1787).
310
372
  *
@@ -575,9 +637,6 @@ async function findOrderForDispute(paymentIntentId) {
575
637
  link: `/${hostId}/products`
576
638
  });
577
639
  }
578
- /** Stripe failures a redelivery can actually fix. */ function isTransientStripeStatus(status) {
579
- return status === 429 || status >= 500;
580
- }
581
640
  /** One authorized GET against the Stripe API, body parsed either way. */ async function stripeGet(url, stripeKey) {
582
641
  const response = await fetch(url, {
583
642
  headers: {
@@ -1369,8 +1428,7 @@ async function findOrderForDispute(paymentIntentId) {
1369
1428
  * notification is keyed on it, so a non-zero answer must mean "the write
1370
1429
  * happened HERE, and nobody has told the merchant yet".
1371
1430
  */ async function reverseSellerShare(orderRef, dispute) {
1372
- var _ref, _snapshot_data, _stored_reversedCents, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8;
1373
- var _charge_body, _charge_body1, _transfer_body_reversals, _transfer_body, _transfer_body1, _transfer_body2;
1431
+ var _ref, _snapshot_data, _stored_reversedCents, _ref1;
1374
1432
  const disputeId = String((_ref = dispute == null ? void 0 : dispute.id) != null ? _ref : '');
1375
1433
  if (!disputeId) return 0;
1376
1434
  const snapshot = await orderRef.get();
@@ -1427,103 +1485,42 @@ async function findOrderForDispute(paymentIntentId) {
1427
1485
  return true;
1428
1486
  });
1429
1487
  };
1430
- const chargeId = String((_ref1 = dispute == null ? void 0 : dispute.charge) != null ? _ref1 : '');
1431
- if (!chargeId) {
1432
- console.error('Dispute carries no charge; seller share not reversed', {
1433
- disputeId
1434
- });
1435
- await settle(0, null, 'Seller share not reversed — no charge on the dispute');
1436
- return 0;
1437
- }
1438
- const charge = await stripeGet(`https://api.stripe.com/v1/charges/${chargeId}`, stripeKey);
1439
- if (!charge.ok) {
1440
- var _charge_body2;
1441
- if (isTransientStripeStatus(charge.status)) {
1442
- throw new Error(`Stripe charge read failed (${charge.status}) for dispute ${disputeId}`);
1443
- }
1444
- console.error('Stripe refused the charge read; seller share not reversed', (_charge_body2 = charge.body) == null ? void 0 : _charge_body2.error);
1445
- await settle(0, null, 'Seller share not reversed — charge not found at Stripe');
1446
- return 0;
1447
- }
1448
- const transferId = String((_ref2 = (_charge_body = charge.body) == null ? void 0 : _charge_body.transfer) != null ? _ref2 : '');
1449
- const chargeAmountCents = Math.round(Number((_ref3 = (_charge_body1 = charge.body) == null ? void 0 : _charge_body1.amount) != null ? _ref3 : 0));
1450
- if (!transferId || !(chargeAmountCents > 0)) {
1451
- // An order from before destination charges, or a charge Stripe holds no
1452
- // transfer for. Logged, recorded, let go.
1453
- console.error('No transfer on the disputed charge; seller share not reversed', {
1454
- disputeId,
1455
- chargeId
1456
- });
1457
- await settle(0, null, 'Seller share not reversed — no transfer on the charge');
1458
- return 0;
1459
- }
1460
- const transfer = await stripeGet(`https://api.stripe.com/v1/transfers/${transferId}`, stripeKey);
1461
- if (!transfer.ok) {
1462
- var _transfer_body3;
1463
- if (isTransientStripeStatus(transfer.status)) {
1464
- throw new Error(`Stripe transfer read failed (${transfer.status}) for dispute ${disputeId}`);
1488
+ // The Stripe half is the shared seam (AGL-3363): the booking deposit's lost
1489
+ // dispute pulls back through the same function, so the two cannot drift.
1490
+ const outcome = await reverseDestinationTransfer({
1491
+ kind: 'dispute',
1492
+ id: disputeId,
1493
+ amountCents: principalCents,
1494
+ chargeId: String((_ref1 = dispute == null ? void 0 : dispute.charge) != null ? _ref1 : ''),
1495
+ metadata: {
1496
+ orderId: orderRef.id
1465
1497
  }
1466
- console.error('Stripe refused the transfer read; seller share not reversed', (_transfer_body3 = transfer.body) == null ? void 0 : _transfer_body3.error);
1467
- await settle(0, null, 'Seller share not reversed — transfer not found at Stripe');
1468
- return 0;
1469
- }
1470
- // The crash window's backstop: the POST landed on a previous delivery and
1471
- // the record did not. Adopt what exists rather than creating a second one.
1472
- const existing = ((_ref4 = (_transfer_body = transfer.body) == null ? void 0 : (_transfer_body_reversals = _transfer_body.reversals) == null ? void 0 : _transfer_body_reversals.data) != null ? _ref4 : []).find((item)=>{
1473
- var _ref;
1474
- var _item_metadata;
1475
- return String((_ref = item == null ? void 0 : (_item_metadata = item.metadata) == null ? void 0 : _item_metadata.disputeId) != null ? _ref : '') === disputeId;
1498
+ }, {
1499
+ stripeKey
1476
1500
  });
1477
- if (existing) {
1478
- var _existing_amount, _existing_id;
1479
- const adoptedCents = Math.round(Number((_existing_amount = existing.amount) != null ? _existing_amount : 0));
1480
- // Announced on adoption too: the delivery that created this reversal died
1481
- // before recording it, so it died before notifying. The money left the
1482
- // connected account either way and the merchant has not been told yet.
1483
- const wrote = await settle(adoptedCents, String((_existing_id = existing.id) != null ? _existing_id : ''), `$${(adoptedCents / 100).toFixed(2)} seller share reversed for lost dispute`);
1484
- return wrote ? adoptedCents : 0;
1485
- }
1486
- const transferCents = Math.round(Number((_ref5 = (_transfer_body1 = transfer.body) == null ? void 0 : _transfer_body1.amount) != null ? _ref5 : 0));
1487
- const alreadyReversedCents = Math.round(Number((_ref6 = (_transfer_body2 = transfer.body) == null ? void 0 : _transfer_body2.amount_reversed) != null ? _ref6 : 0));
1488
- const remainingCents = Math.max(0, transferCents - alreadyReversedCents);
1489
- const shareCents = Math.min(chargeAmountCents > 0 ? Math.floor(principalCents * transferCents / chargeAmountCents) : 0, remainingCents);
1490
- if (!(shareCents > 0)) {
1491
- console.error('Transfer has nothing left to reverse; seller share not reversed', {
1501
+ if (outcome.kind === 'skipped') return 0;
1502
+ if (outcome.kind === 'not-reversed') {
1503
+ console.error(outcome.reason === 'transfer-fully-reversed' ? 'Transfer has nothing left to reverse; seller share not reversed' : 'Seller share not reversed', {
1492
1504
  disputeId,
1493
- transferId,
1494
- transferCents,
1495
- alreadyReversedCents
1505
+ reason: outcome.reason
1496
1506
  });
1497
- await settle(0, null, 'Seller share already reversed on the transfer — nothing left to pull back');
1498
- return 0;
1499
- }
1500
- const params = new URLSearchParams({
1501
- amount: String(shareCents),
1502
- 'metadata[disputeId]': disputeId,
1503
- 'metadata[orderId]': orderRef.id
1504
- });
1505
- const response = await fetch(`https://api.stripe.com/v1/transfers/${transferId}/reversals`, {
1506
- method: 'POST',
1507
- headers: {
1508
- Authorization: `Bearer ${stripeKey}`,
1509
- 'Content-Type': 'application/x-www-form-urlencoded',
1510
- 'Idempotency-Key': `dispute-reversal-${disputeId}`
1511
- },
1512
- body: params.toString()
1513
- });
1514
- const reversal = await response.json().catch(()=>null);
1515
- if (!response.ok) {
1516
- if (isTransientStripeStatus(response.status)) {
1517
- throw new Error(`Stripe transfer reversal failed (${response.status}) for dispute ${disputeId}`);
1518
- }
1519
- console.error('Stripe refused the transfer reversal', reversal == null ? void 0 : reversal.error);
1520
- await settle(0, null, 'Seller share not reversed — Stripe refused the reversal');
1507
+ await settle(0, null, SELLER_SHARE_NOT_REVERSED_NOTE[outcome.reason]);
1521
1508
  return 0;
1522
1509
  }
1523
- const reversedTransferCents = Math.round(Number((_ref7 = reversal == null ? void 0 : reversal.amount) != null ? _ref7 : shareCents));
1524
- const wrote = await settle(reversedTransferCents, String((_ref8 = reversal == null ? void 0 : reversal.id) != null ? _ref8 : ''), `$${(reversedTransferCents / 100).toFixed(2)} seller share reversed for lost dispute`);
1525
- return wrote ? reversedTransferCents : 0;
1510
+ // Announced on adoption too: the delivery that created an adopted reversal
1511
+ // died before recording it, so it died before notifying. The money left the
1512
+ // connected account either way and the merchant has not been told yet.
1513
+ const wrote = await settle(outcome.cents, outcome.reversalId, `$${(outcome.cents / 100).toFixed(2)} seller share reversed for lost dispute`);
1514
+ return wrote ? outcome.cents : 0;
1526
1515
  }
1516
+ /** The order timeline's words for each final reason nothing was pulled back. */ const SELLER_SHARE_NOT_REVERSED_NOTE = {
1517
+ 'no-charge-on-cause': 'Seller share not reversed — no charge on the dispute',
1518
+ 'charge-read-refused': 'Seller share not reversed — charge not found at Stripe',
1519
+ 'no-transfer-on-charge': 'Seller share not reversed — no transfer on the charge',
1520
+ 'transfer-read-refused': 'Seller share not reversed — transfer not found at Stripe',
1521
+ 'transfer-fully-reversed': 'Seller share already reversed on the transfer — nothing left to pull back',
1522
+ 'reversal-refused': 'Seller share not reversed — Stripe refused the reversal'
1523
+ };
1527
1524
  /**
1528
1525
  * Commerce sections of the platform Stripe webhook (AGL-418): relocated
1529
1526
  * verbatim from the console route — subscriptions sync, reservations,
@@ -3084,6 +3081,9 @@ async function findOrderForDispute(paymentIntentId) {
3084
3081
  balanceCents: line.unitAmountCents,
3085
3082
  recipientEmail: (_ref53 = object == null ? void 0 : (_object_customer_details12 = object.customer_details) == null ? void 0 : _object_customer_details12.email) != null ? _ref53 : null,
3086
3083
  orderId: String(object.id),
3084
+ // Which line bought it, so a refund that withdraws that line
3085
+ // voids this card and no other (AGL-3363).
3086
+ productId: line.productId,
3087
3087
  // What the console's Gift cards search asks (AGL-3321).
3088
3088
  searchTokens: giftCardSearchTokens(code, (_ref54 = object == null ? void 0 : (_object_customer_details13 = object.customer_details) == null ? void 0 : _object_customer_details13.email) != null ? _ref54 : null),
3089
3089
  createdAtMs: Date.now()
@@ -3887,6 +3887,48 @@ async function findOrderForDispute(paymentIntentId) {
3887
3887
  }
3888
3888
  }
3889
3889
  }
3890
+ // A refund made OUTSIDE the console (AGL-3363) — in the Stripe Dashboard.
3891
+ // `refund.ts` reserves `refundedCents` on the order before it asks Stripe,
3892
+ // so by the time its own `charge.refunded` arrives the order already counts
3893
+ // it; whatever Stripe has refunded beyond the order's count, and beyond the
3894
+ // Dashboard refunds already recorded here, is new. The order's gift cards
3895
+ // then follow the in-console rules (`applyExternalRefundToGiftCards`), and
3896
+ // the refund is written on the order's timeline. Nothing else is moved: a
3897
+ // Dashboard refund is staff's act, and they chose its transfer reversal.
3898
+ if (type === 'charge.refunded') {
3899
+ var _ref83, _ref84;
3900
+ var _object_payment_intent, _snapshot_ref_parent_parent;
3901
+ const paymentIntentId = typeof (object == null ? void 0 : object.payment_intent) === 'string' ? object.payment_intent : String((_ref83 = object == null ? void 0 : (_object_payment_intent = object.payment_intent) == null ? void 0 : _object_payment_intent.id) != null ? _ref83 : '');
3902
+ const lookup = paymentIntentId ? await findOrderForDispute(paymentIntentId) : {
3903
+ kind: 'not-ours'
3904
+ };
3905
+ if (lookup.kind !== 'order') return;
3906
+ const snapshot = lookup.snapshot;
3907
+ const hostId = String((_ref84 = (_snapshot_ref_parent_parent = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent.id) != null ? _ref84 : '');
3908
+ const external = await recordExternalRefund(snapshot.ref, object);
3909
+ if (external && hostId && snapshot.ref.parent.parent) {
3910
+ const outcome = await applyExternalRefundToGiftCards({
3911
+ firestore: firebaseAdmin.app().firestore(),
3912
+ hostRef: snapshot.ref.parent.parent,
3913
+ orderId: snapshot.id,
3914
+ lines: external.lines,
3915
+ refundCents: external.newCents,
3916
+ fullyRefunded: external.fullyRefunded
3917
+ });
3918
+ if (outcome.kind === 'frozen-for-review') {
3919
+ await notifyGiftCardHold({
3920
+ hostId,
3921
+ orderLabel: `Order ${snapshot.id}`,
3922
+ cards: outcome.cards,
3923
+ reason: 'refund-review'
3924
+ }, notifyHostManagers);
3925
+ }
3926
+ }
3927
+ return {
3928
+ claimed: true,
3929
+ hostId
3930
+ };
3931
+ }
3890
3932
  // Fraud signals on a store's sale (AGL-3360): an issuer's early fraud
3891
3933
  // warning and a Radar review. The merchant decides what to do for their
3892
3934
  // own customer, so the signal lands on the ORDER — a timeline line and the
@@ -3898,14 +3940,14 @@ async function findOrderForDispute(paymentIntentId) {
3898
3940
  // merchant; both report the site so the seller's fraud ledger can name it.
3899
3941
  const risk = paymentRiskEventFrom(type, object);
3900
3942
  if (risk && risk.signal.kind !== 'dispute') {
3901
- var _ref83, _snapshot_data;
3902
- var _snapshot_ref_parent_parent;
3943
+ var _ref85, _snapshot_data;
3944
+ var _snapshot_ref_parent_parent1;
3903
3945
  const lookup = risk.paymentIntentId ? await findOrderForDispute(risk.paymentIntentId) : {
3904
3946
  kind: 'not-ours'
3905
3947
  };
3906
3948
  if (lookup.kind !== 'order') return;
3907
3949
  const snapshot = lookup.snapshot;
3908
- const hostId = String((_ref83 = (_snapshot_ref_parent_parent = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent.id) != null ? _ref83 : '');
3950
+ const hostId = String((_ref85 = (_snapshot_ref_parent_parent1 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent1.id) != null ? _ref85 : '');
3909
3951
  const order = CommerceModel.liftLegacyOrder((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
3910
3952
  await recordPaymentRiskOnRecord({
3911
3953
  ref: snapshot.ref,
@@ -3921,6 +3963,18 @@ async function findOrderForDispute(paymentIntentId) {
3921
3963
  firestore: firebaseAdmin.app().firestore(),
3922
3964
  notify: notifyHostManagers
3923
3965
  });
3966
+ // The cards this order bought are frozen until the merchant releases them
3967
+ // (AGL-3363): a warning or review is the moment a stolen-card gift card
3968
+ // is still unspent.
3969
+ await notifyGiftCardHold({
3970
+ hostId,
3971
+ orderLabel: `Order ${CommerceModel.formatOrderNumber(order, snapshot.id)}`,
3972
+ cards: await applyOrderGiftCardRisk(snapshot, {
3973
+ kind: 'freeze',
3974
+ reason: risk.signal.kind
3975
+ }),
3976
+ reason: risk.signal.kind
3977
+ }, notifyHostManagers);
3924
3978
  return {
3925
3979
  claimed: true,
3926
3980
  hostId
@@ -3951,8 +4005,8 @@ async function findOrderForDispute(paymentIntentId) {
3951
4005
  // finds). AGL-1554 will add its own `closed` branch for the marketplace
3952
4006
  // side; the two coexist because each self-selects by finding its own record.
3953
4007
  if (type === 'charge.dispute.created' || type === 'charge.dispute.closed') {
3954
- var _dispute_payment_intent, _ref84;
3955
- var _snapshot_ref_parent_parent1;
4008
+ var _dispute_payment_intent, _ref86;
4009
+ var _snapshot_ref_parent_parent2;
3956
4010
  const dispute = object != null ? object : {};
3957
4011
  const paymentIntentId = String((_dispute_payment_intent = dispute.payment_intent) != null ? _dispute_payment_intent : '');
3958
4012
  const lookup = paymentIntentId ? await findOrderForDispute(paymentIntentId) : {
@@ -3977,10 +4031,20 @@ async function findOrderForDispute(paymentIntentId) {
3977
4031
  const claimed = lookup.kind === 'order' || lookup.kind === 'unresolved';
3978
4032
  const snapshot = lookup.kind === 'order' ? lookup.snapshot : null;
3979
4033
  if (snapshot) {
3980
- var _ref85;
3981
- var _snapshot_ref_parent_parent2;
3982
- const hostId = String((_ref85 = (_snapshot_ref_parent_parent2 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent2.id) != null ? _ref85 : '');
4034
+ var _ref87;
4035
+ var _snapshot_ref_parent_parent3;
4036
+ const hostId = String((_ref87 = (_snapshot_ref_parent_parent3 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent3.id) != null ? _ref87 : '');
3983
4037
  if (type === 'charge.dispute.created') {
4038
+ // A chargeback's gift cards are frozen while it is open (AGL-3363).
4039
+ await notifyGiftCardHold({
4040
+ hostId,
4041
+ orderLabel: `Order ${snapshot.id}`,
4042
+ cards: await applyOrderGiftCardRisk(snapshot, {
4043
+ kind: 'freeze',
4044
+ reason: 'dispute'
4045
+ }),
4046
+ reason: 'dispute'
4047
+ }, notifyHostManagers);
3984
4048
  const { opened, record } = await recordDisputeOpened(snapshot, dispute);
3985
4049
  if (opened && hostId) {
3986
4050
  // Time-critical — Stripe's evidence window is days, not weeks — so
@@ -3996,7 +4060,17 @@ async function findOrderForDispute(paymentIntentId) {
3996
4060
  });
3997
4061
  }
3998
4062
  } else {
3999
- var _ref86;
4063
+ var _ref88, _ref89;
4064
+ // Lost: the money went back, so the cards it bought are voided. Won
4065
+ // (or closed on a warning): the dispute's freeze lifts; a freeze a
4066
+ // fraud warning or review put on stays for the merchant (AGL-3363).
4067
+ await applyOrderGiftCardRisk(snapshot, String((_ref88 = dispute == null ? void 0 : dispute.status) != null ? _ref88 : '') === 'lost' ? {
4068
+ kind: 'void',
4069
+ reason: 'dispute-lost'
4070
+ } : {
4071
+ kind: 'release',
4072
+ reason: 'dispute'
4073
+ });
4000
4074
  const settled = await recordDisputeClosed(snapshot, dispute);
4001
4075
  if (settled.recorded && hostId) {
4002
4076
  await notifyHostManagers(hostId, {
@@ -4054,7 +4128,7 @@ async function findOrderForDispute(paymentIntentId) {
4054
4128
  // transiently-failed reversal gets its retry — and gated inside on
4055
4129
  // the order's own `reversedTransferCents` marker, so it runs at most
4056
4130
  // once per dispute however many times Stripe delivers.
4057
- if (String((_ref86 = dispute == null ? void 0 : dispute.status) != null ? _ref86 : '') === 'lost') {
4131
+ if (String((_ref89 = dispute == null ? void 0 : dispute.status) != null ? _ref89 : '') === 'lost') {
4058
4132
  const pulledBackCents = await reverseSellerShare(snapshot.ref, dispute);
4059
4133
  // The merchant-experience half of the AGL-1794 decision, and not an
4060
4134
  // optional extra: the reversal takes money out of the connected
@@ -4084,7 +4158,7 @@ async function findOrderForDispute(paymentIntentId) {
4084
4158
  return _extends({
4085
4159
  claimed
4086
4160
  }, snapshot ? {
4087
- hostId: String((_ref84 = (_snapshot_ref_parent_parent1 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent1.id) != null ? _ref84 : '')
4161
+ hostId: String((_ref86 = (_snapshot_ref_parent_parent2 = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent2.id) != null ? _ref86 : '')
4088
4162
  } : {});
4089
4163
  }
4090
4164
  };