mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 12:27:44 +00:00
fix(stripe): subscription is created without completing payment (#4548)
This commit is contained in:
@@ -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",
|
(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 (
|
if (
|
||||||
existingSubscription &&
|
activeOrTrialingSubscription &&
|
||||||
existingSubscription.status === "active" &&
|
activeOrTrialingSubscription.status === "active" &&
|
||||||
existingSubscription.plan === ctx.body.plan &&
|
activeOrTrialingSubscription.plan === ctx.body.plan &&
|
||||||
existingSubscription.seats === (ctx.body.seats || 1)
|
activeOrTrialingSubscription.seats === (ctx.body.seats || 1)
|
||||||
) {
|
) {
|
||||||
throw new APIError("BAD_REQUEST", {
|
throw new APIError("BAD_REQUEST", {
|
||||||
message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN,
|
message: STRIPE_ERROR_CODES.ALREADY_SUBSCRIBED_PLAN,
|
||||||
@@ -408,9 +413,32 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription =
|
let subscription: Subscription | undefined =
|
||||||
existingSubscription ||
|
activeOrTrialingSubscription || incompleteSubscription;
|
||||||
(await ctx.context.adapter.create<InputSubscription, Subscription>({
|
|
||||||
|
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",
|
model: "subscription",
|
||||||
data: {
|
data: {
|
||||||
plan: plan.name.toLowerCase(),
|
plan: plan.name.toLowerCase(),
|
||||||
@@ -419,7 +447,8 @@ export const stripe = <O extends StripeOptions>(options: O) => {
|
|||||||
referenceId,
|
referenceId,
|
||||||
seats: ctx.body.seats || 1,
|
seats: ctx.body.seats || 1,
|
||||||
},
|
},
|
||||||
}));
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
ctx.context.logger.error("Subscription ID not found");
|
ctx.context.logger.error("Subscription ID not found");
|
||||||
|
|||||||
@@ -1102,4 +1102,69 @@ describe("stripe", async () => {
|
|||||||
});
|
});
|
||||||
expect(personalAfter?.status).toBe("active");
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user