@owlmeans/server-payment 0.1.18-rc.2 → 0.1.18-rc.21
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 +109 -25
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-payment/SKILL.md +475 -52
- package/build/actions/index.d.ts +1 -0
- package/build/actions/index.d.ts.map +1 -1
- package/build/actions/index.js +1 -0
- package/build/actions/index.js.map +1 -1
- package/build/actions/resync-subscriptions.d.ts +3 -0
- package/build/actions/resync-subscriptions.d.ts.map +1 -0
- package/build/actions/resync-subscriptions.js +7 -0
- package/build/actions/resync-subscriptions.js.map +1 -0
- package/build/actions/resync.d.ts +1 -0
- package/build/actions/resync.d.ts.map +1 -1
- package/build/actions/resync.js +7 -3
- package/build/actions/resync.js.map +1 -1
- package/build/actions/webhook.d.ts.map +1 -1
- package/build/actions/webhook.js +6 -3
- package/build/actions/webhook.js.map +1 -1
- package/build/config.d.ts +60 -5
- package/build/config.d.ts.map +1 -1
- package/build/config.js +237 -5
- package/build/config.js.map +1 -1
- package/build/consts.d.ts +75 -3
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +94 -5
- package/build/consts.js.map +1 -1
- package/build/consumer/capture.d.ts +74 -0
- package/build/consumer/capture.d.ts.map +1 -0
- package/build/consumer/capture.js +291 -0
- package/build/consumer/capture.js.map +1 -0
- package/build/consumer/format.d.ts +27 -0
- package/build/consumer/format.d.ts.map +1 -0
- package/build/consumer/format.js +81 -0
- package/build/consumer/format.js.map +1 -0
- package/build/consumer/handlers.d.ts +28 -0
- package/build/consumer/handlers.d.ts.map +1 -0
- package/build/consumer/handlers.js +173 -0
- package/build/consumer/handlers.js.map +1 -0
- package/build/consumer/index.d.ts +7 -0
- package/build/consumer/index.d.ts.map +1 -0
- package/build/consumer/index.js +6 -0
- package/build/consumer/index.js.map +1 -0
- package/build/consumer/mail.d.ts +27 -0
- package/build/consumer/mail.d.ts.map +1 -0
- package/build/consumer/mail.js +314 -0
- package/build/consumer/mail.js.map +1 -0
- package/build/consumer/origin.d.ts +14 -0
- package/build/consumer/origin.d.ts.map +1 -0
- package/build/consumer/origin.js +47 -0
- package/build/consumer/origin.js.map +1 -0
- package/build/consumer/reconcile.d.ts +12 -0
- package/build/consumer/reconcile.d.ts.map +1 -0
- package/build/consumer/reconcile.js +317 -0
- package/build/consumer/reconcile.js.map +1 -0
- package/build/consumer/records.d.ts +78 -0
- package/build/consumer/records.d.ts.map +1 -0
- package/build/consumer/records.js +296 -0
- package/build/consumer/records.js.map +1 -0
- package/build/consumer/service.d.ts +51 -0
- package/build/consumer/service.d.ts.map +1 -0
- package/build/consumer/service.js +760 -0
- package/build/consumer/service.js.map +1 -0
- package/build/consumer/withdrawal.d.ts +57 -0
- package/build/consumer/withdrawal.d.ts.map +1 -0
- package/build/consumer/withdrawal.js +247 -0
- package/build/consumer/withdrawal.js.map +1 -0
- package/build/entitlement.d.ts +10 -0
- package/build/entitlement.d.ts.map +1 -0
- package/build/entitlement.js +69 -0
- package/build/entitlement.js.map +1 -0
- package/build/entrypoints.d.ts +1 -1
- package/build/entrypoints.d.ts.map +1 -1
- package/build/entrypoints.js +2 -1
- package/build/entrypoints.js.map +1 -1
- package/build/gate.d.ts +32 -4
- package/build/gate.d.ts.map +1 -1
- package/build/gate.js +59 -19
- package/build/gate.js.map +1 -1
- package/build/index.d.ts +18 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +15 -3
- package/build/index.js.map +1 -1
- package/build/limit.d.ts +13 -0
- package/build/limit.d.ts.map +1 -0
- package/build/limit.js +47 -0
- package/build/limit.js.map +1 -0
- package/build/model.d.ts +10 -1
- package/build/model.d.ts.map +1 -1
- package/build/model.js +215 -17
- package/build/model.js.map +1 -1
- package/build/observer.d.ts +9 -0
- package/build/observer.d.ts.map +1 -1
- package/build/observer.js +38 -12
- package/build/observer.js.map +1 -1
- package/build/plan.d.ts +22 -0
- package/build/plan.d.ts.map +1 -0
- package/build/plan.js +72 -0
- package/build/plan.js.map +1 -0
- package/build/plugins/checkout-plugins.d.ts +49 -0
- package/build/plugins/checkout-plugins.d.ts.map +1 -0
- package/build/plugins/checkout-plugins.js +124 -0
- package/build/plugins/checkout-plugins.js.map +1 -0
- package/build/plugins/estimate.d.ts +43 -0
- package/build/plugins/estimate.d.ts.map +1 -0
- package/build/plugins/estimate.js +268 -0
- package/build/plugins/estimate.js.map +1 -0
- package/build/plugins/events.d.ts +45 -10
- package/build/plugins/events.d.ts.map +1 -1
- package/build/plugins/events.js +605 -136
- package/build/plugins/events.js.map +1 -1
- package/build/plugins/fx.d.ts +31 -0
- package/build/plugins/fx.d.ts.map +1 -0
- package/build/plugins/fx.js +81 -0
- package/build/plugins/fx.js.map +1 -0
- package/build/plugins/portal.d.ts +37 -0
- package/build/plugins/portal.d.ts.map +1 -0
- package/build/plugins/portal.js +265 -0
- package/build/plugins/portal.js.map +1 -0
- package/build/plugins/refunds.d.ts +33 -0
- package/build/plugins/refunds.d.ts.map +1 -0
- package/build/plugins/refunds.js +80 -0
- package/build/plugins/refunds.js.map +1 -0
- package/build/plugins/stripe.d.ts +36 -4
- package/build/plugins/stripe.d.ts.map +1 -1
- package/build/plugins/stripe.js +492 -97
- package/build/plugins/stripe.js.map +1 -1
- package/build/plugins/webhook-manager.d.ts +46 -0
- package/build/plugins/webhook-manager.d.ts.map +1 -0
- package/build/plugins/webhook-manager.js +231 -0
- package/build/plugins/webhook-manager.js.map +1 -0
- package/build/reconcile.d.ts +15 -0
- package/build/reconcile.d.ts.map +1 -0
- package/build/reconcile.js +88 -0
- package/build/reconcile.js.map +1 -0
- package/build/resource.d.ts +12 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +97 -6
- package/build/resource.js.map +1 -1
- package/build/service.d.ts +28 -3
- package/build/service.d.ts.map +1 -1
- package/build/service.js +146 -19
- package/build/service.js.map +1 -1
- package/build/subscription.d.ts +48 -0
- package/build/subscription.d.ts.map +1 -0
- package/build/subscription.js +176 -0
- package/build/subscription.js.map +1 -0
- package/build/sync.d.ts +28 -1
- package/build/sync.d.ts.map +1 -1
- package/build/sync.js +208 -31
- package/build/sync.js.map +1 -1
- package/build/types.d.ts +1115 -36
- package/build/types.d.ts.map +1 -1
- package/build/usage.d.ts +63 -0
- package/build/usage.d.ts.map +1 -0
- package/build/usage.js +363 -0
- package/build/usage.js.map +1 -0
- package/build/utils.d.ts +52 -1
- package/build/utils.d.ts.map +1 -1
- package/build/utils.js +69 -7
- package/build/utils.js.map +1 -1
- package/package.json +17 -13
- package/src/actions/index.ts +1 -0
- package/src/actions/resync-subscriptions.ts +10 -0
- package/src/actions/resync.ts +6 -3
- package/src/actions/webhook.ts +5 -3
- package/src/config.ts +264 -8
- package/src/consts.ts +114 -6
- package/src/consumer/capture.ts +362 -0
- package/src/consumer/format.ts +90 -0
- package/src/consumer/handlers.ts +211 -0
- package/src/consumer/index.ts +6 -0
- package/src/consumer/mail.ts +368 -0
- package/src/consumer/origin.ts +63 -0
- package/src/consumer/reconcile.ts +329 -0
- package/src/consumer/records.ts +374 -0
- package/src/consumer/service.ts +868 -0
- package/src/consumer/withdrawal.ts +302 -0
- package/src/entitlement.ts +84 -0
- package/src/entrypoints.ts +2 -1
- package/src/gate.ts +88 -21
- package/src/index.ts +24 -3
- package/src/limit.ts +57 -0
- package/src/model.ts +237 -18
- package/src/observer.ts +44 -11
- package/src/plan.ts +89 -0
- package/src/plugins/checkout-plugins.ts +155 -0
- package/src/plugins/estimate.ts +339 -0
- package/src/plugins/events.ts +677 -121
- package/src/plugins/fx.ts +122 -0
- package/src/plugins/portal.ts +306 -0
- package/src/plugins/refunds.ts +108 -0
- package/src/plugins/stripe.ts +581 -96
- package/src/plugins/webhook-manager.ts +270 -0
- package/src/reconcile.ts +103 -0
- package/src/resource.ts +152 -7
- package/src/service.ts +174 -18
- package/src/subscription.ts +231 -0
- package/src/sync.ts +249 -29
- package/src/types.ts +1227 -32
- package/src/usage.ts +453 -0
- package/src/utils.ts +127 -10
- package/tests/checkout-consumer.spec.ts +348 -0
- package/tests/checkout-plugins.spec.ts +164 -0
- package/tests/checkout.spec.ts +184 -83
- package/tests/consumer-events.spec.ts +218 -0
- package/tests/consumer-fixtures.ts +132 -0
- package/tests/consumer-ops.spec.ts +351 -0
- package/tests/consumer-rights.integration.spec.ts +150 -0
- package/tests/consumer-rights.spec.ts +501 -0
- package/tests/context.ts +109 -0
- package/tests/entitlement.spec.ts +103 -0
- package/tests/estimate.spec.ts +240 -0
- package/tests/events.spec.ts +356 -0
- package/tests/fake-stripe.ts +972 -0
- package/tests/gate.spec.ts +62 -72
- package/tests/limit-gate.spec.ts +68 -0
- package/tests/portal.spec.ts +200 -0
- package/tests/protocol.spec.ts +23 -6
- package/tests/sync.spec.ts +114 -0
- package/tests/usage.integration.spec.ts +101 -0
- package/tests/usage.spec.ts +171 -0
- package/tests/webhook-manager.spec.ts +152 -0
package/build/plugins/stripe.js
CHANGED
|
@@ -1,37 +1,70 @@
|
|
|
1
|
-
import { assertCheckoutAmount, chargeAmountMinor, CheckoutPricingMode, PaygateError, ProductError, ProductType, } from '@owlmeans/payment';
|
|
1
|
+
import { assertCheckoutAmount, billingLanguageOf, BillingCountryLocked, chargeAmountMinor, chargeCurrencyOf, CheckoutPricingMode, CONSUMER_RIGHTS_COPY_VERSION, ConsumerRightsError, consumerText, inScope, linksOf, PaygateError, ProductError, ProductType, regionOf, TaxBehavior, WebhookSetupError, } from '@owlmeans/payment';
|
|
2
2
|
import { STRIPE_PAYGATE_ALIAS, STRIPE_SIGNATURE } from '../consts.js';
|
|
3
|
-
import { paygateCustomers, payment,
|
|
4
|
-
import { planLookupKey } from '../sync.js';
|
|
3
|
+
import { compact, consumerRightsOf, errorText, fingerprints, paygateCustomers, payment, stripePricingConfig, } from '../utils.js';
|
|
4
|
+
import { isSoldThrough, planLookupKey } from '../sync.js';
|
|
5
|
+
import { countryName, formatMoney } from '../consumer/format.js';
|
|
6
|
+
import { hasPaid, recordEvent, wasUnlocked } from '../consumer/records.js';
|
|
7
|
+
import { admitCheckout, assertAmountAllowed, narrowAmountFor, releaseAdmitted, sessionTtlOf, } from './checkout-plugins.js';
|
|
5
8
|
import { createEventHandler } from './events.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
import { chargeAmount, settlementAmount } from './fx.js';
|
|
10
|
+
import { stripeWebhookSecrets } from './webhook-manager.js';
|
|
11
|
+
/** Stripe's limit on every `custom_text` message. */
|
|
12
|
+
const CUSTOM_TEXT_MAX = 1200;
|
|
13
|
+
/**
|
|
14
|
+
* A Checkout Session's tax and currency options, entirely driven by the declared `PricingPolicy` —
|
|
15
|
+
* an undeclared one (`DEFAULT_PRICING_POLICY`) reproduces exactly what every session hard-coded
|
|
16
|
+
* before this policy existed: automatic tax and tax-id collection on, no Adaptive Pricing.
|
|
17
|
+
*
|
|
18
|
+
* `customer_update.address` lets automatic tax use the billing address Checkout just collected
|
|
19
|
+
* rather than only a previously saved one; `customer_update.name` lets tax-id collection save the
|
|
20
|
+
* business name it collects. Each is included only for the concern that needs it. Under a country
|
|
21
|
+
* lock the saved address is kept (`address: 'never'`, collection `'auto'`), so tax follows the
|
|
22
|
+
* locked country and Checkout cannot move it.
|
|
23
|
+
*/
|
|
24
|
+
const checkoutOptions = (policy, promotions, flags = {}) => {
|
|
25
|
+
const customerUpdate = {};
|
|
26
|
+
if (policy.tax.automatic)
|
|
27
|
+
customerUpdate.address = flags.locked === true ? 'never' : 'auto';
|
|
28
|
+
if (policy.tax.collectTaxId)
|
|
29
|
+
customerUpdate.name = 'auto';
|
|
30
|
+
return {
|
|
31
|
+
...(policy.tax.automatic
|
|
32
|
+
? { automatic_tax: { enabled: true }, billing_address_collection: flags.locked === true ? 'auto' : 'required' }
|
|
33
|
+
: {}),
|
|
34
|
+
...(policy.tax.collectTaxId ? { tax_id_collection: { enabled: true } } : {}),
|
|
35
|
+
...(Object.keys(customerUpdate).length > 0 ? { customer_update: customerUpdate } : {}),
|
|
36
|
+
...(policy.currency.adaptive === true && flags.adaptive !== false ? { adaptive_pricing: { enabled: true } } : {}),
|
|
37
|
+
allow_promotion_codes: promotions,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
13
40
|
const ensureStripeCustomer = async (ctx, stripe, params) => {
|
|
14
41
|
const resource = paygateCustomers(ctx);
|
|
15
42
|
const existing = await resource.byEntity(params.entityId, STRIPE_PAYGATE_ALIAS);
|
|
16
|
-
if (existing != null) {
|
|
43
|
+
if (existing != null && existing.deletedAt == null) {
|
|
17
44
|
const retrieved = await stripe.customers.retrieve(existing.externalId);
|
|
18
|
-
if (!retrieved.deleted)
|
|
19
|
-
|
|
45
|
+
if (!retrieved.deleted) {
|
|
46
|
+
const customer = retrieved;
|
|
47
|
+
if (params.locale != null && customer.preferred_locales?.[0] !== params.locale) {
|
|
48
|
+
return await stripe.customers.update(customer.id, { preferred_locales: [params.locale] });
|
|
49
|
+
}
|
|
50
|
+
return customer;
|
|
51
|
+
}
|
|
20
52
|
}
|
|
21
53
|
const created = await stripe.customers.create({
|
|
22
54
|
metadata: {
|
|
23
55
|
entityId: params.entityId, ...(params.profileId && { profileId: params.profileId }),
|
|
24
56
|
service: params.service,
|
|
25
57
|
},
|
|
58
|
+
...(params.locale != null ? { preferred_locales: [params.locale] } : {}),
|
|
26
59
|
});
|
|
27
60
|
if (existing != null) {
|
|
28
|
-
|
|
29
|
-
await resource.
|
|
61
|
+
const { deletedAt: _deleted, ...kept } = existing;
|
|
62
|
+
await resource.update({ ...kept, externalId: created.id });
|
|
30
63
|
}
|
|
31
64
|
else {
|
|
32
65
|
await resource.create({
|
|
33
66
|
paygate: STRIPE_PAYGATE_ALIAS, externalId: created.id, entityId: params.entityId,
|
|
34
|
-
profileId: params.profileId,
|
|
67
|
+
...(params.profileId != null ? { profileId: params.profileId } : {}),
|
|
35
68
|
});
|
|
36
69
|
}
|
|
37
70
|
return created;
|
|
@@ -46,6 +79,7 @@ const sharedSession = (customer, params, product, plan, metadata) => ({
|
|
|
46
79
|
customer: customer.id,
|
|
47
80
|
success_url: params.successUrl,
|
|
48
81
|
cancel_url: params.cancelUrl ?? params.successUrl,
|
|
82
|
+
...(params.locale != null ? { locale: params.locale } : {}),
|
|
49
83
|
metadata: {
|
|
50
84
|
pricingMode: plan.pricingMode ?? CheckoutPricingMode.Quantity,
|
|
51
85
|
currency: (plan.currency ?? 'usd').toLowerCase(),
|
|
@@ -57,7 +91,7 @@ const sharedSession = (customer, params, product, plan, metadata) => ({
|
|
|
57
91
|
...metadata,
|
|
58
92
|
},
|
|
59
93
|
});
|
|
60
|
-
export const amountCheckoutLineItem = (product, plan, amountMinor) => {
|
|
94
|
+
export const amountCheckoutLineItem = (product, plan, amountMinor, behavior = TaxBehavior.Exclusive) => {
|
|
61
95
|
if (plan.amountPolicy == null)
|
|
62
96
|
throw new ProductError(`amount-policy:${plan.sku}`);
|
|
63
97
|
assertCheckoutAmount(plan.amountPolicy, amountMinor);
|
|
@@ -66,7 +100,7 @@ export const amountCheckoutLineItem = (product, plan, amountMinor) => {
|
|
|
66
100
|
return {
|
|
67
101
|
lineItem: {
|
|
68
102
|
price_data: {
|
|
69
|
-
product: product.sku, currency, unit_amount: chargeMinor, tax_behavior:
|
|
103
|
+
product: product.sku, currency, unit_amount: chargeMinor, tax_behavior: behavior,
|
|
70
104
|
},
|
|
71
105
|
quantity: 1,
|
|
72
106
|
},
|
|
@@ -79,88 +113,449 @@ export const quantityCheckoutLineItem = (price, policy) => ({
|
|
|
79
113
|
adjustable_quantity: { enabled: true, minimum: policy.minimum, maximum: policy.maximum },
|
|
80
114
|
quantity: policy.default,
|
|
81
115
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
/** The plan an amount/quantity checkout of a consumable product sells: the named one, else the first. */
|
|
117
|
+
export const consumablePlanOf = async (ctx, productSku, planSku) => {
|
|
118
|
+
const product = await payment(ctx).product(productSku);
|
|
119
|
+
const plans = (await payment(ctx).allPlans(product.sku))
|
|
120
|
+
.filter(plan => isSoldThrough(product, plan, STRIPE_PAYGATE_ALIAS));
|
|
121
|
+
if (planSku != null && !plans.some(plan => plan.sku === planSku)) {
|
|
122
|
+
throw new ProductError(`plan:${planSku}`);
|
|
123
|
+
}
|
|
124
|
+
const plan = plans.find(item => item.sku === planSku) ?? plans[0];
|
|
125
|
+
if (plan == null)
|
|
126
|
+
throw new ProductError('plan');
|
|
127
|
+
return { product, plan };
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* The buyer as the consumer-rights policy sees it — a locked profile overrides the declared
|
|
131
|
+
* country (a different one is `BillingCountryLocked`), an organization that already paid before
|
|
132
|
+
* its country was locked is locked lazily from its paygate customer's address, and a locked
|
|
133
|
+
* customer address that no longer carries the locked country refuses (the operator relocks).
|
|
134
|
+
*/
|
|
135
|
+
const buyerOf = async (ctx, stripe, params, catalogueCurrency) => {
|
|
136
|
+
const policy = await payment(ctx).consumerRightsPolicy();
|
|
137
|
+
const rights = policy != null ? consumerRightsOf(ctx) : null;
|
|
138
|
+
let profile = rights != null ? await rights.profile(params.entityId) : null;
|
|
139
|
+
const declared = params.country != null && params.country.trim() !== '' ? params.country.trim().toUpperCase() : undefined;
|
|
140
|
+
if (profile?.locked === true && declared != null && declared !== profile.country) {
|
|
141
|
+
throw new BillingCountryLocked({ country: profile.country, requested: declared });
|
|
142
|
+
}
|
|
143
|
+
let customer = await ensureStripeCustomer(ctx, stripe, params);
|
|
144
|
+
const customerCountry = customer.address?.country?.toUpperCase() ?? undefined;
|
|
145
|
+
if (rights != null && policy?.mechanisms.countryLock === true && profile == null && customerCountry != null
|
|
146
|
+
&& await hasPaid(ctx, params.entityId) && !await wasUnlocked(ctx, params.entityId)) {
|
|
147
|
+
// An organization that paid before countries were locked: its saved address is its country.
|
|
148
|
+
// One an operator unlocked is locked again by its next completed purchase instead.
|
|
149
|
+
profile = await rights.lock(params.entityId, customerCountry, 'customer', { customerId: customer.id });
|
|
150
|
+
}
|
|
151
|
+
if (policy != null && profile == null && declared != null && customerCountry == null) {
|
|
152
|
+
// Preselect the declared country on Checkout's address form; never over a saved address.
|
|
153
|
+
customer = await stripe.customers.update(customer.id, { address: { country: declared } });
|
|
154
|
+
}
|
|
155
|
+
if (profile?.locked === true && declared != null && declared !== profile.country) {
|
|
156
|
+
throw new BillingCountryLocked({ country: profile.country, requested: declared });
|
|
157
|
+
}
|
|
158
|
+
if (profile?.locked === true && customerCountry != null && customerCountry !== profile.country) {
|
|
159
|
+
throw new BillingCountryLocked({ country: profile.country, requested: customerCountry });
|
|
160
|
+
}
|
|
161
|
+
const country = profile?.country ?? declared;
|
|
162
|
+
const region = policy != null ? regionOf(country, policy) : null;
|
|
163
|
+
const settlement = (await stripePricingConfig(ctx))?.settlementCurrency?.toLowerCase();
|
|
164
|
+
const regional = policy?.currencies != null && Object.keys(policy.currencies).length > 0;
|
|
165
|
+
return {
|
|
166
|
+
customer,
|
|
167
|
+
buyer: {
|
|
168
|
+
policy,
|
|
169
|
+
profile,
|
|
170
|
+
...(country != null ? { country } : {}),
|
|
171
|
+
region,
|
|
172
|
+
inScope: policy != null ? inScope(region, country, policy) : false,
|
|
173
|
+
language: params.consumerLanguage ?? profile?.language ?? billingLanguageOf(country, policy),
|
|
174
|
+
chargeCurrency: regional
|
|
175
|
+
? (profile?.currency ?? chargeCurrencyOf(region, policy, settlement ?? catalogueCurrency)).toLowerCase()
|
|
176
|
+
: null,
|
|
177
|
+
addressLocked: profile?.locked === true && customerCountry != null,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* The consumer-rights metadata every session (and its subscription) carries. `termsCollected`
|
|
183
|
+
* says whether Checkout asked for the terms checkbox — `'false'` also after the fallback of a
|
|
184
|
+
* Dashboard without a terms URL.
|
|
185
|
+
*/
|
|
186
|
+
const consumerMetadata = (buyer, params, startRequestId) => buyer.policy == null ? {} : compact({
|
|
187
|
+
region: buyer.region ?? undefined,
|
|
188
|
+
country: buyer.country,
|
|
189
|
+
language: buyer.language,
|
|
190
|
+
termsVersion: buyer.policy.textVersion,
|
|
191
|
+
copyVersion: CONSUMER_RIGHTS_COPY_VERSION,
|
|
192
|
+
termsCollected: String(buyer.policy.mechanisms.checkoutTerms === true),
|
|
193
|
+
ipCountry: params.ipCountry?.toUpperCase(),
|
|
194
|
+
startRequestId,
|
|
195
|
+
});
|
|
196
|
+
const limited = (message, field) => {
|
|
197
|
+
if (message.length > CUSTOM_TEXT_MAX) {
|
|
198
|
+
throw new ConsumerRightsError(`copy:length:${field}`);
|
|
199
|
+
}
|
|
200
|
+
return message;
|
|
201
|
+
};
|
|
202
|
+
/** The terms checkbox and its text, when the policy asks for them on every checkout. */
|
|
203
|
+
const termsOptions = (buyer) => {
|
|
204
|
+
const policy = buyer.policy;
|
|
205
|
+
if (policy?.mechanisms.checkoutTerms !== true) {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
const links = linksOf(policy, buyer.language);
|
|
209
|
+
const withInformation = buyer.inScope && links.withdrawalInformation != null;
|
|
210
|
+
const message = limited(consumerText(buyer.language, `checkout.terms-acceptance.${withInformation ? 'in-scope' : 'other'}`, {
|
|
211
|
+
billingTerms: links.billingTerms, ...(withInformation ? { withdrawalInformation: links.withdrawalInformation } : {}),
|
|
212
|
+
}), 'terms');
|
|
213
|
+
return { consent_collection: { terms_of_service: 'required' }, terms: message };
|
|
214
|
+
};
|
|
215
|
+
const textContextOf = (buyer, currency, extra = {}) => compact({
|
|
216
|
+
language: buyer.language, currency, region: buyer.region, country: buyer.country, ...extra,
|
|
217
|
+
});
|
|
218
|
+
const submitTextOf = (params, context) => {
|
|
219
|
+
if (params.submitText == null) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
const message = typeof params.submitText === 'function' ? params.submitText(context) : params.submitText;
|
|
223
|
+
return message.trim() === '' ? undefined : message;
|
|
224
|
+
};
|
|
225
|
+
/** Whether the synced reusable price of a plan can be charged in `currency` (its default or an option). */
|
|
226
|
+
const priceCarries = async (ctx, product, plan, price, currency) => {
|
|
227
|
+
if (price.currency === currency) {
|
|
228
|
+
return { carries: true, ...(price.unit_amount != null ? { unitAmount: price.unit_amount } : {}) };
|
|
229
|
+
}
|
|
230
|
+
const listed = price.currency_options?.[currency];
|
|
231
|
+
if (listed != null) {
|
|
232
|
+
return { carries: true, ...(listed.unit_amount != null ? { unitAmount: listed.unit_amount } : {}) };
|
|
233
|
+
}
|
|
234
|
+
const synced = (await fingerprints(ctx).bySku(product.sku))?.prices?.find(entry => entry.planSku === plan.sku && entry.priceId === price.id);
|
|
235
|
+
const option = synced?.options.find(entry => entry.currency === currency);
|
|
236
|
+
return option != null ? { carries: true, unitAmount: option.unitAmount } : { carries: false };
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Stripe refuses `consent_collection.terms_of_service` while the account has no terms-of-service URL
|
|
240
|
+
* in its Dashboard (Settings → Public details): an `invalid_request_error` on the param
|
|
241
|
+
* `consent_collection[terms_of_service]` saying "You cannot collect consent to your terms of service
|
|
242
|
+
* unless a URL is set in the Stripe Dashboard …". Matched on the param with a terms message, or on
|
|
243
|
+
* the message alone; any other error type never matches.
|
|
244
|
+
*/
|
|
245
|
+
export const isMissingTermsUrl = (error) => {
|
|
246
|
+
if (error == null || typeof error !== 'object') {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
const typed = error;
|
|
250
|
+
const kind = typed.rawType ?? typed.raw?.type ?? typed.type;
|
|
251
|
+
if (kind != null && kind !== 'invalid_request_error' && kind !== 'StripeInvalidRequestError') {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const param = (typed.param ?? typed.raw?.param ?? '').replace(/\s/g, '');
|
|
255
|
+
const message = typed.message ?? typed.raw?.message ?? '';
|
|
256
|
+
const aboutTerms = /terms of service/i.test(message);
|
|
257
|
+
const onTermsParam = /^consent_collection(\[|\.)terms_of_service\]?$/.test(param);
|
|
258
|
+
return (onTermsParam && aboutTerms) || (aboutTerms && /\burl\b/i.test(message) && /dashboard/i.test(message));
|
|
259
|
+
};
|
|
260
|
+
/** The same session without the terms checkbox and its text — and saying so in its metadata. */
|
|
261
|
+
const withoutTerms = (params) => {
|
|
262
|
+
const { consent_collection: consent, custom_text: customText, ...rest } = params;
|
|
263
|
+
const { terms_of_service: _terms, ...otherConsent } = consent ?? {};
|
|
264
|
+
const { terms_of_service_acceptance: _acceptance, ...otherText } = customText ?? {};
|
|
265
|
+
const marked = (metadata) => metadata != null ? { ...metadata, termsCollected: 'false' } : metadata;
|
|
266
|
+
return {
|
|
267
|
+
...rest,
|
|
268
|
+
...(Object.keys(otherConsent).length > 0 ? { consent_collection: otherConsent } : {}),
|
|
269
|
+
...(Object.keys(otherText).length > 0 ? { custom_text: otherText } : {}),
|
|
270
|
+
metadata: marked(rest.metadata) ?? { termsCollected: 'false' },
|
|
271
|
+
...(rest.subscription_data != null
|
|
272
|
+
? { subscription_data: { ...rest.subscription_data, metadata: marked(rest.subscription_data.metadata) ?? { termsCollected: 'false' } } }
|
|
273
|
+
: {}),
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
/** One console warning per context — a process builds one. */
|
|
277
|
+
const warnedTermsFallback = new WeakSet();
|
|
278
|
+
/**
|
|
279
|
+
* A Dashboard without a terms-of-service URL must never stop a payment: the session is created
|
|
280
|
+
* once more without the checkbox, an operator is told (once), and the degraded checkout is audited
|
|
281
|
+
* as a `checkout-terms-fallback` event of the organization (`externalId` = the session).
|
|
282
|
+
*/
|
|
283
|
+
const createWithoutTerms = async (ctx, stripe, attempt, params, refusal) => {
|
|
284
|
+
const stripeMessage = errorText(refusal);
|
|
285
|
+
if (!warnedTermsFallback.has(ctx)) {
|
|
286
|
+
warnedTermsFallback.add(ctx);
|
|
287
|
+
console.warn('[payment] Stripe refused the terms-of-service checkbox: this account has no terms of service URL. '
|
|
288
|
+
+ 'Checkouts continue WITHOUT the checkbox (metadata termsCollected=false). Operator: set the Billing Terms URL '
|
|
289
|
+
+ 'in the Stripe Dashboard → Settings → Public details (test and live mode alike).');
|
|
290
|
+
}
|
|
291
|
+
const typed = refusal;
|
|
292
|
+
const detail = JSON.stringify(compact({
|
|
293
|
+
message: stripeMessage, code: typed.code ?? typed.raw?.code, param: typed.param ?? typed.raw?.param,
|
|
294
|
+
mode: attempt.mode, productSku: attempt.productSku, planSku: attempt.planSku,
|
|
295
|
+
}));
|
|
296
|
+
try {
|
|
297
|
+
const session = await stripe.checkout.sessions.create(withoutTerms(params));
|
|
298
|
+
await recordEvent(ctx, {
|
|
299
|
+
recordId: attempt.entityId, recordKind: 'checkout', entityId: attempt.entityId, action: 'checkout-terms-fallback',
|
|
300
|
+
ok: false, externalId: session.id, detail,
|
|
301
|
+
});
|
|
302
|
+
return session;
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
await recordEvent(ctx, {
|
|
306
|
+
recordId: attempt.entityId, recordKind: 'checkout', entityId: attempt.entityId, action: 'checkout-terms-fallback',
|
|
307
|
+
ok: false, detail, error: errorText(error),
|
|
308
|
+
});
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* Create the session, then tell the plugins it exists. A plugin's `created` that throws expires
|
|
314
|
+
* the fresh session and releases every admission before the error propagates. A session Stripe
|
|
315
|
+
* refuses for the terms checkbox alone (no terms URL in the Dashboard) is created once more
|
|
316
|
+
* without it — under the same admissions.
|
|
317
|
+
*/
|
|
318
|
+
const createSession = async (ctx, stripe, plugins, attempt, params) => {
|
|
319
|
+
const admitted = await admitCheckout(ctx, plugins, attempt);
|
|
320
|
+
const holders = admitted.filter(entry => entry.plugin.admit != null);
|
|
321
|
+
let session;
|
|
322
|
+
try {
|
|
323
|
+
try {
|
|
324
|
+
session = await stripe.checkout.sessions.create(params);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
if (params.consent_collection?.terms_of_service !== 'required' || !isMissingTermsUrl(error)) {
|
|
328
|
+
throw error;
|
|
109
329
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
throw new PaygateError('session');
|
|
125
|
-
return session.url;
|
|
330
|
+
session = await createWithoutTerms(ctx, stripe, attempt, params, error);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
await releaseAdmitted(ctx, holders, attempt);
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
if (session.url == null) {
|
|
338
|
+
await releaseAdmitted(ctx, holders, attempt, session.id);
|
|
339
|
+
throw new PaygateError('session');
|
|
340
|
+
}
|
|
341
|
+
for (const { plugin, reservationId } of admitted) {
|
|
342
|
+
if (plugin.created == null) {
|
|
343
|
+
continue;
|
|
126
344
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
345
|
+
try {
|
|
346
|
+
await plugin.created(ctx, { ...attempt, sessionId: session.id, url: session.url, ...(reservationId != null ? { reservationId } : {}) });
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
try {
|
|
350
|
+
await stripe.checkout.sessions.expire(session.id);
|
|
351
|
+
}
|
|
352
|
+
catch (expireError) {
|
|
353
|
+
console.error(`[payment] could not expire checkout "${session.id}" after a plugin refused it`, expireError);
|
|
354
|
+
}
|
|
355
|
+
await releaseAdmitted(ctx, holders, attempt, session.id);
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return session.url;
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* A Stripe Checkout URL: an amount or quantity purchase of a consumable product, or a subscription
|
|
363
|
+
* to `planSku` (else the product's first recurring plan). A free plan is never checked out.
|
|
364
|
+
*
|
|
365
|
+
* With a consumer-rights policy: a locked billing country overrides the declared one (another is
|
|
366
|
+
* `BillingCountryLocked`); the charge currency is the profile's, else the region's; an amount
|
|
367
|
+
* checkout is narrowed by the checkout plugins (`CheckoutLimitExceeded`) and charged without FX
|
|
368
|
+
* when its policy currency is the charge currency; a subscription needs a fresh start request
|
|
369
|
+
* bound to its plan; the terms checkbox and the legal submit texts come from the copy.
|
|
370
|
+
*/
|
|
371
|
+
export const createCheckoutLink = async (ctx, stripe, params, plugins = []) => {
|
|
372
|
+
const product = await payment(ctx).product(params.productSku);
|
|
373
|
+
const plans = (await payment(ctx).allPlans(product.sku))
|
|
374
|
+
.filter(plan => isSoldThrough(product, plan, STRIPE_PAYGATE_ALIAS));
|
|
375
|
+
if (params.planSku != null && !plans.some(plan => plan.sku === params.planSku)) {
|
|
376
|
+
throw new ProductError(`plan:${params.planSku}`);
|
|
377
|
+
}
|
|
378
|
+
const consumable = product.type === ProductType.Consumable;
|
|
379
|
+
const plan = consumable
|
|
380
|
+
? plans.find(item => item.sku === params.planSku) ?? plans[0]
|
|
381
|
+
: plans.find(item => item.sku === params.planSku) ?? plans.find(item => item.recurring != null) ?? plans[0];
|
|
382
|
+
if (plan == null)
|
|
383
|
+
throw new ProductError('plan');
|
|
384
|
+
const catalogueCurrency = (plan.amountPolicy?.currency ?? plan.currency ?? 'usd').toLowerCase();
|
|
385
|
+
const { buyer, customer } = await buyerOf(ctx, stripe, params, catalogueCurrency);
|
|
386
|
+
const pricing = await payment(ctx).pricingPolicy();
|
|
387
|
+
const settlement = (await stripePricingConfig(ctx))?.settlementCurrency?.toLowerCase();
|
|
388
|
+
const ttl = sessionTtlOf(plugins);
|
|
389
|
+
const now = new Date();
|
|
390
|
+
const expiresAt = ttl != null ? new Date((Math.floor(now.getTime() / 1000) + ttl) * 1000) : undefined;
|
|
391
|
+
const expiry = expiresAt != null ? { expires_at: Math.floor(expiresAt.getTime() / 1000) } : {};
|
|
392
|
+
const { terms, ...termsParams } = termsOptions(buyer);
|
|
393
|
+
const base = {
|
|
394
|
+
entityId: params.entityId, productSku: product.sku, planSku: plan.sku, at: now,
|
|
395
|
+
...(ttl != null ? { sessionTtlSeconds: ttl } : {}), ...(expiresAt != null ? { expiresAt } : {}),
|
|
396
|
+
};
|
|
397
|
+
if (consumable && plan.pricingMode === CheckoutPricingMode.Amount) {
|
|
398
|
+
if (params.amountMinor == null)
|
|
399
|
+
throw new ProductError('amount');
|
|
400
|
+
const { chargeMinor: sourceChargeMinor, currency: amountCurrency } = amountCheckoutLineItem(product, plan, params.amountMinor, pricing.tax.behavior);
|
|
401
|
+
const view = await narrowAmountFor(ctx, plugins, {
|
|
402
|
+
entityId: params.entityId, productSku: product.sku, planSku: plan.sku,
|
|
403
|
+
base: plan.amountPolicy, at: now,
|
|
404
|
+
});
|
|
405
|
+
assertAmountAllowed(view, params.amountMinor);
|
|
406
|
+
const charged = buyer.chargeCurrency != null
|
|
407
|
+
? await chargeAmount(ctx, stripe, sourceChargeMinor, amountCurrency, buyer.chargeCurrency)
|
|
408
|
+
: await settlementAmount(ctx, stripe, sourceChargeMinor, amountCurrency);
|
|
409
|
+
const adaptive = buyer.chargeCurrency == null || charged.currency === (settlement ?? amountCurrency);
|
|
410
|
+
const context = textContextOf(buyer, charged.currency, { unitAmountMinor: charged.amountMinor });
|
|
411
|
+
const submit = submitTextOf(params, context)
|
|
412
|
+
?? (buyer.policy != null && buyer.inScope && buyer.country != null
|
|
413
|
+
? consumerText(buyer.language, 'checkout.top-up', {
|
|
414
|
+
product: product.title, country: countryName(buyer.country, buyer.language),
|
|
415
|
+
})
|
|
416
|
+
: undefined);
|
|
417
|
+
const customText = compact({
|
|
418
|
+
...(submit != null ? { submit: { message: limited(submit, 'submit') } } : {}),
|
|
419
|
+
...(terms != null ? { terms_of_service_acceptance: { message: terms } } : {}),
|
|
420
|
+
});
|
|
421
|
+
const lineItem = {
|
|
422
|
+
price_data: {
|
|
423
|
+
product: product.sku, currency: charged.currency, unit_amount: charged.amountMinor,
|
|
424
|
+
tax_behavior: pricing.tax.behavior,
|
|
139
425
|
},
|
|
140
|
-
|
|
141
|
-
|
|
426
|
+
quantity: 1,
|
|
427
|
+
};
|
|
428
|
+
return await createSession(ctx, stripe, plugins, {
|
|
429
|
+
...base, mode: 'amount', amountMinor: params.amountMinor, amountCurrency, chargeMinor: charged.amountMinor,
|
|
430
|
+
currency: charged.currency,
|
|
431
|
+
}, {
|
|
432
|
+
mode: 'payment', line_items: [lineItem], invoice_creation: { enabled: true },
|
|
433
|
+
...checkoutOptions(pricing, false, { locked: buyer.addressLocked, adaptive }),
|
|
434
|
+
...termsParams,
|
|
435
|
+
...(Object.keys(customText).length > 0 ? { custom_text: customText } : {}),
|
|
436
|
+
...expiry,
|
|
437
|
+
...sharedSession(customer, params, product, plan, {
|
|
438
|
+
pricingMode: CheckoutPricingMode.Amount,
|
|
439
|
+
currency: charged.currency,
|
|
440
|
+
amountCurrency,
|
|
441
|
+
amountMinor: String(params.amountMinor),
|
|
442
|
+
sourceChargeAmountMinor: String(sourceChargeMinor),
|
|
443
|
+
chargeAmountMinor: String(charged.amountMinor),
|
|
444
|
+
...consumerMetadata(buyer, params),
|
|
445
|
+
}),
|
|
142
446
|
});
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
447
|
+
}
|
|
448
|
+
const price = await findPrice(stripe, product.sku, planLookupKey(product, plan));
|
|
449
|
+
const forced = buyer.chargeCurrency != null
|
|
450
|
+
? await priceCarries(ctx, product, plan, price, buyer.chargeCurrency) : { carries: false };
|
|
451
|
+
if (buyer.chargeCurrency != null && !forced.carries) {
|
|
452
|
+
console.warn(`[payment] plan "${plan.sku}" has no ${buyer.chargeCurrency.toUpperCase()} price; Stripe picks the currency`);
|
|
453
|
+
}
|
|
454
|
+
const currency = forced.carries ? buyer.chargeCurrency : price.currency;
|
|
455
|
+
const adaptive = buyer.chargeCurrency == null || currency === (settlement ?? price.currency);
|
|
456
|
+
// Forced only when the synced price carries it: a session currency the price lacks is refused.
|
|
457
|
+
const currencyParam = forced.carries ? { currency } : {};
|
|
458
|
+
if (consumable) {
|
|
459
|
+
const quantityPolicy = plan.quantityPolicy ?? {
|
|
460
|
+
minimum: plan.minQuantity ?? 1,
|
|
461
|
+
maximum: plan.maxQuantity ?? Math.max(100_000, plan.minQuantity ?? 1),
|
|
462
|
+
default: plan.defaultQuantity ?? plan.minQuantity ?? 1,
|
|
463
|
+
};
|
|
464
|
+
const submit = submitTextOf(params, textContextOf(buyer, currency, compact({ unitAmountMinor: forced.unitAmount ?? price.unit_amount ?? undefined })));
|
|
465
|
+
const customText = compact({
|
|
466
|
+
...(submit != null ? { submit: { message: limited(submit, 'submit') } } : {}),
|
|
467
|
+
...(terms != null ? { terms_of_service_acceptance: { message: terms } } : {}),
|
|
468
|
+
});
|
|
469
|
+
return await createSession(ctx, stripe, plugins, { ...base, mode: 'quantity', currency }, {
|
|
470
|
+
mode: 'payment',
|
|
471
|
+
line_items: [quantityCheckoutLineItem(price, quantityPolicy)],
|
|
472
|
+
invoice_creation: { enabled: true },
|
|
473
|
+
...currencyParam,
|
|
474
|
+
...checkoutOptions(pricing, true, { locked: buyer.addressLocked, adaptive }),
|
|
475
|
+
...termsParams,
|
|
476
|
+
...(Object.keys(customText).length > 0 ? { custom_text: customText } : {}),
|
|
477
|
+
...expiry,
|
|
478
|
+
...sharedSession(customer, params, product, plan, {
|
|
479
|
+
pricingMode: CheckoutPricingMode.Quantity,
|
|
480
|
+
...(forced.carries ? { currency } : {}),
|
|
481
|
+
...consumerMetadata(buyer, params),
|
|
482
|
+
}),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
// A subscription starts services at once: in scope (or unknown and protected) it needs the
|
|
486
|
+
// consumer's express start request, fresh and bound to this organization and plan.
|
|
487
|
+
const start = buyer.policy != null ? await consumerRightsOf(ctx)?.assertStartRequest(params.entityId, plan.sku, params.startRequestId) ?? null : null;
|
|
488
|
+
const subscriptionPaymentMethodTypes = (await stripePricingConfig(ctx))?.subscriptionPaymentMethodTypes;
|
|
489
|
+
const unitAmount = forced.unitAmount ?? price.unit_amount ?? undefined;
|
|
490
|
+
const interval = plan.recurring?.interval;
|
|
491
|
+
const context = textContextOf(buyer, currency, compact({ unitAmountMinor: unitAmount, interval }));
|
|
492
|
+
const renewal = buyer.policy != null && unitAmount != null && interval != null
|
|
493
|
+
? consumerText(buyer.language, `checkout.renewal.${interval}`, {
|
|
494
|
+
price: consumerText(buyer.language, `checkout.price.${pricing.tax.behavior === TaxBehavior.Inclusive ? 'inclusive' : 'exclusive'}`, {
|
|
495
|
+
amount: formatMoney(unitAmount, currency, buyer.language),
|
|
496
|
+
}),
|
|
497
|
+
})
|
|
498
|
+
: undefined;
|
|
499
|
+
const submit = submitTextOf(params, context) ?? renewal;
|
|
500
|
+
const links = buyer.policy != null ? linksOf(buyer.policy, buyer.language) : null;
|
|
501
|
+
const afterSubmit = buyer.policy?.mechanisms.cancellation === true && links?.cancellation != null
|
|
502
|
+
? consumerText(buyer.language, 'checkout.renewal.after-submit', { cancelUrl: links.cancellation })
|
|
503
|
+
: undefined;
|
|
504
|
+
const customText = compact({
|
|
505
|
+
...(submit != null ? { submit: { message: limited(submit, 'submit') } } : {}),
|
|
506
|
+
...(afterSubmit != null ? { after_submit: { message: limited(afterSubmit, 'after-submit') } } : {}),
|
|
507
|
+
...(terms != null ? { terms_of_service_acceptance: { message: terms } } : {}),
|
|
508
|
+
});
|
|
509
|
+
const startRequestId = start?.id ?? undefined;
|
|
510
|
+
const metadata = consumerMetadata(buyer, params, startRequestId);
|
|
511
|
+
return await createSession(ctx, stripe, plugins, { ...base, mode: 'subscription', currency }, {
|
|
512
|
+
mode: 'subscription', line_items: [{ price: price.id, quantity: 1 }],
|
|
513
|
+
...currencyParam,
|
|
514
|
+
...(subscriptionPaymentMethodTypes != null ? { payment_method_types: subscriptionPaymentMethodTypes } : {}),
|
|
515
|
+
...(Object.keys(customText).length > 0 ? { custom_text: customText } : {}),
|
|
516
|
+
subscription_data: {
|
|
517
|
+
metadata: {
|
|
518
|
+
pricingMode: CheckoutPricingMode.Quantity, entityId: params.entityId,
|
|
519
|
+
service: params.service, productSku: product.sku, planSku: plan.sku,
|
|
520
|
+
...(params.profileId != null ? { profileId: params.profileId } : {}),
|
|
521
|
+
...metadata,
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
...checkoutOptions(pricing, true, { locked: buyer.addressLocked, adaptive }),
|
|
525
|
+
...termsParams,
|
|
526
|
+
...expiry,
|
|
527
|
+
...sharedSession(customer, params, product, plan, metadata),
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
/**
|
|
531
|
+
* Verify a Stripe webhook against the configured override secret, then the managed endpoint's
|
|
532
|
+
* stored one, and dispatch it.
|
|
533
|
+
*
|
|
534
|
+
* @throws PaygateError('signature') | WebhookSetupError('secret')
|
|
535
|
+
*/
|
|
536
|
+
export const handleStripeWebhook = async (ctx, stripe, request) => {
|
|
537
|
+
const typed = request;
|
|
538
|
+
const rawBody = typed.original?.rawBody ?? typed.rawBody;
|
|
539
|
+
const signature = typed.headers[STRIPE_SIGNATURE.toLowerCase()];
|
|
540
|
+
if (rawBody == null || typeof signature !== 'string')
|
|
541
|
+
throw new PaygateError('signature');
|
|
542
|
+
const secrets = await stripeWebhookSecrets(ctx);
|
|
543
|
+
if (secrets.length === 0) {
|
|
544
|
+
throw new WebhookSetupError('secret');
|
|
545
|
+
}
|
|
546
|
+
let event = null;
|
|
547
|
+
for (const secret of secrets) {
|
|
548
|
+
try {
|
|
549
|
+
event = await stripe.webhooks.constructEventAsync(rawBody, signature, secret);
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// Try the next secret; a signature none of them verifies is refused below.
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (event == null) {
|
|
557
|
+
throw new PaygateError('signature');
|
|
558
|
+
}
|
|
559
|
+
await createEventHandler(ctx, stripe).process(event);
|
|
165
560
|
};
|
|
166
561
|
//# sourceMappingURL=stripe.js.map
|