Fix: Send responses in confirm booking flow (#7830)

This commit is contained in:
Hariom Balhara 2023-03-27 13:57:10 +05:30 committed by GitHub
parent 04c634ec4b
commit a8825badec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 150 additions and 67 deletions

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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();

View File

@ -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 };
};

View File

@ -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,

View File

@ -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 {

View File

@ -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(),

View File

@ -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 {