fix(stripe): allow re-subscribing to the same plan when subscription has expired (#7459)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Taesu <bytaesu@gmail.com>
Co-authored-by: Taesu <166604494+bytaesu@users.noreply.github.com>
This commit is contained in:
DIYgod
2026-01-21 07:55:22 +08:00
committed by Alex Yang
parent 382486fd8b
commit 7403c258ba
2 changed files with 70 additions and 1 deletions

View File

@@ -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,

View File

@@ -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(
{