Merge branch 'main' into fix/after-meeting-ends-migration
This commit is contained in:
commit
c114fa8837
|
@ -277,179 +277,6 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
integration:
|
integration:
|
||||||
type: string
|
type: string
|
||||||
/api/availability/day:
|
|
||||||
patch:
|
|
||||||
description: Updates the start and end times for a user's availability.
|
|
||||||
summary: Updates the user's start and end times
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
startMins:
|
|
||||||
type: string
|
|
||||||
endMins:
|
|
||||||
type: string
|
|
||||||
bufferMins:
|
|
||||||
type: string
|
|
||||||
description: ""
|
|
||||||
/api/availability/eventtype:
|
|
||||||
post:
|
|
||||||
description: Adds a new event type for the user.
|
|
||||||
summary: Adds a new event type
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
length:
|
|
||||||
type: string
|
|
||||||
hidden:
|
|
||||||
type: boolean
|
|
||||||
requiresConfirmation:
|
|
||||||
type: boolean
|
|
||||||
locations:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
eventName:
|
|
||||||
type: string
|
|
||||||
customInputs:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
eventType:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
length:
|
|
||||||
type: string
|
|
||||||
hidden:
|
|
||||||
type: boolean
|
|
||||||
requiresConfirmation:
|
|
||||||
type: boolean
|
|
||||||
locations:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
eventName:
|
|
||||||
type: string
|
|
||||||
customInputs:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
patch:
|
|
||||||
description: Updates an event type for the user.
|
|
||||||
summary: Updates an event type
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
length:
|
|
||||||
type: string
|
|
||||||
hidden:
|
|
||||||
type: boolean
|
|
||||||
requiresConfirmation:
|
|
||||||
type: boolean
|
|
||||||
locations:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
eventName:
|
|
||||||
type: string
|
|
||||||
customInputs:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
eventType:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
slug:
|
|
||||||
type: string
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
length:
|
|
||||||
type: string
|
|
||||||
hidden:
|
|
||||||
type: boolean
|
|
||||||
requiresConfirmation:
|
|
||||||
type: boolean
|
|
||||||
locations:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
eventName:
|
|
||||||
type: string
|
|
||||||
customInputs:
|
|
||||||
type: array
|
|
||||||
items: {}
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
delete:
|
|
||||||
description: Deletes an event type for the user.
|
|
||||||
summary: Deletes an event type
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties: {}
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
"/api/book/event":
|
"/api/book/event":
|
||||||
post:
|
post:
|
||||||
description: Creates a booking in the user's calendar.
|
description: Creates a booking in the user's calendar.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { BookingStatus } from "@prisma/client";
|
import { BookingStatus } from "@prisma/client";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation } from "react-query";
|
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
@ -16,7 +15,6 @@ import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||||
import { TextArea } from "@calcom/ui/form/fields";
|
import { TextArea } from "@calcom/ui/form/fields";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
import { extractRecurringDates } from "@lib/parseDate";
|
import { extractRecurringDates } from "@lib/parseDate";
|
||||||
|
|
||||||
|
@ -42,12 +40,17 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||||
const mutation = useMutation(
|
const mutation = trpc.useMutation(["viewer.bookings.confirm"], {
|
||||||
async (confirm: boolean) => {
|
onSuccess: () => {
|
||||||
|
setRejectionDialogIsOpen(false);
|
||||||
|
utils.invalidateQueries("viewer.bookings");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookingConfirm = async (confirm: boolean) => {
|
||||||
let body = {
|
let body = {
|
||||||
id: booking.id,
|
bookingId: booking.id,
|
||||||
confirmed: confirm,
|
confirmed: confirm,
|
||||||
language: i18n.language,
|
|
||||||
reason: rejectionReason,
|
reason: rejectionReason,
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
|
@ -57,24 +60,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
|
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
|
||||||
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
|
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
|
||||||
}
|
}
|
||||||
const res = await fetch("/api/book/confirm", {
|
mutation.mutate(body);
|
||||||
method: "PATCH",
|
};
|
||||||
body: JSON.stringify(body),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new HttpError({ statusCode: res.status });
|
|
||||||
}
|
|
||||||
setRejectionDialogIsOpen(false);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
async onSettled() {
|
|
||||||
await utils.invalidateQueries(["viewer.bookings"]);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const isUpcoming = new Date(booking.endTime) >= new Date();
|
const isUpcoming = new Date(booking.endTime) >= new Date();
|
||||||
const isCancelled = booking.status === BookingStatus.CANCELLED;
|
const isCancelled = booking.status === BookingStatus.CANCELLED;
|
||||||
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||||
|
@ -101,7 +89,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
? t("confirm_all")
|
? t("confirm_all")
|
||||||
: t("confirm"),
|
: t("confirm"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
mutation.mutate(true);
|
bookingConfirm(true);
|
||||||
},
|
},
|
||||||
icon: Icon.FiCheck,
|
icon: Icon.FiCheck,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
|
@ -269,7 +257,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutation.mutate(false);
|
bookingConfirm(false);
|
||||||
}}>
|
}}>
|
||||||
{t("rejection_confirmation")}
|
{t("rejection_confirmation")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -8,6 +8,7 @@ export type GetSubscriberOptions = {
|
||||||
triggerEvent: WebhookTriggerEvents;
|
triggerEvent: WebhookTriggerEvents;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @deprecated use `packages/lib/webhooks/subscriptions.tsx` */
|
||||||
const getWebhooks = async (options: GetSubscriberOptions) => {
|
const getWebhooks = async (options: GetSubscriberOptions) => {
|
||||||
const { userId, eventTypeId } = options;
|
const { userId, eventTypeId } = options;
|
||||||
const allWebhooks = await prisma.webhook.findMany({
|
const allWebhooks = await prisma.webhook.findMany({
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
|
||||||
|
|
||||||
import prisma from "../../../lib/prisma";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const session = await getSession({ req: req });
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
res.status(401).json({ message: "Not authenticated" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method == "PATCH") {
|
|
||||||
const startMins = req.body.start;
|
|
||||||
const endMins = req.body.end;
|
|
||||||
const bufferMins = req.body.buffer;
|
|
||||||
|
|
||||||
await prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: session.user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
startTime: startMins,
|
|
||||||
endTime: endMins,
|
|
||||||
bufferTime: bufferMins,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({ message: "Start and end times updated successfully" });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
|
||||||
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
|
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const session = await getSession({ req });
|
|
||||||
/** So we can reuse tRCP queries */
|
|
||||||
const trpcCtx = await createContext({ req, res });
|
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
res.status(401).json({ message: "Not authenticated" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === "POST") {
|
|
||||||
const eventType = await viewerRouter.createCaller(trpcCtx).mutation("eventTypes.create", req.body);
|
|
||||||
res.status(201).json({ eventType });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === "PATCH") {
|
|
||||||
const eventType = await viewerRouter.createCaller(trpcCtx).mutation("eventTypes.update", req.body);
|
|
||||||
res.status(201).json({ eventType });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === "DELETE") {
|
|
||||||
await viewerRouter.createCaller(trpcCtx).mutation("eventTypes.delete", { id: req.body.id });
|
|
||||||
res.status(200).json({ id: req.body.id, message: "Event Type deleted" });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,425 +0,0 @@
|
||||||
import {
|
|
||||||
Booking,
|
|
||||||
BookingStatus,
|
|
||||||
Prisma,
|
|
||||||
SchedulingType,
|
|
||||||
User,
|
|
||||||
WebhookTriggerEvents,
|
|
||||||
Workflow,
|
|
||||||
WorkflowsOnEventTypes,
|
|
||||||
WorkflowStep,
|
|
||||||
} from "@prisma/client";
|
|
||||||
import type { NextApiRequest } from "next";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { refund } from "@calcom/app-store/stripepayment/lib/server";
|
|
||||||
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
|
||||||
import EventManager from "@calcom/core/EventManager";
|
|
||||||
import { sendDeclinedEmails, sendScheduledEmails } from "@calcom/emails";
|
|
||||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
|
||||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
|
||||||
import logger from "@calcom/lib/logger";
|
|
||||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
import getSubscribers from "@lib/webhooks/subscriptions";
|
|
||||||
|
|
||||||
import { getTranslation } from "@server/lib/i18n";
|
|
||||||
|
|
||||||
const authorized = async (
|
|
||||||
currentUser: Pick<User, "id">,
|
|
||||||
booking: Pick<Booking, "eventTypeId" | "userId">
|
|
||||||
) => {
|
|
||||||
// if the organizer
|
|
||||||
if (booking.userId === currentUser.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const eventType = await prisma.eventType.findUnique({
|
|
||||||
where: {
|
|
||||||
id: booking.eventTypeId || undefined,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
schedulingType: true,
|
|
||||||
users: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
eventType?.schedulingType === SchedulingType.COLLECTIVE &&
|
|
||||||
eventType.users.find((user) => user.id === currentUser.id)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
|
||||||
|
|
||||||
const bookingConfirmPatchBodySchema = z.object({
|
|
||||||
confirmed: z.boolean(),
|
|
||||||
id: z.number(),
|
|
||||||
recurringEventId: z.string().optional(),
|
|
||||||
reason: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
async function patchHandler(req: NextApiRequest) {
|
|
||||||
const session = await getSession({ req });
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
id: bookingId,
|
|
||||||
recurringEventId,
|
|
||||||
reason: rejectionReason,
|
|
||||||
confirmed,
|
|
||||||
} = bookingConfirmPatchBodySchema.parse(req.body);
|
|
||||||
|
|
||||||
const currentUser = await prisma.user.findFirst({
|
|
||||||
rejectOnNotFound() {
|
|
||||||
throw new HttpError({ statusCode: 404, message: "User not found" });
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: session.user.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
credentials: {
|
|
||||||
orderBy: { id: "desc" as Prisma.SortOrder },
|
|
||||||
},
|
|
||||||
timeZone: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
username: true,
|
|
||||||
destinationCalendar: true,
|
|
||||||
locale: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tOrganizer = await getTranslation(currentUser.locale ?? "en", "common");
|
|
||||||
|
|
||||||
const booking = await prisma.booking.findFirst({
|
|
||||||
where: {
|
|
||||||
id: bookingId,
|
|
||||||
},
|
|
||||||
rejectOnNotFound() {
|
|
||||||
throw new HttpError({ statusCode: 404, message: "Booking not found" });
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
customInputs: true,
|
|
||||||
startTime: true,
|
|
||||||
endTime: true,
|
|
||||||
attendees: true,
|
|
||||||
eventTypeId: true,
|
|
||||||
eventType: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
recurringEvent: true,
|
|
||||||
requiresConfirmation: true,
|
|
||||||
workflows: {
|
|
||||||
include: {
|
|
||||||
workflow: {
|
|
||||||
include: {
|
|
||||||
steps: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
location: true,
|
|
||||||
userId: true,
|
|
||||||
id: true,
|
|
||||||
uid: true,
|
|
||||||
payment: true,
|
|
||||||
destinationCalendar: true,
|
|
||||||
paid: true,
|
|
||||||
recurringEventId: true,
|
|
||||||
status: true,
|
|
||||||
smsReminderNumber: true,
|
|
||||||
scheduledJobs: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!(await authorized(currentUser, booking))) {
|
|
||||||
throw new HttpError({ statusCode: 401, message: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
|
||||||
if (isConfirmed) {
|
|
||||||
throw new HttpError({ statusCode: 400, message: "booking already confirmed" });
|
|
||||||
}
|
|
||||||
|
|
||||||
/** When a booking that requires payment its being confirmed but doesn't have any payment,
|
|
||||||
* we shouldn’t save it on DestinationCalendars
|
|
||||||
*/
|
|
||||||
if (booking.payment.length > 0 && !booking.paid) {
|
|
||||||
await prisma.booking.update({
|
|
||||||
where: {
|
|
||||||
id: bookingId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BookingStatus.ACCEPTED,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
req.statusCode = 204;
|
|
||||||
return { message: "Booking confirmed" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
|
||||||
return {
|
|
||||||
name: attendee.name,
|
|
||||||
email: attendee.email,
|
|
||||||
timeZone: attendee.timeZone,
|
|
||||||
language: {
|
|
||||||
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
|
||||||
locale: attendee.locale ?? "en",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const attendeesList = await Promise.all(attendeesListPromises);
|
|
||||||
|
|
||||||
const evt: CalendarEvent = {
|
|
||||||
type: booking.title,
|
|
||||||
title: booking.title,
|
|
||||||
description: booking.description,
|
|
||||||
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
|
||||||
startTime: booking.startTime.toISOString(),
|
|
||||||
endTime: booking.endTime.toISOString(),
|
|
||||||
organizer: {
|
|
||||||
email: currentUser.email,
|
|
||||||
name: currentUser.name || "Unnamed",
|
|
||||||
timeZone: currentUser.timeZone,
|
|
||||||
language: { translate: tOrganizer, locale: currentUser.locale ?? "en" },
|
|
||||||
},
|
|
||||||
attendees: attendeesList,
|
|
||||||
location: booking.location ?? "",
|
|
||||||
uid: booking.uid,
|
|
||||||
destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar,
|
|
||||||
requiresConfirmation: booking?.eventType?.requiresConfirmation ?? false,
|
|
||||||
eventTypeId: booking.eventType?.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
|
|
||||||
if (recurringEventId && recurringEvent) {
|
|
||||||
const groupedRecurringBookings = await prisma.booking.groupBy({
|
|
||||||
where: {
|
|
||||||
recurringEventId: booking.recurringEventId,
|
|
||||||
},
|
|
||||||
by: [Prisma.BookingScalarFieldEnum.recurringEventId],
|
|
||||||
_count: true,
|
|
||||||
});
|
|
||||||
// Overriding the recurring event configuration count to be the actual number of events booked for
|
|
||||||
// the recurring event (equal or less than recurring event configuration count)
|
|
||||||
recurringEvent.count = groupedRecurringBookings[0]._count;
|
|
||||||
// count changed, parsing again to get the new value in
|
|
||||||
evt.recurringEvent = parseRecurringEvent(recurringEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
const eventManager = new EventManager(currentUser);
|
|
||||||
const scheduleResult = await eventManager.create(evt);
|
|
||||||
|
|
||||||
const results = scheduleResult.results;
|
|
||||||
|
|
||||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
|
||||||
const error = {
|
|
||||||
errorCode: "BookingCreatingMeetingFailed",
|
|
||||||
message: "Booking failed",
|
|
||||||
};
|
|
||||||
|
|
||||||
log.error(`Booking ${currentUser.username} failed`, error, results);
|
|
||||||
} else {
|
|
||||||
const metadata: AdditionalInformation = {};
|
|
||||||
|
|
||||||
if (results.length) {
|
|
||||||
// TODO: Handle created event metadata more elegantly
|
|
||||||
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
|
|
||||||
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
|
||||||
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await sendScheduledEmails({ ...evt, additionalInformation: metadata });
|
|
||||||
} catch (error) {
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let updatedBookings: {
|
|
||||||
scheduledJobs: string[];
|
|
||||||
id: number;
|
|
||||||
startTime: Date;
|
|
||||||
endTime: Date;
|
|
||||||
uid: string;
|
|
||||||
smsReminderNumber: string | null;
|
|
||||||
eventType: {
|
|
||||||
workflows: (WorkflowsOnEventTypes & {
|
|
||||||
workflow: Workflow & {
|
|
||||||
steps: WorkflowStep[];
|
|
||||||
};
|
|
||||||
})[];
|
|
||||||
} | null;
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
if (recurringEventId) {
|
|
||||||
// The booking to confirm is a recurring event and comes from /booking/recurring, proceeding to mark all related
|
|
||||||
// bookings as confirmed. Prisma updateMany does not support relations, so doing this in two steps for now.
|
|
||||||
const unconfirmedRecurringBookings = await prisma.booking.findMany({
|
|
||||||
where: {
|
|
||||||
recurringEventId,
|
|
||||||
status: BookingStatus.PENDING,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateBookingsPromise = unconfirmedRecurringBookings.map((recurringBooking) => {
|
|
||||||
return prisma.booking.update({
|
|
||||||
where: {
|
|
||||||
id: recurringBooking.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BookingStatus.ACCEPTED,
|
|
||||||
references: {
|
|
||||||
create: scheduleResult.referencesToCreate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
eventType: {
|
|
||||||
select: {
|
|
||||||
workflows: {
|
|
||||||
include: {
|
|
||||||
workflow: {
|
|
||||||
include: {
|
|
||||||
steps: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
uid: true,
|
|
||||||
startTime: true,
|
|
||||||
endTime: true,
|
|
||||||
smsReminderNumber: true,
|
|
||||||
id: true,
|
|
||||||
scheduledJobs: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const updatedBookingsResult = await Promise.all(updateBookingsPromise);
|
|
||||||
updatedBookings = updatedBookings.concat(updatedBookingsResult);
|
|
||||||
} else {
|
|
||||||
// @NOTE: be careful with this as if any error occurs before this booking doesn't get confirmed
|
|
||||||
// Should perform update on booking (confirm) -> then trigger the rest handlers
|
|
||||||
const updatedBooking = await prisma.booking.update({
|
|
||||||
where: {
|
|
||||||
id: bookingId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BookingStatus.ACCEPTED,
|
|
||||||
references: {
|
|
||||||
create: scheduleResult.referencesToCreate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
eventType: {
|
|
||||||
select: {
|
|
||||||
workflows: {
|
|
||||||
include: {
|
|
||||||
workflow: {
|
|
||||||
include: {
|
|
||||||
steps: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
uid: true,
|
|
||||||
startTime: true,
|
|
||||||
endTime: true,
|
|
||||||
smsReminderNumber: true,
|
|
||||||
id: true,
|
|
||||||
scheduledJobs: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
updatedBookings.push(updatedBooking);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Workflows - set reminders for confirmed events
|
|
||||||
for (const updatedBooking of updatedBookings) {
|
|
||||||
if (updatedBooking.eventType?.workflows) {
|
|
||||||
const evtOfBooking = evt;
|
|
||||||
evtOfBooking.startTime = updatedBooking.startTime.toISOString();
|
|
||||||
evtOfBooking.endTime = updatedBooking.endTime.toISOString();
|
|
||||||
evtOfBooking.uid = updatedBooking.uid;
|
|
||||||
|
|
||||||
await scheduleWorkflowReminders(
|
|
||||||
updatedBooking.eventType.workflows,
|
|
||||||
updatedBooking.smsReminderNumber,
|
|
||||||
evtOfBooking,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// schedule job for zapier trigger 'when meeting ends'
|
|
||||||
const subscriberOptionsMeetingEnded = {
|
|
||||||
userId: booking.userId || 0,
|
|
||||||
eventTypeId: booking.eventTypeId || 0,
|
|
||||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
|
|
||||||
|
|
||||||
subscribersMeetingEnded.forEach((subscriber) => {
|
|
||||||
updatedBookings.forEach((booking) => {
|
|
||||||
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
evt.rejectionReason = rejectionReason;
|
|
||||||
if (recurringEventId) {
|
|
||||||
// The booking to reject is a recurring event and comes from /booking/upcoming, proceeding to mark all related
|
|
||||||
// bookings as rejected.
|
|
||||||
await prisma.booking.updateMany({
|
|
||||||
where: {
|
|
||||||
recurringEventId,
|
|
||||||
status: BookingStatus.PENDING,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BookingStatus.REJECTED,
|
|
||||||
rejectionReason,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await refund(booking, evt); // No payment integration for recurring events for v1
|
|
||||||
await prisma.booking.update({
|
|
||||||
where: {
|
|
||||||
id: bookingId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BookingStatus.REJECTED,
|
|
||||||
rejectionReason,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendDeclinedEmails(evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.statusCode = 204;
|
|
||||||
return { message: "Booking " + confirmed ? "confirmed" : "rejected" };
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BookConfirmPatchResponse = Awaited<ReturnType<typeof patchHandler>>;
|
|
||||||
|
|
||||||
export default defaultHandler({
|
|
||||||
// To prevent too much git diff until moved to another file
|
|
||||||
PATCH: Promise.resolve({ default: defaultResponder(patchHandler) }),
|
|
||||||
});
|
|
|
@ -26,9 +26,9 @@ import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defa
|
||||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
|
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { getLuckyUser } from "@calcom/lib/server";
|
import { defaultResponder, getLuckyUser } from "@calcom/lib/server";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
|
||||||
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
|
import getSubscribers from "@calcom/lib/webhooks/subscriptions";
|
||||||
import prisma, { userSelect } from "@calcom/prisma";
|
import prisma, { userSelect } from "@calcom/prisma";
|
||||||
import { extendedBookingCreateBody } from "@calcom/prisma/zod-utils";
|
import { extendedBookingCreateBody } from "@calcom/prisma/zod-utils";
|
||||||
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
|
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
|
||||||
|
@ -38,7 +38,6 @@ import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
import sendPayload from "@lib/webhooks/sendPayload";
|
import sendPayload from "@lib/webhooks/sendPayload";
|
||||||
import getSubscribers from "@lib/webhooks/subscriptions";
|
|
||||||
|
|
||||||
import { getTranslation } from "@server/lib/i18n";
|
import { getTranslation } from "@server/lib/i18n";
|
||||||
|
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const session = await getSession({ req: req });
|
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
|
||||||
res.status(401).json({ message: "Not authenticated" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
|
||||||
id: true,
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
length: true,
|
|
||||||
schedulingType: true,
|
|
||||||
slug: true,
|
|
||||||
hidden: true,
|
|
||||||
price: true,
|
|
||||||
currency: true,
|
|
||||||
metadata: true,
|
|
||||||
users: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
avatar: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
rejectOnNotFound: true,
|
|
||||||
where: {
|
|
||||||
id: session.user.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
name: true,
|
|
||||||
startTime: true,
|
|
||||||
endTime: true,
|
|
||||||
bufferTime: true,
|
|
||||||
avatar: true,
|
|
||||||
completedOnboarding: true,
|
|
||||||
createdDate: true,
|
|
||||||
plan: true,
|
|
||||||
teams: {
|
|
||||||
where: {
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
role: true,
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
logo: true,
|
|
||||||
members: {
|
|
||||||
select: {
|
|
||||||
userId: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
eventTypes: {
|
|
||||||
select: eventTypeSelect,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
eventTypes: {
|
|
||||||
where: {
|
|
||||||
team: null,
|
|
||||||
},
|
|
||||||
select: eventTypeSelect,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// backwards compatibility, TMP:
|
|
||||||
const typesRaw = await prisma.eventType.findMany({
|
|
||||||
where: {
|
|
||||||
userId: session.user.id,
|
|
||||||
},
|
|
||||||
select: eventTypeSelect,
|
|
||||||
});
|
|
||||||
|
|
||||||
type EventTypeGroup = {
|
|
||||||
teamId?: number | null;
|
|
||||||
profile?: {
|
|
||||||
slug: typeof user["username"];
|
|
||||||
name: typeof user["name"];
|
|
||||||
image: typeof user["avatar"];
|
|
||||||
};
|
|
||||||
metadata: {
|
|
||||||
membershipCount: number;
|
|
||||||
readOnly: boolean;
|
|
||||||
};
|
|
||||||
eventTypes: (typeof user.eventTypes[number] & { $disabled?: boolean })[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventTypesHashMap = user.eventTypes.concat(typesRaw).reduce((hashMap, newItem) => {
|
|
||||||
const oldItem = hashMap[newItem.id] || {};
|
|
||||||
hashMap[newItem.id] = { ...oldItem, ...newItem };
|
|
||||||
return hashMap;
|
|
||||||
}, {} as Record<number, EventTypeGroup["eventTypes"][number]>);
|
|
||||||
const mergedEventTypes = Object.values(eventTypesHashMap).map((et, index) => ({
|
|
||||||
...et,
|
|
||||||
$disabled: user?.plan === "FREE" && index > 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return res.status(200).json({ eventTypes: mergedEventTypes });
|
|
||||||
}
|
|
|
@ -89,6 +89,13 @@ export const bookingCreateBodySchema = z.object({
|
||||||
|
|
||||||
export type BookingCreateBody = z.input<typeof bookingCreateBodySchema>;
|
export type BookingCreateBody = z.input<typeof bookingCreateBodySchema>;
|
||||||
|
|
||||||
|
export const bookingConfirmPatchBodySchema = z.object({
|
||||||
|
bookingId: z.number(),
|
||||||
|
confirmed: z.boolean(),
|
||||||
|
recurringEventId: z.string().optional(),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const extendedBookingCreateBody = bookingCreateBodySchema.merge(
|
export const extendedBookingCreateBody = bookingCreateBodySchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
noEmail: z.boolean().optional(),
|
noEmail: z.boolean().optional(),
|
||||||
|
|
|
@ -1,13 +1,26 @@
|
||||||
import { SchedulingType } from "@prisma/client";
|
import {
|
||||||
|
BookingStatus,
|
||||||
|
Prisma,
|
||||||
|
SchedulingType,
|
||||||
|
WebhookTriggerEvents,
|
||||||
|
Workflow,
|
||||||
|
WorkflowsOnEventTypes,
|
||||||
|
WorkflowStep,
|
||||||
|
} from "@prisma/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||||
|
import { refund } from "@calcom/app-store/stripepayment/lib/server";
|
||||||
|
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
||||||
import EventManager from "@calcom/core/EventManager";
|
import EventManager from "@calcom/core/EventManager";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sendLocationChangeEmails } from "@calcom/emails";
|
import { sendDeclinedEmails, sendLocationChangeEmails, sendScheduledEmails } from "@calcom/emails";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||||
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server";
|
||||||
|
import getSubscribers from "@calcom/lib/webhooks/subscriptions";
|
||||||
|
import { bookingConfirmPatchBodySchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
@ -19,6 +32,8 @@ const commonBookingSchema = z.object({
|
||||||
bookingId: z.number(),
|
bookingId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
||||||
|
|
||||||
export const bookingsRouter = createProtectedRouter()
|
export const bookingsRouter = createProtectedRouter()
|
||||||
.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
|
||||||
|
@ -169,4 +184,344 @@ export const bookingsRouter = createProtectedRouter()
|
||||||
}
|
}
|
||||||
return { message: "Location updated" };
|
return { message: "Location updated" };
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
.mutation("confirm", {
|
||||||
|
input: bookingConfirmPatchBodySchema,
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const { user, prisma } = ctx;
|
||||||
|
const { bookingId, recurringEventId, reason: rejectionReason, confirmed } = input;
|
||||||
|
|
||||||
|
const tOrganizer = await getTranslation(user.locale ?? "en", "common");
|
||||||
|
|
||||||
|
const booking = await prisma.booking.findFirst({
|
||||||
|
where: {
|
||||||
|
id: bookingId,
|
||||||
|
},
|
||||||
|
rejectOnNotFound() {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Booking not found" });
|
||||||
|
},
|
||||||
|
// should trpc handle this error ?
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
customInputs: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
attendees: true,
|
||||||
|
eventTypeId: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
recurringEvent: true,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
workflows: {
|
||||||
|
include: {
|
||||||
|
workflow: {
|
||||||
|
include: {
|
||||||
|
steps: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
location: true,
|
||||||
|
userId: true,
|
||||||
|
id: true,
|
||||||
|
uid: true,
|
||||||
|
payment: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
paid: true,
|
||||||
|
recurringEventId: true,
|
||||||
|
status: true,
|
||||||
|
smsReminderNumber: true,
|
||||||
|
scheduledJobs: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const authorized = async () => {
|
||||||
|
// if the organizer
|
||||||
|
if (booking.userId === user.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const eventType = await prisma.eventType.findUnique({
|
||||||
|
where: {
|
||||||
|
id: booking.eventTypeId || undefined,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
schedulingType: true,
|
||||||
|
users: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
eventType?.schedulingType === SchedulingType.COLLECTIVE &&
|
||||||
|
eventType.users.find((user) => user.id === user.id)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!(await authorized())) throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
|
||||||
|
|
||||||
|
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||||
|
if (isConfirmed) throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" });
|
||||||
|
|
||||||
|
/** When a booking that requires payment its being confirmed but doesn't have any payment,
|
||||||
|
* we shouldn’t save it on DestinationCalendars
|
||||||
|
*/
|
||||||
|
if (booking.payment.length > 0 && !booking.paid) {
|
||||||
|
await prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
id: bookingId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: "Booking confirmed" };
|
||||||
|
}
|
||||||
|
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
||||||
|
return {
|
||||||
|
name: attendee.name,
|
||||||
|
email: attendee.email,
|
||||||
|
timeZone: attendee.timeZone,
|
||||||
|
language: {
|
||||||
|
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
||||||
|
locale: attendee.locale ?? "en",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const attendeesList = await Promise.all(attendeesListPromises);
|
||||||
|
|
||||||
|
const evt: CalendarEvent = {
|
||||||
|
type: booking.title,
|
||||||
|
title: booking.title,
|
||||||
|
description: booking.description,
|
||||||
|
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
||||||
|
startTime: booking.startTime.toISOString(),
|
||||||
|
endTime: booking.endTime.toISOString(),
|
||||||
|
organizer: {
|
||||||
|
email: user.email,
|
||||||
|
name: user.name || "Unnamed",
|
||||||
|
timeZone: user.timeZone,
|
||||||
|
language: { translate: tOrganizer, locale: user.locale ?? "en" },
|
||||||
|
},
|
||||||
|
attendees: attendeesList,
|
||||||
|
location: booking.location ?? "",
|
||||||
|
uid: booking.uid,
|
||||||
|
destinationCalendar: booking?.destinationCalendar || user.destinationCalendar,
|
||||||
|
requiresConfirmation: booking?.eventType?.requiresConfirmation ?? false,
|
||||||
|
eventTypeId: booking.eventType?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
|
||||||
|
if (recurringEventId && recurringEvent) {
|
||||||
|
const groupedRecurringBookings = await prisma.booking.groupBy({
|
||||||
|
where: {
|
||||||
|
recurringEventId: booking.recurringEventId,
|
||||||
|
},
|
||||||
|
by: [Prisma.BookingScalarFieldEnum.recurringEventId],
|
||||||
|
_count: true,
|
||||||
|
});
|
||||||
|
// Overriding the recurring event configuration count to be the actual number of events booked for
|
||||||
|
// the recurring event (equal or less than recurring event configuration count)
|
||||||
|
recurringEvent.count = groupedRecurringBookings[0]._count;
|
||||||
|
// count changed, parsing again to get the new value in
|
||||||
|
evt.recurringEvent = parseRecurringEvent(recurringEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
const eventManager = new EventManager(user);
|
||||||
|
const scheduleResult = await eventManager.create(evt);
|
||||||
|
|
||||||
|
const results = scheduleResult.results;
|
||||||
|
|
||||||
|
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||||
|
const error = {
|
||||||
|
errorCode: "BookingCreatingMeetingFailed",
|
||||||
|
message: "Booking failed",
|
||||||
|
};
|
||||||
|
|
||||||
|
log.error(`Booking ${user.username} failed`, error, results);
|
||||||
|
} else {
|
||||||
|
const metadata: AdditionalInformation = {};
|
||||||
|
|
||||||
|
if (results.length) {
|
||||||
|
// TODO: Handle created event metadata more elegantly
|
||||||
|
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
|
||||||
|
metadata.conferenceData = results[0].createdEvent?.conferenceData;
|
||||||
|
metadata.entryPoints = results[0].createdEvent?.entryPoints;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await sendScheduledEmails({ ...evt, additionalInformation: metadata });
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let updatedBookings: {
|
||||||
|
scheduledJobs: string[];
|
||||||
|
id: number;
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
uid: string;
|
||||||
|
smsReminderNumber: string | null;
|
||||||
|
eventType: {
|
||||||
|
workflows: (WorkflowsOnEventTypes & {
|
||||||
|
workflow: Workflow & {
|
||||||
|
steps: WorkflowStep[];
|
||||||
|
};
|
||||||
|
})[];
|
||||||
|
} | null;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
if (recurringEventId) {
|
||||||
|
// The booking to confirm is a recurring event and comes from /booking/recurring, proceeding to mark all related
|
||||||
|
// bookings as confirmed. Prisma updateMany does not support relations, so doing this in two steps for now.
|
||||||
|
const unconfirmedRecurringBookings = await prisma.booking.findMany({
|
||||||
|
where: {
|
||||||
|
recurringEventId,
|
||||||
|
status: BookingStatus.PENDING,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateBookingsPromise = unconfirmedRecurringBookings.map((recurringBooking) => {
|
||||||
|
return prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
id: recurringBooking.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
references: {
|
||||||
|
create: scheduleResult.referencesToCreate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
workflows: {
|
||||||
|
include: {
|
||||||
|
workflow: {
|
||||||
|
include: {
|
||||||
|
steps: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uid: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
smsReminderNumber: true,
|
||||||
|
id: true,
|
||||||
|
scheduledJobs: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const updatedBookingsResult = await Promise.all(updateBookingsPromise);
|
||||||
|
updatedBookings = updatedBookings.concat(updatedBookingsResult);
|
||||||
|
} else {
|
||||||
|
// @NOTE: be careful with this as if any error occurs before this booking doesn't get confirmed
|
||||||
|
// Should perform update on booking (confirm) -> then trigger the rest handlers
|
||||||
|
const updatedBooking = await prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
id: bookingId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: BookingStatus.ACCEPTED,
|
||||||
|
references: {
|
||||||
|
create: scheduleResult.referencesToCreate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
workflows: {
|
||||||
|
include: {
|
||||||
|
workflow: {
|
||||||
|
include: {
|
||||||
|
steps: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uid: true,
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
smsReminderNumber: true,
|
||||||
|
id: true,
|
||||||
|
scheduledJobs: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
updatedBookings.push(updatedBooking);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Workflows - set reminders for confirmed events
|
||||||
|
for (const updatedBooking of updatedBookings) {
|
||||||
|
if (updatedBooking.eventType?.workflows) {
|
||||||
|
const evtOfBooking = evt;
|
||||||
|
evtOfBooking.startTime = updatedBooking.startTime.toISOString();
|
||||||
|
evtOfBooking.endTime = updatedBooking.endTime.toISOString();
|
||||||
|
evtOfBooking.uid = updatedBooking.uid;
|
||||||
|
|
||||||
|
await scheduleWorkflowReminders(
|
||||||
|
updatedBooking.eventType.workflows,
|
||||||
|
updatedBooking.smsReminderNumber,
|
||||||
|
evtOfBooking,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// schedule job for zapier trigger 'when meeting ends'
|
||||||
|
const subscriberOptionsMeetingEnded = {
|
||||||
|
userId: booking.userId || 0,
|
||||||
|
eventTypeId: booking.eventTypeId || 0,
|
||||||
|
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
|
||||||
|
|
||||||
|
subscribersMeetingEnded.forEach((subscriber) => {
|
||||||
|
updatedBookings.forEach((booking) => {
|
||||||
|
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
evt.rejectionReason = rejectionReason;
|
||||||
|
if (recurringEventId) {
|
||||||
|
// The booking to reject is a recurring event and comes from /booking/upcoming, proceeding to mark all related
|
||||||
|
// bookings as rejected.
|
||||||
|
await prisma.booking.updateMany({
|
||||||
|
where: {
|
||||||
|
recurringEventId,
|
||||||
|
status: BookingStatus.PENDING,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: BookingStatus.REJECTED,
|
||||||
|
rejectionReason,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await refund(booking, evt); // No payment integration for recurring events for v1
|
||||||
|
await prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
id: bookingId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: BookingStatus.REJECTED,
|
||||||
|
rejectionReason,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendDeclinedEmails(evt);
|
||||||
|
}
|
||||||
|
return { message: "Booking " + confirmed ? "confirmed" : "rejected" };
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user