@fantastic.dev/better-stripe 0.24.0 → 0.25.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 +7 -0
- package/README.md +7 -1
- package/dist/client/billing/checkout.d.ts +3 -43
- package/dist/client/billing/checkout.d.ts.map +1 -1
- package/dist/client/billing/checkout.js +5 -260
- package/dist/client/billing/checkout.js.map +1 -1
- package/dist/client/billing/checkoutRequest.d.ts +129 -0
- package/dist/client/billing/checkoutRequest.d.ts.map +1 -0
- package/dist/client/billing/checkoutRequest.js +263 -0
- package/dist/client/billing/checkoutRequest.js.map +1 -0
- package/dist/client/billing/preparedCheckout.d.ts +22 -0
- package/dist/client/billing/preparedCheckout.d.ts.map +1 -0
- package/dist/client/billing/preparedCheckout.js +86 -0
- package/dist/client/billing/preparedCheckout.js.map +1 -0
- package/dist/client/checkoutRequests.d.ts +80 -0
- package/dist/client/checkoutRequests.d.ts.map +1 -0
- package/dist/client/checkoutRequests.js +28 -0
- package/dist/client/checkoutRequests.js.map +1 -0
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts +464 -0
- package/dist/client/connect/payoutAdoptionTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutCollection.d.ts +21 -0
- package/dist/client/connect/payoutCollection.d.ts.map +1 -0
- package/dist/client/connect/payoutCollection.js +47 -0
- package/dist/client/connect/payoutCollection.js.map +1 -0
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts +1805 -0
- package/dist/client/connect/payoutExecutionTestHelpers.d.ts.map +1 -0
- package/dist/client/connect/payoutExecutionTestHelpers.js +194 -0
- package/dist/client/connect/payoutExecutionTestHelpers.js.map +1 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts +232 -0
- package/dist/client/connect/payoutSyncTestHelpers.d.ts.map +1 -1
- package/dist/client/connect/payoutWorker.d.ts +48 -0
- package/dist/client/connect/payoutWorker.d.ts.map +1 -0
- package/dist/client/connect/payoutWorker.js +78 -0
- package/dist/client/connect/payoutWorker.js.map +1 -0
- package/dist/client/index.d.ts +4 -11
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -9
- package/dist/client/index.js.map +1 -1
- package/dist/client/payoutPolicy.d.ts +67 -0
- package/dist/client/payoutPolicy.d.ts.map +1 -1
- package/dist/client/payoutPolicy.js +10 -0
- package/dist/client/payoutPolicy.js.map +1 -1
- package/dist/client/readContracts.d.ts +11 -1
- package/dist/client/readContracts.d.ts.map +1 -1
- package/dist/client/readContracts.js +10 -0
- package/dist/client/readContracts.js.map +1 -1
- package/dist/component/_generated/component.d.ts +293 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/core/mutations.d.ts.map +1 -1
- package/dist/component/core/mutations.js +2 -0
- package/dist/component/core/mutations.js.map +1 -1
- package/dist/component/core/resetSafety.d.ts +4 -0
- package/dist/component/core/resetSafety.d.ts.map +1 -0
- package/dist/component/core/resetSafety.js +54 -0
- package/dist/component/core/resetSafety.js.map +1 -0
- package/dist/component/earnings/triggers.d.ts +418 -0
- package/dist/component/earnings/triggers.d.ts.map +1 -1
- package/dist/component/earnings/triggers.js +2 -0
- package/dist/component/earnings/triggers.js.map +1 -1
- package/dist/component/payoutPolicy/schema.d.ts +2 -0
- package/dist/component/payoutPolicy/schema.d.ts.map +1 -1
- package/dist/component/payoutPolicy/schema.js +3 -1
- package/dist/component/payoutPolicy/schema.js.map +1 -1
- package/dist/component/payoutRelease/mutations.js +1 -1
- package/dist/component/payoutRelease/schema.d.ts +1 -0
- package/dist/component/payoutRelease/schema.d.ts.map +1 -1
- package/dist/component/payoutRelease/schema.js +1 -0
- package/dist/component/payoutRelease/schema.js.map +1 -1
- package/dist/component/payoutSnapshots/localState.d.ts +89 -0
- package/dist/component/payoutSnapshots/localState.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/localState.js +186 -0
- package/dist/component/payoutSnapshots/localState.js.map +1 -0
- package/dist/component/payoutSnapshots/mutations.d.ts +108 -0
- package/dist/component/payoutSnapshots/mutations.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/mutations.js +263 -0
- package/dist/component/payoutSnapshots/mutations.js.map +1 -0
- package/dist/component/payoutSnapshots/revisions.d.ts +12 -0
- package/dist/component/payoutSnapshots/revisions.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/revisions.js +173 -0
- package/dist/component/payoutSnapshots/revisions.js.map +1 -0
- package/dist/component/payoutSnapshots/schema.d.ts +285 -0
- package/dist/component/payoutSnapshots/schema.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/schema.js +108 -0
- package/dist/component/payoutSnapshots/schema.js.map +1 -0
- package/dist/component/payoutSnapshots/scope.d.ts +7 -0
- package/dist/component/payoutSnapshots/scope.d.ts.map +1 -0
- package/dist/component/payoutSnapshots/scope.js +8 -0
- package/dist/component/payoutSnapshots/scope.js.map +1 -0
- package/dist/component/payoutWorker/helpers.d.ts +30 -0
- package/dist/component/payoutWorker/helpers.d.ts.map +1 -0
- package/dist/component/payoutWorker/helpers.js +21 -0
- package/dist/component/payoutWorker/helpers.js.map +1 -0
- package/dist/component/payoutWorker/mutations.d.ts +68 -0
- package/dist/component/payoutWorker/mutations.d.ts.map +1 -0
- package/dist/component/payoutWorker/mutations.js +161 -0
- package/dist/component/payoutWorker/mutations.js.map +1 -0
- package/dist/component/payoutWorker/queries.d.ts +123 -0
- package/dist/component/payoutWorker/queries.d.ts.map +1 -0
- package/dist/component/payoutWorker/queries.js +61 -0
- package/dist/component/payoutWorker/queries.js.map +1 -0
- package/dist/component/payoutWorker/schema.d.ts +91 -0
- package/dist/component/payoutWorker/schema.d.ts.map +1 -0
- package/dist/component/payoutWorker/schema.js +27 -0
- package/dist/component/payoutWorker/schema.js.map +1 -0
- package/dist/component/schema.d.ts +232 -0
- package/dist/component/schema.js +10 -0
- package/dist/component/schema.js.map +1 -1
- package/docs/read-contracts.md +48 -0
- package/docs/settlement.md +128 -1
- package/package.json +1 -1
- package/src/client/billing/checkout.ts +11 -359
- package/src/client/billing/checkoutRequest.ts +362 -0
- package/src/client/billing/prepared-checkout.test.ts +430 -0
- package/src/client/billing/preparedCheckout.ts +143 -0
- package/src/client/checkoutRequests.ts +40 -0
- package/src/client/connect/payoutCollection.test.ts +74 -0
- package/src/client/connect/payoutCollection.ts +82 -0
- package/src/client/connect/payoutExecution.test.ts +1 -211
- package/src/client/connect/payoutExecutionTestHelpers.ts +213 -0
- package/src/client/connect/payoutWorker.test.ts +145 -0
- package/src/client/connect/payoutWorker.ts +144 -0
- package/src/client/index.ts +10 -21
- package/src/client/payoutPolicy.ts +26 -1
- package/src/client/readContracts.ts +23 -0
- package/src/component/_generated/api.ts +16 -0
- package/src/component/_generated/component.ts +361 -0
- package/src/component/core/mutations.ts +2 -0
- package/src/component/core/resetSafety.ts +67 -0
- package/src/component/earnings/triggers.ts +2 -0
- package/src/component/payout-reset-safety.test.ts +148 -0
- package/src/component/payout-revisions.test.ts +125 -0
- package/src/component/payout-snapshots.test.ts +395 -0
- package/src/component/payout-worker.test.ts +165 -0
- package/src/component/payoutPolicy/schema.ts +3 -1
- package/src/component/payoutRelease/mutations.ts +1 -1
- package/src/component/payoutRelease/schema.ts +1 -0
- package/src/component/payoutSnapshots/localState.ts +238 -0
- package/src/component/payoutSnapshots/mutations.ts +275 -0
- package/src/component/payoutSnapshots/revisions.ts +247 -0
- package/src/component/payoutSnapshots/schema.ts +125 -0
- package/src/component/payoutSnapshots/scope.ts +7 -0
- package/src/component/payoutWorker/helpers.ts +30 -0
- package/src/component/payoutWorker/mutations.ts +181 -0
- package/src/component/payoutWorker/queries.ts +64 -0
- package/src/component/payoutWorker/schema.ts +26 -0
- package/src/component/public.test.ts +2 -2
- package/src/component/schema.ts +18 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
|
|
3
|
+
import type { SplitRecipient } from "../../component/lib/fees.js";
|
|
4
|
+
import { computeFee, isPercentOnlyFee, validateSplit } from "../core/fees.js";
|
|
5
|
+
import { throwStripeError } from "../errors.js";
|
|
6
|
+
import type { Component, RunCtx } from "../helpers.js";
|
|
7
|
+
import { stripReservedMetadata } from "../helpers.js";
|
|
8
|
+
import type { CheckoutSessionCreateParams } from "../stripe-types.js";
|
|
9
|
+
import type { PlatformFeeConfig } from "../types.js";
|
|
10
|
+
import { componentRef } from "../webhooks/helpers.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Derive the persisted fee/routing fields (BTS-11) from the FINAL Checkout
|
|
14
|
+
* Session request, reading the nested `subscription_data`/`payment_intent_data`
|
|
15
|
+
* that will actually be sent to Stripe. Deriving from the final request (rather
|
|
16
|
+
* than a pre-override draft) keeps the stored row consistent when callers pass
|
|
17
|
+
* `sessionOverrides` that replace those nested params.
|
|
18
|
+
*/
|
|
19
|
+
function deriveFeeRow(params: CheckoutSessionCreateParams): {
|
|
20
|
+
chargeType?: "destination";
|
|
21
|
+
destinationAccountId?: string;
|
|
22
|
+
applicationFeePercent?: number;
|
|
23
|
+
applicationFeeAmount?: number;
|
|
24
|
+
} {
|
|
25
|
+
const dest = (d: unknown): string | undefined =>
|
|
26
|
+
typeof d === "string"
|
|
27
|
+
? d
|
|
28
|
+
: ((d as { id?: string } | null | undefined)?.id ?? undefined);
|
|
29
|
+
|
|
30
|
+
if (params.mode === "subscription") {
|
|
31
|
+
const destination = dest(
|
|
32
|
+
params.subscription_data?.transfer_data?.destination,
|
|
33
|
+
);
|
|
34
|
+
if (!destination) return {};
|
|
35
|
+
const pct = params.subscription_data?.application_fee_percent;
|
|
36
|
+
return {
|
|
37
|
+
chargeType: "destination",
|
|
38
|
+
destinationAccountId: destination,
|
|
39
|
+
...(pct !== undefined ? { applicationFeePercent: pct } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (params.mode === "payment") {
|
|
43
|
+
const destination = dest(
|
|
44
|
+
params.payment_intent_data?.transfer_data?.destination,
|
|
45
|
+
);
|
|
46
|
+
if (!destination) return {};
|
|
47
|
+
const amt = params.payment_intent_data?.application_fee_amount;
|
|
48
|
+
return {
|
|
49
|
+
chargeType: "destination",
|
|
50
|
+
destinationAccountId: destination,
|
|
51
|
+
...(amt !== undefined ? { applicationFeeAmount: amt } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Destination and separate charges use platform prices. Catalog accountId is a
|
|
59
|
+
* store attribution tag, not Stripe ownership (createProduct uses the platform).
|
|
60
|
+
* For tagged products, verify the price in the platform Stripe context before
|
|
61
|
+
* rejecting it. Untagged/unknown catalog entries are validated by Checkout.
|
|
62
|
+
*/
|
|
63
|
+
async function assertPlatformPrice(
|
|
64
|
+
stripe: Stripe,
|
|
65
|
+
component: Component,
|
|
66
|
+
ctx: RunCtx,
|
|
67
|
+
stripePriceId: string,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const price = (await ctx.runQuery(
|
|
70
|
+
componentRef(component, "products/queries/getPriceByStripeId"),
|
|
71
|
+
{ stripePriceId },
|
|
72
|
+
)) as { stripeProductId?: string } | null;
|
|
73
|
+
if (!price?.stripeProductId) return;
|
|
74
|
+
|
|
75
|
+
const product = (await ctx.runQuery(
|
|
76
|
+
componentRef(component, "products/queries/getProductByStripeId"),
|
|
77
|
+
{ stripeProductId: price.stripeProductId },
|
|
78
|
+
)) as { accountId?: string } | null;
|
|
79
|
+
|
|
80
|
+
if (product?.accountId) {
|
|
81
|
+
try {
|
|
82
|
+
await stripe.prices.retrieve(stripePriceId);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Only absence on the platform means the ownership check failed.
|
|
85
|
+
// Auth/network/rate-limit failures must not be misreported as ownership.
|
|
86
|
+
if (
|
|
87
|
+
!error ||
|
|
88
|
+
typeof error !== "object" ||
|
|
89
|
+
!("code" in error) ||
|
|
90
|
+
error.code !== "resource_missing"
|
|
91
|
+
)
|
|
92
|
+
throw error;
|
|
93
|
+
throwStripeError(
|
|
94
|
+
"INVALID_CONFIGURATION",
|
|
95
|
+
`Price ${stripePriceId} is unavailable on the platform account; destination charges require a platform-owned price.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// =============================================================================
|
|
102
|
+
// Checkout methods
|
|
103
|
+
// =============================================================================
|
|
104
|
+
|
|
105
|
+
export async function renderCheckoutSession(
|
|
106
|
+
stripe: Stripe,
|
|
107
|
+
component: Component,
|
|
108
|
+
ctx: RunCtx,
|
|
109
|
+
opts: {
|
|
110
|
+
idempotencyKey?: string;
|
|
111
|
+
userId: string;
|
|
112
|
+
orgId?: string;
|
|
113
|
+
stripePriceId: string;
|
|
114
|
+
mode: "payment" | "subscription" | "setup";
|
|
115
|
+
uiMode?: "embedded" | "redirect" | "custom" | "form";
|
|
116
|
+
quantity?: number;
|
|
117
|
+
returnUrl: string;
|
|
118
|
+
trialDays?: number;
|
|
119
|
+
accountId?: string;
|
|
120
|
+
customerEmail?: string;
|
|
121
|
+
/**
|
|
122
|
+
* The seller/recipient connected account funds are routed to (destination
|
|
123
|
+
* charge). Fees only apply when this is set.
|
|
124
|
+
*/
|
|
125
|
+
destinationAccountId?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Multiple recipients for one sale (store + affiliate[s]). With >1 recipient
|
|
128
|
+
* this routes via separate charges & transfers (no `application_fee`); the
|
|
129
|
+
* transfers are created by the webhook engine. A single-recipient split is
|
|
130
|
+
* treated as a destination charge. Takes precedence over `destinationAccountId`.
|
|
131
|
+
*/
|
|
132
|
+
split?: SplitRecipient[];
|
|
133
|
+
/** Resolved platform fee config (the caller resolves override → default). */
|
|
134
|
+
feeConfig?: PlatformFeeConfig;
|
|
135
|
+
/**
|
|
136
|
+
* Charge total in minor units (one-time payment mode). When known, the
|
|
137
|
+
* fixed `application_fee_amount` is computed up front; otherwise the fee is
|
|
138
|
+
* deferred to the webhook.
|
|
139
|
+
*/
|
|
140
|
+
amount?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Platform-default statement-descriptor suffix (BTS-32), used when the
|
|
143
|
+
* destination account record carries no per-store suffix. The caller
|
|
144
|
+
* (BetterStripe) passes its validated configured default.
|
|
145
|
+
*/
|
|
146
|
+
defaultStatementDescriptorSuffix?: string;
|
|
147
|
+
metadata?: Record<string, string>;
|
|
148
|
+
sessionOverrides?: Record<string, unknown>;
|
|
149
|
+
},
|
|
150
|
+
capturedDescriptor?: { value?: string },
|
|
151
|
+
) {
|
|
152
|
+
const uiMode = opts.uiMode ?? "embedded";
|
|
153
|
+
|
|
154
|
+
// Setup-mode sessions create no charge, so routing/fees are meaningless and
|
|
155
|
+
// would persist an unfunded `chargeType` the webhook engine can't act on.
|
|
156
|
+
if (
|
|
157
|
+
opts.mode === "setup" &&
|
|
158
|
+
(opts.split?.length || opts.destinationAccountId || opts.feeConfig)
|
|
159
|
+
) {
|
|
160
|
+
throwStripeError(
|
|
161
|
+
"INVALID_CONFIGURATION",
|
|
162
|
+
"split/destinationAccountId/fee are not supported for setup-mode Checkout Sessions",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Resolve the routing model. A `split` with >1 recipient uses separate charges
|
|
167
|
+
// & transfers (no application_fee; webhook engine creates the transfers); a
|
|
168
|
+
// single-recipient split is just a destination charge. `split` wins over
|
|
169
|
+
// `destinationAccountId`.
|
|
170
|
+
let isSeparate = false;
|
|
171
|
+
let destinationAccountId = opts.destinationAccountId;
|
|
172
|
+
if (opts.split && opts.split.length > 0) {
|
|
173
|
+
validateSplit(opts.split);
|
|
174
|
+
if (opts.split.length === 1) {
|
|
175
|
+
destinationAccountId = opts.split[0].destinationAccountId;
|
|
176
|
+
} else {
|
|
177
|
+
isSeparate = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Metadata markers the webhook split engine reads to create the transfers.
|
|
181
|
+
// For subscriptions these live on the subscription and are consumed on each
|
|
182
|
+
// invoice.paid; for one-time payments they live on the PaymentIntent.
|
|
183
|
+
const separateMeta = isSeparate
|
|
184
|
+
? {
|
|
185
|
+
bsChargeType: "separate",
|
|
186
|
+
bsSplit: JSON.stringify(opts.split),
|
|
187
|
+
...(opts.feeConfig
|
|
188
|
+
? { bsFeeConfig: JSON.stringify(opts.feeConfig) }
|
|
189
|
+
: {}),
|
|
190
|
+
}
|
|
191
|
+
: {};
|
|
192
|
+
|
|
193
|
+
// Strip the reserved `bs*` namespace from caller metadata so it can't forge
|
|
194
|
+
// webhook instructions (e.g. trigger split transfers without routing).
|
|
195
|
+
const metadata = {
|
|
196
|
+
...stripReservedMetadata(opts.metadata),
|
|
197
|
+
userId: opts.userId,
|
|
198
|
+
...(opts.orgId ? { orgId: opts.orgId } : {}),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Per-store statement descriptor (BTS-32). Destination charges are platform
|
|
202
|
+
// charges (platform = merchant of record), so buyer recognition is
|
|
203
|
+
// suffix-based: the store's stored suffix, else the configured platform
|
|
204
|
+
// default. Resolved from the component account record at session creation.
|
|
205
|
+
let statementDescriptorSuffix: string | undefined;
|
|
206
|
+
if (destinationAccountId && !isSeparate && opts.mode !== "setup") {
|
|
207
|
+
if (capturedDescriptor)
|
|
208
|
+
statementDescriptorSuffix = capturedDescriptor.value;
|
|
209
|
+
else {
|
|
210
|
+
const account = (await ctx.runQuery(
|
|
211
|
+
componentRef(component, "core/queries/getAccountByStripeId"),
|
|
212
|
+
{ stripeAccountId: destinationAccountId },
|
|
213
|
+
)) as { statementDescriptor?: string } | null;
|
|
214
|
+
statementDescriptorSuffix =
|
|
215
|
+
account?.statementDescriptor ?? opts.defaultStatementDescriptorSuffix;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const sessionParams: CheckoutSessionCreateParams = {
|
|
220
|
+
mode: opts.mode,
|
|
221
|
+
line_items: [{ price: opts.stripePriceId, quantity: opts.quantity ?? 1 }],
|
|
222
|
+
metadata,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (opts.mode === "subscription") {
|
|
226
|
+
const subscriptionData: CheckoutSessionCreateParams["subscription_data"] = {
|
|
227
|
+
metadata,
|
|
228
|
+
};
|
|
229
|
+
if (opts.trialDays) {
|
|
230
|
+
subscriptionData.trial_period_days = opts.trialDays;
|
|
231
|
+
}
|
|
232
|
+
if (isSeparate) {
|
|
233
|
+
// Separate charges & transfers: NO transfer_data / application_fee. The
|
|
234
|
+
// split is carried in metadata for the webhook engine to execute.
|
|
235
|
+
subscriptionData.metadata = { ...metadata, ...separateMeta };
|
|
236
|
+
} else if (destinationAccountId) {
|
|
237
|
+
// Single-recipient destination charge: route funds to the seller and take
|
|
238
|
+
// the platform's cut. Percent-only fees map to `application_fee_percent`;
|
|
239
|
+
// percent+fixed/tiered fees can't be expressed that way, so flag the
|
|
240
|
+
// subscription for per-invoice fee computation (handled by WEBHOOK_FEE).
|
|
241
|
+
subscriptionData.transfer_data = { destination: destinationAccountId };
|
|
242
|
+
if (opts.feeConfig) {
|
|
243
|
+
if (isPercentOnlyFee(opts.feeConfig)) {
|
|
244
|
+
subscriptionData.application_fee_percent = opts.feeConfig.percent;
|
|
245
|
+
} else {
|
|
246
|
+
subscriptionData.metadata = {
|
|
247
|
+
...metadata,
|
|
248
|
+
bsFeeMode: "per_invoice",
|
|
249
|
+
bsFeeConfig: JSON.stringify(opts.feeConfig),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (statementDescriptorSuffix) {
|
|
254
|
+
// Subscriptions can't carry a suffix directly; the invoice.created
|
|
255
|
+
// processor reads this marker and sets it on each invoice.
|
|
256
|
+
subscriptionData.metadata = {
|
|
257
|
+
...(subscriptionData.metadata ?? metadata),
|
|
258
|
+
bsStatementDescriptor: statementDescriptorSuffix,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
sessionParams.subscription_data = subscriptionData;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (opts.mode === "payment") {
|
|
266
|
+
const paymentIntentData: CheckoutSessionCreateParams["payment_intent_data"] =
|
|
267
|
+
{ metadata };
|
|
268
|
+
if (isSeparate) {
|
|
269
|
+
paymentIntentData.metadata = { ...metadata, ...separateMeta };
|
|
270
|
+
} else if (destinationAccountId) {
|
|
271
|
+
// Single-recipient destination charge for a one-time purchase. The platform
|
|
272
|
+
// fee is a fixed `application_fee_amount`: compute it when the amount is
|
|
273
|
+
// known; otherwise flag the PI with `bsFeeMode=per_charge` and the webhook
|
|
274
|
+
// collects it at payment_intent.succeeded via a partial reversal of the
|
|
275
|
+
// destination transfer, sized from the amount actually charged (BTS-60).
|
|
276
|
+
paymentIntentData.transfer_data = {
|
|
277
|
+
destination: destinationAccountId,
|
|
278
|
+
};
|
|
279
|
+
if (statementDescriptorSuffix) {
|
|
280
|
+
paymentIntentData.statement_descriptor_suffix =
|
|
281
|
+
statementDescriptorSuffix;
|
|
282
|
+
}
|
|
283
|
+
if (opts.feeConfig) {
|
|
284
|
+
if (opts.amount !== undefined) {
|
|
285
|
+
paymentIntentData.application_fee_amount = computeFee(
|
|
286
|
+
opts.amount,
|
|
287
|
+
opts.feeConfig,
|
|
288
|
+
).feeAmount;
|
|
289
|
+
} else {
|
|
290
|
+
paymentIntentData.metadata = {
|
|
291
|
+
...metadata,
|
|
292
|
+
bsFeeMode: "per_charge",
|
|
293
|
+
bsFeeConfig: JSON.stringify(opts.feeConfig),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
sessionParams.payment_intent_data = paymentIntentData;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (uiMode === "embedded" || uiMode === "custom" || uiMode === "form") {
|
|
302
|
+
sessionParams.ui_mode =
|
|
303
|
+
uiMode === "custom"
|
|
304
|
+
? "elements"
|
|
305
|
+
: uiMode === "form"
|
|
306
|
+
? "form"
|
|
307
|
+
: "embedded_page";
|
|
308
|
+
sessionParams.return_url = opts.returnUrl;
|
|
309
|
+
} else {
|
|
310
|
+
sessionParams.success_url = `${opts.returnUrl}?session_id={CHECKOUT_SESSION_ID}`;
|
|
311
|
+
sessionParams.cancel_url = opts.returnUrl;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (opts.accountId) {
|
|
315
|
+
sessionParams.customer_account = opts.accountId;
|
|
316
|
+
} else if (opts.customerEmail) {
|
|
317
|
+
sessionParams.customer_email = opts.customerEmail;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Merge overrides, then derive the persisted fee/routing row from the FINAL
|
|
321
|
+
// request — sessionOverrides can replace subscription_data/payment_intent_data
|
|
322
|
+
// wholesale, so the row must reflect what was actually sent to Stripe.
|
|
323
|
+
const finalSessionParams: CheckoutSessionCreateParams = {
|
|
324
|
+
...sessionParams,
|
|
325
|
+
...(opts.sessionOverrides as Partial<CheckoutSessionCreateParams>),
|
|
326
|
+
};
|
|
327
|
+
// Separate-charge sales persist the split + chargeType directly; single-recipient
|
|
328
|
+
// destination charges derive the row from the final transfer_data/fee.
|
|
329
|
+
const feeRow = isSeparate
|
|
330
|
+
? { chargeType: "separate" as const, splitRecipients: opts.split }
|
|
331
|
+
: deriveFeeRow(finalSessionParams);
|
|
332
|
+
|
|
333
|
+
// Both destination and separate charges run on the platform account, so the
|
|
334
|
+
// price must be platform-owned. Validate the FINAL request (post-overrides) so
|
|
335
|
+
// callers can't inject transfer_data or swap line_items to bypass the check.
|
|
336
|
+
const finalPriceId = finalSessionParams.line_items?.[0]?.price;
|
|
337
|
+
if (
|
|
338
|
+
(isSeparate || feeRow.chargeType === "destination") &&
|
|
339
|
+
typeof finalPriceId === "string"
|
|
340
|
+
) {
|
|
341
|
+
await assertPlatformPrice(stripe, component, ctx, finalPriceId);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
version: 1 as const,
|
|
346
|
+
request: finalSessionParams,
|
|
347
|
+
options: opts,
|
|
348
|
+
descriptor: { value: statementDescriptorSuffix },
|
|
349
|
+
routingMetadata: {
|
|
350
|
+
subscription: sessionParams.subscription_data?.metadata,
|
|
351
|
+
payment: sessionParams.payment_intent_data?.metadata,
|
|
352
|
+
},
|
|
353
|
+
row: {
|
|
354
|
+
userId: opts.userId,
|
|
355
|
+
orgId: opts.orgId,
|
|
356
|
+
accountId: opts.accountId,
|
|
357
|
+
priceId: opts.stripePriceId,
|
|
358
|
+
...feeRow,
|
|
359
|
+
metadata,
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|