@fantastic.dev/better-stripe 0.4.0 → 0.6.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 +61 -0
- package/MIGRATION.md +135 -0
- package/README.md +23 -2
- package/dist/client/billing/checkout.d.ts.map +1 -1
- package/dist/client/billing/checkout.js +22 -8
- package/dist/client/billing/checkout.js.map +1 -1
- package/dist/client/billing/subscriptionRouting.d.ts +24 -0
- package/dist/client/billing/subscriptionRouting.d.ts.map +1 -0
- package/dist/client/billing/subscriptionRouting.js +19 -0
- package/dist/client/billing/subscriptionRouting.js.map +1 -0
- package/dist/client/billing/subscriptions.d.ts +6 -6
- package/dist/client/billing/subscriptions.d.ts.map +1 -1
- package/dist/client/billing/subscriptions.js +31 -12
- package/dist/client/billing/subscriptions.js.map +1 -1
- package/dist/client/connect/disputes.js.map +1 -1
- package/dist/client/constants.js +1 -1
- package/dist/client/core/accountLinks.d.ts +1 -0
- package/dist/client/core/accountLinks.d.ts.map +1 -1
- package/dist/client/core/accountLinks.js +0 -6
- package/dist/client/core/accountLinks.js.map +1 -1
- package/dist/client/core/accountStatus.d.ts.map +1 -1
- package/dist/client/core/accountStatus.js +23 -8
- package/dist/client/core/accountStatus.js.map +1 -1
- package/dist/client/index.d.ts +5 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/types/documents.d.ts +1 -0
- package/dist/client/types/documents.d.ts.map +1 -1
- package/dist/client/types/events.d.ts +2 -1
- package/dist/client/types/events.d.ts.map +1 -1
- package/dist/client/types/options.d.ts +7 -0
- package/dist/client/types/options.d.ts.map +1 -1
- package/dist/client/utils/stripeDashboardUrl.d.ts.map +1 -1
- package/dist/client/utils/webhookEndpoints.d.ts.map +1 -1
- package/dist/client/webhooks/handler.d.ts.map +1 -1
- package/dist/client/webhooks/handler.js +35 -1
- package/dist/client/webhooks/handler.js.map +1 -1
- package/dist/client/webhooks/hooks.d.ts +1 -1
- package/dist/client/webhooks/hooks.d.ts.map +1 -1
- package/dist/client/webhooks/hooks.js +5 -1
- package/dist/client/webhooks/hooks.js.map +1 -1
- package/dist/client/webhooks/processors.d.ts.map +1 -1
- package/dist/client/webhooks/processors.js +12 -2
- package/dist/client/webhooks/processors.js.map +1 -1
- package/dist/client/webhooks/v2.d.ts.map +1 -1
- package/dist/client/webhooks/v2.js +11 -3
- package/dist/client/webhooks/v2.js.map +1 -1
- package/dist/component/_generated/component.d.ts +8 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +7 -1
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js +2 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/billing/mutations.d.ts +29 -28
- package/dist/component/billing/mutations.d.ts.map +1 -1
- package/dist/component/billing/mutations.js +1 -0
- package/dist/component/billing/mutations.js.map +1 -1
- package/dist/component/billing/queries.d.ts +138 -131
- package/dist/component/billing/queries.d.ts.map +1 -1
- package/dist/component/billing/schema.d.ts +67 -65
- package/dist/component/billing/schema.d.ts.map +1 -1
- package/dist/component/billing/validators.d.ts +94 -91
- package/dist/component/billing/validators.d.ts.map +1 -1
- package/dist/component/billing/validators.js +1 -0
- package/dist/component/billing/validators.js.map +1 -1
- package/dist/component/connect/mutations.d.ts +37 -37
- package/dist/component/connect/queries.d.ts +115 -115
- package/dist/component/connect/queries.d.ts.map +1 -1
- package/dist/component/connect/queries.js +34 -71
- package/dist/component/connect/queries.js.map +1 -1
- package/dist/component/connect/schema.d.ts +73 -73
- package/dist/component/connect/validators.d.ts +119 -119
- package/dist/component/connect/validators.d.ts.map +1 -1
- package/dist/component/convex.config.d.ts +1 -0
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +2 -1
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/core/mutations.d.ts +12 -12
- package/dist/component/core/queries.d.ts +34 -34
- package/dist/component/core/schema.d.ts +11 -11
- package/dist/component/core/validators.d.ts +14 -14
- package/dist/component/core/validators.d.ts.map +1 -1
- package/dist/component/lib/fees.d.ts +12 -12
- package/dist/component/lib/fees.d.ts.map +1 -1
- package/dist/component/lib/fees.js +1 -1
- package/dist/component/lib/reversals.js.map +1 -1
- package/dist/component/products/mutations.d.ts +7 -7
- package/dist/component/products/queries.d.ts +33 -33
- package/dist/component/products/schema.d.ts +15 -15
- package/dist/component/products/validators.d.ts +16 -16
- package/dist/component/products/validators.d.ts.map +1 -1
- package/dist/component/schema.d.ts +173 -171
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/webhooks/mutations.d.ts +3 -3
- package/dist/component/webhooks/queries.d.ts +12 -12
- package/dist/component/webhooks/schema.d.ts +7 -7
- package/dist/component/webhooks/validators.d.ts +10 -10
- package/dist/component/webhooks/validators.d.ts.map +1 -1
- package/dist/react/components/AccountCreateCard.js.map +1 -1
- package/dist/react/components/BuyerBillingView.js +1 -1
- package/dist/react/components/CheckoutSessionProviderWithKey.d.ts +1 -1
- package/dist/react/components/DisputeDetail.js +1 -1
- package/dist/react/components/DisputesList.js +1 -1
- package/dist/react/components/EarningsSummary.js +1 -1
- package/dist/react/components/EmbeddedCheckout.d.ts.map +1 -1
- package/dist/react/components/EmbeddedCheckout.js +1 -1
- package/dist/react/components/EmbeddedCheckout.js.map +1 -1
- package/dist/react/components/PayoutSchedule.js +1 -1
- package/dist/react/components/SplitBreakdown.js +1 -1
- package/dist/react/components/StripeProvider.d.ts +1 -1
- package/dist/react/components/StripeProviderWithKey.d.ts +1 -1
- package/dist/react/hooks/useAccount.d.ts.map +1 -1
- package/dist/react/hooks/useAccountOnboarding.d.ts.map +1 -1
- package/dist/react/hooks/useCheckout.d.ts.map +1 -1
- package/dist/react/hooks/useDisputeWithCountdown.d.ts.map +1 -1
- package/dist/react/hooks/useDisputes.d.ts.map +1 -1
- package/dist/react/hooks/useEarnings.d.ts.map +1 -1
- package/dist/react/hooks/useInvoices.d.ts.map +1 -1
- package/dist/react/hooks/usePaymentMethodActions.js.map +1 -1
- package/dist/react/hooks/usePaymentMethods.d.ts.map +1 -1
- package/dist/react/hooks/usePrices.d.ts.map +1 -1
- package/dist/react/hooks/useProducts.d.ts.map +1 -1
- package/dist/react/hooks/useSplitBreakdown.d.ts.map +1 -1
- package/dist/react/hooks/useSubscription.d.ts.map +1 -1
- package/dist/react/hooks/useSubscriptions.d.ts.map +1 -1
- package/dist/react/lib/stripe-element-styles.d.ts.map +1 -1
- package/dist/react/lib/stripe-element-styles.js.map +1 -1
- package/package.json +25 -23
- package/src/client/billing/checkout.test.ts +62 -14
- package/src/client/billing/checkout.ts +26 -10
- package/src/client/billing/subscriptionRouting.ts +25 -0
- package/src/client/billing/subscriptions.test.ts +90 -28
- package/src/client/billing/subscriptions.ts +53 -18
- package/src/client/constants.ts +1 -1
- package/src/client/core/accountLinks.test.ts +20 -13
- package/src/client/core/accountLinks.ts +2 -10
- package/src/client/core/accountStatus.test.ts +67 -0
- package/src/client/core/accountStatus.ts +30 -13
- package/src/client/helpers.test.ts +2 -2
- package/src/client/index.ts +49 -44
- package/src/client/stripe-version.test.ts +47 -0
- package/src/client/types/documents.ts +1 -0
- package/src/client/types/events.ts +2 -1
- package/src/client/types/options.ts +7 -0
- package/src/client/webhooks/handler.ts +38 -1
- package/src/client/webhooks/hooks.ts +3 -0
- package/src/client/webhooks/processors.test.ts +201 -40
- package/src/client/webhooks/processors.ts +21 -17
- package/src/client/webhooks/retry.integration.test.ts +196 -0
- package/src/client/webhooks/v2.test.ts +36 -0
- package/src/client/webhooks/v2.ts +14 -4
- package/src/component/_generated/component.ts +15 -28
- package/src/component/_generated/server.ts +9 -2
- package/src/component/billing/mutations.ts +1 -0
- package/src/component/billing/validators.ts +1 -0
- package/src/component/config-queries.test.ts +8 -4
- package/src/component/connect/queries.ts +31 -66
- package/src/component/convex.config.ts +2 -1
- package/src/component/earnings-totals.test.ts +27 -1
- package/src/component/lib/fees.ts +1 -1
- package/src/react/components/EmbeddedCheckout.render.test.tsx +57 -0
- package/src/react/components/EmbeddedCheckout.tsx +1 -0
|
@@ -45,7 +45,8 @@ function makeStripe() {
|
|
|
45
45
|
type MockStripe = ReturnType<typeof makeStripe>;
|
|
46
46
|
|
|
47
47
|
// Cast helper — the mock only implements the slice of Stripe these functions use.
|
|
48
|
-
const asStripe = (s: MockStripe) =>
|
|
48
|
+
const asStripe = (s: MockStripe) =>
|
|
49
|
+
s as unknown as Parameters<typeof createAccountLink>[0];
|
|
49
50
|
|
|
50
51
|
describe("createAccountLink", () => {
|
|
51
52
|
it("passes through params and returns the url", async () => {
|
|
@@ -94,7 +95,6 @@ describe("createV2AccountLink", () => {
|
|
|
94
95
|
use_case: {
|
|
95
96
|
type: "account_onboarding",
|
|
96
97
|
account_onboarding: {
|
|
97
|
-
configurations: ["merchant"],
|
|
98
98
|
refresh_url: "https://refresh",
|
|
99
99
|
return_url: "https://return",
|
|
100
100
|
},
|
|
@@ -122,7 +122,6 @@ describe("createV2AccountLink", () => {
|
|
|
122
122
|
use_case: {
|
|
123
123
|
type: "account_update",
|
|
124
124
|
account_update: {
|
|
125
|
-
configurations: ["recipient"],
|
|
126
125
|
refresh_url: "https://refresh",
|
|
127
126
|
return_url: "https://return",
|
|
128
127
|
},
|
|
@@ -130,7 +129,7 @@ describe("createV2AccountLink", () => {
|
|
|
130
129
|
});
|
|
131
130
|
});
|
|
132
131
|
|
|
133
|
-
it("
|
|
132
|
+
it("omits configurations for account_update", async () => {
|
|
134
133
|
const stripe = makeStripe();
|
|
135
134
|
stripe.v2.core.accountLinks.create.mockResolvedValue({
|
|
136
135
|
url: "https://v2",
|
|
@@ -145,10 +144,10 @@ describe("createV2AccountLink", () => {
|
|
|
145
144
|
});
|
|
146
145
|
|
|
147
146
|
const arg = stripe.v2.core.accountLinks.create.mock.calls[0][0];
|
|
148
|
-
expect(arg.use_case.account_update
|
|
147
|
+
expect(arg.use_case.account_update).not.toHaveProperty("configurations");
|
|
149
148
|
});
|
|
150
149
|
|
|
151
|
-
it("
|
|
150
|
+
it("omits configurations for account_onboarding", async () => {
|
|
152
151
|
const stripe = makeStripe();
|
|
153
152
|
stripe.v2.core.accountLinks.create.mockResolvedValue({
|
|
154
153
|
url: "https://v2",
|
|
@@ -163,7 +162,9 @@ describe("createV2AccountLink", () => {
|
|
|
163
162
|
});
|
|
164
163
|
|
|
165
164
|
const arg = stripe.v2.core.accountLinks.create.mock.calls[0][0];
|
|
166
|
-
expect(arg.use_case.account_onboarding
|
|
165
|
+
expect(arg.use_case.account_onboarding).not.toHaveProperty(
|
|
166
|
+
"configurations",
|
|
167
|
+
);
|
|
167
168
|
});
|
|
168
169
|
});
|
|
169
170
|
|
|
@@ -283,9 +284,11 @@ describe("getAccountLinkWithStatus", () => {
|
|
|
283
284
|
expect(result).toEqual({ url: "https://onboard", linkType: "onboarding" });
|
|
284
285
|
// login link must NOT be used while requirements are outstanding
|
|
285
286
|
expect(stripe.accounts.createLoginLink).not.toHaveBeenCalled();
|
|
286
|
-
//
|
|
287
|
+
// Stripe 23 collects requirements without a configurations parameter.
|
|
287
288
|
const arg = stripe.v2.core.accountLinks.create.mock.calls[0][0];
|
|
288
|
-
expect(arg.use_case.account_onboarding
|
|
289
|
+
expect(arg.use_case.account_onboarding).not.toHaveProperty(
|
|
290
|
+
"configurations",
|
|
291
|
+
);
|
|
289
292
|
});
|
|
290
293
|
|
|
291
294
|
it("returns a login link for an express dashboard account with no requirements", async () => {
|
|
@@ -352,15 +355,19 @@ describe("getAccountLinkWithStatus", () => {
|
|
|
352
355
|
|
|
353
356
|
expect(result.linkType).toBe("onboarding");
|
|
354
357
|
const arg = stripe.v2.core.accountLinks.create.mock.calls[0][0];
|
|
355
|
-
//
|
|
356
|
-
expect(arg.use_case.account_onboarding
|
|
358
|
+
// No configurations parameter is sent.
|
|
359
|
+
expect(arg.use_case.account_onboarding).not.toHaveProperty(
|
|
360
|
+
"configurations",
|
|
361
|
+
);
|
|
357
362
|
});
|
|
358
363
|
});
|
|
359
364
|
|
|
360
365
|
describe("createDisputeSession (BTS-30)", () => {
|
|
361
366
|
it("creates an account session with the disputes components for the seller", async () => {
|
|
362
367
|
const stripe = makeStripe();
|
|
363
|
-
stripe.accountSessions.create.mockResolvedValue({
|
|
368
|
+
stripe.accountSessions.create.mockResolvedValue({
|
|
369
|
+
client_secret: "acs_secret",
|
|
370
|
+
});
|
|
364
371
|
|
|
365
372
|
const result = await createDisputeSession(asStripe(stripe), {} as RunCtx, {
|
|
366
373
|
stripeAccountId: "acct_seller",
|
|
@@ -375,4 +382,4 @@ describe("createDisputeSession (BTS-30)", () => {
|
|
|
375
382
|
});
|
|
376
383
|
expect(result).toEqual({ clientSecret: "acs_secret" });
|
|
377
384
|
});
|
|
378
|
-
})
|
|
385
|
+
});
|
|
@@ -2,10 +2,7 @@ import type Stripe from "stripe";
|
|
|
2
2
|
|
|
3
3
|
import { throwStripeError } from "../errors.js";
|
|
4
4
|
import type { RunCtx } from "../helpers.js";
|
|
5
|
-
import type {
|
|
6
|
-
V2AccountLinkCreateParams,
|
|
7
|
-
V2AppliedConfiguration,
|
|
8
|
-
} from "../stripe-types.js";
|
|
5
|
+
import type { V2AccountLinkCreateParams } from "../stripe-types.js";
|
|
9
6
|
import type { AccountLinkWithStatus } from "../types.js";
|
|
10
7
|
|
|
11
8
|
// =============================================================================
|
|
@@ -39,6 +36,7 @@ export async function createV2AccountLink(
|
|
|
39
36
|
type: "account_onboarding" | "account_update";
|
|
40
37
|
refreshUrl: string;
|
|
41
38
|
returnUrl: string;
|
|
39
|
+
/** @deprecated Endive collects requirements without configurations. Ignored. */
|
|
42
40
|
configurations?: string[];
|
|
43
41
|
},
|
|
44
42
|
) {
|
|
@@ -47,8 +45,6 @@ export async function createV2AccountLink(
|
|
|
47
45
|
? {
|
|
48
46
|
type: "account_update",
|
|
49
47
|
account_update: {
|
|
50
|
-
configurations: (opts.configurations ??
|
|
51
|
-
[]) as V2AppliedConfiguration[],
|
|
52
48
|
refresh_url: opts.refreshUrl,
|
|
53
49
|
return_url: opts.returnUrl,
|
|
54
50
|
},
|
|
@@ -56,8 +52,6 @@ export async function createV2AccountLink(
|
|
|
56
52
|
: {
|
|
57
53
|
type: "account_onboarding",
|
|
58
54
|
account_onboarding: {
|
|
59
|
-
configurations: (opts.configurations ??
|
|
60
|
-
[]) as V2AppliedConfiguration[],
|
|
61
55
|
refresh_url: opts.refreshUrl,
|
|
62
56
|
return_url: opts.returnUrl,
|
|
63
57
|
},
|
|
@@ -154,11 +148,9 @@ export async function getAccountLinkWithStatus(
|
|
|
154
148
|
});
|
|
155
149
|
|
|
156
150
|
const getOnboardingLink = async (): Promise<AccountLinkWithStatus> => {
|
|
157
|
-
const configs = account.applied_configurations ?? [];
|
|
158
151
|
const link = await createV2AccountLink(stripe, ctx, {
|
|
159
152
|
stripeAccountId: opts.stripeAccountId,
|
|
160
153
|
type: "account_onboarding",
|
|
161
|
-
configurations: configs,
|
|
162
154
|
refreshUrl: opts.refreshUrl,
|
|
163
155
|
returnUrl: opts.returnUrl,
|
|
164
156
|
});
|
|
@@ -28,6 +28,73 @@ function makeEntry(
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
describe("deriveAccountStatus", () => {
|
|
31
|
+
it.each(["rejected", "restricted"] as const)(
|
|
32
|
+
"maps an Endive %s capability to restricted even when the config is applied",
|
|
33
|
+
(status) => {
|
|
34
|
+
expect(
|
|
35
|
+
deriveAccountStatus({
|
|
36
|
+
configuration: {
|
|
37
|
+
merchant: {
|
|
38
|
+
applied: true,
|
|
39
|
+
capabilities: { card_payments: { status } },
|
|
40
|
+
} as V2AccountConfigMerchant,
|
|
41
|
+
},
|
|
42
|
+
}).onboardingStatus,
|
|
43
|
+
).toBe("restricted");
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
it("keeps pending nested capabilities in progress", () => {
|
|
48
|
+
expect(
|
|
49
|
+
deriveAccountStatus({
|
|
50
|
+
configuration: {
|
|
51
|
+
merchant: {
|
|
52
|
+
applied: true,
|
|
53
|
+
capabilities: {
|
|
54
|
+
stripe_balance: { payouts: { status: "pending" } },
|
|
55
|
+
},
|
|
56
|
+
} as V2AccountConfigMerchant,
|
|
57
|
+
},
|
|
58
|
+
}).onboardingStatus,
|
|
59
|
+
).toBe("in_progress");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("prioritizes rejected capabilities over requirements awaiting Stripe", () => {
|
|
63
|
+
expect(
|
|
64
|
+
deriveAccountStatus({
|
|
65
|
+
requirements: {
|
|
66
|
+
entries: [
|
|
67
|
+
makeEntry("identity_document", { awaiting_action_from: "stripe" }),
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
configuration: {
|
|
71
|
+
merchant: {
|
|
72
|
+
applied: true,
|
|
73
|
+
capabilities: { card_payments: { status: "rejected" } },
|
|
74
|
+
} as V2AccountConfigMerchant,
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
).toMatchObject({
|
|
78
|
+
onboardingStatus: "restricted",
|
|
79
|
+
missingRequirements: ["identity_document"],
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("permits active capabilities and ignores unsupported optional methods", () => {
|
|
84
|
+
expect(
|
|
85
|
+
deriveAccountStatus({
|
|
86
|
+
configuration: {
|
|
87
|
+
merchant: {
|
|
88
|
+
applied: true,
|
|
89
|
+
capabilities: {
|
|
90
|
+
card_payments: { status: "active" },
|
|
91
|
+
ach_debit_payments: { status: "unsupported" },
|
|
92
|
+
},
|
|
93
|
+
} as V2AccountConfigMerchant,
|
|
94
|
+
},
|
|
95
|
+
}).onboardingStatus,
|
|
96
|
+
).toBe("complete");
|
|
97
|
+
});
|
|
31
98
|
it("returns pending when no requirements and no config", () => {
|
|
32
99
|
const status = deriveAccountStatus({});
|
|
33
100
|
expect(status.onboardingStatus).toBe("pending");
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type Stripe from "stripe";
|
|
2
2
|
|
|
3
3
|
export type BetterStripeOnboardingStatus =
|
|
4
|
-
| "
|
|
5
|
-
| "in_progress"
|
|
6
|
-
| "complete"
|
|
7
|
-
| "restricted";
|
|
4
|
+
"pending" | "in_progress" | "complete" | "restricted";
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* Accepts a V2 Account (or partial shape with configuration/requirements).
|
|
@@ -14,6 +11,14 @@ type StripeAccountLike = Pick<
|
|
|
14
11
|
"configuration" | "requirements"
|
|
15
12
|
>;
|
|
16
13
|
|
|
14
|
+
/** Capability groups such as stripe_balance nest their leaf statuses. */
|
|
15
|
+
function capabilityStatuses(value: unknown): string[] {
|
|
16
|
+
if (!value || typeof value !== "object") return [];
|
|
17
|
+
const record = value as Record<string, unknown>;
|
|
18
|
+
if (typeof record.status === "string") return [record.status];
|
|
19
|
+
return Object.values(record).flatMap(capabilityStatuses);
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export function deriveAccountStatus(account: StripeAccountLike): {
|
|
18
23
|
onboardingStatus: BetterStripeOnboardingStatus;
|
|
19
24
|
missingRequirements: string[];
|
|
@@ -53,6 +58,25 @@ export function deriveAccountStatus(account: StripeAccountLike): {
|
|
|
53
58
|
};
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
const configuration = account.configuration;
|
|
62
|
+
const configs = [
|
|
63
|
+
configuration?.customer,
|
|
64
|
+
configuration?.merchant,
|
|
65
|
+
configuration?.recipient,
|
|
66
|
+
].filter(Boolean);
|
|
67
|
+
const statuses = configs.flatMap((cfg) =>
|
|
68
|
+
capabilityStatuses(cfg?.capabilities),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// An applied configuration does not imply that its capabilities are active.
|
|
72
|
+
// Endive adds rejected to the V2 capability status enum. A blocked capability
|
|
73
|
+
// takes precedence over requirements still awaiting Stripe verification.
|
|
74
|
+
if (
|
|
75
|
+
statuses.some((status) => status === "restricted" || status === "rejected")
|
|
76
|
+
) {
|
|
77
|
+
return { onboardingStatus: "restricted", missingRequirements };
|
|
78
|
+
}
|
|
79
|
+
|
|
56
80
|
if (missingRequirements.length > 0) {
|
|
57
81
|
return {
|
|
58
82
|
onboardingStatus: "in_progress",
|
|
@@ -60,7 +84,6 @@ export function deriveAccountStatus(account: StripeAccountLike): {
|
|
|
60
84
|
};
|
|
61
85
|
}
|
|
62
86
|
|
|
63
|
-
const configuration = account.configuration;
|
|
64
87
|
if (!configuration) {
|
|
65
88
|
return {
|
|
66
89
|
onboardingStatus: "pending",
|
|
@@ -68,13 +91,6 @@ export function deriveAccountStatus(account: StripeAccountLike): {
|
|
|
68
91
|
};
|
|
69
92
|
}
|
|
70
93
|
|
|
71
|
-
// V2 configurations use `applied: boolean` to indicate active state
|
|
72
|
-
const configs = [
|
|
73
|
-
configuration.customer,
|
|
74
|
-
configuration.merchant,
|
|
75
|
-
configuration.recipient,
|
|
76
|
-
].filter(Boolean);
|
|
77
|
-
|
|
78
94
|
if (configs.length === 0) {
|
|
79
95
|
return {
|
|
80
96
|
onboardingStatus: "pending",
|
|
@@ -85,7 +101,8 @@ export function deriveAccountStatus(account: StripeAccountLike): {
|
|
|
85
101
|
const allApplied = configs.every((cfg) => cfg?.applied === true);
|
|
86
102
|
|
|
87
103
|
return {
|
|
88
|
-
onboardingStatus:
|
|
104
|
+
onboardingStatus:
|
|
105
|
+
allApplied && !statuses.includes("pending") ? "complete" : "in_progress",
|
|
89
106
|
missingRequirements,
|
|
90
107
|
};
|
|
91
108
|
}
|
|
@@ -4,8 +4,8 @@ import { STRIPE_API_VERSION } from "./constants.js";
|
|
|
4
4
|
import { epochToIso, getStripeClient, runMutationOrThrow } from "./helpers.js";
|
|
5
5
|
|
|
6
6
|
describe("STRIPE_API_VERSION", () => {
|
|
7
|
-
it("
|
|
8
|
-
expect(STRIPE_API_VERSION).
|
|
7
|
+
it("matches the installed SDK API version", () => {
|
|
8
|
+
expect(STRIPE_API_VERSION).toBe("2026-09-30.endive");
|
|
9
9
|
});
|
|
10
10
|
});
|
|
11
11
|
|
package/src/client/index.ts
CHANGED
|
@@ -679,15 +679,20 @@ export class BetterStripe {
|
|
|
679
679
|
},
|
|
680
680
|
) {
|
|
681
681
|
const { fee, ...rest } = opts;
|
|
682
|
-
return checkoutImpl.createCheckoutSession(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
682
|
+
return checkoutImpl.createCheckoutSession(
|
|
683
|
+
this.stripe(),
|
|
684
|
+
this.component,
|
|
685
|
+
ctx,
|
|
686
|
+
{
|
|
687
|
+
...rest,
|
|
688
|
+
// Fees apply to destination or split sales; resolve override → default.
|
|
689
|
+
feeConfig:
|
|
690
|
+
opts.destinationAccountId || opts.split?.length
|
|
691
|
+
? this.resolveFee(fee)
|
|
692
|
+
: undefined,
|
|
693
|
+
defaultStatementDescriptorSuffix: this._statementDescriptorSuffix,
|
|
694
|
+
},
|
|
695
|
+
);
|
|
691
696
|
}
|
|
692
697
|
|
|
693
698
|
async getCheckoutSession(
|
|
@@ -760,20 +765,27 @@ export class BetterStripe {
|
|
|
760
765
|
split?: SplitRecipient[];
|
|
761
766
|
fee?: FeeOverride;
|
|
762
767
|
trialDays?: number;
|
|
768
|
+
/** Attached payment method to use for this subscription. */
|
|
769
|
+
defaultPaymentMethod?: string;
|
|
763
770
|
metadata?: Record<string, string>;
|
|
764
771
|
/** Stripe idempotency key for retry-safe subscription creation. */
|
|
765
772
|
idempotencyKey?: string;
|
|
766
773
|
},
|
|
767
774
|
) {
|
|
768
775
|
const { fee, ...rest } = opts;
|
|
769
|
-
return subscriptionsImpl.createSubscription(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
776
|
+
return subscriptionsImpl.createSubscription(
|
|
777
|
+
this.stripe(),
|
|
778
|
+
this.component,
|
|
779
|
+
ctx,
|
|
780
|
+
{
|
|
781
|
+
...rest,
|
|
782
|
+
feeConfig:
|
|
783
|
+
opts.destinationAccountId || opts.split?.length
|
|
784
|
+
? this.resolveFee(fee)
|
|
785
|
+
: undefined,
|
|
786
|
+
defaultStatementDescriptorSuffix: this._statementDescriptorSuffix,
|
|
787
|
+
},
|
|
788
|
+
);
|
|
777
789
|
}
|
|
778
790
|
|
|
779
791
|
async getSubscription(
|
|
@@ -1083,6 +1095,7 @@ export class BetterStripe {
|
|
|
1083
1095
|
type: "account_onboarding" | "account_update";
|
|
1084
1096
|
refreshUrl: string;
|
|
1085
1097
|
returnUrl: string;
|
|
1098
|
+
/** @deprecated Endive collects requirements without configurations. Ignored. */
|
|
1086
1099
|
configurations?: string[];
|
|
1087
1100
|
},
|
|
1088
1101
|
) {
|
|
@@ -1169,7 +1182,12 @@ export class BetterStripe {
|
|
|
1169
1182
|
operationId?: string;
|
|
1170
1183
|
},
|
|
1171
1184
|
) {
|
|
1172
|
-
return transfersImpl.reverseTransfers(
|
|
1185
|
+
return transfersImpl.reverseTransfers(
|
|
1186
|
+
this.stripe(),
|
|
1187
|
+
this.component,
|
|
1188
|
+
ctx,
|
|
1189
|
+
opts,
|
|
1190
|
+
);
|
|
1173
1191
|
}
|
|
1174
1192
|
|
|
1175
1193
|
/**
|
|
@@ -1799,44 +1817,31 @@ export class BetterStripe {
|
|
|
1799
1817
|
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined
|
|
1800
1818
|
> = {
|
|
1801
1819
|
afterAccountUpdated: hooks?.onAccountUpdated as
|
|
1802
|
-
|
|
1803
|
-
| undefined,
|
|
1820
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1804
1821
|
afterCheckoutCompleted: hooks?.onCheckoutCompleted as
|
|
1805
|
-
|
|
1806
|
-
| undefined,
|
|
1822
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1807
1823
|
afterSubscriptionUpdated: hooks?.onSubscriptionUpdated as
|
|
1808
|
-
|
|
1809
|
-
| undefined,
|
|
1824
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1810
1825
|
afterSubscriptionCanceled: hooks?.onSubscriptionCanceled as
|
|
1811
|
-
|
|
1812
|
-
| undefined,
|
|
1826
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1813
1827
|
afterTrialEnding: hooks?.onTrialEnding as
|
|
1814
|
-
|
|
1815
|
-
| undefined,
|
|
1828
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1816
1829
|
afterInvoicePaid: hooks?.onInvoicePaid as
|
|
1817
|
-
|
|
1818
|
-
| undefined,
|
|
1830
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1819
1831
|
afterInvoicePaymentFailed: hooks?.onInvoicePaymentFailed as
|
|
1820
|
-
|
|
1821
|
-
| undefined,
|
|
1832
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1822
1833
|
afterPaymentSucceeded: hooks?.onPaymentSucceeded as
|
|
1823
|
-
|
|
1824
|
-
| undefined,
|
|
1834
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1825
1835
|
afterPaymentFailed: hooks?.onPaymentFailed as
|
|
1826
|
-
|
|
1827
|
-
| undefined,
|
|
1836
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1828
1837
|
afterPayoutCompleted: hooks?.onPayoutCompleted as
|
|
1829
|
-
|
|
1830
|
-
| undefined,
|
|
1838
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1831
1839
|
afterRefundCreated: hooks?.onRefundCreated as
|
|
1832
|
-
|
|
1833
|
-
| undefined,
|
|
1840
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1834
1841
|
afterDisputeCreated: hooks?.onDisputeCreated as
|
|
1835
|
-
|
|
1836
|
-
| undefined,
|
|
1842
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1837
1843
|
afterDisputeClosed: hooks?.onDisputeClosed as
|
|
1838
|
-
|
|
1839
|
-
| undefined,
|
|
1844
|
+
((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
|
|
1840
1845
|
};
|
|
1841
1846
|
|
|
1842
1847
|
const syncWebhook = internalMutationGeneric({
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { getStripe } from "../component/lib/stripeFactory.js";
|
|
5
|
+
import { STRIPE_API_VERSION } from "./constants.js";
|
|
6
|
+
import { getStripeClient } from "./helpers.js";
|
|
7
|
+
|
|
8
|
+
describe("Stripe version pin", () => {
|
|
9
|
+
it("matches the supported stripe-node API, including the full endive suffix", () => {
|
|
10
|
+
expect(STRIPE_API_VERSION).toBe("2026-09-30.endive");
|
|
11
|
+
expect(Stripe.API_VERSION).toBe(STRIPE_API_VERSION);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it.each([getStripeClient, getStripe])(
|
|
15
|
+
"pins outgoing API requests from each factory",
|
|
16
|
+
async (factory) => {
|
|
17
|
+
const stripe = factory("sk_test_version_transport_only");
|
|
18
|
+
let version: string | undefined;
|
|
19
|
+
// Factories deliberately expose no transport override. Replace only the
|
|
20
|
+
// SDK's transport slot, leaving its request serializer/headers untouched.
|
|
21
|
+
const transportSlot = stripe as unknown as {
|
|
22
|
+
_setApiField: (field: string, value: unknown) => void;
|
|
23
|
+
};
|
|
24
|
+
transportSlot._setApiField("httpClient", {
|
|
25
|
+
getClientName: () => "test",
|
|
26
|
+
makeRequest: async (
|
|
27
|
+
...[, , , , headers]: Parameters<
|
|
28
|
+
ReturnType<typeof Stripe.createNodeHttpClient>["makeRequest"]
|
|
29
|
+
>
|
|
30
|
+
) => {
|
|
31
|
+
version = String(headers["Stripe-Version"]);
|
|
32
|
+
return {
|
|
33
|
+
getStatusCode: () => 200,
|
|
34
|
+
getHeaders: () => ({}),
|
|
35
|
+
getRawResponse: () => ({}),
|
|
36
|
+
toJSON: async () => ({ id: "prod_local", object: "product" }),
|
|
37
|
+
toStream: () => {
|
|
38
|
+
throw new Error("Unexpected stream");
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
await stripe.products.retrieve("prod_local");
|
|
44
|
+
expect(version).toBe(STRIPE_API_VERSION);
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
});
|
|
@@ -79,6 +79,13 @@ export type RegisterRoutesConfig = {
|
|
|
79
79
|
webhookSecretV2?: string;
|
|
80
80
|
onEvent?: StripeEventHandler<StripeWebhookEvent>;
|
|
81
81
|
events?: StripeEventHandlers;
|
|
82
|
+
/**
|
|
83
|
+
* Select verified V1 events whose onEvent/event hooks must succeed before
|
|
84
|
+
* delivery is acknowledged. Failures return 500 and permit redelivery;
|
|
85
|
+
* an in-flight delivery returns 503. Hooks must be idempotent by event.id.
|
|
86
|
+
* V2 events and scheduled asyncWebhook hooks retain best-effort behavior.
|
|
87
|
+
*/
|
|
88
|
+
retryOnHookError?: (event: StripeWebhookEvent) => boolean;
|
|
82
89
|
/**
|
|
83
90
|
* Function references to the app's exported `webhookHandlers()` pair,
|
|
84
91
|
* e.g. `webhooks: internal.stripe` (no cast needed). When provided,
|
|
@@ -179,6 +179,13 @@ export async function handleWebhookRequest(
|
|
|
179
179
|
);
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
let requiresHookSuccess: boolean;
|
|
183
|
+
try {
|
|
184
|
+
requiresHookSuccess = config?.retryOnHookError?.(event) ?? false;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error("[better-stripe] retryOnHookError selector failed:", error);
|
|
187
|
+
return jsonResponse({ error: "Webhook retry policy failed" }, 500);
|
|
188
|
+
}
|
|
182
189
|
// --- Atomic webhook dedup via event ledger ---
|
|
183
190
|
const eventId = event.id;
|
|
184
191
|
console.info(
|
|
@@ -195,6 +202,9 @@ export async function handleWebhookRequest(
|
|
|
195
202
|
},
|
|
196
203
|
);
|
|
197
204
|
if (dedupStatus !== "inserted") {
|
|
205
|
+
if (requiresHookSuccess && dedupStatus === "processing") {
|
|
206
|
+
return jsonResponse({ error: "Event processing in progress" }, 503);
|
|
207
|
+
}
|
|
198
208
|
console.info(`[better-stripe] ↳ deduplicated (already processed)`);
|
|
199
209
|
return jsonResponse({ success: true, deduplicated: true }, 200);
|
|
200
210
|
}
|
|
@@ -204,6 +214,8 @@ export async function handleWebhookRequest(
|
|
|
204
214
|
`[better-stripe] Webhook ledger insert failed for ${eventId}:`,
|
|
205
215
|
error,
|
|
206
216
|
);
|
|
217
|
+
if (requiresHookSuccess)
|
|
218
|
+
return jsonResponse({ error: "Webhook ledger unavailable" }, 500);
|
|
207
219
|
}
|
|
208
220
|
|
|
209
221
|
// --- Process event (sync) ---
|
|
@@ -226,6 +238,29 @@ export async function handleWebhookRequest(
|
|
|
226
238
|
return jsonResponse({ error: "Sync processing failed" }, 500);
|
|
227
239
|
}
|
|
228
240
|
|
|
241
|
+
if (requiresHookSuccess) {
|
|
242
|
+
try {
|
|
243
|
+
await runHooks(ctx, config, event, true);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error(
|
|
246
|
+
`[better-stripe] Required hook failed for ${event.type}:`,
|
|
247
|
+
error,
|
|
248
|
+
);
|
|
249
|
+
try {
|
|
250
|
+
await ctx.runMutation(
|
|
251
|
+
componentRef(component, "webhooks/mutations/markWebhookEventFailed"),
|
|
252
|
+
{
|
|
253
|
+
stripeEventId: eventId,
|
|
254
|
+
error: error instanceof Error ? error.message : String(error),
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
} catch {
|
|
258
|
+
// A stale processing row is recoverable on a later delivery.
|
|
259
|
+
}
|
|
260
|
+
return jsonResponse({ error: "Application fulfillment failed" }, 500);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
229
264
|
// --- Mark processed ---
|
|
230
265
|
try {
|
|
231
266
|
await ctx.runMutation(
|
|
@@ -235,6 +270,8 @@ export async function handleWebhookRequest(
|
|
|
235
270
|
},
|
|
236
271
|
);
|
|
237
272
|
} catch {
|
|
273
|
+
if (requiresHookSuccess)
|
|
274
|
+
return jsonResponse({ error: "Webhook completion unavailable" }, 500);
|
|
238
275
|
/* ledger update best-effort */
|
|
239
276
|
}
|
|
240
277
|
|
|
@@ -244,7 +281,7 @@ export async function handleWebhookRequest(
|
|
|
244
281
|
|
|
245
282
|
// --- Run async hooks (errors must not crash the handler) ---
|
|
246
283
|
try {
|
|
247
|
-
await runHooks(ctx, config, event);
|
|
284
|
+
if (!requiresHookSuccess) await runHooks(ctx, config, event);
|
|
248
285
|
} catch (error) {
|
|
249
286
|
console.error(
|
|
250
287
|
`[better-stripe] Hook runner error for ${event.type}:`,
|
|
@@ -14,11 +14,13 @@ export async function runHooks(
|
|
|
14
14
|
ctx: WebhookActionCtx,
|
|
15
15
|
config: RegisterRoutesConfig | undefined,
|
|
16
16
|
event: StripeWebhookEvent,
|
|
17
|
+
shouldThrow = false,
|
|
17
18
|
): Promise<void> {
|
|
18
19
|
if (config?.onEvent) {
|
|
19
20
|
try {
|
|
20
21
|
await config.onEvent(ctx, event);
|
|
21
22
|
} catch (error) {
|
|
23
|
+
if (shouldThrow) throw error;
|
|
22
24
|
console.error(
|
|
23
25
|
`[better-stripe] onEvent hook error for ${event.type}:`,
|
|
24
26
|
error,
|
|
@@ -31,6 +33,7 @@ export async function runHooks(
|
|
|
31
33
|
try {
|
|
32
34
|
await handler(ctx, event);
|
|
33
35
|
} catch (error) {
|
|
36
|
+
if (shouldThrow) throw error;
|
|
34
37
|
console.error(
|
|
35
38
|
`[better-stripe] Event hook error for ${event.type}:`,
|
|
36
39
|
error,
|