Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: handleNewBooking #4 #15673

Merged
merged 20 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
394 changes: 94 additions & 300 deletions packages/features/bookings/lib/handleNewBooking.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { BookingReference } from "@calcom/prisma/client";
import type { CalendarEvent } from "@calcom/types/Calendar";

/** Updates the evt object with video call data found from booking references
*
* @param bookingReferences
* @param evt
*
* @returns updated evt with video call data
*/
export const addVideoCallDataToEvent = (bookingReferences: BookingReference[], evt: CalendarEvent) => {
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));

if (videoCallReference) {
evt.videoCallData = {
type: videoCallReference.type,
id: videoCallReference.meetingId,
password: videoCallReference?.meetingPassword,
url: videoCallReference.meetingUrl,
};
}

return evt;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import dayjs from "@calcom/dayjs";
import { checkBookingLimits, checkDurationLimits } from "@calcom/lib/server";
import type { IntervalLimit } from "@calcom/types/Calendar";

import type { NewBookingEventType } from "./types";

type EventType = Pick<NewBookingEventType, "bookingLimits" | "durationLimits" | "id" | "schedule">;

type InputProps = {
eventType: EventType;
reqBodyStart: string;
reqBodyRescheduleUid?: string;
};

export const checkBookingAndDurationLimits = async ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check both booking and duration limits.

eventType,
reqBodyStart,
reqBodyRescheduleUid,
}: InputProps) => {
if (
Object.prototype.hasOwnProperty.call(eventType, "bookingLimits") ||
Object.prototype.hasOwnProperty.call(eventType, "durationLimits")
) {
const startAsDate = dayjs(reqBodyStart).toDate();
if (eventType.bookingLimits && Object.keys(eventType.bookingLimits).length > 0) {
await checkBookingLimits(
eventType.bookingLimits as IntervalLimit,
startAsDate,
eventType.id,
reqBodyRescheduleUid,
eventType.schedule?.timeZone
);
}
if (eventType.durationLimits) {
await checkDurationLimits(
eventType.durationLimits as IntervalLimit,
startAsDate,
eventType.id,
reqBodyRescheduleUid
);
}
}
};
176 changes: 63 additions & 113 deletions packages/features/bookings/lib/handleNewBooking/createBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,40 @@ import type {
EventTypeId,
AwaitedBookingData,
NewBookingEventType,
IsConfirmedByDefault,
PaymentAppData,
OriginalRescheduledBooking,
AwaitedLoadUsers,
LoadedUsers,
} from "./types";

type ReqBodyWithEnd = TgetBookingDataSchema & { end: string };

type CreateBookingParams = {
originalRescheduledBooking: OriginalRescheduledBooking;
evt: CalendarEvent;
eventType: NewBookingEventType;
eventTypeId: EventTypeId;
eventTypeSlug: AwaitedBookingData["eventTypeSlug"];
reqBodyUser: ReqBodyWithEnd["user"];
reqBodyMetadata: ReqBodyWithEnd["metadata"];
reqBodyRecurringEventId: ReqBodyWithEnd["recurringEventId"];
uid: short.SUUID;
responses: ReqBodyWithEnd["responses"] | null;
isConfirmedByDefault: IsConfirmedByDefault;
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
organizerUser: AwaitedLoadUsers[number] & {
isFixed?: boolean;
metadata?: Prisma.JsonValue;
reqBody: {
user: ReqBodyWithEnd["user"];
metadata: ReqBodyWithEnd["metadata"];
recurringEventId: ReqBodyWithEnd["recurringEventId"];
};
eventType: {
eventTypeData: NewBookingEventType;
id: EventTypeId;
slug: AwaitedBookingData["eventTypeSlug"];
organizerUser: LoadedUsers[number] & {
isFixed?: boolean;
metadata?: Prisma.JsonValue;
};
isConfirmedByDefault: boolean;
paymentAppData: PaymentAppData;
};
input: {
bookerEmail: AwaitedBookingData["email"];
rescheduleReason: AwaitedBookingData["rescheduleReason"];
changedOrganizer: boolean;
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
responses: ReqBodyWithEnd["responses"] | null;
};
rescheduleReason: AwaitedBookingData["rescheduleReason"];
bookerEmail: AwaitedBookingData["email"];
paymentAppData: PaymentAppData;
changedOrganizer: boolean;
evt: CalendarEvent;
originalRescheduledBooking: OriginalRescheduledBooking;
};

function updateEventDetails(
Expand All @@ -57,52 +62,37 @@ function updateEventDetails(
}

export async function createBooking({
originalRescheduledBooking,
evt,
eventTypeId,
eventTypeSlug,
reqBodyUser,
reqBodyMetadata,
reqBodyRecurringEventId,
uid,
responses,
isConfirmedByDefault,
smsReminderNumber,
organizerUser,
rescheduleReason,
reqBody,
eventType,
bookerEmail,
paymentAppData,
changedOrganizer,
input,
evt,
originalRescheduledBooking,
}: CreateBookingParams) {
updateEventDetails(evt, originalRescheduledBooking, changedOrganizer);
updateEventDetails(evt, originalRescheduledBooking, input.changedOrganizer);

const newBookingData = buildNewBookingData({
uid,
reqBody,
eventType,
input,
evt,
responses,
isConfirmedByDefault,
reqBodyMetadata,
smsReminderNumber,
eventTypeSlug,
organizerUser,
reqBodyRecurringEventId,
originalRescheduledBooking,
bookerEmail,
rescheduleReason,
eventType,
eventTypeId,
reqBodyUser,
});

return await saveBooking(newBookingData, originalRescheduledBooking, paymentAppData, organizerUser);
return await saveBooking(
newBookingData,
originalRescheduledBooking,
eventType.paymentAppData,
eventType.organizerUser
);
}

async function saveBooking(
newBookingData: Prisma.BookingCreateInput,
originalRescheduledBooking: OriginalRescheduledBooking,
paymentAppData: PaymentAppData,
organizerUser: CreateBookingParams["organizerUser"]
organizerUser: CreateBookingParams["eventType"]["organizerUser"]
) {
const createBookingObj = {
include: {
Expand Down Expand Up @@ -143,93 +133,49 @@ function getEventTypeRel(eventTypeId: EventTypeId) {
function getAttendeesData(evt: Pick<CalendarEvent, "attendees" | "team">) {
//if attendee is team member, it should fetch their locale not booker's locale
//perhaps make email fetch request to see if his locale is stored, else
const attendees = evt.attendees.map((attendee) => ({
const teamMembers = evt?.team?.members ?? [];

return evt.attendees.concat(teamMembers).map((attendee) => ({
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
locale: attendee.language.locale,
phoneNumber: attendee.phoneNumber,
}));

if (evt.team?.members) {
attendees.push(
...evt.team.members.map((member) => ({
email: member.email,
name: member.name,
timeZone: member.timeZone,
locale: member.language.locale,
phoneNumber: member.phoneNumber,
}))
);
}

return attendees;
}

function buildNewBookingData(params: {
uid: short.SUUID;
evt: CalendarEvent;
responses: ReqBodyWithEnd["responses"] | null;
isConfirmedByDefault: IsConfirmedByDefault;
reqBodyMetadata: ReqBodyWithEnd["metadata"];
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
eventTypeSlug: AwaitedBookingData["eventTypeSlug"];
organizerUser: CreateBookingParams["organizerUser"];
reqBodyRecurringEventId: ReqBodyWithEnd["recurringEventId"];
originalRescheduledBooking: OriginalRescheduledBooking | null;
bookerEmail: AwaitedBookingData["email"];
rescheduleReason: AwaitedBookingData["rescheduleReason"];
eventType: NewBookingEventType;
eventTypeId: EventTypeId;
reqBodyUser: ReqBodyWithEnd["user"];
}): Prisma.BookingCreateInput {
const {
uid,
evt,
responses,
isConfirmedByDefault,
reqBodyMetadata,
smsReminderNumber,
eventTypeSlug,
organizerUser,
reqBodyRecurringEventId,
originalRescheduledBooking,
bookerEmail,
rescheduleReason,
eventType,
eventTypeId,
reqBodyUser,
} = params;
function buildNewBookingData(params: CreateBookingParams): Prisma.BookingCreateInput {
const { uid, evt, reqBody, eventType, input, originalRescheduledBooking } = params;

const attendeesData = getAttendeesData(evt);
const eventTypeRel = getEventTypeRel(eventTypeId);
const eventTypeRel = getEventTypeRel(eventType.id);

const newBookingData: Prisma.BookingCreateInput = {
uid,
userPrimaryEmail: evt.organizer.email,
responses: responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : responses,
responses: input.responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : input.responses,
title: evt.title,
startTime: dayjs.utc(evt.startTime).toDate(),
endTime: dayjs.utc(evt.endTime).toDate(),
description: evt.seatsPerTimeSlot ? null : evt.additionalNotes,
customInputs: isPrismaObjOrUndefined(evt.customInputs),
status: isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING,
status: eventType.isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING,
oneTimePassword: evt.oneTimePassword,
location: evt.location,
eventType: eventTypeRel,
smsReminderNumber,
metadata: reqBodyMetadata,
smsReminderNumber: input.smsReminderNumber,
metadata: reqBody.metadata,
attendees: {
createMany: {
data: attendeesData,
},
},
dynamicEventSlugRef: !eventTypeId ? eventTypeSlug : null,
dynamicGroupSlugRef: !eventTypeId ? (reqBodyUser as string).toLowerCase() : null,
dynamicEventSlugRef: !eventType.id ? eventType.slug : null,
dynamicGroupSlugRef: !eventType.id ? (reqBody.user as string).toLowerCase() : null,
iCalUID: evt.iCalUID ?? "",
user: {
connect: {
id: organizerUser.id,
id: eventType.organizerUser.id,
},
},
destinationCalendar:
Expand All @@ -240,24 +186,28 @@ function buildNewBookingData(params: {
: undefined,
};

if (reqBodyRecurringEventId) {
newBookingData.recurringEventId = reqBodyRecurringEventId;
if (reqBody.recurringEventId) {
newBookingData.recurringEventId = reqBody.recurringEventId;
}

if (originalRescheduledBooking) {
newBookingData.metadata = {
...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata),
...reqBodyMetadata,
...reqBody.metadata,
};
newBookingData.paid = originalRescheduledBooking.paid;
newBookingData.fromReschedule = originalRescheduledBooking.uid;
if (originalRescheduledBooking.uid) {
newBookingData.cancellationReason = rescheduleReason;
newBookingData.cancellationReason = input.rescheduleReason;
}
// Reschedule logic with booking with seats
if (newBookingData.attendees?.createMany?.data && eventType?.seatsPerTimeSlot && bookerEmail) {
if (
newBookingData.attendees?.createMany?.data &&
eventType?.eventTypeData?.seatsPerTimeSlot &&
input.bookerEmail
) {
newBookingData.attendees.createMany.data = attendeesData.filter(
(attendee) => attendee.email === bookerEmail
(attendee) => attendee.email === input.bookerEmail
);
}
if (originalRescheduledBooking.recurringEventId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type z from "zod";

import { slugify } from "@calcom/lib/slugify";
import type { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";

import type { getEventTypeResponse } from "./types";

type CustomInputs = z.infer<typeof bookingCreateSchemaLegacyPropsForApi>["customInputs"];

type RequestBody = {
responses?: Record<string, object>;
customInputs?: CustomInputs;
};

function mapCustomInputs(
customInputs: { label: string; value: CustomInputs[number]["value"] }[]
): Record<string, CustomInputs[number]["value"]> {
return customInputs.reduce((acc, { label, value }) => {
acc[label] = value;
return acc;
}, {} as Record<string, CustomInputs[number]["value"]>);
}

function mapResponsesToCustomInputs(
responses: Record<string, object>,
eventTypeCustomInputs: getEventTypeResponse["customInputs"]
): NonNullable<CalendarEvent["customInputs"]> {
// Backward Compatibility: Map new `responses` to old `customInputs` format so that webhooks can still receive same values.
return Object.entries(responses).reduce((acc, [fieldName, fieldValue]) => {
const foundInput = eventTypeCustomInputs.find((input) => slugify(input.label) === fieldName);
if (foundInput) {
acc[foundInput.label] = fieldValue;
}
return acc;
}, {} as NonNullable<CalendarEvent["customInputs"]>);
}
Comment on lines +30 to +37
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use .reduce method instead of for loop


export function getCustomInputsResponses(
reqBody: RequestBody,
eventTypeCustomInputs: getEventTypeResponse["customInputs"]
): NonNullable<CalendarEvent["customInputs"]> {
if (reqBody.customInputs && reqBody.customInputs.length > 0) {
return mapCustomInputs(reqBody.customInputs);
}

const responses = reqBody.responses || {};
return mapResponsesToCustomInputs(responses, eventTypeCustomInputs);
Comment on lines +39 to +48
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored this function

}
Loading
Loading