 3c6ac395cc
			
		
	
	
		3c6ac395cc
		
			
		
	
	
	
	
		
			
			* WIP bookings page ui changes, created api endpoint * Ui changes mobile/desktop * Added translations * Fix lib import and common names * WIP reschedule * WIP * Save wip * [WIP] builder and class for CalendarEvent, email for attende * update rescheduled emails, booking view and availability page view * Working version reschedule * Fix for req.user as array * Added missing translation and refactor dialog to self component * Test for reschedule * update on types * Update lib no required * Update type on createBooking * fix types * remove preview stripe sub * remove unused file * remove unused import * Fix reschedule test * Refactor and cleaning up code * Email reschedule title fixes * Adding calendar delete and recreate placeholder of cancelled * Add translation * Removed logs, notes, fixed types * Fixes process.env types * Use strict compare * Fixes type inference * Type fixing is my middle name * Update apps/web/components/booking/BookingListItem.tsx * Update apps/web/components/dialog/RescheduleDialog.tsx * Update packages/core/builders/CalendarEvent/director.ts * Update apps/web/pages/success.tsx * Updates rescheduling labels * Update packages/core/builders/CalendarEvent/builder.ts * Type fixes * Update packages/core/builders/CalendarEvent/builder.ts * Only validating input blocked once * E2E fixes * Stripe tests fixes Co-authored-by: Peer Richelsen <peer@cal.com> Co-authored-by: zomars <zomars@me.com>
		
			
				
	
	
		
			214 lines
		
	
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			214 lines
		
	
	
	
		
			6.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { Prisma, User, Booking, SchedulingType, BookingStatus } from "@prisma/client";
 | |
| import type { NextApiRequest, NextApiResponse } from "next";
 | |
| 
 | |
| import EventManager from "@calcom/core/EventManager";
 | |
| import logger from "@calcom/lib/logger";
 | |
| import type { AdditionInformation } from "@calcom/types/Calendar";
 | |
| import type { CalendarEvent } from "@calcom/types/Calendar";
 | |
| import { refund } from "@ee/lib/stripe/server";
 | |
| 
 | |
| import { asStringOrNull } from "@lib/asStringOrNull";
 | |
| import { getSession } from "@lib/auth";
 | |
| import { sendDeclinedEmails } from "@lib/emails/email-manager";
 | |
| import { sendScheduledEmails } from "@lib/emails/email-manager";
 | |
| import prisma from "@lib/prisma";
 | |
| import { BookingConfirmBody } from "@lib/types/booking";
 | |
| 
 | |
| import { getTranslation } from "@server/lib/i18n";
 | |
| 
 | |
| const authorized = async (
 | |
|   currentUser: Pick<User, "id">,
 | |
|   booking: Pick<Booking, "eventTypeId" | "userId">
 | |
| ) => {
 | |
|   // if the organizer
 | |
|   if (booking.userId === currentUser.id) {
 | |
|     return true;
 | |
|   }
 | |
|   const eventType = await prisma.eventType.findUnique({
 | |
|     where: {
 | |
|       id: booking.eventTypeId || undefined,
 | |
|     },
 | |
|     select: {
 | |
|       schedulingType: true,
 | |
|       users: true,
 | |
|     },
 | |
|   });
 | |
|   if (
 | |
|     eventType?.schedulingType === SchedulingType.COLLECTIVE &&
 | |
|     eventType.users.find((user) => user.id === currentUser.id)
 | |
|   ) {
 | |
|     return true;
 | |
|   }
 | |
|   return false;
 | |
| };
 | |
| 
 | |
| const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
 | |
| 
 | |
| export default async function handler(req: NextApiRequest, res: NextApiResponse) {
 | |
|   const session = await getSession({ req: req });
 | |
|   if (!session?.user?.id) {
 | |
|     return res.status(401).json({ message: "Not authenticated" });
 | |
|   }
 | |
| 
 | |
|   const reqBody = req.body as BookingConfirmBody;
 | |
|   const bookingId = reqBody.id;
 | |
| 
 | |
|   if (!bookingId) {
 | |
|     return res.status(400).json({ message: "bookingId missing" });
 | |
|   }
 | |
| 
 | |
|   const currentUser = await prisma.user.findFirst({
 | |
|     where: {
 | |
|       id: session.user.id,
 | |
|     },
 | |
|     select: {
 | |
|       id: true,
 | |
|       credentials: {
 | |
|         orderBy: { id: "desc" as Prisma.SortOrder },
 | |
|       },
 | |
|       timeZone: true,
 | |
|       email: true,
 | |
|       name: true,
 | |
|       username: true,
 | |
|       destinationCalendar: true,
 | |
|       locale: true,
 | |
|     },
 | |
|   });
 | |
| 
 | |
|   if (!currentUser) {
 | |
|     return res.status(404).json({ message: "User not found" });
 | |
|   }
 | |
| 
 | |
|   const tOrganizer = await getTranslation(currentUser.locale ?? "en", "common");
 | |
| 
 | |
|   if (req.method === "PATCH") {
 | |
|     const booking = await prisma.booking.findFirst({
 | |
|       where: {
 | |
|         id: bookingId,
 | |
|       },
 | |
|       select: {
 | |
|         title: true,
 | |
|         description: true,
 | |
|         startTime: true,
 | |
|         endTime: true,
 | |
|         confirmed: true,
 | |
|         attendees: true,
 | |
|         eventTypeId: true,
 | |
|         location: true,
 | |
|         userId: true,
 | |
|         id: true,
 | |
|         uid: true,
 | |
|         payment: true,
 | |
|         destinationCalendar: true,
 | |
|       },
 | |
|     });
 | |
| 
 | |
|     if (!booking) {
 | |
|       return res.status(404).json({ message: "booking not found" });
 | |
|     }
 | |
| 
 | |
|     if (!(await authorized(currentUser, booking))) {
 | |
|       return res.status(401).end();
 | |
|     }
 | |
| 
 | |
|     if (booking.confirmed) {
 | |
|       return res.status(400).json({ message: "booking already confirmed" });
 | |
|     }
 | |
| 
 | |
|     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",
 | |
|         },
 | |
|       };
 | |
|     });
 | |
| 
 | |
|     const attendeesList = await Promise.all(attendeesListPromises);
 | |
| 
 | |
|     const evt: CalendarEvent = {
 | |
|       type: booking.title,
 | |
|       title: booking.title,
 | |
|       description: booking.description,
 | |
|       startTime: booking.startTime.toISOString(),
 | |
|       endTime: booking.endTime.toISOString(),
 | |
|       organizer: {
 | |
|         email: currentUser.email,
 | |
|         name: currentUser.name || "Unnamed",
 | |
|         timeZone: currentUser.timeZone,
 | |
|         language: { translate: tOrganizer, locale: currentUser.locale ?? "en" },
 | |
|       },
 | |
|       attendees: attendeesList,
 | |
|       location: booking.location ?? "",
 | |
|       uid: booking.uid,
 | |
|       destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar,
 | |
|     };
 | |
| 
 | |
|     if (reqBody.confirmed) {
 | |
|       const eventManager = new EventManager(currentUser);
 | |
|       const scheduleResult = await eventManager.create(evt);
 | |
| 
 | |
|       const results = scheduleResult.results;
 | |
| 
 | |
|       if (results.length > 0 && results.every((res) => !res.success)) {
 | |
|         const error = {
 | |
|           errorCode: "BookingCreatingMeetingFailed",
 | |
|           message: "Booking failed",
 | |
|         };
 | |
| 
 | |
|         log.error(`Booking ${currentUser.username} failed`, error, results);
 | |
|       } else {
 | |
|         const metadata: AdditionInformation = {};
 | |
| 
 | |
|         if (results.length) {
 | |
|           // TODO: Handle created event metadata more elegantly
 | |
|           metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
 | |
|           metadata.conferenceData = results[0].createdEvent?.conferenceData;
 | |
|           metadata.entryPoints = results[0].createdEvent?.entryPoints;
 | |
|         }
 | |
|         try {
 | |
|           await sendScheduledEmails({ ...evt, additionInformation: metadata });
 | |
|         } catch (error) {
 | |
|           log.error(error);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // @NOTE: be careful with this as if any error occurs before this booking doesn't get confirmed
 | |
|       // Should perform update on booking (confirm) -> then trigger the rest handlers
 | |
|       await prisma.booking.update({
 | |
|         where: {
 | |
|           id: bookingId,
 | |
|         },
 | |
|         data: {
 | |
|           confirmed: true,
 | |
|           references: {
 | |
|             create: scheduleResult.referencesToCreate,
 | |
|           },
 | |
|         },
 | |
|       });
 | |
| 
 | |
|       res.status(204).end();
 | |
|     } else {
 | |
|       await refund(booking, evt);
 | |
|       const rejectionReason = asStringOrNull(req.body.reason) || "";
 | |
|       evt.rejectionReason = rejectionReason;
 | |
|       await prisma.booking.update({
 | |
|         where: {
 | |
|           id: bookingId,
 | |
|         },
 | |
|         data: {
 | |
|           rejected: true,
 | |
|           status: BookingStatus.REJECTED,
 | |
|           rejectionReason: rejectionReason,
 | |
|         },
 | |
|       });
 | |
| 
 | |
|       await sendDeclinedEmails(evt);
 | |
| 
 | |
|       res.status(204).end();
 | |
|     }
 | |
|   }
 | |
| }
 |