From 59faffe0d5596f3b1e14ff34ab9fcbbdea8ed616 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Fri, 13 Oct 2023 22:52:57 +0530 Subject: [PATCH] fix: Requires Confirmation when organizer reschedules (#11848) --- packages/core/EventManager.ts | 3 +- .../bookings/lib/handleBookingRequested.ts | 3 +- .../features/bookings/lib/handleNewBooking.ts | 102 ++++++--- .../test/fresh-booking.test.ts | 137 ++++++++++- .../handleNewBooking/test/reschedule.test.ts | 216 +++++++++++++++++- packages/lib/testEmails.ts | 4 + 6 files changed, 419 insertions(+), 46 deletions(-) diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 55f8abd6b0..2fea2ad8ca 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -454,7 +454,8 @@ export default class EventManager { // Because we are just cleaning up the events and meetings, we don't want to throw an error if one of them fails. (await Promise.allSettled(allPromises)).some((result) => { if (result.status === "rejected") { - log.error( + // Make it a soft error because in case a PENDING booking is rescheduled there would be no calendar events or video meetings. + log.warn( "Error deleting calendar event or video meeting for booking", safeStringify({ error: result.reason }) ); diff --git a/packages/features/bookings/lib/handleBookingRequested.ts b/packages/features/bookings/lib/handleBookingRequested.ts index e38f50d634..d7ba0b4a54 100644 --- a/packages/features/bookings/lib/handleBookingRequested.ts +++ b/packages/features/bookings/lib/handleBookingRequested.ts @@ -6,7 +6,7 @@ import logger from "@calcom/lib/logger"; import { WebhookTriggerEvents } from "@calcom/prisma/enums"; import type { CalendarEvent } from "@calcom/types/Calendar"; -const log = logger.getChildLogger({ prefix: ["[handleConfirmation] book:user"] }); +const log = logger.getChildLogger({ prefix: ["[handleBookingRequested] book:user"] }); /** * Supposed to do whatever is needed when a booking is requested. @@ -31,6 +31,7 @@ export async function handleBookingRequested(args: { }) { const { evt, booking } = args; + log.debug("Emails: Sending booking requested emails"); await sendOrganizerRequestEmail({ ...evt }); await sendAttendeeRequestEmail({ ...evt }, evt.attendees[0]); diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 005470f846..5b7df766ba 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -979,10 +979,17 @@ async function handler( const allCredentials = await getAllCredentials(organizerUser, eventType); - const isOrganizerRescheduling = organizerUser.id === userId; + const { userReschedulingIsOwner, isConfirmedByDefault } = getRequiresConfirmationFlags({ + eventType, + bookingStartTime: reqBody.start, + userId, + originalRescheduledBookingOrganizerId: originalRescheduledBooking?.user?.id, + paymentAppData, + }); + // If the Organizer himself is rescheduling, the booker should be sent the communication in his timezone and locale. const attendeeInfoOnReschedule = - isOrganizerRescheduling && originalRescheduledBooking + userReschedulingIsOwner && originalRescheduledBooking ? originalRescheduledBooking.attendees.find((attendee) => attendee.email === bookerEmail) : null; @@ -1088,14 +1095,6 @@ async function handler( t: tOrganizer, }; - let requiresConfirmation = eventType?.requiresConfirmation; - const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold; - if (rcThreshold) { - if (dayjs(dayjs(reqBody.start).utc().format()).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) { - requiresConfirmation = false; - } - } - const calEventUserFieldsResponses = "calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null; @@ -1128,7 +1127,7 @@ async function handler( ? [organizerUser.destinationCalendar] : null, hideCalendarNotes: eventType.hideCalendarNotes, - requiresConfirmation: requiresConfirmation ?? false, + requiresConfirmation: !isConfirmedByDefault, eventTypeId: eventType.id, // if seats are not enabled we should default true seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true, @@ -1210,7 +1209,6 @@ async function handler( const eventTypeInfo: EventTypeInfo = { eventTitle: eventType.title, eventDescription: eventType.description, - requiresConfirmation: requiresConfirmation || null, price: paymentAppData.price, currency: eventType.currency, length: eventType.length, @@ -1438,8 +1436,9 @@ async function handler( } } - if (noEmail !== true && (!requiresConfirmation || isOrganizerRescheduling)) { + if (noEmail !== true && isConfirmedByDefault) { const copyEvent = cloneDeep(evt); + loggerWithEventDetails.debug("Emails: Sending reschedule emails - handleSeats"); await sendRescheduledEmails({ ...copyEvent, additionalNotes, // Resets back to the additionalNote input and not the override value @@ -1558,8 +1557,9 @@ async function handler( ? calendarResult?.updatedEvent[0]?.iCalUID : calendarResult?.updatedEvent?.iCalUID || undefined; - if (!requiresConfirmation || isOrganizerRescheduling) { + if (noEmail !== true && isConfirmedByDefault) { // TODO send reschedule emails to attendees of the old booking + loggerWithEventDetails.debug("Emails: Sending reschedule emails - handleSeats"); await sendRescheduledEmails({ ...copyEvent, additionalNotes, // Resets back to the additionalNote input and not the override value @@ -1890,12 +1890,6 @@ async function handler( evt.recurringEvent = eventType.recurringEvent; } - // If the user is not the owner of the event, new booking should be always pending. - // Otherwise, an owner rescheduling should be always accepted. - // Before comparing make sure that userId is set, otherwise undefined === undefined - const userReschedulingIsOwner = userId && originalRescheduledBooking?.user?.id === userId; - const isConfirmedByDefault = (!requiresConfirmation && !paymentAppData.price) || userReschedulingIsOwner; - async function createBooking() { if (originalRescheduledBooking) { evt.title = originalRescheduledBooking?.title || evt.title; @@ -1914,12 +1908,6 @@ async function handler( const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null; const dynamicGroupSlugRef = !eventTypeId ? (reqBody.user as string).toLowerCase() : null; - // If the user is not the owner of the event, new booking should be always pending. - // Otherwise, an owner rescheduling should be always accepted. - // Before comparing make sure that userId is set, otherwise undefined === undefined - const userReschedulingIsOwner = userId && originalRescheduledBooking?.user?.id === userId; - const isConfirmedByDefault = (!requiresConfirmation && !paymentAppData.price) || userReschedulingIsOwner; - 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 @@ -2049,7 +2037,7 @@ async function handler( safeStringify({ organizerUser: organizerUser.id, attendeesList: attendeesList.map((guest) => ({ timeZone: guest.timeZone })), - requiresConfirmation, + requiresConfirmation: evt.requiresConfirmation, isConfirmedByDefault, userReschedulingIsOwner, }) @@ -2214,8 +2202,9 @@ async function handler( videoCallUrl = metadata.hangoutLink || videoCallUrl || updatedEvent?.url; } } - if (noEmail !== true && (!requiresConfirmation || isOrganizerRescheduling)) { + if (noEmail !== true && isConfirmedByDefault) { const copyEvent = cloneDeep(evt); + loggerWithEventDetails.debug("Emails: Sending rescheduled emails for booking confirmation"); await sendRescheduledEmails({ ...copyEvent, additionalInformation: metadata, @@ -2226,7 +2215,7 @@ async function handler( } // If it's not a reschedule, doesn't require confirmation and there's no price, // Create a booking - } else if (!requiresConfirmation && !paymentAppData.price) { + } else if (isConfirmedByDefault) { // Use EventManager to conditionally use all needed integrations. const createManager = await eventManager.create(evt); @@ -2333,7 +2322,7 @@ async function handler( } loggerWithEventDetails.debug( - "Sending scheduled emails for booking confirmation", + "Emails: Sending scheduled emails for booking confirmation", safeStringify({ calEvent: getPiiFreeCalendarEvent(evt), }) @@ -2351,6 +2340,16 @@ async function handler( ); } } + } else { + // If isConfirmedByDefault is false, then booking can't be considered ACCEPTED and thus EventManager has no role to play. Booking is created as PENDING + loggerWithEventDetails.debug( + `EventManager doesn't need to create or reschedule event for booking ${organizerUser.username}`, + safeStringify({ + calEvent: getPiiFreeCalendarEvent(evt), + isConfirmedByDefault, + paymentValue: paymentAppData.price, + }) + ); } const bookingRequiresPayment = @@ -2361,7 +2360,7 @@ async function handler( if (!isConfirmedByDefault && noEmail !== true && !bookingRequiresPayment) { loggerWithEventDetails.debug( - `Booking ${organizerUser.username} requires confirmation, sending request emails`, + `Emails: Booking ${organizerUser.username} requires confirmation, sending request emails`, safeStringify({ calEvent: getPiiFreeCalendarEvent(evt), }) @@ -2458,6 +2457,7 @@ async function handler( videoCallUrl = booking.location; } + // We are here so, booking doesn't require payment and booking is also created in DB already, through createBooking call if (isConfirmedByDefault) { try { const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); @@ -2479,7 +2479,7 @@ async function handler( // Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData }); - } else if (eventType.requiresConfirmation) { + } else { // if eventType requires confirmation we will trigger the BOOKING REQUESTED Webhook const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REQUESTED; subscriberOptions.triggerEvent = eventTrigger; @@ -2558,6 +2558,44 @@ async function handler( export default handler; +function getRequiresConfirmationFlags({ + eventType, + bookingStartTime, + userId, + paymentAppData, + originalRescheduledBookingOrganizerId, +}: { + eventType: Pick>, "metadata" | "requiresConfirmation">; + bookingStartTime: string; + userId: number | undefined; + paymentAppData: { price: number }; + originalRescheduledBookingOrganizerId: number | undefined; +}) { + let requiresConfirmation = eventType?.requiresConfirmation; + const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold; + if (rcThreshold) { + if (dayjs(dayjs(bookingStartTime).utc().format()).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) { + requiresConfirmation = false; + } + } + + // If the user is not the owner of the event, new booking should be always pending. + // Otherwise, an owner rescheduling should be always accepted. + // Before comparing make sure that userId is set, otherwise undefined === undefined + const userReschedulingIsOwner = !!(userId && originalRescheduledBookingOrganizerId === userId); + const isConfirmedByDefault = (!requiresConfirmation && !paymentAppData.price) || userReschedulingIsOwner; + return { + /** + * Organizer of the booking is rescheduling + */ + userReschedulingIsOwner, + /** + * Booking won't need confirmation to be ACCEPTED + */ + isConfirmedByDefault, + }; +} + function handleCustomInputs( eventTypeCustomInputs: EventTypeCustomInput[], reqCustomInputs: { diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index 8f3a35f22b..59ae2809e4 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -13,6 +13,7 @@ import { describe, expect } from "vitest"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { resetTestEmails } from "@calcom/lib/testEmails"; import { BookingStatus } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; import { @@ -1213,6 +1214,122 @@ describe("handleNewBooking", () => { timeout ); + /** + * NOTE: We might want to think about making the bookings get ACCEPTED automatically if the booker is the organizer of the event-type. This is a design decision it seems for now. + */ + test( + `should make a fresh booking in PENDING state even when the booker is the organizer of the event-type + 1. Should create a booking in the database with status PENDING + 2. Should send emails to the booker as well as organizer for booking request and awaiting approval + 3. Should trigger BOOKING_REQUESTED webhook + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const subscriberUrl = "http://my-webhook.example.com"; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + const scenarioData = getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl, + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 45, + requiresConfirmation: true, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }); + await createBookingScenario(scenarioData); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + req.userId = organizer.id; + + const createdBooking = await handleNewBooking(req); + + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.PENDING, + location: BookingLocations.CalVideo, + responses: expect.objectContaining({ + email: booker.email, + name: booker.name, + }), + }); + + expectWorkflowToBeTriggered(); + + expectBookingRequestedEmails({ + booker, + organizer, + emails, + }); + + expectBookingRequestedWebhookToHaveBeenFired({ + booker, + organizer, + location: BookingLocations.CalVideo, + subscriberUrl, + eventType: scenarioData.eventTypes[0], + }); + }, + timeout + ); + test( `should create a booking for event that requires confirmation based on a booking notice duration threshold, if threshold is not met 1. Should create a booking in the database with status ACCEPTED @@ -1720,21 +1837,24 @@ describe("handleNewBooking", () => { }); const createdBooking = await handleNewBooking(req); - expect(createdBooking.responses).toContain({ - email: booker.email, - name: booker.name, - }); expect(createdBooking).toContain({ location: BookingLocations.CalVideo, paymentUid: paymentUid, }); + await expectBookingToBeInDatabase({ description: "", + location: BookingLocations.CalVideo, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.PENDING, + responses: expect.objectContaining({ + email: booker.email, + name: booker.name, + }), }); + expectWorkflowToBeTriggered(); expectAwaitingPaymentEmails({ organizer, booker, emails }); @@ -1887,6 +2007,9 @@ describe("handleNewBooking", () => { paymentId: createdBooking.paymentId!, }); + // FIXME: Right now we need to reset the test Emails because email expects only tests first email content for an email address + // Reset Test Emails to test for more Emails + resetTestEmails(); const { webhookResponse } = await mockPaymentSuccessWebhookFromStripe({ externalId }); expect(webhookResponse?.statusCode).toBe(200); @@ -1897,6 +2020,12 @@ describe("handleNewBooking", () => { eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.PENDING, }); + + expectBookingRequestedEmails({ + booker, + organizer, + emails, + }); expectBookingRequestedWebhookToHaveBeenFired({ booker, organizer, diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index 015fdb0309..f82f804008 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -616,7 +616,7 @@ describe("handleNewBooking", () => { describe("Event Type that requires confirmation", () => { test( - `should reschedule a booking that requires confirmation in PENDING state - When a booker(who is not the organizer himself) is doing the schedule + `should reschedule a booking that requires confirmation in PENDING state - When a booker(who is not the organizer himself) is doing the reschedule 1. Should cancel the existing booking 2. Should delete existing calendar invite and Video meeting 2. Should create a new booking in the database in PENDING state @@ -813,9 +813,8 @@ describe("handleNewBooking", () => { timeout ); - // eslint-disable-next-line playwright/no-skipped-test - test.skip( - `should rechedule a booking, that requires confirmation, without confirmation - When Organizer is doing the reschedule + test( + `should rechedule a booking, that requires confirmation, without confirmation - When booker is the organizer of the existing booking as well as the event-type 1. Should cancel the existing booking 2. Should delete existing calendar invite and Video meeting 2. Should create a new booking in the database in ACCEPTED state @@ -873,6 +872,7 @@ describe("handleNewBooking", () => { { uid: uidOfBookingToBeRescheduled, eventTypeId: 1, + userId: organizer.id, status: BookingStatus.ACCEPTED, startTime: `${plus1DateString}T05:00:00.000Z`, endTime: `${plus1DateString}T05:15:00.000Z`, @@ -1054,9 +1054,209 @@ describe("handleNewBooking", () => { timeout ); - // eslint-disable-next-line playwright/no-skipped-test - test.skip( - `should rechedule a booking, that requires confirmation, without confirmation - When the owner of the previous booking is doing the reschedule + test( + `should rechedule a booking, that requires confirmation, in PENDING state - Even when the rescheduler is the organizer of the event-type but not the organizer of the existing booking + 1. Should cancel the existing booking + 2. Should delete existing calendar invite and Video meeting + 2. Should create a new booking in the database in PENDING state + 3. Should send booking requested emails to the booker as well as organizer + 4. Should trigger BOOKING_REQUESTED webhook + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const subscriberUrl = "http://my-webhook.example.com"; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP"; + + const scenarioData = getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl, + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 45, + requiresConfirmation: true, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + bookings: [ + { + uid: uidOfBookingToBeRescheduled, + eventTypeId: 1, + status: BookingStatus.ACCEPTED, + startTime: `${plus1DateString}T05:00:00.000Z`, + endTime: `${plus1DateString}T05:15:00.000Z`, + references: [ + getMockBookingReference({ + type: appStoreMetadata.dailyvideo.type, + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + credentialId: 0, + }), + getMockBookingReference({ + type: appStoreMetadata.googlecalendar.type, + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASSWORD", + meetingUrl: "https://UNUSED_URL", + externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", + credentialId: 1, + }), + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }); + await createBookingScenario(scenarioData); + + const videoMock = mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + + const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + uid: "MOCK_ID", + }, + update: { + uid: "UPDATED_MOCK_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + rescheduleUid: uidOfBookingToBeRescheduled, + start: `${plus1DateString}T04:00:00.000Z`, + end: `${plus1DateString}T04:15:00.000Z`, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + // Fake the request to be from organizer + req.userId = organizer.id; + + const createdBooking = await handleNewBooking(req); + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + + await expectBookingInDBToBeRescheduledFromTo({ + from: { + uid: uidOfBookingToBeRescheduled, + }, + to: { + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + // Rescheduled booking sill stays in pending state + status: BookingStatus.PENDING, + location: BookingLocations.CalVideo, + responses: expect.objectContaining({ + email: booker.email, + name: booker.name, + }), + references: [ + { + type: appStoreMetadata.dailyvideo.type, + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + }, + { + type: appStoreMetadata.googlecalendar.type, + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASSWORD", + meetingUrl: "https://UNUSED_URL", + externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", + }, + ], + }, + }); + + expectWorkflowToBeTriggered(); + + expectBookingRequestedEmails({ + booker, + organizer, + emails, + }); + + expectBookingRequestedWebhookToHaveBeenFired({ + booker, + organizer, + location: BookingLocations.CalVideo, + subscriberUrl, + eventType: scenarioData.eventTypes[0], + }); + + expectSuccessfulVideoMeetingDeletionInCalendar(videoMock, { + bookingRef: { + type: appStoreMetadata.dailyvideo.type, + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + }, + }); + + expectSuccessfulCalendarEventDeletionInCalendar(calendarMock, { + externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", + calEvent: { + videoCallData: expect.objectContaining({ + url: "http://mock-dailyvideo.example.com", + }), + }, + uid: "MOCK_ID", + }); + }, + timeout + ); + + test( + `should rechedule a booking, that requires confirmation, without confirmation - When the owner of the previous booking is doing the reschedule(but he isn't the organizer of the event-type now) 1. Should cancel the existing booking 2. Should delete existing calendar invite and Video meeting 2. Should create a new booking in the database in ACCEPTED state @@ -1163,9 +1363,9 @@ describe("handleNewBooking", () => { { id: previousOrganizerIdForTheBooking, name: "Previous Organizer", - username: "prev-organizer", email: "", schedules: [TestData.schedules.IstWorkHours], + username: "prev-organizer", timeZone: "Europe/London", }, ], diff --git a/packages/lib/testEmails.ts b/packages/lib/testEmails.ts index b6687be0ba..f660ec1ca6 100644 --- a/packages/lib/testEmails.ts +++ b/packages/lib/testEmails.ts @@ -20,3 +20,7 @@ export const setTestEmail = (email: (typeof globalThis.testEmails)[number]) => { export const getTestEmails = () => { return globalThis.testEmails; }; + +export const resetTestEmails = () => { + globalThis.testEmails = []; +};