customer-dashboard-api/src/controllers/paymentController.ts

352 lines
9.1 KiB
TypeScript

import { Request, Response } from "express";
import logger, { userLogger } from "../logger/logger";
import Stripe from "stripe";
import {
ACCOUNT_STATE,
DASHBOARD_URL,
PAYMENT_PLAN_SETTINGS,
} from "../utils/constants";
import UserPendingPayment from "../models/userPendingPayment";
import User from "../models/user";
import { getUserSession, stripe } from "../utils/utils";
import Store from "../models/store";
import Session from "../models/session";
const ZeitAdlerBasicMonthly = "zeitadler-basic-monthly";
const ZeitAdlerBasicYearly = "zeitadler-basic-yearly";
const ZeitAdlerPremiumMonthly = "zeitadler-premium-monthly";
const ZeitAdlerPremiumYearly = "zeitadler-premium-yearly";
let cachedPrices = [] as any[];
export async function loadPrices() {
if (cachedPrices.length > 0) {
return;
}
logger.info("Loading prices from Stripe API");
// load prices from stripe
const prices = await stripe.prices.list({
lookup_keys: [
ZeitAdlerBasicMonthly,
ZeitAdlerBasicYearly,
ZeitAdlerPremiumMonthly,
ZeitAdlerPremiumYearly,
],
expand: ["data.product"],
});
for (const price of prices.data) {
cachedPrices.push({
id: price.id,
unit_amount: price.unit_amount,
lookup_key: price.lookup_key,
});
}
}
export async function getPriceId(paymentPlan: number, interval: number) {
await loadPrices();
const val = `zeitadler-${PAYMENT_PLAN_SETTINGS[paymentPlan].id}-${
interval === 1 ? "yearly" : "monthly"
}`;
const found = cachedPrices.find((price) => price.lookup_key === val);
if (found === undefined) {
return null;
}
return found.id;
}
// used to get the prices for the frontend
export async function GetPrices(req: Request, res: Response) {
try {
const { productId } = req.params;
if (productId === undefined || (productId !== "1" && productId !== "2")) {
return res.status(400).send({ err: "invalid request" });
}
await loadPrices();
let lookupKey: any[] = [];
if (productId === "1") {
lookupKey = [ZeitAdlerBasicMonthly, ZeitAdlerBasicYearly];
} else if (productId === "2") {
lookupKey = [ZeitAdlerPremiumMonthly, ZeitAdlerPremiumYearly];
}
res.status(200).send({
prices: cachedPrices
.filter(
(price) =>
price.lookup_key === lookupKey[0] ||
price.lookup_key === lookupKey[1]
)
.map((price) => {
return {
lookup_key: price.lookup_key,
unit_amount: price.unit_amount,
};
}),
});
} catch (error) {
logger.error("GetPrices", error as string);
res.status(500).send({ err: "invalid request" });
}
}
export async function CreateCheckoutSession(priceId: string, userId: string) {
try {
if (priceId === undefined || userId === undefined) {
logger.error("CreateCheckoutSession: invalid request");
return "";
}
await loadPrices();
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [
{
price: priceId,
quantity: 1,
},
],
mode: "subscription",
success_url: `${DASHBOARD_URL}/checkout/success/{CHECKOUT_SESSION_ID}`,
cancel_url: `${DASHBOARD_URL}/checkout/canceled/{CHECKOUT_SESSION_ID}`,
metadata: {
userId: userId,
},
automatic_tax: {
// see https://dashboard.stripe.com/settings/tax
enabled: true,
},
});
await UserPendingPayment.create({
payment_session_id: session.id,
user_id: userId,
payment_session_url: session.url as string,
});
logger.info(`CreateCheckoutSession: ${session.id}`);
return session.url;
} catch (error) {
logger.error("CreateCheckoutSession", error as string);
return "";
}
}
// user is redirected to dashboard checkout page after successful payment
// and then makes request to this endpoint to create a user session
export async function CheckoutSuccess(req: Request, res: Response) {
try {
const { sessionId } = req.body;
logger.info("CheckoutSuccess", sessionId);
if (sessionId === undefined) {
return res.status(400).send({ err: "invalid request" });
}
const userPendingPayment = await UserPendingPayment.findOne({
where: {
payment_session_id: sessionId,
},
});
if (userPendingPayment === null) {
logger.info("CheckoutSuccess payment successful");
return res.status(200).send({ status: 1 });
}
logger.info("CheckoutSuccess pending payment");
res.status(200).send({ status: 0 });
} catch (err) {
logger.error("CheckoutSuccess", err as string);
res.status(500).send({ err: "invalid request" });
}
}
export async function CheckoutCanceled(req: Request, res: Response) {
try {
const { sessionId } = req.body;
if (sessionId === undefined) {
logger.error("CheckoutCanceled: invalid request");
return res.status(400).send({ err: "invalid request" });
}
const userPendingPayment = await UserPendingPayment.findOne({
where: {
payment_session_id: sessionId,
},
});
if (userPendingPayment === null) {
logger.error("CheckoutCanceled: user pending payment not found");
return res.status(400).send({ err: "invalid request" });
}
// delete user
await User.destroy({
where: {
user_id: userPendingPayment.user_id,
},
});
// delete store
await Store.destroy({
where: {
owner_user_id: userPendingPayment.user_id,
},
});
// delete all user sessions
await Session.destroy({
where: {
user_id: userPendingPayment.user_id,
},
});
// delete user pending payment
await userPendingPayment.destroy();
logger.info(`CheckoutCanceled: ${sessionId}`);
res.status(200).send({ status: 1 });
} catch (error) {
logger.error("CheckoutCanceled", error as string);
res.status(500).send({ err: "invalid request" });
}
}
export async function StripeWebhook(req: Request, res: Response) {
try {
if (req.method !== "POST") {
logger.error("StripeWebhook: invalid reques. Method", req.method);
return res.status(405).end(); // Method Not Allowed
}
const payload = req.body;
const sig = req.headers["stripe-signature"] as string;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
payload,
sig,
process.env.STRIPE_WEBHOOK_ENDPOINT_SECRET as string
);
} catch (error) {
logger.error("Webhook error", error as string);
return res.status(400).send(`Webhook Error: ${error}`);
}
if (event.type === "checkout.session.completed") {
const userId = (event.data.object as Stripe.Checkout.Session)?.metadata
?.userId;
// delete user pending payment
logger.info(
"delete user pending payment",
(event.data.object as Stripe.Checkout.Session).id
);
const pendingPayment = await UserPendingPayment.findOne({
where: {
payment_session_id: (event.data.object as Stripe.Checkout.Session).id,
},
});
if (pendingPayment !== null) {
await pendingPayment.destroy();
const user = await User.findOne({
where: {
user_id: userId,
},
});
if (user !== null && user.state === ACCOUNT_STATE.INIT_PAYMENT) {
// update user state
await User.update(
{
state: ACCOUNT_STATE.ACTIVE,
stripe_customer_id: (event.data.object as Stripe.Checkout.Session)
.customer as string,
},
{
where: {
user_id: userId,
},
}
);
}
}
} else if (event.type === "checkout.session.expired") {
logger.info(`webhook checkout.session.expired: ${event.data.object}`);
} else {
logger.warn(`Ignoring unknown event: ${event.type}`);
}
res.sendStatus(200);
} catch (error) {
logger.error("StripeWebhook", error as string);
res.status(500).send({ err: "invalid request" });
}
}
export async function GetBillingPortal(req: Request, res: Response) {
try {
const userSession = await getUserSession(req);
if (userSession === null) {
return res.status(401).send({ err: "unauthorized" });
}
const user = await User.findOne({
where: {
user_id: userSession.user_id,
},
attributes: ["stripe_customer_id"],
});
if (user === null || user.stripe_customer_id === null) {
userLogger.error(
userSession.user_id,
"GetBillingPortal: user not found",
userSession.user_id
);
return res.status(400).send({ err: "invalid request" });
}
const session = await stripe.billingPortal.sessions.create({
customer: user.stripe_customer_id,
return_url: `${DASHBOARD_URL}/payment-plan`,
});
res.status(200).send({
url: session.url,
});
} catch (error) {
logger.error("GetBillingPortal", error as string);
res.status(500).send({ err: "invalid request" });
}
}