server/stripeCreateCheckoutSessionHandler.ts (107 lines of code) (raw):

import * as Sentry from '@sentry/node'; import type express from 'express'; import fetch from 'node-fetch'; import type { StripeCreateCheckoutSessionRequest } from '@/shared/requests/stripe-create-checkout-session'; import { STRIPE_PUBLIC_KEY_HEADER } from '../shared/stripeSetupIntent'; import { conf } from './config'; import { log, putMetric } from './log'; import { stripeSetupIntentConfigPromise } from './stripeSetupIntentConfig'; export const stripeCreateCheckoutSessionHandler = async ( request: express.Request, response: express.Response, ) => { // Read Request JSON Body const clientRequestBodyData = request.body.toString(); if (!clientRequestBodyData) { response.status(400).send('missing request body'); return; } // Map request const clientRequestBody: StripeCreateCheckoutSessionRequest = JSON.parse( clientRequestBodyData, ); // Get Stripe Secret Key stripeSetupIntentConfigPromise .then((stripePublicToSecretKeyMapping) => { if (!stripePublicToSecretKeyMapping) { throw new Error('missing Stripe SetupIntent config'); } const stripePublicKey = request.header(STRIPE_PUBLIC_KEY_HEADER); if (!stripePublicKey) { response .status(400) .send(`missing header '${STRIPE_PUBLIC_KEY_HEADER}'`); return; } const stripeSecretKey = stripePublicToSecretKeyMapping[stripePublicKey]; if (!stripeSecretKey) { throw new Error( `no secret key mapping for Stripe public key '${stripePublicKey}'`, ); } const httpMethod = 'POST'; const outgoingURL = 'https://api.stripe.com/v1/checkout/sessions'; const requestBody = new URLSearchParams({ mode: 'setup', success_url: `https://manage.${conf.DOMAIN}/payment/${clientRequestBody.productTypeUrlPart}/checkout-session-return?id={CHECKOUT_SESSION_ID}&paymentMethodType=${clientRequestBody.paymentMethodType}&subscriptionId=${clientRequestBody.subscriptionId}`, cancel_url: `https://manage.${conf.DOMAIN}/payment/${clientRequestBody.productTypeUrlPart}`, 'payment_method_types[0]': clientRequestBody.paymentMethodType, /** * https://docs.stripe.com/api/checkout/sessions/create?lang=php#create_checkout_session-customer_email */ customer_email: response.locals.identity?.email || '', }).toString(); // tslint:disable-next-line:no-object-mutation response.locals.loggingDetail = { loggingCode: 'STRIPE_CHECKOUT_SESSION', stripePublicKey, // this will indicate 'test mode' vs 'live' httpMethod, identityID: response.locals.identity && response.locals.identity.userId, incomingURL: request.originalUrl, requestBody, outgoingURL, }; fetch(outgoingURL, { method: httpMethod, headers: { Authorization: `Bearer ${stripeSecretKey}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: requestBody, }) .then((stripeResponse) => { // tslint:disable-next-line:no-object-mutation response.locals.loggingDetail.status = stripeResponse.status; // tslint:disable-next-line:no-object-mutation response.locals.loggingDetail.isOK = stripeResponse.ok; if (stripeResponse.ok) { return stripeResponse.json(); } else { throw new Error( `Failed to create CheckoutSession : ${ stripeResponse.status } ${ stripeResponse.statusText // eslint-disable-next-line @typescript-eslint/no-base-to-string -- we believe this function will not evaluate to '[object Object' } : ${stripeResponse.text()}`, ); } }) .then((checkoutSession: { id: string; url: string }) => { const suitableLog = response.locals.loggingDetail.isOK ? log.info : log.warning; suitableLog('fetching', response.locals.loggingDetail); putMetric(response.locals.loggingDetail); response.json({ id: checkoutSession.id, url: checkoutSession.url, }); }) .catch(handleTerminalError(response)); }) .catch(handleTerminalError(response)); }; const handleTerminalError = (response: express.Response) => (error: Error) => { Sentry.captureException(error); log.error('Failed to create CheckoutSession', { ...response.locals.loggingDetail, exception: error || 'undefined', }); putMetric(response.locals.loggingDetail); response.status(500).send(); };