refactor: move functions to separate files (#15590)
* refactor: move functions to separate files * chore: remove inline function * chore: type error * fix: type error --------- Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
co-authored by
Keith Williams
parent
3e10e2db36
commit
a52f7efe88
@@ -1,6 +1,5 @@
|
||||
import type { App, DestinationCalendar, EventTypeCustomInput } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { cloneDeep } from "lodash";
|
||||
import type { NextApiRequest } from "next";
|
||||
@@ -8,11 +7,10 @@ import type { TFunction } from "next-i18next";
|
||||
import short, { uuid } from "short-uuid";
|
||||
import type { Logger } from "tslog";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import z from "zod";
|
||||
import type z from "zod";
|
||||
|
||||
import processExternalId from "@calcom/app-store/_utils/calendars/processExternalId";
|
||||
import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata";
|
||||
import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import {
|
||||
MeetLocationType,
|
||||
OrganizerDefaultConferencingAppType,
|
||||
@@ -55,12 +53,7 @@ import {
|
||||
deleteWebhookScheduledTriggers,
|
||||
scheduleTrigger,
|
||||
} from "@calcom/features/webhooks/lib/scheduleTrigger";
|
||||
import {
|
||||
isPrismaObjOrUndefined,
|
||||
parseBookingLimit,
|
||||
parseDurationLimit,
|
||||
parseRecurringEvent,
|
||||
} from "@calcom/lib";
|
||||
import { isPrismaObjOrUndefined, parseBookingLimit, parseDurationLimit } from "@calcom/lib";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import { getUTCOffsetByTimezone } from "@calcom/lib/date-fns";
|
||||
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
@@ -87,9 +80,7 @@ import type { BookingReference } from "@calcom/prisma/client";
|
||||
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import {
|
||||
EventTypeMetaDataSchema,
|
||||
bookingCreateSchemaLegacyPropsForApi,
|
||||
customInputSchema,
|
||||
userMetadata as userMetadataSchema,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import type {
|
||||
@@ -107,6 +98,12 @@ import { checkForConflicts } from "./conflictChecker/checkForConflicts";
|
||||
import { getAllCredentials } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
|
||||
import { refreshCredentials } from "./getAllCredentialsForUsersOnEvent/refreshCredentials";
|
||||
import getBookingDataSchema from "./getBookingDataSchema";
|
||||
import { checkIfBookerEmailIsBlocked } from "./handleNewBooking/checkIfBookerEmailIsBlocked";
|
||||
import { getEventTypesFromDB } from "./handleNewBooking/getEventTypesFromDB";
|
||||
import type { getEventTypeResponse } from "./handleNewBooking/getEventTypesFromDB";
|
||||
import { getRequiresConfirmationFlags } from "./handleNewBooking/getRequiresConfirmationFlags";
|
||||
import { handleAppsStatus } from "./handleNewBooking/handleAppsStatus";
|
||||
import { handleCustomInputs } from "./handleNewBooking/handleCustomInputs";
|
||||
import { loadUsers } from "./handleNewBooking/loadUsers";
|
||||
import handleSeats from "./handleSeats/handleSeats";
|
||||
import type { BookingSeat } from "./handleSeats/types";
|
||||
@@ -117,9 +114,7 @@ const log = logger.getSubLogger({ prefix: ["[api] book:user"] });
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
||||
export type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
||||
export type NewBookingEventType =
|
||||
| Awaited<ReturnType<typeof getDefaultEvent>>
|
||||
| Awaited<ReturnType<typeof getEventTypesFromDB>>;
|
||||
export type NewBookingEventType = Awaited<ReturnType<typeof getDefaultEvent>> | getEventTypeResponse;
|
||||
|
||||
// Work with Typescript to require reqBody.end
|
||||
type ReqBodyWithoutEnd = z.infer<ReturnType<typeof getBookingDataSchema>>;
|
||||
@@ -162,127 +157,6 @@ export interface IEventTypePaymentCredentialType {
|
||||
key: Prisma.JsonValue;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
type IsFixedAwareUser = User & {
|
||||
isFixed: boolean;
|
||||
credentials: CredentialPayload[];
|
||||
@@ -291,7 +165,7 @@ type IsFixedAwareUser = User & {
|
||||
};
|
||||
|
||||
export async function ensureAvailableUsers(
|
||||
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>> & {
|
||||
eventType: getEventTypeResponse & {
|
||||
users: IsFixedAwareUser[];
|
||||
},
|
||||
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
|
||||
@@ -479,7 +353,7 @@ export async function getBookingData<T extends z.ZodType>({
|
||||
schema,
|
||||
}: {
|
||||
req: NextApiRequest;
|
||||
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>;
|
||||
eventType: getEventTypeResponse;
|
||||
schema: T;
|
||||
}) {
|
||||
const reqBody = await schema.parseAsync(req.body);
|
||||
@@ -722,7 +596,7 @@ export function getCustomInputsResponses(
|
||||
responses?: Record<string, object>;
|
||||
customInputs?: z.infer<typeof bookingCreateSchemaLegacyPropsForApi>["customInputs"];
|
||||
},
|
||||
eventTypeCustomInputs: Awaited<ReturnType<typeof getEventTypesFromDB>>["customInputs"]
|
||||
eventTypeCustomInputs: getEventTypeResponse["customInputs"]
|
||||
) {
|
||||
const customInputsResponses = {} as NonNullable<CalendarEvent["customInputs"]>;
|
||||
if (reqBody.customInputs && (reqBody.customInputs.length || 0) > 0) {
|
||||
@@ -777,43 +651,6 @@ export const createLoggerWithEventDetails = (
|
||||
});
|
||||
};
|
||||
|
||||
export function handleAppsStatus(
|
||||
results: EventResult<AdditionalInformation>[],
|
||||
booking: (Booking & { appsStatus?: AppsStatus[] }) | null,
|
||||
reqAppsStatus: ReqAppsStatus
|
||||
) {
|
||||
// Taking care of apps status
|
||||
const resultStatus: AppsStatus[] = 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,
|
||||
}));
|
||||
|
||||
if (reqAppsStatus === undefined) {
|
||||
if (booking !== null) {
|
||||
booking.appsStatus = resultStatus;
|
||||
}
|
||||
return resultStatus;
|
||||
}
|
||||
// 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 calcAppsStatus = reqAppsStatus.concat(resultStatus).reduce((prev, curr) => {
|
||||
if (prev[curr.type]) {
|
||||
prev[curr.type].success += curr.success;
|
||||
prev[curr.type].errors = prev[curr.type].errors.concat(curr.errors);
|
||||
prev[curr.type].warnings = prev[curr.type].warnings?.concat(curr.warnings || []);
|
||||
} else {
|
||||
prev[curr.type] = curr;
|
||||
}
|
||||
return prev;
|
||||
}, {} as { [key: string]: AppsStatus });
|
||||
return Object.values(calcAppsStatus);
|
||||
}
|
||||
|
||||
function getICalSequence(originalRescheduledBooking: BookingType | null) {
|
||||
// If new booking set the sequence to 0
|
||||
if (!originalRescheduledBooking) {
|
||||
@@ -880,65 +717,6 @@ type BookingDataSchemaGetter =
|
||||
| typeof getBookingDataSchema
|
||||
| typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default;
|
||||
|
||||
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.`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: NextApiRequest & {
|
||||
userId?: number | undefined;
|
||||
@@ -1245,7 +1023,7 @@ async function handler(
|
||||
|
||||
//checks what users are available
|
||||
if (isFirstSeat) {
|
||||
const eventTypeWithUsers: Awaited<ReturnType<typeof getEventTypesFromDB>> & {
|
||||
const eventTypeWithUsers: getEventTypeResponse & {
|
||||
users: IsFixedAwareUser[];
|
||||
} = {
|
||||
...eventType,
|
||||
@@ -2540,77 +2318,3 @@ function getVideoCallDetails({
|
||||
|
||||
return { videoCallUrl, metadata, updatedVideoEvent };
|
||||
}
|
||||
|
||||
function getRequiresConfirmationFlags({
|
||||
eventType,
|
||||
bookingStartTime,
|
||||
userId,
|
||||
paymentAppData,
|
||||
originalRescheduledBookingOrganizerId,
|
||||
}: {
|
||||
eventType: Pick<Awaited<ReturnType<typeof getEventTypesFromDB>>, "metadata" | "requiresConfirmation">;
|
||||
bookingStartTime: string;
|
||||
userId: number | undefined;
|
||||
paymentAppData: { price: number };
|
||||
originalRescheduledBookingOrganizerId: number | undefined;
|
||||
}) {
|
||||
let requiresConfirmation = eventType?.requiresConfirmation;
|
||||
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
|
||||
if (rcThreshold) {
|
||||
if (dayjs(dayjs(bookingStartTime).utc().format()).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) {
|
||||
requiresConfirmation = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
const userReschedulingIsOwner = !!(userId && originalRescheduledBookingOrganizerId === userId);
|
||||
const isConfirmedByDefault = (!requiresConfirmation && !paymentAppData.price) || userReschedulingIsOwner;
|
||||
return {
|
||||
/**
|
||||
* Organizer of the booking is rescheduling
|
||||
*/
|
||||
userReschedulingIsOwner,
|
||||
/**
|
||||
* Booking won't need confirmation to be ACCEPTED
|
||||
*/
|
||||
isConfirmedByDefault,
|
||||
};
|
||||
}
|
||||
|
||||
function handleCustomInputs(
|
||||
eventTypeCustomInputs: EventTypeCustomInput[],
|
||||
reqCustomInputs: {
|
||||
value: string | boolean;
|
||||
label: string;
|
||||
}[]
|
||||
) {
|
||||
eventTypeCustomInputs.forEach((etcInput) => {
|
||||
if (etcInput.required) {
|
||||
const input = reqCustomInputs.find((i) => i.label === etcInput.label);
|
||||
if (etcInput.type === "BOOL") {
|
||||
z.literal(true, {
|
||||
errorMap: () => ({ message: `Missing ${etcInput.type} customInput: '${etcInput.label}'` }),
|
||||
}).parse(input?.value);
|
||||
} else if (etcInput.type === "PHONE") {
|
||||
z.string({
|
||||
errorMap: () => ({
|
||||
message: `Missing ${etcInput.type} customInput: '${etcInput.label}'`,
|
||||
}),
|
||||
})
|
||||
.refine((val) => isValidPhoneNumber(val), {
|
||||
message: "Phone number is invalid",
|
||||
})
|
||||
.parse(input?.value);
|
||||
} else {
|
||||
// type: NUMBER are also passed as string
|
||||
z.string({
|
||||
errorMap: () => ({ message: `Missing ${etcInput.type} customInput: '${etcInput.label}'` }),
|
||||
})
|
||||
.min(1)
|
||||
.parse(input?.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.`,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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>>;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+2
-1
@@ -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 (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
|
||||
|
||||
import type { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import type { getEventTypeResponse } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
|
||||
import { scheduleEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
||||
import type { BookingInfo } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
||||
import type { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
@@ -9,9 +9,7 @@ import { WorkflowTriggerEvents, TimeUnit, WorkflowActions, WorkflowTemplates } f
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[scheduleMandatoryReminder]"] });
|
||||
|
||||
export type NewBookingEventType =
|
||||
| Awaited<ReturnType<typeof getDefaultEvent>>
|
||||
| Awaited<ReturnType<typeof getEventTypesFromDB>>;
|
||||
export type NewBookingEventType = Awaited<ReturnType<typeof getDefaultEvent>> | getEventTypeResponse;
|
||||
|
||||
export async function scheduleMandatoryReminder(
|
||||
evt: BookingInfo,
|
||||
|
||||
@@ -8,11 +8,8 @@ import { createInstantMeetingWithCalVideo } from "@calcom/core/videoClient";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import getBookingDataSchema from "@calcom/features/bookings/lib/getBookingDataSchema";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import {
|
||||
getBookingData,
|
||||
getCustomInputsResponses,
|
||||
getEventTypesFromDB,
|
||||
} from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { getBookingData, getCustomInputsResponses } from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
|
||||
import { getFullName } from "@calcom/features/form-builder/utils";
|
||||
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { isPrismaObjOrUndefined } from "@calcom/lib";
|
||||
|
||||
Reference in New Issue
Block a user