Booking confirm endpoint refactoring (#2949)
* Adds new default handler and responder * Moved confirm endpoint * Fixes availability for unconfirmed bookings * Cleanup * Update _patch.ts * Prevent too much diffs * Adds missing BookingStatus * Migrates confirmed & rejected to status * Adds requiresConfirmation icon to listing * Adds booking status migration * Adds migrations to remove confirmed/rejected * Undo refactor * Sets the organizer as "accepted" in gCal * Update getBusyTimes.ts Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>pull/2981/head
parent
3b321e5d3c
commit
12d66cb9df
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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) {
|
|||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={"flex-1 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}
|
||||
onClick={onClick}>
|
||||
<td className={"flex-1 ltr:pl-4 rtl:pr-4" + (isRejected ? " line-through" : "")} onClick={onClick}>
|
||||
<div className="cursor-pointer py-4">
|
||||
<div className="sm:hidden">
|
||||
{!booking.confirmed && !booking.rejected && (
|
||||
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>
|
||||
)}
|
||||
{isPending && <Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">Pending payment</Tag>
|
||||
)}
|
||||
|
@ -351,9 +350,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
||||
)}
|
||||
{!booking.confirmed && !booking.rejected && (
|
||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag>
|
||||
)}
|
||||
{isPending && <Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag>}
|
||||
</div>
|
||||
{booking.description && (
|
||||
<div
|
||||
|
@ -382,13 +379,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
<td className="whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">
|
||||
{isUpcoming && !isCancelled ? (
|
||||
<>
|
||||
{!booking.confirmed && !booking.rejected && user!.id === booking.user!.id && (
|
||||
<TableActions actions={pendingActions} />
|
||||
)}
|
||||
{booking.confirmed && !booking.rejected && <TableActions actions={bookedActions} />}
|
||||
{!booking.confirmed && booking.rejected && (
|
||||
<div className="text-sm text-gray-500">{t("rejected")}</div>
|
||||
)}
|
||||
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
|
||||
{isConfirmed && <TableActions actions={bookedActions} />}
|
||||
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
|
||||
</>
|
||||
) : null}
|
||||
{isCancelled && booking.rescheduled && (
|
||||
|
|
|
@ -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<Prisma.EventTypeArgs>()({
|
||||
select: {
|
||||
id: true,
|
||||
length: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
description: true,
|
||||
},
|
||||
select: baseEventTypeSelect,
|
||||
});
|
||||
|
||||
type EventType = Prisma.EventTypeGetPayload<typeof eventTypeData>;
|
||||
|
@ -83,6 +82,12 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
|||
</IntlProvider>
|
||||
</li>
|
||||
)}
|
||||
{eventType.requiresConfirmation && (
|
||||
<li className="mr-4 flex items-center whitespace-nowrap">
|
||||
<ClipboardCheckIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
Opt-in
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { Prisma, UserPlan } from "@prisma/client";
|
||||
|
||||
import { baseEventTypeSelect } from "@calcom/prisma";
|
||||
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (...args: any) => Promise<infer R>
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import { Attendee, Booking } from "@prisma/client";
|
||||
|
||||
export type BookingConfirmBody = {
|
||||
confirmed: boolean;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type BookingCreateBody = {
|
||||
email: string;
|
||||
end: string;
|
||||
|
|
|
@ -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;
|
|
@ -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<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
|
||||
|
||||
/** Allows us to get type inference from API handler responses */
|
||||
function defaultResponder<T>(f: Handle<T>) {
|
||||
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;
|
|
@ -0,0 +1,2 @@
|
|||
export { default as defaultHandler } from "./defaultHandler";
|
||||
export { default as defaultResponder } from "./defaultResponder";
|
|
@ -0,0 +1 @@
|
|||
export * from "./api";
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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<User, "id">,
|
||||
booking: Pick<Booking, "eventTypeId" | "userId">
|
||||
|
@ -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<ReturnType<typeof patchHandler>>;
|
||||
|
||||
export default defaultHandler({
|
||||
// To prevent too much git diff until moved to another file
|
||||
PATCH: Promise.resolve({ default: defaultResponder(patchHandler) }),
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -200,7 +200,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
id: bookingToDelete.id,
|
||||
},
|
||||
data: {
|
||||
rejected: true,
|
||||
status: BookingStatus.REJECTED,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Prisma.BookingCreateInput> = {}
|
||||
{ rescheduled = false, paid = false, status = "ACCEPTED" }: Partial<Prisma.BookingCreateInput> = {}
|
||||
) => {
|
||||
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,
|
||||
|
|
|
@ -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<Prisma.EventTypeSelect>()({
|
||||
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: {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["modules/*"],
|
||||
"@components/*": ["components/*"],
|
||||
"@lib/*": ["lib/*"],
|
||||
"@server/*": ["server/*"],
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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;
|
|
@ -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";
|
|
@ -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[]
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export const baseEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
length: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
slug: true,
|
||||
hidden: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
requiresConfirmation: true,
|
||||
});
|
|
@ -1 +1,2 @@
|
|||
export * from "./booking";
|
||||
export * from "./event-types";
|
||||
|
|
Loading…
Reference in New Issue