@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.
- package/README.md +5 -0
- package/package.json +14 -14
- package/src/lib/components/console/catalog-organization-card.component.js +626 -446
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/payments-settings-card.component.js +1 -1
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
- package/src/lib/components/console/product-editor-dialog.component.js +1127 -846
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/site-member-purchases.component.d.ts +24 -0
- package/src/lib/components/console/site-member-purchases.component.js +339 -0
- package/src/lib/components/console/site-member-purchases.component.js.map +1 -0
- package/src/lib/components/member-signup.js +2 -2
- package/src/lib/components/member-signup.js.map +1 -1
- package/src/lib/components/newsletter-signup.js +2 -2
- package/src/lib/components/newsletter-signup.js.map +1 -1
- package/src/lib/constants/operator-alerts.js +5 -3
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/model/commerce-io.js +1 -1
- package/src/lib/model/commerce-io.js.map +1 -1
- package/src/lib/model/commerce-record-lists.d.ts +31 -0
- package/src/lib/model/commerce-record-lists.js +45 -0
- package/src/lib/model/commerce-record-lists.js.map +1 -0
- package/src/lib/model/commerce-record-routes.d.ts +18 -0
- package/src/lib/model/commerce-record-routes.js +1 -1
- package/src/lib/model/commerce-record-routes.js.map +1 -1
- package/src/lib/model/commerce-search-sources.js +4 -2
- package/src/lib/model/commerce-search-sources.js.map +1 -1
- package/src/lib/model/commerce-topics.d.ts +20 -0
- package/src/lib/model/commerce-topics.js +33 -0
- package/src/lib/model/commerce-topics.js.map +1 -0
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/product-record.d.ts +32 -0
- package/src/lib/model/product-record.js +82 -0
- package/src/lib/model/product-record.js.map +1 -0
- package/src/lib/model/site-member-purchases.d.ts +48 -0
- package/src/lib/model/site-member-purchases.js +50 -0
- package/src/lib/model/site-member-purchases.js.map +1 -0
- package/src/lib/plugin.js +12 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +342 -170
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.js +1 -1
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/gift-cards.js +7 -3
- package/src/lib/server/gift-cards.js.map +1 -1
- package/src/lib/server/low-stock.js +3 -1
- package/src/lib/server/low-stock.js.map +1 -1
- package/src/lib/server/member-post.js +2 -2
- package/src/lib/server/member-post.js.map +1 -1
- package/src/lib/server/membership-admin-password.js +4 -1
- package/src/lib/server/membership-admin-password.js.map +1 -1
- package/src/lib/server/membership-register.js +13 -2
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/newsletter.js +32 -19
- package/src/lib/server/newsletter.js.map +1 -1
- package/src/lib/server/pos-order.js +1 -1
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/process-abandoned.js +4 -2
- package/src/lib/server/process-abandoned.js.map +1 -1
- package/src/lib/server/process-restock.js +7 -5
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/product-record-index.js +5 -37
- package/src/lib/server/product-record-index.js.map +1 -1
- package/src/lib/server/reconcile-stock.js +1 -1
- package/src/lib/server/reconcile-stock.js.map +1 -1
- package/src/lib/server/reserve-stock.js +6 -1
- package/src/lib/server/reserve-stock.js.map +1 -1
- package/src/lib/server/revenue-source.js +3 -3
- package/src/lib/server/revenue-source.js.map +1 -1
- package/src/lib/server/supplier-outbox.js +16 -1
- package/src/lib/server/supplier-outbox.js.map +1 -1
- package/src/lib/server/supplier-update.js +19 -9
- package/src/lib/server/supplier-update.js.map +1 -1
- package/src/lib/starter-templates.d.ts +19 -0
- package/src/lib/starter-templates.js +157 -0
- package/src/lib/starter-templates.js.map +1 -0
- package/src/lib/tenant-emails.js +54 -14
- 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
|
-
|
|
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:
|
|
112
|
-
body:
|
|
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
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
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
|
|
655
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
1899
|
-
|
|
1900
|
-
body:
|
|
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
|
-
|
|
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
|
|
2415
|
-
const cycleQuantity = (
|
|
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
|
|
2441
|
-
const renewalEmail = (
|
|
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
|
-
|
|
2504
|
+
const renewalPrice = `$${(paidCents / 100).toFixed(2)}${interval ? `/${interval}` : ''}`;
|
|
2505
|
+
void notifyHostManagers(invoiceHostId, {
|
|
2447
2506
|
type: 'content.order',
|
|
2448
|
-
title: `Subscription renewed
|
|
2449
|
-
|
|
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
|
|
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((
|
|
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((
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2585
|
-
|
|
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 = (
|
|
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
|
|
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: (
|
|
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
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
const
|
|
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((
|
|
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: (
|
|
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,
|
|
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 = (
|
|
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 ${
|
|
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
|
|
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), (
|
|
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
|
|
2995
|
+
var _ref47, _ref48, _ref49, _ref50;
|
|
2902
2996
|
const receiptSettings = await hostRef.collection('settings').doc('store').get().catch(()=>null);
|
|
2903
|
-
const receiptFooter = String((
|
|
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((
|
|
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((
|
|
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: (
|
|
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((
|
|
3013
|
-
|
|
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
|
|
3016
|
-
|
|
3017
|
-
body: `
|
|
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: (
|
|
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((
|
|
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((
|
|
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
|
|
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((
|
|
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
|
|
3149
|
+
var _ref52, _ref53;
|
|
3051
3150
|
var _object_metadata24, _object_metadata25;
|
|
3052
|
-
const giftCardCents = Number((
|
|
3053
|
-
const holdKey = String((
|
|
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
|
|
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: (
|
|
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, (
|
|
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
|
|
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
|
|
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: (
|
|
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
|
|
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((
|
|
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
|
|
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((
|
|
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((
|
|
3289
|
-
const chargedTotalCents = Number((
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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 — ${
|
|
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 = (
|
|
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
|
|
3450
|
-
var
|
|
3451
|
-
const chargedCents = Number((
|
|
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: (
|
|
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,
|
|
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((
|
|
3476
|
-
const quantity = (
|
|
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
|
|
3638
|
+
var _ref73, _ref74, _line_variantId1;
|
|
3529
3639
|
var _lineProduct_variants_;
|
|
3530
|
-
const lineProductId = String((
|
|
3531
|
-
const soldQty = Math.round(Number((
|
|
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
|
|
3595
|
-
var _object_metadata32, _buyNowLineItems_,
|
|
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((
|
|
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((
|
|
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((
|
|
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 = (
|
|
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
|
-
|
|
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
|
|
3691
|
-
|
|
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
|
|
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
|
-
|
|
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 : (
|
|
3799
|
-
name: (
|
|
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((
|
|
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((
|
|
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,
|
|
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((
|
|
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((
|
|
4009
|
+
await releaseStockHold(hostRef, String((_ref82 = (_object_metadata33 = object.metadata) == null ? void 0 : _object_metadata33.stockHoldKey) != null ? _ref82 : ''));
|
|
3860
4010
|
if (couponCode) {
|
|
3861
|
-
var
|
|
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((
|
|
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,
|
|
3876
|
-
var
|
|
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((
|
|
3879
|
-
const buyerEmail = object == null ? void 0 : (
|
|
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
|
|
3883
|
-
|
|
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':
|
|
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: (
|
|
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,
|
|
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: (
|
|
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
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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
|
|
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((
|
|
4023
|
-
const order = CommerceModel.liftLegacyOrder((
|
|
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,
|
|
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
|
|
4273
|
+
var _ref95;
|
|
4111
4274
|
var _snapshot_ref_parent_parent3;
|
|
4112
|
-
const hostId = String((
|
|
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
|
|
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((
|
|
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((
|
|
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
|
|
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((
|
|
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 —
|
|
4168
|
-
body: `
|
|
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((
|
|
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: `
|
|
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((
|
|
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
|
};
|