import { Request, Response } from "express"; import logger, { userLogger } from "../logger/logger"; import Stripe from "stripe"; import { DASHBOARD_URL, PAYMENT_PLAN, 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 = "za-basic-monthly"; const ZeitAdlerBasicYearly = "za-basic-yearly"; const lookupKeys = [ZeitAdlerBasicMonthly, ZeitAdlerBasicYearly]; 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: lookupKeys, expand: ["data.product"], }); logger.debug("Prices from Stripe API", JSON.stringify(prices, null, 2)); for (const price of prices.data) { cachedPrices.push({ id: price.id, unit_amount: (price.unit_amount as number) / 100, lookup_key: price.lookup_key, }); } } export async function getUserPlanInfo(req: Request, res: Response) { try { const userSession = await getUserSession(req); if (userSession === null) { return res.status(401).send({ err: "unauthorized" }); } await loadPrices(); const user = await User.findOne({ where: { user_id: userSession.user_id, }, attributes: [ "payment_plan", "payment_plan_interval", "payment_plan_trial_end", "payment_plan_canceled_at", ], }); if (user === null) { return res.status(400).send({ err: "invalid request" }); } let resData = { payment_plan: user.payment_plan, payment_plan_interval: user.payment_plan_interval, prices: cachedPrices .filter( (price) => price.lookup_key === lookupKeys[0] || price.lookup_key === lookupKeys[1] ) .map((price) => { return { lookup_key: price.lookup_key, unit_amount: price.unit_amount, }; }), } as any; if (user.payment_plan_trial_end !== null) { resData["payment_plan_trial_end"] = user.payment_plan_trial_end; } if (user.payment_plan_canceled_at !== null) { resData["payment_plan_canceled_at"] = user.payment_plan_canceled_at; } userLogger.info(userSession.user_id, "GetUserPlanInfo"); res.status(200).send(resData); } catch (error) { logger.error("getUserPlanInfo", error as string); res.status(500).send({ err: "invalid request" }); } } 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 !== "0" && productId !== "1")) { return res.status(400).send({ err: "invalid request" }); } await loadPrices(); let lookupKey = [ZeitAdlerBasicMonthly, ZeitAdlerBasicYearly]; 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(req: Request, res: Response) { try { const { lookupKey } = req.body; if (lookupKey === undefined) { return res.status(400).send({ err: "invalid request" }); } await loadPrices(); // check if lookupKey is valid const priceId = cachedPrices.find( (price) => price.lookup_key === lookupKey )?.id; if (priceId === undefined) { return res.status(400).send({ err: "invalid request" }); } 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: ["user_id", "payment_plan_trial_end", "stripe_customer_id"], }); if (user === null) { return res.status(400).send({ err: "invalid request" }); } if (user.stripe_customer_id !== null && user.stripe_customer_id !== "") { const subscriptions = await stripe.subscriptions.list({ customer: user.stripe_customer_id, limit: 1, }); if ( subscriptions.data.length > 0 && subscriptions.data[0].id !== priceId ) { const latestSubscription = subscriptions.data[0]; await stripe.subscriptions.update(latestSubscription.id, { items: [ { id: latestSubscription.items.data[0].id, price: priceId, }, ], }); // update user await User.update( { payment_plan_interval: lookupKey === ZeitAdlerBasicYearly ? 1 : 0, }, { where: { user_id: userSession.user_id, }, } ); userLogger.info( userSession.user_id, `CreateCheckoutSession: update subscription ${latestSubscription.id} to ${priceId}` ); return res.status(200).json({ status: "ok" }); } } // create checkout session const session = await stripe.checkout.sessions.create({ payment_method_types: ["card"], line_items: [ { price: priceId, quantity: 1, }, ], billing_address_collection: "required", mode: "subscription", subscription_data: { trial_end: new Date(user.payment_plan_trial_end).getTime() / 1000, }, success_url: `${DASHBOARD_URL}/payment-plan`, cancel_url: `${DASHBOARD_URL}/payment-plan`, metadata: { userId: userSession.user_id, payment_plan_interval: lookupKey === ZeitAdlerBasicYearly ? 1 : 0, } /* automatic_tax: { // see https://dashboard.stripe.com/settings/tax enabled: true, },*/, }); /* await UserPendingPayment.create({ payment_session_id: session.id, user_id: userSession.user_id, payment_session_url: session.url as string, }); */ userLogger.info( userSession.user_id, `CreateCheckoutSession: ${session.id}` ); res.status(200).send({ url: session.url, }); } catch (error) { logger.error("CreateCheckoutSession", error as string); res.status(500).send({ err: "invalid request" }); } } // 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}`); } let user; let userId; let customerId; switch (event.type) { case "checkout.session.completed": const session = event.data.object as Stripe.Checkout.Session; if (session === undefined) { logger.error( "StripeWebhook: checkout.session.completed: session undefined" ); break; } userId = session.metadata?.userId; if (userId === undefined) { logger.error( "StripeWebhook: checkout.session.completed: userId undefined" ); break; } const paymentPlanInterval = session.metadata?.payment_plan_interval; if (paymentPlanInterval === undefined) { logger.error( "StripeWebhook: checkout.session.completed: paymentPlanInterval undefined" ); break; } user = await User.findOne({ where: { user_id: userId, }, }); if (user !== null) { await User.update( { payment_plan: PAYMENT_PLAN.BASIC, stripe_customer_id: (event.data.object as Stripe.Checkout.Session) .customer as string, payment_plan_interval: parseInt(paymentPlanInterval), }, { where: { user_id: userId, }, } ); logger.info( `StripeWebhook: checkout.session.completed: ${userId} ${JSON.stringify( event.data.object )}` ); } else { logger.error( `StripeWebhook: checkout.session.completed: user not found for userId: ${userId}` ); } break; case "checkout.session.expired": logger.info( `StripeWebhook: checkout.session.expired: ${event.data.object.id}` ); break; case "billing_portal.session.created": customerId = event.data.object.customer as string; user = await User.findOne({ where: { stripe_customer_id: customerId, }, }); if (user === null) { logger.error( `StripeWebhook: customer.subscription.updated: user not found for customerId: ${customerId}` ); break; } userLogger.info( user.user_id as string, `StripeWebhook: billing_portal.session.created: customerId: ${customerId}` ); break; case "customer.subscription.updated": const subscription = event.data.object as Stripe.Subscription; const status = subscription.status; const trialEnd = subscription.trial_end as number | null; const canceledAt = subscription.canceled_at; customerId = subscription.customer as string; logger.info( `StripeWebhook: customer.subscription.updated: ${subscription.id} status: ${status} trialEnd: ${trialEnd} canceledAt: ${canceledAt} customerId: ${customerId}` ); user = await User.findOne({ where: { stripe_customer_id: customerId, }, }); if (user === null) { logger.error( `StripeWebhook: customer.subscription.updated: user not found for customerId: ${customerId}` ); break; } let updateData: any = { payment_plan_status: status, /*payment_plan_trial_end: trialEnd !== null ? new Date(trialEnd * 1000) : null, */ payment_plan_canceled_at: canceledAt !== null ? new Date(canceledAt * 1000) : null, }; await User.update(updateData, { where: { stripe_customer_id: customerId, }, }); logger.info( `StripeWebhook: customer.subscription.updated: updating user: ${ user.user_id } ${JSON.stringify(updateData)}` ); break; default: logger.warn(`StripeWebhook: Ignoring unknown event: ${event.type}`); break; } /* 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.id}`); } else { logger.warn(`Ignoring unknown event: ${event.type}`); } */ res.sendStatus(200); } catch (error) { logger.error("StripeWebhook err:", 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`, }); userLogger.info(userSession.user_id, "GetBillingPortal"); res.status(200).send({ url: session.url, }); } catch (error) { logger.error("GetBillingPortal", error as string); res.status(500).send({ err: "invalid request" }); } }