@better-auth/stripe 1.5.0-beta.2 → 1.5.0-beta.3
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/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +15 -13
- package/LICENSE.md +15 -12
- package/dist/client.d.mts +105 -1
- package/dist/client.mjs +1 -1
- package/dist/error-codes-Bkj5yJMT.mjs +29 -0
- package/dist/{index-SbT5j9k6.d.mts → index-BnHmwMru.d.mts} +269 -154
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +449 -194
- package/package.json +6 -6
- package/src/error-codes.ts +16 -0
- package/src/hooks.ts +98 -71
- package/src/index.ts +142 -45
- package/src/middleware.ts +89 -42
- package/src/routes.ts +502 -224
- package/src/schema.ts +18 -0
- package/src/types.ts +75 -19
- package/src/utils.ts +11 -0
- package/test/stripe-organization.test.ts +1993 -0
- package/{src → test}/stripe.test.ts +821 -18
- package/dist/error-codes-qqooUh6R.mjs +0 -16
|
@@ -0,0 +1,1993 @@
|
|
|
1
|
+
import { organizationClient } from "better-auth/client/plugins";
|
|
2
|
+
import { organization } from "better-auth/plugins/organization";
|
|
3
|
+
import { getTestInstance } from "better-auth/test";
|
|
4
|
+
import type Stripe from "stripe";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { stripe } from "../src";
|
|
7
|
+
import { stripeClient } from "../src/client";
|
|
8
|
+
import type { StripeOptions, Subscription } from "../src/types";
|
|
9
|
+
|
|
10
|
+
describe("stripe - organization customer", () => {
|
|
11
|
+
const mockStripeOrg = {
|
|
12
|
+
prices: {
|
|
13
|
+
list: vi.fn().mockResolvedValue({ data: [{ id: "price_lookup_123" }] }),
|
|
14
|
+
},
|
|
15
|
+
customers: {
|
|
16
|
+
create: vi.fn().mockResolvedValue({ id: "cus_org_mock123" }),
|
|
17
|
+
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
18
|
+
search: vi.fn().mockResolvedValue({ data: [] }),
|
|
19
|
+
retrieve: vi.fn().mockResolvedValue({
|
|
20
|
+
id: "cus_org_mock123",
|
|
21
|
+
email: "org@email.com",
|
|
22
|
+
deleted: false,
|
|
23
|
+
}),
|
|
24
|
+
update: vi.fn().mockResolvedValue({
|
|
25
|
+
id: "cus_org_mock123",
|
|
26
|
+
email: "org@email.com",
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
checkout: {
|
|
30
|
+
sessions: {
|
|
31
|
+
create: vi.fn().mockResolvedValue({
|
|
32
|
+
url: "https://checkout.stripe.com/mock",
|
|
33
|
+
id: "cs_mock123",
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
billingPortal: {
|
|
38
|
+
sessions: {
|
|
39
|
+
create: vi
|
|
40
|
+
.fn()
|
|
41
|
+
.mockResolvedValue({ url: "https://billing.stripe.com/mock" }),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
subscriptions: {
|
|
45
|
+
retrieve: vi.fn(),
|
|
46
|
+
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
47
|
+
update: vi.fn(),
|
|
48
|
+
},
|
|
49
|
+
webhooks: {
|
|
50
|
+
constructEventAsync: vi.fn(),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const testUser = {
|
|
54
|
+
email: "test@email.com",
|
|
55
|
+
password: "password",
|
|
56
|
+
name: "Test User",
|
|
57
|
+
};
|
|
58
|
+
const _stripeOrg = mockStripeOrg as unknown as Stripe;
|
|
59
|
+
const baseOrgStripeOptions = {
|
|
60
|
+
stripeClient: _stripeOrg,
|
|
61
|
+
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
62
|
+
createCustomerOnSignUp: false, // Disable for org tests
|
|
63
|
+
organization: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
},
|
|
66
|
+
subscription: {
|
|
67
|
+
enabled: true,
|
|
68
|
+
plans: [
|
|
69
|
+
{
|
|
70
|
+
priceId: process.env.STRIPE_PRICE_ID_1!,
|
|
71
|
+
name: "starter",
|
|
72
|
+
lookupKey: "lookup_key_123",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
priceId: process.env.STRIPE_PRICE_ID_2!,
|
|
76
|
+
name: "premium",
|
|
77
|
+
lookupKey: "lookup_key_234",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
authorizeReference: async () => true,
|
|
81
|
+
},
|
|
82
|
+
} satisfies StripeOptions;
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
vi.clearAllMocks();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should create a Stripe customer for organization when upgrading subscription", async () => {
|
|
89
|
+
const onCustomerCreate = vi.fn();
|
|
90
|
+
const stripeOptionsWithOrgCallback: StripeOptions = {
|
|
91
|
+
...baseOrgStripeOptions,
|
|
92
|
+
organization: {
|
|
93
|
+
...baseOrgStripeOptions.organization,
|
|
94
|
+
onCustomerCreate,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const { client, auth, sessionSetter } = await getTestInstance(
|
|
99
|
+
{
|
|
100
|
+
plugins: [organization(), stripe(stripeOptionsWithOrgCallback)],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
disableTestUser: true,
|
|
104
|
+
clientOptions: {
|
|
105
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
const ctx = await auth.$context;
|
|
110
|
+
|
|
111
|
+
// Create user and organization
|
|
112
|
+
await client.signUp.email(
|
|
113
|
+
{ ...testUser, email: "org-customer-test@email.com" },
|
|
114
|
+
{ throw: true },
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const headers = new Headers();
|
|
118
|
+
await client.signIn.email(
|
|
119
|
+
{ ...testUser, email: "org-customer-test@email.com" },
|
|
120
|
+
{
|
|
121
|
+
throw: true,
|
|
122
|
+
onSuccess: sessionSetter(headers),
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Create organization via client
|
|
127
|
+
const org = await client.organization.create({
|
|
128
|
+
name: "Test Organization",
|
|
129
|
+
slug: "test-org",
|
|
130
|
+
fetchOptions: { headers },
|
|
131
|
+
});
|
|
132
|
+
const orgId = org.data?.id as string;
|
|
133
|
+
|
|
134
|
+
// Upgrade subscription for organization
|
|
135
|
+
const res = await client.subscription.upgrade({
|
|
136
|
+
plan: "starter",
|
|
137
|
+
customerType: "organization",
|
|
138
|
+
referenceId: orgId,
|
|
139
|
+
fetchOptions: { headers },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(res.data?.url).toBeDefined();
|
|
143
|
+
|
|
144
|
+
// Verify Stripe customer was created for org
|
|
145
|
+
expect(mockStripeOrg.customers.create).toHaveBeenCalledWith(
|
|
146
|
+
expect.objectContaining({
|
|
147
|
+
name: "Test Organization",
|
|
148
|
+
metadata: expect.objectContaining({
|
|
149
|
+
organizationId: orgId,
|
|
150
|
+
customerType: "organization",
|
|
151
|
+
}),
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Verify org was updated with stripeCustomerId
|
|
156
|
+
const updatedOrg = await ctx.adapter.findOne<{
|
|
157
|
+
id: string;
|
|
158
|
+
stripeCustomerId?: string;
|
|
159
|
+
}>({
|
|
160
|
+
model: "organization",
|
|
161
|
+
where: [{ field: "id", value: orgId }],
|
|
162
|
+
});
|
|
163
|
+
expect(updatedOrg?.stripeCustomerId).toBe("cus_org_mock123");
|
|
164
|
+
|
|
165
|
+
// Verify callback was called
|
|
166
|
+
expect(onCustomerCreate).toHaveBeenCalledWith(
|
|
167
|
+
expect.objectContaining({
|
|
168
|
+
stripeCustomer: expect.objectContaining({ id: "cus_org_mock123" }),
|
|
169
|
+
organization: expect.objectContaining({
|
|
170
|
+
id: orgId,
|
|
171
|
+
stripeCustomerId: "cus_org_mock123",
|
|
172
|
+
}),
|
|
173
|
+
}),
|
|
174
|
+
expect.anything(),
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should use existing Stripe customer ID from organization", async () => {
|
|
179
|
+
const { client, auth, sessionSetter } = await getTestInstance(
|
|
180
|
+
{
|
|
181
|
+
plugins: [organization(), stripe(baseOrgStripeOptions)],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
disableTestUser: true,
|
|
185
|
+
clientOptions: {
|
|
186
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
const ctx = await auth.$context;
|
|
191
|
+
|
|
192
|
+
// Create user and organization with existing stripeCustomerId
|
|
193
|
+
await client.signUp.email(
|
|
194
|
+
{ ...testUser, email: "org-existing-customer@email.com" },
|
|
195
|
+
{ throw: true },
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const headers = new Headers();
|
|
199
|
+
await client.signIn.email(
|
|
200
|
+
{ ...testUser, email: "org-existing-customer@email.com" },
|
|
201
|
+
{
|
|
202
|
+
throw: true,
|
|
203
|
+
onSuccess: sessionSetter(headers),
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Create organization via client
|
|
208
|
+
const org = await client.organization.create({
|
|
209
|
+
name: "Existing Stripe Org",
|
|
210
|
+
slug: "existing-stripe-org",
|
|
211
|
+
fetchOptions: { headers },
|
|
212
|
+
});
|
|
213
|
+
const orgId = org.data?.id as string;
|
|
214
|
+
|
|
215
|
+
// Update org with existing stripeCustomerId
|
|
216
|
+
await ctx.adapter.update({
|
|
217
|
+
model: "organization",
|
|
218
|
+
update: { stripeCustomerId: "cus_existing_org_123" },
|
|
219
|
+
where: [{ field: "id", value: orgId }],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await client.subscription.upgrade({
|
|
223
|
+
plan: "starter",
|
|
224
|
+
customerType: "organization",
|
|
225
|
+
referenceId: orgId,
|
|
226
|
+
fetchOptions: { headers },
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Should NOT create a new Stripe customer
|
|
230
|
+
expect(mockStripeOrg.customers.create).not.toHaveBeenCalled();
|
|
231
|
+
|
|
232
|
+
// Should use existing customer ID in checkout
|
|
233
|
+
expect(mockStripeOrg.checkout.sessions.create).toHaveBeenCalledWith(
|
|
234
|
+
expect.objectContaining({
|
|
235
|
+
customer: "cus_existing_org_123",
|
|
236
|
+
}),
|
|
237
|
+
undefined,
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should call getCustomerCreateParams when creating org customer", async () => {
|
|
242
|
+
const getCustomerCreateParams = vi.fn().mockResolvedValue({
|
|
243
|
+
email: "billing@org.com",
|
|
244
|
+
description: "Custom org description",
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const stripeOptionsWithParams: StripeOptions = {
|
|
248
|
+
...baseOrgStripeOptions,
|
|
249
|
+
organization: {
|
|
250
|
+
...baseOrgStripeOptions.organization,
|
|
251
|
+
getCustomerCreateParams,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const { client, sessionSetter } = await getTestInstance(
|
|
256
|
+
{
|
|
257
|
+
plugins: [organization(), stripe(stripeOptionsWithParams)],
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
disableTestUser: true,
|
|
261
|
+
clientOptions: {
|
|
262
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await client.signUp.email(
|
|
268
|
+
{ ...testUser, email: "org-params-test@email.com" },
|
|
269
|
+
{ throw: true },
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const headers = new Headers();
|
|
273
|
+
await client.signIn.email(
|
|
274
|
+
{ ...testUser, email: "org-params-test@email.com" },
|
|
275
|
+
{
|
|
276
|
+
throw: true,
|
|
277
|
+
onSuccess: sessionSetter(headers),
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Create organization via client
|
|
282
|
+
const org = await client.organization.create({
|
|
283
|
+
name: "Params Test Org",
|
|
284
|
+
slug: "params-test-org",
|
|
285
|
+
fetchOptions: { headers },
|
|
286
|
+
});
|
|
287
|
+
const orgId = org.data?.id as string;
|
|
288
|
+
|
|
289
|
+
await client.subscription.upgrade({
|
|
290
|
+
plan: "starter",
|
|
291
|
+
customerType: "organization",
|
|
292
|
+
referenceId: orgId,
|
|
293
|
+
fetchOptions: { headers },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Verify getCustomerCreateParams was called with org
|
|
297
|
+
expect(getCustomerCreateParams).toHaveBeenCalled();
|
|
298
|
+
const callArgs = getCustomerCreateParams.mock.calls[0]!;
|
|
299
|
+
expect(callArgs[0]).toMatchObject({ id: orgId });
|
|
300
|
+
|
|
301
|
+
// Verify custom params were passed to Stripe
|
|
302
|
+
expect(mockStripeOrg.customers.create).toHaveBeenCalledWith(
|
|
303
|
+
expect.objectContaining({
|
|
304
|
+
email: "billing@org.com",
|
|
305
|
+
description: "Custom org description",
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should create billing portal for organization", async () => {
|
|
311
|
+
const { client, auth, sessionSetter } = await getTestInstance(
|
|
312
|
+
{
|
|
313
|
+
plugins: [organization(), stripe(baseOrgStripeOptions)],
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
disableTestUser: true,
|
|
317
|
+
clientOptions: {
|
|
318
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
const ctx = await auth.$context;
|
|
323
|
+
|
|
324
|
+
await client.signUp.email(
|
|
325
|
+
{ ...testUser, email: "org-portal-test@email.com" },
|
|
326
|
+
{ throw: true },
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const headers = new Headers();
|
|
330
|
+
await client.signIn.email(
|
|
331
|
+
{ ...testUser, email: "org-portal-test@email.com" },
|
|
332
|
+
{
|
|
333
|
+
throw: true,
|
|
334
|
+
onSuccess: sessionSetter(headers),
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Create organization via client
|
|
339
|
+
const org = await client.organization.create({
|
|
340
|
+
name: "Portal Test Org",
|
|
341
|
+
slug: "portal-test-org",
|
|
342
|
+
fetchOptions: { headers },
|
|
343
|
+
});
|
|
344
|
+
const orgId = org.data?.id as string;
|
|
345
|
+
|
|
346
|
+
// Update org with stripeCustomerId
|
|
347
|
+
await ctx.adapter.update({
|
|
348
|
+
model: "organization",
|
|
349
|
+
update: { stripeCustomerId: "cus_portal_org_123" },
|
|
350
|
+
where: [{ field: "id", value: orgId }],
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Create a subscription for the org
|
|
354
|
+
await ctx.adapter.create({
|
|
355
|
+
model: "subscription",
|
|
356
|
+
data: {
|
|
357
|
+
referenceId: orgId,
|
|
358
|
+
stripeCustomerId: "cus_portal_org_123",
|
|
359
|
+
status: "active",
|
|
360
|
+
plan: "starter",
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const res = await client.subscription.billingPortal({
|
|
365
|
+
customerType: "organization",
|
|
366
|
+
referenceId: orgId,
|
|
367
|
+
returnUrl: "/dashboard",
|
|
368
|
+
fetchOptions: { headers },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(res.data?.url).toBe("https://billing.stripe.com/mock");
|
|
372
|
+
expect(mockStripeOrg.billingPortal.sessions.create).toHaveBeenCalledWith(
|
|
373
|
+
expect.objectContaining({
|
|
374
|
+
customer: "cus_portal_org_123",
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should cancel subscription for organization", async () => {
|
|
380
|
+
mockStripeOrg.subscriptions.list.mockResolvedValueOnce({
|
|
381
|
+
data: [
|
|
382
|
+
{
|
|
383
|
+
id: "sub_org_cancel_123",
|
|
384
|
+
status: "active",
|
|
385
|
+
cancel_at_period_end: false,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const { client, auth, sessionSetter } = await getTestInstance(
|
|
391
|
+
{
|
|
392
|
+
plugins: [organization(), stripe(baseOrgStripeOptions)],
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
disableTestUser: true,
|
|
396
|
+
clientOptions: {
|
|
397
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
const ctx = await auth.$context;
|
|
402
|
+
|
|
403
|
+
await client.signUp.email(
|
|
404
|
+
{ ...testUser, email: "org-cancel-test@email.com" },
|
|
405
|
+
{ throw: true },
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
const headers = new Headers();
|
|
409
|
+
await client.signIn.email(
|
|
410
|
+
{ ...testUser, email: "org-cancel-test@email.com" },
|
|
411
|
+
{
|
|
412
|
+
throw: true,
|
|
413
|
+
onSuccess: sessionSetter(headers),
|
|
414
|
+
},
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Create organization via client
|
|
418
|
+
const org = await client.organization.create({
|
|
419
|
+
name: "Cancel Test Org",
|
|
420
|
+
slug: "cancel-test-org",
|
|
421
|
+
fetchOptions: { headers },
|
|
422
|
+
});
|
|
423
|
+
const orgId = org.data?.id as string;
|
|
424
|
+
|
|
425
|
+
// Update org with stripeCustomerId
|
|
426
|
+
await ctx.adapter.update({
|
|
427
|
+
model: "organization",
|
|
428
|
+
update: { stripeCustomerId: "cus_cancel_org_123" },
|
|
429
|
+
where: [{ field: "id", value: orgId }],
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await ctx.adapter.create({
|
|
433
|
+
model: "subscription",
|
|
434
|
+
data: {
|
|
435
|
+
referenceId: orgId,
|
|
436
|
+
stripeCustomerId: "cus_cancel_org_123",
|
|
437
|
+
stripeSubscriptionId: "sub_org_cancel_123",
|
|
438
|
+
status: "active",
|
|
439
|
+
plan: "starter",
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const res = await client.subscription.cancel({
|
|
444
|
+
customerType: "organization",
|
|
445
|
+
referenceId: orgId,
|
|
446
|
+
returnUrl: "/dashboard",
|
|
447
|
+
fetchOptions: { headers },
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(res.data?.url).toBeDefined();
|
|
451
|
+
expect(mockStripeOrg.billingPortal.sessions.create).toHaveBeenCalledWith(
|
|
452
|
+
expect.objectContaining({
|
|
453
|
+
customer: "cus_cancel_org_123",
|
|
454
|
+
flow_data: expect.objectContaining({
|
|
455
|
+
type: "subscription_cancel",
|
|
456
|
+
subscription_cancel: {
|
|
457
|
+
subscription: "sub_org_cancel_123",
|
|
458
|
+
},
|
|
459
|
+
}),
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("should restore subscription for organization", async () => {
|
|
465
|
+
mockStripeOrg.subscriptions.list.mockResolvedValueOnce({
|
|
466
|
+
data: [
|
|
467
|
+
{
|
|
468
|
+
id: "sub_org_restore_123",
|
|
469
|
+
status: "active",
|
|
470
|
+
cancel_at_period_end: true,
|
|
471
|
+
cancel_at: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
});
|
|
475
|
+
mockStripeOrg.subscriptions.update.mockResolvedValueOnce({
|
|
476
|
+
id: "sub_org_restore_123",
|
|
477
|
+
status: "active",
|
|
478
|
+
cancel_at_period_end: false,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const { client, auth, sessionSetter } = await getTestInstance(
|
|
482
|
+
{
|
|
483
|
+
plugins: [organization(), stripe(baseOrgStripeOptions)],
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
disableTestUser: true,
|
|
487
|
+
clientOptions: {
|
|
488
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
const ctx = await auth.$context;
|
|
493
|
+
|
|
494
|
+
await client.signUp.email(
|
|
495
|
+
{ ...testUser, email: "org-restore-test@email.com" },
|
|
496
|
+
{ throw: true },
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
const headers = new Headers();
|
|
500
|
+
await client.signIn.email(
|
|
501
|
+
{ ...testUser, email: "org-restore-test@email.com" },
|
|
502
|
+
{
|
|
503
|
+
throw: true,
|
|
504
|
+
onSuccess: sessionSetter(headers),
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// Create organization via client
|
|
509
|
+
const org = await client.organization.create({
|
|
510
|
+
name: "Restore Test Org",
|
|
511
|
+
slug: "restore-test-org",
|
|
512
|
+
fetchOptions: { headers },
|
|
513
|
+
});
|
|
514
|
+
const orgId = org.data?.id as string;
|
|
515
|
+
|
|
516
|
+
// Update org with stripeCustomerId
|
|
517
|
+
await ctx.adapter.update({
|
|
518
|
+
model: "organization",
|
|
519
|
+
update: { stripeCustomerId: "cus_restore_org_123" },
|
|
520
|
+
where: [{ field: "id", value: orgId }],
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await ctx.adapter.create({
|
|
524
|
+
model: "subscription",
|
|
525
|
+
data: {
|
|
526
|
+
referenceId: orgId,
|
|
527
|
+
stripeCustomerId: "cus_restore_org_123",
|
|
528
|
+
stripeSubscriptionId: "sub_org_restore_123",
|
|
529
|
+
status: "active",
|
|
530
|
+
plan: "starter",
|
|
531
|
+
cancelAtPeriodEnd: true,
|
|
532
|
+
canceledAt: new Date(),
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const res = await client.subscription.restore({
|
|
537
|
+
customerType: "organization",
|
|
538
|
+
referenceId: orgId,
|
|
539
|
+
fetchOptions: { headers },
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
expect(res.data).toBeDefined();
|
|
543
|
+
// Note: Stripe API doesn't accept both cancel_at and cancel_at_period_end simultaneously
|
|
544
|
+
expect(mockStripeOrg.subscriptions.update).toHaveBeenCalledWith(
|
|
545
|
+
"sub_org_restore_123",
|
|
546
|
+
expect.objectContaining({
|
|
547
|
+
cancel_at: "",
|
|
548
|
+
}),
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Verify subscription was updated in DB
|
|
552
|
+
const updatedSub = await ctx.adapter.findOne<Subscription>({
|
|
553
|
+
model: "subscription",
|
|
554
|
+
where: [{ field: "referenceId", value: orgId }],
|
|
555
|
+
});
|
|
556
|
+
expect(updatedSub?.cancelAtPeriodEnd).toBe(false);
|
|
557
|
+
expect(updatedSub?.canceledAt).toBeNull();
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("should list subscriptions for organization", async () => {
|
|
561
|
+
const { client, auth, sessionSetter } = await getTestInstance(
|
|
562
|
+
{
|
|
563
|
+
plugins: [organization(), stripe(baseOrgStripeOptions)],
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
disableTestUser: true,
|
|
567
|
+
clientOptions: {
|
|
568
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
const ctx = await auth.$context;
|
|
573
|
+
|
|
574
|
+
await client.signUp.email(
|
|
575
|
+
{ ...testUser, email: "org-list-test@email.com" },
|
|
576
|
+
{ throw: true },
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
const headers = new Headers();
|
|
580
|
+
await client.signIn.email(
|
|
581
|
+
{ ...testUser, email: "org-list-test@email.com" },
|
|
582
|
+
{
|
|
583
|
+
throw: true,
|
|
584
|
+
onSuccess: sessionSetter(headers),
|
|
585
|
+
},
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
// Create organization via client
|
|
589
|
+
const org = await client.organization.create({
|
|
590
|
+
name: "List Test Org",
|
|
591
|
+
slug: "list-test-org",
|
|
592
|
+
fetchOptions: { headers },
|
|
593
|
+
});
|
|
594
|
+
const orgId = org.data?.id as string;
|
|
595
|
+
|
|
596
|
+
// Update org with stripeCustomerId
|
|
597
|
+
await ctx.adapter.update({
|
|
598
|
+
model: "organization",
|
|
599
|
+
update: { stripeCustomerId: "cus_list_org_123" },
|
|
600
|
+
where: [{ field: "id", value: orgId }],
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Create subscription for org
|
|
604
|
+
await ctx.adapter.create({
|
|
605
|
+
model: "subscription",
|
|
606
|
+
data: {
|
|
607
|
+
referenceId: orgId,
|
|
608
|
+
stripeCustomerId: "cus_list_org_123",
|
|
609
|
+
stripeSubscriptionId: "sub_org_list_123",
|
|
610
|
+
status: "active",
|
|
611
|
+
plan: "starter",
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const res = await client.subscription.list({
|
|
616
|
+
query: {
|
|
617
|
+
customerType: "organization",
|
|
618
|
+
referenceId: orgId,
|
|
619
|
+
},
|
|
620
|
+
fetchOptions: { headers },
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(res.data?.length).toBe(1);
|
|
624
|
+
expect(res.data?.[0]).toMatchObject({
|
|
625
|
+
referenceId: orgId,
|
|
626
|
+
plan: "starter",
|
|
627
|
+
status: "active",
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("should handle webhook for organization subscription created from dashboard", async () => {
|
|
632
|
+
const mockEvent = {
|
|
633
|
+
type: "customer.subscription.created",
|
|
634
|
+
data: {
|
|
635
|
+
object: {
|
|
636
|
+
id: "sub_org_webhook_123",
|
|
637
|
+
customer: "cus_org_webhook_123",
|
|
638
|
+
status: "active",
|
|
639
|
+
items: {
|
|
640
|
+
data: [
|
|
641
|
+
{
|
|
642
|
+
price: {
|
|
643
|
+
id: process.env.STRIPE_PRICE_ID_1,
|
|
644
|
+
lookup_key: null,
|
|
645
|
+
},
|
|
646
|
+
quantity: 5,
|
|
647
|
+
current_period_start: Math.floor(Date.now() / 1000),
|
|
648
|
+
current_period_end:
|
|
649
|
+
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
},
|
|
653
|
+
cancel_at_period_end: false,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const stripeForOrgWebhook = {
|
|
659
|
+
...mockStripeOrg,
|
|
660
|
+
webhooks: {
|
|
661
|
+
constructEventAsync: vi.fn().mockResolvedValue(mockEvent),
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const testOrgWebhookOptions: StripeOptions = {
|
|
666
|
+
...baseOrgStripeOptions,
|
|
667
|
+
stripeClient: stripeForOrgWebhook as unknown as Stripe,
|
|
668
|
+
stripeWebhookSecret: "test_secret",
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const { auth, client, sessionSetter } = await getTestInstance(
|
|
672
|
+
{
|
|
673
|
+
plugins: [organization(), stripe(testOrgWebhookOptions)],
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
disableTestUser: true,
|
|
677
|
+
clientOptions: {
|
|
678
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
);
|
|
682
|
+
const ctx = await auth.$context;
|
|
683
|
+
|
|
684
|
+
// Sign up and sign in
|
|
685
|
+
await client.signUp.email(
|
|
686
|
+
{ ...testUser, email: "org-webhook-test@email.com" },
|
|
687
|
+
{ throw: true },
|
|
688
|
+
);
|
|
689
|
+
const headers = new Headers();
|
|
690
|
+
await client.signIn.email(
|
|
691
|
+
{ ...testUser, email: "org-webhook-test@email.com" },
|
|
692
|
+
{
|
|
693
|
+
throw: true,
|
|
694
|
+
onSuccess: sessionSetter(headers),
|
|
695
|
+
},
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
// Create organization via client
|
|
699
|
+
const org = await client.organization.create({
|
|
700
|
+
name: "Webhook Test Org",
|
|
701
|
+
slug: "webhook-test-org",
|
|
702
|
+
fetchOptions: { headers },
|
|
703
|
+
});
|
|
704
|
+
const orgId = org.data?.id as string;
|
|
705
|
+
|
|
706
|
+
// Update org with stripeCustomerId
|
|
707
|
+
await ctx.adapter.update({
|
|
708
|
+
model: "organization",
|
|
709
|
+
update: { stripeCustomerId: "cus_org_webhook_123" },
|
|
710
|
+
where: [{ field: "id", value: orgId }],
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const mockRequest = new Request(
|
|
714
|
+
"http://localhost:3000/api/auth/stripe/webhook",
|
|
715
|
+
{
|
|
716
|
+
method: "POST",
|
|
717
|
+
headers: {
|
|
718
|
+
"stripe-signature": "test_signature",
|
|
719
|
+
},
|
|
720
|
+
body: JSON.stringify(mockEvent),
|
|
721
|
+
},
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
const response = await auth.handler(mockRequest);
|
|
725
|
+
expect(response.status).toBe(200);
|
|
726
|
+
|
|
727
|
+
// Verify subscription was created for organization
|
|
728
|
+
const subscription = await ctx.adapter.findOne<Subscription>({
|
|
729
|
+
model: "subscription",
|
|
730
|
+
where: [{ field: "stripeSubscriptionId", value: "sub_org_webhook_123" }],
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
expect(subscription).toBeDefined();
|
|
734
|
+
expect(subscription?.referenceId).toBe(orgId);
|
|
735
|
+
expect(subscription?.stripeCustomerId).toBe("cus_org_webhook_123");
|
|
736
|
+
expect(subscription?.status).toBe("active");
|
|
737
|
+
expect(subscription?.plan).toBe("starter");
|
|
738
|
+
expect(subscription?.seats).toBe(5);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it("should not allow cross-organization subscription operations", async () => {
|
|
742
|
+
// Track which org is being accessed
|
|
743
|
+
let otherOrgIdForCheck: string = "";
|
|
744
|
+
|
|
745
|
+
const { client, auth, sessionSetter } = await getTestInstance(
|
|
746
|
+
{
|
|
747
|
+
plugins: [
|
|
748
|
+
organization(),
|
|
749
|
+
stripe({
|
|
750
|
+
...baseOrgStripeOptions,
|
|
751
|
+
subscription: {
|
|
752
|
+
...baseOrgStripeOptions.subscription,
|
|
753
|
+
authorizeReference: async ({ referenceId }) => {
|
|
754
|
+
// Simulate member check: only allow if user is member of org
|
|
755
|
+
// For test, we'll return false for the "other" org
|
|
756
|
+
return referenceId !== otherOrgIdForCheck;
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
}),
|
|
760
|
+
],
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
disableTestUser: true,
|
|
764
|
+
clientOptions: {
|
|
765
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
);
|
|
769
|
+
const ctx = await auth.$context;
|
|
770
|
+
|
|
771
|
+
await client.signUp.email(
|
|
772
|
+
{ ...testUser, email: "cross-org-test@email.com" },
|
|
773
|
+
{ throw: true },
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
const headers = new Headers();
|
|
777
|
+
await client.signIn.email(
|
|
778
|
+
{ ...testUser, email: "cross-org-test@email.com" },
|
|
779
|
+
{
|
|
780
|
+
throw: true,
|
|
781
|
+
onSuccess: sessionSetter(headers),
|
|
782
|
+
},
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
// Create user's organization via client
|
|
786
|
+
const userOrg = await client.organization.create({
|
|
787
|
+
name: "User Org",
|
|
788
|
+
slug: "user-org",
|
|
789
|
+
fetchOptions: { headers },
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// Update org with stripeCustomerId
|
|
793
|
+
await ctx.adapter.update({
|
|
794
|
+
model: "organization",
|
|
795
|
+
update: { stripeCustomerId: "cus_user_org" },
|
|
796
|
+
where: [{ field: "id", value: userOrg.data?.id as string }],
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Create other organization via client
|
|
800
|
+
const { id: otherOrgId } = await ctx.adapter.create({
|
|
801
|
+
model: "organization",
|
|
802
|
+
data: {
|
|
803
|
+
name: "Other Org",
|
|
804
|
+
slug: "other-org",
|
|
805
|
+
stripeCustomerId: "cus_other_org",
|
|
806
|
+
createdAt: new Date(),
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
otherOrgIdForCheck = otherOrgId;
|
|
810
|
+
|
|
811
|
+
// Create subscription for other org
|
|
812
|
+
await ctx.adapter.create({
|
|
813
|
+
model: "subscription",
|
|
814
|
+
data: {
|
|
815
|
+
referenceId: otherOrgId,
|
|
816
|
+
stripeCustomerId: "cus_other_org",
|
|
817
|
+
stripeSubscriptionId: "sub_other_org",
|
|
818
|
+
status: "active",
|
|
819
|
+
plan: "starter",
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// Try to cancel other org's subscription - authorizeReference returns false for org_other
|
|
824
|
+
const cancelRes = await client.subscription.cancel({
|
|
825
|
+
customerType: "organization",
|
|
826
|
+
referenceId: otherOrgId,
|
|
827
|
+
returnUrl: "/dashboard",
|
|
828
|
+
fetchOptions: { headers },
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// authorizeReference returns false -> UNAUTHORIZED from middleware
|
|
832
|
+
expect(cancelRes.error?.code).toBe("UNAUTHORIZED");
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it("should reject organization subscription when organization.enabled is false", async () => {
|
|
836
|
+
const stripeOptionsWithoutOrg: StripeOptions = {
|
|
837
|
+
...baseOrgStripeOptions,
|
|
838
|
+
organization: undefined, // Disable organization support
|
|
839
|
+
subscription: {
|
|
840
|
+
...baseOrgStripeOptions.subscription,
|
|
841
|
+
// Remove authorizeReference so middleware can check organization.enabled
|
|
842
|
+
authorizeReference: undefined,
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const { client, sessionSetter } = await getTestInstance(
|
|
847
|
+
{
|
|
848
|
+
plugins: [stripe(stripeOptionsWithoutOrg)],
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
disableTestUser: true,
|
|
852
|
+
clientOptions: {
|
|
853
|
+
plugins: [stripeClient({ subscription: true })],
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
await client.signUp.email(
|
|
859
|
+
{ ...testUser, email: "org-disabled-test@email.com" },
|
|
860
|
+
{ throw: true },
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
const headers = new Headers();
|
|
864
|
+
await client.signIn.email(
|
|
865
|
+
{ ...testUser, email: "org-disabled-test@email.com" },
|
|
866
|
+
{
|
|
867
|
+
throw: true,
|
|
868
|
+
onSuccess: sessionSetter(headers),
|
|
869
|
+
},
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
// Try to upgrade with organization customerType when organization is not enabled
|
|
873
|
+
// Without authorizeReference, middleware rejects organization subscriptions
|
|
874
|
+
const res = await client.subscription.upgrade({
|
|
875
|
+
plan: "starter",
|
|
876
|
+
customerType: "organization",
|
|
877
|
+
referenceId: "fake-org-id",
|
|
878
|
+
fetchOptions: { headers },
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
expect(res.error?.code).toBe("ORGANIZATION_SUBSCRIPTION_NOT_ENABLED");
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it("should keep user and organization subscriptions separate", async () => {
|
|
885
|
+
const { client, auth, sessionSetter } = await getTestInstance(
|
|
886
|
+
{
|
|
887
|
+
plugins: [organization(), stripe(baseOrgStripeOptions)],
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
disableTestUser: true,
|
|
891
|
+
clientOptions: {
|
|
892
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
);
|
|
896
|
+
const ctx = await auth.$context;
|
|
897
|
+
|
|
898
|
+
const userRes = await client.signUp.email(
|
|
899
|
+
{ ...testUser, email: "separate-sub-test@email.com" },
|
|
900
|
+
{ throw: true },
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
const headers = new Headers();
|
|
904
|
+
await client.signIn.email(
|
|
905
|
+
{ ...testUser, email: "separate-sub-test@email.com" },
|
|
906
|
+
{
|
|
907
|
+
throw: true,
|
|
908
|
+
onSuccess: sessionSetter(headers),
|
|
909
|
+
},
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
// Create organization via client
|
|
913
|
+
const org = await client.organization.create({
|
|
914
|
+
name: "Separate Sub Org",
|
|
915
|
+
slug: "separate-sub-org",
|
|
916
|
+
fetchOptions: { headers },
|
|
917
|
+
});
|
|
918
|
+
const orgId = org.data?.id as string;
|
|
919
|
+
|
|
920
|
+
// Update org with stripeCustomerId
|
|
921
|
+
await ctx.adapter.update({
|
|
922
|
+
model: "organization",
|
|
923
|
+
update: { stripeCustomerId: "cus_separate_org" },
|
|
924
|
+
where: [{ field: "id", value: orgId }],
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// Create user subscription
|
|
928
|
+
await ctx.adapter.create({
|
|
929
|
+
model: "subscription",
|
|
930
|
+
data: {
|
|
931
|
+
referenceId: userRes.user.id,
|
|
932
|
+
stripeCustomerId: "cus_user_123",
|
|
933
|
+
stripeSubscriptionId: "sub_user_123",
|
|
934
|
+
status: "active",
|
|
935
|
+
plan: "starter",
|
|
936
|
+
},
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// Create org subscription
|
|
940
|
+
await ctx.adapter.create({
|
|
941
|
+
model: "subscription",
|
|
942
|
+
data: {
|
|
943
|
+
referenceId: orgId,
|
|
944
|
+
stripeCustomerId: "cus_separate_org",
|
|
945
|
+
stripeSubscriptionId: "sub_org_123",
|
|
946
|
+
status: "active",
|
|
947
|
+
plan: "premium",
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// List user subscriptions
|
|
952
|
+
const userSubs = await client.subscription.list({
|
|
953
|
+
fetchOptions: { headers },
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
// List org subscriptions
|
|
957
|
+
const orgSubs = await client.subscription.list({
|
|
958
|
+
query: {
|
|
959
|
+
customerType: "organization",
|
|
960
|
+
referenceId: orgId,
|
|
961
|
+
},
|
|
962
|
+
fetchOptions: { headers },
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
expect(userSubs.data?.length).toBe(1);
|
|
966
|
+
expect(userSubs.data?.[0]?.plan).toBe("starter");
|
|
967
|
+
expect(userSubs.data?.[0]?.referenceId).toBe(userRes.user.id);
|
|
968
|
+
|
|
969
|
+
expect(orgSubs.data?.length).toBe(1);
|
|
970
|
+
expect(orgSubs.data?.[0]?.plan).toBe("premium");
|
|
971
|
+
expect(orgSubs.data?.[0]?.referenceId).toBe(orgId);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it("should handle customer.subscription.updated webhook for organization", async () => {
|
|
975
|
+
const onSubscriptionUpdate = vi.fn();
|
|
976
|
+
const onSubscriptionCancel = vi.fn();
|
|
977
|
+
|
|
978
|
+
const mockUpdateEvent = {
|
|
979
|
+
type: "customer.subscription.updated",
|
|
980
|
+
data: {
|
|
981
|
+
object: {
|
|
982
|
+
id: "sub_org_update_123",
|
|
983
|
+
customer: "cus_org_update_123",
|
|
984
|
+
status: "active",
|
|
985
|
+
items: {
|
|
986
|
+
data: [
|
|
987
|
+
{
|
|
988
|
+
price: {
|
|
989
|
+
id: "price_premium_123", // Different price ID
|
|
990
|
+
lookup_key: "lookup_key_234", // Matches premium plan
|
|
991
|
+
},
|
|
992
|
+
quantity: 10,
|
|
993
|
+
current_period_start: Math.floor(Date.now() / 1000),
|
|
994
|
+
current_period_end:
|
|
995
|
+
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
},
|
|
999
|
+
cancel_at_period_end: false,
|
|
1000
|
+
cancel_at: null,
|
|
1001
|
+
canceled_at: null,
|
|
1002
|
+
ended_at: null,
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
const stripeForOrgUpdateWebhook = {
|
|
1008
|
+
...mockStripeOrg,
|
|
1009
|
+
webhooks: {
|
|
1010
|
+
constructEventAsync: vi.fn().mockResolvedValue(mockUpdateEvent),
|
|
1011
|
+
},
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
const testOrgUpdateWebhookOptions: StripeOptions = {
|
|
1015
|
+
...baseOrgStripeOptions,
|
|
1016
|
+
stripeClient: stripeForOrgUpdateWebhook as unknown as Stripe,
|
|
1017
|
+
stripeWebhookSecret: "test_secret",
|
|
1018
|
+
subscription: {
|
|
1019
|
+
...baseOrgStripeOptions.subscription,
|
|
1020
|
+
onSubscriptionUpdate,
|
|
1021
|
+
onSubscriptionCancel,
|
|
1022
|
+
},
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
const { auth, client, sessionSetter } = await getTestInstance(
|
|
1026
|
+
{
|
|
1027
|
+
plugins: [organization(), stripe(testOrgUpdateWebhookOptions)],
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
disableTestUser: true,
|
|
1031
|
+
clientOptions: {
|
|
1032
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
);
|
|
1036
|
+
const ctx = await auth.$context;
|
|
1037
|
+
|
|
1038
|
+
// Sign up and sign in
|
|
1039
|
+
await client.signUp.email(
|
|
1040
|
+
{ ...testUser, email: "org-update-webhook-test@email.com" },
|
|
1041
|
+
{ throw: true },
|
|
1042
|
+
);
|
|
1043
|
+
const headers = new Headers();
|
|
1044
|
+
await client.signIn.email(
|
|
1045
|
+
{ ...testUser, email: "org-update-webhook-test@email.com" },
|
|
1046
|
+
{
|
|
1047
|
+
throw: true,
|
|
1048
|
+
onSuccess: sessionSetter(headers),
|
|
1049
|
+
},
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
// Create organization via client
|
|
1053
|
+
const org = await client.organization.create({
|
|
1054
|
+
name: "Update Webhook Test Org",
|
|
1055
|
+
slug: "update-webhook-test-org",
|
|
1056
|
+
fetchOptions: { headers },
|
|
1057
|
+
});
|
|
1058
|
+
const orgId = org.data?.id as string;
|
|
1059
|
+
|
|
1060
|
+
// Update org with stripeCustomerId
|
|
1061
|
+
await ctx.adapter.update({
|
|
1062
|
+
model: "organization",
|
|
1063
|
+
update: { stripeCustomerId: "cus_org_update_123" },
|
|
1064
|
+
where: [{ field: "id", value: orgId }],
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
// Create existing subscription for org
|
|
1068
|
+
const { id: subId } = await ctx.adapter.create({
|
|
1069
|
+
model: "subscription",
|
|
1070
|
+
data: {
|
|
1071
|
+
referenceId: orgId,
|
|
1072
|
+
stripeCustomerId: "cus_org_update_123",
|
|
1073
|
+
stripeSubscriptionId: "sub_org_update_123",
|
|
1074
|
+
status: "active",
|
|
1075
|
+
plan: "starter",
|
|
1076
|
+
seats: 5,
|
|
1077
|
+
},
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
const mockRequest = new Request(
|
|
1081
|
+
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1082
|
+
{
|
|
1083
|
+
method: "POST",
|
|
1084
|
+
headers: {
|
|
1085
|
+
"stripe-signature": "test_signature",
|
|
1086
|
+
},
|
|
1087
|
+
body: JSON.stringify(mockUpdateEvent),
|
|
1088
|
+
},
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
const response = await auth.handler(mockRequest);
|
|
1092
|
+
expect(response.status).toBe(200);
|
|
1093
|
+
|
|
1094
|
+
// Verify subscription was updated
|
|
1095
|
+
const updatedSubscription = await ctx.adapter.findOne<Subscription>({
|
|
1096
|
+
model: "subscription",
|
|
1097
|
+
where: [{ field: "id", value: subId }],
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
expect(updatedSubscription).toBeDefined();
|
|
1101
|
+
expect(updatedSubscription?.plan).toBe("premium");
|
|
1102
|
+
expect(updatedSubscription?.seats).toBe(10);
|
|
1103
|
+
expect(updatedSubscription?.status).toBe("active");
|
|
1104
|
+
|
|
1105
|
+
// Verify callback was called
|
|
1106
|
+
expect(onSubscriptionUpdate).toHaveBeenCalledWith(
|
|
1107
|
+
expect.objectContaining({
|
|
1108
|
+
subscription: expect.objectContaining({
|
|
1109
|
+
referenceId: orgId,
|
|
1110
|
+
plan: "premium",
|
|
1111
|
+
}),
|
|
1112
|
+
}),
|
|
1113
|
+
);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
it("should handle customer.subscription.updated webhook with cancellation for organization", async () => {
|
|
1117
|
+
const onSubscriptionCancel = vi.fn();
|
|
1118
|
+
|
|
1119
|
+
const cancelAt = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60;
|
|
1120
|
+
const mockCancelEvent = {
|
|
1121
|
+
type: "customer.subscription.updated",
|
|
1122
|
+
data: {
|
|
1123
|
+
object: {
|
|
1124
|
+
id: "sub_org_cancel_webhook_123",
|
|
1125
|
+
customer: "cus_org_cancel_webhook_123",
|
|
1126
|
+
status: "active",
|
|
1127
|
+
items: {
|
|
1128
|
+
data: [
|
|
1129
|
+
{
|
|
1130
|
+
price: {
|
|
1131
|
+
id: process.env.STRIPE_PRICE_ID_1,
|
|
1132
|
+
lookup_key: null,
|
|
1133
|
+
},
|
|
1134
|
+
quantity: 5,
|
|
1135
|
+
current_period_start: Math.floor(Date.now() / 1000),
|
|
1136
|
+
current_period_end: cancelAt,
|
|
1137
|
+
},
|
|
1138
|
+
],
|
|
1139
|
+
},
|
|
1140
|
+
cancel_at_period_end: true,
|
|
1141
|
+
cancel_at: cancelAt,
|
|
1142
|
+
canceled_at: Math.floor(Date.now() / 1000),
|
|
1143
|
+
ended_at: null,
|
|
1144
|
+
cancellation_details: {
|
|
1145
|
+
reason: "cancellation_requested",
|
|
1146
|
+
comment: "User requested cancellation",
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
const stripeForOrgCancelWebhook = {
|
|
1153
|
+
...mockStripeOrg,
|
|
1154
|
+
webhooks: {
|
|
1155
|
+
constructEventAsync: vi.fn().mockResolvedValue(mockCancelEvent),
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
const testOrgCancelWebhookOptions: StripeOptions = {
|
|
1160
|
+
...baseOrgStripeOptions,
|
|
1161
|
+
stripeClient: stripeForOrgCancelWebhook as unknown as Stripe,
|
|
1162
|
+
stripeWebhookSecret: "test_secret",
|
|
1163
|
+
subscription: {
|
|
1164
|
+
...baseOrgStripeOptions.subscription,
|
|
1165
|
+
onSubscriptionCancel,
|
|
1166
|
+
},
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
const { auth, client, sessionSetter } = await getTestInstance(
|
|
1170
|
+
{
|
|
1171
|
+
plugins: [organization(), stripe(testOrgCancelWebhookOptions)],
|
|
1172
|
+
},
|
|
1173
|
+
{
|
|
1174
|
+
disableTestUser: true,
|
|
1175
|
+
clientOptions: {
|
|
1176
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1179
|
+
);
|
|
1180
|
+
const ctx = await auth.$context;
|
|
1181
|
+
|
|
1182
|
+
// Sign up and sign in
|
|
1183
|
+
await client.signUp.email(
|
|
1184
|
+
{ ...testUser, email: "org-cancel-webhook-test@email.com" },
|
|
1185
|
+
{ throw: true },
|
|
1186
|
+
);
|
|
1187
|
+
const headers = new Headers();
|
|
1188
|
+
await client.signIn.email(
|
|
1189
|
+
{ ...testUser, email: "org-cancel-webhook-test@email.com" },
|
|
1190
|
+
{
|
|
1191
|
+
throw: true,
|
|
1192
|
+
onSuccess: sessionSetter(headers),
|
|
1193
|
+
},
|
|
1194
|
+
);
|
|
1195
|
+
|
|
1196
|
+
// Create organization via client
|
|
1197
|
+
const org = await client.organization.create({
|
|
1198
|
+
name: "Cancel Webhook Test Org",
|
|
1199
|
+
slug: "cancel-webhook-test-org",
|
|
1200
|
+
fetchOptions: { headers },
|
|
1201
|
+
});
|
|
1202
|
+
const orgId = org.data?.id as string;
|
|
1203
|
+
|
|
1204
|
+
// Update org with stripeCustomerId
|
|
1205
|
+
await ctx.adapter.update({
|
|
1206
|
+
model: "organization",
|
|
1207
|
+
update: { stripeCustomerId: "cus_org_cancel_webhook_123" },
|
|
1208
|
+
where: [{ field: "id", value: orgId }],
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// Create subscription (not yet marked as pending cancel)
|
|
1212
|
+
const { id: subId } = await ctx.adapter.create({
|
|
1213
|
+
model: "subscription",
|
|
1214
|
+
data: {
|
|
1215
|
+
referenceId: orgId,
|
|
1216
|
+
stripeCustomerId: "cus_org_cancel_webhook_123",
|
|
1217
|
+
stripeSubscriptionId: "sub_org_cancel_webhook_123",
|
|
1218
|
+
status: "active",
|
|
1219
|
+
plan: "starter",
|
|
1220
|
+
cancelAtPeriodEnd: false,
|
|
1221
|
+
},
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
const mockRequest = new Request(
|
|
1225
|
+
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1226
|
+
{
|
|
1227
|
+
method: "POST",
|
|
1228
|
+
headers: {
|
|
1229
|
+
"stripe-signature": "test_signature",
|
|
1230
|
+
},
|
|
1231
|
+
body: JSON.stringify(mockCancelEvent),
|
|
1232
|
+
},
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
const response = await auth.handler(mockRequest);
|
|
1236
|
+
expect(response.status).toBe(200);
|
|
1237
|
+
|
|
1238
|
+
// Verify subscription was updated with cancellation info
|
|
1239
|
+
const updatedSubscription = await ctx.adapter.findOne<Subscription>({
|
|
1240
|
+
model: "subscription",
|
|
1241
|
+
where: [{ field: "id", value: subId }],
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
expect(updatedSubscription?.cancelAtPeriodEnd).toBe(true);
|
|
1245
|
+
expect(updatedSubscription?.cancelAt).toBeDefined();
|
|
1246
|
+
expect(updatedSubscription?.canceledAt).toBeDefined();
|
|
1247
|
+
|
|
1248
|
+
// Verify onSubscriptionCancel callback was called
|
|
1249
|
+
expect(onSubscriptionCancel).toHaveBeenCalledWith(
|
|
1250
|
+
expect.objectContaining({
|
|
1251
|
+
subscription: expect.objectContaining({
|
|
1252
|
+
referenceId: orgId,
|
|
1253
|
+
}),
|
|
1254
|
+
cancellationDetails: expect.objectContaining({
|
|
1255
|
+
reason: "cancellation_requested",
|
|
1256
|
+
}),
|
|
1257
|
+
}),
|
|
1258
|
+
);
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
it("should handle customer.subscription.deleted webhook for organization", async () => {
|
|
1262
|
+
const onSubscriptionDeleted = vi.fn();
|
|
1263
|
+
|
|
1264
|
+
const mockDeleteEvent = {
|
|
1265
|
+
type: "customer.subscription.deleted",
|
|
1266
|
+
data: {
|
|
1267
|
+
object: {
|
|
1268
|
+
id: "sub_org_delete_123",
|
|
1269
|
+
customer: "cus_org_delete_123",
|
|
1270
|
+
status: "canceled",
|
|
1271
|
+
cancel_at_period_end: false,
|
|
1272
|
+
cancel_at: null,
|
|
1273
|
+
canceled_at: Math.floor(Date.now() / 1000),
|
|
1274
|
+
ended_at: Math.floor(Date.now() / 1000),
|
|
1275
|
+
},
|
|
1276
|
+
},
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
const stripeForOrgDeleteWebhook = {
|
|
1280
|
+
...mockStripeOrg,
|
|
1281
|
+
webhooks: {
|
|
1282
|
+
constructEventAsync: vi.fn().mockResolvedValue(mockDeleteEvent),
|
|
1283
|
+
},
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
const testOrgDeleteWebhookOptions: StripeOptions = {
|
|
1287
|
+
...baseOrgStripeOptions,
|
|
1288
|
+
stripeClient: stripeForOrgDeleteWebhook as unknown as Stripe,
|
|
1289
|
+
stripeWebhookSecret: "test_secret",
|
|
1290
|
+
subscription: {
|
|
1291
|
+
...baseOrgStripeOptions.subscription,
|
|
1292
|
+
onSubscriptionDeleted,
|
|
1293
|
+
},
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
const { auth, client, sessionSetter } = await getTestInstance(
|
|
1297
|
+
{
|
|
1298
|
+
plugins: [organization(), stripe(testOrgDeleteWebhookOptions)],
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
disableTestUser: true,
|
|
1302
|
+
clientOptions: {
|
|
1303
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1304
|
+
},
|
|
1305
|
+
},
|
|
1306
|
+
);
|
|
1307
|
+
const ctx = await auth.$context;
|
|
1308
|
+
|
|
1309
|
+
// Sign up and sign in
|
|
1310
|
+
await client.signUp.email(
|
|
1311
|
+
{ ...testUser, email: "org-delete-webhook-test@email.com" },
|
|
1312
|
+
{ throw: true },
|
|
1313
|
+
);
|
|
1314
|
+
const headers = new Headers();
|
|
1315
|
+
await client.signIn.email(
|
|
1316
|
+
{ ...testUser, email: "org-delete-webhook-test@email.com" },
|
|
1317
|
+
{
|
|
1318
|
+
throw: true,
|
|
1319
|
+
onSuccess: sessionSetter(headers),
|
|
1320
|
+
},
|
|
1321
|
+
);
|
|
1322
|
+
|
|
1323
|
+
// Create organization via client
|
|
1324
|
+
const org = await client.organization.create({
|
|
1325
|
+
name: "Delete Webhook Test Org",
|
|
1326
|
+
slug: "delete-webhook-test-org",
|
|
1327
|
+
fetchOptions: { headers },
|
|
1328
|
+
});
|
|
1329
|
+
const orgId = org.data?.id as string;
|
|
1330
|
+
|
|
1331
|
+
// Update org with stripeCustomerId
|
|
1332
|
+
await ctx.adapter.update({
|
|
1333
|
+
model: "organization",
|
|
1334
|
+
update: { stripeCustomerId: "cus_org_delete_123" },
|
|
1335
|
+
where: [{ field: "id", value: orgId }],
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
// Create subscription
|
|
1339
|
+
const { id: subId } = await ctx.adapter.create({
|
|
1340
|
+
model: "subscription",
|
|
1341
|
+
data: {
|
|
1342
|
+
referenceId: orgId,
|
|
1343
|
+
stripeCustomerId: "cus_org_delete_123",
|
|
1344
|
+
stripeSubscriptionId: "sub_org_delete_123",
|
|
1345
|
+
status: "active",
|
|
1346
|
+
plan: "starter",
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
const mockRequest = new Request(
|
|
1351
|
+
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1352
|
+
{
|
|
1353
|
+
method: "POST",
|
|
1354
|
+
headers: {
|
|
1355
|
+
"stripe-signature": "test_signature",
|
|
1356
|
+
},
|
|
1357
|
+
body: JSON.stringify(mockDeleteEvent),
|
|
1358
|
+
},
|
|
1359
|
+
);
|
|
1360
|
+
|
|
1361
|
+
const response = await auth.handler(mockRequest);
|
|
1362
|
+
expect(response.status).toBe(200);
|
|
1363
|
+
|
|
1364
|
+
// Verify subscription was marked as canceled
|
|
1365
|
+
const deletedSubscription = await ctx.adapter.findOne<Subscription>({
|
|
1366
|
+
model: "subscription",
|
|
1367
|
+
where: [{ field: "id", value: subId }],
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
expect(deletedSubscription?.status).toBe("canceled");
|
|
1371
|
+
expect(deletedSubscription?.canceledAt).toBeDefined();
|
|
1372
|
+
expect(deletedSubscription?.endedAt).toBeDefined();
|
|
1373
|
+
|
|
1374
|
+
// Verify callback was called
|
|
1375
|
+
expect(onSubscriptionDeleted).toHaveBeenCalledWith(
|
|
1376
|
+
expect.objectContaining({
|
|
1377
|
+
subscription: expect.objectContaining({
|
|
1378
|
+
referenceId: orgId,
|
|
1379
|
+
}),
|
|
1380
|
+
stripeSubscription: expect.objectContaining({
|
|
1381
|
+
id: "sub_org_delete_123",
|
|
1382
|
+
}),
|
|
1383
|
+
}),
|
|
1384
|
+
);
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
it("should return ORGANIZATION_NOT_FOUND when upgrading for non-existent organization", async () => {
|
|
1388
|
+
const { client, sessionSetter } = await getTestInstance(
|
|
1389
|
+
{
|
|
1390
|
+
plugins: [organization(), stripe(baseOrgStripeOptions)],
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
disableTestUser: true,
|
|
1394
|
+
clientOptions: {
|
|
1395
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1396
|
+
},
|
|
1397
|
+
},
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
await client.signUp.email(
|
|
1401
|
+
{ ...testUser, email: "org-not-found-test@email.com" },
|
|
1402
|
+
{ throw: true },
|
|
1403
|
+
);
|
|
1404
|
+
|
|
1405
|
+
const headers = new Headers();
|
|
1406
|
+
await client.signIn.email(
|
|
1407
|
+
{ ...testUser, email: "org-not-found-test@email.com" },
|
|
1408
|
+
{
|
|
1409
|
+
throw: true,
|
|
1410
|
+
onSuccess: sessionSetter(headers),
|
|
1411
|
+
},
|
|
1412
|
+
);
|
|
1413
|
+
|
|
1414
|
+
// Try to upgrade subscription for non-existent organization
|
|
1415
|
+
const res = await client.subscription.upgrade({
|
|
1416
|
+
plan: "starter",
|
|
1417
|
+
customerType: "organization",
|
|
1418
|
+
referenceId: "non_existent_org_id",
|
|
1419
|
+
fetchOptions: { headers },
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
expect(res.error?.code).toBe("ORGANIZATION_NOT_FOUND");
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
it("should return error when Stripe customer creation fails for organization", async () => {
|
|
1426
|
+
const mockStripeOrgFail = {
|
|
1427
|
+
...mockStripeOrg,
|
|
1428
|
+
customers: {
|
|
1429
|
+
...mockStripeOrg.customers,
|
|
1430
|
+
create: vi.fn().mockRejectedValue(new Error("Stripe API error")),
|
|
1431
|
+
},
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
const stripeOptionsWithFailingStripe: StripeOptions = {
|
|
1435
|
+
...baseOrgStripeOptions,
|
|
1436
|
+
stripeClient: mockStripeOrgFail as unknown as Stripe,
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
const { client, sessionSetter } = await getTestInstance(
|
|
1440
|
+
{
|
|
1441
|
+
plugins: [organization(), stripe(stripeOptionsWithFailingStripe)],
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
disableTestUser: true,
|
|
1445
|
+
clientOptions: {
|
|
1446
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1447
|
+
},
|
|
1448
|
+
},
|
|
1449
|
+
);
|
|
1450
|
+
|
|
1451
|
+
await client.signUp.email(
|
|
1452
|
+
{ ...testUser, email: "stripe-fail-test@email.com" },
|
|
1453
|
+
{ throw: true },
|
|
1454
|
+
);
|
|
1455
|
+
|
|
1456
|
+
const headers = new Headers();
|
|
1457
|
+
await client.signIn.email(
|
|
1458
|
+
{ ...testUser, email: "stripe-fail-test@email.com" },
|
|
1459
|
+
{
|
|
1460
|
+
throw: true,
|
|
1461
|
+
onSuccess: sessionSetter(headers),
|
|
1462
|
+
},
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
// Create organization without stripeCustomerId
|
|
1466
|
+
const org = await client.organization.create({
|
|
1467
|
+
name: "Stripe Fail Test Org",
|
|
1468
|
+
slug: "stripe-fail-test-org",
|
|
1469
|
+
fetchOptions: { headers },
|
|
1470
|
+
});
|
|
1471
|
+
const orgId = org.data?.id as string;
|
|
1472
|
+
|
|
1473
|
+
const res = await client.subscription.upgrade({
|
|
1474
|
+
plan: "starter",
|
|
1475
|
+
customerType: "organization",
|
|
1476
|
+
referenceId: orgId,
|
|
1477
|
+
fetchOptions: { headers },
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
expect(res.error?.code).toBe("UNABLE_TO_CREATE_CUSTOMER");
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
it("should return error when getCustomerCreateParams callback throws", async () => {
|
|
1484
|
+
const stripeOptionsWithThrowingCallback: StripeOptions = {
|
|
1485
|
+
...baseOrgStripeOptions,
|
|
1486
|
+
organization: {
|
|
1487
|
+
...baseOrgStripeOptions.organization,
|
|
1488
|
+
getCustomerCreateParams: async () => {
|
|
1489
|
+
throw new Error("Callback error");
|
|
1490
|
+
},
|
|
1491
|
+
},
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
const { client, sessionSetter } = await getTestInstance(
|
|
1495
|
+
{
|
|
1496
|
+
plugins: [organization(), stripe(stripeOptionsWithThrowingCallback)],
|
|
1497
|
+
},
|
|
1498
|
+
{
|
|
1499
|
+
disableTestUser: true,
|
|
1500
|
+
clientOptions: {
|
|
1501
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1502
|
+
},
|
|
1503
|
+
},
|
|
1504
|
+
);
|
|
1505
|
+
await client.signUp.email(
|
|
1506
|
+
{ ...testUser, email: "callback-throw-test@email.com" },
|
|
1507
|
+
{ throw: true },
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
const headers = new Headers();
|
|
1511
|
+
await client.signIn.email(
|
|
1512
|
+
{ ...testUser, email: "callback-throw-test@email.com" },
|
|
1513
|
+
{
|
|
1514
|
+
throw: true,
|
|
1515
|
+
onSuccess: sessionSetter(headers),
|
|
1516
|
+
},
|
|
1517
|
+
);
|
|
1518
|
+
|
|
1519
|
+
// Create organization without stripeCustomerId
|
|
1520
|
+
const org = await client.organization.create({
|
|
1521
|
+
name: "Callback Throw Test Org",
|
|
1522
|
+
slug: "callback-throw-test-org",
|
|
1523
|
+
fetchOptions: { headers },
|
|
1524
|
+
});
|
|
1525
|
+
const orgId = org.data?.id as string;
|
|
1526
|
+
|
|
1527
|
+
const res = await client.subscription.upgrade({
|
|
1528
|
+
plan: "starter",
|
|
1529
|
+
customerType: "organization",
|
|
1530
|
+
referenceId: orgId,
|
|
1531
|
+
fetchOptions: { headers },
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
expect(res.error?.code).toBe("UNABLE_TO_CREATE_CUSTOMER");
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
it("should call onSubscriptionCreated callback for organization subscription from dashboard", async () => {
|
|
1538
|
+
const onSubscriptionCreated = vi.fn();
|
|
1539
|
+
|
|
1540
|
+
const mockCreateEvent = {
|
|
1541
|
+
type: "customer.subscription.created",
|
|
1542
|
+
data: {
|
|
1543
|
+
object: {
|
|
1544
|
+
id: "sub_org_created_callback_123",
|
|
1545
|
+
customer: "cus_org_created_callback_123",
|
|
1546
|
+
status: "active",
|
|
1547
|
+
items: {
|
|
1548
|
+
data: [
|
|
1549
|
+
{
|
|
1550
|
+
price: {
|
|
1551
|
+
id: process.env.STRIPE_PRICE_ID_1,
|
|
1552
|
+
lookup_key: null,
|
|
1553
|
+
},
|
|
1554
|
+
quantity: 5,
|
|
1555
|
+
current_period_start: Math.floor(Date.now() / 1000),
|
|
1556
|
+
current_period_end:
|
|
1557
|
+
Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
1558
|
+
},
|
|
1559
|
+
],
|
|
1560
|
+
},
|
|
1561
|
+
cancel_at_period_end: false,
|
|
1562
|
+
trial_start: null,
|
|
1563
|
+
trial_end: null,
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
const stripeForCreatedCallback = {
|
|
1569
|
+
...mockStripeOrg,
|
|
1570
|
+
webhooks: {
|
|
1571
|
+
constructEventAsync: vi.fn().mockResolvedValue(mockCreateEvent),
|
|
1572
|
+
},
|
|
1573
|
+
};
|
|
1574
|
+
|
|
1575
|
+
const testCreatedCallbackOptions: StripeOptions = {
|
|
1576
|
+
...baseOrgStripeOptions,
|
|
1577
|
+
stripeClient: stripeForCreatedCallback as unknown as Stripe,
|
|
1578
|
+
stripeWebhookSecret: "test_secret",
|
|
1579
|
+
subscription: {
|
|
1580
|
+
...baseOrgStripeOptions.subscription,
|
|
1581
|
+
onSubscriptionCreated,
|
|
1582
|
+
},
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
const { auth, client, sessionSetter } = await getTestInstance(
|
|
1586
|
+
{
|
|
1587
|
+
plugins: [organization(), stripe(testCreatedCallbackOptions)],
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
disableTestUser: true,
|
|
1591
|
+
clientOptions: {
|
|
1592
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1593
|
+
},
|
|
1594
|
+
},
|
|
1595
|
+
);
|
|
1596
|
+
const ctx = await auth.$context;
|
|
1597
|
+
|
|
1598
|
+
// Sign up and sign in
|
|
1599
|
+
await client.signUp.email(
|
|
1600
|
+
{ ...testUser, email: "org-created-callback-test@email.com" },
|
|
1601
|
+
{ throw: true },
|
|
1602
|
+
);
|
|
1603
|
+
const headers = new Headers();
|
|
1604
|
+
await client.signIn.email(
|
|
1605
|
+
{ ...testUser, email: "org-created-callback-test@email.com" },
|
|
1606
|
+
{
|
|
1607
|
+
throw: true,
|
|
1608
|
+
onSuccess: sessionSetter(headers),
|
|
1609
|
+
},
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1612
|
+
// Create organization with stripeCustomerId
|
|
1613
|
+
const org = await client.organization.create({
|
|
1614
|
+
name: "Created Callback Test Org",
|
|
1615
|
+
slug: "created-callback-test-org",
|
|
1616
|
+
fetchOptions: { headers },
|
|
1617
|
+
});
|
|
1618
|
+
const orgId = org.data?.id as string;
|
|
1619
|
+
|
|
1620
|
+
// Update org with stripeCustomerId
|
|
1621
|
+
await ctx.adapter.update({
|
|
1622
|
+
model: "organization",
|
|
1623
|
+
update: { stripeCustomerId: "cus_org_created_callback_123" },
|
|
1624
|
+
where: [{ field: "id", value: orgId }],
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
const mockRequest = new Request(
|
|
1628
|
+
"http://localhost:3000/api/auth/stripe/webhook",
|
|
1629
|
+
{
|
|
1630
|
+
method: "POST",
|
|
1631
|
+
headers: {
|
|
1632
|
+
"stripe-signature": "test_signature",
|
|
1633
|
+
},
|
|
1634
|
+
body: JSON.stringify(mockCreateEvent),
|
|
1635
|
+
},
|
|
1636
|
+
);
|
|
1637
|
+
|
|
1638
|
+
const response = await auth.handler(mockRequest);
|
|
1639
|
+
expect(response.status).toBe(200);
|
|
1640
|
+
|
|
1641
|
+
// Verify subscription was created
|
|
1642
|
+
const subscription = await ctx.adapter.findOne<Subscription>({
|
|
1643
|
+
model: "subscription",
|
|
1644
|
+
where: [
|
|
1645
|
+
{
|
|
1646
|
+
field: "stripeSubscriptionId",
|
|
1647
|
+
value: "sub_org_created_callback_123",
|
|
1648
|
+
},
|
|
1649
|
+
],
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
expect(subscription).toBeDefined();
|
|
1653
|
+
expect(subscription?.referenceId).toBe(orgId);
|
|
1654
|
+
|
|
1655
|
+
// Verify callback was called
|
|
1656
|
+
expect(onSubscriptionCreated).toHaveBeenCalledWith(
|
|
1657
|
+
expect.objectContaining({
|
|
1658
|
+
subscription: expect.objectContaining({
|
|
1659
|
+
referenceId: orgId,
|
|
1660
|
+
plan: "starter",
|
|
1661
|
+
}),
|
|
1662
|
+
stripeSubscription: expect.objectContaining({
|
|
1663
|
+
id: "sub_org_created_callback_123",
|
|
1664
|
+
}),
|
|
1665
|
+
plan: expect.objectContaining({
|
|
1666
|
+
name: "starter",
|
|
1667
|
+
}),
|
|
1668
|
+
}),
|
|
1669
|
+
);
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
describe("stripe - organizationHooks integration", () => {
|
|
1674
|
+
const mockStripeHooks = {
|
|
1675
|
+
prices: {
|
|
1676
|
+
list: vi.fn().mockResolvedValue({ data: [{ id: "price_lookup_123" }] }),
|
|
1677
|
+
},
|
|
1678
|
+
customers: {
|
|
1679
|
+
create: vi.fn().mockResolvedValue({ id: "cus_org_hooks_123" }),
|
|
1680
|
+
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
1681
|
+
retrieve: vi.fn().mockResolvedValue({
|
|
1682
|
+
id: "cus_org_hooks_123",
|
|
1683
|
+
name: "Old Org Name",
|
|
1684
|
+
deleted: false,
|
|
1685
|
+
}),
|
|
1686
|
+
update: vi.fn().mockResolvedValue({
|
|
1687
|
+
id: "cus_org_hooks_123",
|
|
1688
|
+
name: "Updated Org Name",
|
|
1689
|
+
}),
|
|
1690
|
+
del: vi
|
|
1691
|
+
.fn()
|
|
1692
|
+
.mockResolvedValue({ id: "cus_org_hooks_123", deleted: true }),
|
|
1693
|
+
},
|
|
1694
|
+
checkout: {
|
|
1695
|
+
sessions: {
|
|
1696
|
+
create: vi.fn().mockResolvedValue({
|
|
1697
|
+
url: "https://checkout.stripe.com/mock",
|
|
1698
|
+
id: "cs_mock123",
|
|
1699
|
+
}),
|
|
1700
|
+
},
|
|
1701
|
+
},
|
|
1702
|
+
billingPortal: {
|
|
1703
|
+
sessions: {
|
|
1704
|
+
create: vi
|
|
1705
|
+
.fn()
|
|
1706
|
+
.mockResolvedValue({ url: "https://billing.stripe.com/mock" }),
|
|
1707
|
+
},
|
|
1708
|
+
},
|
|
1709
|
+
subscriptions: {
|
|
1710
|
+
retrieve: vi.fn(),
|
|
1711
|
+
list: vi.fn().mockResolvedValue({ data: [] }),
|
|
1712
|
+
update: vi.fn(),
|
|
1713
|
+
},
|
|
1714
|
+
webhooks: {
|
|
1715
|
+
constructEventAsync: vi.fn(),
|
|
1716
|
+
},
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
const baseHooksStripeOptions: StripeOptions = {
|
|
1720
|
+
stripeClient: mockStripeHooks as unknown as Stripe,
|
|
1721
|
+
stripeWebhookSecret: "test_secret",
|
|
1722
|
+
createCustomerOnSignUp: false,
|
|
1723
|
+
organization: {
|
|
1724
|
+
enabled: true,
|
|
1725
|
+
},
|
|
1726
|
+
subscription: {
|
|
1727
|
+
enabled: true,
|
|
1728
|
+
plans: [
|
|
1729
|
+
{
|
|
1730
|
+
priceId: "price_starter_123",
|
|
1731
|
+
name: "starter",
|
|
1732
|
+
},
|
|
1733
|
+
],
|
|
1734
|
+
authorizeReference: async () => true,
|
|
1735
|
+
},
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
beforeEach(() => {
|
|
1739
|
+
vi.resetAllMocks();
|
|
1740
|
+
mockStripeHooks.subscriptions.list.mockResolvedValue({ data: [] });
|
|
1741
|
+
mockStripeHooks.customers.list.mockResolvedValue({ data: [] });
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
it("should sync organization name to Stripe customer on update", async () => {
|
|
1745
|
+
const { client, sessionSetter, auth } = await getTestInstance(
|
|
1746
|
+
{
|
|
1747
|
+
plugins: [organization(), stripe(baseHooksStripeOptions)],
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
disableTestUser: true,
|
|
1751
|
+
clientOptions: {
|
|
1752
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1753
|
+
},
|
|
1754
|
+
},
|
|
1755
|
+
);
|
|
1756
|
+
|
|
1757
|
+
// Sign up and sign in
|
|
1758
|
+
await client.signUp.email({
|
|
1759
|
+
email: "org-hook-test@example.com",
|
|
1760
|
+
password: "password123",
|
|
1761
|
+
name: "Org Hook Test User",
|
|
1762
|
+
});
|
|
1763
|
+
const headers = new Headers();
|
|
1764
|
+
await client.signIn.email(
|
|
1765
|
+
{
|
|
1766
|
+
email: "org-hook-test@example.com",
|
|
1767
|
+
password: "password123",
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
onSuccess: sessionSetter(headers),
|
|
1771
|
+
},
|
|
1772
|
+
);
|
|
1773
|
+
|
|
1774
|
+
// Create organization via client
|
|
1775
|
+
const org = await client.organization.create({
|
|
1776
|
+
name: "Old Org Name",
|
|
1777
|
+
slug: "sync-test-org",
|
|
1778
|
+
fetchOptions: { headers },
|
|
1779
|
+
});
|
|
1780
|
+
const orgId = org.data?.id as string;
|
|
1781
|
+
expect(orgId).toBeDefined();
|
|
1782
|
+
|
|
1783
|
+
// Set stripeCustomerId on organization
|
|
1784
|
+
const ctx = await auth.$context;
|
|
1785
|
+
await ctx.adapter.update({
|
|
1786
|
+
model: "organization",
|
|
1787
|
+
update: { stripeCustomerId: "cus_sync_test_123" },
|
|
1788
|
+
where: [{ field: "id", value: orgId }],
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// Mock Stripe customer retrieve to return old name
|
|
1792
|
+
mockStripeHooks.customers.retrieve.mockResolvedValueOnce({
|
|
1793
|
+
id: "cus_sync_test_123",
|
|
1794
|
+
name: "Old Org Name",
|
|
1795
|
+
deleted: false,
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
// Update organization name
|
|
1799
|
+
const updateResult = await client.organization.update({
|
|
1800
|
+
organizationId: orgId,
|
|
1801
|
+
data: { name: "New Org Name" },
|
|
1802
|
+
fetchOptions: { headers },
|
|
1803
|
+
});
|
|
1804
|
+
expect(updateResult.error).toBeNull();
|
|
1805
|
+
|
|
1806
|
+
// Verify Stripe customer was updated with new name
|
|
1807
|
+
expect(mockStripeHooks.customers.update).toHaveBeenCalledWith(
|
|
1808
|
+
"cus_sync_test_123",
|
|
1809
|
+
expect.objectContaining({ name: "New Org Name" }),
|
|
1810
|
+
);
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
it("should block organization deletion when active subscription exists", async () => {
|
|
1814
|
+
const { client, sessionSetter, auth } = await getTestInstance(
|
|
1815
|
+
{
|
|
1816
|
+
plugins: [organization(), stripe(baseHooksStripeOptions)],
|
|
1817
|
+
},
|
|
1818
|
+
{
|
|
1819
|
+
disableTestUser: true,
|
|
1820
|
+
clientOptions: {
|
|
1821
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1822
|
+
},
|
|
1823
|
+
},
|
|
1824
|
+
);
|
|
1825
|
+
|
|
1826
|
+
// Sign up and sign in
|
|
1827
|
+
await client.signUp.email({
|
|
1828
|
+
email: "org-block-test@example.com",
|
|
1829
|
+
password: "password123",
|
|
1830
|
+
name: "Block Test User",
|
|
1831
|
+
});
|
|
1832
|
+
const headers = new Headers();
|
|
1833
|
+
await client.signIn.email(
|
|
1834
|
+
{
|
|
1835
|
+
email: "org-block-test@example.com",
|
|
1836
|
+
password: "password123",
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
onSuccess: sessionSetter(headers),
|
|
1840
|
+
},
|
|
1841
|
+
);
|
|
1842
|
+
|
|
1843
|
+
// Create organization via client
|
|
1844
|
+
const org = await client.organization.create({
|
|
1845
|
+
name: "Delete Block Test Org",
|
|
1846
|
+
slug: "delete-block-test-org",
|
|
1847
|
+
fetchOptions: { headers },
|
|
1848
|
+
});
|
|
1849
|
+
const orgId = org.data?.id as string;
|
|
1850
|
+
|
|
1851
|
+
// Update org with stripeCustomerId via adapter
|
|
1852
|
+
const ctx = await auth.$context;
|
|
1853
|
+
await ctx.adapter.update({
|
|
1854
|
+
model: "organization",
|
|
1855
|
+
update: { stripeCustomerId: "cus_delete_block_123" },
|
|
1856
|
+
where: [{ field: "id", value: orgId }],
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
// Create active subscription for the org in DB
|
|
1860
|
+
await ctx.adapter.create({
|
|
1861
|
+
model: "subscription",
|
|
1862
|
+
data: {
|
|
1863
|
+
referenceId: orgId,
|
|
1864
|
+
stripeCustomerId: "cus_delete_block_123",
|
|
1865
|
+
stripeSubscriptionId: "sub_active_123",
|
|
1866
|
+
status: "active",
|
|
1867
|
+
plan: "starter",
|
|
1868
|
+
},
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
// Mock Stripe API to return an active subscription
|
|
1872
|
+
mockStripeHooks.subscriptions.list.mockResolvedValueOnce({
|
|
1873
|
+
data: [
|
|
1874
|
+
{
|
|
1875
|
+
id: "sub_active_123",
|
|
1876
|
+
status: "active",
|
|
1877
|
+
customer: "cus_delete_block_123",
|
|
1878
|
+
},
|
|
1879
|
+
],
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
// Attempt to delete the organization
|
|
1883
|
+
const deleteResult = await client.organization.delete({
|
|
1884
|
+
organizationId: orgId,
|
|
1885
|
+
fetchOptions: { headers },
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
// Verify deletion was blocked with expected error
|
|
1889
|
+
expect(deleteResult.error).toBeDefined();
|
|
1890
|
+
expect(deleteResult.error?.code).toBe(
|
|
1891
|
+
"ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION",
|
|
1892
|
+
);
|
|
1893
|
+
|
|
1894
|
+
// Verify organization still exists
|
|
1895
|
+
const orgAfterDelete = await ctx.adapter.findOne({
|
|
1896
|
+
model: "organization",
|
|
1897
|
+
where: [{ field: "id", value: orgId }],
|
|
1898
|
+
});
|
|
1899
|
+
expect(orgAfterDelete).not.toBeNull();
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
it("should allow organization deletion when no active subscription", async () => {
|
|
1903
|
+
const { client, sessionSetter, auth } = await getTestInstance(
|
|
1904
|
+
{
|
|
1905
|
+
plugins: [organization(), stripe(baseHooksStripeOptions)],
|
|
1906
|
+
},
|
|
1907
|
+
{
|
|
1908
|
+
disableTestUser: true,
|
|
1909
|
+
clientOptions: {
|
|
1910
|
+
plugins: [organizationClient(), stripeClient({ subscription: true })],
|
|
1911
|
+
},
|
|
1912
|
+
},
|
|
1913
|
+
);
|
|
1914
|
+
|
|
1915
|
+
// Sign up and sign in
|
|
1916
|
+
await client.signUp.email({
|
|
1917
|
+
email: "org-allow-test@example.com",
|
|
1918
|
+
password: "password123",
|
|
1919
|
+
name: "Allow Test User",
|
|
1920
|
+
});
|
|
1921
|
+
const headers = new Headers();
|
|
1922
|
+
await client.signIn.email(
|
|
1923
|
+
{
|
|
1924
|
+
email: "org-allow-test@example.com",
|
|
1925
|
+
password: "password123",
|
|
1926
|
+
},
|
|
1927
|
+
{
|
|
1928
|
+
onSuccess: sessionSetter(headers),
|
|
1929
|
+
},
|
|
1930
|
+
);
|
|
1931
|
+
|
|
1932
|
+
// Create organization via client
|
|
1933
|
+
const org = await client.organization.create({
|
|
1934
|
+
name: "Delete Allow Test Org",
|
|
1935
|
+
slug: "delete-allow-test-org",
|
|
1936
|
+
fetchOptions: { headers },
|
|
1937
|
+
});
|
|
1938
|
+
const orgId = org.data?.id as string;
|
|
1939
|
+
|
|
1940
|
+
// Update org with stripeCustomerId via adapter
|
|
1941
|
+
const ctx = await auth.$context;
|
|
1942
|
+
await ctx.adapter.update({
|
|
1943
|
+
model: "organization",
|
|
1944
|
+
update: { stripeCustomerId: "cus_delete_allow_123" },
|
|
1945
|
+
where: [{ field: "id", value: orgId }],
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
// Create canceled subscription for the org
|
|
1949
|
+
await ctx.adapter.create({
|
|
1950
|
+
model: "subscription",
|
|
1951
|
+
data: {
|
|
1952
|
+
referenceId: orgId,
|
|
1953
|
+
stripeCustomerId: "cus_delete_allow_123",
|
|
1954
|
+
stripeSubscriptionId: "sub_canceled_123",
|
|
1955
|
+
status: "canceled",
|
|
1956
|
+
plan: "starter",
|
|
1957
|
+
},
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
// Verify the subscription is canceled in DB
|
|
1961
|
+
const subscription = await ctx.adapter.findOne<Subscription>({
|
|
1962
|
+
model: "subscription",
|
|
1963
|
+
where: [{ field: "referenceId", value: orgId }],
|
|
1964
|
+
});
|
|
1965
|
+
expect(subscription).toBeDefined();
|
|
1966
|
+
expect(subscription?.status).toBe("canceled");
|
|
1967
|
+
|
|
1968
|
+
// Mock Stripe API to return only canceled subscriptions
|
|
1969
|
+
mockStripeHooks.subscriptions.list.mockResolvedValueOnce({
|
|
1970
|
+
data: [
|
|
1971
|
+
{
|
|
1972
|
+
id: "sub_canceled_123",
|
|
1973
|
+
status: "canceled",
|
|
1974
|
+
customer: "cus_delete_allow_123",
|
|
1975
|
+
},
|
|
1976
|
+
],
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
// Actually delete the organization and verify it succeeds
|
|
1980
|
+
const deleteResult = await client.organization.delete({
|
|
1981
|
+
organizationId: orgId,
|
|
1982
|
+
fetchOptions: { headers },
|
|
1983
|
+
});
|
|
1984
|
+
expect(deleteResult.error).toBeNull();
|
|
1985
|
+
|
|
1986
|
+
// Verify organization is deleted
|
|
1987
|
+
const deletedOrg = await ctx.adapter.findOne({
|
|
1988
|
+
model: "organization",
|
|
1989
|
+
where: [{ field: "id", value: orgId }],
|
|
1990
|
+
});
|
|
1991
|
+
expect(deletedOrg).toBeNull();
|
|
1992
|
+
});
|
|
1993
|
+
});
|