@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.
@@ -1,13 +1,22 @@
1
1
  import { runWithEndpointContext } from "@better-auth/core/context";
2
2
  import type { Auth, User } from "better-auth";
3
3
  import { memoryAdapter } from "better-auth/adapters/memory";
4
+ import { organization } from "better-auth/plugins/organization";
4
5
  import { getTestInstance } from "better-auth/test";
5
6
  import type Stripe from "stripe";
6
- import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
7
- import type { StripePlugin } from ".";
8
- import { stripe } from ".";
9
- import { stripeClient } from "./client";
10
- import type { StripeOptions, Subscription } from "./types";
7
+ import {
8
+ assert,
9
+ beforeEach,
10
+ describe,
11
+ expect,
12
+ expectTypeOf,
13
+ it,
14
+ vi,
15
+ } from "vitest";
16
+ import type { StripePlugin } from "../src";
17
+ import { stripe } from "../src";
18
+ import { stripeClient } from "../src/client";
19
+ import type { StripeOptions, Subscription } from "../src/types";
11
20
 
12
21
  describe("stripe type", () => {
13
22
  it("should api endpoint exists", () => {
@@ -59,6 +68,7 @@ describe("stripe", () => {
59
68
  customers: {
60
69
  create: vi.fn().mockResolvedValue({ id: "cus_mock123" }),
61
70
  list: vi.fn().mockResolvedValue({ data: [] }),
71
+ search: vi.fn().mockResolvedValue({ data: [] }),
62
72
  retrieve: vi.fn().mockResolvedValue({
63
73
  id: "cus_mock123",
64
74
  email: "test@email.com",
@@ -329,6 +339,71 @@ describe("stripe", () => {
329
339
  expect(mockStripe.subscriptions.update).not.toHaveBeenCalled();
330
340
  });
331
341
 
342
+ it("should pass metadata to subscription when upgrading", async () => {
343
+ const { client, sessionSetter } = await getTestInstance(
344
+ {
345
+ database: memory,
346
+ plugins: [stripe(stripeOptions)],
347
+ },
348
+ {
349
+ disableTestUser: true,
350
+ clientOptions: {
351
+ plugins: [
352
+ stripeClient({
353
+ subscription: true,
354
+ }),
355
+ ],
356
+ },
357
+ },
358
+ );
359
+
360
+ await client.signUp.email(
361
+ {
362
+ ...testUser,
363
+ email: "metadata-test@email.com",
364
+ },
365
+ {
366
+ throw: true,
367
+ },
368
+ );
369
+
370
+ const headers = new Headers();
371
+ await client.signIn.email(
372
+ {
373
+ ...testUser,
374
+ email: "metadata-test@email.com",
375
+ },
376
+ {
377
+ throw: true,
378
+ onSuccess: sessionSetter(headers),
379
+ },
380
+ );
381
+
382
+ const customMetadata = {
383
+ customField: "customValue",
384
+ organizationId: "org_123",
385
+ projectId: "proj_456",
386
+ };
387
+
388
+ await client.subscription.upgrade({
389
+ plan: "starter",
390
+ metadata: customMetadata,
391
+ fetchOptions: {
392
+ headers,
393
+ },
394
+ });
395
+
396
+ expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith(
397
+ expect.objectContaining({
398
+ subscription_data: expect.objectContaining({
399
+ metadata: expect.objectContaining(customMetadata),
400
+ }),
401
+ metadata: expect.objectContaining(customMetadata),
402
+ }),
403
+ undefined,
404
+ );
405
+ });
406
+
332
407
  it("should list active subscriptions", async () => {
333
408
  const { client, auth, sessionSetter } = await getTestInstance(
334
409
  {
@@ -1153,6 +1228,137 @@ describe("stripe", () => {
1153
1228
  expect(onSubscriptionCreatedCallback).not.toHaveBeenCalled();
1154
1229
  });
1155
1230
 
1231
+ it("should skip creating subscription when metadata.subscriptionId exists", async () => {
1232
+ const stripeForTest = {
1233
+ ...stripeOptions.stripeClient,
1234
+ webhooks: {
1235
+ constructEventAsync: vi.fn(),
1236
+ },
1237
+ };
1238
+
1239
+ const testOptions = {
1240
+ ...stripeOptions,
1241
+ stripeClient: stripeForTest as unknown as Stripe,
1242
+ stripeWebhookSecret: "test_secret",
1243
+ };
1244
+
1245
+ const {
1246
+ auth: testAuth,
1247
+ client,
1248
+ sessionSetter,
1249
+ } = await getTestInstance(
1250
+ {
1251
+ database: memory,
1252
+ plugins: [stripe(testOptions)],
1253
+ },
1254
+ {
1255
+ disableTestUser: true,
1256
+ clientOptions: {
1257
+ plugins: [stripeClient({ subscription: true })],
1258
+ },
1259
+ },
1260
+ );
1261
+ const testCtx = await testAuth.$context;
1262
+
1263
+ // Create user and sign in
1264
+ const userRes = await client.signUp.email(
1265
+ {
1266
+ ...testUser,
1267
+ },
1268
+ { throw: true },
1269
+ );
1270
+ const userId = userRes.user.id;
1271
+
1272
+ const headers = new Headers();
1273
+ await client.signIn.email(
1274
+ {
1275
+ ...testUser,
1276
+ },
1277
+ {
1278
+ throw: true,
1279
+ onSuccess: sessionSetter(headers),
1280
+ },
1281
+ );
1282
+
1283
+ // User upgrades to paid plan - this creates an "incomplete" subscription
1284
+ await client.subscription.upgrade({
1285
+ plan: "starter",
1286
+ fetchOptions: { headers },
1287
+ });
1288
+
1289
+ // Verify the incomplete subscription was created
1290
+ const incompleteSubscription = await testCtx.adapter.findOne<Subscription>({
1291
+ model: "subscription",
1292
+ where: [{ field: "referenceId", value: userId }],
1293
+ });
1294
+ assert(
1295
+ incompleteSubscription,
1296
+ "Expected incomplete subscription to be created",
1297
+ );
1298
+ expect(incompleteSubscription.status).toBe("incomplete");
1299
+ expect(incompleteSubscription.stripeSubscriptionId).toBeUndefined();
1300
+
1301
+ // Get user with stripeCustomerId
1302
+ const user = await testCtx.adapter.findOne<any>({
1303
+ model: "user",
1304
+ where: [{ field: "id", value: userId }],
1305
+ });
1306
+ const stripeCustomerId = user?.stripeCustomerId;
1307
+ expect(stripeCustomerId).toBeDefined();
1308
+
1309
+ // Simulate `customer.subscription.created` webhook arriving
1310
+ const mockEvent = {
1311
+ type: "customer.subscription.created",
1312
+ data: {
1313
+ object: {
1314
+ id: "sub_new_from_checkout",
1315
+ customer: stripeCustomerId,
1316
+ status: "active",
1317
+ metadata: {
1318
+ subscriptionId: incompleteSubscription.id,
1319
+ },
1320
+ items: {
1321
+ data: [
1322
+ {
1323
+ price: { id: process.env.STRIPE_PRICE_ID_1 },
1324
+ quantity: 1,
1325
+ current_period_start: Math.floor(Date.now() / 1000),
1326
+ current_period_end:
1327
+ Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
1328
+ },
1329
+ ],
1330
+ },
1331
+ cancel_at_period_end: false,
1332
+ },
1333
+ },
1334
+ };
1335
+
1336
+ (stripeForTest.webhooks.constructEventAsync as any).mockResolvedValue(
1337
+ mockEvent,
1338
+ );
1339
+
1340
+ const mockRequest = new Request(
1341
+ "http://localhost:3000/api/auth/stripe/webhook",
1342
+ {
1343
+ method: "POST",
1344
+ headers: {
1345
+ "stripe-signature": "test_signature",
1346
+ },
1347
+ body: JSON.stringify(mockEvent),
1348
+ },
1349
+ );
1350
+
1351
+ const response = await testAuth.handler(mockRequest);
1352
+ expect(response.status).toBe(200);
1353
+
1354
+ // Verify that no duplicate subscription was created
1355
+ const allSubscriptions = await testCtx.adapter.findMany<Subscription>({
1356
+ model: "subscription",
1357
+ where: [{ field: "referenceId", value: userId }],
1358
+ });
1359
+ expect(allSubscriptions.length).toBe(1);
1360
+ });
1361
+
1156
1362
  it("should execute subscription event handlers", async () => {
1157
1363
  const { auth: testAuth } = await getTestInstance(
1158
1364
  {
@@ -2074,8 +2280,8 @@ describe("stripe", () => {
2074
2280
  },
2075
2281
  );
2076
2282
 
2077
- // Mock customers.list to find existing customer
2078
- mockStripe.customers.list.mockResolvedValueOnce({
2283
+ // Mock customers.search to find existing user customer
2284
+ mockStripe.customers.search.mockResolvedValueOnce({
2079
2285
  data: [{ id: "cus_test_123" }],
2080
2286
  });
2081
2287
 
@@ -2335,6 +2541,7 @@ describe("stripe", () => {
2335
2541
  email: testUser.email,
2336
2542
  name: testUser.name,
2337
2543
  metadata: {
2544
+ customerType: "user",
2338
2545
  userId: userRes.user.id,
2339
2546
  },
2340
2547
  });
@@ -2544,6 +2751,7 @@ describe("stripe", () => {
2544
2751
  name: "Merge User",
2545
2752
  phone: "+1234567890",
2546
2753
  metadata: {
2754
+ customerType: "user",
2547
2755
  userId: userRes.user.id,
2548
2756
  customField: "customValue",
2549
2757
  anotherField: "anotherValue",
@@ -2590,6 +2798,7 @@ describe("stripe", () => {
2590
2798
  email: "no-custom-params@email.com",
2591
2799
  name: "Default User",
2592
2800
  metadata: {
2801
+ customerType: "user",
2593
2802
  userId: userRes.user.id,
2594
2803
  },
2595
2804
  });
@@ -2636,7 +2845,7 @@ describe("stripe", () => {
2636
2845
  const response = await testAuth.handler(mockRequest);
2637
2846
  expect(response.status).toBe(400);
2638
2847
  const data = await response.json();
2639
- expect(data.message).toContain("Webhook Error");
2848
+ expect(data.message).toContain("Failed to construct Stripe event");
2640
2849
  });
2641
2850
 
2642
2851
  it("should reject webhook request without stripe-signature header", async () => {
@@ -2664,7 +2873,7 @@ describe("stripe", () => {
2664
2873
  const response = await testAuth.handler(mockRequest);
2665
2874
  expect(response.status).toBe(400);
2666
2875
  const data = await response.json();
2667
- expect(data.message).toContain("Stripe webhook secret not found");
2876
+ expect(data.message).toContain("Stripe signature not found");
2668
2877
  });
2669
2878
 
2670
2879
  it("should handle constructEventAsync returning null/undefined", async () => {
@@ -2705,7 +2914,7 @@ describe("stripe", () => {
2705
2914
  const response = await testAuth.handler(mockRequest);
2706
2915
  expect(response.status).toBe(400);
2707
2916
  const data = await response.json();
2708
- expect(data.message).toContain("Failed to construct event");
2917
+ expect(data.message).toContain("Failed to construct Stripe event");
2709
2918
  });
2710
2919
 
2711
2920
  it("should handle async errors in webhook event processing", async () => {
@@ -3153,7 +3362,7 @@ describe("stripe", () => {
3153
3362
  const existingEmail = "duplicate-email@example.com";
3154
3363
  const existingCustomerId = "cus_stripe_existing_456";
3155
3364
 
3156
- mockStripe.customers.list.mockResolvedValueOnce({
3365
+ mockStripe.customers.search.mockResolvedValueOnce({
3157
3366
  data: [
3158
3367
  {
3159
3368
  id: existingCustomerId,
@@ -3194,9 +3403,9 @@ describe("stripe", () => {
3194
3403
  { throw: true },
3195
3404
  );
3196
3405
 
3197
- // Should check for existing customer by email
3198
- expect(mockStripe.customers.list).toHaveBeenCalledWith({
3199
- email: existingEmail,
3406
+ // Should check for existing user customer by email (excluding organization customers)
3407
+ expect(mockStripe.customers.search).toHaveBeenCalledWith({
3408
+ query: `email:"${existingEmail}" AND -metadata["customerType"]:"organization"`,
3200
3409
  limit: 1,
3201
3410
  });
3202
3411
 
@@ -3216,7 +3425,7 @@ describe("stripe", () => {
3216
3425
  it("should CREATE customer only when user has no stripeCustomerId and none exists in Stripe", async () => {
3217
3426
  const newEmail = "brand-new@example.com";
3218
3427
 
3219
- mockStripe.customers.list.mockResolvedValueOnce({
3428
+ mockStripe.customers.search.mockResolvedValueOnce({
3220
3429
  data: [],
3221
3430
  });
3222
3431
 
@@ -3256,9 +3465,9 @@ describe("stripe", () => {
3256
3465
  { throw: true },
3257
3466
  );
3258
3467
 
3259
- // Should check for existing customer first
3260
- expect(mockStripe.customers.list).toHaveBeenCalledWith({
3261
- email: newEmail,
3468
+ // Should check for existing user customer first (excluding organization customers)
3469
+ expect(mockStripe.customers.search).toHaveBeenCalledWith({
3470
+ query: `email:"${newEmail}" AND -metadata["customerType"]:"organization"`,
3262
3471
  limit: 1,
3263
3472
  });
3264
3473
 
@@ -3269,6 +3478,7 @@ describe("stripe", () => {
3269
3478
  name: "Brand New User",
3270
3479
  metadata: {
3271
3480
  userId: userRes.user.id,
3481
+ customerType: "user",
3272
3482
  },
3273
3483
  });
3274
3484
 
@@ -3283,6 +3493,231 @@ describe("stripe", () => {
3283
3493
  });
3284
3494
  });
3285
3495
 
3496
+ describe("User/Organization customer collision prevention", () => {
3497
+ it("should NOT return organization customer when searching for user customer with same email", async () => {
3498
+ // Scenario: Organization has a Stripe customer with email "shared@example.com"
3499
+ // When a user signs up with the same email, the search should NOT find the org customer
3500
+ const sharedEmail = "shared@example.com";
3501
+ const orgCustomerId = "cus_org_123";
3502
+
3503
+ // Mock: Only organization customer exists with this email
3504
+ // The search query includes `-metadata['customerType']:'organization'`
3505
+ // so this should NOT be returned
3506
+ mockStripe.customers.search.mockResolvedValueOnce({
3507
+ data: [], // Organization customer is excluded by the search query
3508
+ });
3509
+
3510
+ mockStripe.customers.create.mockResolvedValueOnce({
3511
+ id: "cus_user_new_456",
3512
+ email: sharedEmail,
3513
+ });
3514
+
3515
+ const testOptions = {
3516
+ ...stripeOptions,
3517
+ createCustomerOnSignUp: true,
3518
+ } satisfies StripeOptions;
3519
+
3520
+ const { client: testAuthClient, auth: testAuth } = await getTestInstance(
3521
+ {
3522
+ database: memory,
3523
+ plugins: [stripe(testOptions)],
3524
+ },
3525
+ {
3526
+ disableTestUser: true,
3527
+ clientOptions: {
3528
+ plugins: [stripeClient({ subscription: true })],
3529
+ },
3530
+ },
3531
+ );
3532
+ const testCtx = await testAuth.$context;
3533
+
3534
+ vi.clearAllMocks();
3535
+
3536
+ // User signs up with email that organization already uses
3537
+ const userRes = await testAuthClient.signUp.email(
3538
+ {
3539
+ email: sharedEmail,
3540
+ password: "password",
3541
+ name: "User With Shared Email",
3542
+ },
3543
+ { throw: true },
3544
+ );
3545
+
3546
+ // Should search with query that EXCLUDES organization customers
3547
+ expect(mockStripe.customers.search).toHaveBeenCalledWith({
3548
+ query: `email:"${sharedEmail}" AND -metadata["customerType"]:"organization"`,
3549
+ limit: 1,
3550
+ });
3551
+
3552
+ // Should create NEW user customer (not use org customer)
3553
+ expect(mockStripe.customers.create).toHaveBeenCalledTimes(1);
3554
+ expect(mockStripe.customers.create).toHaveBeenCalledWith({
3555
+ email: sharedEmail,
3556
+ name: "User With Shared Email",
3557
+ metadata: {
3558
+ customerType: "user",
3559
+ userId: userRes.user.id,
3560
+ },
3561
+ });
3562
+
3563
+ // Verify user has their own customer ID (not the org's)
3564
+ const user = await testCtx.adapter.findOne<
3565
+ User & { stripeCustomerId?: string }
3566
+ >({
3567
+ model: "user",
3568
+ where: [{ field: "id", value: userRes.user.id }],
3569
+ });
3570
+ expect(user?.stripeCustomerId).toBe("cus_user_new_456");
3571
+ expect(user?.stripeCustomerId).not.toBe(orgCustomerId);
3572
+ });
3573
+
3574
+ it("should find existing user customer even when organization customer with same email exists", async () => {
3575
+ // Scenario: Both user and organization customers exist with same email
3576
+ // The search should only return the user customer
3577
+ const sharedEmail = "both-exist@example.com";
3578
+ const existingUserCustomerId = "cus_user_existing_789";
3579
+
3580
+ // Mock: Search returns ONLY user customer (org customer excluded by query)
3581
+ mockStripe.customers.search.mockResolvedValueOnce({
3582
+ data: [
3583
+ {
3584
+ id: existingUserCustomerId,
3585
+ email: sharedEmail,
3586
+ name: "Existing User Customer",
3587
+ metadata: {
3588
+ customerType: "user",
3589
+ userId: "some-old-user-id",
3590
+ },
3591
+ },
3592
+ ],
3593
+ });
3594
+
3595
+ const testOptions = {
3596
+ ...stripeOptions,
3597
+ createCustomerOnSignUp: true,
3598
+ } satisfies StripeOptions;
3599
+
3600
+ const { client: testAuthClient, auth: testAuth } = await getTestInstance(
3601
+ {
3602
+ database: memory,
3603
+ plugins: [stripe(testOptions)],
3604
+ },
3605
+ {
3606
+ disableTestUser: true,
3607
+ clientOptions: {
3608
+ plugins: [stripeClient({ subscription: true })],
3609
+ },
3610
+ },
3611
+ );
3612
+ const testCtx = await testAuth.$context;
3613
+
3614
+ vi.clearAllMocks();
3615
+
3616
+ // User signs up - should find their existing customer
3617
+ const userRes = await testAuthClient.signUp.email(
3618
+ {
3619
+ email: sharedEmail,
3620
+ password: "password",
3621
+ name: "User Reclaiming Account",
3622
+ },
3623
+ { throw: true },
3624
+ );
3625
+
3626
+ // Should search excluding organization customers
3627
+ expect(mockStripe.customers.search).toHaveBeenCalledWith({
3628
+ query: `email:"${sharedEmail}" AND -metadata["customerType"]:"organization"`,
3629
+ limit: 1,
3630
+ });
3631
+
3632
+ // Should NOT create new customer - use existing user customer
3633
+ expect(mockStripe.customers.create).not.toHaveBeenCalled();
3634
+
3635
+ // Verify user has the existing user customer ID
3636
+ const user = await testCtx.adapter.findOne<
3637
+ User & { stripeCustomerId?: string }
3638
+ >({
3639
+ model: "user",
3640
+ where: [{ field: "id", value: userRes.user.id }],
3641
+ });
3642
+ expect(user?.stripeCustomerId).toBe(existingUserCustomerId);
3643
+ });
3644
+
3645
+ it("should create organization customer with customerType metadata", async () => {
3646
+ // Test that organization customers are properly tagged
3647
+ const orgEmail = "org@example.com";
3648
+ const orgId = "org_test_123";
3649
+
3650
+ mockStripe.customers.search.mockResolvedValueOnce({
3651
+ data: [],
3652
+ });
3653
+
3654
+ mockStripe.customers.create.mockResolvedValueOnce({
3655
+ id: "cus_org_new_999",
3656
+ email: orgEmail,
3657
+ });
3658
+
3659
+ const { auth: testAuth } = await getTestInstance(
3660
+ {
3661
+ database: memory,
3662
+ plugins: [
3663
+ organization(),
3664
+ stripe({
3665
+ ...stripeOptions,
3666
+ organization: {
3667
+ enabled: true,
3668
+ createCustomerOnOrganizationCreate: true,
3669
+ },
3670
+ }),
3671
+ ],
3672
+ },
3673
+ { disableTestUser: true },
3674
+ );
3675
+ const testCtx = await testAuth.$context;
3676
+
3677
+ vi.clearAllMocks();
3678
+
3679
+ // Create organization
3680
+ await testCtx.adapter.create({
3681
+ model: "organization",
3682
+ data: {
3683
+ id: orgId,
3684
+ name: "Test Organization",
3685
+ slug: "test-org-collision",
3686
+ createdAt: new Date(),
3687
+ },
3688
+ });
3689
+
3690
+ // Manually trigger the organization customer creation flow
3691
+ // by calling the internal function (simulating what hooks do)
3692
+ const stripeClient = stripeOptions.stripeClient;
3693
+ const searchResult = await stripeClient.customers.search({
3694
+ query: `email:'${orgEmail}' AND metadata['customerType']:'organization'`,
3695
+ limit: 1,
3696
+ });
3697
+
3698
+ if (searchResult.data.length === 0) {
3699
+ await stripeClient.customers.create({
3700
+ email: orgEmail,
3701
+ name: "Test Organization",
3702
+ metadata: {
3703
+ customerType: "organization",
3704
+ organizationId: orgId,
3705
+ },
3706
+ });
3707
+ }
3708
+
3709
+ // Verify organization customer was created with correct metadata
3710
+ expect(mockStripe.customers.create).toHaveBeenCalledWith({
3711
+ email: orgEmail,
3712
+ name: "Test Organization",
3713
+ metadata: {
3714
+ customerType: "organization",
3715
+ organizationId: orgId,
3716
+ },
3717
+ });
3718
+ });
3719
+ });
3720
+
3286
3721
  describe("webhook: cancel_at_period_end cancellation", () => {
3287
3722
  it("should sync cancelAtPeriodEnd and canceledAt when user cancels via Billing Portal (at_period_end mode)", async () => {
3288
3723
  const { auth } = await getTestInstance(
@@ -3962,4 +4397,372 @@ describe("stripe", () => {
3962
4397
  expect(updatedSub!.cancelAt!.getTime()).toBe(cancelAt * 1000);
3963
4398
  });
3964
4399
  });
4400
+
4401
+ describe("referenceMiddleware", () => {
4402
+ describe("referenceMiddleware - user subscription", () => {
4403
+ it("should pass when no explicit referenceId is provided", async () => {
4404
+ const { client, sessionSetter } = await getTestInstance(
4405
+ {
4406
+ plugins: [stripe(stripeOptions)],
4407
+ },
4408
+ {
4409
+ disableTestUser: true,
4410
+ clientOptions: {
4411
+ plugins: [stripeClient({ subscription: true })],
4412
+ },
4413
+ },
4414
+ );
4415
+
4416
+ await client.signUp.email(testUser, { throw: true });
4417
+ const headers = new Headers();
4418
+ await client.signIn.email(testUser, {
4419
+ throw: true,
4420
+ onSuccess: sessionSetter(headers),
4421
+ });
4422
+
4423
+ const res = await client.subscription.upgrade({
4424
+ plan: "starter",
4425
+ fetchOptions: { headers },
4426
+ });
4427
+
4428
+ expect(res.error).toBeNull();
4429
+ expect(res.data?.url).toBeDefined();
4430
+ });
4431
+
4432
+ it("should pass when referenceId equals user id", async () => {
4433
+ const { client, sessionSetter } = await getTestInstance(
4434
+ {
4435
+ plugins: [stripe(stripeOptions)],
4436
+ },
4437
+ {
4438
+ disableTestUser: true,
4439
+ clientOptions: {
4440
+ plugins: [stripeClient({ subscription: true })],
4441
+ },
4442
+ },
4443
+ );
4444
+
4445
+ const signUpRes = await client.signUp.email(
4446
+ { ...testUser, email: "ref-test-2@example.com" },
4447
+ { throw: true },
4448
+ );
4449
+ const headers = new Headers();
4450
+ await client.signIn.email(
4451
+ { ...testUser, email: "ref-test-2@example.com" },
4452
+ {
4453
+ throw: true,
4454
+ onSuccess: sessionSetter(headers),
4455
+ },
4456
+ );
4457
+
4458
+ const res = await client.subscription.upgrade({
4459
+ plan: "starter",
4460
+ referenceId: signUpRes.user.id,
4461
+ fetchOptions: { headers },
4462
+ });
4463
+
4464
+ expect(res.error).toBeNull();
4465
+ expect(res.data?.url).toBeDefined();
4466
+ });
4467
+
4468
+ it("should reject when authorizeReference is not defined but other referenceId is provided", async () => {
4469
+ const { client, sessionSetter } = await getTestInstance(
4470
+ {
4471
+ plugins: [stripe(stripeOptions)],
4472
+ },
4473
+ {
4474
+ disableTestUser: true,
4475
+ clientOptions: {
4476
+ plugins: [stripeClient({ subscription: true })],
4477
+ },
4478
+ },
4479
+ );
4480
+
4481
+ await client.signUp.email(
4482
+ { ...testUser, email: "ref-test-3@example.com" },
4483
+ { throw: true },
4484
+ );
4485
+ const headers = new Headers();
4486
+ await client.signIn.email(
4487
+ { ...testUser, email: "ref-test-3@example.com" },
4488
+ {
4489
+ throw: true,
4490
+ onSuccess: sessionSetter(headers),
4491
+ },
4492
+ );
4493
+
4494
+ const res = await client.subscription.upgrade({
4495
+ plan: "starter",
4496
+ referenceId: "some-other-id",
4497
+ fetchOptions: { headers },
4498
+ });
4499
+
4500
+ expect(res.error?.code).toBe("REFERENCE_ID_NOT_ALLOWED");
4501
+ });
4502
+
4503
+ it("should reject when authorizeReference returns false", async () => {
4504
+ const stripeOptionsWithAuth: StripeOptions = {
4505
+ ...stripeOptions,
4506
+ subscription: {
4507
+ ...stripeOptions.subscription,
4508
+ authorizeReference: async () => false,
4509
+ },
4510
+ };
4511
+
4512
+ const { client, sessionSetter } = await getTestInstance(
4513
+ {
4514
+ plugins: [stripe(stripeOptionsWithAuth)],
4515
+ },
4516
+ {
4517
+ disableTestUser: true,
4518
+ clientOptions: {
4519
+ plugins: [stripeClient({ subscription: true })],
4520
+ },
4521
+ },
4522
+ );
4523
+
4524
+ await client.signUp.email(
4525
+ { ...testUser, email: "ref-test-4@example.com" },
4526
+ { throw: true },
4527
+ );
4528
+ const headers = new Headers();
4529
+ await client.signIn.email(
4530
+ { ...testUser, email: "ref-test-4@example.com" },
4531
+ {
4532
+ throw: true,
4533
+ onSuccess: sessionSetter(headers),
4534
+ },
4535
+ );
4536
+
4537
+ const res = await client.subscription.upgrade({
4538
+ plan: "starter",
4539
+ referenceId: "some-other-id",
4540
+ fetchOptions: { headers },
4541
+ });
4542
+
4543
+ expect(res.error?.code).toBe("UNAUTHORIZED");
4544
+ });
4545
+
4546
+ it("should pass when authorizeReference returns true", async () => {
4547
+ const stripeOptionsWithAuth: StripeOptions = {
4548
+ ...stripeOptions,
4549
+ subscription: {
4550
+ ...stripeOptions.subscription,
4551
+ authorizeReference: async () => true,
4552
+ },
4553
+ };
4554
+
4555
+ const { client, sessionSetter } = await getTestInstance(
4556
+ {
4557
+ plugins: [stripe(stripeOptionsWithAuth)],
4558
+ },
4559
+ {
4560
+ disableTestUser: true,
4561
+ clientOptions: {
4562
+ plugins: [stripeClient({ subscription: true })],
4563
+ },
4564
+ },
4565
+ );
4566
+
4567
+ await client.signUp.email(
4568
+ { ...testUser, email: "ref-test-5@example.com" },
4569
+ { throw: true },
4570
+ );
4571
+ const headers = new Headers();
4572
+ await client.signIn.email(
4573
+ { ...testUser, email: "ref-test-5@example.com" },
4574
+ {
4575
+ throw: true,
4576
+ onSuccess: sessionSetter(headers),
4577
+ },
4578
+ );
4579
+
4580
+ const res = await client.subscription.upgrade({
4581
+ plan: "starter",
4582
+ referenceId: "some-other-id",
4583
+ fetchOptions: { headers },
4584
+ });
4585
+
4586
+ expect(res.error).toBeNull();
4587
+ expect(res.data?.url).toBeDefined();
4588
+ });
4589
+ });
4590
+
4591
+ describe("referenceMiddleware - organization subscription", () => {
4592
+ it("should reject when authorizeReference is not defined", async () => {
4593
+ const { client, sessionSetter } = await getTestInstance(
4594
+ {
4595
+ plugins: [stripe(stripeOptions)],
4596
+ },
4597
+ {
4598
+ disableTestUser: true,
4599
+ clientOptions: {
4600
+ plugins: [stripeClient({ subscription: true })],
4601
+ },
4602
+ },
4603
+ );
4604
+
4605
+ await client.signUp.email(
4606
+ { ...testUser, email: "org-test-1@example.com" },
4607
+ { throw: true },
4608
+ );
4609
+ const headers = new Headers();
4610
+ await client.signIn.email(
4611
+ { ...testUser, email: "org-test-1@example.com" },
4612
+ {
4613
+ throw: true,
4614
+ onSuccess: sessionSetter(headers),
4615
+ },
4616
+ );
4617
+
4618
+ const res = await client.subscription.upgrade({
4619
+ plan: "starter",
4620
+ customerType: "organization",
4621
+ referenceId: "org_123",
4622
+ fetchOptions: { headers },
4623
+ });
4624
+
4625
+ expect(res.error?.code).toBe("ORGANIZATION_SUBSCRIPTION_NOT_ENABLED");
4626
+ });
4627
+
4628
+ it("should reject when no referenceId or activeOrganizationId", async () => {
4629
+ const stripeOptionsWithAuth: StripeOptions = {
4630
+ ...stripeOptions,
4631
+ subscription: {
4632
+ ...stripeOptions.subscription,
4633
+ authorizeReference: async () => true,
4634
+ },
4635
+ };
4636
+
4637
+ const { client, sessionSetter } = await getTestInstance(
4638
+ {
4639
+ plugins: [stripe(stripeOptionsWithAuth)],
4640
+ },
4641
+ {
4642
+ disableTestUser: true,
4643
+ clientOptions: {
4644
+ plugins: [stripeClient({ subscription: true })],
4645
+ },
4646
+ },
4647
+ );
4648
+
4649
+ await client.signUp.email(
4650
+ { ...testUser, email: "org-test-2@example.com" },
4651
+ { throw: true },
4652
+ );
4653
+ const headers = new Headers();
4654
+ await client.signIn.email(
4655
+ { ...testUser, email: "org-test-2@example.com" },
4656
+ {
4657
+ throw: true,
4658
+ onSuccess: sessionSetter(headers),
4659
+ },
4660
+ );
4661
+
4662
+ const res = await client.subscription.upgrade({
4663
+ plan: "starter",
4664
+ customerType: "organization",
4665
+ fetchOptions: { headers },
4666
+ });
4667
+
4668
+ expect(res.error?.code).toBe("ORGANIZATION_REFERENCE_ID_REQUIRED");
4669
+ });
4670
+
4671
+ it("should reject when authorizeReference returns false", async () => {
4672
+ const stripeOptionsWithAuth: StripeOptions = {
4673
+ ...stripeOptions,
4674
+ subscription: {
4675
+ ...stripeOptions.subscription,
4676
+ authorizeReference: async () => false,
4677
+ },
4678
+ };
4679
+
4680
+ const { client, sessionSetter } = await getTestInstance(
4681
+ {
4682
+ plugins: [stripe(stripeOptionsWithAuth)],
4683
+ },
4684
+ {
4685
+ disableTestUser: true,
4686
+ clientOptions: {
4687
+ plugins: [stripeClient({ subscription: true })],
4688
+ },
4689
+ },
4690
+ );
4691
+
4692
+ await client.signUp.email(
4693
+ { ...testUser, email: "org-test-3@example.com" },
4694
+ { throw: true },
4695
+ );
4696
+ const headers = new Headers();
4697
+ await client.signIn.email(
4698
+ { ...testUser, email: "org-test-3@example.com" },
4699
+ {
4700
+ throw: true,
4701
+ onSuccess: sessionSetter(headers),
4702
+ },
4703
+ );
4704
+
4705
+ const res = await client.subscription.upgrade({
4706
+ plan: "starter",
4707
+ customerType: "organization",
4708
+ referenceId: "org_123",
4709
+ fetchOptions: { headers },
4710
+ });
4711
+
4712
+ expect(res.error?.code).toBe("UNAUTHORIZED");
4713
+ });
4714
+
4715
+ it("should pass when authorizeReference returns true", async () => {
4716
+ const stripeOptionsWithAuth: StripeOptions = {
4717
+ ...stripeOptions,
4718
+ organization: {
4719
+ enabled: true,
4720
+ },
4721
+ subscription: {
4722
+ ...stripeOptions.subscription,
4723
+ authorizeReference: async () => true,
4724
+ },
4725
+ };
4726
+
4727
+ const { client, sessionSetter } = await getTestInstance(
4728
+ {
4729
+ plugins: [stripe(stripeOptionsWithAuth)],
4730
+ },
4731
+ {
4732
+ disableTestUser: true,
4733
+ clientOptions: {
4734
+ plugins: [stripeClient({ subscription: true })],
4735
+ },
4736
+ },
4737
+ );
4738
+
4739
+ await client.signUp.email(
4740
+ { ...testUser, email: "org-test-4@example.com" },
4741
+ { throw: true },
4742
+ );
4743
+ const headers = new Headers();
4744
+ await client.signIn.email(
4745
+ { ...testUser, email: "org-test-4@example.com" },
4746
+ {
4747
+ throw: true,
4748
+ onSuccess: sessionSetter(headers),
4749
+ },
4750
+ );
4751
+
4752
+ const res = await client.subscription.upgrade({
4753
+ plan: "starter",
4754
+ customerType: "organization",
4755
+ referenceId: "org_123",
4756
+ fetchOptions: { headers },
4757
+ });
4758
+
4759
+ // Should pass middleware but may fail later due to org not existing
4760
+ // We're testing middleware authorization, not the full flow
4761
+ expect(res.error?.code).not.toBe(
4762
+ "ORGANIZATION_SUBSCRIPTION_NOT_ENABLED",
4763
+ );
4764
+ expect(res.error?.code).not.toBe("UNAUTHORIZED");
4765
+ });
4766
+ });
4767
+ });
3965
4768
  });