diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 447e1ed7db..cae98d6257 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -7,6 +7,7 @@
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
"ban.spellright", // Spell check for docs
"stripe.vscode-stripe", // stripe VSCode extension
- "Prisma.prisma" // syntax|format|completion for prisma
+ "Prisma.prisma", // syntax|format|completion for prisma
+ "rebornix.project-snippets" // Share useful snippets between collaborators
]
}
diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx
index 575371d14d..2b237f9cf1 100644
--- a/apps/web/components/booking/BookingListItem.tsx
+++ b/apps/web/components/booking/BookingListItem.tsx
@@ -87,6 +87,9 @@ function BookingListItem(booking: BookingItemProps) {
);
const isUpcoming = new Date(booking.endTime) >= new Date();
const isCancelled = booking.status === BookingStatus.CANCELLED;
+ const isConfirmed = booking.status === BookingStatus.ACCEPTED;
+ const isRejected = booking.status === BookingStatus.REJECTED;
+ const isPending = booking.status === BookingStatus.PENDING;
const pendingActions: ActionType[] = [
{
@@ -205,7 +208,7 @@ function BookingListItem(booking: BookingItemProps) {
if (location.includes("integration")) {
if (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) {
location = t("web_conference");
- } else if (booking.confirmed) {
+ } else if (isConfirmed) {
location = linkValueToString(booking.location, t);
} else {
location = t("web_conferencing_details_to_follow");
@@ -227,7 +230,7 @@ function BookingListItem(booking: BookingItemProps) {
eventName: booking.eventType.eventName || "",
bookingId: booking.id,
recur: booking.recurringEventId,
- reschedule: booking.confirmed,
+ reschedule: isConfirmed,
listingStatus: booking.listingStatus,
status: booking.status,
},
@@ -322,14 +325,10 @@ function BookingListItem(booking: BookingItemProps) {
-
+ |
- {!booking.confirmed && !booking.rejected && (
- {t("unconfirmed")}
- )}
+ {isPending && {t("unconfirmed")}}
{!!booking?.eventType?.price && !booking.paid && (
Pending payment
)}
@@ -351,9 +350,7 @@ function BookingListItem(booking: BookingItemProps) {
{!!booking?.eventType?.price && !booking.paid && (
Pending payment
)}
- {!booking.confirmed && !booking.rejected && (
- {t("unconfirmed")}
- )}
+ {isPending && {t("unconfirmed")}}
{booking.description && (
{isUpcoming && !isCancelled ? (
<>
- {!booking.confirmed && !booking.rejected && user!.id === booking.user!.id && (
-
- )}
- {booking.confirmed && !booking.rejected && }
- {!booking.confirmed && booking.rejected && (
- {t("rejected")}
- )}
+ {isPending && user?.id === booking.user?.id && }
+ {isConfirmed && }
+ {isRejected && {t("rejected")} }
>
) : null}
{isCancelled && booking.rescheduled && (
diff --git a/apps/web/components/eventtype/EventTypeDescription.tsx b/apps/web/components/eventtype/EventTypeDescription.tsx
index 8f0e7aab66..612be28614 100644
--- a/apps/web/components/eventtype/EventTypeDescription.tsx
+++ b/apps/web/components/eventtype/EventTypeDescription.tsx
@@ -1,24 +1,23 @@
-import { ClockIcon, CreditCardIcon, RefreshIcon, UserIcon, UsersIcon } from "@heroicons/react/solid";
-import { SchedulingType } from "@prisma/client";
-import { Prisma } from "@prisma/client";
-import React, { useMemo } from "react";
+import {
+ ClipboardCheckIcon,
+ ClockIcon,
+ CreditCardIcon,
+ RefreshIcon,
+ UserIcon,
+ UsersIcon,
+} from "@heroicons/react/solid";
+import { Prisma, SchedulingType } from "@prisma/client";
+import { useMemo } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { RecurringEvent } from "@calcom/types/Calendar";
import classNames from "@lib/classNames";
const eventTypeData = Prisma.validator ()({
- select: {
- id: true,
- length: true,
- price: true,
- currency: true,
- schedulingType: true,
- recurringEvent: true,
- description: true,
- },
+ select: baseEventTypeSelect,
});
type EventType = Prisma.EventTypeGetPayload;
@@ -83,6 +82,12 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
)}
+ {eventType.requiresConfirmation && (
+
+
+ Opt-in
+
+ )}
>
diff --git a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts
index e0dc07017f..ff23521921 100644
--- a/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts
+++ b/apps/web/ee/pages/api/integrations/stripepayment/webhook.ts
@@ -1,4 +1,4 @@
-import { Prisma } from "@prisma/client";
+import { BookingStatus, Prisma } from "@prisma/client";
import { buffer } from "micro";
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
@@ -44,13 +44,13 @@ async function handlePaymentSuccess(event: Stripe.Event) {
},
select: {
...bookingMinimalSelect,
- confirmed: true,
location: true,
eventTypeId: true,
userId: true,
uid: true,
paid: true,
destinationCalendar: true,
+ status: true,
user: {
select: {
id: true,
@@ -125,10 +125,11 @@ async function handlePaymentSuccess(event: Stripe.Event) {
const bookingData: Prisma.BookingUpdateInput = {
paid: true,
- confirmed: true,
+ status: BookingStatus.ACCEPTED,
};
- if (booking.confirmed) {
+ const isConfirmed = booking.status === BookingStatus.ACCEPTED;
+ if (isConfirmed) {
const eventManager = new EventManager(user);
const scheduleResult = await eventManager.create(evt);
bookingData.references = { create: scheduleResult.referencesToCreate };
diff --git a/apps/web/lib/getBusyTimes.ts b/apps/web/lib/getBusyTimes.ts
index b6e81d07ce..8b14e6582c 100644
--- a/apps/web/lib/getBusyTimes.ts
+++ b/apps/web/lib/getBusyTimes.ts
@@ -1,4 +1,4 @@
-import { Credential, SelectedCalendar } from "@prisma/client";
+import { BookingStatus, Credential, SelectedCalendar } from "@prisma/client";
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
import { getBusyVideoTimes } from "@calcom/core/videoClient";
@@ -19,24 +19,13 @@ async function getBusyTimes(params: {
const busyTimes: EventBusyDate[] = await prisma.booking
.findMany({
where: {
- AND: [
- {
- userId,
- eventTypeId,
- startTime: { gte: new Date(startTime) },
- endTime: { lte: new Date(endTime) },
- },
- {
- OR: [
- {
- status: "ACCEPTED",
- },
- {
- status: "PENDING",
- },
- ],
- },
- ],
+ userId,
+ eventTypeId,
+ startTime: { gte: new Date(startTime) },
+ endTime: { lte: new Date(endTime) },
+ status: {
+ in: [BookingStatus.ACCEPTED],
+ },
},
select: {
startTime: true,
diff --git a/apps/web/lib/queries/teams/index.ts b/apps/web/lib/queries/teams/index.ts
index b25869d550..65bdab5ad8 100644
--- a/apps/web/lib/queries/teams/index.ts
+++ b/apps/web/lib/queries/teams/index.ts
@@ -1,5 +1,7 @@
import { Prisma, UserPlan } from "@prisma/client";
+import { baseEventTypeSelect } from "@calcom/prisma";
+
import prisma from "@lib/prisma";
type AsyncReturnType Promise> = T extends (...args: any) => Promise
@@ -37,18 +39,10 @@ export async function getTeamWithMembers(id?: number, slug?: string) {
hidden: false,
},
select: {
- id: true,
- title: true,
- description: true,
- length: true,
- slug: true,
- schedulingType: true,
- recurringEvent: true,
- price: true,
- currency: true,
users: {
select: userSelect,
},
+ ...baseEventTypeSelect,
},
},
});
diff --git a/apps/web/lib/types/booking.ts b/apps/web/lib/types/booking.ts
index 6f879767c3..81eb7fa3fb 100644
--- a/apps/web/lib/types/booking.ts
+++ b/apps/web/lib/types/booking.ts
@@ -1,10 +1,5 @@
import { Attendee, Booking } from "@prisma/client";
-export type BookingConfirmBody = {
- confirmed: boolean;
- id: number;
-};
-
export type BookingCreateBody = {
email: string;
end: string;
diff --git a/apps/web/modules/common/api/defaultHandler.ts b/apps/web/modules/common/api/defaultHandler.ts
new file mode 100644
index 0000000000..bd2b2c4a92
--- /dev/null
+++ b/apps/web/modules/common/api/defaultHandler.ts
@@ -0,0 +1,22 @@
+import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
+
+type Handlers = {
+ [method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: Promise<{ default: NextApiHandler }>;
+};
+
+/** Allows us to split big API handlers by method and auto catch unsupported methods */
+const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => {
+ const handler = (await handlers[req.method as keyof typeof handlers])?.default;
+
+ if (!handler) return res.status(405).json({ message: "Method not allowed" });
+
+ try {
+ await handler(req, res);
+ return;
+ } catch (error) {
+ console.error(error);
+ return res.status(500).json({ message: "Something went wrong" });
+ }
+};
+
+export default defaultHandler;
diff --git a/apps/web/modules/common/api/defaultResponder.ts b/apps/web/modules/common/api/defaultResponder.ts
new file mode 100644
index 0000000000..f230928d41
--- /dev/null
+++ b/apps/web/modules/common/api/defaultResponder.ts
@@ -0,0 +1,38 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import Stripe from "stripe";
+import { ZodError } from "zod";
+
+import { HttpError } from "@calcom/lib/http-error";
+
+type Handle = (req: NextApiRequest, res: NextApiResponse) => Promise;
+
+/** Allows us to get type inference from API handler responses */
+function defaultResponder(f: Handle) {
+ return async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const result = await f(req, res);
+ res.json(result);
+ } catch (err) {
+ if (err instanceof HttpError) {
+ res.statusCode = err.statusCode;
+ res.json({ message: err.message });
+ } else if (err instanceof Stripe.errors.StripeInvalidRequestError) {
+ console.error("err", err);
+ res.statusCode = err.statusCode || 500;
+ res.json({ message: "Stripe error: " + err.message });
+ } else if (err instanceof ZodError && err.name === "ZodError") {
+ console.error("err", JSON.parse(err.message)[0].message);
+ res.statusCode = 400;
+ res.json({
+ message: "Validation errors: " + err.issues.map((i) => `—'${i.path}' ${i.message}`).join(". "),
+ });
+ } else {
+ console.error("err", err);
+ res.statusCode = 500;
+ res.json({ message: "Unknown error" });
+ }
+ }
+ };
+}
+
+export default defaultResponder;
diff --git a/apps/web/modules/common/api/index.ts b/apps/web/modules/common/api/index.ts
new file mode 100644
index 0000000000..db67b1334c
--- /dev/null
+++ b/apps/web/modules/common/api/index.ts
@@ -0,0 +1,2 @@
+export { default as defaultHandler } from "./defaultHandler";
+export { default as defaultResponder } from "./defaultResponder";
diff --git a/apps/web/modules/common/index.ts b/apps/web/modules/common/index.ts
new file mode 100644
index 0000000000..d158c57640
--- /dev/null
+++ b/apps/web/modules/common/index.ts
@@ -0,0 +1 @@
+export * from "./api";
diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx
index 6b0f40e9fb..3572bbc1f5 100644
--- a/apps/web/pages/[user].tsx
+++ b/apps/web/pages/[user].tsx
@@ -23,6 +23,7 @@ import defaultEvents, {
getUsernameSlugLink,
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import useTheme from "@lib/hooks/useTheme";
@@ -274,17 +275,8 @@ const getEventTypesWithHiddenFromDB = async (userId: number, plan: UserPlan) =>
},
],
select: {
- id: true,
- slug: true,
- title: true,
- length: true,
- description: true,
- hidden: true,
- schedulingType: true,
- recurringEvent: true,
- price: true,
- currency: true,
metadata: true,
+ ...baseEventTypeSelect,
},
take: plan === UserPlan.FREE ? 1 : undefined,
});
diff --git a/apps/web/pages/api/book/confirm.ts b/apps/web/pages/api/book/confirm.ts
index cb7b5b5c49..06c5edf34a 100644
--- a/apps/web/pages/api/book/confirm.ts
+++ b/apps/web/pages/api/book/confirm.ts
@@ -1,20 +1,22 @@
import { Booking, BookingStatus, Prisma, SchedulingType, User } from "@prisma/client";
-import type { NextApiRequest, NextApiResponse } from "next";
+import type { NextApiRequest } from "next";
+import { z } from "zod";
import EventManager from "@calcom/core/EventManager";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import logger from "@calcom/lib/logger";
+import prisma from "@calcom/prisma";
import type { AdditionInformation, CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
-import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
+import { HttpError } from "@lib/core/http/error";
import { sendDeclinedEmails, sendScheduledEmails } from "@lib/emails/email-manager";
-import prisma from "@lib/prisma";
-import { BookingConfirmBody } from "@lib/types/booking";
import { getTranslation } from "@server/lib/i18n";
+import { defaultHandler, defaultResponder } from "~/common";
+
const authorized = async (
currentUser: Pick,
booking: Pick
@@ -43,20 +45,30 @@ const authorized = async (
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
-export default async function handler(req: NextApiRequest, res: NextApiResponse) {
- const session = await getSession({ req: req });
+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) {
- return res.status(401).json({ message: "Not authenticated" });
+ throw new HttpError({ statusCode: 401, message: "Not authenticated" });
}
- const reqBody = req.body as BookingConfirmBody;
- const bookingId = reqBody.id;
-
- if (!bookingId) {
- return res.status(400).json({ message: "bookingId missing" });
- }
+ 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,
},
@@ -74,230 +86,228 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
- if (!currentUser) {
- return res.status(404).json({ message: "User not found" });
- }
-
const tOrganizer = await getTranslation(currentUser.locale ?? "en", "common");
- if (req.method === "PATCH") {
- const booking = await prisma.booking.findFirst({
+ 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: {
+ recurringEvent: true,
+ },
+ },
+ location: true,
+ userId: true,
+ id: true,
+ uid: true,
+ payment: true,
+ destinationCalendar: true,
+ paid: true,
+ recurringEventId: true,
+ status: 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,
},
- select: {
- title: true,
- description: true,
- customInputs: true,
- startTime: true,
- endTime: true,
- confirmed: true,
- attendees: true,
- eventTypeId: true,
- eventType: {
- select: {
- recurringEvent: true,
- },
- },
- location: true,
- userId: true,
- id: true,
- uid: true,
- payment: true,
- destinationCalendar: true,
- paid: true,
- recurringEventId: true,
+ data: {
+ status: BookingStatus.ACCEPTED,
},
});
- if (!booking) {
- return res.status(404).json({ message: "booking not found" });
- }
+ req.statusCode = 204;
+ return { message: "Booking confirmed" };
+ }
- if (!(await authorized(currentUser, booking))) {
- return res.status(401).end();
- }
-
- if (booking.confirmed) {
- return res.status(400).json({ 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: {
- confirmed: true,
- },
- });
-
- return res.status(204).end();
- }
-
- 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" },
+ 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",
},
- attendees: attendeesList,
- location: booking.location ?? "",
- uid: booking.uid,
- destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar,
};
+ });
- const recurringEvent = booking.eventType?.recurringEvent as RecurringEvent;
+ const attendeesList = await Promise.all(attendeesListPromises);
- if (req.body.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;
+ 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,
+ };
+
+ const recurringEvent = booking.eventType?.recurringEvent as 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;
+ }
+
+ 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: AdditionInformation = {};
+
+ 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, additionInformation: metadata },
+ recurringEventId ? recurringEvent : {} // Send email with recurring event info only on recurring event context
+ );
+ } catch (error) {
+ log.error(error);
+ }
}
- if (reqBody.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: AdditionInformation = {};
-
- 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, additionInformation: metadata },
- req.body.recurringEventId ? recurringEvent : {} // Send email with recurring event info only on recurring event context
- );
- } catch (error) {
- log.error(error);
- }
- }
-
- if (req.body.recurringEventId) {
- // The booking to confirm is a recurring event and comes from /booking/upcoming, 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: req.body.recurringEventId,
- confirmed: false,
- },
- });
- unconfirmedRecurringBookings.map(async (recurringBooking) => {
- await prisma.booking.update({
- where: {
- id: recurringBooking.id,
- },
- data: {
- confirmed: true,
- references: {
- create: scheduleResult.referencesToCreate,
- },
- },
- });
- });
- } 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
+ if (recurringEventId) {
+ // The booking to confirm is a recurring event and comes from /booking/upcoming, 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,
+ },
+ });
+ unconfirmedRecurringBookings.map(async (recurringBooking) => {
await prisma.booking.update({
where: {
- id: bookingId,
+ id: recurringBooking.id,
},
data: {
- confirmed: true,
+ status: BookingStatus.ACCEPTED,
references: {
create: scheduleResult.referencesToCreate,
},
},
});
- }
-
- res.status(204).end();
+ });
} else {
- const rejectionReason = asStringOrNull(req.body.reason) || "";
- evt.rejectionReason = rejectionReason;
- if (req.body.recurringEventId) {
- // The booking to reject is a recurring event and comes from /booking/upcoming, proceeding to mark all related
- // bookings as rejected. Prisma updateMany does not support relations, so doing this in two steps for now.
- const unconfirmedRecurringBookings = await prisma.booking.findMany({
- where: {
- recurringEventId: req.body.recurringEventId,
- confirmed: false,
+ // @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
+ await prisma.booking.update({
+ where: {
+ id: bookingId,
+ },
+ data: {
+ status: BookingStatus.ACCEPTED,
+ references: {
+ create: scheduleResult.referencesToCreate,
},
- });
- unconfirmedRecurringBookings.map(async (recurringBooking) => {
- await prisma.booking.update({
- where: {
- id: recurringBooking.id,
- },
- data: {
- rejected: true,
- status: BookingStatus.REJECTED,
- rejectionReason: rejectionReason,
- },
- });
- });
- } else {
- await refund(booking, evt); // No payment integration for recurring events for v1
+ },
+ });
+ }
+ } 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. 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,
+ },
+ });
+ unconfirmedRecurringBookings.map(async (recurringBooking) => {
await prisma.booking.update({
where: {
- id: bookingId,
+ id: recurringBooking.id,
},
data: {
- rejected: true,
status: BookingStatus.REJECTED,
- rejectionReason: rejectionReason,
+ rejectionReason,
},
});
- }
-
- await sendDeclinedEmails(evt, req.body.recurringEventId ? recurringEvent : {}); // Send email with recurring event info only on recurring event context
-
- res.status(204).end();
+ });
+ } 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, recurringEventId ? recurringEvent : {}); // Send email with recurring event info only on recurring event context
}
+
+ req.statusCode = 204;
+ return { message: "Booking " + confirmed ? "confirmed" : "rejected" };
}
+
+export type BookConfirmPatchResponse = Awaited>;
+
+export default defaultHandler({
+ // To prevent too much git diff until moved to another file
+ PATCH: Promise.resolve({ default: defaultResponder(patchHandler) }),
+});
diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts
index 93ae0cc319..9e5c461073 100644
--- a/apps/web/pages/api/book/event.ts
+++ b/apps/web/pages/api/book/event.ts
@@ -478,6 +478,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null;
const dynamicGroupSlugRef = !eventTypeId ? (reqBody.user as string).toLowerCase() : null;
+ const isConfirmedByDefault = (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid;
const newBookingData: Prisma.BookingCreateInput = {
uid,
title: evt.title,
@@ -485,7 +486,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
endTime: dayjs(evt.endTime).toDate(),
description: evt.additionalNotes,
customInputs: isPrismaObjOrUndefined(evt.customInputs),
- confirmed: (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid,
+ status: isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING,
location: evt.location,
eventType: eventTypeRel,
attendees: {
diff --git a/apps/web/pages/api/cancel.ts b/apps/web/pages/api/cancel.ts
index 302eb99ceb..c37cf73a23 100644
--- a/apps/web/pages/api/cancel.ts
+++ b/apps/web/pages/api/cancel.ts
@@ -200,7 +200,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: bookingToDelete.id,
},
data: {
- rejected: true,
+ status: BookingStatus.REJECTED,
},
});
diff --git a/apps/web/pages/api/cron/bookingReminder.ts b/apps/web/pages/api/cron/bookingReminder.ts
index cec5ac1e7e..9887b5d262 100644
--- a/apps/web/pages/api/cron/bookingReminder.ts
+++ b/apps/web/pages/api/cron/bookingReminder.ts
@@ -1,4 +1,4 @@
-import { ReminderType } from "@prisma/client";
+import { BookingStatus, ReminderType } from "@prisma/client";
import dayjs from "dayjs";
import type { NextApiRequest, NextApiResponse } from "next";
@@ -26,8 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
for (const interval of reminderIntervalMinutes) {
const bookings = await prisma.booking.findMany({
where: {
- confirmed: false,
- rejected: false,
+ status: BookingStatus.PENDING,
createdAt: {
lte: dayjs().add(-interval, "minutes").toDate(),
},
diff --git a/apps/web/pages/api/integrations.ts b/apps/web/pages/api/integrations.ts
index 0aa8e4090a..aa924b0913 100644
--- a/apps/web/pages/api/integrations.ts
+++ b/apps/web/pages/api/integrations.ts
@@ -1,4 +1,4 @@
-import { Prisma } from "@prisma/client";
+import { BookingStatus, Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
@@ -105,7 +105,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
},
data: {
- status: "CANCELLED",
+ status: BookingStatus.CANCELLED,
rejectionReason: "Payment provider got removed",
},
});
@@ -113,8 +113,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const bookingReferences = await prisma.booking
.findMany({
where: {
- confirmed: true,
- rejected: false,
+ status: BookingStatus.ACCEPTED,
},
select: {
id: true,
diff --git a/apps/web/playwright/fixtures/bookings.ts b/apps/web/playwright/fixtures/bookings.ts
index 86d69bdd17..72045a7b64 100644
--- a/apps/web/playwright/fixtures/bookings.ts
+++ b/apps/web/playwright/fixtures/bookings.ts
@@ -21,12 +21,7 @@ export const createBookingsFixture = (page: Page) => {
userId: number,
username: string | null,
eventTypeId = -1,
- {
- confirmed = true,
- rescheduled = false,
- paid = false,
- status = "ACCEPTED",
- }: Partial = {}
+ { rescheduled = false, paid = false, status = "ACCEPTED" }: Partial = {}
) => {
const startDate = dayjs().add(1, "day").toDate();
const seed = `${username}:${dayjs(startDate).utc().format()}:${new Date().getTime()}`;
@@ -54,7 +49,6 @@ export const createBookingsFixture = (page: Page) => {
id: eventTypeId,
},
},
- confirmed,
rescheduled,
paid,
status,
diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx
index 01b96d610c..50ca54b1a6 100644
--- a/apps/web/server/routers/viewer.tsx
+++ b/apps/web/server/routers/viewer.tsx
@@ -7,7 +7,7 @@ import { z } from "zod";
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { checkPremiumUsername } from "@calcom/ee/lib/core/checkPremiumUsername";
-import { bookingMinimalSelect } from "@calcom/prisma";
+import { baseEventTypeSelect, bookingMinimalSelect } from "@calcom/prisma";
import { RecurringEvent } from "@calcom/types/Calendar";
import { checkRegularUsername } from "@lib/core/checkRegularUsername";
@@ -131,16 +131,6 @@ const loggedInViewerRouter = createProtectedRouter()
async resolve({ ctx }) {
const { prisma } = ctx;
const eventTypeSelect = Prisma.validator()({
- id: true,
- title: true,
- description: true,
- length: true,
- schedulingType: true,
- recurringEvent: true,
- slug: true,
- hidden: true,
- price: true,
- currency: true,
position: true,
successRedirectUrl: true,
hashedLink: true,
@@ -151,6 +141,7 @@ const loggedInViewerRouter = createProtectedRouter()
name: true,
},
},
+ ...baseEventTypeSelect,
});
const user = await prisma.user.findUnique({
@@ -328,7 +319,7 @@ const loggedInViewerRouter = createProtectedRouter()
// handled separately for each occurrence
OR: [
{
- AND: [{ NOT: { recurringEventId: { equals: null } } }, { confirmed: false }],
+ AND: [{ NOT: { recurringEventId: { equals: null } } }, { status: BookingStatus.PENDING }],
},
{
AND: [
@@ -398,8 +389,6 @@ const loggedInViewerRouter = createProtectedRouter()
select: {
...bookingMinimalSelect,
uid: true,
- confirmed: true,
- rejected: true,
recurringEventId: true,
location: true,
eventType: {
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 22b05a3665..7d0598edff 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -3,6 +3,7 @@
"compilerOptions": {
"baseUrl": ".",
"paths": {
+ "~/*": ["modules/*"],
"@components/*": ["components/*"],
"@lib/*": ["lib/*"],
"@server/*": ["server/*"],
diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts
index 3edd911471..5df04987b9 100644
--- a/packages/app-store/googlecalendar/lib/CalendarService.ts
+++ b/packages/app-store/googlecalendar/lib/CalendarService.ts
@@ -14,7 +14,6 @@ import type {
IntegrationCalendar,
NewCalendarEventType,
} from "@calcom/types/Calendar";
-import type { PartialReference } from "@calcom/types/EventManager";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
@@ -103,11 +102,8 @@ export default class GoogleCalendarService implements Calendar {
timeZone: calEventRaw.organizer.timeZone,
},
attendees: [
- { ...calEventRaw.organizer, organizer: true },
- ...calEventRaw.attendees.map((attendee) => ({
- ...attendee,
- responseStatus: "accepted",
- })),
+ { ...calEventRaw.organizer, organizer: true, responseStatus: "accepted" },
+ ...calEventRaw.attendees.map((attendee) => ({ ...attendee, responseStatus: "accepted" })),
],
reminders: {
useDefault: true,
@@ -185,7 +181,7 @@ export default class GoogleCalendarService implements Calendar {
dateTime: event.endTime,
timeZone: event.organizer.timeZone,
},
- attendees: event.attendees,
+ attendees: [{ ...event.organizer, organizer: true, responseStatus: "accepted" }, ...event.attendees],
reminders: {
useDefault: true,
},
diff --git a/packages/prisma/migrations/20220604144700_fixes_booking_status/migration.sql b/packages/prisma/migrations/20220604144700_fixes_booking_status/migration.sql
new file mode 100644
index 0000000000..022876e945
--- /dev/null
+++ b/packages/prisma/migrations/20220604144700_fixes_booking_status/migration.sql
@@ -0,0 +1,11 @@
+-- Set BookingStatus.PENDING
+UPDATE "Booking" SET "status" = 'pending' WHERE "confirmed" = false AND "rejected" = false AND "rescheduled" IS NOT true;
+
+-- Set BookingStatus.REJECTED
+UPDATE "Booking" SET "status" = 'rejected' WHERE "confirmed" = false AND "rejected" = true AND "rescheduled" IS NOT true;
+
+-- Set BookingStatus.CANCELLED
+UPDATE "Booking" SET "status" = 'cancelled' WHERE "confirmed" = false AND "rejected" = false AND "rescheduled" IS true;
+
+-- Set BookingStatus.ACCEPTED
+UPDATE "Booking" SET "status" = 'accepted' WHERE "confirmed" = true AND "rejected" = false AND "rescheduled" IS NOT true;
diff --git a/packages/prisma/migrations/20220604210102_removes_booking_confirmed_rejected/migration.sql b/packages/prisma/migrations/20220604210102_removes_booking_confirmed_rejected/migration.sql
new file mode 100644
index 0000000000..54fa28fab2
--- /dev/null
+++ b/packages/prisma/migrations/20220604210102_removes_booking_confirmed_rejected/migration.sql
@@ -0,0 +1,10 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `confirmed` on the `Booking` table. All the data in the column will be lost.
+ - You are about to drop the column `rejected` on the `Booking` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "Booking" DROP COLUMN "confirmed",
+DROP COLUMN "rejected";
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index ff6b059480..d082e4e938 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -273,8 +273,6 @@ model Booking {
dailyRef DailyEventReference?
createdAt DateTime @default(now())
updatedAt DateTime?
- confirmed Boolean @default(true)
- rejected Boolean @default(false)
status BookingStatus @default(ACCEPTED)
paid Boolean @default(false)
payment Payment[]
diff --git a/packages/prisma/seed.ts b/packages/prisma/seed.ts
index 1f8003bf13..c4b0f08c5e 100644
--- a/packages/prisma/seed.ts
+++ b/packages/prisma/seed.ts
@@ -1,4 +1,4 @@
-import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
+import { BookingStatus, MembershipRole, Prisma, UserPlan } from "@prisma/client";
import dayjs from "dayjs";
import { uuid } from "short-uuid";
@@ -110,7 +110,7 @@ async function createUserAndEventType(opts: {
id,
},
},
- confirmed: bookingInput.confirmed,
+ status: bookingInput.status,
},
});
console.log(
@@ -238,7 +238,7 @@ async function main() {
title: "30min",
startTime: dayjs().add(2, "day").toDate(),
endTime: dayjs().add(2, "day").add(30, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
],
},
@@ -289,7 +289,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").toDate(),
endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
{
uid: uuid(),
@@ -297,7 +297,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(1, "week").toDate(),
endTime: dayjs().add(1, "day").add(1, "week").add(30, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
{
uid: uuid(),
@@ -305,7 +305,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(2, "week").toDate(),
endTime: dayjs().add(1, "day").add(2, "week").add(30, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
{
uid: uuid(),
@@ -313,7 +313,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(3, "week").toDate(),
endTime: dayjs().add(1, "day").add(3, "week").add(30, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
{
uid: uuid(),
@@ -321,7 +321,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(4, "week").toDate(),
endTime: dayjs().add(1, "day").add(4, "week").add(30, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
{
uid: uuid(),
@@ -329,7 +329,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(5, "week").toDate(),
endTime: dayjs().add(1, "day").add(5, "week").add(30, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
],
},
@@ -346,7 +346,7 @@ async function main() {
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").toDate(),
endTime: dayjs().add(2, "day").add(60, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
{
uid: uuid(),
@@ -354,7 +354,7 @@ async function main() {
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(2, "week").toDate(),
endTime: dayjs().add(2, "day").add(2, "week").add(60, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
{
uid: uuid(),
@@ -362,7 +362,7 @@ async function main() {
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(4, "week").toDate(),
endTime: dayjs().add(2, "day").add(4, "week").add(60, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
{
uid: uuid(),
@@ -370,7 +370,7 @@ async function main() {
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(8, "week").toDate(),
endTime: dayjs().add(2, "day").add(8, "week").add(60, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
{
uid: uuid(),
@@ -378,7 +378,7 @@ async function main() {
recurringEventId: Buffer.from("tennis-class").toString("base64"),
startTime: dayjs().add(2, "day").add(10, "week").toDate(),
endTime: dayjs().add(2, "day").add(10, "week").add(60, "minutes").toDate(),
- confirmed: false,
+ status: BookingStatus.PENDING,
},
],
},
diff --git a/packages/prisma/selects/event-types.ts b/packages/prisma/selects/event-types.ts
new file mode 100644
index 0000000000..59e4d3199d
--- /dev/null
+++ b/packages/prisma/selects/event-types.ts
@@ -0,0 +1,15 @@
+import { Prisma } from "@prisma/client";
+
+export const baseEventTypeSelect = Prisma.validator()({
+ id: true,
+ title: true,
+ description: true,
+ length: true,
+ schedulingType: true,
+ recurringEvent: true,
+ slug: true,
+ hidden: true,
+ price: true,
+ currency: true,
+ requiresConfirmation: true,
+});
diff --git a/packages/prisma/selects/index.ts b/packages/prisma/selects/index.ts
index b032fb844e..b59bed2e77 100644
--- a/packages/prisma/selects/index.ts
+++ b/packages/prisma/selects/index.ts
@@ -1 +1,2 @@
export * from "./booking";
+export * from "./event-types";
|