mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-11-19 04:46:43 +01:00
abstract stripe payment stuff
This commit is contained in:
@@ -1,30 +1,28 @@
|
||||
--- template/app/src/payment/stripe/paymentProcessor.ts
|
||||
+++ opensaas-sh/app/src/payment/stripe/paymentProcessor.ts
|
||||
@@ -13,8 +13,8 @@
|
||||
} from "./checkoutUtils";
|
||||
import { stripeClient } from "./stripeClient";
|
||||
@@ -8,8 +8,8 @@
|
||||
} from "../paymentProcessor";
|
||||
import type { PaymentPlanEffect } from "../plans";
|
||||
import {
|
||||
- fetchUserPaymentProcessorUserId,
|
||||
- updateUserPaymentProcessorUserId,
|
||||
+ fetchUserStripeId,
|
||||
+ updateUserStripeId
|
||||
} from "./user";
|
||||
import { stripeMiddlewareConfigFn, stripeWebhook } from "./webhook";
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
} from "../user";
|
||||
import {
|
||||
createStripeCheckoutSession,
|
||||
@@ -28,8 +28,8 @@
|
||||
}: CreateCheckoutSessionArgs) => {
|
||||
const customer = await ensureStripeCustomer(userEmail);
|
||||
|
||||
- await updateUserPaymentProcessorUserId(
|
||||
- { userId, paymentProcessorUserId: customer.id },
|
||||
+ await updateUserStripeId(
|
||||
{
|
||||
userId,
|
||||
- paymentProcessorUserId: customer.id,
|
||||
+ stripeId: customer.id,
|
||||
},
|
||||
+ { userId, stripeId: customer.id },
|
||||
prismaUserDelegate,
|
||||
);
|
||||
@@ -59,18 +59,18 @@
|
||||
|
||||
@@ -56,18 +56,18 @@
|
||||
prismaUserDelegate,
|
||||
userId,
|
||||
}: FetchCustomerPortalUrlArgs) => {
|
||||
|
||||
38
opensaas-sh/app_diff/src/payment/stripe/webhook.ts.diff
Normal file
38
opensaas-sh/app_diff/src/payment/stripe/webhook.ts.diff
Normal file
@@ -0,0 +1,38 @@
|
||||
--- template/app/src/payment/stripe/webhook.ts
|
||||
+++ opensaas-sh/app/src/payment/stripe/webhook.ts
|
||||
@@ -111,7 +111,7 @@
|
||||
case PaymentPlanId.Credits10:
|
||||
await updateUserCredits(
|
||||
{
|
||||
- paymentProcessorUserId: customerId,
|
||||
+ stripeId: customerId,
|
||||
datePaid: invoicePaidAtDate,
|
||||
numOfCreditsPurchased: paymentPlans[paymentPlanId].effect.amount,
|
||||
},
|
||||
@@ -122,7 +122,7 @@
|
||||
case PaymentPlanId.Hobby:
|
||||
await updateUserSubscription(
|
||||
{
|
||||
- paymentProcessorUserId: customerId,
|
||||
+ stripeId: customerId,
|
||||
datePaid: invoicePaidAtDate,
|
||||
paymentPlanId,
|
||||
subscriptionStatus: SubscriptionStatus.Active,
|
||||
@@ -169,7 +169,7 @@
|
||||
);
|
||||
|
||||
const user = await updateUserSubscription(
|
||||
- { paymentProcessorUserId: customerId, paymentPlanId, subscriptionStatus },
|
||||
+ { stripeId: customerId, paymentPlanId, subscriptionStatus },
|
||||
prismaUserDelegate,
|
||||
);
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
|
||||
await updateUserSubscription(
|
||||
{
|
||||
- paymentProcessorUserId: customerId,
|
||||
+ stripeId: customerId,
|
||||
subscriptionStatus: SubscriptionStatus.Deleted,
|
||||
},
|
||||
prismaUserDelegate,
|
||||
98
opensaas-sh/app_diff/src/payment/user.ts.diff
Normal file
98
opensaas-sh/app_diff/src/payment/user.ts.diff
Normal file
@@ -0,0 +1,98 @@
|
||||
--- template/app/src/payment/user.ts
|
||||
+++ opensaas-sh/app/src/payment/user.ts
|
||||
@@ -2,7 +2,7 @@
|
||||
import { PrismaClient } from "wasp/server";
|
||||
import { PaymentPlanId, SubscriptionStatus } from "./plans";
|
||||
|
||||
-export async function fetchUserPaymentProcessorUserId(
|
||||
+export async function fetchUserStripeId(
|
||||
userId: User["id"],
|
||||
prismaUserDelegate: PrismaClient["user"],
|
||||
): Promise<string | null> {
|
||||
@@ -11,20 +11,20 @@
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
- paymentProcessorUserId: true,
|
||||
+ stripeId: true,
|
||||
},
|
||||
});
|
||||
|
||||
- return user.paymentProcessorUserId;
|
||||
+ return user.stripeId;
|
||||
}
|
||||
|
||||
-interface UpdateUserPaymentProcessorUserIdArgs {
|
||||
+interface UpdateUserStripeIdArgs {
|
||||
userId: User["id"];
|
||||
- paymentProcessorUserId: NonNullable<User["paymentProcessorUserId"]>;
|
||||
+ stripeId: NonNullable<User["stripeId"]>;
|
||||
}
|
||||
|
||||
-export function updateUserPaymentProcessorUserId(
|
||||
- { userId, paymentProcessorUserId }: UpdateUserPaymentProcessorUserIdArgs,
|
||||
+export function updateUserStripeId(
|
||||
+ { userId, stripeId }: UpdateUserStripeIdArgs,
|
||||
prismaUserDelegate: PrismaClient["user"],
|
||||
): Promise<User> {
|
||||
return prismaUserDelegate.update({
|
||||
@@ -32,13 +32,13 @@
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
- paymentProcessorUserId,
|
||||
+ stripeId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface UpdateUserSubscriptionArgs {
|
||||
- paymentProcessorUserId: NonNullable<User["paymentProcessorUserId"]>;
|
||||
+ stripeId: NonNullable<User["stripeId"]>;
|
||||
subscriptionStatus: SubscriptionStatus;
|
||||
paymentPlanId?: PaymentPlanId;
|
||||
datePaid?: Date;
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
export function updateUserSubscription(
|
||||
{
|
||||
- paymentProcessorUserId,
|
||||
+ stripeId,
|
||||
paymentPlanId,
|
||||
subscriptionStatus,
|
||||
datePaid,
|
||||
@@ -55,7 +55,7 @@
|
||||
) {
|
||||
return userDelegate.update({
|
||||
where: {
|
||||
- paymentProcessorUserId,
|
||||
+ stripeId,
|
||||
},
|
||||
data: {
|
||||
subscriptionPlan: paymentPlanId,
|
||||
@@ -66,14 +66,14 @@
|
||||
}
|
||||
|
||||
interface UpdateUserCreditsArgs {
|
||||
- paymentProcessorUserId: NonNullable<User["paymentProcessorUserId"]>;
|
||||
+ stripeId: NonNullable<User["stripeId"]>;
|
||||
numOfCreditsPurchased: number;
|
||||
datePaid: Date;
|
||||
}
|
||||
|
||||
export function updateUserCredits(
|
||||
{
|
||||
- paymentProcessorUserId,
|
||||
+ stripeId,
|
||||
numOfCreditsPurchased,
|
||||
datePaid,
|
||||
}: UpdateUserCreditsArgs,
|
||||
@@ -81,7 +81,7 @@
|
||||
) {
|
||||
return userDelegate.update({
|
||||
where: {
|
||||
- paymentProcessorUserId,
|
||||
+ stripeId,
|
||||
},
|
||||
data: {
|
||||
credits: { increment: numOfCreditsPurchased },
|
||||
@@ -1,3 +1,4 @@
|
||||
import { User } from "wasp/entities";
|
||||
import { requireNodeEnvVar } from "../server/utils";
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
@@ -67,3 +68,15 @@ export function getSubscriptionPaymentPlanIds(): PaymentPlanId[] {
|
||||
(planId) => paymentPlans[planId].effect.kind === "subscription",
|
||||
);
|
||||
}
|
||||
|
||||
export function getPaymentPlanIdByPaymentProcessorPlanId(
|
||||
paymentProcessorUserId: NonNullable<User["paymentProcessorUserId"]>,
|
||||
): PaymentPlanId {
|
||||
for (const [planId, plan] of Object.entries(paymentPlans)) {
|
||||
if (plan.getPaymentProcessorPlanId() === paymentProcessorUserId) {
|
||||
return planId as PaymentPlanId;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown payment processor ID: ${paymentProcessorUserId}`);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@ import type {
|
||||
PaymentProcessor,
|
||||
} from "../paymentProcessor";
|
||||
import type { PaymentPlanEffect } from "../plans";
|
||||
import {
|
||||
fetchUserPaymentProcessorUserId,
|
||||
updateUserPaymentProcessorUserId,
|
||||
} from "../user";
|
||||
import {
|
||||
createStripeCheckoutSession,
|
||||
ensureStripeCustomer,
|
||||
} from "./checkoutUtils";
|
||||
import { stripeClient } from "./stripeClient";
|
||||
import {
|
||||
fetchUserPaymentProcessorUserId,
|
||||
updateUserPaymentProcessorUserId,
|
||||
} from "./user";
|
||||
import { stripeMiddlewareConfigFn, stripeWebhook } from "./webhook";
|
||||
|
||||
export const stripePaymentProcessor: PaymentProcessor = {
|
||||
@@ -29,20 +29,17 @@ export const stripePaymentProcessor: PaymentProcessor = {
|
||||
const customer = await ensureStripeCustomer(userEmail);
|
||||
|
||||
await updateUserPaymentProcessorUserId(
|
||||
{
|
||||
userId,
|
||||
paymentProcessorUserId: customer.id,
|
||||
},
|
||||
{ userId, paymentProcessorUserId: customer.id },
|
||||
prismaUserDelegate,
|
||||
);
|
||||
|
||||
const stripeSession = await createStripeCheckoutSession({
|
||||
const checkoutSession = await createStripeCheckoutSession({
|
||||
customerId: customer.id,
|
||||
priceId: paymentPlan.getPaymentProcessorPlanId(),
|
||||
mode: paymentPlanEffectToStripeCheckoutSessionMode(paymentPlan.effect),
|
||||
});
|
||||
|
||||
if (!stripeSession.url) {
|
||||
if (!checkoutSession.url) {
|
||||
throw new Error(
|
||||
"Stripe checkout session URL is missing. Checkout session might not be active.",
|
||||
);
|
||||
@@ -50,8 +47,8 @@ export const stripePaymentProcessor: PaymentProcessor = {
|
||||
|
||||
return {
|
||||
session: {
|
||||
url: stripeSession.url,
|
||||
id: stripeSession.id,
|
||||
url: checkoutSession.url,
|
||||
id: checkoutSession.id,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -7,12 +7,14 @@ import { emailSender } from "wasp/server/email";
|
||||
import { requireNodeEnvVar } from "../../server/utils";
|
||||
import { assertUnreachable } from "../../shared/utils";
|
||||
import { UnhandledWebhookEventError } from "../errors";
|
||||
import { PaymentPlanId, paymentPlans, SubscriptionStatus } from "../plans";
|
||||
import { stripeClient } from "./stripeClient";
|
||||
import {
|
||||
updateUserOneTimePaymentDetails,
|
||||
updateUserSubscriptionDetails,
|
||||
} from "./user";
|
||||
getPaymentPlanIdByPaymentProcessorPlanId,
|
||||
PaymentPlanId,
|
||||
paymentPlans,
|
||||
SubscriptionStatus,
|
||||
} from "../plans";
|
||||
import { updateUserCredits, updateUserSubscription } from "../user";
|
||||
import { stripeClient } from "./stripeClient";
|
||||
|
||||
/**
|
||||
* Stripe requires a raw request to construct events successfully.
|
||||
@@ -35,38 +37,30 @@ export const stripeWebhook: PaymentsWebhook = async (
|
||||
) => {
|
||||
const prismaUserDelegate = context.entities.User;
|
||||
try {
|
||||
const stripeEvent = constructStripeEvent(request);
|
||||
const event = constructStripeEvent(request);
|
||||
|
||||
// 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
|
||||
// to only send the events that you're handling above.
|
||||
// See: https://docs.opensaas.sh/guides/deploying/#setting-up-your-stripe-webhook
|
||||
switch (stripeEvent.type) {
|
||||
switch (event.type) {
|
||||
case "invoice.paid":
|
||||
await handleInvoicePaid(stripeEvent, prismaUserDelegate);
|
||||
await handleInvoicePaid(event, prismaUserDelegate);
|
||||
break;
|
||||
case "customer.subscription.updated":
|
||||
await handleCustomerSubscriptionUpdated(
|
||||
stripeEvent,
|
||||
prismaUserDelegate,
|
||||
);
|
||||
await handleCustomerSubscriptionUpdated(event, prismaUserDelegate);
|
||||
break;
|
||||
case "customer.subscription.deleted":
|
||||
await handleCustomerSubscriptionDeleted(
|
||||
stripeEvent,
|
||||
prismaUserDelegate,
|
||||
);
|
||||
await handleCustomerSubscriptionDeleted(event, prismaUserDelegate);
|
||||
break;
|
||||
default:
|
||||
throw new UnhandledWebhookEventError(stripeEvent.type);
|
||||
throw new UnhandledWebhookEventError(event.type);
|
||||
}
|
||||
return response.status(204).send();
|
||||
} catch (error) {
|
||||
if (error instanceof UnhandledWebhookEventError) {
|
||||
// In development, it is likely that we will receive events that we are not handling.
|
||||
// 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") {
|
||||
console.info("Unhandled Stripe webhook event in development: ", error);
|
||||
} else if (process.env.NODE_ENV === "production") {
|
||||
@@ -82,7 +76,7 @@ export const stripeWebhook: PaymentsWebhook = async (
|
||||
return response.status(400).json({ error: error.message });
|
||||
} else {
|
||||
return response
|
||||
.status(400)
|
||||
.status(500)
|
||||
.json({ error: "Error processing Stripe webhook event" });
|
||||
}
|
||||
}
|
||||
@@ -109,13 +103,15 @@ async function handleInvoicePaid(
|
||||
const invoice = event.data.object;
|
||||
const customerId = getCustomerId(invoice.customer);
|
||||
const invoicePaidAtDate = getInvoicePaidAtDate(invoice);
|
||||
const paymentPlanId = getPaymentPlanIdByPriceId(getInvoicePriceId(invoice));
|
||||
const paymentPlanId = getPaymentPlanIdByPaymentProcessorPlanId(
|
||||
getInvoicePriceId(invoice),
|
||||
);
|
||||
|
||||
switch (paymentPlanId) {
|
||||
case PaymentPlanId.Credits10:
|
||||
await updateUserOneTimePaymentDetails(
|
||||
await updateUserCredits(
|
||||
{
|
||||
customerId,
|
||||
paymentProcessorUserId: customerId,
|
||||
datePaid: invoicePaidAtDate,
|
||||
numOfCreditsPurchased: paymentPlans[paymentPlanId].effect.amount,
|
||||
},
|
||||
@@ -124,9 +120,9 @@ async function handleInvoicePaid(
|
||||
break;
|
||||
case PaymentPlanId.Pro:
|
||||
case PaymentPlanId.Hobby:
|
||||
await updateUserSubscriptionDetails(
|
||||
await updateUserSubscription(
|
||||
{
|
||||
customerId,
|
||||
paymentProcessorUserId: customerId,
|
||||
datePaid: invoicePaidAtDate,
|
||||
paymentPlanId,
|
||||
subscriptionStatus: SubscriptionStatus.Active,
|
||||
@@ -168,12 +164,12 @@ async function handleCustomerSubscriptionUpdated(
|
||||
}
|
||||
|
||||
const customerId = getCustomerId(subscription.customer);
|
||||
const paymentPlanId = getPaymentPlanIdByPriceId(
|
||||
const paymentPlanId = getPaymentPlanIdByPaymentProcessorPlanId(
|
||||
getSubscriptionPriceId(subscription),
|
||||
);
|
||||
|
||||
const user = await updateUserSubscriptionDetails(
|
||||
{ customerId, paymentPlanId, subscriptionStatus },
|
||||
const user = await updateUserSubscription(
|
||||
{ paymentProcessorUserId: customerId, paymentPlanId, subscriptionStatus },
|
||||
prismaUserDelegate,
|
||||
);
|
||||
|
||||
@@ -190,14 +186,31 @@ async function handleCustomerSubscriptionUpdated(
|
||||
function getOpenSaasSubscriptionStatus(
|
||||
subscription: Stripe.Subscription,
|
||||
): SubscriptionStatus | undefined {
|
||||
if (subscription.status === SubscriptionStatus.Active) {
|
||||
if (subscription.cancel_at_period_end) {
|
||||
return SubscriptionStatus.CancelAtPeriodEnd;
|
||||
}
|
||||
return SubscriptionStatus.Active;
|
||||
} else if (subscription.status === SubscriptionStatus.PastDue) {
|
||||
return SubscriptionStatus.PastDue;
|
||||
const stripeToOpenSaasSubscriptionStatusMap: Record<
|
||||
Stripe.Subscription.Status,
|
||||
SubscriptionStatus | undefined
|
||||
> = {
|
||||
trialing: SubscriptionStatus.Active,
|
||||
active: SubscriptionStatus.Active,
|
||||
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(
|
||||
@@ -222,8 +235,11 @@ async function handleCustomerSubscriptionDeleted(
|
||||
const subscription = event.data.object;
|
||||
const customerId = getCustomerId(subscription.customer);
|
||||
|
||||
await updateUserSubscriptionDetails(
|
||||
{ customerId, subscriptionStatus: SubscriptionStatus.Deleted },
|
||||
await updateUserSubscription(
|
||||
{
|
||||
paymentProcessorUserId: customerId,
|
||||
subscriptionStatus: SubscriptionStatus.Deleted,
|
||||
},
|
||||
prismaUserDelegate,
|
||||
);
|
||||
}
|
||||
@@ -249,15 +265,3 @@ function getInvoicePaidAtDate(invoice: Stripe.Invoice): Date {
|
||||
// so we multiply by 1000 to convert to milliseconds.
|
||||
return new Date(invoice.status_transitions.paid_at * 1000);
|
||||
}
|
||||
|
||||
function getPaymentPlanIdByPriceId(priceId: string): PaymentPlanId {
|
||||
const paymentPlanId = Object.values(PaymentPlanId).find(
|
||||
(paymentPlanId) =>
|
||||
paymentPlans[paymentPlanId].getPaymentProcessorPlanId() === priceId,
|
||||
);
|
||||
if (!paymentPlanId) {
|
||||
throw new Error(`No payment plan with Stripe price id ${priceId}`);
|
||||
}
|
||||
|
||||
return paymentPlanId;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import Stripe from "stripe";
|
||||
import { User } from "wasp/entities";
|
||||
import type { SubscriptionStatus } from "../plans";
|
||||
import { PaymentPlanId } from "../plans";
|
||||
import { PrismaClient } from "wasp/server";
|
||||
import { PaymentPlanId, SubscriptionStatus } from "./plans";
|
||||
|
||||
export async function fetchUserPaymentProcessorUserId(
|
||||
userId: User["id"],
|
||||
@@ -39,50 +37,25 @@ export function updateUserPaymentProcessorUserId(
|
||||
});
|
||||
}
|
||||
|
||||
interface UpdateUserOneTimePaymentDetailsArgs {
|
||||
customerId: Stripe.Customer["id"];
|
||||
datePaid: Date;
|
||||
numOfCreditsPurchased: number;
|
||||
}
|
||||
|
||||
export function updateUserOneTimePaymentDetails(
|
||||
{
|
||||
customerId,
|
||||
datePaid,
|
||||
numOfCreditsPurchased,
|
||||
}: UpdateUserOneTimePaymentDetailsArgs,
|
||||
userDelegate: PrismaClient["user"],
|
||||
): Promise<User> {
|
||||
return userDelegate.update({
|
||||
where: {
|
||||
paymentProcessorUserId: customerId,
|
||||
},
|
||||
data: {
|
||||
datePaid,
|
||||
credits: { increment: numOfCreditsPurchased },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface UpdateUserSubscriptionDetailsArgs {
|
||||
customerId: Stripe.Customer["id"];
|
||||
interface UpdateUserSubscriptionArgs {
|
||||
paymentProcessorUserId: NonNullable<User["paymentProcessorUserId"]>;
|
||||
subscriptionStatus: SubscriptionStatus;
|
||||
datePaid?: Date;
|
||||
paymentPlanId?: PaymentPlanId;
|
||||
datePaid?: Date;
|
||||
}
|
||||
|
||||
export function updateUserSubscriptionDetails(
|
||||
export function updateUserSubscription(
|
||||
{
|
||||
customerId,
|
||||
paymentProcessorUserId,
|
||||
paymentPlanId,
|
||||
subscriptionStatus,
|
||||
datePaid,
|
||||
}: UpdateUserSubscriptionDetailsArgs,
|
||||
}: UpdateUserSubscriptionArgs,
|
||||
userDelegate: PrismaClient["user"],
|
||||
): Promise<User> {
|
||||
) {
|
||||
return userDelegate.update({
|
||||
where: {
|
||||
paymentProcessorUserId: customerId,
|
||||
paymentProcessorUserId,
|
||||
},
|
||||
data: {
|
||||
subscriptionPlan: paymentPlanId,
|
||||
@@ -91,3 +64,28 @@ export function updateUserSubscriptionDetails(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
interface UpdateUserCreditsArgs {
|
||||
paymentProcessorUserId: NonNullable<User["paymentProcessorUserId"]>;
|
||||
numOfCreditsPurchased: number;
|
||||
datePaid: Date;
|
||||
}
|
||||
|
||||
export function updateUserCredits(
|
||||
{
|
||||
paymentProcessorUserId,
|
||||
numOfCreditsPurchased,
|
||||
datePaid,
|
||||
}: UpdateUserCreditsArgs,
|
||||
userDelegate: PrismaClient["user"],
|
||||
) {
|
||||
return userDelegate.update({
|
||||
where: {
|
||||
paymentProcessorUserId,
|
||||
},
|
||||
data: {
|
||||
credits: { increment: numOfCreditsPurchased },
|
||||
datePaid,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user