fix: Fix 'Book a new time' link in request-reschedule for Team Event (#12261)

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Hariom Balhara
2023-11-14 13:23:44 -07:00
committed by GitHub
co-authored by Carina Wollendorfer Alex van Andel
parent 00df493623
commit 9d06f6dd0e
19 changed files with 510 additions and 48 deletions
@@ -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<Omit<NonNullable<TrpcSessionUser>, "id" | "email" | "username">> &
Pick<NonNullable<TrpcSessionUser>, "id" | "email" | "username">;
}) {
return {
ctx: {
user: {
...getSampleUserInSession(),
...user,
} satisfies TrpcSessionUser,
},
input: input,
};
}
@@ -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<typeof TestData.users.example, "defaultScheduleId"> & {
credentials?: InputCredential[];
organizationId?: number | null;
selectedCalendars?: InputSelectedCalendar[];
teams?: {
membership: Partial<Membership>;
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<InputUser["teams"]>[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,
};
}
@@ -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<typeof createMocks>) {
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...args);
+63 -8
View File
@@ -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<typeof getOrganizer>;
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,
@@ -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,
};
};
@@ -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 = [];
@@ -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;
@@ -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<Prisma.UserArgs>()({
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;
@@ -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<Booking>;
@@ -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");
}
@@ -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();
@@ -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,
@@ -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();
@@ -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;
@@ -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;
@@ -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;
+5 -2
View File
@@ -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();
@@ -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,
+9
View File
@@ -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