Skip to content
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
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.`,
});
}
};
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) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

No change in the code here

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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

created smaller functions

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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved the logic to validateInput

}
});
}

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