* feat: hide orgainzer email * fix type error * update * update * Update schema.prisma
661 lines
16 KiB
TypeScript
661 lines
16 KiB
TypeScript
import { Prisma } from "@prisma/client";
|
|
|
|
import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
|
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
|
import type { Booking } from "@calcom/prisma/client";
|
|
import { BookingStatus } from "@calcom/prisma/enums";
|
|
|
|
import { UserRepository } from "./user";
|
|
|
|
type TeamBookingsParamsBase = {
|
|
user: { id: number; email: string };
|
|
teamId: number;
|
|
startDate: Date;
|
|
endDate: Date;
|
|
excludedUid?: string | null;
|
|
includeManagedEvents: boolean;
|
|
shouldReturnCount?: boolean;
|
|
};
|
|
|
|
type TeamBookingsParamsWithCount = TeamBookingsParamsBase & {
|
|
shouldReturnCount: true;
|
|
};
|
|
|
|
type TeamBookingsParamsWithoutCount = TeamBookingsParamsBase;
|
|
|
|
const buildWhereClauseForActiveBookings = ({
|
|
eventTypeId,
|
|
startDate,
|
|
endDate,
|
|
users,
|
|
virtualQueuesData,
|
|
}: {
|
|
eventTypeId: number;
|
|
startDate?: Date;
|
|
endDate?: Date;
|
|
users: { id: number; email: string }[];
|
|
virtualQueuesData: {
|
|
chosenRouteId: string;
|
|
fieldOptionData: {
|
|
fieldId: string;
|
|
selectedOptionIds: string | number | string[];
|
|
};
|
|
} | null;
|
|
}): Prisma.BookingWhereInput => ({
|
|
OR: [
|
|
{
|
|
userId: {
|
|
in: users.map((user) => user.id),
|
|
},
|
|
OR: [
|
|
{
|
|
noShowHost: false,
|
|
},
|
|
{
|
|
noShowHost: null,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
attendees: {
|
|
some: {
|
|
email: {
|
|
in: users.map((user) => user.email),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
attendees: { some: { noShow: false } },
|
|
status: BookingStatus.ACCEPTED,
|
|
eventTypeId,
|
|
...(startDate || endDate
|
|
? {
|
|
createdAt: {
|
|
...(startDate ? { gte: startDate } : {}),
|
|
...(endDate ? { lte: endDate } : {}),
|
|
},
|
|
}
|
|
: {}),
|
|
...(virtualQueuesData
|
|
? {
|
|
routedFromRoutingFormReponse: {
|
|
chosenRouteId: virtualQueuesData.chosenRouteId,
|
|
},
|
|
}
|
|
: {}),
|
|
});
|
|
|
|
export class BookingRepository {
|
|
static async getBookingAttendees(bookingId: number) {
|
|
return await prisma.attendee.findMany({
|
|
where: {
|
|
bookingId,
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Determines if the user is the organizer, team admin, or org admin that the booking was created under */
|
|
static async doesUserIdHaveAccessToBooking({ userId, bookingId }: { userId: number; bookingId: number }) {
|
|
const booking = await prisma.booking.findFirst({
|
|
where: {
|
|
id: bookingId,
|
|
},
|
|
select: {
|
|
userId: true,
|
|
eventType: {
|
|
select: {
|
|
teamId: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!booking) return false;
|
|
|
|
if (userId === booking.userId) return true;
|
|
|
|
// If the booking doesn't belong to the user and there's no team then return early
|
|
if (!booking.eventType || !booking.eventType.teamId) return false;
|
|
|
|
// TODO add checks for team and org
|
|
const isAdminOrUser = await UserRepository.isAdminOfTeamOrParentOrg({
|
|
userId,
|
|
teamId: booking.eventType.teamId,
|
|
});
|
|
|
|
return isAdminOrUser;
|
|
}
|
|
|
|
static async findFirstBookingByReschedule({ originalBookingUid }: { originalBookingUid: string }) {
|
|
return await prisma.booking.findFirst({
|
|
where: {
|
|
fromReschedule: originalBookingUid,
|
|
},
|
|
select: {
|
|
uid: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
static async findReschedulerByUid({ uid }: { uid: string }) {
|
|
return await prisma.booking.findFirst({
|
|
where: {
|
|
uid,
|
|
},
|
|
select: {
|
|
rescheduledBy: true,
|
|
uid: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
static async groupByActiveBookingCounts({
|
|
users,
|
|
eventTypeId,
|
|
startDate,
|
|
}: {
|
|
users: { id: number; email: string }[];
|
|
eventTypeId: number;
|
|
startDate?: Date;
|
|
}) {
|
|
return await prisma.booking.groupBy({
|
|
by: ["userId"],
|
|
where: buildWhereClauseForActiveBookings({
|
|
users,
|
|
eventTypeId,
|
|
startDate,
|
|
virtualQueuesData: null,
|
|
}),
|
|
_count: {
|
|
_all: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
static async findAllExistingBookingsForEventTypeBetween({
|
|
eventTypeId,
|
|
seatedEvent = false,
|
|
startDate,
|
|
endDate,
|
|
userIdAndEmailMap,
|
|
}: {
|
|
startDate: Date;
|
|
endDate: Date;
|
|
eventTypeId?: number | null;
|
|
seatedEvent?: boolean;
|
|
userIdAndEmailMap: Map<number, string>;
|
|
}) {
|
|
const sharedQuery = {
|
|
startTime: { lte: endDate },
|
|
endTime: { gte: startDate },
|
|
status: {
|
|
in: [BookingStatus.ACCEPTED],
|
|
},
|
|
};
|
|
|
|
const bookingsSelect = Prisma.validator<Prisma.BookingSelect>()({
|
|
id: true,
|
|
uid: true,
|
|
userId: true,
|
|
startTime: true,
|
|
endTime: true,
|
|
title: true,
|
|
attendees: true,
|
|
eventType: {
|
|
select: {
|
|
id: true,
|
|
onlyShowFirstAvailableSlot: true,
|
|
afterEventBuffer: true,
|
|
beforeEventBuffer: true,
|
|
seatsPerTimeSlot: true,
|
|
requiresConfirmationWillBlockSlot: true,
|
|
requiresConfirmation: true,
|
|
allowReschedulingPastBookings: true,
|
|
hideOrganizerEmail: true,
|
|
},
|
|
},
|
|
...(seatedEvent && {
|
|
_count: {
|
|
select: {
|
|
seatsReferences: true,
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
|
|
const currentBookingsAllUsersQueryOne = prisma.booking.findMany({
|
|
where: {
|
|
...sharedQuery,
|
|
userId: {
|
|
in: Array.from(userIdAndEmailMap.keys()),
|
|
},
|
|
},
|
|
select: bookingsSelect,
|
|
});
|
|
|
|
const currentBookingsAllUsersQueryTwo = prisma.booking.findMany({
|
|
where: {
|
|
...sharedQuery,
|
|
attendees: {
|
|
some: {
|
|
email: {
|
|
in: Array.from(userIdAndEmailMap.values()),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
select: bookingsSelect,
|
|
});
|
|
|
|
const currentBookingsAllUsersQueryThree = eventTypeId
|
|
? prisma.booking.findMany({
|
|
where: {
|
|
startTime: { lte: endDate },
|
|
endTime: { gte: startDate },
|
|
eventType: {
|
|
id: eventTypeId,
|
|
requiresConfirmation: true,
|
|
requiresConfirmationWillBlockSlot: true,
|
|
},
|
|
status: {
|
|
in: [BookingStatus.PENDING],
|
|
},
|
|
},
|
|
select: bookingsSelect,
|
|
})
|
|
: [];
|
|
|
|
const [resultOne, resultTwo, resultThree] = await Promise.all([
|
|
currentBookingsAllUsersQueryOne,
|
|
currentBookingsAllUsersQueryTwo,
|
|
currentBookingsAllUsersQueryThree,
|
|
]);
|
|
// Prevent duplicate booking records when the organizer books his own event type.
|
|
//
|
|
// *Three is about PENDING, so will never overlap
|
|
// with the other two, which are about ACCEPTED. But ACCEPTED affects *One & *Two
|
|
// and WILL result in a duplicate booking if the organizer books himself.
|
|
const resultTwoWithOrganizersRemoved = resultTwo.filter((booking) => {
|
|
if (!booking.userId) return true;
|
|
const organizerEmail = userIdAndEmailMap.get(booking.userId);
|
|
return !booking.attendees.some((attendee) => attendee.email === organizerEmail);
|
|
});
|
|
|
|
return [...resultOne, ...resultTwoWithOrganizersRemoved, ...resultThree];
|
|
}
|
|
|
|
static async getAllBookingsForRoundRobin({
|
|
users,
|
|
eventTypeId,
|
|
startDate,
|
|
endDate,
|
|
virtualQueuesData,
|
|
}: {
|
|
users: { id: number; email: string }[];
|
|
eventTypeId: number;
|
|
startDate?: Date;
|
|
endDate?: Date;
|
|
virtualQueuesData: {
|
|
chosenRouteId: string;
|
|
fieldOptionData: {
|
|
fieldId: string;
|
|
selectedOptionIds: string | number | string[];
|
|
};
|
|
} | null;
|
|
}) {
|
|
const allBookings = await prisma.booking.findMany({
|
|
where: buildWhereClauseForActiveBookings({
|
|
eventTypeId,
|
|
startDate,
|
|
endDate,
|
|
users,
|
|
virtualQueuesData,
|
|
}),
|
|
select: {
|
|
id: true,
|
|
attendees: true,
|
|
userId: true,
|
|
createdAt: true,
|
|
status: true,
|
|
startTime: true,
|
|
routedFromRoutingFormReponse: true,
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
});
|
|
|
|
let queueBookings = allBookings;
|
|
|
|
if (virtualQueuesData) {
|
|
queueBookings = allBookings.filter((booking) => {
|
|
const responses = booking.routedFromRoutingFormReponse;
|
|
const fieldId = virtualQueuesData.fieldOptionData.fieldId;
|
|
const selectedOptionIds = virtualQueuesData.fieldOptionData.selectedOptionIds;
|
|
|
|
const response = responses?.response as FormResponse;
|
|
|
|
const responseValue = response[fieldId].value;
|
|
|
|
if (Array.isArray(responseValue) && Array.isArray(selectedOptionIds)) {
|
|
//check if all values are the same (this only support 'all in' not 'any in')
|
|
return (
|
|
responseValue.length === selectedOptionIds.length &&
|
|
responseValue.every((value, index) => value === selectedOptionIds[index])
|
|
);
|
|
} else {
|
|
return responseValue === selectedOptionIds;
|
|
}
|
|
});
|
|
}
|
|
return queueBookings;
|
|
}
|
|
|
|
static async findBookingByUid({ bookingUid }: { bookingUid: string }) {
|
|
return await prisma.booking.findUnique({
|
|
where: {
|
|
uid: bookingUid,
|
|
},
|
|
select: bookingMinimalSelect,
|
|
});
|
|
}
|
|
|
|
static async findBookingForMeetingPage({ bookingUid }: { bookingUid: string }) {
|
|
return await prisma.booking.findUnique({
|
|
where: {
|
|
uid: bookingUid,
|
|
},
|
|
select: {
|
|
...bookingMinimalSelect,
|
|
uid: true,
|
|
description: true,
|
|
isRecorded: true,
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
timeZone: true,
|
|
name: true,
|
|
email: true,
|
|
username: true,
|
|
},
|
|
},
|
|
references: {
|
|
select: {
|
|
id: true,
|
|
uid: true,
|
|
type: true,
|
|
meetingUrl: true,
|
|
meetingPassword: true,
|
|
},
|
|
where: {
|
|
type: "daily_video",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
static async findBookingForMeetingEndedPage({ bookingUid }: { bookingUid: string }) {
|
|
return await prisma.booking.findUnique({
|
|
where: {
|
|
uid: bookingUid,
|
|
},
|
|
select: {
|
|
...bookingMinimalSelect,
|
|
uid: true,
|
|
user: {
|
|
select: {
|
|
credentials: true,
|
|
},
|
|
},
|
|
references: {
|
|
select: {
|
|
uid: true,
|
|
type: true,
|
|
meetingUrl: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
static async findBookingByUidAndUserId({ bookingUid, userId }: { bookingUid: string; userId: number }) {
|
|
return await prisma.booking.findFirst({
|
|
where: {
|
|
uid: bookingUid,
|
|
OR: [
|
|
{ userId: userId },
|
|
{
|
|
eventType: {
|
|
hosts: {
|
|
some: {
|
|
userId,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
eventType: {
|
|
users: {
|
|
some: {
|
|
id: userId,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
eventType: {
|
|
team: {
|
|
members: {
|
|
some: {
|
|
userId,
|
|
accepted: true,
|
|
role: {
|
|
in: ["ADMIN", "OWNER"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
eventType: {
|
|
parent: {
|
|
team: {
|
|
members: {
|
|
some: {
|
|
userId,
|
|
accepted: true,
|
|
role: {
|
|
in: ["ADMIN", "OWNER"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
}
|
|
|
|
static async updateLocationById({
|
|
where: { id },
|
|
data: { location, metadata, referencesToCreate },
|
|
}: {
|
|
where: { id: number };
|
|
data: {
|
|
location: string;
|
|
metadata: Record<string, unknown>;
|
|
referencesToCreate: Prisma.BookingReferenceCreateInput[];
|
|
};
|
|
}) {
|
|
await prisma.booking.update({
|
|
where: {
|
|
id,
|
|
},
|
|
data: {
|
|
location,
|
|
metadata,
|
|
references: {
|
|
create: referencesToCreate,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
static async getAllAcceptedTeamBookingsOfUser(params: TeamBookingsParamsWithCount): Promise<number>;
|
|
|
|
static async getAllAcceptedTeamBookingsOfUser(
|
|
params: TeamBookingsParamsWithoutCount
|
|
): Promise<Array<Booking>>;
|
|
|
|
static async getAllAcceptedTeamBookingsOfUser(params: TeamBookingsParamsBase) {
|
|
const { user, teamId, startDate, endDate, excludedUid, shouldReturnCount, includeManagedEvents } = params;
|
|
|
|
const baseWhere: Prisma.BookingWhereInput = {
|
|
status: BookingStatus.ACCEPTED,
|
|
startTime: {
|
|
gte: startDate,
|
|
},
|
|
endTime: {
|
|
lte: endDate,
|
|
},
|
|
...(excludedUid && {
|
|
uid: {
|
|
not: excludedUid,
|
|
},
|
|
}),
|
|
};
|
|
|
|
const whereCollectiveRoundRobinOwner: Prisma.BookingWhereInput = {
|
|
...baseWhere,
|
|
userId: user.id,
|
|
eventType: {
|
|
teamId,
|
|
},
|
|
};
|
|
|
|
const whereCollectiveRoundRobinBookingsAttendee: Prisma.BookingWhereInput = {
|
|
...baseWhere,
|
|
attendees: {
|
|
some: {
|
|
email: user.email,
|
|
},
|
|
},
|
|
eventType: {
|
|
teamId,
|
|
},
|
|
};
|
|
|
|
const whereManagedBookings: Prisma.BookingWhereInput = {
|
|
...baseWhere,
|
|
userId: user.id,
|
|
eventType: {
|
|
parent: {
|
|
teamId,
|
|
},
|
|
},
|
|
};
|
|
|
|
if (shouldReturnCount) {
|
|
const collectiveRoundRobinBookingsOwner = await prisma.booking.count({
|
|
where: whereCollectiveRoundRobinOwner,
|
|
});
|
|
|
|
const collectiveRoundRobinBookingsAttendee = await prisma.booking.count({
|
|
where: whereCollectiveRoundRobinBookingsAttendee,
|
|
});
|
|
|
|
let managedBookings = 0;
|
|
|
|
if (includeManagedEvents) {
|
|
managedBookings = await prisma.booking.count({
|
|
where: whereManagedBookings,
|
|
});
|
|
}
|
|
|
|
const totalNrOfBooking =
|
|
collectiveRoundRobinBookingsOwner + collectiveRoundRobinBookingsAttendee + managedBookings;
|
|
|
|
return totalNrOfBooking;
|
|
}
|
|
const collectiveRoundRobinBookingsOwner = await prisma.booking.findMany({
|
|
where: whereCollectiveRoundRobinOwner,
|
|
});
|
|
|
|
const collectiveRoundRobinBookingsAttendee = await prisma.booking.findMany({
|
|
where: whereCollectiveRoundRobinBookingsAttendee,
|
|
});
|
|
|
|
let managedBookings: typeof collectiveRoundRobinBookingsAttendee = [];
|
|
|
|
if (includeManagedEvents) {
|
|
managedBookings = await prisma.booking.findMany({
|
|
where: whereManagedBookings,
|
|
});
|
|
}
|
|
|
|
return [
|
|
...collectiveRoundRobinBookingsOwner,
|
|
...collectiveRoundRobinBookingsAttendee,
|
|
...managedBookings,
|
|
];
|
|
}
|
|
|
|
static async findOriginalRescheduledBooking(uid: string, seatsEventType?: boolean) {
|
|
return await prisma.booking.findFirst({
|
|
where: {
|
|
uid: uid,
|
|
status: {
|
|
in: [BookingStatus.ACCEPTED, BookingStatus.CANCELLED, BookingStatus.PENDING],
|
|
},
|
|
},
|
|
include: {
|
|
attendees: {
|
|
select: {
|
|
name: true,
|
|
email: true,
|
|
locale: true,
|
|
timeZone: true,
|
|
phoneNumber: true,
|
|
...(seatsEventType && { bookingSeat: true, id: true }),
|
|
},
|
|
},
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
email: true,
|
|
locale: true,
|
|
timeZone: true,
|
|
destinationCalendar: true,
|
|
credentials: {
|
|
select: {
|
|
id: true,
|
|
userId: true,
|
|
key: true,
|
|
type: true,
|
|
teamId: true,
|
|
appId: true,
|
|
invalid: true,
|
|
user: {
|
|
select: {
|
|
email: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
destinationCalendar: true,
|
|
payment: true,
|
|
references: true,
|
|
workflowReminders: true,
|
|
},
|
|
});
|
|
}
|
|
}
|