fix: Grab booking organizer credentials when team admins request reschedule (#24645)
* Change arg name from `bookingUId` to `bookingUid` * Lint fix * Use `BookingRepository` to find booking to reschedule * Move early return further up if no booking is found * Use `PermissionCheckService` if request rescheduling a team booking * Remove redundent check * Remove redundent eventType query * Using `BookingRepository` to update the booking to rescheduled * Update type in `getUsersCredentialsIncludeServiceAccountKey` to only require params that are required * Get booking organizer credentials * Type fixes * test: Add tests for team admin request reschedule with organizer credentials - Add test for team admin requesting reschedule with proper permissions - Add test verifying organizer's credentials are used (not requester's) - Add test for team member without permissions (should fail) These tests cover the fix in PR #24645 which ensures that when a team admin requests a reschedule, the booking organizer's credentials are used to delete calendar events instead of the requester's credentials. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: Address code review comments for request reschedule - Change user: true to user: { select: { id, email } } to only fetch required fields - Change eventType include to select with explicit fields including teamId - Remove sensitive information (user object, cancellationReason) from debug log - All integration tests passing locally Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Type fix * Remove businesss logic references from repository methods. * Move business logic to handler * Type fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
joe@cal.com <j.auyeung419@gmail.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
272d97c0a3
commit
2b2bf36703
@@ -418,7 +418,7 @@ export function BookingActionsDropdown({
|
||||
<RescheduleDialog
|
||||
isOpenDialog={isOpenRescheduleDialog}
|
||||
setIsOpenDialog={setIsOpenRescheduleDialog}
|
||||
bookingUId={booking.uid}
|
||||
bookingUid={booking.uid}
|
||||
/>
|
||||
{isOpenReassignDialog && (
|
||||
<ReassignDialog
|
||||
|
||||
@@ -13,13 +13,13 @@ import { showToast } from "@calcom/ui/components/toast";
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||
bookingUId: string;
|
||||
bookingUid: string;
|
||||
}
|
||||
|
||||
export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
|
||||
const { isOpenDialog, setIsOpenDialog, bookingUid } = props;
|
||||
const [rescheduleReason, setRescheduleReason] = useState("");
|
||||
|
||||
const { mutate: rescheduleApi, isPending } = trpc.viewer.bookings.requestReschedule.useMutation({
|
||||
@@ -66,7 +66,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
rescheduleApi({
|
||||
bookingId,
|
||||
bookingUid,
|
||||
rescheduleReason,
|
||||
});
|
||||
}}>
|
||||
|
||||
@@ -9,14 +9,15 @@ import {
|
||||
getScenarioData,
|
||||
getMockBookingAttendee,
|
||||
getDate,
|
||||
mockCalendar,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import { expectBookingRequestRescheduledEmails } from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { describe } from "vitest";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { SchedulingType, MembershipRole } 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/types";
|
||||
@@ -116,7 +117,7 @@ describe("Handler: requestReschedule", () => {
|
||||
getTrpcHandlerData({
|
||||
user: loggedInUser,
|
||||
input: {
|
||||
bookingId: bookingUid,
|
||||
bookingUid,
|
||||
rescheduleReason: "",
|
||||
},
|
||||
})
|
||||
@@ -236,7 +237,7 @@ describe("Handler: requestReschedule", () => {
|
||||
getTrpcHandlerData({
|
||||
user: loggedInUser,
|
||||
input: {
|
||||
bookingId: bookingUid,
|
||||
bookingUid,
|
||||
rescheduleReason: "",
|
||||
},
|
||||
})
|
||||
@@ -253,6 +254,297 @@ describe("Handler: requestReschedule", () => {
|
||||
bookNewTimePath: "/team/team-1/event-type-1",
|
||||
});
|
||||
});
|
||||
|
||||
test(`should allow team admin to request-reschedule for a team booking and use organizer's credentials
|
||||
1. Team admin (non-organizer) can request reschedule with proper permissions
|
||||
2. Organizer's credentials are used to delete calendar events`, 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,
|
||||
role: "MEMBER",
|
||||
},
|
||||
team: {
|
||||
id: 1,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
},
|
||||
},
|
||||
],
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const teamAdmin = {
|
||||
id: 102,
|
||||
username: "team-admin",
|
||||
name: "Team Admin",
|
||||
email: "team-admin@example.com",
|
||||
locale: "en",
|
||||
timeZone: "America/New_York",
|
||||
teams: [
|
||||
{
|
||||
membership: {
|
||||
accepted: true,
|
||||
role: MembershipRole.ADMIN,
|
||||
},
|
||||
team: {
|
||||
id: 1,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
},
|
||||
},
|
||||
],
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [], // No credentials
|
||||
selectedCalendars: [],
|
||||
};
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const bookingUid = "MOCKED_BOOKING_UID_TEAM_ADMIN";
|
||||
const eventTypeSlug = "event-type-1";
|
||||
|
||||
const calendarMock = await mockCalendar("googlecalendar");
|
||||
|
||||
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, // Booking belongs to organizer
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
references: [
|
||||
{
|
||||
type: "google_calendar",
|
||||
uid: "MOCK_CALENDAR_EVENT_UID",
|
||||
meetingId: "MOCK_MEETING_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
credentialId: 1,
|
||||
},
|
||||
],
|
||||
attendees: [
|
||||
getMockBookingAttendee({
|
||||
id: 2,
|
||||
name: booker.name,
|
||||
email: booker.email,
|
||||
locale: "hi",
|
||||
timeZone: "Asia/Kolkata",
|
||||
noShow: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
usersApartFromOrganizer: [teamAdmin],
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
const loggedInTeamAdmin = {
|
||||
organizationId: null,
|
||||
id: 102, // Team admin ID
|
||||
username: "team-admin",
|
||||
name: "Team Admin",
|
||||
email: "team-admin@example.com",
|
||||
};
|
||||
|
||||
await requestRescheduleHandler(
|
||||
getTrpcHandlerData({
|
||||
user: loggedInTeamAdmin,
|
||||
input: {
|
||||
bookingUid,
|
||||
rescheduleReason: "Team admin requesting reschedule",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expectBookingRequestRescheduledEmails({
|
||||
booking: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
booker,
|
||||
organizer: organizer,
|
||||
loggedInUser: loggedInTeamAdmin,
|
||||
emails,
|
||||
bookNewTimePath: "/team/team-1/event-type-1",
|
||||
});
|
||||
|
||||
const deleteEventCalls = calendarMock.deleteEventCalls;
|
||||
expect(deleteEventCalls.length).toBe(1);
|
||||
|
||||
const credentialUsed = deleteEventCalls[0].calendarServiceConstructorArgs.credential;
|
||||
expect(credentialUsed.userId).toBe(organizer.id);
|
||||
expect(credentialUsed.id).toBe(1);
|
||||
});
|
||||
|
||||
test(`should reject request-reschedule from team member without proper permissions`, async () => {
|
||||
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,
|
||||
role: "MEMBER",
|
||||
},
|
||||
team: {
|
||||
id: 1,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
},
|
||||
},
|
||||
],
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const teamMember = {
|
||||
id: 103,
|
||||
username: "team-member",
|
||||
name: "Team Member",
|
||||
email: "team-member@example.com",
|
||||
locale: "en",
|
||||
timeZone: "America/New_York",
|
||||
teams: [
|
||||
{
|
||||
membership: {
|
||||
accepted: true,
|
||||
role: MembershipRole.MEMBER,
|
||||
},
|
||||
team: {
|
||||
id: 1,
|
||||
name: "Team 1",
|
||||
slug: "team-1",
|
||||
},
|
||||
},
|
||||
],
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [],
|
||||
selectedCalendars: [],
|
||||
};
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const bookingUid = "MOCKED_BOOKING_UID_MEMBER";
|
||||
const eventTypeSlug = "event-type-1";
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slug: eventTypeSlug,
|
||||
slotInterval: 45,
|
||||
teamId: 1,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: bookingUid,
|
||||
eventTypeId: 1,
|
||||
userId: 101, // Booking belongs to organizer
|
||||
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,
|
||||
locale: "hi",
|
||||
timeZone: "Asia/Kolkata",
|
||||
noShow: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
usersApartFromOrganizer: [teamMember],
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
const loggedInTeamMember = {
|
||||
organizationId: null,
|
||||
id: 103, // Team member ID
|
||||
username: "team-member",
|
||||
name: "Team Member",
|
||||
email: "team-member@example.com",
|
||||
};
|
||||
|
||||
await expect(
|
||||
requestRescheduleHandler(
|
||||
getTrpcHandlerData({
|
||||
user: loggedInTeamMember,
|
||||
input: {
|
||||
bookingUid,
|
||||
rescheduleReason: "Team member trying to reschedule",
|
||||
},
|
||||
})
|
||||
)
|
||||
).rejects.toThrow("User does not have permission to request reschedule for this booking");
|
||||
});
|
||||
|
||||
test.todo("Verify that the email should go to organizer as well as the team members");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1371,4 +1371,89 @@ export class BookingRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findByUidIncludeEventTypeAndReferences({ bookingUid }: { bookingUid: string }) {
|
||||
return this.prismaClient.booking.findUniqueOrThrow({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
userId: true,
|
||||
status: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
title: true,
|
||||
description: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
eventTypeId: true,
|
||||
userPrimaryEmail: true,
|
||||
eventType: {
|
||||
select: {
|
||||
teamId: true,
|
||||
parentId: true,
|
||||
slug: true,
|
||||
hideOrganizerEmail: true,
|
||||
customReplyToEmail: true,
|
||||
bookingFields: true,
|
||||
metadata: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
location: true,
|
||||
attendees: true,
|
||||
references: true,
|
||||
customInputs: true,
|
||||
dynamicEventSlugRef: true,
|
||||
dynamicGroupSlugRef: true,
|
||||
destinationCalendar: true,
|
||||
smsReminderNumber: true,
|
||||
workflowReminders: true,
|
||||
responses: true,
|
||||
iCalUID: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateBookingStatus({
|
||||
bookingId,
|
||||
status,
|
||||
cancellationReason,
|
||||
cancelledBy,
|
||||
rescheduledBy,
|
||||
rescheduled,
|
||||
}: {
|
||||
bookingId: number;
|
||||
status?: BookingStatus;
|
||||
cancellationReason?: string;
|
||||
cancelledBy?: string;
|
||||
rescheduledBy?: string;
|
||||
rescheduled?: boolean;
|
||||
}) {
|
||||
return await this.prismaClient.booking.update({
|
||||
where: {
|
||||
id: bookingId,
|
||||
},
|
||||
data: {
|
||||
...(status !== undefined && { status }),
|
||||
...(rescheduled !== undefined && { rescheduled }),
|
||||
...(cancellationReason !== undefined && { cancellationReason }),
|
||||
...(cancelledBy !== undefined && { cancelledBy }),
|
||||
...(rescheduledBy !== undefined && { rescheduledBy }),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/app-store/d
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendRequestRescheduleEmailAndSMS } from "@calcom/emails/email-manager";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { deleteMeeting } from "@calcom/features/conferencing/lib/videoClient";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import {
|
||||
deleteWebhookScheduledTriggers,
|
||||
@@ -25,8 +27,8 @@ import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { BookingWebhookFactory } from "@calcom/lib/server/service/BookingWebhookFactory";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { BookingReference, EventType } from "@calcom/prisma/client";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
import type { Person } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -45,53 +47,22 @@ type RequestRescheduleOptions = {
|
||||
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.findUniqueOrThrow({
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
userId: true,
|
||||
title: true,
|
||||
description: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
eventTypeId: true,
|
||||
userPrimaryEmail: true,
|
||||
eventType: {
|
||||
include: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
parentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
location: true,
|
||||
attendees: true,
|
||||
references: true,
|
||||
customInputs: true,
|
||||
dynamicEventSlugRef: true,
|
||||
dynamicGroupSlugRef: true,
|
||||
destinationCalendar: true,
|
||||
smsReminderNumber: true,
|
||||
workflowReminders: true,
|
||||
responses: true,
|
||||
iCalUID: true,
|
||||
},
|
||||
where: {
|
||||
uid: bookingId,
|
||||
NOT: {
|
||||
status: {
|
||||
in: [BookingStatus.CANCELLED, BookingStatus.REJECTED],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const { bookingUid, rescheduleReason: cancellationReason } = input;
|
||||
log.debug("Started", safeStringify({ bookingUid }));
|
||||
const bookingRepository = new BookingRepository(prisma);
|
||||
const bookingToReschedule = await bookingRepository.findByUidIncludeEventTypeAndReferences({ bookingUid });
|
||||
|
||||
if (!bookingToReschedule.userId) {
|
||||
if (
|
||||
bookingToReschedule.status === BookingStatus.CANCELLED ||
|
||||
bookingToReschedule.status === BookingStatus.REJECTED
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Cannot request reschedule for cancelled or rejected booking",
|
||||
});
|
||||
}
|
||||
|
||||
if (!bookingToReschedule.userId || !bookingToReschedule.user) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Booking to reschedule doesn't have an owner" });
|
||||
}
|
||||
|
||||
@@ -100,20 +71,22 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
}
|
||||
|
||||
const bookingBelongsToTeam = !!bookingToReschedule.eventType?.teamId;
|
||||
const isBookingOrganizer = bookingToReschedule.userId === user.id;
|
||||
|
||||
const userTeams = await prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
select: {
|
||||
teams: true,
|
||||
},
|
||||
});
|
||||
if (!isBookingOrganizer && bookingBelongsToTeam && bookingToReschedule.eventType?.teamId) {
|
||||
const permissionCheckService = new PermissionCheckService();
|
||||
const hasPermission = await permissionCheckService.checkPermission({
|
||||
userId: user.id,
|
||||
teamId: bookingToReschedule.eventType.teamId,
|
||||
permission: "booking.update",
|
||||
fallbackRoles: ["ADMIN", "OWNER"],
|
||||
});
|
||||
|
||||
if (bookingBelongsToTeam && bookingToReschedule.eventType?.teamId) {
|
||||
const userTeamIds = userTeams.teams.map((item) => item.teamId);
|
||||
if (userTeamIds.indexOf(bookingToReschedule.eventType?.teamId) === -1) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "User isn't a member on the team" });
|
||||
if (!hasPermission) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User does not have permission to request reschedule for this booking",
|
||||
});
|
||||
}
|
||||
log.debug(
|
||||
"Request reschedule for team booking",
|
||||
@@ -122,36 +95,20 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!bookingBelongsToTeam && bookingToReschedule.userId !== user.id) {
|
||||
if (!bookingBelongsToTeam && !isBookingOrganizer) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "User isn't owner of the current booking" });
|
||||
}
|
||||
|
||||
if (!bookingToReschedule) return;
|
||||
|
||||
let event: Partial<EventType> = {};
|
||||
if (bookingToReschedule.eventTypeId) {
|
||||
event = await prisma.eventType.findUniqueOrThrow({
|
||||
select: {
|
||||
title: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
},
|
||||
where: {
|
||||
id: bookingToReschedule.eventTypeId,
|
||||
},
|
||||
});
|
||||
if (bookingToReschedule.eventType) {
|
||||
event = bookingToReschedule.eventType;
|
||||
}
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: bookingToReschedule.id,
|
||||
},
|
||||
data: {
|
||||
rescheduled: true,
|
||||
cancellationReason,
|
||||
status: BookingStatus.CANCELLED,
|
||||
updatedAt: dayjs().toISOString(),
|
||||
cancelledBy: user.email,
|
||||
},
|
||||
await bookingRepository.updateBookingStatus({
|
||||
bookingId: bookingToReschedule.id,
|
||||
status: BookingStatus.CANCELLED,
|
||||
rescheduled: true,
|
||||
cancellationReason,
|
||||
cancelledBy: user.email,
|
||||
});
|
||||
|
||||
// delete scheduled jobs of previous booking
|
||||
@@ -232,7 +189,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
|
||||
|
||||
// Handling calendar and videos cancellation
|
||||
// This can set previous time as available, until virtual calendar is done
|
||||
const credentials = await getUsersCredentialsIncludeServiceAccountKey(user);
|
||||
const credentials = await getUsersCredentialsIncludeServiceAccountKey(bookingToReschedule.user);
|
||||
const credentialsMap = new Map();
|
||||
credentials.forEach((credential) => {
|
||||
credentialsMap.set(credential.type, credential);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZRequestRescheduleInputSchema = z.object({
|
||||
bookingId: z.string(),
|
||||
bookingUid: z.string(),
|
||||
rescheduleReason: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user