* refactor: handleNewBooking #3 * fix: type error * fix: type err * fix: type err * refactor: create booking factor * chore: refactor availusers * chore: type err --------- Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
co-authored by
Hariom Balhara
Keith Williams
parent
4a2a2d165b
commit
5f8d090d3c
@@ -17,4 +17,6 @@ const getBookingDataSchema = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type TgetBookingDataSchema = z.infer<ReturnType<typeof getBookingDataSchema>>;
|
||||
|
||||
export default getBookingDataSchema;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { App, DestinationCalendar, EventTypeCustomInput } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { DestinationCalendar } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { cloneDeep } from "lodash";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import short, { uuid } from "short-uuid";
|
||||
import type { Logger } from "tslog";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import type z from "zod";
|
||||
|
||||
@@ -16,12 +14,9 @@ import {
|
||||
OrganizerDefaultConferencingAppType,
|
||||
getLocationValueForDB,
|
||||
} from "@calcom/app-store/locations";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||
import EventManager from "@calcom/core/EventManager";
|
||||
import { getEventName } from "@calcom/core/event";
|
||||
import { getBusyTimesForLimitChecks } from "@calcom/core/getBusyTimes";
|
||||
import { getUsersAvailability } from "@calcom/core/getUserAvailability";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/scheduleMandatoryReminder";
|
||||
import {
|
||||
@@ -35,7 +30,6 @@ import {
|
||||
} from "@calcom/emails";
|
||||
import getICalUID from "@calcom/emails/lib/getICalUID";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
|
||||
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
|
||||
import {
|
||||
@@ -50,7 +44,6 @@ import {
|
||||
deleteWebhookScheduledTriggers,
|
||||
scheduleTrigger,
|
||||
} from "@calcom/features/webhooks/lib/scheduleTrigger";
|
||||
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";
|
||||
@@ -76,10 +69,8 @@ import prisma, { userSelect } from "@calcom/prisma";
|
||||
import type { BookingReference } from "@calcom/prisma/client";
|
||||
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import {
|
||||
bookingCreateSchemaLegacyPropsForApi,
|
||||
userMetadata as userMetadataSchema,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import type { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod-utils";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import {
|
||||
deleteAllWorkflowReminders,
|
||||
getAllWorkflowsFromEventType,
|
||||
@@ -91,507 +82,35 @@ import type {
|
||||
IntervalLimit,
|
||||
Person,
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
||||
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 { createBooking } from "./handleNewBooking/createBooking";
|
||||
import { ensureAvailableUsers } from "./handleNewBooking/ensureAvailableUsers";
|
||||
import { getBookingData } from "./handleNewBooking/getBookingData";
|
||||
import { getEventTypesFromDB } from "./handleNewBooking/getEventTypesFromDB";
|
||||
import type { getEventTypeResponse } from "./handleNewBooking/getEventTypesFromDB";
|
||||
import { getOriginalRescheduledBooking } from "./handleNewBooking/getOriginalRescheduledBooking";
|
||||
import { getRequiresConfirmationFlags } from "./handleNewBooking/getRequiresConfirmationFlags";
|
||||
import { handleAppsStatus } from "./handleNewBooking/handleAppsStatus";
|
||||
import { handleCustomInputs } from "./handleNewBooking/handleCustomInputs";
|
||||
import { loadUsers } from "./handleNewBooking/loadUsers";
|
||||
import type {
|
||||
Invitee,
|
||||
IEventTypePaymentCredentialType,
|
||||
IsFixedAwareUser,
|
||||
BookingType,
|
||||
Booking,
|
||||
} from "./handleNewBooking/types";
|
||||
import handleSeats from "./handleSeats/handleSeats";
|
||||
import type { BookingSeat } from "./handleSeats/types";
|
||||
|
||||
const translator = short();
|
||||
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>> | getEventTypeResponse;
|
||||
|
||||
// Work with Typescript to require reqBody.end
|
||||
type ReqBodyWithoutEnd = z.infer<ReturnType<typeof getBookingDataSchema>>;
|
||||
type ReqBodyWithEnd = ReqBodyWithoutEnd & { end: string };
|
||||
export type Invitee = {
|
||||
email: string;
|
||||
name: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
timeZone: string;
|
||||
language: {
|
||||
translate: TFunction;
|
||||
locale: string;
|
||||
};
|
||||
}[];
|
||||
export type OrganizerUser = Awaited<ReturnType<typeof loadUsers>>[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
};
|
||||
export type OriginalRescheduledBooking = Awaited<ReturnType<typeof getOriginalRescheduledBooking>>;
|
||||
|
||||
type AwaitedBookingData = Awaited<ReturnType<typeof getBookingData>>;
|
||||
export type RescheduleReason = AwaitedBookingData["rescheduleReason"];
|
||||
export type NoEmail = AwaitedBookingData["noEmail"];
|
||||
export type AdditionalNotes = AwaitedBookingData["notes"];
|
||||
export type ReqAppsStatus = AwaitedBookingData["appsStatus"];
|
||||
export type SmsReminderNumber = AwaitedBookingData["smsReminderNumber"];
|
||||
export type EventTypeId = AwaitedBookingData["eventTypeId"];
|
||||
export type ReqBodyMetadata = ReqBodyWithEnd["metadata"];
|
||||
|
||||
export type IsConfirmedByDefault = ReturnType<typeof getRequiresConfirmationFlags>["isConfirmedByDefault"];
|
||||
export type PaymentAppData = ReturnType<typeof getPaymentAppData>;
|
||||
|
||||
export interface IEventTypePaymentCredentialType {
|
||||
appId: EventTypeAppsList;
|
||||
app: {
|
||||
categories: App["categories"];
|
||||
dirName: string;
|
||||
};
|
||||
key: Prisma.JsonValue;
|
||||
}
|
||||
|
||||
type IsFixedAwareUser = User & {
|
||||
isFixed: boolean;
|
||||
credentials: CredentialPayload[];
|
||||
organization: { slug: string };
|
||||
priority?: number;
|
||||
};
|
||||
|
||||
export async function ensureAvailableUsers(
|
||||
eventType: getEventTypeResponse & {
|
||||
users: IsFixedAwareUser[];
|
||||
},
|
||||
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
|
||||
loggerWithEventDetails: Logger<unknown>
|
||||
) {
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
const getStartDateTimeUtc = (startDateTimeInput: string, timeZone?: string) => {
|
||||
return timeZone === "Etc/GMT"
|
||||
? dayjs.utc(startDateTimeInput)
|
||||
: dayjs(startDateTimeInput).tz(timeZone).utc();
|
||||
};
|
||||
|
||||
const startDateTimeUtc = getStartDateTimeUtc(input.dateFrom, input.timeZone);
|
||||
const endDateTimeUtc =
|
||||
input.timeZone === "Etc/GMT" ? dayjs.utc(input.dateTo) : dayjs(input.dateTo).tz(input.timeZone).utc();
|
||||
|
||||
const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute");
|
||||
const originalBookingDuration = input.originalRescheduledBooking
|
||||
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
||||
dayjs(input.originalRescheduledBooking.startTime),
|
||||
"minutes"
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
|
||||
const durationLimits = parseDurationLimit(eventType?.durationLimits);
|
||||
let busyTimesFromLimitsBookingsAllUsers: Awaited<ReturnType<typeof getBusyTimesForLimitChecks>> = [];
|
||||
|
||||
if (eventType && (bookingLimits || durationLimits)) {
|
||||
busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({
|
||||
userIds: eventType.users.map((u) => u.id),
|
||||
eventTypeId: eventType.id,
|
||||
startDate: startDateTimeUtc.format(),
|
||||
endDate: endDateTimeUtc.format(),
|
||||
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
});
|
||||
}
|
||||
|
||||
(
|
||||
await getUsersAvailability({
|
||||
users: eventType.users,
|
||||
query: {
|
||||
...input,
|
||||
eventTypeId: eventType.id,
|
||||
duration: originalBookingDuration,
|
||||
returnDateOverrides: false,
|
||||
dateFrom: startDateTimeUtc.format(),
|
||||
dateTo: endDateTimeUtc.format(),
|
||||
},
|
||||
initialData: {
|
||||
eventType,
|
||||
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
|
||||
busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers,
|
||||
},
|
||||
})
|
||||
).forEach(({ oooExcludedDateRanges: dateRanges, busy: bufferedBusyTimes }, index) => {
|
||||
const user = eventType.users[index];
|
||||
|
||||
log.debug(
|
||||
"calendarBusyTimes==>>>",
|
||||
JSON.stringify({ bufferedBusyTimes, dateRanges, isRecurringEvent: eventType.recurringEvent })
|
||||
);
|
||||
|
||||
if (!dateRanges.length) {
|
||||
loggerWithEventDetails.error(
|
||||
`User does not have availability at this time.`,
|
||||
safeStringify({
|
||||
startDateTimeUtc,
|
||||
endDateTimeUtc,
|
||||
input,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let foundConflict = false;
|
||||
|
||||
let dateRangeForBooking = false;
|
||||
|
||||
//check if event time is within the date range
|
||||
for (const dateRange of dateRanges) {
|
||||
if (
|
||||
(startDateTimeUtc.isAfter(dateRange.start) || startDateTimeUtc.isSame(dateRange.start)) &&
|
||||
(endDateTimeUtc.isBefore(dateRange.end) || endDateTimeUtc.isSame(dateRange.end))
|
||||
) {
|
||||
dateRangeForBooking = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dateRangeForBooking) {
|
||||
loggerWithEventDetails.error(
|
||||
`No date range for booking.`,
|
||||
safeStringify({
|
||||
startDateTimeUtc,
|
||||
endDateTimeUtc,
|
||||
input,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
foundConflict = checkForConflicts(bufferedBusyTimes, startDateTimeUtc, duration);
|
||||
} catch (error) {
|
||||
loggerWithEventDetails.error("Unable set isAvailableToBeBooked. Using true. ", error);
|
||||
}
|
||||
// no conflicts found, add to available users.
|
||||
if (!foundConflict) {
|
||||
availableUsers.push(user);
|
||||
}
|
||||
});
|
||||
|
||||
if (!availableUsers.length) {
|
||||
loggerWithEventDetails.error(
|
||||
`No available users found.`,
|
||||
safeStringify({
|
||||
startDateTimeUtc,
|
||||
endDateTimeUtc,
|
||||
input,
|
||||
})
|
||||
);
|
||||
throw new Error(ErrorCode.NoAvailableUsersFound);
|
||||
}
|
||||
return availableUsers;
|
||||
}
|
||||
|
||||
async function getOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
|
||||
return prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: uid,
|
||||
status: {
|
||||
in: [BookingStatus.ACCEPTED, BookingStatus.CANCELLED, BookingStatus.PENDING],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
timeZone: true,
|
||||
...(seatsEventType && { bookingSeat: true, id: true }),
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
timeZone: true,
|
||||
destinationCalendar: true,
|
||||
credentials: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
key: true,
|
||||
type: true,
|
||||
teamId: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destinationCalendar: true,
|
||||
payment: true,
|
||||
references: true,
|
||||
workflowReminders: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getBookingData<T extends z.ZodType>({
|
||||
req,
|
||||
eventType,
|
||||
schema,
|
||||
}: {
|
||||
req: NextApiRequest;
|
||||
eventType: getEventTypeResponse;
|
||||
schema: T;
|
||||
}) {
|
||||
const reqBody = await schema.parseAsync(req.body);
|
||||
const reqBodyWithEnd = (reqBody: ReqBodyWithoutEnd): reqBody is ReqBodyWithEnd => {
|
||||
// Use the event length to auto-set the event end time.
|
||||
if (!Object.prototype.hasOwnProperty.call(reqBody, "end")) {
|
||||
reqBody.end = dayjs.utc(reqBody.start).add(eventType.length, "minutes").format();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!reqBodyWithEnd(reqBody)) {
|
||||
throw new Error(ErrorCode.RequestBodyWithouEnd);
|
||||
}
|
||||
// reqBody.end is no longer an optional property.
|
||||
if (reqBody.customInputs) {
|
||||
// Check if required custom inputs exist
|
||||
handleCustomInputs(eventType.customInputs as EventTypeCustomInput[], reqBody.customInputs);
|
||||
const reqBodyWithLegacyProps = bookingCreateSchemaLegacyPropsForApi.parse(reqBody);
|
||||
return {
|
||||
...reqBody,
|
||||
name: reqBodyWithLegacyProps.name,
|
||||
email: reqBodyWithLegacyProps.email,
|
||||
guests: reqBodyWithLegacyProps.guests,
|
||||
location: reqBodyWithLegacyProps.location || "",
|
||||
smsReminderNumber: reqBodyWithLegacyProps.smsReminderNumber,
|
||||
notes: reqBodyWithLegacyProps.notes,
|
||||
rescheduleReason: reqBodyWithLegacyProps.rescheduleReason,
|
||||
// So TS doesn't complain about unknown properties
|
||||
calEventUserFieldsResponses: undefined,
|
||||
calEventResponses: undefined,
|
||||
customInputs: undefined,
|
||||
};
|
||||
}
|
||||
if (!reqBody.responses) {
|
||||
throw new Error("`responses` must not be nullish");
|
||||
}
|
||||
const responses = reqBody.responses;
|
||||
|
||||
const { userFieldsResponses: calEventUserFieldsResponses, responses: calEventResponses } =
|
||||
getCalEventResponses({
|
||||
bookingFields: eventType.bookingFields,
|
||||
responses,
|
||||
});
|
||||
return {
|
||||
...reqBody,
|
||||
name: responses.name,
|
||||
email: responses.email,
|
||||
guests: responses.guests ? responses.guests : [],
|
||||
location: responses.location?.optionValue || responses.location?.value || "",
|
||||
smsReminderNumber: responses.smsReminderNumber,
|
||||
notes: responses.notes || "",
|
||||
calEventUserFieldsResponses,
|
||||
rescheduleReason: responses.rescheduleReason,
|
||||
calEventResponses,
|
||||
// So TS doesn't complain about unknown properties
|
||||
customInputs: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function createBooking({
|
||||
originalRescheduledBooking,
|
||||
evt,
|
||||
eventTypeId,
|
||||
eventTypeSlug,
|
||||
reqBodyUser,
|
||||
reqBodyMetadata,
|
||||
reqBodyRecurringEventId,
|
||||
uid,
|
||||
responses,
|
||||
isConfirmedByDefault,
|
||||
smsReminderNumber,
|
||||
organizerUser,
|
||||
rescheduleReason,
|
||||
eventType,
|
||||
bookerEmail,
|
||||
paymentAppData,
|
||||
changedOrganizer,
|
||||
}: {
|
||||
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: Awaited<ReturnType<typeof loadUsers>>[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
};
|
||||
rescheduleReason: Awaited<ReturnType<typeof getBookingData>>["rescheduleReason"];
|
||||
bookerEmail: Awaited<ReturnType<typeof getBookingData>>["email"];
|
||||
paymentAppData: ReturnType<typeof getPaymentAppData>;
|
||||
changedOrganizer: boolean;
|
||||
}) {
|
||||
if (originalRescheduledBooking) {
|
||||
evt.title = originalRescheduledBooking?.title || evt.title;
|
||||
evt.description = originalRescheduledBooking?.description || evt.description;
|
||||
evt.location = originalRescheduledBooking?.location || evt.location;
|
||||
evt.location = changedOrganizer ? evt.location : originalRescheduledBooking?.location || evt.location;
|
||||
}
|
||||
|
||||
const eventTypeRel = !eventTypeId
|
||||
? {}
|
||||
: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
};
|
||||
|
||||
const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null;
|
||||
const dynamicGroupSlugRef = !eventTypeId ? (reqBodyUser as string).toLowerCase() : null;
|
||||
|
||||
const attendeesData = evt.attendees.map((attendee) => {
|
||||
//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
|
||||
return {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
locale: attendee.language.locale,
|
||||
};
|
||||
});
|
||||
|
||||
if (evt.team?.members) {
|
||||
attendeesData.push(
|
||||
...evt.team.members.map((member) => ({
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
timeZone: member.timeZone,
|
||||
locale: member.language.locale,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const newBookingData: Prisma.BookingCreateInput = {
|
||||
uid,
|
||||
userPrimaryEmail: evt.organizer.email,
|
||||
responses: responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : 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,
|
||||
location: evt.location,
|
||||
eventType: eventTypeRel,
|
||||
smsReminderNumber,
|
||||
metadata: reqBodyMetadata,
|
||||
attendees: {
|
||||
createMany: {
|
||||
data: attendeesData,
|
||||
},
|
||||
},
|
||||
dynamicEventSlugRef,
|
||||
dynamicGroupSlugRef,
|
||||
iCalUID: evt.iCalUID ?? "",
|
||||
user: {
|
||||
connect: {
|
||||
id: organizerUser.id,
|
||||
},
|
||||
},
|
||||
destinationCalendar:
|
||||
evt.destinationCalendar && evt.destinationCalendar.length > 0
|
||||
? {
|
||||
connect: { id: evt.destinationCalendar[0].id },
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (reqBodyRecurringEventId) {
|
||||
newBookingData.recurringEventId = reqBodyRecurringEventId;
|
||||
}
|
||||
if (originalRescheduledBooking) {
|
||||
newBookingData.metadata = {
|
||||
...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata),
|
||||
};
|
||||
newBookingData["paid"] = originalRescheduledBooking.paid;
|
||||
newBookingData["fromReschedule"] = originalRescheduledBooking.uid;
|
||||
if (originalRescheduledBooking.uid) {
|
||||
newBookingData.cancellationReason = rescheduleReason;
|
||||
}
|
||||
if (newBookingData.attendees?.createMany?.data) {
|
||||
// Reschedule logic with booking with seats
|
||||
if (eventType?.seatsPerTimeSlot && bookerEmail) {
|
||||
newBookingData.attendees.createMany.data = attendeesData.filter(
|
||||
(attendee) => attendee.email === bookerEmail
|
||||
);
|
||||
}
|
||||
}
|
||||
if (originalRescheduledBooking.recurringEventId) {
|
||||
newBookingData.recurringEventId = originalRescheduledBooking.recurringEventId;
|
||||
}
|
||||
}
|
||||
|
||||
const createBookingObj = {
|
||||
include: {
|
||||
user: {
|
||||
select: { email: true, name: true, timeZone: true, username: true },
|
||||
},
|
||||
attendees: true,
|
||||
payment: true,
|
||||
references: true,
|
||||
},
|
||||
data: newBookingData,
|
||||
};
|
||||
|
||||
if (originalRescheduledBooking?.paid && originalRescheduledBooking?.payment) {
|
||||
const bookingPayment = originalRescheduledBooking?.payment?.find((payment) => payment.success);
|
||||
|
||||
if (bookingPayment) {
|
||||
createBookingObj.data.payment = {
|
||||
connect: { id: bookingPayment.id },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof paymentAppData.price === "number" && paymentAppData.price > 0) {
|
||||
/* Validate if there is any payment app credential for this user */
|
||||
await prisma.credential.findFirstOrThrow({
|
||||
where: {
|
||||
appId: paymentAppData.appId,
|
||||
...(paymentAppData.credentialId ? { id: paymentAppData.credentialId } : { userId: organizerUser.id }),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return prisma.booking.create(createBookingObj);
|
||||
}
|
||||
|
||||
export function getCustomInputsResponses(
|
||||
reqBody: {
|
||||
responses?: Record<string, object>;
|
||||
@@ -667,53 +186,6 @@ function getICalSequence(originalRescheduledBooking: BookingType | null) {
|
||||
return originalRescheduledBooking.iCalSequence + 1;
|
||||
}
|
||||
|
||||
export const findBookingQuery = async (bookingId: number) => {
|
||||
const foundBooking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
location: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
description: true,
|
||||
status: true,
|
||||
responses: true,
|
||||
metadata: true,
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
select: {
|
||||
title: true,
|
||||
description: true,
|
||||
currency: true,
|
||||
length: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
price: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// This should never happen but it's just typescript safe
|
||||
if (!foundBooking) {
|
||||
throw new Error("Internal Error. Couldn't find booking");
|
||||
}
|
||||
|
||||
// Don't leak any sensitive data
|
||||
return foundBooking;
|
||||
};
|
||||
|
||||
type BookingDataSchemaGetter =
|
||||
| typeof getBookingDataSchema
|
||||
| typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default;
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type short from "short-uuid";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { isPrismaObjOrUndefined } from "@calcom/lib";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import type { TgetBookingDataSchema } from "../getBookingDataSchema";
|
||||
import type {
|
||||
EventTypeId,
|
||||
AwaitedBookingData,
|
||||
NewBookingEventType,
|
||||
IsConfirmedByDefault,
|
||||
PaymentAppData,
|
||||
OriginalRescheduledBooking,
|
||||
AwaitedLoadUsers,
|
||||
} 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;
|
||||
};
|
||||
rescheduleReason: AwaitedBookingData["rescheduleReason"];
|
||||
bookerEmail: AwaitedBookingData["email"];
|
||||
paymentAppData: PaymentAppData;
|
||||
changedOrganizer: boolean;
|
||||
};
|
||||
|
||||
function updateEventDetails(
|
||||
evt: CalendarEvent,
|
||||
originalRescheduledBooking: OriginalRescheduledBooking | null,
|
||||
changedOrganizer: boolean
|
||||
) {
|
||||
if (originalRescheduledBooking) {
|
||||
evt.title = originalRescheduledBooking?.title || evt.title;
|
||||
evt.description = originalRescheduledBooking?.description || evt.description;
|
||||
evt.location = originalRescheduledBooking?.location || evt.location;
|
||||
evt.location = changedOrganizer ? evt.location : originalRescheduledBooking?.location || evt.location;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createBooking({
|
||||
originalRescheduledBooking,
|
||||
evt,
|
||||
eventTypeId,
|
||||
eventTypeSlug,
|
||||
reqBodyUser,
|
||||
reqBodyMetadata,
|
||||
reqBodyRecurringEventId,
|
||||
uid,
|
||||
responses,
|
||||
isConfirmedByDefault,
|
||||
smsReminderNumber,
|
||||
organizerUser,
|
||||
rescheduleReason,
|
||||
eventType,
|
||||
bookerEmail,
|
||||
paymentAppData,
|
||||
changedOrganizer,
|
||||
}: CreateBookingParams) {
|
||||
updateEventDetails(evt, originalRescheduledBooking, changedOrganizer);
|
||||
|
||||
const newBookingData = buildNewBookingData({
|
||||
uid,
|
||||
evt,
|
||||
responses,
|
||||
isConfirmedByDefault,
|
||||
reqBodyMetadata,
|
||||
smsReminderNumber,
|
||||
eventTypeSlug,
|
||||
organizerUser,
|
||||
reqBodyRecurringEventId,
|
||||
originalRescheduledBooking,
|
||||
bookerEmail,
|
||||
rescheduleReason,
|
||||
eventType,
|
||||
eventTypeId,
|
||||
reqBodyUser,
|
||||
});
|
||||
|
||||
return await saveBooking(newBookingData, originalRescheduledBooking, paymentAppData, organizerUser);
|
||||
}
|
||||
|
||||
async function saveBooking(
|
||||
newBookingData: Prisma.BookingCreateInput,
|
||||
originalRescheduledBooking: OriginalRescheduledBooking,
|
||||
paymentAppData: PaymentAppData,
|
||||
organizerUser: CreateBookingParams["organizerUser"]
|
||||
) {
|
||||
const createBookingObj = {
|
||||
include: {
|
||||
user: {
|
||||
select: { email: true, name: true, timeZone: true, username: true },
|
||||
},
|
||||
attendees: true,
|
||||
payment: true,
|
||||
references: true,
|
||||
},
|
||||
data: newBookingData,
|
||||
};
|
||||
|
||||
if (originalRescheduledBooking?.paid && originalRescheduledBooking?.payment) {
|
||||
const bookingPayment = originalRescheduledBooking.payment.find((payment) => payment.success);
|
||||
if (bookingPayment) {
|
||||
createBookingObj.data.payment = { connect: { id: bookingPayment.id } };
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof paymentAppData.price === "number" && paymentAppData.price > 0) {
|
||||
await prisma.credential.findFirstOrThrow({
|
||||
where: {
|
||||
appId: paymentAppData.appId,
|
||||
...(paymentAppData.credentialId ? { id: paymentAppData.credentialId } : { userId: organizerUser.id }),
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
|
||||
return prisma.booking.create(createBookingObj);
|
||||
}
|
||||
|
||||
function getEventTypeRel(eventTypeId: EventTypeId) {
|
||||
return eventTypeId ? { connect: { id: 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) => ({
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
locale: attendee.language.locale,
|
||||
}));
|
||||
|
||||
if (evt.team?.members) {
|
||||
attendees.push(
|
||||
...evt.team.members.map((member) => ({
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
timeZone: member.timeZone,
|
||||
locale: member.language.locale,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const attendeesData = getAttendeesData(evt);
|
||||
const eventTypeRel = getEventTypeRel(eventTypeId);
|
||||
|
||||
const newBookingData: Prisma.BookingCreateInput = {
|
||||
uid,
|
||||
userPrimaryEmail: evt.organizer.email,
|
||||
responses: responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : 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,
|
||||
location: evt.location,
|
||||
eventType: eventTypeRel,
|
||||
smsReminderNumber,
|
||||
metadata: reqBodyMetadata,
|
||||
attendees: {
|
||||
createMany: {
|
||||
data: attendeesData,
|
||||
},
|
||||
},
|
||||
dynamicEventSlugRef: !eventTypeId ? eventTypeSlug : null,
|
||||
dynamicGroupSlugRef: !eventTypeId ? (reqBodyUser as string).toLowerCase() : null,
|
||||
iCalUID: evt.iCalUID ?? "",
|
||||
user: {
|
||||
connect: {
|
||||
id: organizerUser.id,
|
||||
},
|
||||
},
|
||||
destinationCalendar:
|
||||
evt.destinationCalendar && evt.destinationCalendar.length > 0
|
||||
? {
|
||||
connect: { id: evt.destinationCalendar[0].id },
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (reqBodyRecurringEventId) {
|
||||
newBookingData.recurringEventId = reqBodyRecurringEventId;
|
||||
}
|
||||
|
||||
if (originalRescheduledBooking) {
|
||||
newBookingData.metadata = {
|
||||
...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata),
|
||||
};
|
||||
newBookingData.paid = originalRescheduledBooking.paid;
|
||||
newBookingData.fromReschedule = originalRescheduledBooking.uid;
|
||||
if (originalRescheduledBooking.uid) {
|
||||
newBookingData.cancellationReason = rescheduleReason;
|
||||
}
|
||||
// Reschedule logic with booking with seats
|
||||
if (newBookingData.attendees?.createMany?.data && eventType?.seatsPerTimeSlot && bookerEmail) {
|
||||
newBookingData.attendees.createMany.data = attendeesData.filter(
|
||||
(attendee) => attendee.email === bookerEmail
|
||||
);
|
||||
}
|
||||
if (originalRescheduledBooking.recurringEventId) {
|
||||
newBookingData.recurringEventId = originalRescheduledBooking.recurringEventId;
|
||||
}
|
||||
}
|
||||
|
||||
return newBookingData;
|
||||
}
|
||||
|
||||
export type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
||||
@@ -0,0 +1,155 @@
|
||||
import type { Logger } from "tslog";
|
||||
|
||||
import { getBusyTimesForLimitChecks } from "@calcom/core/getBusyTimes";
|
||||
import { getUsersAvailability } from "@calcom/core/getUserAvailability";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import { parseBookingLimit, parseDurationLimit } from "@calcom/lib";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
|
||||
import { checkForConflicts } from "../conflictChecker/checkForConflicts";
|
||||
import type { getEventTypeResponse } from "./getEventTypesFromDB";
|
||||
import type { IsFixedAwareUser, BookingType } from "./types";
|
||||
|
||||
type DateRange = {
|
||||
start: Dayjs;
|
||||
end: Dayjs;
|
||||
};
|
||||
|
||||
const getDateTimeInUtc = (timeInput: string, timeZone?: string) => {
|
||||
return timeZone === "Etc/GMT" ? dayjs.utc(timeInput) : dayjs(timeInput).tz(timeZone).utc();
|
||||
};
|
||||
|
||||
const getOriginalBookingDuration = (originalBooking?: BookingType) => {
|
||||
return originalBooking
|
||||
? dayjs(originalBooking.endTime).diff(dayjs(originalBooking.startTime), "minutes")
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const hasDateRangeForBooking = (
|
||||
dateRanges: DateRange[],
|
||||
startDateTimeUtc: dayjs.Dayjs,
|
||||
endDateTimeUtc: dayjs.Dayjs
|
||||
) => {
|
||||
let dateRangeForBooking = false;
|
||||
|
||||
for (const dateRange of dateRanges) {
|
||||
if (
|
||||
(startDateTimeUtc.isAfter(dateRange.start) || startDateTimeUtc.isSame(dateRange.start)) &&
|
||||
(endDateTimeUtc.isBefore(dateRange.end) || endDateTimeUtc.isSame(dateRange.end))
|
||||
) {
|
||||
dateRangeForBooking = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return dateRangeForBooking;
|
||||
};
|
||||
|
||||
export async function ensureAvailableUsers(
|
||||
eventType: getEventTypeResponse & {
|
||||
users: IsFixedAwareUser[];
|
||||
},
|
||||
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
|
||||
loggerWithEventDetails: Logger<unknown>
|
||||
) {
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
|
||||
const startDateTimeUtc = getDateTimeInUtc(input.dateFrom, input.timeZone);
|
||||
const endDateTimeUtc = getDateTimeInUtc(input.dateTo, input.timeZone);
|
||||
|
||||
const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute");
|
||||
const originalBookingDuration = getOriginalBookingDuration(input.originalRescheduledBooking);
|
||||
|
||||
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
|
||||
const durationLimits = parseDurationLimit(eventType?.durationLimits);
|
||||
|
||||
const busyTimesFromLimitsBookingsAllUsers: Awaited<ReturnType<typeof getBusyTimesForLimitChecks>> =
|
||||
eventType && (bookingLimits || durationLimits)
|
||||
? await getBusyTimesForLimitChecks({
|
||||
userIds: eventType.users.map((u) => u.id),
|
||||
eventTypeId: eventType.id,
|
||||
startDate: startDateTimeUtc.format(),
|
||||
endDate: endDateTimeUtc.format(),
|
||||
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
})
|
||||
: [];
|
||||
|
||||
const usersAvailability = await getUsersAvailability({
|
||||
users: eventType.users,
|
||||
query: {
|
||||
...input,
|
||||
eventTypeId: eventType.id,
|
||||
duration: originalBookingDuration,
|
||||
returnDateOverrides: false,
|
||||
dateFrom: startDateTimeUtc.format(),
|
||||
dateTo: endDateTimeUtc.format(),
|
||||
},
|
||||
initialData: {
|
||||
eventType,
|
||||
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
|
||||
busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers,
|
||||
},
|
||||
});
|
||||
|
||||
usersAvailability.forEach(({ oooExcludedDateRanges: dateRanges, busy: bufferedBusyTimes }, index) => {
|
||||
const user = eventType.users[index];
|
||||
|
||||
loggerWithEventDetails.debug(
|
||||
"calendarBusyTimes==>>>",
|
||||
JSON.stringify({ bufferedBusyTimes, dateRanges, isRecurringEvent: eventType.recurringEvent })
|
||||
);
|
||||
|
||||
if (!dateRanges.length) {
|
||||
loggerWithEventDetails.error(
|
||||
`User does not have availability at this time.`,
|
||||
safeStringify({
|
||||
startDateTimeUtc,
|
||||
endDateTimeUtc,
|
||||
input,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
//check if event time is within the date range
|
||||
if (!hasDateRangeForBooking(dateRanges, startDateTimeUtc, endDateTimeUtc)) {
|
||||
loggerWithEventDetails.error(
|
||||
`No date range for booking.`,
|
||||
safeStringify({
|
||||
startDateTimeUtc,
|
||||
endDateTimeUtc,
|
||||
input,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const foundConflict = checkForConflicts(bufferedBusyTimes, startDateTimeUtc, duration);
|
||||
// no conflicts found, add to available users.
|
||||
if (!foundConflict) {
|
||||
availableUsers.push(user);
|
||||
}
|
||||
} catch (error) {
|
||||
loggerWithEventDetails.error("Unable set isAvailableToBeBooked. Using true. ", error);
|
||||
}
|
||||
});
|
||||
|
||||
if (!availableUsers.length) {
|
||||
loggerWithEventDetails.error(
|
||||
`No available users found.`,
|
||||
safeStringify({
|
||||
startDateTimeUtc,
|
||||
endDateTimeUtc,
|
||||
input,
|
||||
})
|
||||
);
|
||||
throw new Error(ErrorCode.NoAvailableUsersFound);
|
||||
}
|
||||
|
||||
return availableUsers;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
export const findBookingQuery = async (bookingId: number) => {
|
||||
const foundBooking = await prisma.booking.findUnique({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
location: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
description: true,
|
||||
status: true,
|
||||
responses: true,
|
||||
metadata: true,
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
select: {
|
||||
title: true,
|
||||
description: true,
|
||||
currency: true,
|
||||
length: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
requiresConfirmation: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
price: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// This should never happen but it's just typescript safe
|
||||
if (!foundBooking) {
|
||||
throw new Error("Internal Error. Couldn't find booking");
|
||||
}
|
||||
|
||||
// Don't leak any sensitive data
|
||||
return foundBooking;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { EventTypeCustomInput } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type z from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { TgetBookingDataSchema } from "../getBookingDataSchema";
|
||||
import { handleCustomInputs } from "./handleCustomInputs";
|
||||
import type { getEventTypeResponse } from "./types";
|
||||
|
||||
type ReqBodyWithEnd = TgetBookingDataSchema & { end: string };
|
||||
|
||||
export async function getBookingData<T extends z.ZodType>({
|
||||
req,
|
||||
eventType,
|
||||
schema,
|
||||
}: {
|
||||
req: NextApiRequest;
|
||||
eventType: getEventTypeResponse;
|
||||
schema: T;
|
||||
}) {
|
||||
const reqBody = await schema.parseAsync(req.body);
|
||||
const reqBodyWithEnd = (reqBody: TgetBookingDataSchema): reqBody is ReqBodyWithEnd => {
|
||||
// Use the event length to auto-set the event end time.
|
||||
if (!Object.prototype.hasOwnProperty.call(reqBody, "end")) {
|
||||
reqBody.end = dayjs.utc(reqBody.start).add(eventType.length, "minutes").format();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!reqBodyWithEnd(reqBody)) {
|
||||
throw new Error(ErrorCode.RequestBodyWithouEnd);
|
||||
}
|
||||
// reqBody.end is no longer an optional property.
|
||||
if (reqBody.customInputs) {
|
||||
// Check if required custom inputs exist
|
||||
handleCustomInputs(eventType.customInputs as EventTypeCustomInput[], reqBody.customInputs);
|
||||
const reqBodyWithLegacyProps = bookingCreateSchemaLegacyPropsForApi.parse(reqBody);
|
||||
return {
|
||||
...reqBody,
|
||||
name: reqBodyWithLegacyProps.name,
|
||||
email: reqBodyWithLegacyProps.email,
|
||||
guests: reqBodyWithLegacyProps.guests,
|
||||
location: reqBodyWithLegacyProps.location || "",
|
||||
smsReminderNumber: reqBodyWithLegacyProps.smsReminderNumber,
|
||||
notes: reqBodyWithLegacyProps.notes,
|
||||
rescheduleReason: reqBodyWithLegacyProps.rescheduleReason,
|
||||
// So TS doesn't complain about unknown properties
|
||||
calEventUserFieldsResponses: undefined,
|
||||
calEventResponses: undefined,
|
||||
customInputs: undefined,
|
||||
};
|
||||
}
|
||||
if (!reqBody.responses) {
|
||||
throw new Error("`responses` must not be nullish");
|
||||
}
|
||||
const responses = reqBody.responses;
|
||||
|
||||
const { userFieldsResponses: calEventUserFieldsResponses, responses: calEventResponses } =
|
||||
getCalEventResponses({
|
||||
bookingFields: eventType.bookingFields,
|
||||
responses,
|
||||
});
|
||||
return {
|
||||
...reqBody,
|
||||
name: responses.name,
|
||||
email: responses.email,
|
||||
guests: responses.guests ? responses.guests : [],
|
||||
location: responses.location?.optionValue || responses.location?.value || "",
|
||||
smsReminderNumber: responses.smsReminderNumber,
|
||||
notes: responses.notes || "",
|
||||
calEventUserFieldsResponses,
|
||||
rescheduleReason: responses.rescheduleReason,
|
||||
calEventResponses,
|
||||
// So TS doesn't complain about unknown properties
|
||||
customInputs: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export type AwaitedBookingData = Awaited<ReturnType<typeof getBookingData>>;
|
||||
export type RescheduleReason = AwaitedBookingData["rescheduleReason"];
|
||||
export type NoEmail = AwaitedBookingData["noEmail"];
|
||||
export type AdditionalNotes = AwaitedBookingData["notes"];
|
||||
export type ReqAppsStatus = AwaitedBookingData["appsStatus"];
|
||||
export type SmsReminderNumber = AwaitedBookingData["smsReminderNumber"];
|
||||
export type EventTypeId = AwaitedBookingData["eventTypeId"];
|
||||
export type ReqBodyMetadata = ReqBodyWithEnd["metadata"];
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
export async function getOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
|
||||
return prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: uid,
|
||||
status: {
|
||||
in: [BookingStatus.ACCEPTED, BookingStatus.CANCELLED, BookingStatus.PENDING],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
timeZone: true,
|
||||
...(seatsEventType && { bookingSeat: true, id: true }),
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
timeZone: true,
|
||||
destinationCalendar: true,
|
||||
credentials: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
key: true,
|
||||
type: true,
|
||||
teamId: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destinationCalendar: true,
|
||||
payment: true,
|
||||
references: true,
|
||||
workflowReminders: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
||||
|
||||
export type OriginalRescheduledBooking = Awaited<ReturnType<typeof getOriginalRescheduledBooking>>;
|
||||
@@ -69,3 +69,5 @@ function determineIsConfirmedByDefault(
|
||||
): boolean {
|
||||
return (!requiresConfirmation && price === 0) || userReschedulingIsOwner;
|
||||
}
|
||||
|
||||
export type IsConfirmedByDefault = ReturnType<typeof getRequiresConfirmationFlags>["isConfirmedByDefault"];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AdditionalInformation, AppsStatus } from "@calcom/types/Calendar";
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
|
||||
import type { ReqAppsStatus, Booking } from "../handleNewBooking";
|
||||
import type { ReqAppsStatus, Booking } from "./types";
|
||||
|
||||
export function handleAppsStatus(
|
||||
results: EventResult<AdditionalInformation>[],
|
||||
|
||||
@@ -8,7 +8,7 @@ import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma, { userSelect } from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import type { NewBookingEventType } from "../handleNewBooking";
|
||||
import type { NewBookingEventType } from "./types";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[loadUsers]:handleNewBooking "] });
|
||||
|
||||
@@ -85,3 +85,5 @@ export const findUsersByUsername = async ({
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export type AwaitedLoadUsers = Awaited<ReturnType<typeof loadUsers>>;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { App } from "@prisma/client";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import type { AwaitedGetDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import type { PaymentAppData } from "@calcom/lib/getPaymentAppData";
|
||||
import type { userSelect } from "@calcom/prisma";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import type { Booking } from "./createBooking";
|
||||
import type {
|
||||
AwaitedBookingData,
|
||||
RescheduleReason,
|
||||
NoEmail,
|
||||
AdditionalNotes,
|
||||
ReqAppsStatus,
|
||||
SmsReminderNumber,
|
||||
EventTypeId,
|
||||
ReqBodyMetadata,
|
||||
} from "./getBookingData";
|
||||
import type { getEventTypeResponse } from "./getEventTypesFromDB";
|
||||
import type { BookingType, OriginalRescheduledBooking } from "./getOriginalRescheduledBooking";
|
||||
import type { getRequiresConfirmationFlags } from "./getRequiresConfirmationFlags";
|
||||
import type { AwaitedLoadUsers } from "./loadUsers";
|
||||
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
|
||||
export type OrganizerUser = AwaitedLoadUsers[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
};
|
||||
|
||||
export type Invitee = {
|
||||
email: string;
|
||||
name: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
timeZone: string;
|
||||
language: {
|
||||
translate: TFunction;
|
||||
locale: string;
|
||||
};
|
||||
}[];
|
||||
|
||||
export interface IEventTypePaymentCredentialType {
|
||||
appId: EventTypeAppsList;
|
||||
app: {
|
||||
categories: App["categories"];
|
||||
dirName: string;
|
||||
};
|
||||
key: Prisma.JsonValue;
|
||||
}
|
||||
|
||||
export type IsFixedAwareUser = User & {
|
||||
isFixed: boolean;
|
||||
credentials: CredentialPayload[];
|
||||
organization: { slug: string };
|
||||
priority?: number;
|
||||
};
|
||||
|
||||
export type NewBookingEventType = AwaitedGetDefaultEvent | getEventTypeResponse;
|
||||
|
||||
export type IsConfirmedByDefault = ReturnType<typeof getRequiresConfirmationFlags>["isConfirmedByDefault"];
|
||||
|
||||
export type {
|
||||
AwaitedBookingData,
|
||||
RescheduleReason,
|
||||
NoEmail,
|
||||
AdditionalNotes,
|
||||
ReqAppsStatus,
|
||||
SmsReminderNumber,
|
||||
EventTypeId,
|
||||
ReqBodyMetadata,
|
||||
PaymentAppData,
|
||||
BookingType,
|
||||
Booking,
|
||||
OriginalRescheduledBooking,
|
||||
AwaitedLoadUsers,
|
||||
getEventTypeResponse,
|
||||
};
|
||||
@@ -15,8 +15,8 @@ import { handlePayment } from "@calcom/lib/payment/handlePayment";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import type { IEventTypePaymentCredentialType } from "../../handleNewBooking";
|
||||
import { findBookingQuery } from "../../handleNewBooking";
|
||||
import { findBookingQuery } from "../../handleNewBooking/findBookingQuery";
|
||||
import type { IEventTypePaymentCredentialType } from "../../handleNewBooking/types";
|
||||
import type { SeatedBooking, NewSeatedBookingObject, HandleSeatsResultBooking } from "../types";
|
||||
|
||||
const createNewSeat = async (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import type { OriginalRescheduledBooking } from "../../handleNewBooking";
|
||||
import type { OriginalRescheduledBooking } from "../../handleNewBooking/types";
|
||||
|
||||
/* Check if the original booking has no more attendees, if so delete the booking
|
||||
and any calendar or video integrations */
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Person, CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { findBookingQuery } from "../../../handleNewBooking";
|
||||
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
|
||||
import lastAttendeeDeleteBooking from "../../lib/lastAttendeeDeleteBooking";
|
||||
import type { RescheduleSeatedBookingObject, SeatAttendee, NewTimeSlotBooking } from "../../types";
|
||||
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@ import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import type { createLoggerWithEventDetails } from "../../../handleNewBooking";
|
||||
import { addVideoCallDataToEvent, findBookingQuery } from "../../../handleNewBooking";
|
||||
import { addVideoCallDataToEvent } from "../../../handleNewBooking";
|
||||
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
|
||||
import type { SeatedBooking, RescheduleSeatedBookingObject, NewTimeSlotBooking } from "../../types";
|
||||
|
||||
const combineTwoSeatedBookings = async (
|
||||
|
||||
+4
-2
@@ -6,9 +6,11 @@ import { sendRescheduledEmails } from "@calcom/emails";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AdditionalInformation, AppsStatus } from "@calcom/types/Calendar";
|
||||
|
||||
import { addVideoCallDataToEvent, findBookingQuery } from "../../../handleNewBooking";
|
||||
import type { Booking, createLoggerWithEventDetails } from "../../../handleNewBooking";
|
||||
import { addVideoCallDataToEvent } from "../../../handleNewBooking";
|
||||
import type { createLoggerWithEventDetails } from "../../../handleNewBooking";
|
||||
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
|
||||
import { handleAppsStatus } from "../../../handleNewBooking/handleAppsStatus";
|
||||
import type { Booking } from "../../../handleNewBooking/types";
|
||||
import type { SeatedBooking, RescheduleSeatedBookingObject } from "../../types";
|
||||
|
||||
const moveSeatedBookingToNewTimeSlot = async (
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import type z from "zod";
|
||||
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
|
||||
import type { AppsStatus } from "@calcom/types/Calendar";
|
||||
|
||||
import type { Booking, NewBookingEventType, OriginalRescheduledBooking } from "../handleNewBooking";
|
||||
import type { Booking, NewBookingEventType, OriginalRescheduledBooking } from "../handleNewBooking/types";
|
||||
|
||||
export type BookingSeat = Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null;
|
||||
|
||||
|
||||
@@ -8,7 +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 } from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { getCustomInputsResponses } from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { getBookingData } from "@calcom/features/bookings/lib/handleNewBooking/getBookingData";
|
||||
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";
|
||||
|
||||
@@ -168,3 +168,5 @@ export const getUsernameList = (users: string | string[] | undefined): string[]
|
||||
};
|
||||
|
||||
export default defaultEvents;
|
||||
|
||||
export type AwaitedGetDefaultEvent = Awaited<ReturnType<typeof getDefaultEvent>>;
|
||||
|
||||
@@ -53,3 +53,5 @@ export default function getPaymentAppData(
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export type PaymentAppData = ReturnType<typeof getPaymentAppData>;
|
||||
|
||||
Reference in New Issue
Block a user