| 
									
										
										
										
											2022-04-04 20:39:59 +00:00
										 |  |  | import { Prisma } from "@prisma/client"; | 
					
						
							| 
									
										
										
										
											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-23 22:00:30 +00:00
										 |  |  | import EventManager from "@calcom/core/EventManager"; | 
					
						
							| 
									
										
										
										
											2022-03-16 23:36:43 +00:00
										 |  |  | import { getErrorFromUnknown } from "@calcom/lib/errors"; | 
					
						
							| 
									
										
										
										
											2022-03-23 22:00:30 +00:00
										 |  |  | import prisma from "@calcom/prisma"; | 
					
						
							| 
									
										
										
										
											2022-03-09 22:56:05 +00:00
										 |  |  | import stripe from "@calcom/stripe/server"; | 
					
						
							| 
									
										
										
										
											2022-03-23 22:00:30 +00:00
										 |  |  | import { CalendarEvent } from "@calcom/types/Calendar"; | 
					
						
							| 
									
										
										
										
											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"; | 
					
						
							| 
									
										
										
										
											2022-04-02 14:30:07 +00:00
										 |  |  | import { sendScheduledEmails } from "@lib/emails/email-manager"; | 
					
						
							| 
									
										
										
										
											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; | 
					
						
							| 
									
										
										
										
											2022-04-04 20:39:59 +00:00
										 |  |  |   const payment = await prisma.payment.findFirst({ | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |     where: { | 
					
						
							|  |  |  |       externalId: paymentIntent.id, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     select: { | 
					
						
							| 
									
										
										
										
											2022-04-04 20:39:59 +00:00
										 |  |  |       id: true, | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |       bookingId: true, | 
					
						
							| 
									
										
										
										
											2022-04-04 20:39:59 +00:00
										 |  |  |     }, | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2022-04-23 18:45:46 +00:00
										 |  |  |   if (!payment?.bookingId) { | 
					
						
							| 
									
										
										
										
											2022-04-26 11:15:57 +00:00
										 |  |  |     console.log(JSON.stringify(paymentIntent), JSON.stringify(payment)); | 
					
						
							| 
									
										
										
										
											2022-04-23 18:45:46 +00:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-04-04 20:39:59 +00:00
										 |  |  |   if (!payment?.bookingId) throw new Error("Payment not found"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const booking = await prisma.booking.findUnique({ | 
					
						
							|  |  |  |     where: { | 
					
						
							|  |  |  |       id: payment.bookingId, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     select: { | 
					
						
							|  |  |  |       title: true, | 
					
						
							|  |  |  |       description: true, | 
					
						
							|  |  |  |       startTime: true, | 
					
						
							|  |  |  |       endTime: true, | 
					
						
							|  |  |  |       confirmed: true, | 
					
						
							|  |  |  |       attendees: true, | 
					
						
							|  |  |  |       location: true, | 
					
						
							|  |  |  |       userId: true, | 
					
						
							|  |  |  |       id: true, | 
					
						
							|  |  |  |       uid: true, | 
					
						
							|  |  |  |       paid: true, | 
					
						
							|  |  |  |       destinationCalendar: true, | 
					
						
							|  |  |  |       user: { | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |         select: { | 
					
						
							|  |  |  |           id: true, | 
					
						
							| 
									
										
										
										
											2022-04-04 20:39:59 +00:00
										 |  |  |           credentials: true, | 
					
						
							|  |  |  |           timeZone: true, | 
					
						
							|  |  |  |           email: true, | 
					
						
							|  |  |  |           name: true, | 
					
						
							|  |  |  |           locale: true, | 
					
						
							| 
									
										
										
										
											2022-01-21 21:35:31 +00:00
										 |  |  |           destinationCalendar: true, | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |         }, | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   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; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-04 20:39:59 +00:00
										 |  |  |   let bookingData: Prisma.BookingUpdateInput = { | 
					
						
							|  |  |  |     paid: true, | 
					
						
							|  |  |  |     confirmed: true, | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |   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); | 
					
						
							| 
									
										
										
										
											2022-04-04 20:39:59 +00:00
										 |  |  |     bookingData.references = { create: scheduleResult.referencesToCreate }; | 
					
						
							| 
									
										
										
										
											2021-09-22 18:36:13 +00:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-12-17 16:58:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-04 20:39:59 +00:00
										 |  |  |   const paymentUpdate = prisma.payment.update({ | 
					
						
							|  |  |  |     where: { | 
					
						
							|  |  |  |       id: payment.id, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     data: { | 
					
						
							|  |  |  |       success: true, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const bookingUpdate = prisma.booking.update({ | 
					
						
							|  |  |  |     where: { | 
					
						
							|  |  |  |       id: booking.id, | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     data: bookingData, | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   await prisma.$transaction([paymentUpdate, bookingUpdate]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-02 14:30:07 +00:00
										 |  |  |   await sendScheduledEmails({ ...evt }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-23 22:00:30 +00:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @deprecated | 
					
						
							|  |  |  |  * We need to create a PaymentManager in `@calcom/core` | 
					
						
							|  |  |  |  * to prevent circular dependencies on App Store migration | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-27 21:34:04 +00:00
										 |  |  |     if (event.account) { | 
					
						
							|  |  |  |       throw new HttpCode({ statusCode: 202, message: "Incoming connected account" }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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 }); | 
					
						
							|  |  |  | } |