This commit is contained in:
Franjo Mindek
2025-11-14 17:06:33 +01:00
parent adb8bb62e6
commit 467139d79c
3 changed files with 38 additions and 29 deletions

View File

@@ -10,16 +10,16 @@ import { stripeClient } from "./stripeClient";
export async function ensureStripeCustomer( export async function ensureStripeCustomer(
userEmail: NonNullable<User["email"]>, userEmail: NonNullable<User["email"]>,
): Promise<Stripe.Customer> { ): Promise<Stripe.Customer> {
const stripeCustomers = await stripeClient.customers.list({ const customers = await stripeClient.customers.list({
email: userEmail, email: userEmail,
}); });
if (stripeCustomers.data.length === 0) { if (customers.data.length === 0) {
return stripeClient.customers.create({ return stripeClient.customers.create({
email: userEmail, email: userEmail,
}); });
} else { } else {
return stripeCustomers.data[0]; return customers.data[0];
} }
} }

View File

@@ -36,13 +36,13 @@ export const stripePaymentProcessor: PaymentProcessor = {
prismaUserDelegate, prismaUserDelegate,
); );
const stripeSession = await createStripeCheckoutSession({ const checkoutSession = await createStripeCheckoutSession({
customerId: customer.id, customerId: customer.id,
priceId: paymentPlan.getPaymentProcessorPlanId(), priceId: paymentPlan.getPaymentProcessorPlanId(),
mode: paymentPlanEffectToStripeCheckoutSessionMode(paymentPlan.effect), mode: paymentPlanEffectToStripeCheckoutSessionMode(paymentPlan.effect),
}); });
if (!stripeSession.url) { if (!checkoutSession.url) {
throw new Error( throw new Error(
"Stripe checkout session URL is missing. Checkout session might not be active.", "Stripe checkout session URL is missing. Checkout session might not be active.",
); );
@@ -50,8 +50,8 @@ export const stripePaymentProcessor: PaymentProcessor = {
return { return {
session: { session: {
url: stripeSession.url, url: checkoutSession.url,
id: stripeSession.id, id: checkoutSession.id,
}, },
}; };
}, },

View File

@@ -35,38 +35,30 @@ export const stripeWebhook: PaymentsWebhook = async (
) => { ) => {
const prismaUserDelegate = context.entities.User; const prismaUserDelegate = context.entities.User;
try { try {
const stripeEvent = constructStripeEvent(request); const event = constructStripeEvent(request);
// If you'd like to handle more events, you can add more cases below. // If you'd like to handle more events, you can add more cases below.
// When deploying your app, you configure your webhook in the Stripe dashboard // When deploying your app, you configure your webhook in the Stripe dashboard
// to only send the events that you're handling above. // to only send the events that you're handling above.
// See: https://docs.opensaas.sh/guides/deploying/#setting-up-your-stripe-webhook // See: https://docs.opensaas.sh/guides/deploying/#setting-up-your-stripe-webhook
switch (stripeEvent.type) { switch (event.type) {
case "invoice.paid": case "invoice.paid":
await handleInvoicePaid(stripeEvent, prismaUserDelegate); await handleInvoicePaid(event, prismaUserDelegate);
break; break;
case "customer.subscription.updated": case "customer.subscription.updated":
await handleCustomerSubscriptionUpdated( await handleCustomerSubscriptionUpdated(event, prismaUserDelegate);
stripeEvent,
prismaUserDelegate,
);
break; break;
case "customer.subscription.deleted": case "customer.subscription.deleted":
await handleCustomerSubscriptionDeleted( await handleCustomerSubscriptionDeleted(event, prismaUserDelegate);
stripeEvent,
prismaUserDelegate,
);
break; break;
default: default:
throw new UnhandledWebhookEventError(stripeEvent.type); throw new UnhandledWebhookEventError(event.type);
} }
return response.status(204).send(); return response.status(204).send();
} catch (error) { } catch (error) {
if (error instanceof UnhandledWebhookEventError) { if (error instanceof UnhandledWebhookEventError) {
// In development, it is likely that we will receive events that we are not handling. // In development, it is likely that we will receive events that we are not handling.
// E.g. via the `stripe trigger` command. // E.g. via the `stripe trigger` command.
// While these can be ignored safely in development, it's good to be aware of them.
// For production we shouldn't have any extra webhook events.
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
console.info("Unhandled Stripe webhook event in development: ", error); console.info("Unhandled Stripe webhook event in development: ", error);
} else if (process.env.NODE_ENV === "production") { } else if (process.env.NODE_ENV === "production") {
@@ -82,7 +74,7 @@ export const stripeWebhook: PaymentsWebhook = async (
return response.status(400).json({ error: error.message }); return response.status(400).json({ error: error.message });
} else { } else {
return response return response
.status(400) .status(500)
.json({ error: "Error processing Stripe webhook event" }); .json({ error: "Error processing Stripe webhook event" });
} }
} }
@@ -190,14 +182,31 @@ async function handleCustomerSubscriptionUpdated(
function getOpenSaasSubscriptionStatus( function getOpenSaasSubscriptionStatus(
subscription: Stripe.Subscription, subscription: Stripe.Subscription,
): SubscriptionStatus | undefined { ): SubscriptionStatus | undefined {
if (subscription.status === SubscriptionStatus.Active) { const stripeToOpenSaasSubscriptionStatusMap: Record<
if (subscription.cancel_at_period_end) { Stripe.Subscription.Status,
return SubscriptionStatus.CancelAtPeriodEnd; SubscriptionStatus | undefined
} > = {
return SubscriptionStatus.Active; trialing: SubscriptionStatus.Active,
} else if (subscription.status === SubscriptionStatus.PastDue) { active: SubscriptionStatus.Active,
return SubscriptionStatus.PastDue; past_due: SubscriptionStatus.PastDue,
canceled: SubscriptionStatus.Deleted,
unpaid: SubscriptionStatus.Deleted,
incomplete_expired: SubscriptionStatus.Deleted,
paused: undefined,
incomplete: undefined,
};
const subscriptionStauts =
stripeToOpenSaasSubscriptionStatusMap[subscription.status];
if (
subscriptionStauts === SubscriptionStatus.Active &&
subscription.cancel_at_period_end
) {
return SubscriptionStatus.CancelAtPeriodEnd;
} }
return subscriptionStauts;
} }
function getSubscriptionPriceId( function getSubscriptionPriceId(