diff --git a/apps/web/lib/booking.ts b/apps/web/lib/booking.ts index 1bff1aa195..8a73958603 100644 --- a/apps/web/lib/booking.ts +++ b/apps/web/lib/booking.ts @@ -1,4 +1,5 @@ import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; +import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import prisma from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import { BookingStatus } from "@calcom/prisma/enums"; @@ -115,21 +116,23 @@ export const handleSeatsEventTypeOnBooking = async ( }> >, seatReferenceUid?: string, - userId?: number + isHost?: boolean ) => { - if (eventType?.seatsPerTimeSlot !== null) { - // @TODO: right now bookings with seats doesn't save every description that its entered by every user - delete bookingInfo.description; - } else { - return; - } - // @TODO: If handling teams, we need to do more check ups for this. - if (bookingInfo?.user?.id === userId) { - return; - } - - if (!eventType.seatsShowAttendees) { - const seatAttendee = await prisma.bookingSeat.findFirst({ + bookingInfo["responses"] = {}; + type seatAttendee = { + attendee: { + email: string; + name: string; + }; + id: number; + data: Prisma.JsonValue; + bookingId: number; + attendeeId: number; + referenceUid: string; + } | null; + let seatAttendee: seatAttendee = null; + if (seatReferenceUid) { + seatAttendee = await prisma.bookingSeat.findFirst({ where: { referenceUid: seatReferenceUid, }, @@ -142,16 +145,31 @@ export const handleSeatsEventTypeOnBooking = async ( }, }, }); + } + if (seatAttendee) { + const seatAttendeeData = seatAttendee.data as unknown as { + description?: string; + responses: Prisma.JsonValue; + }; + bookingInfo["description"] = seatAttendeeData.description ?? null; + bookingInfo["responses"] = bookingResponsesDbSchema.parse(seatAttendeeData.responses ?? {}); + } + if (!eventType.seatsShowAttendees && !isHost) { if (seatAttendee) { const attendee = bookingInfo?.attendees?.find((a) => { - return a.email === seatAttendee.attendee?.email; + return a.email === seatAttendee?.attendee?.email; }); bookingInfo["attendees"] = attendee ? [attendee] : []; } else { bookingInfo["attendees"] = []; } } + + // // @TODO: If handling teams, we need to do more check ups for this. + // if (bookingInfo?.user?.id === userId) { + // return; + // } return bookingInfo; }; diff --git a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx index 62a6d4dade..4c476f1bab 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx @@ -56,12 +56,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const parsedQuery = querySchema.safeParse(context.query); if (!parsedQuery.success) return { notFound: true } as const; - const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data; + const { eventTypeSlug } = parsedQuery.data; + let { uid, seatReferenceUid } = parsedQuery.data; - const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid); + const maybeBookingUidFromSeat = await maybeGetBookingUidFromSeat(prisma, uid); + if (maybeBookingUidFromSeat.uid) uid = maybeBookingUidFromSeat.uid; + if (maybeBookingUidFromSeat.seatReferenceUid) seatReferenceUid = maybeBookingUidFromSeat.seatReferenceUid; const bookingInfoRaw = await prisma.booking.findFirst({ where: { - uid: maybeUid, + uid: uid, }, select: { title: true, @@ -168,7 +171,12 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; if (bookingInfo !== null && eventType.seatsPerTimeSlot) { - await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id); + await handleSeatsEventTypeOnBooking( + eventType, + bookingInfo, + seatReferenceUid, + session?.user.id === eventType.userId + ); } const payment = await prisma.payment.findFirst({ diff --git a/apps/web/modules/bookings/views/bookings-single-view.tsx b/apps/web/modules/bookings/views/bookings-single-view.tsx index 94347610b1..eb3ecfa145 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.tsx @@ -14,6 +14,7 @@ import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager"; import type { getEventLocationValue } from "@calcom/app-store/locations"; import { getSuccessPageLocationMessage, guessEventLocationType } from "@calcom/app-store/locations"; import { getEventTypeAppData } from "@calcom/app-store/utils"; +import type { nameObjectSchema } from "@calcom/core/event"; import { getEventName } from "@calcom/core/event"; import type { ConfigType } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; @@ -90,6 +91,7 @@ export default function Success(props: PageProps) { const routerQuery = useRouterQuery(); const pathname = usePathname(); const searchParams = useCompatSearchParams(); + const { eventType, bookingInfo, requiresLoginToUpdate } = props; const { allRemainingBookings, isSuccessBookingPage, @@ -99,35 +101,28 @@ export default function Success(props: PageProps) { seatReferenceUid, } = querySchema.parse(routerQuery); - const attendeeTimeZone = props?.bookingInfo?.attendees.find( - (attendee) => attendee.email === email - )?.timeZone; + const attendeeTimeZone = bookingInfo?.attendees.find((attendee) => attendee.email === email)?.timeZone; const tz = props.tz ? props.tz : isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : timeZone(); - const location = props.bookingInfo.location as ReturnType; + const location = bookingInfo.location as ReturnType; let rescheduleLocation: string | undefined; if ( - typeof props.bookingInfo.responses.location === "object" && - "optionValue" in props.bookingInfo.responses.location + typeof bookingInfo.responses?.location === "object" && + "optionValue" in bookingInfo.responses.location ) { - rescheduleLocation = props.bookingInfo.responses.location.optionValue; + rescheduleLocation = bookingInfo.responses.location.optionValue; } const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse( - props?.bookingInfo?.metadata || {} + bookingInfo?.metadata || {} )?.videoCallUrl; - const status = props.bookingInfo?.status; - const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED; - const cancellationReason = props.bookingInfo.cancellationReason || props.bookingInfo.rejectionReason; + const status = bookingInfo?.status; + const reschedule = bookingInfo.status === BookingStatus.ACCEPTED; + const cancellationReason = bookingInfo.cancellationReason || bookingInfo.rejectionReason; - const attendeeName = - typeof props?.bookingInfo?.attendees?.[0]?.name === "string" - ? props?.bookingInfo?.attendees?.[0]?.name - : "Nameless"; - - const attendees = props?.bookingInfo?.attendees; + const attendees = bookingInfo?.attendees; const isGmail = !!attendees.find((attendee) => attendee.email.includes("gmail.com")); @@ -136,15 +131,14 @@ export default function Success(props: PageProps) { ); const { data: session } = useSession(); - const [date, setDate] = useState(dayjs.utc(props.bookingInfo.startTime)); - const { eventType, bookingInfo } = props; + const [date, setDate] = useState(dayjs.utc(bookingInfo.startTime)); const isBackgroundTransparent = useIsBackgroundTransparent(); const isEmbed = useIsEmbed(); const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left"; const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed; const [calculatedDuration, setCalculatedDuration] = useState(undefined); - const { requiresLoginToUpdate } = props; + function setIsCancellationMode(value: boolean) { const _searchParams = new URLSearchParams(searchParams ?? undefined); @@ -159,13 +153,13 @@ export default function Success(props: PageProps) { router.replace(`${pathname}?${_searchParams.toString()}`); } - let evtName = props.eventType.eventName; + let evtName = eventType.eventName; if (eventType.isDynamic && bookingInfo.responses?.title) { evtName = bookingInfo.responses.title as string; } const eventNameObject = { - attendeeName, - eventType: props.eventType.title, + attendeeName: bookingInfo.responses.name as z.infer | string, + eventType: eventType.title, eventName: evtName, host: props.profile.name || "Nameless", location: location, @@ -223,9 +217,7 @@ export default function Success(props: PageProps) { }, [eventType, needsConfirmation]); useEffect(() => { - setCalculatedDuration( - dayjs(props.bookingInfo.endTime).diff(dayjs(props.bookingInfo.startTime), "minutes") - ); + setCalculatedDuration(dayjs(bookingInfo.endTime).diff(dayjs(bookingInfo.startTime), "minutes")); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -245,7 +237,7 @@ export default function Success(props: PageProps) { ], startInputType: "utc", title: eventName, - description: props.eventType.description ? props.eventType.description : undefined, + description: eventType.description ? eventType.description : undefined, /** formatted to required type of description ^ */ duration: { minutes: calculatedDuration, @@ -424,7 +416,7 @@ export default function Success(props: PageProps) { {reschedule && !!formerTime && (

)} + download={`${eventType.title}.ics`}> { where: { bookingId: booking.id }, select: { id: true, + name: true, + email: true, }, }); - const bookingSeats = [ - { bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() }, - { bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() }, - { bookingId: booking.id, attendeeId: bookingAttendees[2].id, referenceUid: uuidv4() }, - ]; + const bookingSeats = bookingAttendees.map((attendee) => ({ + bookingId: booking.id, + attendeeId: attendee.id, + referenceUid: uuidv4(), + data: { + responses: { + name: attendee.name, + email: attendee.email, + }, + }, + })); await prisma.bookingSeat.createMany({ data: bookingSeats, @@ -122,13 +130,22 @@ test.describe("Reschedule for booking with seats", () => { where: { bookingId: booking.id }, select: { id: true, + name: true, + email: true, }, }); - const bookingSeats = [ - { bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() }, - { bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() }, - ]; + const bookingSeats = bookingAttendees.map((attendee) => ({ + bookingId: booking.id, + attendeeId: attendee.id, + referenceUid: uuidv4(), + data: { + responses: { + name: attendee.name, + email: attendee.email, + }, + }, + })); await prisma.bookingSeat.createMany({ data: bookingSeats, @@ -181,13 +198,22 @@ test.describe("Reschedule for booking with seats", () => { where: { bookingId: booking.id }, select: { id: true, + name: true, + email: true, }, }); - const bookingSeats = [ - { bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() }, - { bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() }, - ]; + const bookingSeats = bookingAttendees.map((attendee) => ({ + bookingId: booking.id, + attendeeId: attendee.id, + referenceUid: uuidv4(), + data: { + responses: { + name: attendee.name, + email: attendee.email, + }, + }, + })); await prisma.bookingSeat.createMany({ data: bookingSeats, @@ -248,13 +274,22 @@ test.describe("Reschedule for booking with seats", () => { where: { bookingId: booking.id }, select: { id: true, + name: true, + email: true, }, }); - const bookingSeats = [ - { bookingId: booking.id, attendeeId: bookingAttendees[0].id, referenceUid: uuidv4() }, - { bookingId: booking.id, attendeeId: bookingAttendees[1].id, referenceUid: uuidv4() }, - ]; + const bookingSeats = bookingAttendees.map((attendee) => ({ + bookingId: booking.id, + attendeeId: attendee.id, + referenceUid: uuidv4(), + data: { + responses: { + name: attendee.name, + email: attendee.email, + }, + }, + })); await prisma.bookingSeat.createMany({ data: bookingSeats, @@ -350,7 +385,7 @@ test.describe("Reschedule for booking with seats", () => { // data displayed in form should be user owner const nameElement = await page.locator("input[name=name]"); const name = await nameElement.inputValue(); - expect(name).toBe(user.username); + expect(name).toBe(user.name); //same for email const emailElement = await page.locator("input[name=email]"); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index bbf998fa98..7289b8178d 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -293,6 +293,7 @@ export const createUserWithLimits = ({ async function createUserWithSeatedEvent(users: Fixtures["users"]) { const slug = "seats"; const user = await users.create({ + name: "Seated event user", eventTypes: [ { title: "Seated event", diff --git a/apps/web/playwright/webhook.e2e.ts b/apps/web/playwright/webhook.e2e.ts index e8c00780dd..10e3a042d3 100644 --- a/apps/web/playwright/webhook.e2e.ts +++ b/apps/web/playwright/webhook.e2e.ts @@ -478,6 +478,7 @@ test.describe("Webhook tests", async () => { select: { id: true, email: true, + name: true, }, }); @@ -485,6 +486,12 @@ test.describe("Webhook tests", async () => { bookingId: booking.id, attendeeId: attendee.id, referenceUid: uuidv4(), + data: { + responses: { + name: attendee.name, + email: attendee.email, + }, + }, })); await prisma.bookingSeat.createMany({ diff --git a/packages/core/event.ts b/packages/core/event.ts index 191dbe1ee3..512efb92e9 100644 --- a/packages/core/event.ts +++ b/packages/core/event.ts @@ -1,10 +1,23 @@ import type { TFunction } from "next-i18next"; +import z from "zod"; import { guessEventLocationType } from "@calcom/app-store/locations"; import type { Prisma } from "@calcom/prisma/client"; +export const nameObjectSchema = z.object({ + firstName: z.string(), + lastName: z.string().optional(), +}); + +function parseName(name: z.infer | string | undefined) { + if (typeof name === "string") return name; + else if (typeof name === "object" && nameObjectSchema.parse(name)) + return `${name.firstName} ${name.lastName}`.trim(); + else return "Nameless"; +} + export type EventNameObjectType = { - attendeeName: string; + attendeeName: z.infer | string; eventType: string; eventName?: string | null; teamName?: string | null; @@ -15,11 +28,13 @@ export type EventNameObjectType = { }; export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView = false) { + const attendeeName = parseName(eventNameObj.attendeeName); + if (!eventNameObj.eventName) return eventNameObj.t("event_between_users", { eventName: eventNameObj.eventType, host: eventNameObj.teamName || eventNameObj.host, - attendeeName: eventNameObj.attendeeName, + attendeeName, interpolation: { escapeValue: false, }, @@ -40,12 +55,12 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView let dynamicEventName = eventName // Need this for compatibility with older event names .replaceAll("{Event type title}", eventNameObj.eventType) - .replaceAll("{Scheduler}", eventNameObj.attendeeName) + .replaceAll("{Scheduler}", attendeeName) .replaceAll("{Organiser}", eventNameObj.host) - .replaceAll("{USER}", eventNameObj.attendeeName) - .replaceAll("{ATTENDEE}", eventNameObj.attendeeName) + .replaceAll("{USER}", attendeeName) + .replaceAll("{ATTENDEE}", attendeeName) .replaceAll("{HOST}", eventNameObj.host) - .replaceAll("{HOST/ATTENDEE}", forAttendeeView ? eventNameObj.host : eventNameObj.attendeeName); + .replaceAll("{HOST/ATTENDEE}", forAttendeeView ? eventNameObj.host : attendeeName); const customInputvariables = dynamicEventName.match(/\{(.+?)}/g)?.map((variable) => { return variable.replace("{", "").replace("}", ""); diff --git a/packages/features/bookings/lib/get-booking.ts b/packages/features/bookings/lib/get-booking.ts index bd7bcc727b..f4079951fd 100644 --- a/packages/features/bookings/lib/get-booking.ts +++ b/packages/features/bookings/lib/get-booking.ts @@ -45,7 +45,7 @@ function getResponsesFromOldBooking( }; } -async function getBooking(prisma: PrismaClient, uid: string) { +async function getBooking(prisma: PrismaClient, uid: string, isSeatedEvent?: boolean) { const rawBooking = await prisma.booking.findFirst({ where: { uid, @@ -79,7 +79,7 @@ async function getBooking(prisma: PrismaClient, uid: string) { return rawBooking; } - const booking = getBookingWithResponses(rawBooking); + const booking = getBookingWithResponses(rawBooking, isSeatedEvent); if (booking) { // @NOTE: had to do this because Server side cant return [Object objects] @@ -99,11 +99,14 @@ export const getBookingWithResponses = < }; }> >( - booking: T + booking: T, + isSeatedEvent?: boolean ) => { return { ...booking, - responses: bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)), + responses: isSeatedEvent + ? bookingResponsesDbSchema.parse(booking.responses || {}) + : bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)), } as Omit & { responses: z.infer }; }; @@ -140,6 +143,7 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => { // If no booking is found via the uid, it's probably a booking seat // that its being rescheduled, which we query next. let attendeeEmail: string | null = null; + let bookingSeatData: { description?: string; responses: Prisma.JsonValue } | null = null; if (!theBooking) { const bookingSeat = await prisma.bookingSeat.findFirst({ where: { @@ -153,6 +157,7 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => { email: true, }, }, + data: true, booking: { select: { uid: true, @@ -161,6 +166,7 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => { }, }); if (bookingSeat) { + bookingSeatData = bookingSeat.data as unknown as { description?: string; responses: Prisma.JsonValue }; bookingSeatReferenceUid = bookingSeat.id; rescheduleUid = bookingSeat.booking.uid; attendeeEmail = bookingSeat.attendee.email; @@ -185,10 +191,14 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => { // and we return null here. if (!theBooking && !rescheduleUid) return null; - const booking = await getBooking(prisma, rescheduleUid || uid); + const booking = await getBooking(prisma, rescheduleUid || uid, bookingSeatReferenceUid ? true : false); if (!booking) return null; + if (bookingSeatReferenceUid) { + booking["description"] = bookingSeatData?.description ?? null; + booking["responses"] = bookingResponsesDbSchema.parse(bookingSeatData?.responses ?? {}); + } return { ...booking, attendees: rescheduleUid diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 06300e0b52..92895fdb4d 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -727,11 +727,11 @@ async function createBooking({ const newBookingData: Prisma.BookingCreateInput = { uid, - responses: responses === null ? Prisma.JsonNull : responses, + responses: responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : responses, title: evt.title, startTime: dayjs.utc(evt.startTime).toDate(), endTime: dayjs.utc(evt.endTime).toDate(), - description: evt.additionalNotes, + description: evt.seatsPerTimeSlot ? null : evt.additionalNotes, customInputs: isPrismaObjOrUndefined(evt.customInputs), status: isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING, location: evt.location, @@ -1573,6 +1573,7 @@ async function handler( reqBodyMetadata: reqBody.metadata, subscriberOptions, eventTrigger, + responses, }); if (newBooking) { req.statusCode = 201; @@ -1658,7 +1659,8 @@ async function handler( data: { referenceUid: uniqueAttendeeId, data: { - description: evt.additionalNotes, + description: additionalNotes, + responses, }, booking: { connect: { diff --git a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts index 2b381a405c..39ab495e16 100644 --- a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts +++ b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts @@ -35,6 +35,7 @@ const createNewSeat = async ( organizerUser, fullName, bookerEmail, + responses, } = rescheduleSeatedBookingObject; let { evt } = rescheduleSeatedBookingObject; let resultBooking: HandleSeatsResultBooking; @@ -83,6 +84,7 @@ const createNewSeat = async ( referenceUid: attendeeUniqueId, data: { description: additionalNotes, + responses, }, booking: { connect: { diff --git a/packages/features/bookings/lib/handleSeats/handleSeats.ts b/packages/features/bookings/lib/handleSeats/handleSeats.ts index e34db97dc0..7fbde526e0 100644 --- a/packages/features/bookings/lib/handleSeats/handleSeats.ts +++ b/packages/features/bookings/lib/handleSeats/handleSeats.ts @@ -28,8 +28,9 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => { eventTypeId, subscriberOptions, eventTrigger, + evt, } = newSeatedBookingObject; - const { evt } = newSeatedBookingObject; + const loggerWithEventDetails = createLoggerWithEventDetails(eventType.id, reqBodyUser, eventType.slug); let resultBooking: HandleSeatsResultBooking = null; diff --git a/packages/features/bookings/lib/handleSeats/types.d.ts b/packages/features/bookings/lib/handleSeats/types.d.ts index 748767455e..b26ca2982c 100644 --- a/packages/features/bookings/lib/handleSeats/types.d.ts +++ b/packages/features/bookings/lib/handleSeats/types.d.ts @@ -1,4 +1,5 @@ import type { Prisma } from "@prisma/client"; +import type z from "zod"; import type { AppsStatus } from "@calcom/types/Calendar"; @@ -35,6 +36,7 @@ export type NewSeatedBookingObject = { reqBodyMetadata: ReqBodyMetadata; subscriberOptions: GetSubscriberOptions; eventTrigger: WebhookTriggerEvents; + responses: z.infer>["responses"] | null; }; export type RescheduleSeatedBookingObject = NewSeatedBookingObject & { rescheduleUid: string }; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 00d41f73d1..89a87ace2d 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -902,6 +902,7 @@ model BookingSeat { booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade) attendeeId Int @unique attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade) + /// @zod.custom(imports.bookingSeatDataSchema) data Json? @@index([bookingId]) diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index da4f327f55..46d1cb3b1e 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -662,3 +662,8 @@ export type ZVerifyCodeInputSchema = z.infer; export const coerceToDate = z.coerce.date(); export const getStringAsNumberRequiredSchema = (t: TFunction) => z.string().min(1, t("error_required_field")).pipe(z.coerce.number()); + +export const bookingSeatDataSchema = z.object({ + description: z.string().optional(), + responses: bookingResponses, +});