migrate /api/book/request-reschedule to viewer.booking.requestResched… (#4488)
* migrate /api/book/request-reschedule to viewer.booking.requestReschedule in trpc * Type fixes Co-authored-by: hussamkhatib <hussamkhatib@gmail.com> Co-authored-by: zomars <zomars@me.com>fix-team-query
parent
8fc4d342fd
commit
9118ca328f
|
@ -1,5 +1,3 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { RescheduleResponse } from "pages/api/book/request-reschedule";
|
|
||||||
import React, { useState, Dispatch, SetStateAction } from "react";
|
import React, { useState, Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -10,8 +8,6 @@ import { Icon } from "@calcom/ui/Icon";
|
||||||
import { TextArea } from "@calcom/ui/form/fields";
|
import { TextArea } from "@calcom/ui/form/fields";
|
||||||
import Button from "@calcom/ui/v2/core/Button";
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
|
||||||
import * as fetchWrapper from "@lib/core/http/fetch-wrapper";
|
|
||||||
|
|
||||||
interface IRescheduleDialog {
|
interface IRescheduleDialog {
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||||
|
@ -23,35 +19,18 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
|
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
|
||||||
const [rescheduleReason, setRescheduleReason] = useState("");
|
const [rescheduleReason, setRescheduleReason] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const rescheduleApi = useMutation(
|
|
||||||
async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await fetchWrapper.post<
|
|
||||||
{ bookingId: string; rescheduleReason: string },
|
|
||||||
RescheduleResponse
|
|
||||||
>("/api/book/request-reschedule", {
|
|
||||||
bookingId,
|
|
||||||
rescheduleReason,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result) {
|
const { mutate: rescheduleApi, isLoading } = trpc.useMutation("viewer.bookings.requestReschedule", {
|
||||||
showToast(t("reschedule_request_sent"), "success");
|
async onSuccess() {
|
||||||
setIsOpenDialog(false);
|
showToast(t("reschedule_request_sent"), "success");
|
||||||
}
|
setIsOpenDialog(false);
|
||||||
} catch (error) {
|
await utils.invalidateQueries(["viewer.bookings"]);
|
||||||
showToast(t("unexpected_error_try_again"), "error");
|
|
||||||
// @TODO: notify sentry
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
},
|
},
|
||||||
{
|
onError() {
|
||||||
async onSettled() {
|
showToast(t("unexpected_error_try_again"), "error");
|
||||||
await utils.invalidateQueries(["viewer.bookings"]);
|
// @TODO: notify sentry
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
|
@ -84,7 +63,10 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||||
data-testid="send_request"
|
data-testid="send_request"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
rescheduleApi.mutate();
|
rescheduleApi({
|
||||||
|
bookingId,
|
||||||
|
rescheduleReason,
|
||||||
|
});
|
||||||
}}>
|
}}>
|
||||||
{t("send_reschedule_request")}
|
{t("send_reschedule_request")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,294 +0,0 @@
|
||||||
import {
|
|
||||||
Attendee,
|
|
||||||
Booking,
|
|
||||||
BookingReference,
|
|
||||||
BookingStatus,
|
|
||||||
EventType,
|
|
||||||
User,
|
|
||||||
WebhookTriggerEvents,
|
|
||||||
} from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
import type { TFunction } from "next-i18next";
|
|
||||||
import { z, ZodError } from "zod";
|
|
||||||
|
|
||||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
|
||||||
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
|
|
||||||
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
|
|
||||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
|
||||||
import dayjs from "@calcom/dayjs";
|
|
||||||
import { sendRequestRescheduleEmail } from "@calcom/emails";
|
|
||||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
|
||||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
|
||||||
import { isPrismaObjOrUndefined } from "@calcom/lib";
|
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
|
||||||
|
|
||||||
export type RescheduleResponse = Booking & {
|
|
||||||
attendees: Attendee[];
|
|
||||||
};
|
|
||||||
export type PersonAttendeeCommonFields = Pick<
|
|
||||||
User,
|
|
||||||
"id" | "email" | "name" | "locale" | "timeZone" | "username"
|
|
||||||
>;
|
|
||||||
|
|
||||||
const rescheduleSchema = z.object({
|
|
||||||
bookingId: z.string(),
|
|
||||||
rescheduleReason: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const findUserDataByUserId = async (userId: number) => {
|
|
||||||
return await prisma.user.findUniqueOrThrow({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
username: true,
|
|
||||||
email: true,
|
|
||||||
timeZone: true,
|
|
||||||
locale: true,
|
|
||||||
credentials: true,
|
|
||||||
destinationCalendar: true,
|
|
||||||
teams: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handler = async (
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
): Promise<RescheduleResponse | NextApiResponse | void> => {
|
|
||||||
const session = await getSession({ req });
|
|
||||||
const {
|
|
||||||
bookingId,
|
|
||||||
rescheduleReason: cancellationReason,
|
|
||||||
}: { bookingId: string; rescheduleReason: string; cancellationReason: string } = req.body;
|
|
||||||
let user: Awaited<ReturnType<typeof findUserDataByUserId>>;
|
|
||||||
try {
|
|
||||||
if (session?.user?.id) {
|
|
||||||
user = await findUserDataByUserId(session?.user.id);
|
|
||||||
} else {
|
|
||||||
return res.status(501).end();
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookingToReschedule = await prisma.booking.findFirstOrThrow({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
uid: true,
|
|
||||||
userId: true,
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
startTime: true,
|
|
||||||
endTime: true,
|
|
||||||
eventTypeId: true,
|
|
||||||
eventType: true,
|
|
||||||
location: true,
|
|
||||||
attendees: true,
|
|
||||||
references: true,
|
|
||||||
customInputs: true,
|
|
||||||
dynamicEventSlugRef: true,
|
|
||||||
dynamicGroupSlugRef: true,
|
|
||||||
destinationCalendar: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
uid: bookingId,
|
|
||||||
NOT: {
|
|
||||||
status: {
|
|
||||||
in: [BookingStatus.CANCELLED, BookingStatus.REJECTED],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bookingToReschedule.userId) {
|
|
||||||
throw new Error("Booking to reschedule doesn't have an owner.");
|
|
||||||
}
|
|
||||||
if (!bookingToReschedule.eventType) {
|
|
||||||
throw new Error("EventType not found for current booking.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookingBelongsToTeam = !!bookingToReschedule.eventType?.teamId;
|
|
||||||
if (bookingBelongsToTeam && bookingToReschedule.eventType?.teamId) {
|
|
||||||
const userTeamIds = user.teams.map((item) => item.teamId);
|
|
||||||
if (userTeamIds.indexOf(bookingToReschedule?.eventType?.teamId) === -1) {
|
|
||||||
throw new Error("User isn't a member on the team");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!bookingBelongsToTeam && bookingToReschedule.userId !== user.id) {
|
|
||||||
throw new Error("User isn't owner of the current booking");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bookingToReschedule && user) {
|
|
||||||
let event: Partial<EventType> = {};
|
|
||||||
if (bookingToReschedule.eventTypeId) {
|
|
||||||
event = await prisma.eventType.findFirstOrThrow({
|
|
||||||
select: {
|
|
||||||
title: true,
|
|
||||||
users: true,
|
|
||||||
schedulingType: true,
|
|
||||||
recurringEvent: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: bookingToReschedule.eventTypeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await prisma.booking.update({
|
|
||||||
where: {
|
|
||||||
id: bookingToReschedule.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
rescheduled: true,
|
|
||||||
cancellationReason,
|
|
||||||
status: BookingStatus.CANCELLED,
|
|
||||||
updatedAt: dayjs().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [mainAttendee] = bookingToReschedule.attendees;
|
|
||||||
// @NOTE: Should we assume attendees language?
|
|
||||||
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
|
|
||||||
const usersToPeopleType = (
|
|
||||||
users: PersonAttendeeCommonFields[],
|
|
||||||
selectedLanguage: TFunction
|
|
||||||
): Person[] => {
|
|
||||||
return users?.map((user) => {
|
|
||||||
return {
|
|
||||||
email: user.email || "",
|
|
||||||
name: user.name || "",
|
|
||||||
username: user?.username || "",
|
|
||||||
language: { translate: selectedLanguage, locale: user.locale || "en" },
|
|
||||||
timeZone: user?.timeZone,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const userTranslation = await getTranslation(user.locale ?? "en", "common");
|
|
||||||
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
|
|
||||||
|
|
||||||
const builder = new CalendarEventBuilder();
|
|
||||||
builder.init({
|
|
||||||
title: bookingToReschedule.title,
|
|
||||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
|
||||||
startTime: bookingToReschedule.startTime.toISOString(),
|
|
||||||
endTime: bookingToReschedule.endTime.toISOString(),
|
|
||||||
attendees: usersToPeopleType(
|
|
||||||
// username field doesn't exists on attendee but could be in the future
|
|
||||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
|
||||||
tAttendees
|
|
||||||
),
|
|
||||||
organizer: userAsPeopleType,
|
|
||||||
});
|
|
||||||
|
|
||||||
const director = new CalendarEventDirector();
|
|
||||||
director.setBuilder(builder);
|
|
||||||
director.setExistingBooking(bookingToReschedule);
|
|
||||||
director.setCancellationReason(cancellationReason);
|
|
||||||
if (event) {
|
|
||||||
await director.buildForRescheduleEmail();
|
|
||||||
} else {
|
|
||||||
await director.buildWithoutEventTypeForRescheduleEmail();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handling calendar and videos cancellation
|
|
||||||
// This can set previous time as available, until virtual calendar is done
|
|
||||||
const credentialsMap = new Map();
|
|
||||||
user.credentials.forEach((credential) => {
|
|
||||||
credentialsMap.set(credential.type, credential);
|
|
||||||
});
|
|
||||||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
|
|
||||||
(ref) => !!credentialsMap.get(ref.type)
|
|
||||||
);
|
|
||||||
bookingRefsFiltered.forEach((bookingRef) => {
|
|
||||||
if (bookingRef.uid) {
|
|
||||||
if (bookingRef.type.endsWith("_calendar")) {
|
|
||||||
const calendar = getCalendar(credentialsMap.get(bookingRef.type));
|
|
||||||
|
|
||||||
return calendar?.deleteEvent(
|
|
||||||
bookingRef.uid,
|
|
||||||
builder.calendarEvent,
|
|
||||||
bookingRef.externalCalendarId
|
|
||||||
);
|
|
||||||
} else if (bookingRef.type.endsWith("_video")) {
|
|
||||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send emails
|
|
||||||
await sendRequestRescheduleEmail(builder.calendarEvent, {
|
|
||||||
rescheduleLink: builder.rescheduleLink,
|
|
||||||
});
|
|
||||||
|
|
||||||
const evt: CalendarEvent = {
|
|
||||||
title: bookingToReschedule?.title,
|
|
||||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
|
||||||
description: bookingToReschedule?.description || "",
|
|
||||||
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
|
|
||||||
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
|
||||||
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
|
||||||
organizer: userAsPeopleType,
|
|
||||||
attendees: usersToPeopleType(
|
|
||||||
// username field doesn't exists on attendee but could be in the future
|
|
||||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
|
||||||
tAttendees
|
|
||||||
),
|
|
||||||
uid: bookingToReschedule?.uid,
|
|
||||||
location: bookingToReschedule?.location,
|
|
||||||
destinationCalendar:
|
|
||||||
bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
|
|
||||||
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send webhook
|
|
||||||
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
|
||||||
// Send Webhook call if hooked to BOOKING.CANCELLED
|
|
||||||
const subscriberOptions = {
|
|
||||||
userId: bookingToReschedule.userId,
|
|
||||||
eventTypeId: (bookingToReschedule.eventTypeId as number) || 0,
|
|
||||||
triggerEvent: eventTrigger,
|
|
||||||
};
|
|
||||||
const webhooks = await getWebhooks(subscriberOptions);
|
|
||||||
const promises = webhooks.map((webhook) =>
|
|
||||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, evt).catch((e) => {
|
|
||||||
console.error(
|
|
||||||
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json(bookingToReschedule);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new Error("Error.request.reschedule " + error?.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function validate(
|
|
||||||
handler: (req: NextApiRequest, res: NextApiResponse) => Promise<RescheduleResponse | NextApiResponse | void>
|
|
||||||
) {
|
|
||||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
if (req.method === "POST") {
|
|
||||||
try {
|
|
||||||
rescheduleSchema.parse(req.body);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ZodError && error?.name === "ZodError") {
|
|
||||||
return res.status(400).json(error?.issues);
|
|
||||||
}
|
|
||||||
return res.status(402).end();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return res.status(405).end();
|
|
||||||
}
|
|
||||||
await handler(req, res);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default validate(handler);
|
|
|
@ -1,32 +1,51 @@
|
||||||
import {
|
import {
|
||||||
|
BookingReference,
|
||||||
BookingStatus,
|
BookingStatus,
|
||||||
|
EventType,
|
||||||
Prisma,
|
Prisma,
|
||||||
SchedulingType,
|
SchedulingType,
|
||||||
|
User,
|
||||||
WebhookTriggerEvents,
|
WebhookTriggerEvents,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowsOnEventTypes,
|
WorkflowsOnEventTypes,
|
||||||
WorkflowStep,
|
WorkflowStep,
|
||||||
} from "@prisma/client";
|
} from "@prisma/client";
|
||||||
|
import type { TFunction } from "next-i18next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||||
import { refund } from "@calcom/app-store/stripepayment/lib/server";
|
import { refund } from "@calcom/app-store/stripepayment/lib/server";
|
||||||
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||||
import EventManager from "@calcom/core/EventManager";
|
import EventManager from "@calcom/core/EventManager";
|
||||||
|
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
|
||||||
|
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
|
||||||
|
import { deleteMeeting } from "@calcom/core/videoClient";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sendDeclinedEmails, sendLocationChangeEmails, sendScheduledEmails } from "@calcom/emails";
|
import {
|
||||||
|
sendDeclinedEmails,
|
||||||
|
sendLocationChangeEmails,
|
||||||
|
sendRequestRescheduleEmail,
|
||||||
|
sendScheduledEmails,
|
||||||
|
} from "@calcom/emails";
|
||||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
|
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { getTranslation } from "@calcom/lib/server";
|
import { getTranslation } from "@calcom/lib/server";
|
||||||
import { bookingConfirmPatchBodySchema } from "@calcom/prisma/zod-utils";
|
import { bookingConfirmPatchBodySchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
import type { AdditionalInformation, CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import { createProtectedRouter } from "../../createRouter";
|
import { createProtectedRouter } from "../../createRouter";
|
||||||
|
|
||||||
|
export type PersonAttendeeCommonFields = Pick<
|
||||||
|
User,
|
||||||
|
"id" | "email" | "name" | "locale" | "timeZone" | "username"
|
||||||
|
>;
|
||||||
|
|
||||||
// Common data for all endpoints under webhook
|
// Common data for all endpoints under webhook
|
||||||
const commonBookingSchema = z.object({
|
const commonBookingSchema = z.object({
|
||||||
bookingId: z.number(),
|
bookingId: z.number(),
|
||||||
|
@ -35,6 +54,216 @@ const commonBookingSchema = z.object({
|
||||||
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
||||||
|
|
||||||
export const bookingsRouter = createProtectedRouter()
|
export const bookingsRouter = createProtectedRouter()
|
||||||
|
.mutation("requestReschedule", {
|
||||||
|
input: z.object({
|
||||||
|
bookingId: z.string(),
|
||||||
|
rescheduleReason: z.string().optional(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const { user, prisma } = ctx;
|
||||||
|
const { bookingId, rescheduleReason: cancellationReason } = input;
|
||||||
|
|
||||||
|
const bookingToReschedule = await prisma.booking.findFirstOrThrow({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
uid: true,
|
||||||
|
userId: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
eventTypeId: true,
|
||||||
|
eventType: true,
|
||||||
|
location: true,
|
||||||
|
attendees: true,
|
||||||
|
references: true,
|
||||||
|
customInputs: true,
|
||||||
|
dynamicEventSlugRef: true,
|
||||||
|
dynamicGroupSlugRef: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
uid: bookingId,
|
||||||
|
NOT: {
|
||||||
|
status: {
|
||||||
|
in: [BookingStatus.CANCELLED, BookingStatus.REJECTED],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bookingToReschedule.userId) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN", message: "Booking to reschedule doesn't have an owner" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookingToReschedule.eventType) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN", message: "EventType not found for current booking." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookingBelongsToTeam = !!bookingToReschedule.eventType?.teamId;
|
||||||
|
|
||||||
|
const userTeams = await prisma.user.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
teams: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (!bookingBelongsToTeam && bookingToReschedule.userId !== user.id) {
|
||||||
|
throw new TRPCError({ code: "FORBIDDEN", message: "User isn't owner of the current booking" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookingToReschedule) {
|
||||||
|
let event: Partial<EventType> = {};
|
||||||
|
if (bookingToReschedule.eventTypeId) {
|
||||||
|
event = await prisma.eventType.findFirstOrThrow({
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
users: true,
|
||||||
|
schedulingType: true,
|
||||||
|
recurringEvent: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: bookingToReschedule.eventTypeId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
id: bookingToReschedule.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
rescheduled: true,
|
||||||
|
cancellationReason,
|
||||||
|
status: BookingStatus.CANCELLED,
|
||||||
|
updatedAt: dayjs().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [mainAttendee] = bookingToReschedule.attendees;
|
||||||
|
// @NOTE: Should we assume attendees language?
|
||||||
|
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
|
||||||
|
const usersToPeopleType = (
|
||||||
|
users: PersonAttendeeCommonFields[],
|
||||||
|
selectedLanguage: TFunction
|
||||||
|
): Person[] => {
|
||||||
|
return users?.map((user) => {
|
||||||
|
return {
|
||||||
|
email: user.email || "",
|
||||||
|
name: user.name || "",
|
||||||
|
username: user?.username || "",
|
||||||
|
language: { translate: selectedLanguage, locale: user.locale || "en" },
|
||||||
|
timeZone: user?.timeZone,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const userTranslation = await getTranslation(user.locale ?? "en", "common");
|
||||||
|
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
|
||||||
|
|
||||||
|
const builder = new CalendarEventBuilder();
|
||||||
|
builder.init({
|
||||||
|
title: bookingToReschedule.title,
|
||||||
|
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||||
|
startTime: bookingToReschedule.startTime.toISOString(),
|
||||||
|
endTime: bookingToReschedule.endTime.toISOString(),
|
||||||
|
attendees: usersToPeopleType(
|
||||||
|
// username field doesn't exists on attendee but could be in the future
|
||||||
|
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||||
|
tAttendees
|
||||||
|
),
|
||||||
|
organizer: userAsPeopleType,
|
||||||
|
});
|
||||||
|
|
||||||
|
const director = new CalendarEventDirector();
|
||||||
|
director.setBuilder(builder);
|
||||||
|
director.setExistingBooking(bookingToReschedule);
|
||||||
|
cancellationReason && director.setCancellationReason(cancellationReason);
|
||||||
|
if (event) {
|
||||||
|
await director.buildForRescheduleEmail();
|
||||||
|
} else {
|
||||||
|
await director.buildWithoutEventTypeForRescheduleEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handling calendar and videos cancellation
|
||||||
|
// This can set previous time as available, until virtual calendar is done
|
||||||
|
const credentialsMap = new Map();
|
||||||
|
user.credentials.forEach((credential) => {
|
||||||
|
credentialsMap.set(credential.type, credential);
|
||||||
|
});
|
||||||
|
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
|
||||||
|
(ref) => !!credentialsMap.get(ref.type)
|
||||||
|
);
|
||||||
|
bookingRefsFiltered.forEach((bookingRef) => {
|
||||||
|
if (bookingRef.uid) {
|
||||||
|
if (bookingRef.type.endsWith("_calendar")) {
|
||||||
|
const calendar = getCalendar(credentialsMap.get(bookingRef.type));
|
||||||
|
|
||||||
|
return calendar?.deleteEvent(
|
||||||
|
bookingRef.uid,
|
||||||
|
builder.calendarEvent,
|
||||||
|
bookingRef.externalCalendarId
|
||||||
|
);
|
||||||
|
} else if (bookingRef.type.endsWith("_video")) {
|
||||||
|
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send emails
|
||||||
|
await sendRequestRescheduleEmail(builder.calendarEvent, {
|
||||||
|
rescheduleLink: builder.rescheduleLink,
|
||||||
|
});
|
||||||
|
|
||||||
|
const evt: CalendarEvent = {
|
||||||
|
title: bookingToReschedule?.title,
|
||||||
|
type: event && event.title ? event.title : bookingToReschedule.title,
|
||||||
|
description: bookingToReschedule?.description || "",
|
||||||
|
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
|
||||||
|
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
||||||
|
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
||||||
|
organizer: userAsPeopleType,
|
||||||
|
attendees: usersToPeopleType(
|
||||||
|
// username field doesn't exists on attendee but could be in the future
|
||||||
|
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
||||||
|
tAttendees
|
||||||
|
),
|
||||||
|
uid: bookingToReschedule?.uid,
|
||||||
|
location: bookingToReschedule?.location,
|
||||||
|
destinationCalendar:
|
||||||
|
bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
|
||||||
|
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send webhook
|
||||||
|
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
||||||
|
// Send Webhook call if hooked to BOOKING.CANCELLED
|
||||||
|
const subscriberOptions = {
|
||||||
|
userId: bookingToReschedule.userId,
|
||||||
|
eventTypeId: (bookingToReschedule.eventTypeId as number) || 0,
|
||||||
|
triggerEvent: eventTrigger,
|
||||||
|
};
|
||||||
|
const webhooks = await getWebhooks(subscriberOptions);
|
||||||
|
const promises = webhooks.map((webhook) =>
|
||||||
|
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, evt).catch((e) => {
|
||||||
|
console.error(
|
||||||
|
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
.middleware(async ({ ctx, rawInput, next }) => {
|
.middleware(async ({ ctx, rawInput, next }) => {
|
||||||
// Endpoints that just read the logged in user's data - like 'list' don't necessary have any input
|
// Endpoints that just read the logged in user's data - like 'list' don't necessary have any input
|
||||||
if (!rawInput) return next({ ctx: { ...ctx, booking: null } });
|
if (!rawInput) return next({ ctx: { ...ctx, booking: null } });
|
||||||
|
|
Loading…
Reference in New Issue