Files
2023-05-21 20:30:37 +08:00

120 lines
4.4 KiB
TypeScript

import { PrismaClient, Prisma, SubscriptionPlan } from "@prisma/client";
import { buffer } from "micro";
import Cors from "micro-cors";
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
import { getSubscriptionByEmail } from "../utils/subscription";
const stripe = new Stripe(process.env.STRIPE_API_KEY, {
// https://github.com/stripe/stripe-node#configuration
apiVersion: "2022-11-15",
});
const webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET!;
const prisma = new PrismaClient();
// Stripe requires the raw body to construct the event.
export const config = {
api: {
bodyParser: false,
},
};
const cors = Cors({
allowMethods: ["POST", "HEAD"],
});
const webhookHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST") {
const buf = await buffer(req);
const sig = req.headers["stripe-signature"]!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(buf.toString(), sig, webhookSecret);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Unknown error";
// On error, log and return the error message.
if (err! instanceof Error) console.log(err);
console.log(`❌ Error message: ${errorMessage}`);
res.status(400).send(`Webhook Error: ${errorMessage}`);
return;
}
// Cast event data to Stripe object.
if (event.type === "payment_intent.succeeded") {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
const charge = await stripe.charges.retrieve(paymentIntent.latest_charge as string);
const customerId = paymentIntent.customer as string;
if (customerId) {
// Save the stripe customer id so that we can relate this customer to future payments.
await prisma.user.update({
where: {
email: paymentIntent.metadata.email,
},
data: {
stripeId: customerId,
},
});
}
const user = await prisma.user.findUniqueOrThrow({
where: { email: paymentIntent.metadata.email },
});
const payment: Prisma.PaymentUncheckedCreateInput = {
userId: user.id,
email: paymentIntent.metadata.email,
createdAt: new Date(paymentIntent.created * 1000),
paymentId: paymentIntent.id,
customerId: customerId || "",
description: paymentIntent.metadata.description,
amount: paymentIntent.amount,
currency: paymentIntent.currency,
receipt: charge.receipt_url as string,
};
await prisma.payment.create({ data: payment });
const currentSubscription = await getSubscriptionByEmail(paymentIntent.metadata.email);
// Create a new subscription if there is no active paid subscription.
if (currentSubscription.plan === "FREE" || currentSubscription.canceledAt || currentSubscription.expireAt < new Date().getTime()) {
const today = new Date(new Date().setHours(0, 0, 0, 0));
// Subtract 1 second from the year from now to make it 23:59:59
const yearFromNow = new Date(new Date(new Date().setHours(0, 0, 0, 0)).setFullYear(today.getFullYear() + 1) - 1000);
const subscription: Prisma.SubscriptionUncheckedCreateInput = {
userId: user.id,
email: paymentIntent.metadata.email,
createdAt: new Date(paymentIntent.created * 1000),
startAt: today,
expireAt: yearFromNow,
plan: paymentIntent.metadata.plan as SubscriptionPlan,
};
await prisma.subscription.create({ data: subscription });
} else {
// Extend the current subscription if there is an active paid subscription.
const expireAt = new Date(Math.max(currentSubscription.expireAt, new Date().getTime()));
expireAt.setFullYear(expireAt.getFullYear() + 1);
await prisma.subscription.update({
where: { id: currentSubscription.id },
data: {
expireAt,
},
});
}
} else if (event.type === "payment_intent.payment_failed") {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
console.log(`❌ Payment failed: ${paymentIntent.last_payment_error?.message}`);
}
// Return a response to acknowledge receipt of the event.
res.json({ received: true });
} else {
res.setHeader("Allow", "POST");
res.status(405).end("Method Not Allowed");
}
};
export default cors(webhookHandler as any);