@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.
Files changed (160) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/MIGRATION.md +26 -0
  3. package/README.md +38 -1
  4. package/dist/client/apiVersion.d.ts +6 -0
  5. package/dist/client/apiVersion.d.ts.map +1 -0
  6. package/dist/client/apiVersion.js +13 -0
  7. package/dist/client/apiVersion.js.map +1 -0
  8. package/dist/client/billing/checkout.d.ts +1 -0
  9. package/dist/client/billing/checkout.d.ts.map +1 -1
  10. package/dist/client/billing/checkout.js +5 -1
  11. package/dist/client/billing/checkout.js.map +1 -1
  12. package/dist/client/billing/providerReads.d.ts +18 -0
  13. package/dist/client/billing/providerReads.d.ts.map +1 -0
  14. package/dist/client/billing/providerReads.js +15 -0
  15. package/dist/client/billing/providerReads.js.map +1 -0
  16. package/dist/client/billing/subscriptions.d.ts +1 -1
  17. package/dist/client/billing/subscriptions.d.ts.map +1 -1
  18. package/dist/client/billing/subscriptions.js.map +1 -1
  19. package/dist/client/connect/payoutSnapshot.d.ts +28 -0
  20. package/dist/client/connect/payoutSnapshot.d.ts.map +1 -0
  21. package/dist/client/connect/payoutSnapshot.js +81 -0
  22. package/dist/client/connect/payoutSnapshot.js.map +1 -0
  23. package/dist/client/connect/transfers.d.ts.map +1 -1
  24. package/dist/client/connect/transfers.js +13 -32
  25. package/dist/client/connect/transfers.js.map +1 -1
  26. package/dist/client/core/accounts.d.ts +12 -23
  27. package/dist/client/core/accounts.d.ts.map +1 -1
  28. package/dist/client/core/accounts.js +5 -1
  29. package/dist/client/core/accounts.js.map +1 -1
  30. package/dist/client/core/idempotency.d.ts +5 -0
  31. package/dist/client/core/idempotency.d.ts.map +1 -0
  32. package/dist/client/core/idempotency.js +10 -0
  33. package/dist/client/core/idempotency.js.map +1 -0
  34. package/dist/client/index.d.ts +39 -37
  35. package/dist/client/index.d.ts.map +1 -1
  36. package/dist/client/index.js +47 -56
  37. package/dist/client/index.js.map +1 -1
  38. package/dist/client/previewRoutes.d.ts +9 -0
  39. package/dist/client/previewRoutes.d.ts.map +1 -0
  40. package/dist/client/previewRoutes.js +15 -0
  41. package/dist/client/previewRoutes.js.map +1 -0
  42. package/dist/client/settlement/allocation.d.ts +30 -0
  43. package/dist/client/settlement/allocation.d.ts.map +1 -0
  44. package/dist/client/settlement/allocation.js +71 -0
  45. package/dist/client/settlement/allocation.js.map +1 -0
  46. package/dist/client/settlement/commissions.d.ts +27 -0
  47. package/dist/client/settlement/commissions.d.ts.map +1 -0
  48. package/dist/client/settlement/commissions.js +69 -0
  49. package/dist/client/settlement/commissions.js.map +1 -0
  50. package/dist/client/settlement/execution.d.ts +27 -0
  51. package/dist/client/settlement/execution.d.ts.map +1 -0
  52. package/dist/client/settlement/execution.js +70 -0
  53. package/dist/client/settlement/execution.js.map +1 -0
  54. package/dist/client/settlement/reconciliation.d.ts +10 -0
  55. package/dist/client/settlement/reconciliation.d.ts.map +1 -0
  56. package/dist/client/settlement/reconciliation.js +73 -0
  57. package/dist/client/settlement/reconciliation.js.map +1 -0
  58. package/dist/client/settlement/settleSale.d.ts +21 -0
  59. package/dist/client/settlement/settleSale.d.ts.map +1 -0
  60. package/dist/client/settlement/settleSale.js +27 -0
  61. package/dist/client/settlement/settleSale.js.map +1 -0
  62. package/dist/client/settlement/verifiedSale.d.ts +25 -0
  63. package/dist/client/settlement/verifiedSale.d.ts.map +1 -0
  64. package/dist/client/settlement/verifiedSale.js +100 -0
  65. package/dist/client/settlement/verifiedSale.js.map +1 -0
  66. package/dist/client/types/options.d.ts +3 -0
  67. package/dist/client/types/options.d.ts.map +1 -1
  68. package/dist/client/utils/webhookEndpoints.d.ts +12 -8
  69. package/dist/client/utils/webhookEndpoints.d.ts.map +1 -1
  70. package/dist/client/utils/webhookEndpoints.js +41 -2
  71. package/dist/client/utils/webhookEndpoints.js.map +1 -1
  72. package/dist/client/webhooks/asyncHooks.d.ts +5 -0
  73. package/dist/client/webhooks/asyncHooks.d.ts.map +1 -0
  74. package/dist/client/webhooks/asyncHooks.js +20 -0
  75. package/dist/client/webhooks/asyncHooks.js.map +1 -0
  76. package/dist/client/webhooks/v2.d.ts.map +1 -1
  77. package/dist/client/webhooks/v2.js +10 -7
  78. package/dist/client/webhooks/v2.js.map +1 -1
  79. package/dist/component/_generated/component.d.ts +80 -0
  80. package/dist/component/_generated/component.d.ts.map +1 -1
  81. package/dist/component/lib/stripeFactory.d.ts +3 -2
  82. package/dist/component/lib/stripeFactory.d.ts.map +1 -1
  83. package/dist/component/lib/stripeFactory.js +6 -5
  84. package/dist/component/lib/stripeFactory.js.map +1 -1
  85. package/dist/component/schema.d.ts +67 -0
  86. package/dist/component/schema.js +4 -0
  87. package/dist/component/schema.js.map +1 -1
  88. package/dist/component/settlement/mutations.d.ts +47 -0
  89. package/dist/component/settlement/mutations.d.ts.map +1 -0
  90. package/dist/component/settlement/mutations.js +104 -0
  91. package/dist/component/settlement/mutations.js.map +1 -0
  92. package/dist/component/settlement/queries.d.ts +29 -0
  93. package/dist/component/settlement/queries.d.ts.map +1 -0
  94. package/dist/component/settlement/queries.js +35 -0
  95. package/dist/component/settlement/queries.js.map +1 -0
  96. package/dist/component/settlement/schema.d.ts +99 -0
  97. package/dist/component/settlement/schema.d.ts.map +1 -0
  98. package/dist/component/settlement/schema.js +35 -0
  99. package/dist/component/settlement/schema.js.map +1 -0
  100. package/dist/react/workflows/Checkout.d.ts +3 -1
  101. package/dist/react/workflows/Checkout.d.ts.map +1 -1
  102. package/dist/react/workflows/Checkout.js +3 -2
  103. package/dist/react/workflows/Checkout.js.map +1 -1
  104. package/dist/react/workflows/Payouts.d.ts +3 -18
  105. package/dist/react/workflows/Payouts.d.ts.map +1 -1
  106. package/dist/react/workflows/Payouts.js +2 -2
  107. package/dist/react/workflows/Payouts.js.map +1 -1
  108. package/docs/accounts-v2-preview.md +132 -0
  109. package/docs/react-workflows.md +5 -2
  110. package/docs/settlement.md +158 -0
  111. package/package.json +3 -1
  112. package/src/client/apiVersion.ts +20 -0
  113. package/src/client/billing/checkout-idempotency.test.ts +82 -0
  114. package/src/client/billing/checkout.ts +6 -1
  115. package/src/client/billing/providerReads.test.ts +60 -0
  116. package/src/client/billing/providerReads.ts +30 -0
  117. package/src/client/billing/subscriptions.ts +1 -8
  118. package/src/client/connect/payoutSnapshot.test.ts +91 -0
  119. package/src/client/connect/payoutSnapshot.ts +130 -0
  120. package/src/client/connect/transfers.test.ts +15 -4
  121. package/src/client/connect/transfers.ts +13 -42
  122. package/src/client/core/account-idempotency.test.ts +78 -0
  123. package/src/client/core/accounts.ts +18 -25
  124. package/src/client/core/idempotency.test.ts +10 -0
  125. package/src/client/core/idempotency.ts +11 -0
  126. package/src/client/customer-preview-thin.test.ts +41 -0
  127. package/src/client/customer-preview-webhooks.test.ts +150 -0
  128. package/src/client/customer-preview.test.ts +185 -0
  129. package/src/client/index.ts +115 -119
  130. package/src/client/previewRoutes.ts +26 -0
  131. package/src/client/settlement/allocation.test.ts +98 -0
  132. package/src/client/settlement/allocation.ts +110 -0
  133. package/src/client/settlement/commissions.test.ts +103 -0
  134. package/src/client/settlement/commissions.ts +125 -0
  135. package/src/client/settlement/execution.test.ts +186 -0
  136. package/src/client/settlement/execution.ts +134 -0
  137. package/src/client/settlement/reconciliation.ts +119 -0
  138. package/src/client/settlement/settleSale.ts +59 -0
  139. package/src/client/settlement/verifiedSale.test.ts +139 -0
  140. package/src/client/settlement/verifiedSale.ts +159 -0
  141. package/src/client/stripe-version.test.ts +10 -4
  142. package/src/client/types/options.ts +3 -0
  143. package/src/client/utils/webhookEndpoints.ts +70 -15
  144. package/src/client/webhooks/asyncHooks.ts +38 -0
  145. package/src/client/webhooks/processors.test.ts +9 -9
  146. package/src/client/webhooks/v2.ts +14 -10
  147. package/src/component/_generated/api.ts +4 -0
  148. package/src/component/_generated/component.ts +100 -0
  149. package/src/component/lib/stripeFactory.ts +13 -5
  150. package/src/component/schema.ts +8 -0
  151. package/src/component/settlement/mutations.ts +119 -0
  152. package/src/component/settlement/queries.ts +38 -0
  153. package/src/component/settlement/schema.ts +45 -0
  154. package/src/component/settlement-claims.test.ts +77 -0
  155. package/src/react/components/EmbeddedDisputes.render.test.tsx +2 -2
  156. package/src/react/index.payment-element.test.ts +22 -37
  157. package/src/react/workflows/Checkout.test.tsx +30 -0
  158. package/src/react/workflows/Checkout.tsx +10 -3
  159. package/src/react/workflows/Payouts.test.tsx +33 -0
  160. 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
- ? [...BETTER_STRIPE_V2_WEBHOOK_EVENTS]
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
- events_from: payloadType === "thin" ? ["self", "other_accounts"] : ["self"],
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
- // `claimReversalSlices` returns the slices the reversal engines execute
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
- include: [
79
- "configuration.merchant",
80
- "configuration.recipient",
81
- "configuration.customer",
82
- "identity",
83
- "requirements",
84
- ] satisfies NonNullable<
85
- Parameters<Stripe["v2"]["core"]["accounts"]["retrieve"]>[1]
86
- >["include"],
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 { STRIPE_API_VERSION } from "../../client/constants.js";
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
- * Uses the component's pinned API version.
21
+ * Defaults to GA; an explicitly selected supported preview gets its own cache entry.
18
22
  */
19
- export function getStripe(apiKey: string): Stripe {
20
- const cacheKey = `${apiKey}:${STRIPE_API_VERSION}`;
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: STRIPE_API_VERSION,
32
+ apiVersion: version as StripeApiVersion,
25
33
  typescript: true,
26
34
  });
27
35
  _clients.set(cacheKey, client);
@@ -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"]);