Fix: Send responses in confirm booking flow (#7830)
parent
04c634ec4b
commit
a8825badec
|
@ -1,9 +1,8 @@
|
|||
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
|
||||
type BookingSelect = {
|
||||
description: true;
|
||||
|
@ -45,11 +44,7 @@ function getResponsesFromOldBooking(
|
|||
};
|
||||
}
|
||||
|
||||
async function getBooking(
|
||||
prisma: PrismaClient,
|
||||
uid: string,
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">
|
||||
) {
|
||||
async function getBooking(prisma: PrismaClient, uid: string) {
|
||||
const rawBooking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid,
|
||||
|
@ -82,9 +77,7 @@ async function getBooking(
|
|||
return rawBooking;
|
||||
}
|
||||
|
||||
const booking = getBookingWithResponses(rawBooking, {
|
||||
bookingFields,
|
||||
});
|
||||
const booking = getBookingWithResponses(rawBooking);
|
||||
|
||||
if (booking) {
|
||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||
|
@ -104,20 +97,11 @@ export const getBookingWithResponses = <
|
|||
};
|
||||
}>
|
||||
>(
|
||||
booking: T,
|
||||
eventType: {
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
|
||||
}
|
||||
booking: T
|
||||
) => {
|
||||
return {
|
||||
...booking,
|
||||
responses: getBookingResponsesPartialSchema({
|
||||
eventType: {
|
||||
bookingFields: eventType.bookingFields,
|
||||
},
|
||||
// An existing booking can have data from any number of views, so the schema should consider ALL_VIEWS
|
||||
view: "ALL_VIEWS",
|
||||
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
|
||||
};
|
||||
responses: bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)),
|
||||
} as Omit<T, "responses"> & { responses: z.infer<typeof bookingResponsesDbSchema> };
|
||||
};
|
||||
export default getBooking;
|
||||
|
|
|
@ -237,11 +237,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
|
||||
booking = await getBooking(
|
||||
prisma,
|
||||
rescheduleUid || query.bookingUid || bookingUidWithSeats || "",
|
||||
eventTypeObject.bookingFields
|
||||
);
|
||||
booking = await getBooking(prisma, rescheduleUid || query.bookingUid || bookingUidWithSeats || "");
|
||||
}
|
||||
|
||||
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
|
||||
|
|
|
@ -1091,8 +1091,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
};
|
||||
}
|
||||
|
||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw);
|
||||
|
||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||
// probably fixable with json.stringify -> json.parse
|
||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||
|
|
|
@ -2,7 +2,6 @@ import type { GetServerSidePropsContext } from "next";
|
|||
|
||||
import type { LocationObject } from "@calcom/core/location";
|
||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getWorkingHours } from "@calcom/lib/availability";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
@ -177,7 +176,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBooking(prisma, rescheduleUid, getBookingFieldsWithSystemFields(eventTypeObject));
|
||||
booking = await getBooking(prisma, rescheduleUid);
|
||||
}
|
||||
|
||||
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
|
||||
|
|
|
@ -127,7 +127,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
let booking: GetBookingType | null = null;
|
||||
const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
|
||||
if (rescheduleUid || bookingUid) {
|
||||
booking = await getBooking(prisma, rescheduleUid || bookingUid || "", eventTypeObject.bookingFields);
|
||||
booking = await getBooking(prisma, rescheduleUid || bookingUid || "");
|
||||
}
|
||||
|
||||
// Checking if number of recurring event ocurrances is valid against event type configuration
|
||||
|
|
|
@ -9,6 +9,20 @@ type EventType = Parameters<typeof preprocess>[0]["eventType"];
|
|||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type View = ALL_VIEWS | (string & {});
|
||||
|
||||
export const bookingResponse = z.union([
|
||||
z.string(),
|
||||
z.boolean(),
|
||||
z.string().array(),
|
||||
z.object({
|
||||
optionValue: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const bookingResponsesDbSchema = z.record(bookingResponse);
|
||||
|
||||
const catchAllSchema = bookingResponsesDbSchema;
|
||||
|
||||
export const getBookingResponsesPartialSchema = ({
|
||||
eventType,
|
||||
view,
|
||||
|
@ -16,7 +30,7 @@ export const getBookingResponsesPartialSchema = ({
|
|||
eventType: EventType;
|
||||
view: View;
|
||||
}) => {
|
||||
const schema = bookingResponses.unwrap().partial().and(z.record(z.any()));
|
||||
const schema = bookingResponses.unwrap().partial().and(catchAllSchema);
|
||||
|
||||
return preprocess({ schema, eventType, isPartialSchema: true, view });
|
||||
};
|
||||
|
@ -38,9 +52,12 @@ function preprocess<T extends z.ZodType>({
|
|||
view: currentView,
|
||||
}: {
|
||||
schema: T;
|
||||
// It is useful when we want to prefill the responses with the partial values. Partial can be in 2 ways
|
||||
// - Not all required fields are need to be provided for prefill.
|
||||
// - Even a field response itself can be partial so the content isn't validated e.g. a field with type="phone" can be given a partial phone number(e.g. Specifying the country code like +91)
|
||||
isPartialSchema: boolean;
|
||||
eventType: {
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
|
||||
bookingFields: (z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">) | null;
|
||||
};
|
||||
view: View;
|
||||
}): z.ZodType<z.infer<T>, z.infer<T>, z.infer<T>> {
|
||||
|
@ -48,6 +65,8 @@ function preprocess<T extends z.ZodType>({
|
|||
(responses) => {
|
||||
const parsedResponses = z.record(z.any()).nullable().parse(responses) || {};
|
||||
const newResponses = {} as typeof parsedResponses;
|
||||
// if eventType has been deleted, we won't have bookingFields and thus we can't preprocess or validate them.
|
||||
if (!eventType.bookingFields) return parsedResponses;
|
||||
eventType.bookingFields.forEach((field) => {
|
||||
const value = parsedResponses[field.name];
|
||||
if (value === undefined) {
|
||||
|
@ -86,6 +105,10 @@ function preprocess<T extends z.ZodType>({
|
|||
return newResponses;
|
||||
},
|
||||
schema.superRefine((responses, ctx) => {
|
||||
if (!eventType.bookingFields) {
|
||||
// if eventType has been deleted, we won't have bookingFields and thus we can't validate the responses.
|
||||
return;
|
||||
}
|
||||
eventType.bookingFields.forEach((bookingField) => {
|
||||
const value = responses[bookingField.name];
|
||||
const stringSchema = z.string();
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import type z from "zod";
|
||||
|
||||
import { SystemField } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import type { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export const getCalEventResponses = ({
|
||||
bookingFields,
|
||||
responses,
|
||||
}: {
|
||||
// If the eventType has been deleted and a booking is Accepted later on, then bookingFields will be null and we can't know the label of fields. So, we should store the label as well in the DB
|
||||
// Also, it is no longer straightforward to identify if a field is system field or not
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields> | null;
|
||||
responses: z.infer<typeof bookingResponsesDbSchema>;
|
||||
}) => {
|
||||
const calEventUserFieldsResponses = {} as NonNullable<CalendarEvent["userFieldsResponses"]>;
|
||||
const calEventResponses = {} as NonNullable<CalendarEvent["responses"]>;
|
||||
|
||||
if (bookingFields) {
|
||||
bookingFields.forEach((field) => {
|
||||
const label = field.label || field.defaultLabel;
|
||||
if (!label) {
|
||||
throw new Error('Missing label for booking field "' + field.name + '"');
|
||||
}
|
||||
if (field.editable === "user" || field.editable === "user-readonly") {
|
||||
calEventUserFieldsResponses[field.name] = {
|
||||
label,
|
||||
value: responses[field.name],
|
||||
};
|
||||
}
|
||||
calEventResponses[field.name] = {
|
||||
label,
|
||||
value: responses[field.name],
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Alternative way to generate for a booking of whose eventType has been deleted
|
||||
for (const [name, value] of Object.entries(responses)) {
|
||||
const isSystemField = SystemField.safeParse(name);
|
||||
|
||||
// Use name for Label because we don't have access to the label. This will not be needed once we start storing the label along with the response
|
||||
const label = name;
|
||||
|
||||
if (!isSystemField.success) {
|
||||
calEventUserFieldsResponses[name] = {
|
||||
label,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
calEventResponses[name] = {
|
||||
label,
|
||||
value,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { calEventUserFieldsResponses, calEventResponses };
|
||||
};
|
|
@ -32,6 +32,7 @@ import {
|
|||
sendScheduledSeatsEmails,
|
||||
} from "@calcom/emails";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
||||
|
@ -403,23 +404,9 @@ function getBookingData({
|
|||
const reqBody = bookingDataSchema.parse(req.body);
|
||||
if ("responses" in reqBody) {
|
||||
const responses = reqBody.responses;
|
||||
const calEventResponses = {} as NonNullable<CalendarEvent["responses"]>;
|
||||
const calEventUserFieldsResponses = {} as NonNullable<CalendarEvent["userFieldsResponses"]>;
|
||||
eventType.bookingFields.forEach((field) => {
|
||||
const label = field.label || field.defaultLabel;
|
||||
if (!label) {
|
||||
throw new Error('Missing label for booking field "' + field.name + '"');
|
||||
}
|
||||
if (field.editable === "user" || field.editable === "user-readonly") {
|
||||
calEventUserFieldsResponses[field.name] = {
|
||||
label,
|
||||
value: responses[field.name],
|
||||
};
|
||||
}
|
||||
calEventResponses[field.name] = {
|
||||
label,
|
||||
value: responses[field.name],
|
||||
};
|
||||
const { calEventUserFieldsResponses, calEventResponses } = getCalEventResponses({
|
||||
bookingFields: eventType.bookingFields,
|
||||
responses,
|
||||
});
|
||||
return {
|
||||
...reqBody,
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import type z from "zod";
|
||||
|
||||
import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export default function getLabelValueMapFromResponses(calEvent: CalendarEvent) {
|
||||
const { customInputs, userFieldsResponses } = calEvent;
|
||||
|
||||
let labelValueMap: Record<string, string | string[]> = {};
|
||||
let labelValueMap: Record<string, z.infer<typeof bookingResponse>> = {};
|
||||
if (userFieldsResponses) {
|
||||
for (const [, value] of Object.entries(userFieldsResponses)) {
|
||||
if (!value.label) {
|
||||
continue;
|
||||
}
|
||||
labelValueMap[value.label] = value.value;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -15,6 +15,9 @@ import dayjs from "@calcom/dayjs";
|
|||
import { deleteScheduledEmailReminder } from "@calcom/ee/workflows/lib/reminders/emailReminderManager";
|
||||
import { deleteScheduledSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager";
|
||||
import { sendDeclinedEmails, sendLocationChangeEmails, sendRequestRescheduleEmail } from "@calcom/emails";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||
|
@ -717,7 +720,7 @@ export const bookingsRouter = router({
|
|||
|
||||
const tOrganizer = await getTranslation(user.locale ?? "en", "common");
|
||||
|
||||
const booking = await prisma.booking.findUniqueOrThrow({
|
||||
const bookingRaw = await prisma.booking.findUniqueOrThrow({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
|
@ -729,6 +732,7 @@ export const bookingsRouter = router({
|
|||
endTime: true,
|
||||
attendees: true,
|
||||
eventTypeId: true,
|
||||
responses: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -741,6 +745,9 @@ export const bookingsRouter = router({
|
|||
length: true,
|
||||
description: true,
|
||||
price: true,
|
||||
bookingFields: true,
|
||||
disableGuests: true,
|
||||
metadata: true,
|
||||
workflows: {
|
||||
include: {
|
||||
workflow: {
|
||||
|
@ -750,6 +757,7 @@ export const bookingsRouter = router({
|
|||
},
|
||||
},
|
||||
},
|
||||
customInputs: true,
|
||||
},
|
||||
},
|
||||
location: true,
|
||||
|
@ -765,6 +773,22 @@ export const bookingsRouter = router({
|
|||
scheduledJobs: true,
|
||||
},
|
||||
});
|
||||
|
||||
const bookingFields = bookingRaw.eventType
|
||||
? getBookingFieldsWithSystemFields(bookingRaw.eventType)
|
||||
: null;
|
||||
|
||||
const booking = {
|
||||
...bookingRaw,
|
||||
responses: bookingResponsesDbSchema.parse(bookingRaw.responses),
|
||||
eventType: bookingRaw.eventType
|
||||
? {
|
||||
...bookingRaw.eventType,
|
||||
bookingFields,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
const authorized = async () => {
|
||||
// if the organizer
|
||||
if (booking.userId === user.id) {
|
||||
|
@ -822,10 +846,18 @@ export const bookingsRouter = router({
|
|||
|
||||
const attendeesList = await Promise.all(attendeesListPromises);
|
||||
|
||||
// TODO: Remove the usage of `bookingFields` in computing responses. We can do that by storing `label` with the response. Also, this would allow us to correctly show the label for a field even after the Event Type has been deleted.
|
||||
const { calEventUserFieldsResponses, calEventResponses } = getCalEventResponses({
|
||||
bookingFields: booking.eventType?.bookingFields ?? null,
|
||||
responses: booking.responses,
|
||||
});
|
||||
|
||||
const evt: CalendarEvent = {
|
||||
type: booking.eventType?.title || booking.title,
|
||||
title: booking.title,
|
||||
description: booking.description,
|
||||
responses: calEventResponses,
|
||||
userFieldsResponses: calEventUserFieldsResponses,
|
||||
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
|
|
|
@ -3,7 +3,9 @@ import type { Dayjs } from "dayjs";
|
|||
import type { calendar_v3 } from "googleapis";
|
||||
import type { Time } from "ical.js";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import type z from "zod";
|
||||
|
||||
import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import type { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { Frequency } from "@calcom/prisma/zod-utils";
|
||||
|
@ -129,6 +131,14 @@ export type AppsStatus = {
|
|||
warnings?: string[];
|
||||
};
|
||||
|
||||
type CalEventResponses = Record<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
value: z.infer<typeof bookingResponse>;
|
||||
}
|
||||
>;
|
||||
|
||||
// If modifying this interface, probably should update builders/calendarEvent files
|
||||
export interface CalendarEvent {
|
||||
type: string;
|
||||
|
@ -164,22 +174,10 @@ export interface CalendarEvent {
|
|||
seatsPerTimeSlot?: number | null;
|
||||
|
||||
// It has responses to all the fields(system + user)
|
||||
responses?: Record<
|
||||
string,
|
||||
{
|
||||
value: string | string[];
|
||||
label: string;
|
||||
}
|
||||
> | null;
|
||||
responses?: CalEventResponses | null;
|
||||
|
||||
// It just has responses to only the user fields. It allows to easily iterate over to show only user fields
|
||||
userFieldsResponses?: Record<
|
||||
string,
|
||||
{
|
||||
value: string | string[];
|
||||
label: string;
|
||||
}
|
||||
> | null;
|
||||
userFieldsResponses?: CalEventResponses | null;
|
||||
}
|
||||
|
||||
export interface EntryPoint {
|
||||
|
|
Loading…
Reference in New Issue