| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  | import { buffer } from "micro"; | 
					
						
							|  |  |  | import type { NextApiRequest, NextApiResponse } from "next"; | 
					
						
							| 
									
										
										
										
											2021-09-22 19:52:38 +00:00
										 |  |  | import Stripe from "stripe"; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-09 22:56:05 +00:00
										 |  |  | import stripe from "@calcom/stripe/server"; | 
					
						
							| 
									
										
										
										
											2021-09-22 19:52:38 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-01 10:32:08 +00:00
										 |  |  | import { IS_PRODUCTION } from "@lib/config/constants"; | 
					
						
							| 
									
										
										
										
											2021-12-17 16:58:23 +00:00
										 |  |  | import { HttpError as HttpCode } from "@lib/core/http/error"; | 
					
						
							| 
									
										
										
										
											2021-10-20 15:42:40 +00:00
										 |  |  | import { getErrorFromUnknown } from "@lib/errors"; | 
					
						
							| 
									
										
										
										
											2021-09-22 19:52:38 +00:00
										 |  |  | import EventManager from "@lib/events/EventManager"; | 
					
						
							| 
									
										
										
										
											2022-01-06 17:28:31 +00:00
										 |  |  | import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar"; | 
					
						
							| 
									
										
										
										
											2021-09-22 19:52:38 +00:00
										 |  |  | import prisma from "@lib/prisma"; | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | import { getTranslation } from "@server/lib/i18n"; | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | export const config = { | 
					
						
							|  |  |  |   api: { | 
					
						
							|  |  |  |     bodyParser: false, | 
					
						
							|  |  |  |   }, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function handlePaymentSuccess(event: Stripe.Event) { | 
					
						
							|  |  |  |   const paymentIntent = event.data.object as Stripe.PaymentIntent; | 
					
						
							|  |  |  |   const payment = await prisma.payment.update({ | 
					
						
							|  |  |  |     where: { | 
					
						
							|  |  |  |       externalId: paymentIntent.id, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     data: { | 
					
						
							|  |  |  |       success: true, | 
					
						
							|  |  |  |       booking: { | 
					
						
							|  |  |  |         update: { | 
					
						
							|  |  |  |           paid: true, | 
					
						
							| 
									
										
										
										
											2021-12-17 16:58:23 +00:00
										 |  |  |           confirmed: true, | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |         }, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     select: { | 
					
						
							|  |  |  |       bookingId: true, | 
					
						
							|  |  |  |       booking: { | 
					
						
							|  |  |  |         select: { | 
					
						
							|  |  |  |           title: true, | 
					
						
							|  |  |  |           description: true, | 
					
						
							|  |  |  |           startTime: true, | 
					
						
							|  |  |  |           endTime: true, | 
					
						
							|  |  |  |           confirmed: true, | 
					
						
							|  |  |  |           attendees: true, | 
					
						
							|  |  |  |           location: true, | 
					
						
							|  |  |  |           userId: true, | 
					
						
							|  |  |  |           id: true, | 
					
						
							|  |  |  |           uid: true, | 
					
						
							|  |  |  |           paid: true, | 
					
						
							| 
									
										
										
										
											2022-01-21 21:35:31 +00:00
										 |  |  |           destinationCalendar: true, | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |           user: { | 
					
						
							|  |  |  |             select: { | 
					
						
							|  |  |  |               id: true, | 
					
						
							|  |  |  |               credentials: true, | 
					
						
							|  |  |  |               timeZone: true, | 
					
						
							|  |  |  |               email: true, | 
					
						
							|  |  |  |               name: true, | 
					
						
							| 
									
										
										
										
											2021-10-28 22:58:26 +00:00
										 |  |  |               locale: true, | 
					
						
							| 
									
										
										
										
											2021-12-09 15:51:37 +00:00
										 |  |  |               destinationCalendar: true, | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |             }, | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!payment) throw new Error("No payment found"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const { booking } = payment; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!booking) throw new Error("No booking found"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const { user } = booking; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (!user) throw new Error("No user found"); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-28 22:58:26 +00:00
										 |  |  |   const t = await getTranslation(user.locale ?? "en", "common"); | 
					
						
							| 
									
										
										
										
											2022-01-27 20:32:53 +00:00
										 |  |  |   const attendeesListPromises = booking.attendees.map(async (attendee) => { | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       name: attendee.name, | 
					
						
							|  |  |  |       email: attendee.email, | 
					
						
							|  |  |  |       timeZone: attendee.timeZone, | 
					
						
							|  |  |  |       language: { | 
					
						
							|  |  |  |         translate: await getTranslation(attendee.locale ?? "en", "common"), | 
					
						
							|  |  |  |         locale: attendee.locale ?? "en", | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-27 20:32:53 +00:00
										 |  |  |   const attendeesList = await Promise.all(attendeesListPromises); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const evt: CalendarEvent = { | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |     type: booking.title, | 
					
						
							|  |  |  |     title: booking.title, | 
					
						
							|  |  |  |     description: booking.description || undefined, | 
					
						
							|  |  |  |     startTime: booking.startTime.toISOString(), | 
					
						
							|  |  |  |     endTime: booking.endTime.toISOString(), | 
					
						
							| 
									
										
										
										
											2022-01-27 20:32:53 +00:00
										 |  |  |     organizer: { | 
					
						
							|  |  |  |       email: user.email!, | 
					
						
							|  |  |  |       name: user.name!, | 
					
						
							|  |  |  |       timeZone: user.timeZone, | 
					
						
							|  |  |  |       language: { translate: t, locale: user.locale ?? "en" }, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     attendees: attendeesList, | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |     uid: booking.uid, | 
					
						
							| 
									
										
										
										
											2022-01-21 21:35:31 +00:00
										 |  |  |     destinationCalendar: booking.destinationCalendar || user.destinationCalendar, | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |   }; | 
					
						
							| 
									
										
										
										
											2021-10-28 22:58:26 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |   if (booking.location) evt.location = booking.location; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (booking.confirmed) { | 
					
						
							| 
									
										
										
										
											2021-12-09 15:51:37 +00:00
										 |  |  |     const eventManager = new EventManager(user); | 
					
						
							| 
									
										
										
										
											2021-10-26 16:17:24 +00:00
										 |  |  |     const scheduleResult = await eventManager.create(evt); | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     await prisma.booking.update({ | 
					
						
							|  |  |  |       where: { | 
					
						
							| 
									
										
										
										
											2021-12-17 16:58:23 +00:00
										 |  |  |         id: booking.id, | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |       }, | 
					
						
							|  |  |  |       data: { | 
					
						
							|  |  |  |         references: { | 
					
						
							|  |  |  |           create: scheduleResult.referencesToCreate, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-12-17 16:58:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   throw new HttpCode({ | 
					
						
							|  |  |  |     statusCode: 200, | 
					
						
							|  |  |  |     message: `Booking with id '${booking.id}' was paid and confirmed.`, | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-28 13:00:19 +00:00
										 |  |  | type WebhookHandler = (event: Stripe.Event) => Promise<void>; | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-28 13:00:19 +00:00
										 |  |  | const webhookHandlers: Record<string, WebhookHandler | undefined> = { | 
					
						
							|  |  |  |   "payment_intent.succeeded": handlePaymentSuccess, | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-28 13:00:19 +00:00
										 |  |  | export default async function handler(req: NextApiRequest, res: NextApiResponse) { | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |   try { | 
					
						
							| 
									
										
										
										
											2021-09-28 13:00:19 +00:00
										 |  |  |     if (req.method !== "POST") { | 
					
						
							| 
									
										
										
										
											2021-12-17 16:58:23 +00:00
										 |  |  |       throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" }); | 
					
						
							| 
									
										
										
										
											2021-09-28 13:00:19 +00:00
										 |  |  |     } | 
					
						
							|  |  |  |     const sig = req.headers["stripe-signature"]; | 
					
						
							|  |  |  |     if (!sig) { | 
					
						
							| 
									
										
										
										
											2021-12-17 16:58:23 +00:00
										 |  |  |       throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" }); | 
					
						
							| 
									
										
										
										
											2021-09-28 13:00:19 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!process.env.STRIPE_WEBHOOK_SECRET) { | 
					
						
							| 
									
										
										
										
											2021-12-17 16:58:23 +00:00
										 |  |  |       throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" }); | 
					
						
							| 
									
										
										
										
											2021-09-28 13:00:19 +00:00
										 |  |  |     } | 
					
						
							|  |  |  |     const requestBuffer = await buffer(req); | 
					
						
							|  |  |  |     const payload = requestBuffer.toString(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET); | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-28 13:00:19 +00:00
										 |  |  |     const handler = webhookHandlers[event.type]; | 
					
						
							|  |  |  |     if (handler) { | 
					
						
							|  |  |  |       await handler(event); | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |     } else { | 
					
						
							| 
									
										
										
										
											2021-11-30 23:50:49 +00:00
										 |  |  |       /** Not really an error, just letting Stripe know that the webhook was received but unhandled */ | 
					
						
							| 
									
										
										
										
											2021-12-17 16:58:23 +00:00
										 |  |  |       throw new HttpCode({ | 
					
						
							| 
									
										
										
										
											2021-11-30 23:50:49 +00:00
										 |  |  |         statusCode: 202, | 
					
						
							|  |  |  |         message: `Unhandled Stripe Webhook event type ${event.type}`, | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |     } | 
					
						
							|  |  |  |   } catch (_err) { | 
					
						
							|  |  |  |     const err = getErrorFromUnknown(_err); | 
					
						
							|  |  |  |     console.error(`Webhook Error: ${err.message}`); | 
					
						
							|  |  |  |     res.status(err.statusCode ?? 500).send({ | 
					
						
							|  |  |  |       message: err.message, | 
					
						
							| 
									
										
										
										
											2021-12-01 10:32:08 +00:00
										 |  |  |       stack: IS_PRODUCTION ? undefined : err.stack, | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Return a response to acknowledge receipt of the event
 | 
					
						
							|  |  |  |   res.json({ received: true }); | 
					
						
							|  |  |  | } |