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