fix(stripe): subscription is created without completing payment (#4548)

This commit is contained in:
Alex Yang
2025-09-09 17:06:05 -07:00
committed by GitHub
parent f6f1902b59
commit e663f98141
2 changed files with 103 additions and 9 deletions

View File

@@ -354,15 +354,20 @@ export const stripe = <O extends StripeOptions>(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 = <O extends StripeOptions>(options: O) => {
});
}
const subscription =
existingSubscription ||
(await ctx.context.adapter.create<InputSubscription, Subscription>({
let subscription: Subscription | undefined =
activeOrTrialingSubscription || incompleteSubscription;
if (incompleteSubscription && !activeOrTrialingSubscription) {
const updated = await ctx.context.adapter.update<InputSubscription>({
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 = <O extends StripeOptions>(options: O) => {
referenceId,
seats: ctx.body.seats || 1,
},
}));
});
}
if (!subscription) {
ctx.context.logger.error("Subscription ID not found");

View File

@@ -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<Subscription>({
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<Subscription>({
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);
});
});