@fantastic.dev/better-stripe 0.10.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 +6 -0
- package/MIGRATION.md +14 -0
- package/README.md +10 -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/index.d.ts +8 -12
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +18 -4
- 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/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/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/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/docs/accounts-v2-preview.md +132 -0
- package/package.json +2 -1
- package/src/client/apiVersion.ts +20 -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 +31 -16
- package/src/client/previewRoutes.ts +26 -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/v2.ts +14 -10
- package/src/component/lib/stripeFactory.ts +13 -5
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { STRIPE_CUSTOMER_PREVIEW_API_VERSION } from "../apiVersion.js";
|
|
2
|
+
import { throwStripeError } from "../errors.js";
|
|
1
3
|
// =============================================================================
|
|
2
4
|
// Webhook endpoint management
|
|
3
5
|
// =============================================================================
|
|
@@ -63,6 +65,12 @@ export const BETTER_STRIPE_V2_WEBHOOK_EVENTS = [
|
|
|
63
65
|
"v2.core.account_person.updated",
|
|
64
66
|
"v2.core.account_link.returned",
|
|
65
67
|
];
|
|
68
|
+
/** Minimal customer lifecycle subscription for non-Connect preview merchants. */
|
|
69
|
+
export const BETTER_STRIPE_CUSTOMER_ACCOUNT_EVENTS = [
|
|
70
|
+
"v2.core.account.created",
|
|
71
|
+
"v2.core.account[configuration.customer].updated",
|
|
72
|
+
"v2.core.account[identity].updated",
|
|
73
|
+
];
|
|
66
74
|
/**
|
|
67
75
|
* All events (V1 + V2) that better-stripe processes.
|
|
68
76
|
* Use with setupEventDestination() for complete webhook coverage.
|
|
@@ -93,6 +101,11 @@ export async function createWebhookEndpoint(stripe, _ctx, opts) {
|
|
|
93
101
|
}
|
|
94
102
|
const endpoint = await stripe.webhookEndpoints.create({
|
|
95
103
|
url: opts.url,
|
|
104
|
+
...(opts.apiVersion
|
|
105
|
+
? {
|
|
106
|
+
api_version: opts.apiVersion,
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
96
109
|
enabled_events: (opts.enabledEvents ??
|
|
97
110
|
BETTER_STRIPE_WEBHOOK_EVENTS),
|
|
98
111
|
description: opts.description ?? "Created by better-stripe createWebhookEndpoint",
|
|
@@ -132,7 +145,9 @@ export async function setupEventDestination(stripe, _ctx, opts) {
|
|
|
132
145
|
const payloadType = opts.eventPayload ?? "thin";
|
|
133
146
|
const events = opts.enabledEvents ??
|
|
134
147
|
(payloadType === "thin"
|
|
135
|
-
?
|
|
148
|
+
? opts.apiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
149
|
+
? [...BETTER_STRIPE_CUSTOMER_ACCOUNT_EVENTS]
|
|
150
|
+
: [...BETTER_STRIPE_V2_WEBHOOK_EVENTS]
|
|
136
151
|
: [...BETTER_STRIPE_WEBHOOK_EVENTS]);
|
|
137
152
|
const name = opts.name ??
|
|
138
153
|
(payloadType === "thin" ? "better-stripe-v2" : "better-stripe");
|
|
@@ -145,6 +160,21 @@ export async function setupEventDestination(stripe, _ctx, opts) {
|
|
|
145
160
|
include: ["webhook_endpoint.url"],
|
|
146
161
|
});
|
|
147
162
|
const match = existing.data.find((d) => d.webhook_endpoint?.url === opts.url && d.event_payload === payloadType);
|
|
163
|
+
if (match &&
|
|
164
|
+
payloadType === "snapshot" &&
|
|
165
|
+
opts.apiVersion &&
|
|
166
|
+
match.snapshot_api_version !== opts.apiVersion)
|
|
167
|
+
throwStripeError("INVALID_CONFIGURATION", "Existing snapshot destination API version differs. Create a replacement destination with the selected version and configure its signing secret before retiring the old destination.");
|
|
168
|
+
if (match && opts.apiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION) {
|
|
169
|
+
const scopes = new Set((match.events_from ?? []).map((scope) => scope === "self"
|
|
170
|
+
? "@self"
|
|
171
|
+
: scope === "other_accounts"
|
|
172
|
+
? "@accounts"
|
|
173
|
+
: scope));
|
|
174
|
+
const required = payloadType === "thin" ? ["@self", "@accounts"] : ["@self"];
|
|
175
|
+
if (required.some((scope) => !scopes.has(scope)))
|
|
176
|
+
throwStripeError("INVALID_CONFIGURATION", "Existing destination event scope differs. Create a replacement with the customer preview scopes before retiring the old destination.");
|
|
177
|
+
}
|
|
148
178
|
if (match) {
|
|
149
179
|
const updated = await stripe.v2.core.eventDestinations.update(match.id, {
|
|
150
180
|
name,
|
|
@@ -170,7 +200,16 @@ export async function setupEventDestination(stripe, _ctx, opts) {
|
|
|
170
200
|
type: "webhook_endpoint",
|
|
171
201
|
event_payload: payloadType,
|
|
172
202
|
enabled_events: events,
|
|
173
|
-
|
|
203
|
+
...(payloadType === "snapshot" && opts.apiVersion
|
|
204
|
+
? { snapshot_api_version: opts.apiVersion }
|
|
205
|
+
: {}),
|
|
206
|
+
events_from: opts.apiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
207
|
+
? payloadType === "thin"
|
|
208
|
+
? ["@self", "@accounts"]
|
|
209
|
+
: ["@self"]
|
|
210
|
+
: payloadType === "thin"
|
|
211
|
+
? ["self", "other_accounts"]
|
|
212
|
+
: ["self"],
|
|
174
213
|
webhook_endpoint: { url: opts.url },
|
|
175
214
|
include: ["webhook_endpoint.signing_secret", "webhook_endpoint.url"],
|
|
176
215
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhookEndpoints.js","sourceRoot":"","sources":["../../../src/client/utils/webhookEndpoints.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"webhookEndpoints.js","sourceRoot":"","sources":["../../../src/client/utils/webhookEndpoints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mCAAmC,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKhD,gFAAgF;AAChF,8BAA8B;AAC9B,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG;IAC1C,oBAAoB;IACpB,iBAAiB;IACjB,iBAAiB;IACjB,eAAe;IACf,eAAe;IACf,gBAAgB;IAChB,+BAA+B;IAC/B,+BAA+B;IAC/B,+BAA+B;IAC/B,sCAAsC;IACtC,WAAW;IACX,4BAA4B;IAC5B,WAAW;IACX,iBAAiB;IACjB,mBAAmB;IACnB,cAAc;IACd,wBAAwB;IACxB,WAAW;IACX,0BAA0B;IAC1B,+BAA+B;IAC/B,yBAAyB;IACzB,UAAU;IACV,gBAAgB;IAChB,gBAAgB;IAChB,aAAa;IACb,eAAe;IACf,UAAU;IACV,gBAAgB;IAChB,gBAAgB;IAChB,eAAe;IACf,4EAA4E;IAC5E,0BAA0B;IAC1B,WAAW;IACX,wBAAwB;IACxB,wBAAwB;IACxB,uBAAuB;IACvB,gCAAgC;IAChC,iCAAiC;CACzB,CAAC;AAKX;;;GAGG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG;IAC7C,yBAAyB;IACzB,yBAAyB;IACzB,uCAAuC;IACvC,mCAAmC;IACnC,iDAAiD;IACjD,mEAAmE;IACnE,iDAAiD;IACjD,kDAAkD;IAClD,mCAAmC;IACnC,gCAAgC;IAChC,gCAAgC;IAChC,+BAA+B;CACvB,CAAC;AAEX,iFAAiF;AACjF,MAAM,CAAC,MAAM,qCAAqC,GAAG;IACnD,yBAAyB;IACzB,iDAAiD;IACjD,mCAAmC;CAC3B,CAAC;AAKX;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAsB;IACzD,GAAG,4BAA4B;IAC/B,GAAG,+BAA+B;CACnC,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAc,EACd,IAAY,EACZ,IAAyB;IAEzB,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,IAAI,CAAC;QACnD,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,GAAG;KAC1B,CAAC,CAAC;IACH,OAAO,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACjC,EAAE,EAAE,EAAE,CAAC,EAAE;QACT,GAAG,EAAE,EAAE,CAAC,GAAG;QACX,MAAM,EAAE,EAAE,CAAC,MAAM;QACjB,aAAa,EAAE,EAAE,CAAC,cAAc;QAChC,WAAW,EAAE,EAAE,CAAC,WAAW,IAAI,SAAS;QACxC,QAAQ,EAAE,EAAE,CAAC,QAAQ;KACtB,CAAC,CAAC,CAAC;AACN,CAAC;AAWD,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAc,EACd,IAAY,EACZ,IAAsD;IAEtD,8EAA8E;IAC9E,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IACpE,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9D,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,MAAM,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,MAAM,CAAC;QACpD,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,GAAG,CAAC,IAAI,CAAC,UAAU;YACjB,CAAC,CAAC;gBACE,WAAW,EACT,IAAI,CAAC,UAA2D;aACnE;YACH,CAAC,CAAC,EAAE,CAAC;QACP,cAAc,EAAE,CAAC,IAAI,CAAC,aAAa;YACjC,4BAA4B,CAAsD;QACpF,WAAW,EACT,IAAI,CAAC,WAAW,IAAI,gDAAgD;KACvE,CAAC,CAAC;IAEH,OAAO;QACL,EAAE,EAAE,QAAQ,CAAC,EAAE;QACf,MAAM,EAAE,QAAQ,CAAC,MAAO;QACxB,GAAG,EAAE,QAAQ,CAAC,GAAG;KAClB,CAAC;AACJ,CAAC;AAwBD,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAc,EACd,IAAY,EACZ,IAAyB;IAEzB,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC;QAC/D,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,GAAG;QACzB,OAAO,EAAE,CAAC,sBAAsB,CAAC;KAClC,CAAC,CAAC;IACH,OAAO,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACnC,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,GAAG,EAAE,CAAC,CAAC,gBAAgB,EAAE,GAAG,IAAI,EAAE;QAClC,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,aAAa,EAAE,CAAC,CAAC,cAAc;QAC/B,YAAY,EAAE,CAAC,CAAC,aAAa;QAC7B,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,WAAW,EAAE,CAAC,CAAC,WAAW;KAC3B,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAc,EACd,IAAY,EACZ,IAAuD;IAQvD,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,IAAI,MAAM,CAAC;IAChD,MAAM,MAAM,GACV,IAAI,CAAC,aAAa;QAClB,CAAC,WAAW,KAAK,MAAM;YACrB,CAAC,CAAC,IAAI,CAAC,UAAU,KAAK,mCAAmC;gBACvD,CAAC,CAAC,CAAC,GAAG,qCAAqC,CAAC;gBAC5C,CAAC,CAAC,CAAC,GAAG,+BAA+B,CAAC;YACxC,CAAC,CAAC,CAAC,GAAG,4BAA4B,CAAC,CAAC,CAAC;IACzC,MAAM,IAAI,GACR,IAAI,CAAC,IAAI;QACT,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;IAClE,MAAM,WAAW,GACf,IAAI,CAAC,WAAW;QAChB,CAAC,WAAW,KAAK,MAAM;YACrB,CAAC,CAAC,yCAAyC;YAC3C,CAAC,CAAC,iCAAiC,CAAC,CAAC;IAEzC,+EAA+E;IAC/E,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC;QAC3D,OAAO,EAAE,CAAC,sBAAsB,CAAC;KAClC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAC9B,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,gBAAgB,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,aAAa,KAAK,WAAW,CAC1E,CAAC;IAEF,IACE,KAAK;QACL,WAAW,KAAK,UAAU;QAC1B,IAAI,CAAC,UAAU;QACf,KAAK,CAAC,oBAAoB,KAAK,IAAI,CAAC,UAAU;QAE9C,gBAAgB,CACd,uBAAuB,EACvB,qLAAqL,CACtL,CAAC;IAEJ,IAAI,KAAK,IAAI,IAAI,CAAC,UAAU,KAAK,mCAAmC,EAAE,CAAC;QACrE,MAAM,MAAM,GAAG,IAAI,GAAG,CACpB,CAAC,KAAK,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACtC,KAAK,KAAK,MAAM;YACd,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,KAAK,KAAK,gBAAgB;gBAC1B,CAAC,CAAC,WAAW;gBACb,CAAC,CAAC,KAAK,CACZ,CACF,CAAC;QACF,MAAM,QAAQ,GACZ,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QAC9D,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC9C,gBAAgB,CACd,uBAAuB,EACvB,sIAAsI,CACvI,CAAC;IACN,CAAC;IAED,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE;YACtE,IAAI;YACJ,WAAW;YACX,cAAc,EAAE,MAAM;YACtB,OAAO,EAAE,CAAC,sBAAsB,CAAC;SAClC,CAAC,CAAC;QAEH,IAAI,OAAO,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAClC,MAAM,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1D,CAAC;QAED,OAAO;YACL,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,MAAM,EAAE,KAAK,CAAC,gBAAgB,EAAE,cAAc,IAAI,EAAE;YACpD,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,aAAa,EAAE,OAAO,CAAC,cAAc;YACrC,OAAO,EAAE,KAAK;SACf,CAAC;IACJ,CAAC;IAED,+BAA+B;IAC/B,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC;QAChE,IAAI;QACJ,WAAW;QACX,IAAI,EAAE,kBAAkB;QACxB,aAAa,EAAE,WAAW;QAC1B,cAAc,EAAE,MAAM;QACtB,GAAG,CAAC,WAAW,KAAK,UAAU,IAAI,IAAI,CAAC,UAAU;YAC/C,CAAC,CAAC,EAAE,oBAAoB,EAAE,IAAI,CAAC,UAAU,EAAE;YAC3C,CAAC,CAAC,EAAE,CAAC;QACP,WAAW,EACT,IAAI,CAAC,UAAU,KAAK,mCAAmC;YACrD,CAAC,CAAC,WAAW,KAAK,MAAM;gBACtB,CAAC,CAAC,CAAC,OAAO,EAAE,WAAW,CAAC;gBACxB,CAAC,CAAC,CAAC,OAAO,CAAC;YACb,CAAC,CAAC,WAAW,KAAK,MAAM;gBACtB,CAAC,CAAC,CAAC,MAAM,EAAE,gBAAgB,CAAC;gBAC5B,CAAC,CAAC,CAAC,MAAM,CAAC;QAChB,gBAAgB,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE;QACnC,OAAO,EAAE,CAAC,iCAAiC,EAAE,sBAAsB,CAAC;KACrE,CAAC,CAAC;IAEH,OAAO;QACL,EAAE,EAAE,WAAW,CAAC,EAAE;QAClB,MAAM,EAAE,WAAW,CAAC,gBAAgB,EAAE,cAAc,IAAI,EAAE;QAC1D,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,aAAa,EAAE,WAAW,CAAC,cAAc;QACzC,OAAO,EAAE,IAAI;KACd,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v2.d.ts","sourceRoot":"","sources":["../../../src/client/webhooks/v2.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"v2.d.ts","sourceRoot":"","sources":["../../../src/client/webhooks/v2.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,cAAc,CAAC;AAMtB,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,CAAC,CActB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,WAAW,GACrB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6FxB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { STRIPE_CUSTOMER_PREVIEW_API_VERSION } from "../apiVersion.js";
|
|
1
2
|
import { deriveAccountStatus } from "../core/accountStatus.js";
|
|
2
3
|
import { dispatchUpsert, extractIdentifiers, } from "./helpers.js";
|
|
3
4
|
// =============================================================================
|
|
@@ -53,13 +54,15 @@ export async function handleV2Event(whCtx, thinEvent) {
|
|
|
53
54
|
return null;
|
|
54
55
|
}
|
|
55
56
|
const retrieveParams = {
|
|
56
|
-
include:
|
|
57
|
-
"configuration.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
include: whCtx.config?.stripeApiVersion === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
58
|
+
? ["configuration.customer", "identity"]
|
|
59
|
+
: [
|
|
60
|
+
"configuration.merchant",
|
|
61
|
+
"configuration.recipient",
|
|
62
|
+
"configuration.customer",
|
|
63
|
+
"identity",
|
|
64
|
+
"requirements",
|
|
65
|
+
],
|
|
63
66
|
};
|
|
64
67
|
// Preserve the verified notification's account/organization context when
|
|
65
68
|
// fetching the latest resource; otherwise organization keys can fetch from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v2.js","sourceRoot":"","sources":["../../../src/client/webhooks/v2.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"v2.js","sourceRoot":"","sources":["../../../src/client/webhooks/v2.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import Stripe from "stripe";
|
|
2
|
+
import { type SupportedStripeApiVersion } from "../../client/apiVersion.js";
|
|
2
3
|
/**
|
|
3
4
|
* Get or create a Stripe SDK instance for the given API key.
|
|
4
|
-
*
|
|
5
|
+
* Defaults to GA; an explicitly selected supported preview gets its own cache entry.
|
|
5
6
|
*/
|
|
6
|
-
export declare function getStripe(apiKey: string): Stripe;
|
|
7
|
+
export declare function getStripe(apiKey: string, apiVersion?: SupportedStripeApiVersion): Stripe;
|
|
7
8
|
//# sourceMappingURL=stripeFactory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stripeFactory.d.ts","sourceRoot":"","sources":["../../../src/component/lib/stripeFactory.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"stripeFactory.d.ts","sourceRoot":"","sources":["../../../src/component/lib/stripeFactory.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,4BAA4B,CAAC;AAapC;;;GAGG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,yBAAyB,GACrC,MAAM,CAYR"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Stripe from "stripe";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveStripeApiVersion, } from "../../client/apiVersion.js";
|
|
3
3
|
/**
|
|
4
4
|
* Keyed cache for Stripe SDK instances.
|
|
5
5
|
* Safe for tests and mixed environments — different keys get different clients.
|
|
@@ -11,14 +11,15 @@ import { STRIPE_API_VERSION } from "../../client/constants.js";
|
|
|
11
11
|
const _clients = new Map();
|
|
12
12
|
/**
|
|
13
13
|
* Get or create a Stripe SDK instance for the given API key.
|
|
14
|
-
*
|
|
14
|
+
* Defaults to GA; an explicitly selected supported preview gets its own cache entry.
|
|
15
15
|
*/
|
|
16
|
-
export function getStripe(apiKey) {
|
|
17
|
-
const
|
|
16
|
+
export function getStripe(apiKey, apiVersion) {
|
|
17
|
+
const version = resolveStripeApiVersion(apiVersion);
|
|
18
|
+
const cacheKey = `${apiKey}:${version}`;
|
|
18
19
|
let client = _clients.get(cacheKey);
|
|
19
20
|
if (!client) {
|
|
20
21
|
client = new Stripe(apiKey, {
|
|
21
|
-
apiVersion:
|
|
22
|
+
apiVersion: version,
|
|
22
23
|
typescript: true,
|
|
23
24
|
});
|
|
24
25
|
_clients.set(cacheKey, client);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stripeFactory.js","sourceRoot":"","sources":["../../../src/component/lib/stripeFactory.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,
|
|
1
|
+
{"version":3,"file":"stripeFactory.js","sourceRoot":"","sources":["../../../src/component/lib/stripeFactory.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,EACL,uBAAuB,GAExB,MAAM,4BAA4B,CAAC;AAGpC;;;;;;;GAOG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;AAE3C;;;GAGG;AACH,MAAM,UAAU,SAAS,CACvB,MAAc,EACd,UAAsC;IAEtC,MAAM,OAAO,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC;IACxC,IAAI,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACpC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE;YAC1B,UAAU,EAAE,OAA2B;YACvC,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;QACH,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Customer-only Accounts v2 preview
|
|
2
|
+
|
|
3
|
+
Connect platforms keep the default `2026-09-30.endive` API version. Non-Connect
|
|
4
|
+
merchants can explicitly opt into `2026-09-30.preview` for customer-configured
|
|
5
|
+
Accounts. The package does not enable Connect or onboard a merchant/recipient
|
|
6
|
+
when creating a buyer with `ensureCustomerAccount`.
|
|
7
|
+
|
|
8
|
+
## Enable the sandbox feature first
|
|
9
|
+
|
|
10
|
+
In the intended sandbox, the account owner must enable **Reusable payment
|
|
11
|
+
methods for Global Payouts** under **Account previews and features**. If the toggle
|
|
12
|
+
is unavailable, request access using Stripe's form. This also enables Stripe's
|
|
13
|
+
reusable payout credentials preview; it is not a Connect onboarding flow.
|
|
14
|
+
Changing an API version cannot enable the feature on the Stripe account.
|
|
15
|
+
|
|
16
|
+
Enabling the preview synchronizes existing Customers with customer-configured
|
|
17
|
+
Accounts and changes how Stripe-hosted integrations create customers. Review
|
|
18
|
+
Stripe's limitations and migration behavior before changing any live account.
|
|
19
|
+
See [Stripe's customer Account guide](https://docs.stripe.com/accounts-v2/use-accounts-as-customers).
|
|
20
|
+
|
|
21
|
+
## Configure one client and its webhook route
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// convex/stripe.ts
|
|
25
|
+
import {
|
|
26
|
+
BetterStripe,
|
|
27
|
+
STRIPE_CUSTOMER_PREVIEW_API_VERSION,
|
|
28
|
+
} from "@fantastic.dev/better-stripe";
|
|
29
|
+
import { components } from "./_generated/api";
|
|
30
|
+
|
|
31
|
+
export const stripe = new BetterStripe(components.betterStripe, {
|
|
32
|
+
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
|
33
|
+
stripeApiVersion: STRIPE_CUSTOMER_PREVIEW_API_VERSION,
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// convex/http.ts
|
|
39
|
+
import { httpRouter } from "convex/server";
|
|
40
|
+
import { stripe } from "./stripe";
|
|
41
|
+
|
|
42
|
+
const http = httpRouter();
|
|
43
|
+
stripe.registerRoutes(http, {
|
|
44
|
+
webhookPath: "/stripe/webhook",
|
|
45
|
+
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
46
|
+
webhookSecretV2: process.env.STRIPE_WEBHOOK_SECRET_V2,
|
|
47
|
+
// Preserve your existing `webhooks` handler references and retry policy here.
|
|
48
|
+
});
|
|
49
|
+
export default http;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The instance route inherits its client's key and version. Conflicting explicit
|
|
53
|
+
values fail at registration. The standalone `registerRoutes` function remains
|
|
54
|
+
available; when using it, pass `stripeApiVersion: stripe.stripeApiVersion` and the
|
|
55
|
+
same secret key explicitly. The client accepts only the supported GA and customer
|
|
56
|
+
preview versions; unsupported values fail with `INVALID_CONFIGURATION`.
|
|
57
|
+
|
|
58
|
+
All client operations use the selected version: account creation/configuration,
|
|
59
|
+
Checkout, subscriptions, portal sessions, refunds, authoritative reads, and the
|
|
60
|
+
provider calls made while handling webhooks. Component database writes remain
|
|
61
|
+
provider-version independent. SDK caches separate clients by key and version.
|
|
62
|
+
|
|
63
|
+
## Create a buyer and Checkout Session
|
|
64
|
+
|
|
65
|
+
Authenticate the caller and validate ownership/prices on the server. The client
|
|
66
|
+
is not an authorization boundary. Persist logical request identities in your host
|
|
67
|
+
app so retries keep the same provider keys and new purchases use distinct keys.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const buyer = await stripe.ensureCustomerAccount(ctx, {
|
|
71
|
+
userId: identity.subject,
|
|
72
|
+
email: verifiedEmail,
|
|
73
|
+
idempotencyKey: accountProvisioningKey,
|
|
74
|
+
});
|
|
75
|
+
const checkout = await stripe.createCheckoutSession(ctx, {
|
|
76
|
+
userId: identity.subject,
|
|
77
|
+
accountId: buyer.stripeAccountId,
|
|
78
|
+
stripePriceId: authorizedPriceId,
|
|
79
|
+
mode: "subscription",
|
|
80
|
+
uiMode: "redirect",
|
|
81
|
+
returnUrl: "https://example.com/receipt?session_id={CHECKOUT_SESSION_ID}",
|
|
82
|
+
idempotencyKey: orderKey,
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The buyer is an `acct_` object with the customer configuration. Checkout uses
|
|
87
|
+
`customer_account`, not a connected-account destination. No seller, recipient,
|
|
88
|
+
transfer, or application fee is added by this configuration.
|
|
89
|
+
|
|
90
|
+
After verifying buyer/order ownership, use `getCheckoutSessionFromStripe` to
|
|
91
|
+
check current payment status. Completion alone does not prove payment: fulfill
|
|
92
|
+
only when your payment policy accepts the authoritative status, and deduplicate
|
|
93
|
+
fulfillment by a durable order key. Subscription and coupon reads use the same
|
|
94
|
+
configured version. Cancellation/reactivation and refunds keep their existing
|
|
95
|
+
public API signatures and caller idempotency behavior.
|
|
96
|
+
|
|
97
|
+
## Match webhook payload versions and scope
|
|
98
|
+
|
|
99
|
+
Snapshot destinations created through this client pin `snapshot_api_version` to
|
|
100
|
+
the selected version. V1 webhook endpoints pin their `api_version`. Thin events
|
|
101
|
+
are unversioned; their related customer Account is fetched with the selected
|
|
102
|
+
request version and only customer/identity includes in customer-preview mode.
|
|
103
|
+
|
|
104
|
+
The preview's default thin subscription covers Account creation, customer
|
|
105
|
+
configuration changes, and identity changes, using Stripe's `@self` and
|
|
106
|
+
`@accounts` scopes. Subscription and invoice changes continue to use V1 snapshot
|
|
107
|
+
events. No merchant or recipient capability events are subscribed by default in
|
|
108
|
+
customer-preview mode. The GA defaults retain their existing event selection.
|
|
109
|
+
|
|
110
|
+
Stripe cannot update a destination's snapshot version or event scopes in place.
|
|
111
|
+
`setupEventDestination` refuses an incompatible existing destination rather than
|
|
112
|
+
silently reusing it. Create a replacement at a new webhook path, register the
|
|
113
|
+
matching route and signing secret, verify deliveries, then retire the old
|
|
114
|
+
endpoint. A low-level `createWebhookEndpoint` call retains its existing same-URL
|
|
115
|
+
secret-rotation behavior; do not use it as an implicit migration plan.
|
|
116
|
+
|
|
117
|
+
See [event destination creation](https://docs.stripe.com/api/v2/core/event-destinations/create)
|
|
118
|
+
and [update restrictions](https://docs.stripe.com/api/v2/core/event-destinations/update).
|
|
119
|
+
|
|
120
|
+
## Verification boundary
|
|
121
|
+
|
|
122
|
+
Package tests exercise serialized SDK request headers, customer-only account
|
|
123
|
+
configuration, idempotency keys, Checkout buyer binding, authoritative reads,
|
|
124
|
+
subscription cancellation/reactivation, portal and refund calls, webhook route
|
|
125
|
+
configuration, thin-event synchronization, and snapshot version/scope checks.
|
|
126
|
+
The GA version has regression coverage alongside the preview version.
|
|
127
|
+
|
|
128
|
+
A real non-Connect sandbox test additionally requires the owner-enabled preview
|
|
129
|
+
and sandbox credentials. A Connect sandbox, mocked transport, or successful
|
|
130
|
+
TypeScript compilation does not prove that separate eligibility requirement.
|
|
131
|
+
Do not report checkout, receipt, renewal, portal or refund acceptance as verified
|
|
132
|
+
for a non-Connect merchant until those flows run in its enabled sandbox.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fantastic.dev/better-stripe",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "A Convex component for Stripe V2 Accounts API integration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"stripe",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"src",
|
|
60
60
|
"docs/react-workflows.md",
|
|
61
61
|
"docs/settlement.md",
|
|
62
|
+
"docs/accounts-v2-preview.md",
|
|
62
63
|
"docs/react-workflows-audit.md",
|
|
63
64
|
"CHANGELOG.md",
|
|
64
65
|
"MIGRATION.md"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { STRIPE_API_VERSION } from "./constants.js";
|
|
2
|
+
import { throwStripeError } from "./errors.js";
|
|
3
|
+
/** Explicit opt-in for Stripe's customer-only Accounts v2 public preview. */
|
|
4
|
+
export const STRIPE_CUSTOMER_PREVIEW_API_VERSION = "2026-09-30.preview";
|
|
5
|
+
export type SupportedStripeApiVersion =
|
|
6
|
+
typeof STRIPE_API_VERSION | typeof STRIPE_CUSTOMER_PREVIEW_API_VERSION;
|
|
7
|
+
export function resolveStripeApiVersion(
|
|
8
|
+
version?: string,
|
|
9
|
+
): SupportedStripeApiVersion {
|
|
10
|
+
if (version === undefined) return STRIPE_API_VERSION;
|
|
11
|
+
if (
|
|
12
|
+
version === STRIPE_API_VERSION ||
|
|
13
|
+
version === STRIPE_CUSTOMER_PREVIEW_API_VERSION
|
|
14
|
+
)
|
|
15
|
+
return version;
|
|
16
|
+
throwStripeError(
|
|
17
|
+
"INVALID_CONFIGURATION",
|
|
18
|
+
`Unsupported Stripe API version. Use ${STRIPE_API_VERSION} or ${STRIPE_CUSTOMER_PREVIEW_API_VERSION}.`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { componentsGeneric } from "convex/server";
|
|
2
|
+
import { expect, it, vi } from "vitest";
|
|
3
|
+
import { handleV2Event } from "./webhooks/v2.js";
|
|
4
|
+
import type { WebhookContext } from "./webhooks/helpers.js";
|
|
5
|
+
import type { V2ThinEvent } from "./types.js";
|
|
6
|
+
it("syncs a preview customer without requesting merchant, recipient or Connect requirements", async () => {
|
|
7
|
+
const retrieve = vi.fn().mockResolvedValue({
|
|
8
|
+
id: "acct_buyer",
|
|
9
|
+
metadata: { userId: "buyer" },
|
|
10
|
+
configuration: { customer: { applied: true } },
|
|
11
|
+
applied_configurations: ["customer"],
|
|
12
|
+
identity: { country: "us" },
|
|
13
|
+
});
|
|
14
|
+
const runMutation = vi.fn();
|
|
15
|
+
const ctx = {
|
|
16
|
+
component: componentsGeneric().betterStripe,
|
|
17
|
+
ctx: { runMutation },
|
|
18
|
+
stripe: { v2: { core: { accounts: { retrieve } } } },
|
|
19
|
+
config: { stripeApiVersion: "2026-09-30.preview" },
|
|
20
|
+
} as unknown as WebhookContext;
|
|
21
|
+
await handleV2Event(ctx, {
|
|
22
|
+
id: "evt_preview",
|
|
23
|
+
type: "v2.core.account[configuration.customer].updated",
|
|
24
|
+
context: "acct_merchant",
|
|
25
|
+
related_object: { id: "acct_buyer", type: "v2.core.account" },
|
|
26
|
+
} as V2ThinEvent);
|
|
27
|
+
expect(retrieve).toHaveBeenCalledWith(
|
|
28
|
+
"acct_buyer",
|
|
29
|
+
{ include: ["configuration.customer", "identity"] },
|
|
30
|
+
{ stripeContext: "acct_merchant" },
|
|
31
|
+
);
|
|
32
|
+
expect(runMutation).toHaveBeenCalledWith(
|
|
33
|
+
expect.anything(),
|
|
34
|
+
expect.objectContaining({
|
|
35
|
+
userId: "buyer",
|
|
36
|
+
stripeAccountId: "acct_buyer",
|
|
37
|
+
appliedConfigurations: ["customer"],
|
|
38
|
+
onboardingStatus: "complete",
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type Stripe from "stripe";
|
|
2
|
+
import { componentsGeneric, type HttpRouter } from "convex/server";
|
|
3
|
+
import { expect, it, vi } from "vitest";
|
|
4
|
+
import { BetterStripe } from "./index.js";
|
|
5
|
+
import type { Component, RunCtx } from "./helpers.js";
|
|
6
|
+
import {
|
|
7
|
+
createWebhookEndpoint,
|
|
8
|
+
setupEventDestination,
|
|
9
|
+
} from "./utils/webhookEndpoints.js";
|
|
10
|
+
const handle = vi.hoisted(() => vi.fn(async () => new Response("ok")));
|
|
11
|
+
vi.mock("./webhooks/handler.js", () => ({ handleWebhookRequest: handle }));
|
|
12
|
+
const version = "2026-09-30.preview";
|
|
13
|
+
const component = componentsGeneric().betterStripe as unknown as Component;
|
|
14
|
+
it("instance routes inherit the selected client version and reject conflicting configuration", async () => {
|
|
15
|
+
const bs = new BetterStripe(component, {
|
|
16
|
+
STRIPE_SECRET_KEY: "sk_test_preview_routes",
|
|
17
|
+
stripeApiVersion: version,
|
|
18
|
+
});
|
|
19
|
+
const route = vi.fn();
|
|
20
|
+
bs.registerRoutes({ route } as unknown as HttpRouter, {
|
|
21
|
+
webhookSecret: "whsec_fixture",
|
|
22
|
+
});
|
|
23
|
+
const handler = route.mock.calls[0][0].handler as {
|
|
24
|
+
_handler: (ctx: unknown, request: Request) => Promise<Response>;
|
|
25
|
+
};
|
|
26
|
+
await handler._handler(
|
|
27
|
+
{},
|
|
28
|
+
new Request("https://example.com/stripe/webhook", { method: "POST" }),
|
|
29
|
+
);
|
|
30
|
+
expect(handle).toHaveBeenCalledWith(
|
|
31
|
+
expect.anything(),
|
|
32
|
+
expect.anything(),
|
|
33
|
+
component,
|
|
34
|
+
expect.objectContaining({
|
|
35
|
+
stripeApiVersion: version,
|
|
36
|
+
stripeSecretKey: "sk_test_preview_routes",
|
|
37
|
+
webhookSecret: "whsec_fixture",
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
expect(() =>
|
|
41
|
+
bs.registerRoutes({ route } as unknown as HttpRouter, {
|
|
42
|
+
stripeApiVersion: "2026-09-30.endive",
|
|
43
|
+
}),
|
|
44
|
+
).toThrow("match");
|
|
45
|
+
expect(route).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
function sdk() {
|
|
48
|
+
const raw = {
|
|
49
|
+
webhookEndpoints: {
|
|
50
|
+
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
51
|
+
create: vi.fn().mockResolvedValue({
|
|
52
|
+
id: "we_preview",
|
|
53
|
+
url: "https://example.com/events",
|
|
54
|
+
secret: "whsec_fixture",
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
v2: {
|
|
58
|
+
core: {
|
|
59
|
+
eventDestinations: {
|
|
60
|
+
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
61
|
+
create: vi.fn().mockResolvedValue({
|
|
62
|
+
id: "ed_preview",
|
|
63
|
+
enabled_events: [],
|
|
64
|
+
webhook_endpoint: { signing_secret: "whsec_fixture" },
|
|
65
|
+
}),
|
|
66
|
+
update: vi.fn(),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
return { raw, stripe: raw as unknown as Stripe };
|
|
72
|
+
}
|
|
73
|
+
it("pins snapshot payloads, leaves thin payloads unversioned, and uses canonical preview scopes", async () => {
|
|
74
|
+
const { raw, stripe } = sdk(),
|
|
75
|
+
ctx = {} as RunCtx;
|
|
76
|
+
await createWebhookEndpoint(stripe, ctx, {
|
|
77
|
+
url: "https://example.com/events",
|
|
78
|
+
apiVersion: version,
|
|
79
|
+
});
|
|
80
|
+
expect(raw.webhookEndpoints.create).toHaveBeenCalledWith(
|
|
81
|
+
expect.objectContaining({ api_version: version }),
|
|
82
|
+
);
|
|
83
|
+
await setupEventDestination(stripe, ctx, {
|
|
84
|
+
url: "https://example.com/events",
|
|
85
|
+
eventPayload: "snapshot",
|
|
86
|
+
apiVersion: version,
|
|
87
|
+
});
|
|
88
|
+
expect(raw.v2.core.eventDestinations.create).toHaveBeenLastCalledWith(
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
snapshot_api_version: version,
|
|
91
|
+
events_from: ["@self"],
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
await setupEventDestination(stripe, ctx, {
|
|
95
|
+
url: "https://example.com/events",
|
|
96
|
+
eventPayload: "thin",
|
|
97
|
+
apiVersion: version,
|
|
98
|
+
});
|
|
99
|
+
const thin = raw.v2.core.eventDestinations.create.mock.calls[1][0];
|
|
100
|
+
expect(thin).not.toHaveProperty("snapshot_api_version");
|
|
101
|
+
expect(thin.events_from).toEqual(["@self", "@accounts"]);
|
|
102
|
+
expect(thin.enabled_events).toEqual([
|
|
103
|
+
"v2.core.account.created",
|
|
104
|
+
"v2.core.account[configuration.customer].updated",
|
|
105
|
+
"v2.core.account[identity].updated",
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
it("refuses to reuse a snapshot destination whose immutable version differs", async () => {
|
|
109
|
+
const { raw, stripe } = sdk();
|
|
110
|
+
raw.v2.core.eventDestinations.list.mockResolvedValue({
|
|
111
|
+
data: [
|
|
112
|
+
{
|
|
113
|
+
id: "ed_old",
|
|
114
|
+
event_payload: "snapshot",
|
|
115
|
+
snapshot_api_version: "2026-09-30.endive",
|
|
116
|
+
webhook_endpoint: { url: "https://example.com/events" },
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
await expect(
|
|
121
|
+
setupEventDestination(stripe, {} as RunCtx, {
|
|
122
|
+
url: "https://example.com/events",
|
|
123
|
+
eventPayload: "snapshot",
|
|
124
|
+
apiVersion: version,
|
|
125
|
+
}),
|
|
126
|
+
).rejects.toThrow("version");
|
|
127
|
+
expect(raw.v2.core.eventDestinations.update).not.toHaveBeenCalled();
|
|
128
|
+
expect(raw.v2.core.eventDestinations.create).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
it("refuses a preview thin destination that cannot deliver managed customer account events", async () => {
|
|
131
|
+
const { raw, stripe } = sdk();
|
|
132
|
+
raw.v2.core.eventDestinations.list.mockResolvedValue({
|
|
133
|
+
data: [
|
|
134
|
+
{
|
|
135
|
+
id: "ed_old",
|
|
136
|
+
event_payload: "thin",
|
|
137
|
+
events_from: ["@self"],
|
|
138
|
+
webhook_endpoint: { url: "https://example.com/events" },
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
await expect(
|
|
143
|
+
setupEventDestination(stripe, {} as RunCtx, {
|
|
144
|
+
url: "https://example.com/events",
|
|
145
|
+
eventPayload: "thin",
|
|
146
|
+
apiVersion: version,
|
|
147
|
+
}),
|
|
148
|
+
).rejects.toThrow("scope");
|
|
149
|
+
expect(raw.v2.core.eventDestinations.update).not.toHaveBeenCalled();
|
|
150
|
+
});
|