mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 20: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",
|
||||
);
|
||||
|
||||
// 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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user