@plumbus/payments-stripe 0.2.0
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/CHANGELOG.md +21 -0
- package/README.md +60 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/catalog.d.ts +14 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +286 -0
- package/dist/catalog.js.map +1 -0
- package/dist/charges.d.ts +64 -0
- package/dist/charges.d.ts.map +1 -0
- package/dist/charges.js +339 -0
- package/dist/charges.js.map +1 -0
- package/dist/config-rules.d.ts +7 -0
- package/dist/config-rules.d.ts.map +1 -0
- package/dist/config-rules.js +87 -0
- package/dist/config-rules.js.map +1 -0
- package/dist/diagnose.d.ts +17 -0
- package/dist/diagnose.d.ts.map +1 -0
- package/dist/diagnose.js +237 -0
- package/dist/diagnose.js.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +45 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +38 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +167 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/mapping.d.ts +328 -0
- package/dist/mapping.d.ts.map +1 -0
- package/dist/mapping.js +437 -0
- package/dist/mapping.js.map +1 -0
- package/dist/provider.d.ts +33 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +634 -0
- package/dist/provider.js.map +1 -0
- package/dist/resolve.d.ts +10 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +171 -0
- package/dist/resolve.js.map +1 -0
- package/dist/secrets.d.ts +7 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +33 -0
- package/dist/secrets.js.map +1 -0
- package/dist/testing/index.d.ts +54 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +119 -0
- package/dist/testing/index.js.map +1 -0
- package/instructions/README.md +14 -0
- package/instructions/configure-stripe.md +69 -0
- package/instructions/framework.md +62 -0
- package/instructions/testing.md +41 -0
- package/instructions/webhooks.md +44 -0
- package/package.json +66 -0
package/dist/provider.js
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
// ── stripeProvider() ──
|
|
2
|
+
// Stripe Connect adapter for @plumbus/payments, on Stripe's newest APIs:
|
|
3
|
+
// sellers are Accounts v2 (merchant configuration for direct charges, recipient
|
|
4
|
+
// configuration for destination charges and transfers), payment pages are
|
|
5
|
+
// Checkout Sessions, the platform's own plans use Billing (prices by lookup key,
|
|
6
|
+
// entitlements, meters), account changes arrive as thin v2 events and
|
|
7
|
+
// everything else as snapshot events from the platform and sellers' accounts.
|
|
8
|
+
import { ErrorCode, PlumbusError } from '@plumbus/core';
|
|
9
|
+
import Stripe from 'stripe';
|
|
10
|
+
import { findPricesByLookupKey, reconcileCatalog } from './catalog.js';
|
|
11
|
+
import { cancelCharge, captureCharge, chargeAccount, chargeSavedMethod, checkoutExpiry, checkoutOptionParams, clip, createCheckoutCharge, createInvoiceCharge, customAmountPrice, moneyRouting, onAccount, PRODUCT_NAME_MAX, } from './charges.js';
|
|
12
|
+
import { stripeDefaultResponsibilities, validateStripeConfig } from './config-rules.js';
|
|
13
|
+
import { diagnoseStripe, STRIPE_API_VERSION, STRIPE_DESTINATION_NAMES, setupStripeDestinations, } from './diagnose.js';
|
|
14
|
+
import { withStripeErrors } from './errors.js';
|
|
15
|
+
import { isRelevantStripeEvent, verifyStripeWebhook } from './events.js';
|
|
16
|
+
import { fromResponsibility, mapAccount, mapDispute, mapPaymentMethod, mapPayout, mapPayoutSchedule, mapRefund, mapTransfer, mapSubscription, } from './mapping.js';
|
|
17
|
+
import { listEntitlementKeys, resolveStripeEvent, retrieveSubscription, SUBSCRIPTION_EXPAND, } from './resolve.js';
|
|
18
|
+
import { keyMode, lazySecret } from './secrets.js';
|
|
19
|
+
export { STRIPE_API_VERSION, STRIPE_DESTINATION_NAMES };
|
|
20
|
+
const ACCOUNT_INCLUDE = [
|
|
21
|
+
'configuration.merchant',
|
|
22
|
+
'configuration.recipient',
|
|
23
|
+
'defaults',
|
|
24
|
+
'identity',
|
|
25
|
+
'requirements',
|
|
26
|
+
];
|
|
27
|
+
const COMPONENT_MAP = {
|
|
28
|
+
onboarding: 'account_onboarding',
|
|
29
|
+
account: 'account_management',
|
|
30
|
+
notifications: 'notification_banner',
|
|
31
|
+
payments: 'payments',
|
|
32
|
+
payouts: 'payouts',
|
|
33
|
+
balances: 'balances',
|
|
34
|
+
disputes: 'disputes_list',
|
|
35
|
+
documents: 'documents',
|
|
36
|
+
};
|
|
37
|
+
function evidenceParams(evidence) {
|
|
38
|
+
return {
|
|
39
|
+
...(evidence.productDescription ? { product_description: evidence.productDescription } : {}),
|
|
40
|
+
...(evidence.customerName ? { customer_name: evidence.customerName } : {}),
|
|
41
|
+
...(evidence.customerEmail ? { customer_email_address: evidence.customerEmail } : {}),
|
|
42
|
+
...(evidence.serviceDate ? { service_date: evidence.serviceDate } : {}),
|
|
43
|
+
// The policy fields take files; the text goes into the disclosures.
|
|
44
|
+
...(evidence.refundPolicy ? { refund_policy_disclosure: evidence.refundPolicy } : {}),
|
|
45
|
+
...(evidence.cancellationPolicy
|
|
46
|
+
? { cancellation_policy_disclosure: evidence.cancellationPolicy }
|
|
47
|
+
: {}),
|
|
48
|
+
...(evidence.uncategorizedText ? { uncategorized_text: evidence.uncategorizedText } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function stripeProvider(options) {
|
|
52
|
+
const secretKey = lazySecret(options.secretKey, 'Stripe secret key');
|
|
53
|
+
const webhookSecrets = lazySecret(options.webhookSecrets, 'Stripe webhook secrets');
|
|
54
|
+
const tolerance = options.webhookToleranceSeconds ?? 300;
|
|
55
|
+
let clientPromise;
|
|
56
|
+
// Billing portal configuration per account ('' = the platform), made once when missing.
|
|
57
|
+
const portalConfigurations = new Map();
|
|
58
|
+
function client() {
|
|
59
|
+
clientPromise ??= secretKey()
|
|
60
|
+
.then((key) => {
|
|
61
|
+
keyMode(key);
|
|
62
|
+
return new Stripe(key, {
|
|
63
|
+
apiVersion: STRIPE_API_VERSION,
|
|
64
|
+
maxNetworkRetries: options.maxNetworkRetries ?? 2,
|
|
65
|
+
...(options.timeoutMs ? { timeout: options.timeoutMs } : {}),
|
|
66
|
+
...(options.httpClient ? { httpClient: options.httpClient } : {}),
|
|
67
|
+
...(options.api?.host ? { host: options.api.host } : {}),
|
|
68
|
+
...(options.api?.port ? { port: options.api.port } : {}),
|
|
69
|
+
...(options.api?.protocol ? { protocol: options.api.protocol } : {}),
|
|
70
|
+
appInfo: {
|
|
71
|
+
name: '@plumbus/payments-stripe',
|
|
72
|
+
url: 'https://github.com/plumbus-framework/plumbus',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
})
|
|
76
|
+
.catch((err) => {
|
|
77
|
+
clientPromise = undefined;
|
|
78
|
+
throw err;
|
|
79
|
+
});
|
|
80
|
+
return clientPromise;
|
|
81
|
+
}
|
|
82
|
+
async function retrieveAccount(accountId) {
|
|
83
|
+
const stripe = await client();
|
|
84
|
+
const account = await stripe.v2.core.accounts.retrieve(accountId, {
|
|
85
|
+
include: [...ACCOUNT_INCLUDE],
|
|
86
|
+
});
|
|
87
|
+
return mapAccount(account);
|
|
88
|
+
}
|
|
89
|
+
/** A portal configuration: the account's default, or one made for it the first time. */
|
|
90
|
+
function portalConfiguration(stripe, accountId) {
|
|
91
|
+
const key = accountId ?? '';
|
|
92
|
+
let found = portalConfigurations.get(key);
|
|
93
|
+
if (!found) {
|
|
94
|
+
found = (async () => {
|
|
95
|
+
const onIt = onAccount(accountId);
|
|
96
|
+
const [byDefault] = (await stripe.billingPortal.configurations.list({ is_default: true, limit: 1 }, onIt)).data;
|
|
97
|
+
if (byDefault)
|
|
98
|
+
return null;
|
|
99
|
+
const [active] = (await stripe.billingPortal.configurations.list({ active: true, limit: 1 }, onIt)).data;
|
|
100
|
+
if (active)
|
|
101
|
+
return active.id;
|
|
102
|
+
const created = await stripe.billingPortal.configurations.create({
|
|
103
|
+
name: 'Plumbus client portal',
|
|
104
|
+
features: {
|
|
105
|
+
invoice_history: { enabled: true },
|
|
106
|
+
payment_method_update: { enabled: true },
|
|
107
|
+
customer_update: { enabled: true, allowed_updates: ['email', 'address', 'phone'] },
|
|
108
|
+
subscription_cancel: { enabled: true, mode: 'at_period_end' },
|
|
109
|
+
},
|
|
110
|
+
}, onIt);
|
|
111
|
+
return created.id;
|
|
112
|
+
})().catch((err) => {
|
|
113
|
+
portalConfigurations.delete(key);
|
|
114
|
+
throw err;
|
|
115
|
+
});
|
|
116
|
+
portalConfigurations.set(key, found);
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
async function lookupPrices(stripe, lookupKeys, onIt) {
|
|
121
|
+
if (lookupKeys.length === 0)
|
|
122
|
+
return new Map();
|
|
123
|
+
const prices = await findPricesByLookupKey(stripe, lookupKeys, onIt);
|
|
124
|
+
const missing = lookupKeys.filter((key) => !prices.get(key)?.active);
|
|
125
|
+
if (missing.length > 0) {
|
|
126
|
+
throw new PlumbusError(ErrorCode.Validation, `No active Stripe price has the lookup key ${missing.join(', ')}; run plumbus payments catalog sync`, { reason: 'stripe_price_missing', lookupKeys: missing });
|
|
127
|
+
}
|
|
128
|
+
return new Map([...prices].map(([key, price]) => [key, price.id]));
|
|
129
|
+
}
|
|
130
|
+
const provider = {
|
|
131
|
+
id: 'stripe',
|
|
132
|
+
displayName: 'Stripe',
|
|
133
|
+
publishableKey: options.publishableKey ?? null,
|
|
134
|
+
client,
|
|
135
|
+
defaultResponsibilities: stripeDefaultResponsibilities,
|
|
136
|
+
validateConfig: validateStripeConfig,
|
|
137
|
+
async resolveLivemode() {
|
|
138
|
+
return keyMode(await secretKey()) === 'live';
|
|
139
|
+
},
|
|
140
|
+
isRelevantEvent(event) {
|
|
141
|
+
return isRelevantStripeEvent(event.type);
|
|
142
|
+
},
|
|
143
|
+
// ── Sellers ──
|
|
144
|
+
createMerchantAccount: withStripeErrors(async (input) => {
|
|
145
|
+
const stripe = await client();
|
|
146
|
+
const account = await stripe.v2.core.accounts.create({
|
|
147
|
+
dashboard: input.dashboard,
|
|
148
|
+
...(input.email ? { contact_email: input.email } : {}),
|
|
149
|
+
...(input.displayName ? { display_name: input.displayName } : {}),
|
|
150
|
+
identity: { country: input.country.toLowerCase() },
|
|
151
|
+
configuration: {
|
|
152
|
+
...(input.capabilities.cardPayments
|
|
153
|
+
? { merchant: { capabilities: { card_payments: { requested: true } } } }
|
|
154
|
+
: {}),
|
|
155
|
+
...(input.capabilities.transfers
|
|
156
|
+
? {
|
|
157
|
+
recipient: {
|
|
158
|
+
capabilities: { stripe_balance: { stripe_transfers: { requested: true } } },
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
},
|
|
163
|
+
defaults: {
|
|
164
|
+
...(input.defaultCurrency ? { currency: input.defaultCurrency } : {}),
|
|
165
|
+
responsibilities: {
|
|
166
|
+
fees_collector: fromResponsibility(input.feesCollector),
|
|
167
|
+
losses_collector: fromResponsibility(input.lossesCollector),
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
metadata: input.metadata,
|
|
171
|
+
include: [...ACCOUNT_INCLUDE],
|
|
172
|
+
}, { idempotencyKey: input.idempotencyKey });
|
|
173
|
+
return mapAccount(account);
|
|
174
|
+
}),
|
|
175
|
+
retrieveMerchantAccount: withStripeErrors(retrieveAccount),
|
|
176
|
+
createOnboardingLink: withStripeErrors(async (input) => {
|
|
177
|
+
const stripe = await client();
|
|
178
|
+
// Onboard every configuration the account has (merchant, recipient, or both).
|
|
179
|
+
const account = await stripe.v2.core.accounts.retrieve(input.accountId);
|
|
180
|
+
const configurations = (account.applied_configurations ?? []).filter((c) => c === 'merchant' || c === 'recipient');
|
|
181
|
+
const link = await stripe.v2.core.accountLinks.create({
|
|
182
|
+
account: input.accountId,
|
|
183
|
+
use_case: {
|
|
184
|
+
type: 'account_onboarding',
|
|
185
|
+
account_onboarding: {
|
|
186
|
+
configurations: configurations.length > 0 ? configurations : ['merchant'],
|
|
187
|
+
refresh_url: input.refreshUrl,
|
|
188
|
+
return_url: input.returnUrl,
|
|
189
|
+
collection_options: {
|
|
190
|
+
fields: input.collectEventuallyDue ? 'eventually_due' : 'currently_due',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
return { url: link.url, expiresAt: new Date(link.expires_at) };
|
|
196
|
+
}),
|
|
197
|
+
createMerchantSession: withStripeErrors(async (input) => {
|
|
198
|
+
const stripe = await client();
|
|
199
|
+
const components = {};
|
|
200
|
+
for (const component of input.components) {
|
|
201
|
+
const key = COMPONENT_MAP[component];
|
|
202
|
+
if (component === 'payments' || component === 'disputes') {
|
|
203
|
+
components[key] = {
|
|
204
|
+
enabled: true,
|
|
205
|
+
features: {
|
|
206
|
+
refund_management: input.allowRefunds,
|
|
207
|
+
dispute_management: input.allowDisputeManagement,
|
|
208
|
+
capture_payments: false,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
components[key] = { enabled: true };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const session = await stripe.accountSessions.create({
|
|
217
|
+
account: input.accountId,
|
|
218
|
+
components: components,
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
clientSecret: session.client_secret,
|
|
222
|
+
expiresAt: new Date(session.expires_at * 1000),
|
|
223
|
+
publishableKey: options.publishableKey ?? null,
|
|
224
|
+
};
|
|
225
|
+
}),
|
|
226
|
+
createDashboardLink: withStripeErrors(async (input) => {
|
|
227
|
+
if (input.dashboard === 'full') {
|
|
228
|
+
// Full-dashboard sellers sign in to their own Stripe account.
|
|
229
|
+
return { url: 'https://dashboard.stripe.com/' };
|
|
230
|
+
}
|
|
231
|
+
if (input.dashboard === 'express') {
|
|
232
|
+
const stripe = await client();
|
|
233
|
+
const link = await stripe.accounts.createLoginLink(input.accountId);
|
|
234
|
+
return { url: link.url };
|
|
235
|
+
}
|
|
236
|
+
throw new PlumbusError(ErrorCode.Conflict, 'This seller has no Stripe dashboard', {
|
|
237
|
+
reason: 'stripe_no_dashboard',
|
|
238
|
+
});
|
|
239
|
+
}),
|
|
240
|
+
// ── Charges ──
|
|
241
|
+
createClient: withStripeErrors(async (input) => {
|
|
242
|
+
const stripe = await client();
|
|
243
|
+
const customer = await stripe.customers.create({
|
|
244
|
+
...(input.email ? { email: input.email } : {}),
|
|
245
|
+
...(input.name ? { name: input.name } : {}),
|
|
246
|
+
metadata: input.metadata,
|
|
247
|
+
}, { ...onAccount(input.sellerAccountId), idempotencyKey: input.idempotencyKey });
|
|
248
|
+
return { clientId: customer.id };
|
|
249
|
+
}),
|
|
250
|
+
createCharge: withStripeErrors(async (input) => createCheckoutCharge(await client(), input)),
|
|
251
|
+
createInvoiceCharge: withStripeErrors(async (input) => createInvoiceCharge(await client(), input)),
|
|
252
|
+
chargeSavedMethod: withStripeErrors(async (input) => chargeSavedMethod(await client(), input)),
|
|
253
|
+
captureCharge: withStripeErrors(async (input) => captureCharge(await client(), input)),
|
|
254
|
+
cancelCharge: withStripeErrors(async (input) => cancelCharge(await client(), input)),
|
|
255
|
+
createRefund: withStripeErrors(async (input) => {
|
|
256
|
+
const stripe = await client();
|
|
257
|
+
const { flow } = input.routing;
|
|
258
|
+
const refund = await stripe.refunds.create({
|
|
259
|
+
payment_intent: input.paymentId,
|
|
260
|
+
...(input.amount ? { amount: input.amount } : {}),
|
|
261
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
262
|
+
// Platform charges carry no application fee and no transfer.
|
|
263
|
+
...(flow !== 'platform' ? { refund_application_fee: input.refundPlatformFee } : {}),
|
|
264
|
+
...(flow === 'destination' && input.reverseTransfer ? { reverse_transfer: true } : {}),
|
|
265
|
+
metadata: input.metadata,
|
|
266
|
+
}, {
|
|
267
|
+
...onAccount(chargeAccount(input.routing)),
|
|
268
|
+
idempotencyKey: input.idempotencyKey,
|
|
269
|
+
});
|
|
270
|
+
return mapRefund(refund);
|
|
271
|
+
}),
|
|
272
|
+
findRefund: withStripeErrors(async (input) => {
|
|
273
|
+
const stripe = await client();
|
|
274
|
+
const refunds = await stripe.refunds.list({ payment_intent: input.paymentId, limit: 100 }, onAccount(chargeAccount(input.routing)));
|
|
275
|
+
const found = refunds.data.find((r) => r.metadata?.plumbus_refund_id === input.reference);
|
|
276
|
+
return found ? mapRefund(found) : null;
|
|
277
|
+
}),
|
|
278
|
+
// ── Saved payment methods and the client portal ──
|
|
279
|
+
createSetupSession: withStripeErrors(async (input) => {
|
|
280
|
+
const stripe = await client();
|
|
281
|
+
const session = await stripe.checkout.sessions.create({
|
|
282
|
+
mode: 'setup',
|
|
283
|
+
customer: input.clientId,
|
|
284
|
+
// Cards need no currency on a setup page.
|
|
285
|
+
payment_method_types: ['card'],
|
|
286
|
+
client_reference_id: input.reference,
|
|
287
|
+
metadata: input.metadata,
|
|
288
|
+
setup_intent_data: { metadata: input.metadata },
|
|
289
|
+
success_url: input.successUrl,
|
|
290
|
+
cancel_url: input.cancelUrl,
|
|
291
|
+
}, { ...onAccount(input.sellerAccountId), idempotencyKey: input.idempotencyKey });
|
|
292
|
+
return {
|
|
293
|
+
id: session.id,
|
|
294
|
+
url: session.url,
|
|
295
|
+
clientSecret: null,
|
|
296
|
+
expiresAt: new Date(session.expires_at * 1000),
|
|
297
|
+
};
|
|
298
|
+
}),
|
|
299
|
+
listPaymentMethods: withStripeErrors(async (input) => {
|
|
300
|
+
const stripe = await client();
|
|
301
|
+
const methods = await stripe.customers.listPaymentMethods(input.clientId, { limit: 100 }, onAccount(input.sellerAccountId));
|
|
302
|
+
return methods.data.map((m) => mapPaymentMethod(m));
|
|
303
|
+
}),
|
|
304
|
+
detachPaymentMethod: withStripeErrors(async (input) => {
|
|
305
|
+
const stripe = await client();
|
|
306
|
+
await stripe.paymentMethods.detach(input.methodId, {}, onAccount(input.sellerAccountId));
|
|
307
|
+
}),
|
|
308
|
+
createPortalSession: withStripeErrors(async (input) => {
|
|
309
|
+
const stripe = await client();
|
|
310
|
+
const configuration = await portalConfiguration(stripe, input.sellerAccountId);
|
|
311
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
312
|
+
customer: input.clientId,
|
|
313
|
+
return_url: input.returnUrl,
|
|
314
|
+
...(configuration ? { configuration } : {}),
|
|
315
|
+
}, onAccount(input.sellerAccountId));
|
|
316
|
+
return { url: session.url };
|
|
317
|
+
}),
|
|
318
|
+
// ── Subscriptions ──
|
|
319
|
+
createSubscriptionCheckout: withStripeErrors(async (input) => {
|
|
320
|
+
const stripe = await client();
|
|
321
|
+
const onIt = onAccount(chargeAccount(input));
|
|
322
|
+
const prices = await lookupPrices(stripe, input.items.flatMap((item) => (item.lookupKey ? [item.lookupKey] : [])), onIt);
|
|
323
|
+
const lineItems = input.items.map((item) => {
|
|
324
|
+
const quantity = item.quantity !== undefined ? { quantity: item.quantity } : {};
|
|
325
|
+
if (item.priceId)
|
|
326
|
+
return { price: item.priceId, ...quantity };
|
|
327
|
+
if (item.lookupKey)
|
|
328
|
+
return { price: prices.get(item.lookupKey), ...quantity };
|
|
329
|
+
const inline = item.inline;
|
|
330
|
+
if (!inline) {
|
|
331
|
+
throw new PlumbusError(ErrorCode.Validation, 'A subscription item needs a price', {
|
|
332
|
+
reason: 'stripe_subscription_item_price_missing',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
quantity: item.quantity ?? 1,
|
|
337
|
+
price_data: {
|
|
338
|
+
currency: input.currency,
|
|
339
|
+
unit_amount: inline.unitAmount,
|
|
340
|
+
product_data: { name: clip(inline.name, PRODUCT_NAME_MAX) },
|
|
341
|
+
recurring: { interval: inline.interval, interval_count: inline.intervalCount },
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
const seller = input.sellerAccountId;
|
|
346
|
+
const fee = input.flow !== 'platform' && input.applicationFeePercent
|
|
347
|
+
? { application_fee_percent: input.applicationFeePercent }
|
|
348
|
+
: {};
|
|
349
|
+
const session = await stripe.checkout.sessions.create({
|
|
350
|
+
mode: 'subscription',
|
|
351
|
+
ui_mode: input.ui === 'embedded' ? 'embedded_page' : 'hosted_page',
|
|
352
|
+
line_items: lineItems,
|
|
353
|
+
customer: input.clientId,
|
|
354
|
+
client_reference_id: input.reference,
|
|
355
|
+
metadata: input.metadata,
|
|
356
|
+
subscription_data: {
|
|
357
|
+
metadata: input.metadata,
|
|
358
|
+
...fee,
|
|
359
|
+
...(input.trialDays && input.trialDays > 0
|
|
360
|
+
? { trial_period_days: input.trialDays }
|
|
361
|
+
: {}),
|
|
362
|
+
...(input.flow === 'destination' && seller
|
|
363
|
+
? {
|
|
364
|
+
transfer_data: { destination: seller },
|
|
365
|
+
...(input.onBehalfOf ? { on_behalf_of: seller } : {}),
|
|
366
|
+
}
|
|
367
|
+
: {}),
|
|
368
|
+
},
|
|
369
|
+
...(input.ui === 'embedded'
|
|
370
|
+
? { return_url: input.returnUrl }
|
|
371
|
+
: { success_url: input.successUrl, cancel_url: input.cancelUrl }),
|
|
372
|
+
expires_at: checkoutExpiry(input.expiresAt),
|
|
373
|
+
...checkoutOptionParams(input.options, 'subscription', true),
|
|
374
|
+
}, { ...onIt, idempotencyKey: input.idempotencyKey });
|
|
375
|
+
return {
|
|
376
|
+
id: session.id,
|
|
377
|
+
url: session.url,
|
|
378
|
+
clientSecret: session.client_secret,
|
|
379
|
+
expiresAt: new Date(session.expires_at * 1000),
|
|
380
|
+
};
|
|
381
|
+
}),
|
|
382
|
+
retrieveSubscription: withStripeErrors(async (input) => retrieveSubscription(await client(), input.subscriptionId, onAccount(input.sellerAccountId))),
|
|
383
|
+
updateSubscription: withStripeErrors(async (input) => {
|
|
384
|
+
const stripe = await client();
|
|
385
|
+
const onIt = onAccount(input.sellerAccountId);
|
|
386
|
+
const prices = await lookupPrices(stripe, (input.items ?? []).flatMap((item) => (item.lookupKey ? [item.lookupKey] : [])), onIt);
|
|
387
|
+
const items = input.items?.map((item) => ({
|
|
388
|
+
...(item.itemId ? { id: item.itemId } : {}),
|
|
389
|
+
...(item.priceId ? { price: item.priceId } : {}),
|
|
390
|
+
...(item.lookupKey ? { price: prices.get(item.lookupKey) } : {}),
|
|
391
|
+
...(item.quantity !== undefined ? { quantity: item.quantity } : {}),
|
|
392
|
+
...(item.deleted ? { deleted: true } : {}),
|
|
393
|
+
}));
|
|
394
|
+
const updated = await stripe.subscriptions.update(input.subscriptionId, {
|
|
395
|
+
...(items ? { items } : {}),
|
|
396
|
+
...(input.cancelAtPeriodEnd !== undefined
|
|
397
|
+
? { cancel_at_period_end: input.cancelAtPeriodEnd }
|
|
398
|
+
: {}),
|
|
399
|
+
proration_behavior: input.prorate ? 'create_prorations' : 'none',
|
|
400
|
+
expand: [...SUBSCRIPTION_EXPAND],
|
|
401
|
+
}, { ...onIt, idempotencyKey: input.idempotencyKey });
|
|
402
|
+
return mapSubscription(updated);
|
|
403
|
+
}),
|
|
404
|
+
cancelSubscription: withStripeErrors(async (input) => {
|
|
405
|
+
const stripe = await client();
|
|
406
|
+
const requestOptions = {
|
|
407
|
+
...onAccount(input.sellerAccountId),
|
|
408
|
+
idempotencyKey: input.idempotencyKey,
|
|
409
|
+
};
|
|
410
|
+
const canceled = input.atPeriodEnd
|
|
411
|
+
? await stripe.subscriptions.update(input.subscriptionId, { cancel_at_period_end: true, expand: [...SUBSCRIPTION_EXPAND] }, requestOptions)
|
|
412
|
+
: await stripe.subscriptions.cancel(input.subscriptionId, { expand: [...SUBSCRIPTION_EXPAND] }, requestOptions);
|
|
413
|
+
return mapSubscription(canceled);
|
|
414
|
+
}),
|
|
415
|
+
// ── Payment links ──
|
|
416
|
+
createPaymentLink: withStripeErrors(async (input) => {
|
|
417
|
+
const stripe = await client();
|
|
418
|
+
const onIt = onAccount(chargeAccount(input));
|
|
419
|
+
const idempotencyKey = `plumbus-link:${input.reference}`;
|
|
420
|
+
const lineItems = input.customAmount
|
|
421
|
+
? [
|
|
422
|
+
{
|
|
423
|
+
quantity: 1,
|
|
424
|
+
price: (await customAmountPrice(stripe, {
|
|
425
|
+
currency: input.currency,
|
|
426
|
+
name: input.items[0]?.name ?? 'Payment',
|
|
427
|
+
customAmount: input.customAmount,
|
|
428
|
+
}, { ...onIt, idempotencyKey: `${idempotencyKey}:price` })).id,
|
|
429
|
+
},
|
|
430
|
+
]
|
|
431
|
+
: input.items.map((item) => ({
|
|
432
|
+
quantity: item.quantity,
|
|
433
|
+
price_data: {
|
|
434
|
+
currency: input.currency,
|
|
435
|
+
unit_amount: item.unitAmount,
|
|
436
|
+
product_data: {
|
|
437
|
+
name: clip(item.name, PRODUCT_NAME_MAX),
|
|
438
|
+
...(item.description ? { description: item.description } : {}),
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
...(item.adjustableQuantity
|
|
442
|
+
? {
|
|
443
|
+
adjustable_quantity: {
|
|
444
|
+
enabled: true,
|
|
445
|
+
minimum: item.adjustableQuantity.minimum,
|
|
446
|
+
maximum: item.adjustableQuantity.maximum,
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
: {}),
|
|
450
|
+
}));
|
|
451
|
+
const { transfer_group: transferGroup, ...money } = moneyRouting(input, input.platformFeeAmount);
|
|
452
|
+
const { options: page } = input;
|
|
453
|
+
const link = await stripe.paymentLinks.create({
|
|
454
|
+
line_items: lineItems,
|
|
455
|
+
metadata: input.metadata,
|
|
456
|
+
payment_intent_data: {
|
|
457
|
+
metadata: input.metadata,
|
|
458
|
+
...(transferGroup ? { transfer_group: transferGroup } : {}),
|
|
459
|
+
...(page.statementDescriptorSuffix
|
|
460
|
+
? { statement_descriptor_suffix: page.statementDescriptorSuffix }
|
|
461
|
+
: {}),
|
|
462
|
+
},
|
|
463
|
+
...money,
|
|
464
|
+
...(input.completedUrl
|
|
465
|
+
? { after_completion: { type: 'redirect', redirect: { url: input.completedUrl } } }
|
|
466
|
+
: {}),
|
|
467
|
+
...(page.allowPromotionCodes ? { allow_promotion_codes: true } : {}),
|
|
468
|
+
...(page.automaticTax ? { automatic_tax: { enabled: true } } : {}),
|
|
469
|
+
...(page.billingAddress ? { billing_address_collection: page.billingAddress } : {}),
|
|
470
|
+
...(page.phone ? { phone_number_collection: { enabled: true } } : {}),
|
|
471
|
+
...(page.shippingCountries?.length
|
|
472
|
+
? {
|
|
473
|
+
shipping_address_collection: {
|
|
474
|
+
allowed_countries: page.shippingCountries,
|
|
475
|
+
},
|
|
476
|
+
}
|
|
477
|
+
: {}),
|
|
478
|
+
...(page.submitType ? { submit_type: page.submitType } : {}),
|
|
479
|
+
}, { ...onIt, idempotencyKey });
|
|
480
|
+
return { id: link.id, url: link.url, active: link.active };
|
|
481
|
+
}),
|
|
482
|
+
updatePaymentLink: withStripeErrors(async (input) => {
|
|
483
|
+
const stripe = await client();
|
|
484
|
+
const link = await stripe.paymentLinks.update(input.linkId, { active: input.active }, onAccount(input.sellerAccountId));
|
|
485
|
+
return { id: link.id, url: link.url, active: link.active };
|
|
486
|
+
}),
|
|
487
|
+
// ── Transfers ──
|
|
488
|
+
createTransfer: withStripeErrors(async (input) => {
|
|
489
|
+
const stripe = await client();
|
|
490
|
+
// A transfer tied to a payment waits for that payment's funds (source_transaction = its charge).
|
|
491
|
+
let sourceCharge = null;
|
|
492
|
+
if (input.sourcePaymentId) {
|
|
493
|
+
const intent = await stripe.paymentIntents.retrieve(input.sourcePaymentId);
|
|
494
|
+
sourceCharge =
|
|
495
|
+
typeof intent.latest_charge === 'string'
|
|
496
|
+
? intent.latest_charge
|
|
497
|
+
: (intent.latest_charge?.id ?? null);
|
|
498
|
+
}
|
|
499
|
+
const transfer = await stripe.transfers.create({
|
|
500
|
+
amount: input.amount,
|
|
501
|
+
currency: input.currency,
|
|
502
|
+
destination: input.destinationAccountId,
|
|
503
|
+
...(input.transferGroup ? { transfer_group: input.transferGroup } : {}),
|
|
504
|
+
...(sourceCharge ? { source_transaction: sourceCharge } : {}),
|
|
505
|
+
metadata: {
|
|
506
|
+
...input.metadata,
|
|
507
|
+
...(input.sourcePaymentId ? { plumbus_source_payment_id: input.sourcePaymentId } : {}),
|
|
508
|
+
},
|
|
509
|
+
}, { idempotencyKey: input.idempotencyKey });
|
|
510
|
+
return mapTransfer(transfer);
|
|
511
|
+
}),
|
|
512
|
+
reverseTransfer: withStripeErrors(async (input) => {
|
|
513
|
+
const stripe = await client();
|
|
514
|
+
await stripe.transfers.createReversal(input.transferId, {
|
|
515
|
+
...(input.amount !== undefined ? { amount: input.amount } : {}),
|
|
516
|
+
metadata: input.metadata,
|
|
517
|
+
}, { idempotencyKey: input.idempotencyKey });
|
|
518
|
+
return mapTransfer((await stripe.transfers.retrieve(input.transferId)));
|
|
519
|
+
}),
|
|
520
|
+
// ── Payouts (Balance Settings manage the schedule of v2 accounts) ──
|
|
521
|
+
retrievePayoutSettings: withStripeErrors(async (input) => {
|
|
522
|
+
const stripe = await client();
|
|
523
|
+
const onIt = onAccount(input.accountId);
|
|
524
|
+
const [settings, balance] = await Promise.all([
|
|
525
|
+
stripe.balanceSettings.retrieve({}, onIt),
|
|
526
|
+
stripe.balance.retrieve({}, onIt),
|
|
527
|
+
]);
|
|
528
|
+
return {
|
|
529
|
+
schedule: mapPayoutSchedule(settings),
|
|
530
|
+
instantAvailable: (balance.instant_available ?? []).some((entry) => entry.amount > 0),
|
|
531
|
+
};
|
|
532
|
+
}),
|
|
533
|
+
updatePayoutSchedule: withStripeErrors(async (input) => {
|
|
534
|
+
const stripe = await client();
|
|
535
|
+
const onIt = onAccount(input.accountId);
|
|
536
|
+
const { schedule } = input;
|
|
537
|
+
const settings = await stripe.balanceSettings.update({
|
|
538
|
+
payments: {
|
|
539
|
+
payouts: {
|
|
540
|
+
schedule: {
|
|
541
|
+
interval: schedule.interval,
|
|
542
|
+
...(schedule.interval === 'weekly' && schedule.weeklyAnchor
|
|
543
|
+
? { weekly_payout_days: [schedule.weeklyAnchor] }
|
|
544
|
+
: {}),
|
|
545
|
+
...(schedule.interval === 'monthly' && schedule.monthlyAnchor
|
|
546
|
+
? { monthly_payout_days: [schedule.monthlyAnchor] }
|
|
547
|
+
: {}),
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
...(schedule.delayDays !== undefined
|
|
551
|
+
? {
|
|
552
|
+
settlement_timing: {
|
|
553
|
+
// '' returns the delay to the shortest the account allows.
|
|
554
|
+
delay_days_override: schedule.delayDays === 'minimum' ? '' : schedule.delayDays,
|
|
555
|
+
},
|
|
556
|
+
}
|
|
557
|
+
: {}),
|
|
558
|
+
},
|
|
559
|
+
}, onIt);
|
|
560
|
+
const balance = await stripe.balance.retrieve({}, onIt);
|
|
561
|
+
return {
|
|
562
|
+
schedule: mapPayoutSchedule(settings),
|
|
563
|
+
instantAvailable: (balance.instant_available ?? []).some((entry) => entry.amount > 0),
|
|
564
|
+
};
|
|
565
|
+
}),
|
|
566
|
+
listPayouts: withStripeErrors(async (input) => {
|
|
567
|
+
const stripe = await client();
|
|
568
|
+
const payouts = await stripe.payouts.list({ limit: Math.min(input.limit, 100) }, onAccount(input.accountId));
|
|
569
|
+
return payouts.data.map((p) => mapPayout(p));
|
|
570
|
+
}),
|
|
571
|
+
createPayout: withStripeErrors(async (input) => {
|
|
572
|
+
const stripe = await client();
|
|
573
|
+
const payout = await stripe.payouts.create({
|
|
574
|
+
amount: input.amount,
|
|
575
|
+
currency: input.currency,
|
|
576
|
+
method: input.method,
|
|
577
|
+
metadata: input.metadata,
|
|
578
|
+
}, { ...onAccount(input.accountId), idempotencyKey: input.idempotencyKey });
|
|
579
|
+
return mapPayout(payout);
|
|
580
|
+
}),
|
|
581
|
+
// ── Disputes ──
|
|
582
|
+
updateDispute: withStripeErrors(async (input) => {
|
|
583
|
+
const stripe = await client();
|
|
584
|
+
const dispute = await stripe.disputes.update(input.disputeId, { evidence: evidenceParams(input.evidence), submit: input.submit }, onAccount(input.sellerAccountId));
|
|
585
|
+
return mapDispute(dispute);
|
|
586
|
+
}),
|
|
587
|
+
acceptDispute: withStripeErrors(async (input) => {
|
|
588
|
+
const stripe = await client();
|
|
589
|
+
const dispute = await stripe.disputes.close(input.disputeId, {}, onAccount(input.sellerAccountId));
|
|
590
|
+
return mapDispute(dispute);
|
|
591
|
+
}),
|
|
592
|
+
// ── The platform's own billing ──
|
|
593
|
+
createBillingCustomer: withStripeErrors(async (input) => {
|
|
594
|
+
const stripe = await client();
|
|
595
|
+
const customer = await stripe.customers.create({
|
|
596
|
+
...(input.email ? { email: input.email } : {}),
|
|
597
|
+
...(input.name ? { name: input.name } : {}),
|
|
598
|
+
metadata: input.metadata,
|
|
599
|
+
}, { idempotencyKey: input.idempotencyKey });
|
|
600
|
+
return { customerId: customer.id };
|
|
601
|
+
}),
|
|
602
|
+
syncCatalog: withStripeErrors(async (input) => reconcileCatalog(await client(), input, true)),
|
|
603
|
+
checkCatalog: withStripeErrors(async (input) => reconcileCatalog(await client(), input, false)),
|
|
604
|
+
listEntitlements: withStripeErrors(async (input) => listEntitlementKeys(await client(), input.customerId)),
|
|
605
|
+
recordUsage: withStripeErrors(async (input) => {
|
|
606
|
+
const stripe = await client();
|
|
607
|
+
await stripe.billing.meterEvents.create({
|
|
608
|
+
event_name: input.eventName,
|
|
609
|
+
payload: { stripe_customer_id: input.customerId, value: String(input.value) },
|
|
610
|
+
identifier: input.identifier,
|
|
611
|
+
timestamp: Math.floor(input.timestamp.getTime() / 1000),
|
|
612
|
+
});
|
|
613
|
+
}),
|
|
614
|
+
// ── Webhooks ──
|
|
615
|
+
async verifyWebhook({ rawBody, headers }) {
|
|
616
|
+
return verifyStripeWebhook({
|
|
617
|
+
rawBody,
|
|
618
|
+
headers,
|
|
619
|
+
secrets: await webhookSecrets(),
|
|
620
|
+
toleranceSeconds: tolerance,
|
|
621
|
+
});
|
|
622
|
+
},
|
|
623
|
+
resolveEvent: withStripeErrors(async (event) => resolveStripeEvent(await client(), event, async (accountId) => ({
|
|
624
|
+
kind: 'merchant',
|
|
625
|
+
account: await retrieveAccount(accountId),
|
|
626
|
+
}))),
|
|
627
|
+
async diagnose({ webhookUrl, catalog } = {}) {
|
|
628
|
+
return diagnoseStripe({ client, secretKey, webhookSecrets, webhookUrl, catalog });
|
|
629
|
+
},
|
|
630
|
+
setupWebhooks: withStripeErrors(async ({ url }) => setupStripeDestinations(await client(), url)),
|
|
631
|
+
};
|
|
632
|
+
return provider;
|
|
633
|
+
}
|
|
634
|
+
//# sourceMappingURL=provider.js.map
|