diff --git a/packages/stripe/src/index.ts b/packages/stripe/src/index.ts index 047f2a4f..87931e72 100644 --- a/packages/stripe/src/index.ts +++ b/packages/stripe/src/index.ts @@ -354,15 +354,20 @@ export const stripe = (options: O) => { ], }); - const existingSubscription = subscriptions.find( + const activeOrTrialingSubscription = subscriptions.find( (sub) => sub.status === "active" || sub.status === "trialing", ); + // Also find any incomplete subscription that we can reuse + const incompleteSubscription = subscriptions.find( + (sub) => sub.status === "incomplete", + ); + if ( - existingSubscription && - existingSubscription.status === "active" && - existingSubscription.plan === ctx.body.plan && - existingSubscription.seats === (ctx.body.seats || 1) + activeOrTrialingSubscription && + activeOrTrialingSubscription.status === "active" && + activeOrTrialingSubscription.plan === ctx.body.plan && + activeOrTrialingSubscription.seats === (ctx.body.seats || 1) ) { throw new APIError("BAD_REQUEST", { message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN, @@ -408,9 +413,32 @@ export const stripe = (options: O) => { }); } - const subscription = - existingSubscription || - (await ctx.context.adapter.create({ + let subscription: Subscription | undefined = + activeOrTrialingSubscription || incompleteSubscription; + + if (incompleteSubscription && !activeOrTrialingSubscription) { + const updated = await ctx.context.adapter.update({ + model: "subscription", + update: { + plan: plan.name.toLowerCase(), + seats: ctx.body.seats || 1, + updatedAt: new Date(), + }, + where: [ + { + field: "id", + value: incompleteSubscription.id, + }, + ], + }); + subscription = (updated as Subscription) || incompleteSubscription; + } + + if (!subscription) { + subscription = await ctx.context.adapter.create< + InputSubscription, + Subscription + >({ model: "subscription", data: { plan: plan.name.toLowerCase(), @@ -419,7 +447,8 @@ export const stripe = (options: O) => { referenceId, seats: ctx.body.seats || 1, }, - })); + }); + } if (!subscription) { ctx.context.logger.error("Subscription ID not found"); diff --git a/packages/stripe/src/stripe.test.ts b/packages/stripe/src/stripe.test.ts index e85c9643..66031c6d 100644 --- a/packages/stripe/src/stripe.test.ts +++ b/packages/stripe/src/stripe.test.ts @@ -1102,4 +1102,69 @@ describe("stripe", async () => { }); expect(personalAfter?.status).toBe("active"); }); + + it("should reuse incomplete subscription when upgrading again", async () => { + // Create a user + const userRes = await authClient.signUp.email( + { + email: "incomplete@example.com", + password: "password", + name: "Incomplete Test", + }, + { + throw: true, + }, + ); + + const headers = new Headers(); + await authClient.signIn.email( + { + email: "incomplete@example.com", + password: "password", + }, + { + throw: true, + onSuccess: setCookieToHeader(headers), + }, + ); + + // First upgrade attempt - creates incomplete subscription + const firstUpgrade = await authClient.subscription.upgrade({ + plan: "starter", + fetchOptions: { + headers, + }, + }); + expect(firstUpgrade.data?.url).toBe("https://checkout.stripe.com/mock"); + + // Check that an incomplete subscription was created + const subscriptions = await ctx.adapter.findMany({ + model: "subscription", + where: [{ field: "referenceId", value: userRes.user.id }], + }); + expect(subscriptions).toHaveLength(1); + expect(subscriptions[0].status).toBe("incomplete"); + const firstSubId = subscriptions[0].id; + + // Second upgrade attempt - should reuse the same subscription + const secondUpgrade = await authClient.subscription.upgrade({ + plan: "premium", + seats: 2, + fetchOptions: { + headers, + }, + }); + expect(secondUpgrade.data?.url).toBe("https://checkout.stripe.com/mock"); + + // Check that the same subscription was updated, not a new one created + const subscriptionsAfter = await ctx.adapter.findMany({ + model: "subscription", + where: [{ field: "referenceId", value: userRes.user.id }], + }); + expect(subscriptionsAfter).toHaveLength(1); + expect(subscriptionsAfter[0].id).toBe(firstSubId); + expect(subscriptionsAfter[0].status).toBe("incomplete"); + expect(subscriptionsAfter[0].plan).toBe("premium"); + expect(subscriptionsAfter[0].seats).toBe(2); + }); });