diff --git a/apps/web/test/handlers/requestReschedule.test.ts b/apps/web/test/handlers/requestReschedule.test.ts new file mode 100644 index 0000000000..09e01ce308 --- /dev/null +++ b/apps/web/test/handlers/requestReschedule.test.ts @@ -0,0 +1,275 @@ +import type { Request, Response } from "express"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { describe } from "vitest"; + +import { SchedulingType } from "@calcom/prisma/enums"; +import { BookingStatus } from "@calcom/prisma/enums"; +import type { TRequestRescheduleInputSchema } from "@calcom/trpc/server/routers/viewer/bookings/requestReschedule.schema"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; +import { test } from "@calcom/web/test/fixtures/fixtures"; +import { + createBookingScenario, + getGoogleCalendarCredential, + TestData, + getOrganizer, + getBooker, + getScenarioData, + getMockBookingAttendee, + getDate, +} from "@calcom/web/test/utils/bookingScenario/bookingScenario"; +import { expectBookingRequestRescheduledEmails } from "@calcom/web/test/utils/bookingScenario/expects"; + +import { getSampleUserInSession } from "../utils/bookingScenario/getSampleUserInSession"; +import { setupAndTeardown } from "../utils/bookingScenario/setupAndTeardown"; + +export type CustomNextApiRequest = NextApiRequest & Request; + +export type CustomNextApiResponse = NextApiResponse & Response; + +describe("Handler: requestReschedule", () => { + setupAndTeardown(); + + describe("User Event Booking", () => { + test(`should be able to request-reschedule for a user booking + 1. RequestReschedule emails go to both attendee and the person requesting the reschedule`, async ({ + emails, + }) => { + const { requestRescheduleHandler } = await import( + "@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler" + ); + 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 bookingUid = "MOCKED_BOOKING_UID"; + const eventTypeSlug = "event-type-1"; + await createBookingScenario( + getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slug: eventTypeSlug, + slotInterval: 45, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + bookings: [ + { + uid: bookingUid, + eventTypeId: 1, + userId: 101, + status: BookingStatus.ACCEPTED, + startTime: `${plus1DateString}T05:00:00.000Z`, + endTime: `${plus1DateString}T05:15:00.000Z`, + attendees: [ + getMockBookingAttendee({ + id: 2, + name: booker.name, + email: booker.email, + // Booker's locale when the fresh booking happened earlier + locale: "hi", + // Booker's timezone when the fresh booking happened earlier + timeZone: "Asia/Kolkata", + }), + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + const loggedInUser = { + organizationId: null, + id: 101, + username: "reschedule-requester", + name: "Reschedule Requester", + email: "reschedule-requester@example.com", + }; + await requestRescheduleHandler( + getTrpcHandlerData({ + user: loggedInUser, + input: { + bookingId: bookingUid, + rescheduleReason: "", + }, + }) + ); + + expectBookingRequestRescheduledEmails({ + booking: { + uid: bookingUid, + }, + booker, + organizer: organizer, + loggedInUser, + emails, + bookNewTimePath: `/${organizer.username}/${eventTypeSlug}`, + }); + }); + }); + + describe("Team Event Booking", () => { + test(`should be able to request-reschedule for a team event booking + 1. RequestReschedule emails go to both attendee and the person requesting the reschedule`, async ({ + emails, + }) => { + const { requestRescheduleHandler } = await import( + "@calcom/trpc/server/routers/viewer/bookings/requestReschedule.handler" + ); + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + teams: [ + { + membership: { + accepted: true, + }, + team: { + id: 1, + name: "Team 1", + slug: "team-1", + }, + }, + ], + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const bookingUid = "MOCKED_BOOKING_UID"; + const eventTypeSlug = "event-type-1"; + await createBookingScenario( + getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slug: eventTypeSlug, + slotInterval: 45, + teamId: 1, + schedulingType: SchedulingType.COLLECTIVE, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + bookings: [ + { + uid: bookingUid, + eventTypeId: 1, + userId: 101, + status: BookingStatus.ACCEPTED, + startTime: `${plus1DateString}T05:00:00.000Z`, + endTime: `${plus1DateString}T05:15:00.000Z`, + attendees: [ + getMockBookingAttendee({ + id: 2, + name: booker.name, + email: booker.email, + // Booker's locale when the fresh booking happened earlier + locale: "hi", + // Booker's timezone when the fresh booking happened earlier + timeZone: "Asia/Kolkata", + }), + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + const loggedInUser = { + organizationId: null, + id: 101, + username: "reschedule-requester", + name: "Reschedule Requester", + email: "reschedule-requester@example.com", + }; + await requestRescheduleHandler( + getTrpcHandlerData({ + user: loggedInUser, + input: { + bookingId: bookingUid, + rescheduleReason: "", + }, + }) + ); + + expectBookingRequestRescheduledEmails({ + booking: { + uid: bookingUid, + }, + booker, + organizer: organizer, + loggedInUser, + emails, + bookNewTimePath: "/team/team-1/event-type-1", + }); + }); + test.todo("Verify that the email should go to organizer as well as the team members"); + }); +}); + +function getTrpcHandlerData({ + input, + user, +}: { + input: TRequestRescheduleInputSchema; + user: Partial, "id" | "email" | "username">> & + Pick, "id" | "email" | "username">; +}) { + return { + ctx: { + user: { + ...getSampleUserInSession(), + ...user, + } satisfies TrpcSessionUser, + }, + input: input, + }; +} diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index 0a3809721c..b1f9367bb2 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -2,15 +2,15 @@ import appStoreMock from "../../../../../tests/libs/__mocks__/app-store"; import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n"; import prismock from "../../../../../tests/libs/__mocks__/prisma"; -import type { BookingReference, Attendee, Booking } from "@prisma/client"; +import type { BookingReference, Attendee, Booking, Membership } from "@prisma/client"; import type { Prisma } from "@prisma/client"; import type { WebhookTriggerEvents } from "@prisma/client"; import type Stripe from "stripe"; +import type { getMockRequestDataForBooking } from "test/utils/bookingScenario/getMockRequestDataForBooking"; import { v4 as uuidv4 } from "uuid"; import "vitest-fetch-mock"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; -import type { getMockRequestDataForBooking } from "@calcom/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking"; import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook"; import type { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; @@ -68,6 +68,14 @@ type InputUser = Omit & { credentials?: InputCredential[]; organizationId?: number | null; selectedCalendars?: InputSelectedCalendar[]; + teams?: { + membership: Partial; + team: { + id: number; + name: string; + slug: string; + }; + }[]; schedules: { // Allows giving id in the input directly so that it can be referenced somewhere else as well id?: number; @@ -98,6 +106,7 @@ export type InputEventType = { schedulingType?: SchedulingType; beforeEventBuffer?: number; afterEventBuffer?: number; + teamId?: number | null; requiresConfirmation?: boolean; destinationCalendar?: Prisma.DestinationCalendarCreateInput; schedule?: InputUser["schedules"][number]; @@ -373,6 +382,7 @@ async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma allUsers: await prismock.user.findMany({ include: { credentials: true, + teams: true, schedules: { include: { availability: true, @@ -385,9 +395,32 @@ async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma ); } +async function addTeamsToDb(teams: NonNullable[number]["team"][]) { + log.silly("TestData: Creating Teams", JSON.stringify(teams)); + await prismock.team.createMany({ + data: teams, + }); + const addedTeams = await prismock.team.findMany({ + where: { + id: { + in: teams.map((team) => team.id), + }, + }, + }); + log.silly( + "Added teams to Db", + safeStringify({ + addedTeams, + }) + ); + return addedTeams; +} + async function addUsers(users: InputUser[]) { - const prismaUsersCreate = users.map((user) => { - const newUser = user; + const prismaUsersCreate = []; + for (let i = 0; i < users.length; i++) { + const newUser = users[i]; + const user = users[i]; if (user.schedules) { newUser.schedules = { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -415,6 +448,22 @@ async function addUsers(users: InputUser[]) { }, }; } + + if (user.teams) { + const addedTeams = await addTeamsToDb(user.teams.map((team) => team.team)); + newUser.teams = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + createMany: { + data: user.teams.map((team, index) => { + return { + teamId: addedTeams[index].id, + ...team.membership, + }; + }), + }, + }; + } if (user.selectedCalendars) { newUser.selectedCalendars = { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -425,8 +474,8 @@ async function addUsers(users: InputUser[]) { }; } - return newUser; - }); + prismaUsersCreate.push(newUser); + } // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore await addUsersToDb(prismaUsersCreate); @@ -739,6 +788,7 @@ export function getOrganizer({ selectedCalendars, destinationCalendar, defaultScheduleId, + teams, }: { name: string; email: string; @@ -748,6 +798,7 @@ export function getOrganizer({ selectedCalendars?: InputSelectedCalendar[]; defaultScheduleId?: number | null; destinationCalendar?: Prisma.DestinationCalendarCreateInput; + teams?: InputUser["teams"]; }) { return { ...TestData.users.example, @@ -760,6 +811,7 @@ export function getOrganizer({ selectedCalendars, destinationCalendar, defaultScheduleId, + teams, }; } diff --git a/packages/features/bookings/lib/handleNewBooking/test/lib/createMockNextJsRequest.ts b/apps/web/test/utils/bookingScenario/createMockNextJsRequest.ts similarity index 59% rename from packages/features/bookings/lib/handleNewBooking/test/lib/createMockNextJsRequest.ts rename to apps/web/test/utils/bookingScenario/createMockNextJsRequest.ts index d9d321544f..f3ac967ec2 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/lib/createMockNextJsRequest.ts +++ b/apps/web/test/utils/bookingScenario/createMockNextJsRequest.ts @@ -1,6 +1,9 @@ import { createMocks } from "node-mocks-http"; -import type { CustomNextApiRequest, CustomNextApiResponse } from "../fresh-booking.test"; +import type { + CustomNextApiRequest, + CustomNextApiResponse, +} from "@calcom/features/bookings/lib/handleNewBooking/test/fresh-booking.test"; export function createMockNextJsRequest(...args: Parameters) { return createMocks(...args); diff --git a/apps/web/test/utils/bookingScenario/expects.ts b/apps/web/test/utils/bookingScenario/expects.ts index 3c7844f1b1..6f234e844e 100644 --- a/apps/web/test/utils/bookingScenario/expects.ts +++ b/apps/web/test/utils/bookingScenario/expects.ts @@ -8,7 +8,6 @@ import { expect } from "vitest"; import "vitest-fetch-mock"; import dayjs from "@calcom/dayjs"; -import { DEFAULT_TIMEZONE_BOOKER } from "@calcom/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking"; import { WEBAPP_URL } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; @@ -17,7 +16,8 @@ import type { AppsStatus } from "@calcom/types/Calendar"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { Fixtures } from "@calcom/web/test/fixtures/fixtures"; -import type { InputEventType } from "./bookingScenario"; +import type { InputEventType, getOrganizer } from "./bookingScenario"; +import { DEFAULT_TIMEZONE_BOOKER } from "./getMockRequestDataForBooking"; // This is too complex at the moment, I really need to simplify this. // Maybe we can replace the exact match with a partial match approach that would be easier to maintain but we would still need Dayjs to do the timezone conversion @@ -66,7 +66,7 @@ type ExpectedEmail = { // }; ics?: { filename: string; - iCalUID: string; + iCalUID?: string; recurrence?: Recurrence; }; /** @@ -96,7 +96,7 @@ expect.extend({ logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog })); return { pass: false, - message: () => `No email sent to ${to}`, + message: () => `No email sent to ${to}. All emails are ${JSON.stringify(emailsToLog)}`, }; } const ics = testEmail.icalEvent; @@ -105,9 +105,10 @@ expect.extend({ let isToAddressExpected = true; const isIcsFilenameExpected = expectedEmail.ics ? ics?.filename === expectedEmail.ics.filename : true; - const isIcsUIDExpected = expectedEmail.ics - ? !!(icsObject ? icsObject[expectedEmail.ics.iCalUID] : null) - : true; + const isIcsUIDExpected = + expectedEmail.ics && expectedEmail.ics.iCalUID + ? !!(icsObject ? icsObject[expectedEmail.ics.iCalUID] : null) + : true; const emailDom = parse(testEmail.html); const actualEmailContent = { @@ -172,7 +173,7 @@ expect.extend({ if (expectedEmail.noIcs && ics) { return { pass: false, - message: () => `${isNot ? "" : "Not"} expected ics file`, + message: () => `${isNot ? "" : "Not"} expected ics file ${JSON.stringify(ics)}`, }; } @@ -584,6 +585,7 @@ export function expectSuccessfulBookingRescheduledEmails({ `${booker.name} <${booker.email}>` ); } + export function expectAwaitingPaymentEmails({ emails, booker, @@ -630,6 +632,59 @@ export function expectBookingRequestedEmails({ ); } +export function expectBookingRequestRescheduledEmails({ + emails, + loggedInUser, + booker, + booking, + bookNewTimePath, + organizer, +}: { + emails: Fixtures["emails"]; + organizer: ReturnType; + loggedInUser: { + email: string; + name: string; + }; + booker: { email: string; name: string }; + booking: { uid: string; urlOrigin?: string }; + bookNewTimePath: string; +}) { + const bookingUrlOrigin = booking.urlOrigin || WEBAPP_URL; + + expect(emails).toHaveEmail( + { + titleTag: "rescheduled_event_type_subject", + heading: "request_reschedule_booking", + subHeading: "request_reschedule_subtitle", + links: [ + { + href: `${bookingUrlOrigin}${bookNewTimePath}?rescheduleUid=${booking.uid}`, + text: "Book a new time", + }, + ], + to: `${booker.email}`, + ics: { + filename: "event.ics", + }, + }, + `${booker.email}` + ); + + expect(emails).toHaveEmail( + { + titleTag: "rescheduled_event_type_subject", + heading: "request_reschedule_title_organizer", + subHeading: "request_reschedule_subtitle_organizer", + to: `${loggedInUser.email}`, + ics: { + filename: "event.ics", + }, + }, + `${loggedInUser.email}` + ); +} + export function expectBookingRequestedWebhookToHaveBeenFired({ booker, location, diff --git a/packages/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking.ts b/apps/web/test/utils/bookingScenario/getMockRequestDataForBooking.ts similarity index 100% rename from packages/features/bookings/lib/handleNewBooking/test/lib/getMockRequestDataForBooking.ts rename to apps/web/test/utils/bookingScenario/getMockRequestDataForBooking.ts diff --git a/apps/web/test/utils/bookingScenario/getSampleUserInSession.ts b/apps/web/test/utils/bookingScenario/getSampleUserInSession.ts new file mode 100644 index 0000000000..20ed58b3cf --- /dev/null +++ b/apps/web/test/utils/bookingScenario/getSampleUserInSession.ts @@ -0,0 +1,43 @@ +import { UserPermissionRole } from "@calcom/prisma/client"; +import { IdentityProvider } from "@calcom/prisma/enums"; + +export const getSampleUserInSession = function () { + return { + locale: "", + avatar: "", + organization: { + isOrgAdmin: false, + metadata: null, + }, + defaultScheduleId: null, + name: "", + defaultBookerLayouts: null, + timeZone: "Asia/Kolkata", + selectedCalendars: [], + destinationCalendar: null, + emailVerified: new Date(), + allowDynamicBooking: false, + bio: "", + weekStart: "", + startTime: 0, + endTime: 0, + bufferTime: 0, + hideBranding: false, + timeFormat: 12, + twoFactorEnabled: false, + identityProvider: IdentityProvider.CAL, + brandColor: "#292929", + darkBrandColor: "#fafafa", + away: false, + metadata: null, + role: UserPermissionRole.USER, + disableImpersonation: false, + organizationId: null, + theme: "", + createdDate: new Date(), + trialEndsAt: new Date(), + completedOnboarding: false, + allowSEOIndexing: false, + receiveMonthlyDigestEmail: false, + }; +}; diff --git a/packages/features/bookings/lib/handleNewBooking/test/lib/setupAndTeardown.ts b/apps/web/test/utils/bookingScenario/setupAndTeardown.ts similarity index 76% rename from packages/features/bookings/lib/handleNewBooking/test/lib/setupAndTeardown.ts rename to apps/web/test/utils/bookingScenario/setupAndTeardown.ts index d910f33918..122b7b4c1e 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/lib/setupAndTeardown.ts +++ b/apps/web/test/utils/bookingScenario/setupAndTeardown.ts @@ -8,10 +8,15 @@ import { export function setupAndTeardown() { beforeEach(() => { // Required to able to generate token in email in some cases + //@ts-expect-error - It is a readonly variable process.env.CALENDSO_ENCRYPTION_KEY = "abcdefghjnmkljhjklmnhjklkmnbhjui"; + //@ts-expect-error - It is a readonly variable process.env.STRIPE_WEBHOOK_SECRET = "MOCK_STRIPE_WEBHOOK_SECRET"; // We are setting it in vitest.config.ts because otherwise it's too late to set it. // process.env.DAILY_API_KEY = "MOCK_DAILY_API_KEY"; + + // Ensure that Rate Limiting isn't enforced for tests + delete process.env.UPSTASH_REDIS_REST_URL; mockNoTranslations(); // mockEnableEmailFeature(); enableEmailFeature(); @@ -19,7 +24,9 @@ export function setupAndTeardown() { fetchMock.resetMocks(); }); afterEach(() => { + //@ts-expect-error - It is a readonly variable delete process.env.CALENDSO_ENCRYPTION_KEY; + //@ts-expect-error - It is a readonly variable delete process.env.STRIPE_WEBHOOK_SECRET; delete process.env.DAILY_API_KEY; globalThis.testEmails = []; diff --git a/packages/features/bookings/lib/handleNewBooking/test/lib/test.ts b/apps/web/test/utils/bookingScenario/test.ts similarity index 87% rename from packages/features/bookings/lib/handleNewBooking/test/lib/test.ts rename to apps/web/test/utils/bookingScenario/test.ts index f78009ef9f..74eb503f86 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/lib/test.ts +++ b/apps/web/test/utils/bookingScenario/test.ts @@ -67,10 +67,10 @@ export const testWithAndWithoutOrg = ( _testWithAndWithoutOrg(description, fn, timeout, "run"); }; -testWithAndWithoutOrg.only = ((description, fn) => { - _testWithAndWithoutOrg(description, fn, "only"); +testWithAndWithoutOrg.only = ((description, fn, timeout) => { + _testWithAndWithoutOrg(description, fn, timeout, "only"); }) as typeof _testWithAndWithoutOrg; -testWithAndWithoutOrg.skip = ((description, fn) => { - _testWithAndWithoutOrg(description, fn, "skip"); +testWithAndWithoutOrg.skip = ((description, fn, timeout) => { + _testWithAndWithoutOrg(description, fn, timeout, "skip"); }) as typeof _testWithAndWithoutOrg; diff --git a/packages/core/builders/CalendarEvent/builder.ts b/packages/core/builders/CalendarEvent/builder.ts index c3e0e75012..1dbf090138 100644 --- a/packages/core/builders/CalendarEvent/builder.ts +++ b/packages/core/builders/CalendarEvent/builder.ts @@ -5,12 +5,15 @@ import { v5 as uuidv5 } from "uuid"; import dayjs from "@calcom/dayjs"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma from "@calcom/prisma"; import type { CalendarEvent } from "@calcom/types/Calendar"; import { CalendarEventClass } from "./class"; +const log = logger.getSubLogger({ prefix: ["builders", "CalendarEvent", "builder"] }); const translator = short(); const userSelect = Prisma.validator()({ select: { @@ -149,6 +152,7 @@ export class CalendarEventBuilder implements ICalendarEventBuilder { } catch (error) { throw new Error("Error while getting eventType"); } + log.debug("getEventFromEventId.resultEventType", safeStringify(resultEventType)); return resultEventType; } @@ -244,7 +248,7 @@ export class CalendarEventBuilder implements ICalendarEventBuilder { let slug = ""; if (isTeam && eventType?.team?.slug) { - slug = `/team/${eventType.team?.slug}`; + slug = `team/${eventType.team?.slug}/${eventType.slug}`; } else if (isDynamic) { const dynamicSlug = isDynamic ? `${booking.dynamicGroupSlugRef}/${booking.dynamicEventSlugRef}` : ""; slug = dynamicSlug; diff --git a/packages/core/builders/CalendarEvent/director.ts b/packages/core/builders/CalendarEvent/director.ts index 3b9e7a3c2b..3de91c5589 100644 --- a/packages/core/builders/CalendarEvent/director.ts +++ b/packages/core/builders/CalendarEvent/director.ts @@ -1,7 +1,11 @@ -import { Booking } from "@prisma/client"; +import type { Booking } from "@prisma/client"; -import { CalendarEventBuilder } from "./builder"; +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; +import type { CalendarEventBuilder } from "./builder"; + +const log = logger.getSubLogger({ prefix: ["builders", "CalendarEvent", "director"] }); export class CalendarEventDirector { private builder!: CalendarEventBuilder; private existingBooking!: Partial; @@ -44,6 +48,10 @@ export class CalendarEventDirector { this.builder.setDescription(this.builder.eventType.description); this.builder.setNotes(this.existingBooking.description); this.builder.buildRescheduleLink(this.existingBooking, this.builder.eventType); + log.debug( + "buildForRescheduleEmail", + safeStringify({ existingBooking: this.existingBooking, builder: this.builder }) + ); } else { throw new Error("buildForRescheduleEmail.missing.params.required"); } diff --git a/packages/features/bookings/lib/handleNewBooking/test/dynamic-group-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/dynamic-group-booking.test.ts index a22dd59679..2da113005d 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/dynamic-group-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/dynamic-group-booking.test.ts @@ -1,8 +1,7 @@ import { describe } from "vitest"; import { test } from "@calcom/web/test/fixtures/fixtures"; - -import { setupAndTeardown } from "./lib/setupAndTeardown"; +import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; describe("handleNewBooking", () => { setupAndTeardown(); 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 ea085f421a..fbc0291e3a 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -37,6 +37,7 @@ import { mockVideoAppToCrashOnCreateMeeting, BookingLocations, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; +import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectWorkflowToBeTriggered, expectSuccessfulBookingCreationEmails, @@ -49,11 +50,9 @@ import { expectBrokenIntegrationEmails, expectSuccessfulCalendarEventCreationInCalendar, } from "@calcom/web/test/utils/bookingScenario/expects"; - -import { createMockNextJsRequest } from "./lib/createMockNextJsRequest"; -import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking"; -import { setupAndTeardown } from "./lib/setupAndTeardown"; -import { testWithAndWithoutOrg } from "./lib/test"; +import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; +import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; +import { testWithAndWithoutOrg } from "@calcom/web/test/utils/bookingScenario/test"; export type CustomNextApiRequest = NextApiRequest & Request; @@ -940,7 +939,7 @@ describe("handleNewBooking", () => { }); await handleNewBooking(req); - expectBrokenIntegrationEmails({ booker, organizer, emails }); + expectBrokenIntegrationEmails({ organizer, emails }); expectBookingCreatedWebhookToHaveBeenFired({ booker, diff --git a/packages/features/bookings/lib/handleNewBooking/test/managed-event-type-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/managed-event-type-booking.test.ts index 81a10098aa..9a86bbec7c 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/managed-event-type-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/managed-event-type-booking.test.ts @@ -1,8 +1,7 @@ import { describe } from "vitest"; import { test } from "@calcom/web/test/fixtures/fixtures"; - -import { setupAndTeardown } from "./lib/setupAndTeardown"; +import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; describe("handleNewBooking", () => { setupAndTeardown(); diff --git a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts index bcf811e1e9..68c1ba52a0 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts @@ -16,6 +16,7 @@ import { mockCalendarToHaveNoBusySlots, getDate, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; +import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectWorkflowToBeTriggered, expectSuccessfulBookingCreationEmails, @@ -23,10 +24,8 @@ import { expectBookingCreatedWebhookToHaveBeenFired, expectSuccessfulCalendarEventCreationInCalendar, } from "@calcom/web/test/utils/bookingScenario/expects"; - -import { createMockNextJsRequest } from "./lib/createMockNextJsRequest"; -import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking"; -import { setupAndTeardown } from "./lib/setupAndTeardown"; +import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; +import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; const DAY_IN_MS = 1000 * 60 * 60 * 24; diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index fc023bc88e..50c1fe2baa 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -24,6 +24,7 @@ import { getMockFailingAppStatus, getMockPassingAppStatus, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; +import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectWorkflowToBeTriggered, expectBookingToBeInDatabase, @@ -37,10 +38,8 @@ import { expectSuccessfulCalendarEventDeletionInCalendar, expectSuccessfulVideoMeetingDeletionInCalendar, } from "@calcom/web/test/utils/bookingScenario/expects"; - -import { createMockNextJsRequest } from "./lib/createMockNextJsRequest"; -import { getMockRequestDataForBooking } from "./lib/getMockRequestDataForBooking"; -import { setupAndTeardown } from "./lib/setupAndTeardown"; +import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; +import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; // Local test runs sometime gets too slow const timeout = process.env.CI ? 5000 : 20000; diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts index e5d7ce7191..386c910e5f 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts @@ -22,6 +22,7 @@ import { BookingLocations, getZoomAppCredential, } from "@calcom/web/test/utils/bookingScenario/bookingScenario"; +import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectWorkflowToBeTriggered, expectSuccessfulBookingCreationEmails, @@ -30,10 +31,8 @@ import { expectSuccessfulCalendarEventCreationInCalendar, expectSuccessfulVideoMeetingCreation, } from "@calcom/web/test/utils/bookingScenario/expects"; - -import { createMockNextJsRequest } from "../lib/createMockNextJsRequest"; -import { getMockRequestDataForBooking } from "../lib/getMockRequestDataForBooking"; -import { setupAndTeardown } from "../lib/setupAndTeardown"; +import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; +import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; export type CustomNextApiRequest = NextApiRequest & Request; diff --git a/packages/lib/server/getBrand.ts b/packages/lib/server/getBrand.ts index ad4066e9f1..e70bf1ec5e 100644 --- a/packages/lib/server/getBrand.ts +++ b/packages/lib/server/getBrand.ts @@ -17,8 +17,11 @@ export const getBrand = async (orgId: number | null) => { metadata: true, }, }); - const metadata = teamMetadataSchema.parse(org?.metadata); - const slug = (org?.slug || metadata?.requestedSlug) as string; + if (!org) { + return null; + } + const metadata = teamMetadataSchema.parse(org.metadata); + const slug = (org.slug || metadata?.requestedSlug) as string; const fullDomain = getOrgFullOrigin(slug); const domainSuffix = subdomainSuffix(); diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 21458ef291..8349d17d4e 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -16,6 +16,8 @@ import { cancelScheduledJobs } from "@calcom/features/webhooks/lib/scheduleTrigg import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; import { isPrismaObjOrUndefined } from "@calcom/lib"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server"; import { getBookerUrl } from "@calcom/lib/server/getBookerUrl"; import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; @@ -36,11 +38,11 @@ type RequestRescheduleOptions = { }; input: TRequestRescheduleInputSchema; }; - +const log = logger.getSubLogger({ prefix: ["requestRescheduleHandler"] }); export const requestRescheduleHandler = async ({ ctx, input }: RequestRescheduleOptions) => { const { user } = ctx; const { bookingId, rescheduleReason: cancellationReason } = input; - + log.debug("Started", safeStringify({ bookingId, cancellationReason, user })); const bookingToReschedule = await prisma.booking.findFirstOrThrow({ select: { id: true, @@ -98,6 +100,12 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule if (userTeamIds.indexOf(bookingToReschedule?.eventType?.teamId) === -1) { throw new TRPCError({ code: "FORBIDDEN", message: "User isn't a member on the team" }); } + log.debug( + "Request reschedule for team booking", + safeStringify({ + teamId: bookingToReschedule.eventType?.teamId, + }) + ); } if (!bookingBelongsToTeam && bookingToReschedule.userId !== user.id) { throw new TRPCError({ code: "FORBIDDEN", message: "User isn't owner of the current booking" }); @@ -220,6 +228,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule }) ); + log.debug("builder.calendarEvent", safeStringify(builder.calendarEvent)); // Send emails await sendRequestRescheduleEmail(builder.calendarEvent, { rescheduleLink: builder.rescheduleLink, diff --git a/tests/libs/__mocks__/prisma.ts b/tests/libs/__mocks__/prisma.ts index 71803b4e04..1f508e1ef4 100644 --- a/tests/libs/__mocks__/prisma.ts +++ b/tests/libs/__mocks__/prisma.ts @@ -12,6 +12,7 @@ vi.mock("@calcom/prisma", () => ({ const handlePrismockBugs = () => { const __updateBooking = prismock.booking.update; + const __findFirstOrThrowBooking = prismock.booking.findFirstOrThrow; const __findManyWebhook = prismock.webhook.findMany; // eslint-disable-next-line @typescript-eslint/no-explicit-any prismock.booking.update = (...rest: any[]) => { @@ -34,6 +35,14 @@ const handlePrismockBugs = () => { return __updateBooking(...rest); }; + prismock.booking.findFirstOrThrow = (...rest: any[]) => { + const { where } = rest[0]; + delete where.NOT; + logger.silly("Fixed Prismock bug with using NOT in where clause"); + + return __findFirstOrThrowBooking(...rest); + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any prismock.webhook.findMany = (...rest: any[]) => { // There is some bug in prismock where it can't handle complex where clauses