* refactor: handleNewBooking #3 * refactor: create booking factor * refactor: handleNewBooking * refactor: seats and rescheduleUId * chore: remove comment * fix: type err * chore: add missing statement * chore: use less params and other improvements * chore: name * chore: improvement * fix: type err * Typo fix * chore: fix type err * refactor: more readable * refactor: improve code * fix: conflicts --------- Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
This commit is contained in:
co-authored by
Joe Au-Yeung
Joe Au-Yeung
Syed Ali Shahbaz
parent
a99e285d76
commit
b4f1b5aebe
@@ -1,11 +1,9 @@
|
||||
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 short, { uuid } from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import type z from "zod";
|
||||
|
||||
import processExternalId from "@calcom/app-store/_utils/calendars/processExternalId";
|
||||
import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata";
|
||||
@@ -45,7 +43,6 @@ import {
|
||||
scheduleTrigger,
|
||||
} from "@calcom/features/webhooks/lib/scheduleTrigger";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import { getUTCOffsetByTimezone } from "@calcom/lib/date-fns";
|
||||
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
@@ -55,47 +52,42 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { handlePayment } from "@calcom/lib/payment/handlePayment";
|
||||
import { getPiiFreeCalendarEvent, getPiiFreeEventType, getPiiFreeUser } from "@calcom/lib/piiFreeData";
|
||||
import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFreeData";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { checkBookingLimits, checkDurationLimits, getLuckyUser } from "@calcom/lib/server";
|
||||
import { getLuckyUser } from "@calcom/lib/server";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
|
||||
import { slugify } from "@calcom/lib/slugify";
|
||||
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma, { userSelect } from "@calcom/prisma";
|
||||
import type { BookingReference } from "@calcom/prisma/client";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod-utils";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type {
|
||||
AdditionalInformation,
|
||||
AppsStatus,
|
||||
CalendarEvent,
|
||||
IntervalLimit,
|
||||
Person,
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { AdditionalInformation, AppsStatus, CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import type { EventPayloadType, EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
||||
import { getAllCredentials } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
|
||||
import { refreshCredentials } from "./getAllCredentialsForUsersOnEvent/refreshCredentials";
|
||||
import getBookingDataSchema from "./getBookingDataSchema";
|
||||
import { addVideoCallDataToEvent } from "./handleNewBooking/addVideoCallDataToEvent";
|
||||
import { checkBookingAndDurationLimits } from "./handleNewBooking/checkBookingAndDurationLimits";
|
||||
import { checkIfBookerEmailIsBlocked } from "./handleNewBooking/checkIfBookerEmailIsBlocked";
|
||||
import { createBooking } from "./handleNewBooking/createBooking";
|
||||
import { ensureAvailableUsers } from "./handleNewBooking/ensureAvailableUsers";
|
||||
import { getBookingData } from "./handleNewBooking/getBookingData";
|
||||
import { getCustomInputsResponses } from "./handleNewBooking/getCustomInputsResponses";
|
||||
import { getEventTypesFromDB } from "./handleNewBooking/getEventTypesFromDB";
|
||||
import type { getEventTypeResponse } from "./handleNewBooking/getEventTypesFromDB";
|
||||
import { getLocationValuesForDb } from "./handleNewBooking/getLocationValuesForDb";
|
||||
import { getOriginalRescheduledBooking } from "./handleNewBooking/getOriginalRescheduledBooking";
|
||||
import { getRequiresConfirmationFlags } from "./handleNewBooking/getRequiresConfirmationFlags";
|
||||
import { getSeatedBooking } from "./handleNewBooking/getSeatedBooking";
|
||||
import { getVideoCallDetails } from "./handleNewBooking/getVideoCallDetails";
|
||||
import { handleAppsStatus } from "./handleNewBooking/handleAppsStatus";
|
||||
import { loadUsers } from "./handleNewBooking/loadUsers";
|
||||
import { loadAndValidateUsers } from "./handleNewBooking/loadAndValidateUsers";
|
||||
import type {
|
||||
Invitee,
|
||||
IEventTypePaymentCredentialType,
|
||||
@@ -103,62 +95,13 @@ import type {
|
||||
BookingType,
|
||||
Booking,
|
||||
} from "./handleNewBooking/types";
|
||||
import { validateBookingTimeIsNotOutOfBounds } from "./handleNewBooking/validateBookingTimeIsNotOutOfBounds";
|
||||
import { validateEventLength } from "./handleNewBooking/validateEventLength";
|
||||
import handleSeats from "./handleSeats/handleSeats";
|
||||
import type { BookingSeat } from "./handleSeats/types";
|
||||
|
||||
const translator = short();
|
||||
const log = logger.getSubLogger({ prefix: ["[api] book:user"] });
|
||||
|
||||
export function getCustomInputsResponses(
|
||||
reqBody: {
|
||||
responses?: Record<string, object>;
|
||||
customInputs?: z.infer<typeof bookingCreateSchemaLegacyPropsForApi>["customInputs"];
|
||||
},
|
||||
eventTypeCustomInputs: getEventTypeResponse["customInputs"]
|
||||
) {
|
||||
const customInputsResponses = {} as NonNullable<CalendarEvent["customInputs"]>;
|
||||
if (reqBody.customInputs && (reqBody.customInputs.length || 0) > 0) {
|
||||
reqBody.customInputs.forEach(({ label, value }) => {
|
||||
customInputsResponses[label] = value;
|
||||
});
|
||||
} else {
|
||||
const responses = reqBody.responses || {};
|
||||
// Backward Compatibility: Map new `responses` to old `customInputs` format so that webhooks can still receive same values.
|
||||
for (const [fieldName, fieldValue] of Object.entries(responses)) {
|
||||
const foundACustomInputForTheResponse = eventTypeCustomInputs.find(
|
||||
(input) => slugify(input.label) === fieldName
|
||||
);
|
||||
if (foundACustomInputForTheResponse) {
|
||||
customInputsResponses[foundACustomInputForTheResponse.label] = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return customInputsResponses;
|
||||
}
|
||||
|
||||
/** Updates the evt object with video call data found from booking references
|
||||
*
|
||||
* @param bookingReferences
|
||||
* @param evt
|
||||
*
|
||||
* @returns updated evt with video call data
|
||||
*/
|
||||
export const addVideoCallDataToEvent = (bookingReferences: BookingReference[], evt: CalendarEvent) => {
|
||||
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
|
||||
|
||||
if (videoCallReference) {
|
||||
evt.videoCallData = {
|
||||
type: videoCallReference.type,
|
||||
id: videoCallReference.meetingId,
|
||||
password: videoCallReference?.meetingPassword,
|
||||
url: videoCallReference.meetingUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return evt;
|
||||
};
|
||||
|
||||
export const createLoggerWithEventDetails = (
|
||||
eventTypeId: number,
|
||||
reqBodyUser: string | string[] | undefined,
|
||||
@@ -184,6 +127,25 @@ function getICalSequence(originalRescheduledBooking: BookingType | null) {
|
||||
return originalRescheduledBooking.iCalSequence + 1;
|
||||
}
|
||||
|
||||
const getEventType = async ({
|
||||
eventTypeId,
|
||||
eventTypeSlug,
|
||||
}: {
|
||||
eventTypeId: number;
|
||||
eventTypeSlug?: string;
|
||||
}) => {
|
||||
// handle dynamic user
|
||||
const eventType =
|
||||
!eventTypeId && !!eventTypeSlug ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId);
|
||||
|
||||
const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId;
|
||||
|
||||
return {
|
||||
...eventType,
|
||||
bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }),
|
||||
};
|
||||
};
|
||||
|
||||
type BookingDataSchemaGetter =
|
||||
| typeof getBookingDataSchema
|
||||
| typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default;
|
||||
@@ -208,18 +170,10 @@ async function handler(
|
||||
platformBookingLocation,
|
||||
} = req;
|
||||
|
||||
// handle dynamic user
|
||||
let eventType =
|
||||
!req.body.eventTypeId && !!req.body.eventTypeSlug
|
||||
? getDefaultEvent(req.body.eventTypeSlug)
|
||||
: await getEventTypesFromDB(req.body.eventTypeId);
|
||||
|
||||
const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId;
|
||||
|
||||
eventType = {
|
||||
...eventType,
|
||||
bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }),
|
||||
};
|
||||
const eventType = await getEventType({
|
||||
eventTypeId: req.body.eventTypeId,
|
||||
eventTypeSlug: req.body.eventTypeSlug,
|
||||
});
|
||||
|
||||
const bookingDataSchema = bookingDataSchemaGetter({
|
||||
view: req.body?.rescheduleUid ? "reschedule" : "booking",
|
||||
@@ -297,192 +251,51 @@ async function handler(
|
||||
);
|
||||
|
||||
const user = eventType.users.find((user) => user.id === eventType.userId);
|
||||
|
||||
const userSchedule = user?.schedules.find((schedule) => schedule.id === user?.defaultScheduleId);
|
||||
|
||||
const eventTimeZone = eventType.schedule?.timeZone ?? userSchedule?.timeZone;
|
||||
|
||||
let timeOutOfBounds = false;
|
||||
try {
|
||||
timeOutOfBounds = isOutOfBounds(
|
||||
reqBody.start,
|
||||
{
|
||||
periodType: eventType.periodType,
|
||||
periodDays: eventType.periodDays,
|
||||
periodEndDate: eventType.periodEndDate,
|
||||
periodStartDate: eventType.periodStartDate,
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays,
|
||||
bookerUtcOffset: getUTCOffsetByTimezone(reqBody.timeZone) ?? 0,
|
||||
eventUtcOffset: eventTimeZone ? getUTCOffsetByTimezone(eventTimeZone) ?? 0 : 0,
|
||||
},
|
||||
eventType.minimumBookingNotice
|
||||
);
|
||||
} catch (error) {
|
||||
loggerWithEventDetails.warn({
|
||||
message: "NewBooking: Unable set timeOutOfBounds. Using false. ",
|
||||
});
|
||||
if (error instanceof BookingDateInPastError) {
|
||||
// TODO: HttpError should not bleed through to the console.
|
||||
loggerWithEventDetails.info(`Booking eventType ${eventTypeId} failed`, JSON.stringify({ error }));
|
||||
throw new HttpError({ statusCode: 400, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (timeOutOfBounds) {
|
||||
const error = {
|
||||
errorCode: "BookingTimeOutOfBounds",
|
||||
message: `EventType '${eventType.eventName}' cannot be booked at this time.`,
|
||||
};
|
||||
loggerWithEventDetails.warn({
|
||||
message: `NewBooking: EventType '${eventType.eventName}' cannot be booked at this time.`,
|
||||
});
|
||||
throw new HttpError({ statusCode: 400, message: error.message });
|
||||
}
|
||||
|
||||
const reqEventLength = dayjs(reqBody.end).diff(dayjs(reqBody.start), "minutes");
|
||||
const validEventLengths = eventType.metadata?.multipleDuration?.length
|
||||
? eventType.metadata.multipleDuration
|
||||
: [eventType.length];
|
||||
if (!validEventLengths.includes(reqEventLength)) {
|
||||
loggerWithEventDetails.warn({ message: "NewBooking: Invalid event length" });
|
||||
throw new HttpError({ statusCode: 400, message: "Invalid event length" });
|
||||
}
|
||||
|
||||
// loadUsers allows type inferring
|
||||
let users: (Awaited<ReturnType<typeof loadUsers>>[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
})[] = await loadUsers(eventType, dynamicUserList, req);
|
||||
|
||||
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
|
||||
if (!isDynamicAllowed && !eventTypeId) {
|
||||
loggerWithEventDetails.warn({
|
||||
message: "NewBooking: Some of the users in this group do not allow dynamic booking",
|
||||
});
|
||||
throw new HttpError({
|
||||
message: "Some of the users in this group do not allow dynamic booking",
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// If this event was pre-relationship migration
|
||||
// TODO: Establish whether this is dead code.
|
||||
if (!users.length && eventType.userId) {
|
||||
const eventTypeUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: eventType.userId,
|
||||
},
|
||||
select: {
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
}, // Don't leak to client
|
||||
...userSelect.select,
|
||||
},
|
||||
});
|
||||
if (!eventTypeUser) {
|
||||
loggerWithEventDetails.warn({ message: "NewBooking: eventTypeUser.notFound" });
|
||||
throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
||||
}
|
||||
users.push(eventTypeUser);
|
||||
}
|
||||
|
||||
if (!users) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
||||
|
||||
users = users.map((user) => ({
|
||||
...user,
|
||||
isFixed:
|
||||
user.isFixed === false
|
||||
? false
|
||||
: user.isFixed || eventType.schedulingType !== SchedulingType.ROUND_ROBIN,
|
||||
}));
|
||||
|
||||
loggerWithEventDetails.debug(
|
||||
"Concerned users",
|
||||
safeStringify({
|
||||
users: users.map(getPiiFreeUser),
|
||||
})
|
||||
await validateBookingTimeIsNotOutOfBounds<typeof eventType>(
|
||||
reqBody.start,
|
||||
reqBody.timeZone,
|
||||
eventType,
|
||||
eventTimeZone,
|
||||
loggerWithEventDetails
|
||||
);
|
||||
|
||||
let locationBodyString = location;
|
||||
validateEventLength({
|
||||
reqBodyStart: reqBody.start,
|
||||
reqBodyEnd: reqBody.end,
|
||||
eventTypeMultipleDuration: eventType.metadata?.multipleDuration,
|
||||
eventTypeLength: eventType.length,
|
||||
logger: loggerWithEventDetails,
|
||||
});
|
||||
|
||||
// TODO: It's definition should be moved to getLocationValueForDb
|
||||
let organizerOrFirstDynamicGroupMemberDefaultLocationUrl = undefined;
|
||||
let users = await loadAndValidateUsers({
|
||||
req,
|
||||
eventType,
|
||||
eventTypeId,
|
||||
dynamicUserList,
|
||||
logger: loggerWithEventDetails,
|
||||
});
|
||||
|
||||
if (dynamicUserList.length > 1) {
|
||||
users = users.sort((a, b) => {
|
||||
const aIndex = (a.username && dynamicUserList.indexOf(a.username)) || 0;
|
||||
const bIndex = (b.username && dynamicUserList.indexOf(b.username)) || 0;
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
const firstUsersMetadata = userMetadataSchema.parse(users[0].metadata);
|
||||
locationBodyString = firstUsersMetadata?.defaultConferencingApp?.appLink || locationBodyString;
|
||||
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
|
||||
firstUsersMetadata?.defaultConferencingApp?.appLink;
|
||||
}
|
||||
let { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl } = getLocationValuesForDb(
|
||||
dynamicUserList,
|
||||
users,
|
||||
location
|
||||
);
|
||||
|
||||
let rescheduleUid = reqBody.rescheduleUid;
|
||||
await checkBookingAndDurationLimits({
|
||||
eventType,
|
||||
reqBodyStart: reqBody.start,
|
||||
reqBodyRescheduleUid: reqBody.rescheduleUid,
|
||||
});
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(eventType, "bookingLimits") ||
|
||||
Object.prototype.hasOwnProperty.call(eventType, "durationLimits")
|
||||
) {
|
||||
const startAsDate = dayjs(reqBody.start).toDate();
|
||||
if (
|
||||
eventType.bookingLimits &&
|
||||
/* Empty object is truthy */ Object.keys(eventType.bookingLimits).length > 0
|
||||
) {
|
||||
await checkBookingLimits(
|
||||
eventType.bookingLimits as IntervalLimit,
|
||||
startAsDate,
|
||||
eventType.id,
|
||||
rescheduleUid,
|
||||
eventTimeZone
|
||||
);
|
||||
}
|
||||
if (eventType.durationLimits) {
|
||||
await checkDurationLimits(
|
||||
eventType.durationLimits as IntervalLimit,
|
||||
startAsDate,
|
||||
eventType.id,
|
||||
rescheduleUid
|
||||
);
|
||||
}
|
||||
}
|
||||
const bookingSeat = reqBody.rescheduleUid ? await getSeatedBooking(reqBody.rescheduleUid) : null;
|
||||
const rescheduleUid = bookingSeat ? bookingSeat.booking.uid : reqBody.rescheduleUid;
|
||||
|
||||
let bookingSeat: BookingSeat = null;
|
||||
|
||||
let originalRescheduledBooking: BookingType = null;
|
||||
|
||||
//this gets the original rescheduled booking
|
||||
if (rescheduleUid) {
|
||||
// rescheduleUid can be bookingUid and bookingSeatUid
|
||||
bookingSeat = await prisma.bookingSeat.findUnique({
|
||||
where: {
|
||||
referenceUid: rescheduleUid,
|
||||
},
|
||||
include: {
|
||||
booking: true,
|
||||
attendee: true,
|
||||
},
|
||||
});
|
||||
if (bookingSeat) {
|
||||
rescheduleUid = bookingSeat.booking.uid;
|
||||
}
|
||||
originalRescheduledBooking = await getOriginalRescheduledBooking(
|
||||
rescheduleUid,
|
||||
!!eventType.seatsPerTimeSlot
|
||||
);
|
||||
if (!originalRescheduledBooking) {
|
||||
throw new HttpError({ statusCode: 404, message: "Could not find original booking" });
|
||||
}
|
||||
|
||||
if (
|
||||
originalRescheduledBooking.status === BookingStatus.CANCELLED &&
|
||||
!originalRescheduledBooking.rescheduled
|
||||
) {
|
||||
throw new HttpError({ statusCode: 403, message: ErrorCode.CancelledBookingsCannotBeRescheduled });
|
||||
}
|
||||
}
|
||||
let originalRescheduledBooking = rescheduleUid
|
||||
? await getOriginalRescheduledBooking(rescheduleUid, !!eventType.seatsPerTimeSlot)
|
||||
: null;
|
||||
|
||||
let luckyUserResponse;
|
||||
let isFirstSeat = true;
|
||||
@@ -929,7 +742,7 @@ async function handler(
|
||||
eventDescription: eventType.description,
|
||||
price: paymentAppData.price,
|
||||
currency: eventType.currency,
|
||||
length: reqEventLength,
|
||||
length: dayjs(reqBody.end).diff(dayjs(reqBody.start), "minutes"),
|
||||
};
|
||||
|
||||
const teamId = await getTeamIdFromEventType({ eventType });
|
||||
@@ -1087,23 +900,29 @@ async function handler(
|
||||
|
||||
try {
|
||||
booking = await createBooking({
|
||||
originalRescheduledBooking,
|
||||
evt,
|
||||
eventTypeId,
|
||||
eventTypeSlug,
|
||||
reqBodyUser: reqBody.user,
|
||||
reqBodyMetadata: reqBody.metadata,
|
||||
reqBodyRecurringEventId: reqBody.recurringEventId,
|
||||
uid,
|
||||
responses,
|
||||
isConfirmedByDefault,
|
||||
smsReminderNumber,
|
||||
organizerUser,
|
||||
rescheduleReason,
|
||||
eventType,
|
||||
bookerEmail,
|
||||
paymentAppData,
|
||||
changedOrganizer,
|
||||
reqBody: {
|
||||
user: reqBody.user,
|
||||
metadata: reqBody.metadata,
|
||||
recurringEventId: reqBody.recurringEventId,
|
||||
},
|
||||
eventType: {
|
||||
eventTypeData: eventType,
|
||||
id: eventTypeId,
|
||||
slug: eventTypeSlug,
|
||||
organizerUser,
|
||||
isConfirmedByDefault,
|
||||
paymentAppData,
|
||||
},
|
||||
input: {
|
||||
bookerEmail,
|
||||
rescheduleReason,
|
||||
changedOrganizer,
|
||||
smsReminderNumber,
|
||||
responses,
|
||||
},
|
||||
evt,
|
||||
originalRescheduledBooking,
|
||||
});
|
||||
|
||||
// @NOTE: Add specific try catch for all subsequent async calls to avoid error
|
||||
@@ -1833,28 +1652,3 @@ async function handler(
|
||||
}
|
||||
|
||||
export default handler;
|
||||
|
||||
function getVideoCallDetails({
|
||||
results,
|
||||
}: {
|
||||
results: EventResult<AdditionalInformation & { url?: string | undefined; iCalUID?: string | undefined }>[];
|
||||
}) {
|
||||
const firstVideoResult = results.find((result) => result.type.includes("_video"));
|
||||
const metadata: AdditionalInformation = {};
|
||||
let updatedVideoEvent = null;
|
||||
|
||||
if (firstVideoResult && firstVideoResult.success) {
|
||||
updatedVideoEvent = Array.isArray(firstVideoResult.updatedEvent)
|
||||
? firstVideoResult.updatedEvent[0]
|
||||
: firstVideoResult.updatedEvent;
|
||||
|
||||
if (updatedVideoEvent) {
|
||||
metadata.hangoutLink = updatedVideoEvent.hangoutLink;
|
||||
metadata.conferenceData = updatedVideoEvent.conferenceData;
|
||||
metadata.entryPoints = updatedVideoEvent.entryPoints;
|
||||
}
|
||||
}
|
||||
const videoCallUrl = metadata.hangoutLink || updatedVideoEvent?.url;
|
||||
|
||||
return { videoCallUrl, metadata, updatedVideoEvent };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { BookingReference } from "@calcom/prisma/client";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
/** Updates the evt object with video call data found from booking references
|
||||
*
|
||||
* @param bookingReferences
|
||||
* @param evt
|
||||
*
|
||||
* @returns updated evt with video call data
|
||||
*/
|
||||
export const addVideoCallDataToEvent = (bookingReferences: BookingReference[], evt: CalendarEvent) => {
|
||||
const videoCallReference = bookingReferences.find((reference) => reference.type.includes("_video"));
|
||||
|
||||
if (videoCallReference) {
|
||||
evt.videoCallData = {
|
||||
type: videoCallReference.type,
|
||||
id: videoCallReference.meetingId,
|
||||
password: videoCallReference?.meetingPassword,
|
||||
url: videoCallReference.meetingUrl,
|
||||
};
|
||||
}
|
||||
|
||||
return evt;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { checkBookingLimits, checkDurationLimits } from "@calcom/lib/server";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
|
||||
import type { NewBookingEventType } from "./types";
|
||||
|
||||
type EventType = Pick<NewBookingEventType, "bookingLimits" | "durationLimits" | "id" | "schedule">;
|
||||
|
||||
type InputProps = {
|
||||
eventType: EventType;
|
||||
reqBodyStart: string;
|
||||
reqBodyRescheduleUid?: string;
|
||||
};
|
||||
|
||||
export const checkBookingAndDurationLimits = async ({
|
||||
eventType,
|
||||
reqBodyStart,
|
||||
reqBodyRescheduleUid,
|
||||
}: InputProps) => {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(eventType, "bookingLimits") ||
|
||||
Object.prototype.hasOwnProperty.call(eventType, "durationLimits")
|
||||
) {
|
||||
const startAsDate = dayjs(reqBodyStart).toDate();
|
||||
if (eventType.bookingLimits && Object.keys(eventType.bookingLimits).length > 0) {
|
||||
await checkBookingLimits(
|
||||
eventType.bookingLimits as IntervalLimit,
|
||||
startAsDate,
|
||||
eventType.id,
|
||||
reqBodyRescheduleUid,
|
||||
eventType.schedule?.timeZone
|
||||
);
|
||||
}
|
||||
if (eventType.durationLimits) {
|
||||
await checkDurationLimits(
|
||||
eventType.durationLimits as IntervalLimit,
|
||||
startAsDate,
|
||||
eventType.id,
|
||||
reqBodyRescheduleUid
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -12,35 +12,40 @@ import type {
|
||||
EventTypeId,
|
||||
AwaitedBookingData,
|
||||
NewBookingEventType,
|
||||
IsConfirmedByDefault,
|
||||
PaymentAppData,
|
||||
OriginalRescheduledBooking,
|
||||
AwaitedLoadUsers,
|
||||
LoadedUsers,
|
||||
} from "./types";
|
||||
|
||||
type ReqBodyWithEnd = TgetBookingDataSchema & { end: string };
|
||||
|
||||
type CreateBookingParams = {
|
||||
originalRescheduledBooking: OriginalRescheduledBooking;
|
||||
evt: CalendarEvent;
|
||||
eventType: NewBookingEventType;
|
||||
eventTypeId: EventTypeId;
|
||||
eventTypeSlug: AwaitedBookingData["eventTypeSlug"];
|
||||
reqBodyUser: ReqBodyWithEnd["user"];
|
||||
reqBodyMetadata: ReqBodyWithEnd["metadata"];
|
||||
reqBodyRecurringEventId: ReqBodyWithEnd["recurringEventId"];
|
||||
uid: short.SUUID;
|
||||
responses: ReqBodyWithEnd["responses"] | null;
|
||||
isConfirmedByDefault: IsConfirmedByDefault;
|
||||
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
|
||||
organizerUser: AwaitedLoadUsers[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
reqBody: {
|
||||
user: ReqBodyWithEnd["user"];
|
||||
metadata: ReqBodyWithEnd["metadata"];
|
||||
recurringEventId: ReqBodyWithEnd["recurringEventId"];
|
||||
};
|
||||
rescheduleReason: AwaitedBookingData["rescheduleReason"];
|
||||
bookerEmail: AwaitedBookingData["email"];
|
||||
paymentAppData: PaymentAppData;
|
||||
changedOrganizer: boolean;
|
||||
eventType: {
|
||||
eventTypeData: NewBookingEventType;
|
||||
id: EventTypeId;
|
||||
slug: AwaitedBookingData["eventTypeSlug"];
|
||||
organizerUser: LoadedUsers[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
};
|
||||
isConfirmedByDefault: boolean;
|
||||
paymentAppData: PaymentAppData;
|
||||
};
|
||||
input: {
|
||||
bookerEmail: AwaitedBookingData["email"];
|
||||
rescheduleReason: AwaitedBookingData["rescheduleReason"];
|
||||
changedOrganizer: boolean;
|
||||
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
|
||||
responses: ReqBodyWithEnd["responses"] | null;
|
||||
};
|
||||
evt: CalendarEvent;
|
||||
originalRescheduledBooking: OriginalRescheduledBooking;
|
||||
};
|
||||
|
||||
function updateEventDetails(
|
||||
@@ -57,52 +62,37 @@ function updateEventDetails(
|
||||
}
|
||||
|
||||
export async function createBooking({
|
||||
originalRescheduledBooking,
|
||||
evt,
|
||||
eventTypeId,
|
||||
eventTypeSlug,
|
||||
reqBodyUser,
|
||||
reqBodyMetadata,
|
||||
reqBodyRecurringEventId,
|
||||
uid,
|
||||
responses,
|
||||
isConfirmedByDefault,
|
||||
smsReminderNumber,
|
||||
organizerUser,
|
||||
rescheduleReason,
|
||||
reqBody,
|
||||
eventType,
|
||||
bookerEmail,
|
||||
paymentAppData,
|
||||
changedOrganizer,
|
||||
input,
|
||||
evt,
|
||||
originalRescheduledBooking,
|
||||
}: CreateBookingParams) {
|
||||
updateEventDetails(evt, originalRescheduledBooking, changedOrganizer);
|
||||
updateEventDetails(evt, originalRescheduledBooking, input.changedOrganizer);
|
||||
|
||||
const newBookingData = buildNewBookingData({
|
||||
uid,
|
||||
evt,
|
||||
responses,
|
||||
isConfirmedByDefault,
|
||||
reqBodyMetadata,
|
||||
smsReminderNumber,
|
||||
eventTypeSlug,
|
||||
organizerUser,
|
||||
reqBodyRecurringEventId,
|
||||
originalRescheduledBooking,
|
||||
bookerEmail,
|
||||
rescheduleReason,
|
||||
reqBody,
|
||||
eventType,
|
||||
eventTypeId,
|
||||
reqBodyUser,
|
||||
input,
|
||||
evt,
|
||||
originalRescheduledBooking,
|
||||
});
|
||||
|
||||
return await saveBooking(newBookingData, originalRescheduledBooking, paymentAppData, organizerUser);
|
||||
return await saveBooking(
|
||||
newBookingData,
|
||||
originalRescheduledBooking,
|
||||
eventType.paymentAppData,
|
||||
eventType.organizerUser
|
||||
);
|
||||
}
|
||||
|
||||
async function saveBooking(
|
||||
newBookingData: Prisma.BookingCreateInput,
|
||||
originalRescheduledBooking: OriginalRescheduledBooking,
|
||||
paymentAppData: PaymentAppData,
|
||||
organizerUser: CreateBookingParams["organizerUser"]
|
||||
organizerUser: CreateBookingParams["eventType"]["organizerUser"]
|
||||
) {
|
||||
const createBookingObj = {
|
||||
include: {
|
||||
@@ -143,93 +133,49 @@ function getEventTypeRel(eventTypeId: EventTypeId) {
|
||||
function getAttendeesData(evt: Pick<CalendarEvent, "attendees" | "team">) {
|
||||
//if attendee is team member, it should fetch their locale not booker's locale
|
||||
//perhaps make email fetch request to see if his locale is stored, else
|
||||
const attendees = evt.attendees.map((attendee) => ({
|
||||
const teamMembers = evt?.team?.members ?? [];
|
||||
|
||||
return evt.attendees.concat(teamMembers).map((attendee) => ({
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
locale: attendee.language.locale,
|
||||
phoneNumber: attendee.phoneNumber,
|
||||
}));
|
||||
|
||||
if (evt.team?.members) {
|
||||
attendees.push(
|
||||
...evt.team.members.map((member) => ({
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
timeZone: member.timeZone,
|
||||
locale: member.language.locale,
|
||||
phoneNumber: member.phoneNumber,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return attendees;
|
||||
}
|
||||
|
||||
function buildNewBookingData(params: {
|
||||
uid: short.SUUID;
|
||||
evt: CalendarEvent;
|
||||
responses: ReqBodyWithEnd["responses"] | null;
|
||||
isConfirmedByDefault: IsConfirmedByDefault;
|
||||
reqBodyMetadata: ReqBodyWithEnd["metadata"];
|
||||
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
|
||||
eventTypeSlug: AwaitedBookingData["eventTypeSlug"];
|
||||
organizerUser: CreateBookingParams["organizerUser"];
|
||||
reqBodyRecurringEventId: ReqBodyWithEnd["recurringEventId"];
|
||||
originalRescheduledBooking: OriginalRescheduledBooking | null;
|
||||
bookerEmail: AwaitedBookingData["email"];
|
||||
rescheduleReason: AwaitedBookingData["rescheduleReason"];
|
||||
eventType: NewBookingEventType;
|
||||
eventTypeId: EventTypeId;
|
||||
reqBodyUser: ReqBodyWithEnd["user"];
|
||||
}): Prisma.BookingCreateInput {
|
||||
const {
|
||||
uid,
|
||||
evt,
|
||||
responses,
|
||||
isConfirmedByDefault,
|
||||
reqBodyMetadata,
|
||||
smsReminderNumber,
|
||||
eventTypeSlug,
|
||||
organizerUser,
|
||||
reqBodyRecurringEventId,
|
||||
originalRescheduledBooking,
|
||||
bookerEmail,
|
||||
rescheduleReason,
|
||||
eventType,
|
||||
eventTypeId,
|
||||
reqBodyUser,
|
||||
} = params;
|
||||
function buildNewBookingData(params: CreateBookingParams): Prisma.BookingCreateInput {
|
||||
const { uid, evt, reqBody, eventType, input, originalRescheduledBooking } = params;
|
||||
|
||||
const attendeesData = getAttendeesData(evt);
|
||||
const eventTypeRel = getEventTypeRel(eventTypeId);
|
||||
const eventTypeRel = getEventTypeRel(eventType.id);
|
||||
|
||||
const newBookingData: Prisma.BookingCreateInput = {
|
||||
uid,
|
||||
userPrimaryEmail: evt.organizer.email,
|
||||
responses: responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : responses,
|
||||
responses: input.responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : input.responses,
|
||||
title: evt.title,
|
||||
startTime: dayjs.utc(evt.startTime).toDate(),
|
||||
endTime: dayjs.utc(evt.endTime).toDate(),
|
||||
description: evt.seatsPerTimeSlot ? null : evt.additionalNotes,
|
||||
customInputs: isPrismaObjOrUndefined(evt.customInputs),
|
||||
status: isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING,
|
||||
status: eventType.isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING,
|
||||
oneTimePassword: evt.oneTimePassword,
|
||||
location: evt.location,
|
||||
eventType: eventTypeRel,
|
||||
smsReminderNumber,
|
||||
metadata: reqBodyMetadata,
|
||||
smsReminderNumber: input.smsReminderNumber,
|
||||
metadata: reqBody.metadata,
|
||||
attendees: {
|
||||
createMany: {
|
||||
data: attendeesData,
|
||||
},
|
||||
},
|
||||
dynamicEventSlugRef: !eventTypeId ? eventTypeSlug : null,
|
||||
dynamicGroupSlugRef: !eventTypeId ? (reqBodyUser as string).toLowerCase() : null,
|
||||
dynamicEventSlugRef: !eventType.id ? eventType.slug : null,
|
||||
dynamicGroupSlugRef: !eventType.id ? (reqBody.user as string).toLowerCase() : null,
|
||||
iCalUID: evt.iCalUID ?? "",
|
||||
user: {
|
||||
connect: {
|
||||
id: organizerUser.id,
|
||||
id: eventType.organizerUser.id,
|
||||
},
|
||||
},
|
||||
destinationCalendar:
|
||||
@@ -240,24 +186,28 @@ function buildNewBookingData(params: {
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (reqBodyRecurringEventId) {
|
||||
newBookingData.recurringEventId = reqBodyRecurringEventId;
|
||||
if (reqBody.recurringEventId) {
|
||||
newBookingData.recurringEventId = reqBody.recurringEventId;
|
||||
}
|
||||
|
||||
if (originalRescheduledBooking) {
|
||||
newBookingData.metadata = {
|
||||
...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata),
|
||||
...reqBodyMetadata,
|
||||
...reqBody.metadata,
|
||||
};
|
||||
newBookingData.paid = originalRescheduledBooking.paid;
|
||||
newBookingData.fromReschedule = originalRescheduledBooking.uid;
|
||||
if (originalRescheduledBooking.uid) {
|
||||
newBookingData.cancellationReason = rescheduleReason;
|
||||
newBookingData.cancellationReason = input.rescheduleReason;
|
||||
}
|
||||
// Reschedule logic with booking with seats
|
||||
if (newBookingData.attendees?.createMany?.data && eventType?.seatsPerTimeSlot && bookerEmail) {
|
||||
if (
|
||||
newBookingData.attendees?.createMany?.data &&
|
||||
eventType?.eventTypeData?.seatsPerTimeSlot &&
|
||||
input.bookerEmail
|
||||
) {
|
||||
newBookingData.attendees.createMany.data = attendeesData.filter(
|
||||
(attendee) => attendee.email === bookerEmail
|
||||
(attendee) => attendee.email === input.bookerEmail
|
||||
);
|
||||
}
|
||||
if (originalRescheduledBooking.recurringEventId) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type z from "zod";
|
||||
|
||||
import { slugify } from "@calcom/lib/slugify";
|
||||
import type { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import type { getEventTypeResponse } from "./types";
|
||||
|
||||
type CustomInputs = z.infer<typeof bookingCreateSchemaLegacyPropsForApi>["customInputs"];
|
||||
|
||||
type RequestBody = {
|
||||
responses?: Record<string, object>;
|
||||
customInputs?: CustomInputs;
|
||||
};
|
||||
|
||||
function mapCustomInputs(
|
||||
customInputs: { label: string; value: CustomInputs[number]["value"] }[]
|
||||
): Record<string, CustomInputs[number]["value"]> {
|
||||
return customInputs.reduce((acc, { label, value }) => {
|
||||
acc[label] = value;
|
||||
return acc;
|
||||
}, {} as Record<string, CustomInputs[number]["value"]>);
|
||||
}
|
||||
|
||||
function mapResponsesToCustomInputs(
|
||||
responses: Record<string, object>,
|
||||
eventTypeCustomInputs: getEventTypeResponse["customInputs"]
|
||||
): NonNullable<CalendarEvent["customInputs"]> {
|
||||
// Backward Compatibility: Map new `responses` to old `customInputs` format so that webhooks can still receive same values.
|
||||
return Object.entries(responses).reduce((acc, [fieldName, fieldValue]) => {
|
||||
const foundInput = eventTypeCustomInputs.find((input) => slugify(input.label) === fieldName);
|
||||
if (foundInput) {
|
||||
acc[foundInput.label] = fieldValue;
|
||||
}
|
||||
return acc;
|
||||
}, {} as NonNullable<CalendarEvent["customInputs"]>);
|
||||
}
|
||||
|
||||
export function getCustomInputsResponses(
|
||||
reqBody: RequestBody,
|
||||
eventTypeCustomInputs: getEventTypeResponse["customInputs"]
|
||||
): NonNullable<CalendarEvent["customInputs"]> {
|
||||
if (reqBody.customInputs && reqBody.customInputs.length > 0) {
|
||||
return mapCustomInputs(reqBody.customInputs);
|
||||
}
|
||||
|
||||
const responses = reqBody.responses || {};
|
||||
return mapResponsesToCustomInputs(responses, eventTypeCustomInputs);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { loadAndValidateUsers } from "./loadAndValidateUsers";
|
||||
|
||||
type Users = Awaited<ReturnType<typeof loadAndValidateUsers>>;
|
||||
|
||||
const sortUsersByDynamicList = (users: Users, dynamicUserList: string[]) => {
|
||||
return users.sort((a, b) => {
|
||||
const aIndex = (a.username && dynamicUserList.indexOf(a.username)) || 0;
|
||||
const bIndex = (b.username && dynamicUserList.indexOf(b.username)) || 0;
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
};
|
||||
|
||||
export const getLocationValuesForDb = (
|
||||
dynamicUserList: string[],
|
||||
users: Users,
|
||||
locationBodyString: string
|
||||
) => {
|
||||
// TODO: It's definition should be moved to getLocationValueForDb
|
||||
let organizerOrFirstDynamicGroupMemberDefaultLocationUrl;
|
||||
if (dynamicUserList.length > 1) {
|
||||
users = sortUsersByDynamicList(users, dynamicUserList);
|
||||
const firstUsersMetadata = userMetadataSchema.parse(users[0].metadata);
|
||||
locationBodyString = firstUsersMetadata?.defaultConferencingApp?.appLink || locationBodyString;
|
||||
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
|
||||
firstUsersMetadata?.defaultConferencingApp?.appLink;
|
||||
}
|
||||
return { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl };
|
||||
};
|
||||
@@ -1,59 +1,14 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { BookingRepository } from "@calcom/lib/server/repository/booking";
|
||||
|
||||
import { validateOriginalRescheduledBooking } from "./validateOriginalRescheduledBooking";
|
||||
|
||||
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,
|
||||
phoneNumber: 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,
|
||||
},
|
||||
});
|
||||
const originalBooking = await BookingRepository.findOriginalRescheduledBooking(uid, seatsEventType);
|
||||
validateOriginalRescheduledBooking(originalBooking);
|
||||
|
||||
return originalBooking;
|
||||
}
|
||||
|
||||
export type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
||||
|
||||
@@ -69,5 +69,3 @@ function determineIsConfirmedByDefault(
|
||||
): boolean {
|
||||
return (!requiresConfirmation && price === 0) || userReschedulingIsOwner;
|
||||
}
|
||||
|
||||
export type IsConfirmedByDefault = ReturnType<typeof getRequiresConfirmationFlags>["isConfirmedByDefault"];
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { BookingSeat } from "../handleSeats/types";
|
||||
|
||||
export const getSeatedBooking = async (bookingSeatUid: string): Promise<BookingSeat | null> => {
|
||||
// rescheduleUid can be bookingUid and bookingSeatUid
|
||||
return prisma.bookingSeat.findUnique({
|
||||
where: {
|
||||
referenceUid: bookingSeatUid,
|
||||
},
|
||||
include: {
|
||||
booking: true,
|
||||
attendee: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { AdditionalInformation } from "@calcom/types/Calendar";
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
|
||||
type ExtraAdditionalInfo = AdditionalInformation & {
|
||||
url?: string | undefined;
|
||||
iCalUID?: string | undefined;
|
||||
};
|
||||
|
||||
type VideoResult = EventResult<ExtraAdditionalInfo>;
|
||||
|
||||
function extractUpdatedVideoEvent(result: VideoResult | undefined): ExtraAdditionalInfo | undefined {
|
||||
if (!result || !result.success) return undefined;
|
||||
return Array.isArray(result.updatedEvent) ? result.updatedEvent[0] : result.updatedEvent;
|
||||
}
|
||||
|
||||
function extractMetadata(event: ExtraAdditionalInfo): AdditionalInformation {
|
||||
return {
|
||||
hangoutLink: event.hangoutLink,
|
||||
conferenceData: event.conferenceData,
|
||||
entryPoints: event.entryPoints,
|
||||
};
|
||||
}
|
||||
|
||||
export function getVideoCallDetails({ results }: { results: VideoResult[] }) {
|
||||
const firstVideoResult = results.find((result) => result.type.includes("_video"));
|
||||
const updatedVideoEvent = extractUpdatedVideoEvent(firstVideoResult);
|
||||
const metadata = updatedVideoEvent ? extractMetadata(updatedVideoEvent) : {};
|
||||
|
||||
const videoCallUrl = metadata.hangoutLink || updatedVideoEvent?.url;
|
||||
|
||||
return { videoCallUrl, metadata, updatedVideoEvent };
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { Logger } from "tslog";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { getPiiFreeUser } from "@calcom/lib/piiFreeData";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { userSelect } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import { loadUsers } from "./loadUsers";
|
||||
import type { NewBookingEventType } from "./types";
|
||||
|
||||
type Users = (Awaited<ReturnType<typeof loadUsers>>[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
})[];
|
||||
|
||||
type EventType = Pick<NewBookingEventType, "hosts" | "users" | "id" | "userId" | "schedulingType">;
|
||||
|
||||
type InputProps = {
|
||||
req: IncomingMessage;
|
||||
eventType: EventType;
|
||||
eventTypeId: number;
|
||||
dynamicUserList: string[];
|
||||
logger: Logger<unknown>;
|
||||
};
|
||||
|
||||
export async function loadAndValidateUsers({
|
||||
req,
|
||||
eventType,
|
||||
eventTypeId,
|
||||
dynamicUserList,
|
||||
logger,
|
||||
}: InputProps): Promise<Users> {
|
||||
let users: Users = await loadUsers(eventType, dynamicUserList, req);
|
||||
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
|
||||
if (!isDynamicAllowed && !eventTypeId) {
|
||||
logger.warn({
|
||||
message: "NewBooking: Some of the users in this group do not allow dynamic booking",
|
||||
});
|
||||
throw new HttpError({
|
||||
message: "Some of the users in this group do not allow dynamic booking",
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// If this event was pre-relationship migration
|
||||
// TODO: Establish whether this is dead code.
|
||||
if (!users.length && eventType.userId) {
|
||||
const eventTypeUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: eventType.userId,
|
||||
},
|
||||
select: {
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
}, // Don't leak to client
|
||||
...userSelect.select,
|
||||
},
|
||||
});
|
||||
if (!eventTypeUser) {
|
||||
logger.warn({ message: "NewBooking: eventTypeUser.notFound" });
|
||||
throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
||||
}
|
||||
users.push(eventTypeUser);
|
||||
}
|
||||
|
||||
if (!users) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
||||
|
||||
users = users.map((user) => ({
|
||||
...user,
|
||||
isFixed:
|
||||
user.isFixed === false
|
||||
? false
|
||||
: user.isFixed || eventType.schedulingType !== SchedulingType.ROUND_ROBIN,
|
||||
}));
|
||||
|
||||
logger.debug(
|
||||
"Concerned users",
|
||||
safeStringify({
|
||||
users: users.map(getPiiFreeUser),
|
||||
})
|
||||
);
|
||||
|
||||
return users;
|
||||
}
|
||||
@@ -88,4 +88,4 @@ export const findUsersByUsername = async ({
|
||||
});
|
||||
};
|
||||
|
||||
export type AwaitedLoadUsers = Awaited<ReturnType<typeof loadUsers>>;
|
||||
export type LoadedUsers = Awaited<ReturnType<typeof loadUsers>>;
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { DefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import type { PaymentAppData } from "@calcom/lib/getPaymentAppData";
|
||||
import type { userSelect } from "@calcom/prisma";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
@@ -21,12 +21,11 @@ import type {
|
||||
} from "./getBookingData";
|
||||
import type { getEventTypeResponse } from "./getEventTypesFromDB";
|
||||
import type { BookingType, OriginalRescheduledBooking } from "./getOriginalRescheduledBooking";
|
||||
import type { getRequiresConfirmationFlags } from "./getRequiresConfirmationFlags";
|
||||
import type { AwaitedLoadUsers } from "./loadUsers";
|
||||
import type { LoadedUsers } from "./loadUsers";
|
||||
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
|
||||
export type OrganizerUser = AwaitedLoadUsers[number] & {
|
||||
export type OrganizerUser = LoadedUsers[number] & {
|
||||
isFixed?: boolean;
|
||||
metadata?: Prisma.JsonValue;
|
||||
};
|
||||
@@ -62,9 +61,7 @@ export type IsFixedAwareUser = User & {
|
||||
weightAdjustment?: number;
|
||||
};
|
||||
|
||||
export type NewBookingEventType = AwaitedGetDefaultEvent | getEventTypeResponse;
|
||||
|
||||
export type IsConfirmedByDefault = ReturnType<typeof getRequiresConfirmationFlags>["isConfirmedByDefault"];
|
||||
export type NewBookingEventType = DefaultEvent | getEventTypeResponse;
|
||||
|
||||
export type {
|
||||
AwaitedBookingData,
|
||||
@@ -79,6 +76,6 @@ export type {
|
||||
BookingType,
|
||||
Booking,
|
||||
OriginalRescheduledBooking,
|
||||
AwaitedLoadUsers,
|
||||
LoadedUsers,
|
||||
getEventTypeResponse,
|
||||
};
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import type { Logger } from "tslog";
|
||||
|
||||
import { getUTCOffsetByTimezone } from "@calcom/lib/date-fns";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
|
||||
import type { EventType } from "@calcom/prisma/client";
|
||||
|
||||
type ValidateBookingTimeEventType = Pick<
|
||||
EventType,
|
||||
| "periodType"
|
||||
| "periodDays"
|
||||
| "periodEndDate"
|
||||
| "periodStartDate"
|
||||
| "periodCountCalendarDays"
|
||||
| "minimumBookingNotice"
|
||||
| "eventName"
|
||||
| "id"
|
||||
>;
|
||||
|
||||
export const validateBookingTimeIsNotOutOfBounds = async <T extends ValidateBookingTimeEventType>(
|
||||
reqBodyStartTime: string,
|
||||
reqBodyTimeZone: string,
|
||||
eventType: T,
|
||||
eventTimeZone: string | null | undefined,
|
||||
logger: Logger<unknown>
|
||||
) => {
|
||||
let timeOutOfBounds = false;
|
||||
try {
|
||||
timeOutOfBounds = isOutOfBounds(
|
||||
reqBodyStartTime,
|
||||
{
|
||||
periodType: eventType.periodType,
|
||||
periodDays: eventType.periodDays,
|
||||
periodEndDate: eventType.periodEndDate,
|
||||
periodStartDate: eventType.periodStartDate,
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays,
|
||||
bookerUtcOffset: getUTCOffsetByTimezone(reqBodyTimeZone) ?? 0,
|
||||
eventUtcOffset: eventTimeZone ? getUTCOffsetByTimezone(eventTimeZone) ?? 0 : 0,
|
||||
},
|
||||
eventType.minimumBookingNotice
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn({
|
||||
message: "NewBooking: Unable set timeOutOfBounds. Using false. ",
|
||||
});
|
||||
if (error instanceof BookingDateInPastError) {
|
||||
logger.info(`Booking eventType ${eventType.id} failed`, JSON.stringify({ error }));
|
||||
throw new HttpError({ statusCode: 400, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (timeOutOfBounds) {
|
||||
const error = {
|
||||
errorCode: "BookingTimeOutOfBounds",
|
||||
message: `EventType '${eventType.eventName}' cannot be booked at this time.`,
|
||||
};
|
||||
logger.warn({
|
||||
message: `NewBooking: EventType '${eventType.eventName}' cannot be booked at this time.`,
|
||||
});
|
||||
throw new HttpError({ statusCode: 400, message: error.message });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Logger } from "tslog";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
type Props = {
|
||||
reqBodyStart: string;
|
||||
reqBodyEnd: string;
|
||||
eventTypeMultipleDuration?: number[];
|
||||
eventTypeLength: number;
|
||||
logger: Logger<unknown>;
|
||||
};
|
||||
|
||||
export const validateEventLength = ({
|
||||
reqBodyStart,
|
||||
reqBodyEnd,
|
||||
eventTypeMultipleDuration,
|
||||
eventTypeLength,
|
||||
logger,
|
||||
}: Props) => {
|
||||
const reqEventLength = dayjs(reqBodyEnd).diff(dayjs(reqBodyStart), "minutes");
|
||||
const validEventLengths = eventTypeMultipleDuration?.length ? eventTypeMultipleDuration : [eventTypeLength];
|
||||
if (!validEventLengths.includes(reqEventLength)) {
|
||||
logger.warn({ message: "NewBooking: Invalid event length" });
|
||||
throw new HttpError({ statusCode: 400, message: "Invalid event length" });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { type OriginalRescheduledBooking } from "./getOriginalRescheduledBooking";
|
||||
|
||||
export const validateOriginalRescheduledBooking = async (
|
||||
originalRescheduledBooking: OriginalRescheduledBooking
|
||||
) => {
|
||||
if (!originalRescheduledBooking) {
|
||||
throw new HttpError({ statusCode: 404, message: "Could not find original booking" });
|
||||
}
|
||||
|
||||
if (
|
||||
originalRescheduledBooking.status === BookingStatus.CANCELLED &&
|
||||
!originalRescheduledBooking.rescheduled
|
||||
) {
|
||||
throw new HttpError({ statusCode: 403, message: ErrorCode.CancelledBookingsCannotBeRescheduled });
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -10,7 +10,7 @@ import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import type { createLoggerWithEventDetails } from "../../../handleNewBooking";
|
||||
import { addVideoCallDataToEvent } from "../../../handleNewBooking";
|
||||
import { addVideoCallDataToEvent } from "../../../handleNewBooking/addVideoCallDataToEvent";
|
||||
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
|
||||
import type { SeatedBooking, RescheduleSeatedBookingObject, NewTimeSlotBooking } from "../../types";
|
||||
|
||||
|
||||
+1
-1
@@ -6,8 +6,8 @@ import { sendRescheduledEmailsAndSMS } from "@calcom/emails";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AdditionalInformation, AppsStatus } from "@calcom/types/Calendar";
|
||||
|
||||
import { addVideoCallDataToEvent } from "../../../handleNewBooking";
|
||||
import type { createLoggerWithEventDetails } from "../../../handleNewBooking";
|
||||
import { addVideoCallDataToEvent } from "../../../handleNewBooking/addVideoCallDataToEvent";
|
||||
import { findBookingQuery } from "../../../handleNewBooking/findBookingQuery";
|
||||
import { handleAppsStatus } from "../../../handleNewBooking/handleAppsStatus";
|
||||
import type { Booking } from "../../../handleNewBooking/types";
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export type NewSeatedBookingObject = {
|
||||
rescheduleReason: RescheduleReason;
|
||||
reqBodyUser: string | string[] | undefined;
|
||||
noEmail: NoEmail;
|
||||
isConfirmedByDefault: IsConfirmedByDefault;
|
||||
isConfirmedByDefault: boolean;
|
||||
additionalNotes: AdditionalNotes;
|
||||
reqAppsStatus: ReqAppsStatus;
|
||||
attendeeLanguage: string | null;
|
||||
|
||||
@@ -8,8 +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 { getCustomInputsResponses } from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { getBookingData } from "@calcom/features/bookings/lib/handleNewBooking/getBookingData";
|
||||
import { getCustomInputsResponses } from "@calcom/features/bookings/lib/handleNewBooking/getCustomInputsResponses";
|
||||
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
|
||||
import { getFullName } from "@calcom/features/form-builder/utils";
|
||||
import { sendNotification } from "@calcom/features/notifications/sendNotification";
|
||||
|
||||
@@ -172,4 +172,4 @@ export const getUsernameList = (users: string | string[] | undefined): string[]
|
||||
|
||||
export default defaultEvents;
|
||||
|
||||
export type AwaitedGetDefaultEvent = Awaited<ReturnType<typeof getDefaultEvent>>;
|
||||
export type DefaultEvent = Awaited<ReturnType<typeof getDefaultEvent>>;
|
||||
|
||||
@@ -336,4 +336,57 @@ export class BookingRepository {
|
||||
|
||||
return [...collectiveRoundRobinBookingsOwner, ...collectiveRoundRobinBookingsAttendee];
|
||||
}
|
||||
|
||||
static async findOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
|
||||
return await 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,
|
||||
phoneNumber: 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user