@fantastic.dev/better-stripe 0.9.0 → 0.10.1
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 +12 -0
- package/MIGRATION.md +26 -0
- package/README.md +38 -1
- package/dist/client/apiVersion.d.ts +6 -0
- package/dist/client/apiVersion.d.ts.map +1 -0
- package/dist/client/apiVersion.js +13 -0
- package/dist/client/apiVersion.js.map +1 -0
- package/dist/client/billing/checkout.d.ts +1 -0
- package/dist/client/billing/checkout.d.ts.map +1 -1
- package/dist/client/billing/checkout.js +5 -1
- package/dist/client/billing/checkout.js.map +1 -1
- package/dist/client/billing/providerReads.d.ts +18 -0
- package/dist/client/billing/providerReads.d.ts.map +1 -0
- package/dist/client/billing/providerReads.js +15 -0
- package/dist/client/billing/providerReads.js.map +1 -0
- package/dist/client/billing/subscriptions.d.ts +1 -1
- package/dist/client/billing/subscriptions.d.ts.map +1 -1
- package/dist/client/billing/subscriptions.js.map +1 -1
- package/dist/client/connect/payoutSnapshot.d.ts +28 -0
- package/dist/client/connect/payoutSnapshot.d.ts.map +1 -0
- package/dist/client/connect/payoutSnapshot.js +81 -0
- package/dist/client/connect/payoutSnapshot.js.map +1 -0
- package/dist/client/connect/transfers.d.ts.map +1 -1
- package/dist/client/connect/transfers.js +13 -32
- package/dist/client/connect/transfers.js.map +1 -1
- package/dist/client/core/accounts.d.ts +12 -23
- package/dist/client/core/accounts.d.ts.map +1 -1
- package/dist/client/core/accounts.js +5 -1
- package/dist/client/core/accounts.js.map +1 -1
- package/dist/client/core/idempotency.d.ts +5 -0
- package/dist/client/core/idempotency.d.ts.map +1 -0
- package/dist/client/core/idempotency.js +10 -0
- package/dist/client/core/idempotency.js.map +1 -0
- package/dist/client/index.d.ts +39 -37
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +47 -56
- package/dist/client/index.js.map +1 -1
- package/dist/client/previewRoutes.d.ts +9 -0
- package/dist/client/previewRoutes.d.ts.map +1 -0
- package/dist/client/previewRoutes.js +15 -0
- package/dist/client/previewRoutes.js.map +1 -0
- package/dist/client/settlement/allocation.d.ts +30 -0
- package/dist/client/settlement/allocation.d.ts.map +1 -0
- package/dist/client/settlement/allocation.js +71 -0
- package/dist/client/settlement/allocation.js.map +1 -0
- package/dist/client/settlement/commissions.d.ts +27 -0
- package/dist/client/settlement/commissions.d.ts.map +1 -0
- package/dist/client/settlement/commissions.js +69 -0
- package/dist/client/settlement/commissions.js.map +1 -0
- package/dist/client/settlement/execution.d.ts +27 -0
- package/dist/client/settlement/execution.d.ts.map +1 -0
- package/dist/client/settlement/execution.js +70 -0
- package/dist/client/settlement/execution.js.map +1 -0
- package/dist/client/settlement/reconciliation.d.ts +10 -0
- package/dist/client/settlement/reconciliation.d.ts.map +1 -0
- package/dist/client/settlement/reconciliation.js +73 -0
- package/dist/client/settlement/reconciliation.js.map +1 -0
- package/dist/client/settlement/settleSale.d.ts +21 -0
- package/dist/client/settlement/settleSale.d.ts.map +1 -0
- package/dist/client/settlement/settleSale.js +27 -0
- package/dist/client/settlement/settleSale.js.map +1 -0
- package/dist/client/settlement/verifiedSale.d.ts +25 -0
- package/dist/client/settlement/verifiedSale.d.ts.map +1 -0
- package/dist/client/settlement/verifiedSale.js +100 -0
- package/dist/client/settlement/verifiedSale.js.map +1 -0
- package/dist/client/types/options.d.ts +3 -0
- package/dist/client/types/options.d.ts.map +1 -1
- package/dist/client/utils/webhookEndpoints.d.ts +12 -8
- package/dist/client/utils/webhookEndpoints.d.ts.map +1 -1
- package/dist/client/utils/webhookEndpoints.js +41 -2
- package/dist/client/utils/webhookEndpoints.js.map +1 -1
- package/dist/client/webhooks/asyncHooks.d.ts +5 -0
- package/dist/client/webhooks/asyncHooks.d.ts.map +1 -0
- package/dist/client/webhooks/asyncHooks.js +20 -0
- package/dist/client/webhooks/asyncHooks.js.map +1 -0
- package/dist/client/webhooks/v2.d.ts.map +1 -1
- package/dist/client/webhooks/v2.js +10 -7
- package/dist/client/webhooks/v2.js.map +1 -1
- package/dist/component/_generated/component.d.ts +80 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/lib/stripeFactory.d.ts +3 -2
- package/dist/component/lib/stripeFactory.d.ts.map +1 -1
- package/dist/component/lib/stripeFactory.js +6 -5
- package/dist/component/lib/stripeFactory.js.map +1 -1
- package/dist/component/schema.d.ts +67 -0
- package/dist/component/schema.js +4 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/settlement/mutations.d.ts +47 -0
- package/dist/component/settlement/mutations.d.ts.map +1 -0
- package/dist/component/settlement/mutations.js +104 -0
- package/dist/component/settlement/mutations.js.map +1 -0
- package/dist/component/settlement/queries.d.ts +29 -0
- package/dist/component/settlement/queries.d.ts.map +1 -0
- package/dist/component/settlement/queries.js +35 -0
- package/dist/component/settlement/queries.js.map +1 -0
- package/dist/component/settlement/schema.d.ts +99 -0
- package/dist/component/settlement/schema.d.ts.map +1 -0
- package/dist/component/settlement/schema.js +35 -0
- package/dist/component/settlement/schema.js.map +1 -0
- package/dist/react/workflows/Checkout.d.ts +3 -1
- package/dist/react/workflows/Checkout.d.ts.map +1 -1
- package/dist/react/workflows/Checkout.js +3 -2
- package/dist/react/workflows/Checkout.js.map +1 -1
- package/dist/react/workflows/Payouts.d.ts +3 -18
- package/dist/react/workflows/Payouts.d.ts.map +1 -1
- package/dist/react/workflows/Payouts.js +2 -2
- package/dist/react/workflows/Payouts.js.map +1 -1
- package/docs/accounts-v2-preview.md +132 -0
- package/docs/react-workflows.md +5 -2
- package/docs/settlement.md +158 -0
- package/package.json +3 -1
- package/src/client/apiVersion.ts +20 -0
- package/src/client/billing/checkout-idempotency.test.ts +82 -0
- package/src/client/billing/checkout.ts +6 -1
- package/src/client/billing/providerReads.test.ts +60 -0
- package/src/client/billing/providerReads.ts +30 -0
- package/src/client/billing/subscriptions.ts +1 -8
- package/src/client/connect/payoutSnapshot.test.ts +91 -0
- package/src/client/connect/payoutSnapshot.ts +130 -0
- package/src/client/connect/transfers.test.ts +15 -4
- package/src/client/connect/transfers.ts +13 -42
- package/src/client/core/account-idempotency.test.ts +78 -0
- package/src/client/core/accounts.ts +18 -25
- package/src/client/core/idempotency.test.ts +10 -0
- package/src/client/core/idempotency.ts +11 -0
- package/src/client/customer-preview-thin.test.ts +41 -0
- package/src/client/customer-preview-webhooks.test.ts +150 -0
- package/src/client/customer-preview.test.ts +185 -0
- package/src/client/index.ts +115 -119
- package/src/client/previewRoutes.ts +26 -0
- package/src/client/settlement/allocation.test.ts +98 -0
- package/src/client/settlement/allocation.ts +110 -0
- package/src/client/settlement/commissions.test.ts +103 -0
- package/src/client/settlement/commissions.ts +125 -0
- package/src/client/settlement/execution.test.ts +186 -0
- package/src/client/settlement/execution.ts +134 -0
- package/src/client/settlement/reconciliation.ts +119 -0
- package/src/client/settlement/settleSale.ts +59 -0
- package/src/client/settlement/verifiedSale.test.ts +139 -0
- package/src/client/settlement/verifiedSale.ts +159 -0
- package/src/client/stripe-version.test.ts +10 -4
- package/src/client/types/options.ts +3 -0
- package/src/client/utils/webhookEndpoints.ts +70 -15
- package/src/client/webhooks/asyncHooks.ts +38 -0
- package/src/client/webhooks/processors.test.ts +9 -9
- package/src/client/webhooks/v2.ts +14 -10
- package/src/component/_generated/api.ts +4 -0
- package/src/component/_generated/component.ts +100 -0
- package/src/component/lib/stripeFactory.ts +13 -5
- package/src/component/schema.ts +8 -0
- package/src/component/settlement/mutations.ts +119 -0
- package/src/component/settlement/queries.ts +38 -0
- package/src/component/settlement/schema.ts +45 -0
- package/src/component/settlement-claims.test.ts +77 -0
- package/src/react/components/EmbeddedDisputes.render.test.tsx +2 -2
- package/src/react/index.payment-element.test.ts +22 -37
- package/src/react/workflows/Checkout.test.tsx +30 -0
- package/src/react/workflows/Checkout.tsx +10 -3
- package/src/react/workflows/Payouts.test.tsx +33 -0
- package/src/react/workflows/Payouts.tsx +21 -15
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { STRIPE_CUSTOMER_PREVIEW_API_VERSION } from "../apiVersion.js";
|
|
2
|
+
import { throwStripeError } from "../errors.js";
|
|
1
3
|
import type Stripe from "stripe";
|
|
2
4
|
|
|
3
5
|
import type { RunCtx } from "../helpers.js";
|
|
@@ -73,6 +75,13 @@ export const BETTER_STRIPE_V2_WEBHOOK_EVENTS = [
|
|
|
73
75
|
"v2.core.account_link.returned",
|
|
74
76
|
] as const;
|
|
75
77
|
|
|
78
|
+
/** Minimal customer lifecycle subscription for non-Connect preview merchants. */
|
|
79
|
+
export const BETTER_STRIPE_CUSTOMER_ACCOUNT_EVENTS = [
|
|
80
|
+
"v2.core.account.created",
|
|
81
|
+
"v2.core.account[configuration.customer].updated",
|
|
82
|
+
"v2.core.account[identity].updated",
|
|
83
|
+
] as const;
|
|
84
|
+
|
|
76
85
|
export type BetterStripeV2WebhookEvent =
|
|
77
86
|
(typeof BETTER_STRIPE_V2_WEBHOOK_EVENTS)[number];
|
|
78
87
|
|
|
@@ -103,14 +112,19 @@ export async function listWebhookEndpoints(
|
|
|
103
112
|
}));
|
|
104
113
|
}
|
|
105
114
|
|
|
115
|
+
export type WebhookEndpointOptions = {
|
|
116
|
+
url: string;
|
|
117
|
+
description?: string;
|
|
118
|
+
enabledEvents?: string[];
|
|
119
|
+
};
|
|
120
|
+
export type EventDestinationOptions = WebhookEndpointOptions & {
|
|
121
|
+
name?: string;
|
|
122
|
+
eventPayload?: "snapshot" | "thin";
|
|
123
|
+
};
|
|
106
124
|
export async function createWebhookEndpoint(
|
|
107
125
|
stripe: Stripe,
|
|
108
126
|
_ctx: RunCtx,
|
|
109
|
-
opts: {
|
|
110
|
-
url: string;
|
|
111
|
-
description?: string;
|
|
112
|
-
enabledEvents?: string[];
|
|
113
|
-
},
|
|
127
|
+
opts: WebhookEndpointOptions & { apiVersion?: string },
|
|
114
128
|
): Promise<{ id: string; secret: string; url: string }> {
|
|
115
129
|
// Remove existing endpoint with same URL (secret is only available on create)
|
|
116
130
|
const existing = await stripe.webhookEndpoints.list({ limit: 100 });
|
|
@@ -121,6 +135,12 @@ export async function createWebhookEndpoint(
|
|
|
121
135
|
|
|
122
136
|
const endpoint = await stripe.webhookEndpoints.create({
|
|
123
137
|
url: opts.url,
|
|
138
|
+
...(opts.apiVersion
|
|
139
|
+
? {
|
|
140
|
+
api_version:
|
|
141
|
+
opts.apiVersion as Stripe.WebhookEndpointCreateParams.ApiVersion,
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
124
144
|
enabled_events: (opts.enabledEvents ??
|
|
125
145
|
BETTER_STRIPE_WEBHOOK_EVENTS) as Stripe.WebhookEndpointCreateParams.EnabledEvent[],
|
|
126
146
|
description:
|
|
@@ -189,14 +209,7 @@ export async function listEventDestinations(
|
|
|
189
209
|
export async function setupEventDestination(
|
|
190
210
|
stripe: Stripe,
|
|
191
211
|
_ctx: RunCtx,
|
|
192
|
-
opts: {
|
|
193
|
-
url: string;
|
|
194
|
-
name?: string;
|
|
195
|
-
description?: string;
|
|
196
|
-
enabledEvents?: string[];
|
|
197
|
-
/** 'snapshot' for V1 events, 'thin' for V2 events. Defaults to 'thin'. */
|
|
198
|
-
eventPayload?: "snapshot" | "thin";
|
|
199
|
-
},
|
|
212
|
+
opts: EventDestinationOptions & { apiVersion?: string },
|
|
200
213
|
): Promise<{
|
|
201
214
|
id: string;
|
|
202
215
|
secret: string;
|
|
@@ -208,7 +221,9 @@ export async function setupEventDestination(
|
|
|
208
221
|
const events =
|
|
209
222
|
opts.enabledEvents ??
|
|
210
223
|
(payloadType === "thin"
|
|
211
|
-
?
|
|
224
|
+
? opts.apiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
225
|
+
? [...BETTER_STRIPE_CUSTOMER_ACCOUNT_EVENTS]
|
|
226
|
+
: [...BETTER_STRIPE_V2_WEBHOOK_EVENTS]
|
|
212
227
|
: [...BETTER_STRIPE_WEBHOOK_EVENTS]);
|
|
213
228
|
const name =
|
|
214
229
|
opts.name ??
|
|
@@ -228,6 +243,36 @@ export async function setupEventDestination(
|
|
|
228
243
|
d.webhook_endpoint?.url === opts.url && d.event_payload === payloadType,
|
|
229
244
|
);
|
|
230
245
|
|
|
246
|
+
if (
|
|
247
|
+
match &&
|
|
248
|
+
payloadType === "snapshot" &&
|
|
249
|
+
opts.apiVersion &&
|
|
250
|
+
match.snapshot_api_version !== opts.apiVersion
|
|
251
|
+
)
|
|
252
|
+
throwStripeError(
|
|
253
|
+
"INVALID_CONFIGURATION",
|
|
254
|
+
"Existing snapshot destination API version differs. Create a replacement destination with the selected version and configure its signing secret before retiring the old destination.",
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (match && opts.apiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION) {
|
|
258
|
+
const scopes = new Set(
|
|
259
|
+
(match.events_from ?? []).map((scope) =>
|
|
260
|
+
scope === "self"
|
|
261
|
+
? "@self"
|
|
262
|
+
: scope === "other_accounts"
|
|
263
|
+
? "@accounts"
|
|
264
|
+
: scope,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
const required =
|
|
268
|
+
payloadType === "thin" ? ["@self", "@accounts"] : ["@self"];
|
|
269
|
+
if (required.some((scope) => !scopes.has(scope)))
|
|
270
|
+
throwStripeError(
|
|
271
|
+
"INVALID_CONFIGURATION",
|
|
272
|
+
"Existing destination event scope differs. Create a replacement with the customer preview scopes before retiring the old destination.",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
231
276
|
if (match) {
|
|
232
277
|
const updated = await stripe.v2.core.eventDestinations.update(match.id, {
|
|
233
278
|
name,
|
|
@@ -256,7 +301,17 @@ export async function setupEventDestination(
|
|
|
256
301
|
type: "webhook_endpoint",
|
|
257
302
|
event_payload: payloadType,
|
|
258
303
|
enabled_events: events,
|
|
259
|
-
|
|
304
|
+
...(payloadType === "snapshot" && opts.apiVersion
|
|
305
|
+
? { snapshot_api_version: opts.apiVersion }
|
|
306
|
+
: {}),
|
|
307
|
+
events_from:
|
|
308
|
+
opts.apiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
309
|
+
? payloadType === "thin"
|
|
310
|
+
? ["@self", "@accounts"]
|
|
311
|
+
: ["@self"]
|
|
312
|
+
: payloadType === "thin"
|
|
313
|
+
? ["self", "other_accounts"]
|
|
314
|
+
: ["self"],
|
|
260
315
|
webhook_endpoint: { url: opts.url },
|
|
261
316
|
include: ["webhook_endpoint.signing_secret", "webhook_endpoint.url"],
|
|
262
317
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AsyncHooks, AsyncHookCtx } from "../types.js";
|
|
2
|
+
import type { AsyncHookName } from "../types/triggers.js";
|
|
3
|
+
/** Route async webhook hooks through the shared public callback contract. */
|
|
4
|
+
export function buildAsyncHookRoutes(hooks: AsyncHooks | undefined) {
|
|
5
|
+
const routes: Record<
|
|
6
|
+
AsyncHookName,
|
|
7
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined
|
|
8
|
+
> = {
|
|
9
|
+
afterAccountUpdated: hooks?.onAccountUpdated as
|
|
10
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
11
|
+
afterCheckoutCompleted: hooks?.onCheckoutCompleted as
|
|
12
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
13
|
+
afterSubscriptionUpdated: hooks?.onSubscriptionUpdated as
|
|
14
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
15
|
+
afterSubscriptionCanceled: hooks?.onSubscriptionCanceled as
|
|
16
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
17
|
+
afterTrialEnding: hooks?.onTrialEnding as
|
|
18
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
19
|
+
afterInvoicePaid: hooks?.onInvoicePaid as
|
|
20
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
21
|
+
afterInvoicePaymentFailed: hooks?.onInvoicePaymentFailed as
|
|
22
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
23
|
+
afterPaymentSucceeded: hooks?.onPaymentSucceeded as
|
|
24
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
25
|
+
afterPaymentFailed: hooks?.onPaymentFailed as
|
|
26
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
27
|
+
afterPayoutCompleted: hooks?.onPayoutCompleted as
|
|
28
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
29
|
+
afterRefundCreated: hooks?.onRefundCreated as
|
|
30
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
31
|
+
afterDisputeCreated: hooks?.onDisputeCreated as
|
|
32
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
33
|
+
afterDisputeClosed: hooks?.onDisputeClosed as
|
|
34
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return routes;
|
|
38
|
+
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
// @vitest-environment edge-runtime
|
|
2
|
-
/**
|
|
3
|
-
* Drives Stripe event normalization through processEvent and checks the payloads
|
|
4
|
-
* passed to component mutations, including owner, fee, and trial fields.
|
|
5
|
-
*/
|
|
6
2
|
import type Stripe from "stripe";
|
|
7
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
4
|
|
|
@@ -12,7 +8,6 @@ import { processEvent } from "./processors.js";
|
|
|
12
8
|
|
|
13
9
|
const TO_REF = Symbol.for("toReferencePath");
|
|
14
10
|
|
|
15
|
-
/** Fake component exposing every query/mutation ref processors.ts resolves. */
|
|
16
11
|
function makeComponent() {
|
|
17
12
|
const refs = (namespace: string, names: string[]) =>
|
|
18
13
|
Object.fromEntries(
|
|
@@ -44,12 +39,16 @@ function makeCtx(opts?: { query?: unknown; queryThrows?: boolean }) {
|
|
|
44
39
|
const runQuery = vi.fn();
|
|
45
40
|
if (opts?.queryThrows) runQuery.mockRejectedValue(new Error("not found"));
|
|
46
41
|
else runQuery.mockResolvedValue(opts?.query ?? null);
|
|
47
|
-
//
|
|
48
|
-
// (BTS-63). Fake only its storage: the slice math is the REAL shared
|
|
49
|
-
// computeReversalSlices, applied to this ctx's ledger legs.
|
|
42
|
+
// Fake claim storage; use the shared reversal sizing math.
|
|
50
43
|
const runMutation = vi.fn(
|
|
51
44
|
async (ref: Record<symbol, string>, args: Record<string, unknown>) => {
|
|
52
45
|
const path = ref?.[TO_REF] ?? "";
|
|
46
|
+
if (
|
|
47
|
+
String(path).endsWith(
|
|
48
|
+
"settlement/mutations/reserveSettlementAllocation",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
return args;
|
|
53
52
|
if (!String(path).endsWith("connect/mutations/claimReversalSlices")) {
|
|
54
53
|
return undefined;
|
|
55
54
|
}
|
|
@@ -107,7 +106,9 @@ function makeStripe() {
|
|
|
107
106
|
update: vi.fn().mockResolvedValue({}),
|
|
108
107
|
retrieve: vi.fn(),
|
|
109
108
|
},
|
|
109
|
+
charges: { retrieve: vi.fn().mockResolvedValue({ created: 1 }) },
|
|
110
110
|
transfers: {
|
|
111
|
+
list: vi.fn().mockResolvedValue({ data: [], has_more: false }),
|
|
111
112
|
create: vi.fn(async (_params: unknown) => ({
|
|
112
113
|
id: `tr_${++trCreateSeq}`,
|
|
113
114
|
})),
|
|
@@ -146,7 +147,6 @@ function makeStripe() {
|
|
|
146
147
|
retrieve: vi.fn(),
|
|
147
148
|
update: vi.fn().mockResolvedValue({}),
|
|
148
149
|
},
|
|
149
|
-
charges: { retrieve: vi.fn() },
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STRIPE_CUSTOMER_PREVIEW_API_VERSION } from "../apiVersion.js";
|
|
1
2
|
import type Stripe from "stripe";
|
|
2
3
|
|
|
3
4
|
import { deriveAccountStatus } from "../core/accountStatus.js";
|
|
@@ -74,16 +75,19 @@ export async function handleV2Event(
|
|
|
74
75
|
return null;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
const retrieveParams
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
const retrieveParams: NonNullable<
|
|
79
|
+
Parameters<Stripe["v2"]["core"]["accounts"]["retrieve"]>[1]
|
|
80
|
+
> = {
|
|
81
|
+
include:
|
|
82
|
+
whCtx.config?.stripeApiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
83
|
+
? ["configuration.customer", "identity"]
|
|
84
|
+
: [
|
|
85
|
+
"configuration.merchant",
|
|
86
|
+
"configuration.recipient",
|
|
87
|
+
"configuration.customer",
|
|
88
|
+
"identity",
|
|
89
|
+
"requirements",
|
|
90
|
+
],
|
|
87
91
|
};
|
|
88
92
|
// Preserve the verified notification's account/organization context when
|
|
89
93
|
// fetching the latest resource; otherwise organization keys can fetch from
|
|
@@ -25,6 +25,8 @@ import type * as lib_stripeFactory from "../lib/stripeFactory.js";
|
|
|
25
25
|
import type * as products_mutations from "../products/mutations.js";
|
|
26
26
|
import type * as products_queries from "../products/queries.js";
|
|
27
27
|
import type * as products_validators from "../products/validators.js";
|
|
28
|
+
import type * as settlement_mutations from "../settlement/mutations.js";
|
|
29
|
+
import type * as settlement_queries from "../settlement/queries.js";
|
|
28
30
|
import type * as webhooks_mutations from "../webhooks/mutations.js";
|
|
29
31
|
import type * as webhooks_queries from "../webhooks/queries.js";
|
|
30
32
|
import type * as webhooks_validators from "../webhooks/validators.js";
|
|
@@ -54,6 +56,8 @@ const fullApi: ApiFromModules<{
|
|
|
54
56
|
"products/mutations": typeof products_mutations;
|
|
55
57
|
"products/queries": typeof products_queries;
|
|
56
58
|
"products/validators": typeof products_validators;
|
|
59
|
+
"settlement/mutations": typeof settlement_mutations;
|
|
60
|
+
"settlement/queries": typeof settlement_queries;
|
|
57
61
|
"webhooks/mutations": typeof webhooks_mutations;
|
|
58
62
|
"webhooks/queries": typeof webhooks_queries;
|
|
59
63
|
"webhooks/validators": typeof webhooks_validators;
|
|
@@ -1763,6 +1763,106 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1763
1763
|
>;
|
|
1764
1764
|
};
|
|
1765
1765
|
};
|
|
1766
|
+
settlement: {
|
|
1767
|
+
mutations: {
|
|
1768
|
+
claimCommissionInvoice: FunctionReference<
|
|
1769
|
+
"mutation",
|
|
1770
|
+
"internal",
|
|
1771
|
+
{
|
|
1772
|
+
affiliateAccountId?: string;
|
|
1773
|
+
firstPaidInvoiceId: string;
|
|
1774
|
+
invoiceId: string;
|
|
1775
|
+
subscriptionId: string;
|
|
1776
|
+
term: "forever" | "one_time";
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
claimKey: string;
|
|
1780
|
+
eligible: boolean;
|
|
1781
|
+
firstPaidInvoiceId: string;
|
|
1782
|
+
replay: boolean;
|
|
1783
|
+
term: "forever" | "one_time";
|
|
1784
|
+
},
|
|
1785
|
+
Name
|
|
1786
|
+
>;
|
|
1787
|
+
reserveSettlementAllocation: FunctionReference<
|
|
1788
|
+
"mutation",
|
|
1789
|
+
"internal",
|
|
1790
|
+
{
|
|
1791
|
+
adjustmentAmount?: number;
|
|
1792
|
+
currency: string;
|
|
1793
|
+
gross: number;
|
|
1794
|
+
paymentId?: string;
|
|
1795
|
+
paymentIntentId?: string;
|
|
1796
|
+
platformNet: number;
|
|
1797
|
+
processingFee: number;
|
|
1798
|
+
sourceChargeId: string;
|
|
1799
|
+
sourceInvoiceId?: string;
|
|
1800
|
+
transfers: Array<{
|
|
1801
|
+
amount: number;
|
|
1802
|
+
destinationAccountId: string;
|
|
1803
|
+
role: "store" | "affiliate" | "other";
|
|
1804
|
+
}>;
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
_creationTime: number;
|
|
1808
|
+
_id: string;
|
|
1809
|
+
adjustmentAmount?: number;
|
|
1810
|
+
currency: string;
|
|
1811
|
+
gross: number;
|
|
1812
|
+
paymentId?: string;
|
|
1813
|
+
paymentIntentId?: string;
|
|
1814
|
+
platformNet: number;
|
|
1815
|
+
processingFee: number;
|
|
1816
|
+
sourceChargeId: string;
|
|
1817
|
+
sourceInvoiceId?: string;
|
|
1818
|
+
transfers: Array<{
|
|
1819
|
+
amount: number;
|
|
1820
|
+
destinationAccountId: string;
|
|
1821
|
+
role: "store" | "affiliate" | "other";
|
|
1822
|
+
}>;
|
|
1823
|
+
},
|
|
1824
|
+
Name
|
|
1825
|
+
>;
|
|
1826
|
+
};
|
|
1827
|
+
queries: {
|
|
1828
|
+
getCommissionTerm: FunctionReference<
|
|
1829
|
+
"query",
|
|
1830
|
+
"internal",
|
|
1831
|
+
{ subscriptionId: string },
|
|
1832
|
+
{
|
|
1833
|
+
affiliateAccountId?: string;
|
|
1834
|
+
firstPaidInvoiceId: string;
|
|
1835
|
+
subscriptionId: string;
|
|
1836
|
+
term: "forever" | "one_time";
|
|
1837
|
+
} | null,
|
|
1838
|
+
Name
|
|
1839
|
+
>;
|
|
1840
|
+
getSettlementAllocation: FunctionReference<
|
|
1841
|
+
"query",
|
|
1842
|
+
"internal",
|
|
1843
|
+
{ sourceChargeId: string },
|
|
1844
|
+
{
|
|
1845
|
+
_creationTime: number;
|
|
1846
|
+
_id: string;
|
|
1847
|
+
adjustmentAmount?: number;
|
|
1848
|
+
currency: string;
|
|
1849
|
+
gross: number;
|
|
1850
|
+
paymentId?: string;
|
|
1851
|
+
paymentIntentId?: string;
|
|
1852
|
+
platformNet: number;
|
|
1853
|
+
processingFee: number;
|
|
1854
|
+
sourceChargeId: string;
|
|
1855
|
+
sourceInvoiceId?: string;
|
|
1856
|
+
transfers: Array<{
|
|
1857
|
+
amount: number;
|
|
1858
|
+
destinationAccountId: string;
|
|
1859
|
+
role: "store" | "affiliate" | "other";
|
|
1860
|
+
}>;
|
|
1861
|
+
} | null,
|
|
1862
|
+
Name
|
|
1863
|
+
>;
|
|
1864
|
+
};
|
|
1865
|
+
};
|
|
1766
1866
|
webhooks: {
|
|
1767
1867
|
mutations: {
|
|
1768
1868
|
insertWebhookEvent: FunctionReference<
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import Stripe from "stripe";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
resolveStripeApiVersion,
|
|
5
|
+
type SupportedStripeApiVersion,
|
|
6
|
+
} from "../../client/apiVersion.js";
|
|
7
|
+
import type { StripeApiVersion } from "../../client/stripe-types.js";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Keyed cache for Stripe SDK instances.
|
|
@@ -14,14 +18,18 @@ const _clients = new Map<string, Stripe>();
|
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Get or create a Stripe SDK instance for the given API key.
|
|
17
|
-
*
|
|
21
|
+
* Defaults to GA; an explicitly selected supported preview gets its own cache entry.
|
|
18
22
|
*/
|
|
19
|
-
export function getStripe(
|
|
20
|
-
|
|
23
|
+
export function getStripe(
|
|
24
|
+
apiKey: string,
|
|
25
|
+
apiVersion?: SupportedStripeApiVersion,
|
|
26
|
+
): Stripe {
|
|
27
|
+
const version = resolveStripeApiVersion(apiVersion);
|
|
28
|
+
const cacheKey = `${apiKey}:${version}`;
|
|
21
29
|
let client = _clients.get(cacheKey);
|
|
22
30
|
if (!client) {
|
|
23
31
|
client = new Stripe(apiKey, {
|
|
24
|
-
apiVersion:
|
|
32
|
+
apiVersion: version as StripeApiVersion,
|
|
25
33
|
typescript: true,
|
|
26
34
|
});
|
|
27
35
|
_clients.set(cacheKey, client);
|
package/src/component/schema.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allocationsTable,
|
|
3
|
+
commissionTermsTable,
|
|
4
|
+
commissionDecisionsTable,
|
|
5
|
+
} from "./settlement/schema";
|
|
1
6
|
import { defineSchema } from "convex/server";
|
|
2
7
|
|
|
3
8
|
import {
|
|
@@ -19,6 +24,9 @@ import { pricesTable, productsTable } from "./products/schema";
|
|
|
19
24
|
import { webhookEventsTable } from "./webhooks/schema";
|
|
20
25
|
|
|
21
26
|
export default defineSchema({
|
|
27
|
+
settlementAllocations: allocationsTable,
|
|
28
|
+
commissionTerms: commissionTermsTable,
|
|
29
|
+
commissionDecisions: commissionDecisionsTable,
|
|
22
30
|
// Core
|
|
23
31
|
accounts: accountsTable,
|
|
24
32
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation } from "../_generated/server.js";
|
|
3
|
+
import { allocationFields, commissionFields, termValidator } from "./schema.js";
|
|
4
|
+
const doc = v.object({
|
|
5
|
+
...allocationFields,
|
|
6
|
+
_id: v.id("settlementAllocations"),
|
|
7
|
+
_creationTime: v.number(),
|
|
8
|
+
});
|
|
9
|
+
export const reserveSettlementAllocation = mutation({
|
|
10
|
+
args: allocationFields,
|
|
11
|
+
returns: doc,
|
|
12
|
+
handler: async (ctx, args) => {
|
|
13
|
+
const existing = await ctx.db
|
|
14
|
+
.query("settlementAllocations")
|
|
15
|
+
.withIndex("by_source_charge", (q) =>
|
|
16
|
+
q.eq("sourceChargeId", args.sourceChargeId),
|
|
17
|
+
)
|
|
18
|
+
.first();
|
|
19
|
+
if (existing) {
|
|
20
|
+
if (
|
|
21
|
+
existing.currency !== args.currency ||
|
|
22
|
+
existing.paymentIntentId !== args.paymentIntentId ||
|
|
23
|
+
existing.sourceInvoiceId !== args.sourceInvoiceId
|
|
24
|
+
)
|
|
25
|
+
throw new Error("Settlement source identity cannot change.");
|
|
26
|
+
return existing;
|
|
27
|
+
}
|
|
28
|
+
const adjustment = args.adjustmentAmount ?? 0;
|
|
29
|
+
const amounts = [
|
|
30
|
+
args.gross,
|
|
31
|
+
args.processingFee,
|
|
32
|
+
args.platformNet,
|
|
33
|
+
...args.transfers.map((t) => t.amount),
|
|
34
|
+
];
|
|
35
|
+
const keys = args.transfers.map(
|
|
36
|
+
(t) => `${t.destinationAccountId}:${t.role}`,
|
|
37
|
+
);
|
|
38
|
+
if (
|
|
39
|
+
!Number.isSafeInteger(adjustment) ||
|
|
40
|
+
args.processingFee + args.platformNet + adjustment < 0 ||
|
|
41
|
+
amounts.some((n) => !Number.isSafeInteger(n) || n < 0) ||
|
|
42
|
+
args.gross <= 0 ||
|
|
43
|
+
!args.currency ||
|
|
44
|
+
!args.sourceChargeId ||
|
|
45
|
+
args.transfers.some((t) => !t.destinationAccountId) ||
|
|
46
|
+
new Set(keys).size !== keys.length ||
|
|
47
|
+
args.gross !==
|
|
48
|
+
args.processingFee +
|
|
49
|
+
args.platformNet +
|
|
50
|
+
adjustment +
|
|
51
|
+
args.transfers.reduce((sum, t) => sum + t.amount, 0)
|
|
52
|
+
)
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Settlement allocation must reconcile in integer currency units with unique recipient legs.",
|
|
55
|
+
);
|
|
56
|
+
const id = await ctx.db.insert("settlementAllocations", args);
|
|
57
|
+
return (await ctx.db.get("settlementAllocations", id))!;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
export const claimCommissionInvoice = mutation({
|
|
61
|
+
args: { ...commissionFields, invoiceId: v.string() },
|
|
62
|
+
returns: v.object({
|
|
63
|
+
eligible: v.boolean(),
|
|
64
|
+
term: termValidator,
|
|
65
|
+
firstPaidInvoiceId: v.string(),
|
|
66
|
+
claimKey: v.string(),
|
|
67
|
+
replay: v.boolean(),
|
|
68
|
+
}),
|
|
69
|
+
handler: async (ctx, args) => {
|
|
70
|
+
let stored = await ctx.db
|
|
71
|
+
.query("commissionTerms")
|
|
72
|
+
.withIndex("by_subscription", (q) =>
|
|
73
|
+
q.eq("subscriptionId", args.subscriptionId),
|
|
74
|
+
)
|
|
75
|
+
.first();
|
|
76
|
+
if (stored && stored.affiliateAccountId !== args.affiliateAccountId)
|
|
77
|
+
throw new Error("Subscription affiliate identity cannot change.");
|
|
78
|
+
if (!stored) {
|
|
79
|
+
const fields = {
|
|
80
|
+
subscriptionId: args.subscriptionId,
|
|
81
|
+
term: args.term,
|
|
82
|
+
affiliateAccountId: args.affiliateAccountId,
|
|
83
|
+
firstPaidInvoiceId: args.firstPaidInvoiceId,
|
|
84
|
+
};
|
|
85
|
+
const id = await ctx.db.insert("commissionTerms", fields);
|
|
86
|
+
stored = (await ctx.db.get("commissionTerms", id))!;
|
|
87
|
+
}
|
|
88
|
+
const existing = await ctx.db
|
|
89
|
+
.query("commissionDecisions")
|
|
90
|
+
.withIndex("by_subscription_invoice", (q) =>
|
|
91
|
+
q
|
|
92
|
+
.eq("subscriptionId", args.subscriptionId)
|
|
93
|
+
.eq("invoiceId", args.invoiceId),
|
|
94
|
+
)
|
|
95
|
+
.first();
|
|
96
|
+
const claimKey =
|
|
97
|
+
existing?.claimKey ??
|
|
98
|
+
`commission:${args.subscriptionId}:${args.invoiceId}`;
|
|
99
|
+
const eligible =
|
|
100
|
+
existing?.eligible ??
|
|
101
|
+
(!!stored.affiliateAccountId &&
|
|
102
|
+
(stored.term === "forever" ||
|
|
103
|
+
args.invoiceId === stored.firstPaidInvoiceId));
|
|
104
|
+
if (!existing)
|
|
105
|
+
await ctx.db.insert("commissionDecisions", {
|
|
106
|
+
subscriptionId: args.subscriptionId,
|
|
107
|
+
invoiceId: args.invoiceId,
|
|
108
|
+
eligible,
|
|
109
|
+
claimKey,
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
eligible,
|
|
113
|
+
term: stored.term,
|
|
114
|
+
firstPaidInvoiceId: stored.firstPaidInvoiceId,
|
|
115
|
+
claimKey,
|
|
116
|
+
replay: !!existing,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { query } from "../_generated/server.js";
|
|
3
|
+
import { allocationFields, commissionFields } from "./schema.js";
|
|
4
|
+
const doc = v.object({
|
|
5
|
+
...allocationFields,
|
|
6
|
+
_id: v.id("settlementAllocations"),
|
|
7
|
+
_creationTime: v.number(),
|
|
8
|
+
});
|
|
9
|
+
export const getSettlementAllocation = query({
|
|
10
|
+
args: { sourceChargeId: v.string() },
|
|
11
|
+
returns: v.union(doc, v.null()),
|
|
12
|
+
handler: (ctx, args) =>
|
|
13
|
+
ctx.db
|
|
14
|
+
.query("settlementAllocations")
|
|
15
|
+
.withIndex("by_source_charge", (q) =>
|
|
16
|
+
q.eq("sourceChargeId", args.sourceChargeId),
|
|
17
|
+
)
|
|
18
|
+
.first(),
|
|
19
|
+
});
|
|
20
|
+
export const getCommissionTerm = query({
|
|
21
|
+
args: { subscriptionId: v.string() },
|
|
22
|
+
returns: v.union(v.object(commissionFields), v.null()),
|
|
23
|
+
handler: async (ctx, args) => {
|
|
24
|
+
const row = await ctx.db
|
|
25
|
+
.query("commissionTerms")
|
|
26
|
+
.withIndex("by_subscription", (q) =>
|
|
27
|
+
q.eq("subscriptionId", args.subscriptionId),
|
|
28
|
+
)
|
|
29
|
+
.first();
|
|
30
|
+
if (!row) return null;
|
|
31
|
+
return {
|
|
32
|
+
subscriptionId: row.subscriptionId,
|
|
33
|
+
term: row.term,
|
|
34
|
+
affiliateAccountId: row.affiliateAccountId,
|
|
35
|
+
firstPaidInvoiceId: row.firstPaidInvoiceId,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import { splitRecipientRoleValidator } from "../lib/fees.js";
|
|
4
|
+
export const termValidator = v.union(
|
|
5
|
+
v.literal("forever"),
|
|
6
|
+
v.literal("one_time"),
|
|
7
|
+
);
|
|
8
|
+
export const allocationFields = {
|
|
9
|
+
sourceChargeId: v.string(),
|
|
10
|
+
paymentIntentId: v.optional(v.string()),
|
|
11
|
+
sourceInvoiceId: v.optional(v.string()),
|
|
12
|
+
gross: v.number(),
|
|
13
|
+
currency: v.string(),
|
|
14
|
+
processingFee: v.number(),
|
|
15
|
+
platformNet: v.number(),
|
|
16
|
+
adjustmentAmount: v.optional(v.number()),
|
|
17
|
+
paymentId: v.optional(v.string()),
|
|
18
|
+
transfers: v.array(
|
|
19
|
+
v.object({
|
|
20
|
+
destinationAccountId: v.string(),
|
|
21
|
+
role: splitRecipientRoleValidator,
|
|
22
|
+
amount: v.number(),
|
|
23
|
+
}),
|
|
24
|
+
),
|
|
25
|
+
};
|
|
26
|
+
export const allocationsTable = defineTable(allocationFields).index(
|
|
27
|
+
"by_source_charge",
|
|
28
|
+
["sourceChargeId"],
|
|
29
|
+
);
|
|
30
|
+
export const commissionFields = {
|
|
31
|
+
subscriptionId: v.string(),
|
|
32
|
+
term: termValidator,
|
|
33
|
+
affiliateAccountId: v.optional(v.string()),
|
|
34
|
+
firstPaidInvoiceId: v.string(),
|
|
35
|
+
};
|
|
36
|
+
export const commissionTermsTable = defineTable(commissionFields).index(
|
|
37
|
+
"by_subscription",
|
|
38
|
+
["subscriptionId"],
|
|
39
|
+
);
|
|
40
|
+
export const commissionDecisionsTable = defineTable({
|
|
41
|
+
subscriptionId: v.string(),
|
|
42
|
+
invoiceId: v.string(),
|
|
43
|
+
eligible: v.boolean(),
|
|
44
|
+
claimKey: v.string(),
|
|
45
|
+
}).index("by_subscription_invoice", ["subscriptionId", "invoiceId"]);
|