fix(stripe): update subscription webhook handling should use customer id as a fallback

This commit is contained in:
Bereket Engida
2025-03-02 13:36:51 +03:00
parent 25de4b5f4f
commit 1c16655aba
4 changed files with 28 additions and 20 deletions

View File

@@ -20,6 +20,7 @@ import { nextCookies } from "better-auth/next-js";
import { passkey } from "better-auth/plugins/passkey"; import { passkey } from "better-auth/plugins/passkey";
import { stripe } from "@better-auth/stripe"; import { stripe } from "@better-auth/stripe";
import { Stripe } from "stripe"; import { Stripe } from "stripe";
import Database from "better-sqlite3";
const from = process.env.BETTER_AUTH_EMAIL || "delivered@resend.dev"; const from = process.env.BETTER_AUTH_EMAIL || "delivered@resend.dev";
const to = process.env.TEST_EMAIL || ""; const to = process.env.TEST_EMAIL || "";
@@ -50,10 +51,7 @@ const STARTER_PRICE_ID = {
export const auth = betterAuth({ export const auth = betterAuth({
appName: "Better Auth Demo", appName: "Better Auth Demo",
database: { database: new Database("stripe-1.db"),
dialect,
type: process.env.USE_MYSQL ? "mysql" : "sqlite",
},
emailVerification: { emailVerification: {
async sendVerificationEmail({ user, url }) { async sendVerificationEmail({ user, url }) {
const res = await resend.emails.send({ const res = await resend.emails.send({

View File

@@ -93,19 +93,32 @@ export async function onSubscriptionUpdated(
const subscriptionUpdated = event.data.object as Stripe.Subscription; const subscriptionUpdated = event.data.object as Stripe.Subscription;
const priceId = subscriptionUpdated.items.data[0].price.id; const priceId = subscriptionUpdated.items.data[0].price.id;
const plan = await getPlanByPriceId(options, priceId); const plan = await getPlanByPriceId(options, priceId);
const stripeId = subscriptionUpdated.id;
const subscription = await ctx.context.adapter.findOne<Subscription>({ const referenceId = subscriptionUpdated.metadata?.referenceId;
const subscriptionId = subscriptionUpdated.id;
const customerId = subscriptionUpdated.customer.toString();
let subscription = await ctx.context.adapter.findOne<Subscription>({
model: "subscription", model: "subscription",
where: [ where: referenceId
{ ? [{ field: "referenceId", value: referenceId }]
field: "stripeSubscriptionId", : subscriptionId
value: stripeId, ? [{ field: "stripeSubscriptionId", value: subscriptionId }]
}, : [],
],
}); });
if (!subscription) { if (!subscription) {
const subs = await ctx.context.adapter.findMany<Subscription>({
model: "subscription",
where: [{ field: "stripeCustomerId", value: customerId }],
});
if (subs.length > 1) {
logger.warn(
`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no referenceId or subscriptionId is provided`,
);
return; return;
} }
subscription = subs[0];
}
const seats = subscriptionUpdated.items.data[0].quantity; const seats = subscriptionUpdated.items.data[0].quantity;
await ctx.context.adapter.update({ await ctx.context.adapter.update({
model: "subscription", model: "subscription",
@@ -125,8 +138,8 @@ export async function onSubscriptionUpdated(
}, },
where: [ where: [
{ {
field: "stripeSubscriptionId", field: "referenceId",
value: subscriptionUpdated.id, value: subscription.referenceId,
}, },
], ],
}); });
@@ -170,7 +183,7 @@ export async function onSubscriptionUpdated(
} }
} }
} catch (error: any) { } catch (error: any) {
logger.error(`Stripe webhook failed. Error: ${error.message}`); logger.error(`Stripe webhook failed. Error: ${error}`);
} }
} }
@@ -217,6 +230,6 @@ export async function onSubscriptionDeleted(
} }
} }
} catch (error: any) { } catch (error: any) {
logger.error(`Stripe webhook failed. Error: ${error.message}`); logger.error(`Stripe webhook failed. Error: ${error}`);
} }
} }

View File

@@ -366,7 +366,6 @@ export const stripe = <O extends StripeOptions>(options: O) => {
}, },
], ],
}); });
console.log({ subscription });
if ( if (
!subscription || !subscription ||
subscription.cancelAtPeriodEnd || subscription.cancelAtPeriodEnd ||
@@ -382,7 +381,6 @@ export const stripe = <O extends StripeOptions>(options: O) => {
const currentSubscription = stripeSubscription.data.find( const currentSubscription = stripeSubscription.data.find(
(sub) => sub.id === subscription.stripeSubscriptionId, (sub) => sub.id === subscription.stripeSubscriptionId,
); );
console.log({ currentSubscription });
if (currentSubscription?.cancel_at_period_end === true) { if (currentSubscription?.cancel_at_period_end === true) {
await ctx.context.adapter.update({ await ctx.context.adapter.update({
model: "subscription", model: "subscription",

View File

@@ -592,7 +592,6 @@ describe("stripe", async () => {
mockStripeForEvents.webhooks.constructEvent.mockReturnValue(updateEvent); mockStripeForEvents.webhooks.constructEvent.mockReturnValue(updateEvent);
await eventTestAuth.handler(updateRequest); await eventTestAuth.handler(updateRequest);
expect(onSubscriptionUpdate).toHaveBeenCalledWith( expect(onSubscriptionUpdate).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
event: expect.any(Object), event: expect.any(Object),