diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index eda60308f49372..887fda3edc1866 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -28,7 +28,6 @@ import { sendScheduledEmailsAndSMS, } from "@calcom/emails"; import getICalUID from "@calcom/emails/lib/getICalUID"; -import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled"; import { @@ -38,13 +37,8 @@ import { import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import { getFullName } from "@calcom/features/form-builder/utils"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; -import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; -import { - deleteWebhookScheduledTriggers, - scheduleTrigger, -} from "@calcom/features/webhooks/lib/scheduleTrigger"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; -import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import { extractBaseEmail } from "@calcom/lib/extract-base-email"; @@ -65,7 +59,7 @@ import prisma from "@calcom/prisma"; import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums"; import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util"; -import type { AdditionalInformation, AppsStatus, CalendarEvent, Person } from "@calcom/types/Calendar"; +import type { AdditionalInformation, CalendarEvent, Person } from "@calcom/types/Calendar"; import type { EventResult, PartialReference } from "@calcom/types/EventManager"; import type { EventPayloadType, EventTypeInfo } from "../../webhooks/lib/sendPayload"; @@ -73,13 +67,14 @@ import { getAllCredentials } from "./getAllCredentialsForUsersOnEvent/getAllCred import { refreshCredentials } from "./getAllCredentialsForUsersOnEvent/refreshCredentials"; import getBookingDataSchema from "./getBookingDataSchema"; import { addVideoCallDataToEvent } from "./handleNewBooking/addVideoCallDataToEvent"; +import { buildLuckyUsersWithJustContactOwner } from "./handleNewBooking/buildLuckyUsersWithJustContactOwner"; 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 { getEventType } from "./handleNewBooking/getEventType"; import type { getEventTypeResponse } from "./handleNewBooking/getEventTypesFromDB"; import { getLocationValuesForDb } from "./handleNewBooking/getLocationValuesForDb"; import { getOriginalRescheduledBooking } from "./handleNewBooking/getOriginalRescheduledBooking"; @@ -89,13 +84,15 @@ import { getVideoCallDetails } from "./handleNewBooking/getVideoCallDetails"; import { handleAppsStatus } from "./handleNewBooking/handleAppsStatus"; import { loadAndValidateUsers } from "./handleNewBooking/loadAndValidateUsers"; import { scheduleNoShowTriggers } from "./handleNewBooking/scheduleNoShowTriggers"; +import { scheduleWebhookTriggerEvents } from "./handleNewBooking/scheduleWebhookTriggerEvents"; import type { - Booking, BookingType, + BookingTypeWithAppsStatus, IEventTypePaymentCredentialType, Invitee, IsFixedAwareUser, } from "./handleNewBooking/types"; +import { assertNonEmptyArray } from "./handleNewBooking/utils"; import { validateBookingTimeIsNotOutOfBounds } from "./handleNewBooking/validateBookingTimeIsNotOutOfBounds"; import { validateEventLength } from "./handleNewBooking/validateEventLength"; import handleSeats from "./handleSeats/handleSeats"; @@ -113,12 +110,6 @@ export const createLoggerWithEventDetails = ( }); }; -function assertNonEmptyArray(arr: T[]): asserts arr is [T, ...T[]] { - if (arr.length === 0) { - throw new Error("Array should have at least one item, but it's empty"); - } -} - function getICalSequence(originalRescheduledBooking: BookingType | null) { // If new booking set the sequence to 0 if (!originalRescheduledBooking) { @@ -134,68 +125,98 @@ function getICalSequence(originalRescheduledBooking: BookingType | null) { return originalRescheduledBooking.iCalSequence + 1; } -const getEventType = async ({ +const checkIsFirstSeat = async ({ + seatsPerTimeSlot, + reqBodyStart, eventTypeId, - eventTypeSlug, }: { eventTypeId: number; - eventTypeSlug?: string; + seatsPerTimeSlot: number | null; + reqBodyStart: string; }) => { - // handle dynamic user - const eventType = - !eventTypeId && !!eventTypeSlug ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId); + if (!seatsPerTimeSlot) return true; - const isOrgTeamEvent = !!eventType?.team && !!eventType?.team?.parentId; + const booking = await prisma.booking.findFirst({ + where: { + eventTypeId, + startTime: new Date(dayjs(reqBodyStart).utc().format()), + status: BookingStatus.ACCEPTED, + }, + }); - return { - ...eventType, - bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }), - }; -}; + if (booking) { + return false; + } -type BookingDataSchemaGetter = - | typeof getBookingDataSchema - | typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default; + return true; +}; -/** - * Adds the contact owner to be the only lucky user - * @returns - */ -function buildLuckyUsersWithJustContactOwner({ - contactOwnerEmail, - availableUsers, - fixedUserPool, +const getGuests = async ({ + reqGuests, + attendeeTimezone, + isTeamEventType, + users, }: { - contactOwnerEmail: string | null; - availableUsers: IsFixedAwareUser[]; - fixedUserPool: IsFixedAwareUser[]; -}) { - const luckyUsers: Awaited> = []; - if (!contactOwnerEmail) { - return luckyUsers; - } + reqGuests: string[] | undefined; + attendeeTimezone: string; + isTeamEventType: boolean; + users: Awaited>; +}) => { + const tGuests = await getTranslation("en", "common"); - const isContactOwnerAFixedHostAlready = fixedUserPool.some((user) => user.email === contactOwnerEmail); - if (isContactOwnerAFixedHostAlready) { - return luckyUsers; - } + const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS + ? process.env.BLACKLISTED_GUEST_EMAILS.split(",") + : []; + + const guestsRemoved: string[] = []; + + const guests = (reqGuests || []).reduce((guestArray, guest) => { + const baseGuestEmail = extractBaseEmail(guest).toLowerCase(); + if (blacklistedGuestEmails.some((e) => e.toLowerCase() === baseGuestEmail)) { + guestsRemoved.push(guest); + return guestArray; + } + // If it's a team event, remove the team member from guests + if (isTeamEventType && users.some((user) => user.email === guest)) { + return guestArray; + } + guestArray.push({ + email: guest, + name: "", + firstName: "", + lastName: "", + timeZone: attendeeTimezone, + language: { translate: tGuests, locale: "en" }, + }); + return guestArray; + }, [] as Invitee); - const teamMember = availableUsers.find((user) => user.email === contactOwnerEmail); - if (teamMember) { - luckyUsers.push(teamMember); + if (guestsRemoved.length > 0) { + log.info("Removed guests from the booking", guestsRemoved); } - return luckyUsers; -} + + return guests; +}; + +type TEventTypeWithUsers = getEventTypeResponse & { + users: IsFixedAwareUser[]; +}; + +type BookingDataSchemaGetter = + | typeof getBookingDataSchema + | typeof import("@calcom/features/bookings/lib/getBookingDataSchemaForApi").default; + +type HandlerReqType = NextApiRequest & { + userId?: number | undefined; + platformClientId?: string; + platformRescheduleUrl?: string; + platformCancelUrl?: string; + platformBookingUrl?: string; + platformBookingLocation?: string; +}; async function handler( - req: NextApiRequest & { - userId?: number | undefined; - platformClientId?: string; - platformRescheduleUrl?: string; - platformCancelUrl?: string; - platformBookingUrl?: string; - platformBookingLocation?: string; - }, + req: HandlerReqType, bookingDataSchemaGetter: BookingDataSchemaGetter = getBookingDataSchema ) { const { @@ -254,8 +275,6 @@ async function handler( } const fullName = getFullName(bookerName); - // Why are we only using "en" locale - const tGuests = await getTranslation("en", "common"); const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user); if (!eventType) throw new HttpError({ statusCode: 404, message: "event_type_not_found" }); @@ -344,25 +363,16 @@ async function handler( : null; let luckyUserResponse; - let isFirstSeat = true; - if (eventType.seatsPerTimeSlot) { - const booking = await prisma.booking.findFirst({ - where: { - eventTypeId: eventType.id, - startTime: new Date(dayjs(reqBody.start).utc().format()), - status: BookingStatus.ACCEPTED, - }, - }); - - if (booking) isFirstSeat = false; - } + const isFirstSeat = await checkIsFirstSeat({ + seatsPerTimeSlot: eventType.seatsPerTimeSlot, + reqBodyStart: reqBody.start, + eventTypeId, + }); //checks what users are available if (isFirstSeat) { - const eventTypeWithUsers: getEventTypeResponse & { - users: IsFixedAwareUser[]; - } = { + const eventTypeWithUsers: TEventTypeWithUsers = { ...eventType, users: users as IsFixedAwareUser[], ...(eventType.recurringEvent && { @@ -372,6 +382,7 @@ async function handler( }, }), }; + if (req.body.allRecurringDates && req.body.isFirstRecurringSlot) { const isTeamEvent = eventType.schedulingType === SchedulingType.COLLECTIVE || @@ -381,38 +392,32 @@ async function handler( ? eventTypeWithUsers.users.filter((user: IsFixedAwareUser) => user.isFixed) : []; - for ( - let i = 0; - i < req.body.allRecurringDates.length && i < req.body.numSlotsToCheckForAvailability; - i++ - ) { - const start = req.body.allRecurringDates[i].start; - const end = req.body.allRecurringDates[i].end; + const numSlotsToCheck = Math.min( + req.body.allRecurringDates.length, + req.body.numSlotsToCheckForAvailability + ); + + for (let i = 0; i < numSlotsToCheck; i++) { + const { start, end } = req.body.allRecurringDates[i]; + + const input = { + dateFrom: dayjs(start).tz(reqBody.timeZone).format(), + dateTo: dayjs(end).tz(reqBody.timeZone).format(), + timeZone: reqBody.timeZone, + originalRescheduledBooking, + }; + if (isTeamEvent) { // each fixed user must be available for (const key in fixedUsers) { await ensureAvailableUsers( { ...eventTypeWithUsers, users: [fixedUsers[key]] }, - { - dateFrom: dayjs(start).tz(reqBody.timeZone).format(), - dateTo: dayjs(end).tz(reqBody.timeZone).format(), - timeZone: reqBody.timeZone, - originalRescheduledBooking, - }, + input, loggerWithEventDetails ); } } else { - await ensureAvailableUsers( - eventTypeWithUsers, - { - dateFrom: dayjs(start).tz(reqBody.timeZone).format(), - dateTo: dayjs(end).tz(reqBody.timeZone).format(), - timeZone: reqBody.timeZone, - originalRescheduledBooking, - }, - loggerWithEventDetails - ); + await ensureAvailableUsers(eventTypeWithUsers, input, loggerWithEventDetails); } } } @@ -462,6 +467,7 @@ async function handler( // freeUsers is ensured const originalRescheduledBookingUserId = originalRescheduledBooking && originalRescheduledBooking.userId; + const isSameRoundRobinHost = !!originalRescheduledBookingUserId && eventType.schedulingType === SchedulingType.ROUND_ROBIN && @@ -500,19 +506,22 @@ async function handler( eventType, routingFormResponse: routingFormResponse ?? null, }); + if (!newLuckyUser) { break; // prevent infinite loop } + if (req.body.isFirstRecurringSlot && eventType.schedulingType === SchedulingType.ROUND_ROBIN) { // for recurring round robin events check if lucky user is available for next slots + + const numSlotsToCheck = Math.min( + req.body.allRecurringDates.length, + req.body.numSlotsToCheckForAvailability + ); + try { - for ( - let i = 0; - i < req.body.allRecurringDates.length && i < req.body.numSlotsToCheckForAvailability; - i++ - ) { - const start = req.body.allRecurringDates[i].start; - const end = req.body.allRecurringDates[i].end; + for (let i = 0; i < numSlotsToCheck; i++) { + const { start, end } = req.body.allRecurringDates[i]; await ensureAvailableUsers( { ...eventTypeWithUsers, users: [newLuckyUser] }, @@ -591,19 +600,19 @@ async function handler( // If location passed is empty , use default location of event // If location of event is not set , use host default if (locationBodyString.trim().length == 0) { - if (eventType.locations.length > 0) { - locationBodyString = eventType.locations[0].type; - } else { - locationBodyString = OrganizerDefaultConferencingAppType; - } + locationBodyString = + eventType.locations.length > 0 ? eventType.locations[0].type : OrganizerDefaultConferencingAppType; } // use host default if (locationBodyString == OrganizerDefaultConferencingAppType) { const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata); const organizerMetadata = metadataParseResult.success ? metadataParseResult.data : undefined; - if (organizerMetadata?.defaultConferencingApp?.appSlug) { - const app = getAppFromSlug(organizerMetadata?.defaultConferencingApp?.appSlug); + const defaultAppSlug = organizerMetadata?.defaultConferencingApp?.appSlug; + + if (defaultAppSlug) { + const app = getAppFromSlug(defaultAppSlug); locationBodyString = app?.appData?.location?.type || locationBodyString; + if (isManagedEventType || isTeamEventType) { organizerOrFirstDynamicGroupMemberDefaultLocationUrl = organizerMetadata?.defaultConferencingApp?.appLink; @@ -613,48 +622,6 @@ async function handler( } } - const invitee: Invitee = [ - { - email: bookerEmail, - name: fullName, - phoneNumber: bookerPhoneNumber, - firstName: (typeof bookerName === "object" && bookerName.firstName) || "", - lastName: (typeof bookerName === "object" && bookerName.lastName) || "", - timeZone: attendeeTimezone, - language: { translate: tAttendees, locale: attendeeLanguage ?? "en" }, - }, - ]; - - const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS - ? process.env.BLACKLISTED_GUEST_EMAILS.split(",") - : []; - - const guestsRemoved: string[] = []; - const guests = (reqGuests || []).reduce((guestArray, guest) => { - const baseGuestEmail = extractBaseEmail(guest).toLowerCase(); - if (blacklistedGuestEmails.some((e) => e.toLowerCase() === baseGuestEmail)) { - guestsRemoved.push(guest); - return guestArray; - } - // If it's a team event, remove the team member from guests - if (isTeamEventType && users.some((user) => user.email === guest)) { - return guestArray; - } - guestArray.push({ - email: guest, - name: "", - firstName: "", - lastName: "", - timeZone: attendeeTimezone, - language: { translate: tGuests, locale: "en" }, - }); - return guestArray; - }, [] as Invitee); - - if (guestsRemoved.length > 0) { - log.info("Removed guests from the booking", guestsRemoved); - } - const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`; const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL)); @@ -696,8 +663,28 @@ async function handler( }, }; }); + const teamMembers = await Promise.all(teamMemberPromises); + const invitee: Invitee = [ + { + email: bookerEmail, + name: fullName, + phoneNumber: bookerPhoneNumber, + firstName: (typeof bookerName === "object" && bookerName.firstName) || "", + lastName: (typeof bookerName === "object" && bookerName.lastName) || "", + timeZone: attendeeTimezone, + language: { translate: tAttendees, locale: attendeeLanguage ?? "en" }, + }, + ]; + + const guests = await getGuests({ + reqGuests, + isTeamEventType, + users, + attendeeTimezone, + }); + const attendeesList = [...invitee, ...guests]; const responses = reqBody.responses || null; @@ -731,9 +718,10 @@ async function handler( }); const organizerOrganizationId = organizerOrganizationProfile?.organizationId; - const bookerUrl = eventType.team - ? await getBookerBaseUrl(eventType.team.parentId) - : await getBookerBaseUrl(organizerOrganizationId ?? null); + + const bookerUrl = await getBookerBaseUrl( + eventType.team ? eventType.team.parentId : organizerOrganizationId ?? null + ); const destinationCalendar = eventType.destinationCalendar ? [eventType.destinationCalendar] @@ -823,41 +811,19 @@ async function handler( const orgId = await getOrgIdFromMemberOrTeamId({ memberId: organizerUserId, teamId }); - const subscriberOptions: GetSubscriberOptions = { - userId: organizerUserId, - eventTypeId, - triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, - teamId, - orgId, - oAuthClientId: platformClientId, - }; - - const eventTrigger: WebhookTriggerEvents = rescheduleUid - ? WebhookTriggerEvents.BOOKING_RESCHEDULED - : WebhookTriggerEvents.BOOKING_CREATED; - - subscriberOptions.triggerEvent = eventTrigger; - - const subscriberOptionsMeetingEnded = { - userId: triggerForUser ? organizerUser.id : null, - eventTypeId, - triggerEvent: WebhookTriggerEvents.MEETING_ENDED, - teamId, - orgId, - oAuthClientId: platformClientId, - }; + const workflows = await getAllWorkflowsFromEventType(eventType, organizerUser.id); - const subscriberOptionsMeetingStarted = { - userId: triggerForUser ? organizerUser.id : null, + const subscriberOptions: GetSubscriberOptions = { + userId, eventTypeId, - triggerEvent: WebhookTriggerEvents.MEETING_STARTED, + triggerEvent: rescheduleUid + ? WebhookTriggerEvents.BOOKING_RESCHEDULED + : WebhookTriggerEvents.BOOKING_CREATED, teamId, orgId, oAuthClientId: platformClientId, }; - const workflows = await getAllWorkflowsFromEventType(eventType, organizerUser.id); - if (isTeamEventType) { evt.team = { members: teamMembers, @@ -897,7 +863,7 @@ async function handler( eventTypeId, reqBodyMetadata: reqBody.metadata, subscriberOptions, - eventTrigger, + eventTrigger: subscriberOptions.triggerEvent, responses, workflows, rescheduledBy: reqBody.rescheduledBy, @@ -942,8 +908,7 @@ async function handler( let results: EventResult[] = []; let referencesToCreate: PartialReference[] = []; - let booking: (Booking & { appsStatus?: AppsStatus[]; paymentUid?: string; paymentId?: number }) | null = - null; + let booking: BookingTypeWithAppsStatus = null; loggerWithEventDetails.debug( "Going to create booking in DB now", @@ -1558,60 +1523,15 @@ async function handler( loggerWithEventDetails.debug(`Booking ${organizerUser.username} completed`); // We are here so, booking doesn't require payment and booking is also created in DB already, through createBooking call - if (isConfirmedByDefault) { - const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); - const subscribersMeetingStarted = await getWebhooks(subscriberOptionsMeetingStarted); - - let deleteWebhookScheduledTriggerPromise: Promise = Promise.resolve(); - const scheduleTriggerPromises = []; - - if (rescheduleUid && originalRescheduledBooking) { - //delete all scheduled triggers for meeting ended and meeting started of booking - deleteWebhookScheduledTriggerPromise = deleteWebhookScheduledTriggers({ - booking: originalRescheduledBooking, - }); - } - - if (booking && booking.status === BookingStatus.ACCEPTED) { - for (const subscriber of subscribersMeetingEnded) { - scheduleTriggerPromises.push( - scheduleTrigger({ - booking, - subscriberUrl: subscriber.subscriberUrl, - subscriber, - triggerEvent: WebhookTriggerEvents.MEETING_ENDED, - }) - ); - } - - for (const subscriber of subscribersMeetingStarted) { - scheduleTriggerPromises.push( - scheduleTrigger({ - booking, - subscriberUrl: subscriber.subscriberUrl, - subscriber, - triggerEvent: WebhookTriggerEvents.MEETING_STARTED, - }) - ); - } - } - - await Promise.all([deleteWebhookScheduledTriggerPromise, ...scheduleTriggerPromises]).catch((error) => { - loggerWithEventDetails.error( - "Error while scheduling or canceling webhook triggers", - JSON.stringify({ error }) - ); - }); - - // Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED - await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData }); - } else { - // if eventType requires confirmation we will trigger the BOOKING REQUESTED Webhook - const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REQUESTED; - subscriberOptions.triggerEvent = eventTrigger; - webhookData.status = "PENDING"; - await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData }); - } + await scheduleWebhookTriggerEvents({ + subscriberOptions, + loggerWithEventDetails, + rescheduleUid, + booking, + originalRescheduledBooking, + webhookData, + isConfirmedByDefault, + }); try { if (hasHashedBookingLink && reqBody.hashedLink) { diff --git a/packages/features/bookings/lib/handleNewBooking/buildLuckyUsersWithJustContactOwner.ts b/packages/features/bookings/lib/handleNewBooking/buildLuckyUsersWithJustContactOwner.ts new file mode 100644 index 00000000000000..e4486aa48f764c --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/buildLuckyUsersWithJustContactOwner.ts @@ -0,0 +1,32 @@ +import type { loadAndValidateUsers } from "./loadAndValidateUsers"; +import type { IsFixedAwareUser } from "./types"; + +/** + * Adds the contact owner to be the only lucky user + * @returns + */ +export function buildLuckyUsersWithJustContactOwner({ + contactOwnerEmail, + availableUsers, + fixedUserPool, +}: { + contactOwnerEmail: string | null; + availableUsers: IsFixedAwareUser[]; + fixedUserPool: IsFixedAwareUser[]; +}) { + const luckyUsers: Awaited> = []; + if (!contactOwnerEmail) { + return luckyUsers; + } + + const isContactOwnerAFixedHostAlready = fixedUserPool.some((user) => user.email === contactOwnerEmail); + if (isContactOwnerAFixedHostAlready) { + return luckyUsers; + } + + const teamMember = availableUsers.find((user) => user.email === contactOwnerEmail); + if (teamMember) { + luckyUsers.push(teamMember); + } + return luckyUsers; +} diff --git a/packages/features/bookings/lib/handleNewBooking/getEventType.ts b/packages/features/bookings/lib/handleNewBooking/getEventType.ts new file mode 100644 index 00000000000000..27e66a92c21a29 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/getEventType.ts @@ -0,0 +1,23 @@ +import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; +import { getDefaultEvent } from "@calcom/lib/defaultEvents"; + +import { getEventTypesFromDB } from "./getEventTypesFromDB"; + +export 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 }), + }; +}; diff --git a/packages/features/bookings/lib/handleNewBooking/scheduleWebhookTriggerEvents.ts b/packages/features/bookings/lib/handleNewBooking/scheduleWebhookTriggerEvents.ts new file mode 100644 index 00000000000000..481a8b93361973 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/scheduleWebhookTriggerEvents.ts @@ -0,0 +1,108 @@ +import type { Logger } from "tslog"; + +import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; +import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; +import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; +import { + deleteWebhookScheduledTriggers, + scheduleTrigger, +} from "@calcom/features/webhooks/lib/scheduleTrigger"; +import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums"; + +import type { EventPayloadType } from "../../../webhooks/lib/sendPayload"; +import type { BookingTypeWithAppsStatus, OriginalRescheduledBooking } from "./types"; + +type args = { + rescheduleUid?: string; + booking: BookingTypeWithAppsStatus; + originalRescheduledBooking: OriginalRescheduledBooking | null; + webhookData: EventPayloadType; + loggerWithEventDetails: Logger; + isConfirmedByDefault: boolean; + subscriberOptions: GetSubscriberOptions; +}; + +export const scheduleWebhookTriggerEvents = async ({ + subscriberOptions, + loggerWithEventDetails, + rescheduleUid, + booking, + originalRescheduledBooking, + webhookData, + isConfirmedByDefault, +}: args) => { + if (isConfirmedByDefault) { + const subscriberOptionsMeetingEnded = { + userId: subscriberOptions.userId, + eventTypeId: subscriberOptions.eventTypeId, + triggerEvent: WebhookTriggerEvents.MEETING_ENDED, + teamId: subscriberOptions.teamId, + orgId: subscriberOptions.orgId, + }; + + const subscriberOptionsMeetingStarted = { + userId: subscriberOptions.userId, + eventTypeId: subscriberOptions.eventTypeId, + triggerEvent: WebhookTriggerEvents.MEETING_STARTED, + teamId: subscriberOptions.teamId, + orgId: subscriberOptions.orgId, + }; + + const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); + const subscribersMeetingStarted = await getWebhooks(subscriberOptionsMeetingStarted); + + let deleteWebhookScheduledTriggerPromise: Promise = Promise.resolve(); + const scheduleTriggerPromises = []; + + if (rescheduleUid && originalRescheduledBooking) { + //delete all scheduled triggers for meeting ended and meeting started of booking + deleteWebhookScheduledTriggerPromise = deleteWebhookScheduledTriggers({ + booking: originalRescheduledBooking, + }); + } + + if (booking && booking.status === BookingStatus.ACCEPTED) { + for (const subscriber of subscribersMeetingEnded) { + scheduleTriggerPromises.push( + scheduleTrigger({ + booking, + subscriberUrl: subscriber.subscriberUrl, + subscriber, + triggerEvent: WebhookTriggerEvents.MEETING_ENDED, + }) + ); + } + + for (const subscriber of subscribersMeetingStarted) { + scheduleTriggerPromises.push( + scheduleTrigger({ + booking, + subscriberUrl: subscriber.subscriberUrl, + subscriber, + triggerEvent: WebhookTriggerEvents.MEETING_STARTED, + }) + ); + } + } + + await Promise.all([deleteWebhookScheduledTriggerPromise, ...scheduleTriggerPromises]).catch((error) => { + loggerWithEventDetails.error( + "Error while scheduling or canceling webhook triggers", + JSON.stringify({ error }) + ); + }); + + // Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED + await handleWebhookTrigger({ + subscriberOptions, + eventTrigger: subscriberOptions.triggerEvent, + webhookData, + }); + } else { + // if eventType requires confirmation we will trigger the BOOKING REQUESTED Webhook + const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REQUESTED; + subscriberOptions.triggerEvent = eventTrigger; + webhookData.status = "PENDING"; + await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData }); + } +}; diff --git a/packages/features/bookings/lib/handleNewBooking/types.ts b/packages/features/bookings/lib/handleNewBooking/types.ts index cacf5a581f5ed6..80b3cf17d03e94 100644 --- a/packages/features/bookings/lib/handleNewBooking/types.ts +++ b/packages/features/bookings/lib/handleNewBooking/types.ts @@ -6,6 +6,7 @@ import type { EventTypeAppsList } from "@calcom/app-store/utils"; import type { DefaultEvent } from "@calcom/lib/defaultEvents"; import type { PaymentAppData } from "@calcom/lib/getPaymentAppData"; import type { userSelect } from "@calcom/prisma"; +import type { AppsStatus } from "@calcom/types/Calendar"; import type { CredentialPayload } from "@calcom/types/Credential"; import type { Booking } from "./createBooking"; @@ -30,6 +31,10 @@ export type OrganizerUser = LoadedUsers[number] & { metadata?: Prisma.JsonValue; }; +export type BookingTypeWithAppsStatus = + | (Booking & { appsStatus?: AppsStatus[]; paymentUid?: string; paymentId?: number }) + | null; + export type Invitee = { email: string; name: string; diff --git a/packages/features/bookings/lib/handleNewBooking/utils.ts b/packages/features/bookings/lib/handleNewBooking/utils.ts new file mode 100644 index 00000000000000..5bef4e1226ae21 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/utils.ts @@ -0,0 +1,5 @@ +export function assertNonEmptyArray(arr: T[]): asserts arr is [T, ...T[]] { + if (arr.length === 0) { + throw new Error("Array should have at least one item, but it's empty"); + } +}