Files
calendar/packages/lib/server/repository/booking.ts
T
3eaccb8738 fix: Preview queue position and contact owner in preview (#17552)
* wip

* Add preview mode in router

* only fetch bookings of this month for weighted rr

* test set up

* only get bookings of current month

* reverts test setup

* first changes for weight adjustments

* reset booking count + adjust calibration

* depreciate weightAdjustment

* get only bookings created this month

* fix typo

* make sure createdAt for hosts is correct

* use earliest possibel date as fallback

* add missing createdAt date to tests

* fix typo

* clean up changes in tests

* fix typo

* change end date to current date

* fix: Fall back to empty host array when no hosts are found

* fix: Restructure code a little

* fixed test, incorrectly used now outdated var

* perf: remove Dayjs from getLuckyUser

* Refactor getHostsWithCalibration for optimised performance, intentionally break test as findMany is always an array

* Better mock for host.findMany

* Remove team-event-types.test.ts, move to appropriate package

* TypeScript cannot auto-infer that an array is non-empty when assigning to a var

* fix: Type Fixes and DistributionMethod enum add

* Optimise tests

* Added test to show that bookings made before a newHost was added affect the lucky user result

* Throw error when the usersWithHighestPriority is empty, which should never happen

* WIP

* remove comment

* update migrations

* get attributes weights and virtual queue data

* use attribute weights and use bookings of virtual queue only

* clean up migrations

* Add shortfall column and add tests

* code clean up from feedback

* wrapper function for getLuckyUser

* code clean up

* fetch routingFormResponse in handleNewBooking

* fix type errors

* fix type errors in tests

* fix getAttributesQueryValue import for tests

* fix totalWeight

* add test for attributes weights and virtual queues

* clean up code

* add test for prepareQueuesAndAttributesData

* remove console.log

* use lazy import

* Add more tests and more columns to matching members queue

* fix issue from merge

* always send usersAndTheirBookingShortfalls

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2024-11-15 20:39:46 +00:00

523 lines
12 KiB
TypeScript

import type { 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: [
{
user: {
id: {
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 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 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;
}
});
}
console.log(`queueBookings ${JSON.stringify(queueBookings.map((booking) => booking.id))}`);
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"],
},
},
},
},
},
},
],
},
});
}
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,
},
});
}
}