From 7403c258babdda63e67a5a181b2144fc364e25ea Mon Sep 17 00:00:00 2001 From: DIYgod Date: Wed, 21 Jan 2026 07:55:22 +0800 Subject: [PATCH] fix(stripe): allow re-subscribing to the same plan when subscription has expired (#7459) Co-authored-by: Claude Opus 4.5 Co-authored-by: Taesu Co-authored-by: Taesu <166604494+bytaesu@users.noreply.github.com> --- packages/stripe/src/routes.ts | 5 ++- packages/stripe/test/stripe.test.ts | 66 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/stripe/src/routes.ts b/packages/stripe/src/routes.ts index ac7d018ea5..3d92ccd51c 100644 --- a/packages/stripe/src/routes.ts +++ b/packages/stripe/src/routes.ts @@ -502,7 +502,10 @@ export const upgradeSubscription = (options: StripeOptions) => { activeOrTrialingSubscription && activeOrTrialingSubscription.status === "active" && activeOrTrialingSubscription.plan === ctx.body.plan && - activeOrTrialingSubscription.seats === (ctx.body.seats || 1) + activeOrTrialingSubscription.seats === (ctx.body.seats || 1) && + // Skip if periodEnd has passed, in case status is stale + (!activeOrTrialingSubscription.periodEnd || + activeOrTrialingSubscription.periodEnd > new Date()) ) { throw new APIError("BAD_REQUEST", { message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN, diff --git a/packages/stripe/test/stripe.test.ts b/packages/stripe/test/stripe.test.ts index 246e57b196..091be48bc9 100644 --- a/packages/stripe/test/stripe.test.ts +++ b/packages/stripe/test/stripe.test.ts @@ -1915,6 +1915,72 @@ describe("stripe", () => { expect(upgradeRes.error?.message).toContain("already subscribed"); }); + it.each([ + { + name: "past", + periodEnd: new Date(Date.now() - 24 * 60 * 60 * 1000), + shouldAllow: true, + }, + { + name: "future", + periodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), + shouldAllow: false, + }, + ])("should handle re-subscribing when periodEnd is in the $name", async ({ + periodEnd, + shouldAllow, + }) => { + const { client, auth, sessionSetter } = await getTestInstance( + { + database: memory, + plugins: [stripe(stripeOptions)], + }, + { + disableTestUser: true, + clientOptions: { + plugins: [stripeClient({ subscription: true })], + }, + }, + ); + const ctx = await auth.$context; + + const userRes = await client.signUp.email( + { ...testUser, email: `periodend-${periodEnd.getTime()}@email.com` }, + { throw: true }, + ); + + const headers = new Headers(); + await client.signIn.email( + { ...testUser, email: `periodend-${periodEnd.getTime()}@email.com` }, + { throw: true, onSuccess: sessionSetter(headers) }, + ); + + await client.subscription.upgrade({ + plan: "starter", + seats: 1, + fetchOptions: { headers }, + }); + + await ctx.adapter.update({ + model: "subscription", + update: { status: "active", seats: 1, periodEnd }, + where: [{ field: "referenceId", value: userRes.user.id }], + }); + + const upgradeRes = await client.subscription.upgrade({ + plan: "starter", + seats: 1, + fetchOptions: { headers }, + }); + + if (shouldAllow) { + expect(upgradeRes.error).toBeNull(); + expect(upgradeRes.data?.url).toBeDefined(); + } else { + expect(upgradeRes.error?.message).toContain("already subscribed"); + } + }); + it("should only call Stripe customers.create once for signup and upgrade", async () => { const { client, sessionSetter } = await getTestInstance( {