Skip to content

Commit

Permalink
refactor: move functions to separate files (#15590)
Browse files Browse the repository at this point in the history
* refactor: move functions to separate files

* chore: remove inline function

* chore: type error

* fix: type error
  • Loading branch information
Udit-takkar authored Jun 27, 2024
1 parent 03311de commit 3d06a2d
Show file tree
Hide file tree
Showing 9 changed files with 397 additions and 319 deletions.
322 changes: 13 additions & 309 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,62 @@
import { extractBaseEmail } from "@calcom/lib/extract-base-email";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";

export const checkIfBookerEmailIsBlocked = async ({
bookerEmail,
loggedInUserId,
}: {
bookerEmail: string;
loggedInUserId?: number;
}) => {
const baseEmail = extractBaseEmail(bookerEmail);
const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS
? process.env.BLACKLISTED_GUEST_EMAILS.split(",")
: [];

const blacklistedEmail = blacklistedGuestEmails.find(
(guestEmail: string) => guestEmail.toLowerCase() === baseEmail.toLowerCase()
);

if (!blacklistedEmail) {
return false;
}

const user = await prisma.user.findFirst({
where: {
OR: [
{
email: baseEmail,
emailVerified: {
not: null,
},
},
{
secondaryEmails: {
some: {
email: baseEmail,
emailVerified: {
not: null,
},
},
},
},
],
},
select: {
id: true,
email: true,
},
});

if (!user) {
throw new HttpError({ statusCode: 403, message: "Cannot use this email to create the booking." });
}

if (user.id !== loggedInUserId) {
throw new HttpError({
statusCode: 403,
message: `Attendee email has been blocked. Make sure to login as ${bookerEmail} to use this email for creating a booking.`,
});
}
};
129 changes: 129 additions & 0 deletions packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { LocationObject } from "@calcom/app-store/locations";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib";
import prisma, { userSelect } from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { EventTypeMetaDataSchema, customInputSchema } from "@calcom/prisma/zod-utils";

export const getEventTypesFromDB = async (eventTypeId: number) => {
const eventType = await prisma.eventType.findUniqueOrThrow({
where: {
id: eventTypeId,
},
select: {
id: true,
customInputs: true,
disableGuests: true,
users: {
select: {
credentials: {
select: credentialForCalendarServiceSelect,
},
...userSelect.select,
},
},
slug: true,
team: {
select: {
id: true,
name: true,
parentId: true,
},
},
bookingFields: true,
title: true,
length: true,
eventName: true,
schedulingType: true,
description: true,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
lockTimeZoneToggleOnBookingPage: true,
requiresConfirmation: true,
requiresBookerEmailVerification: true,
minimumBookingNotice: true,
userId: true,
price: true,
currency: true,
metadata: true,
destinationCalendar: true,
hideCalendarNotes: true,
seatsPerTimeSlot: true,
recurringEvent: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
bookingLimits: true,
durationLimits: true,
assignAllTeamMembers: true,
parentId: true,
useEventTypeDestinationCalendarEmail: true,
owner: {
select: {
hideBranding: true,
},
},
workflows: {
include: {
workflow: {
include: {
steps: true,
},
},
},
},
locations: true,
timeZone: true,
schedule: {
select: {
id: true,
availability: true,
timeZone: true,
},
},
hosts: {
select: {
isFixed: true,
priority: true,
user: {
select: {
credentials: {
select: credentialForCalendarServiceSelect,
},
...userSelect.select,
},
},
},
},
availability: {
select: {
date: true,
startTime: true,
endTime: true,
days: true,
},
},
secondaryEmailId: true,
secondaryEmail: {
select: {
id: true,
email: true,
},
},
},
});

return {
...eventType,
metadata: EventTypeMetaDataSchema.parse(eventType?.metadata || {}),
recurringEvent: parseRecurringEvent(eventType?.recurringEvent),
customInputs: customInputSchema.array().parse(eventType?.customInputs || []),
locations: (eventType?.locations ?? []) as LocationObject[],
bookingFields: getBookingFieldsWithSystemFields(eventType || {}),
isDynamic: false,
};
};

export type getEventTypeResponse = Awaited<ReturnType<typeof getEventTypesFromDB>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import dayjs from "@calcom/dayjs";

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

type EventType = Pick<getEventTypeResponse, "metadata" | "requiresConfirmation">;
type PaymentAppData = { price: number };

export function getRequiresConfirmationFlags({
eventType,
bookingStartTime,
userId,
paymentAppData,
originalRescheduledBookingOrganizerId,
}: {
eventType: EventType;
bookingStartTime: string;
userId: number | undefined;
paymentAppData: PaymentAppData;
originalRescheduledBookingOrganizerId: number | undefined;
}) {
const requiresConfirmation = determineRequiresConfirmation(eventType, bookingStartTime);
const userReschedulingIsOwner = isUserReschedulingOwner(userId, originalRescheduledBookingOrganizerId);
const isConfirmedByDefault = determineIsConfirmedByDefault(
requiresConfirmation,
paymentAppData.price,
userReschedulingIsOwner
);

return {
/**
* Organizer of the booking is rescheduling
*/
userReschedulingIsOwner,
/**
* Booking won't need confirmation to be ACCEPTED
*/
isConfirmedByDefault,
};
}

function determineRequiresConfirmation(eventType: EventType, bookingStartTime: string): boolean {
let requiresConfirmation = eventType?.requiresConfirmation;
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;

if (rcThreshold) {
const timeDifference = dayjs(dayjs(bookingStartTime).utc().format()).diff(dayjs(), rcThreshold.unit);
if (timeDifference > rcThreshold.time) {
requiresConfirmation = false;
}
}

return requiresConfirmation;
}

function isUserReschedulingOwner(
userId: number | undefined,
originalRescheduledBookingOrganizerId: number | undefined
): boolean {
// If the user is not the owner of the event, new booking should be always pending.
// Otherwise, an owner rescheduling should be always accepted.
// Before comparing make sure that userId is set, otherwise undefined === undefined
return !!(userId && originalRescheduledBookingOrganizerId === userId);
}

function determineIsConfirmedByDefault(
requiresConfirmation: boolean,
price: number,
userReschedulingIsOwner: boolean
): boolean {
return (!requiresConfirmation && price === 0) || userReschedulingIsOwner;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { AdditionalInformation, AppsStatus } from "@calcom/types/Calendar";
import type { EventResult } from "@calcom/types/EventManager";

import type { ReqAppsStatus, Booking } from "../handleNewBooking";

export function handleAppsStatus(
results: EventResult<AdditionalInformation>[],
booking: (Booking & { appsStatus?: AppsStatus[] }) | null,
reqAppsStatus: ReqAppsStatus
): AppsStatus[] {
const resultStatus = mapResultsToAppsStatus(results);

if (reqAppsStatus === undefined) {
return updateBookingWithStatus(booking, resultStatus);
}

return calculateAggregatedAppsStatus(reqAppsStatus, resultStatus);
}

function mapResultsToAppsStatus(results: EventResult<AdditionalInformation>[]): AppsStatus[] {
return results.map((app) => ({
appName: app.appName,
type: app.type,
success: app.success ? 1 : 0,
failures: !app.success ? 1 : 0,
errors: app.calError ? [app.calError] : [],
warnings: app.calWarnings,
}));
}

function updateBookingWithStatus(
booking: (Booking & { appsStatus?: AppsStatus[] }) | null,
resultStatus: AppsStatus[]
): AppsStatus[] {
if (booking !== null) {
booking.appsStatus = resultStatus;
}
return resultStatus;
}

function calculateAggregatedAppsStatus(
reqAppsStatus: NonNullable<ReqAppsStatus>,
resultStatus: AppsStatus[]
): AppsStatus[] {
// From down here we can assume reqAppsStatus is not undefined anymore
// Other status exist, so this is the last booking of a series,
// proceeding to prepare the info for the event
const aggregatedStatus = reqAppsStatus.concat(resultStatus).reduce((acc, curr) => {
if (acc[curr.type]) {
acc[curr.type].success += curr.success;
acc[curr.type].errors = acc[curr.type].errors.concat(curr.errors);
acc[curr.type].warnings = acc[curr.type].warnings?.concat(curr.warnings || []);
} else {
acc[curr.type] = curr;
}
return acc;
}, {} as { [key: string]: AppsStatus });

return Object.values(aggregatedStatus);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { EventTypeCustomInput } from "@prisma/client";
import { isValidPhoneNumber } from "libphonenumber-js";
import z from "zod";

type CustomInput = {
value: string | boolean;
label: string;
};

export function handleCustomInputs(
eventTypeCustomInputs: EventTypeCustomInput[],
reqCustomInputs: CustomInput[]
) {
eventTypeCustomInputs.forEach((etcInput) => {
if (etcInput.required) {
const input = reqCustomInputs.find((input) => input.label === etcInput.label);
validateInput(etcInput, input?.value);
}
});
}

function validateInput(etcInput: EventTypeCustomInput, value: string | boolean | undefined) {
const errorMessage = `Missing ${etcInput.type} customInput: '${etcInput.label}'`;

if (etcInput.type === "BOOL") {
validateBooleanInput(value, errorMessage);
} else if (etcInput.type === "PHONE") {
validatePhoneInput(value, errorMessage);
} else {
validateStringInput(value, errorMessage);
}
}

function validateBooleanInput(value: string | boolean | undefined, errorMessage: string) {
z.literal(true, {
errorMap: () => ({ message: errorMessage }),
}).parse(value);
}

function validatePhoneInput(value: string | boolean | undefined, errorMessage: string) {
z.string({
errorMap: () => ({ message: errorMessage }),
})
.refine((val) => isValidPhoneNumber(val), {
message: "Phone number is invalid",
})
.parse(value);
}

function validateStringInput(value: string | boolean | undefined, errorMessage: string) {
z.string({
errorMap: () => ({ message: errorMessage }),
})
.min(1)
.parse(value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { sendRescheduledEmails } from "@calcom/emails";
import prisma from "@calcom/prisma";
import type { AdditionalInformation, AppsStatus } from "@calcom/types/Calendar";

import { addVideoCallDataToEvent, handleAppsStatus, findBookingQuery } from "../../../handleNewBooking";
import { addVideoCallDataToEvent, findBookingQuery } from "../../../handleNewBooking";
import type { Booking, createLoggerWithEventDetails } from "../../../handleNewBooking";
import { handleAppsStatus } from "../../../handleNewBooking/handleAppsStatus";
import type { SeatedBooking, RescheduleSeatedBookingObject } from "../../types";

const moveSeatedBookingToNewTimeSlot = async (
Expand Down
Loading

0 comments on commit 3d06a2d

Please sign in to comment.