Skip to content

Commit

Permalink
refactor: handleNewBooking #4 (#15673)
Browse files Browse the repository at this point in the history
* refactor: handleNewBooking #3

* refactor: create booking factor

* refactor: handleNewBooking

* refactor: seats and rescheduleUId

* chore: remove comment

* fix: type err

* chore: add missing statement

* chore: use less params and other improvements

* chore: name

* chore: improvement

* fix: type err

* Typo fix

* chore: fix type err

* refactor: more readable

* refactor: improve code

* fix: conflicts

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
  • Loading branch information
4 people authored Oct 2, 2024
1 parent a99e285 commit b4f1b5a
Show file tree
Hide file tree
Showing 22 changed files with 620 additions and 481 deletions.
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 ({
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"]>);
}

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);
}
Loading

0 comments on commit b4f1b5a

Please sign in to comment.