@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.
- package/package.json +13 -13
- package/src/lib/components/console/gift-cards-card.component.js +34 -2
- package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
- package/src/lib/model/commerce-gift-cards.d.ts +30 -0
- package/src/lib/model/commerce-gift-cards.js +39 -1
- package/src/lib/model/commerce-gift-cards.js.map +1 -1
- package/src/lib/server/billing-webhook.js +180 -106
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.js +46 -8
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/checkout.js +23 -5
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/download.js +12 -1
- package/src/lib/server/download.js.map +1 -1
- package/src/lib/server/draft-order.js +5 -2
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/feed.js +12 -2
- package/src/lib/server/feed.js.map +1 -1
- package/src/lib/server/gift-card-risk.d.ts +112 -0
- package/src/lib/server/gift-card-risk.js +220 -0
- package/src/lib/server/gift-card-risk.js.map +1 -0
- package/src/lib/server/gift-cards.js +37 -2
- package/src/lib/server/gift-cards.js.map +1 -1
- package/src/lib/server/paid-media-delivery.d.ts +26 -4
- package/src/lib/server/paid-media-delivery.js +36 -9
- package/src/lib/server/paid-media-delivery.js.map +1 -1
- package/src/lib/server/recurring-charges.d.ts +44 -0
- package/src/lib/server/recurring-charges.js +65 -0
- package/src/lib/server/recurring-charges.js.map +1 -0
- package/src/lib/server/refund.js +24 -0
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/reserve.js +14 -5
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/stream.js +12 -1
- package/src/lib/server/stream.js.map +1 -1
- package/src/lib/server/subscription-portal.js +13 -5
- package/src/lib/server/subscription-portal.js.map +1 -1
- package/src/lib/server/supplier-outbox.js +22 -7
- package/src/lib/server/supplier-outbox.js.map +1 -1
- package/src/lib/server.js +15 -3
- 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
|
|
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
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
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 (
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
-
|
|
1494
|
-
transferCents,
|
|
1495
|
-
alreadyReversedCents
|
|
1505
|
+
reason: outcome.reason
|
|
1496
1506
|
});
|
|
1497
|
-
await settle(0, null,
|
|
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
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
|
3902
|
-
var
|
|
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((
|
|
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,
|
|
3955
|
-
var
|
|
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
|
|
3981
|
-
var
|
|
3982
|
-
const hostId = String((
|
|
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
|
|
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((
|
|
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((
|
|
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
|
};
|