2023-04-25 22:39:47 +00:00
import { parseRecurringEvent } from "@calcom/lib" ;
2023-08-24 09:14:10 +00:00
import type { PrismaClient } from "@calcom/prisma" ;
2023-04-25 22:39:47 +00:00
import { bookingMinimalSelect } from "@calcom/prisma" ;
2023-08-24 09:14:10 +00:00
import type { Prisma } from "@calcom/prisma/client" ;
2023-05-02 11:44:05 +00:00
import { BookingStatus } from "@calcom/prisma/enums" ;
2023-04-25 22:39:47 +00:00
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils" ;
import type { TrpcSessionUser } from "../../../trpc" ;
import type { TGetInputSchema } from "./get.schema" ;
type GetOptions = {
ctx : {
user : NonNullable < TrpcSessionUser > ;
prisma : PrismaClient ;
} ;
input : TGetInputSchema ;
} ;
export const getHandler = async ( { ctx , input } : GetOptions ) = > {
// using offset actually because cursor pagination requires a unique column
// for orderBy, but we don't use a unique column in our orderBy
const take = input . limit ? ? 10 ;
const skip = input . cursor ? ? 0 ;
const { prisma , user } = ctx ;
const bookingListingByStatus = input . filters . status ;
const bookingListingFilters : Record < typeof bookingListingByStatus , Prisma.BookingWhereInput > = {
upcoming : {
endTime : { gte : new Date ( ) } ,
// These changes are needed to not show confirmed recurring events,
// as rescheduling or cancel for recurring event bookings should be
// handled separately for each occurrence
OR : [
{
recurringEventId : { not : null } ,
status : { equals : BookingStatus.ACCEPTED } ,
} ,
{
recurringEventId : { equals : null } ,
status : { notIn : [ BookingStatus . CANCELLED , BookingStatus . REJECTED ] } ,
} ,
] ,
} ,
recurring : {
endTime : { gte : new Date ( ) } ,
AND : [
{ NOT : { recurringEventId : { equals : null } } } ,
{ status : { notIn : [ BookingStatus . CANCELLED , BookingStatus . REJECTED ] } } ,
] ,
} ,
past : {
endTime : { lte : new Date ( ) } ,
AND : [
{ NOT : { status : { equals : BookingStatus.CANCELLED } } } ,
{ NOT : { status : { equals : BookingStatus.REJECTED } } } ,
] ,
} ,
cancelled : {
OR : [ { status : { equals : BookingStatus.CANCELLED } } , { status : { equals : BookingStatus.REJECTED } } ] ,
} ,
unconfirmed : {
endTime : { gte : new Date ( ) } ,
status : { equals : BookingStatus.PENDING } ,
} ,
} ;
const bookingListingOrderby : Record <
typeof bookingListingByStatus ,
Prisma . BookingOrderByWithAggregationInput
> = {
upcoming : { startTime : "asc" } ,
recurring : { startTime : "asc" } ,
past : { startTime : "desc" } ,
cancelled : { startTime : "desc" } ,
unconfirmed : { startTime : "asc" } ,
} ;
2023-08-24 09:14:10 +00:00
const passedBookingsStatusFilter = bookingListingFilters [ bookingListingByStatus ] ;
const orderBy = bookingListingOrderby [ bookingListingByStatus ] ;
const { bookings , recurringInfo } = await getBookings ( {
user ,
prisma ,
passedBookingsStatusFilter ,
filters : input.filters ,
orderBy ,
take ,
skip ,
} ) ;
const bookingsFetched = bookings . length ;
let nextCursor : typeof skip | null = skip ;
if ( bookingsFetched > take ) {
nextCursor += bookingsFetched ;
} else {
nextCursor = null ;
}
return {
bookings ,
recurringInfo ,
nextCursor ,
} ;
} ;
const set = new Set ( ) ;
const getUniqueBookings = < T extends { uid : string } > ( arr : T [ ] ) = > {
const unique = arr . filter ( ( booking ) = > {
const duplicate = set . has ( booking . uid ) ;
set . add ( booking . uid ) ;
return ! duplicate ;
} ) ;
set . clear ( ) ;
return unique ;
} ;
async function getBookings ( {
user ,
prisma ,
passedBookingsStatusFilter ,
filters ,
orderBy ,
take ,
skip ,
} : {
user : { id : number ; email : string } ;
filters : TGetInputSchema [ "filters" ] ;
prisma : PrismaClient ;
passedBookingsStatusFilter : Prisma.BookingWhereInput ;
orderBy : Prisma.BookingOrderByWithAggregationInput ;
take : number ;
skip : number ;
} ) {
2023-04-25 22:39:47 +00:00
// TODO: Fix record typing
const bookingWhereInputFilters : Record < string , Prisma.BookingWhereInput > = {
teamIds : {
AND : [
{
eventType : {
team : {
id : {
2023-08-24 09:14:10 +00:00
in : filters ? . teamIds ,
2023-04-25 22:39:47 +00:00
} ,
} ,
} ,
} ,
] ,
} ,
userIds : {
AND : [
{
eventType : {
users : {
some : {
id : {
2023-08-24 09:14:10 +00:00
in : filters ? . userIds ,
2023-04-25 22:39:47 +00:00
} ,
} ,
} ,
} ,
} ,
] ,
} ,
2023-05-29 16:01:17 +00:00
eventTypeIds : {
AND : [
{
eventTypeId : {
2023-08-24 09:14:10 +00:00
in : filters ? . eventTypeIds ,
2023-05-29 16:01:17 +00:00
} ,
} ,
] ,
} ,
2023-04-25 22:39:47 +00:00
} ;
const filtersCombined : Prisma.BookingWhereInput [ ] =
2023-08-24 09:14:10 +00:00
filters &&
Object . keys ( filters ) . map ( ( key ) = > {
2023-04-25 22:39:47 +00:00
return bookingWhereInputFilters [ key ] ;
} ) ;
2023-08-24 09:14:10 +00:00
const bookingSelect = {
. . . bookingMinimalSelect ,
uid : true ,
recurringEventId : true ,
location : true ,
eventType : {
select : {
slug : true ,
id : true ,
eventName : true ,
price : true ,
recurringEvent : true ,
currency : true ,
metadata : true ,
seatsShowAttendees : true ,
2023-09-08 15:37:26 +00:00
seatsShowAvailabilityCount : true ,
2023-08-24 09:14:10 +00:00
team : {
select : {
id : true ,
name : true ,
} ,
} ,
} ,
} ,
status : true ,
paid : true ,
payment : {
select : {
paymentOption : true ,
amount : true ,
currency : true ,
success : true ,
} ,
} ,
user : {
select : {
id : true ,
name : true ,
email : true ,
} ,
} ,
rescheduled : true ,
references : true ,
isRecorded : true ,
seatsReferences : {
where : {
attendee : {
email : user.email ,
} ,
} ,
select : {
referenceUid : true ,
attendee : {
select : {
email : true ,
} ,
} ,
} ,
} ,
} ;
2023-04-25 22:39:47 +00:00
2023-08-24 09:14:10 +00:00
const [
// Quering these in parallel to save time.
// Note that because we are applying `take` to individual queries, we will usually get more bookings then we need. It is okay to have more bookings faster than having what we need slower
bookingsQueryUserId ,
bookingsQueryAttendees ,
bookingsQueryTeamMember ,
bookingsQuerySeatReference ,
//////////////////////////
recurringInfoBasic ,
recurringInfoExtended ,
// We need all promises to be successful, so we are not using Promise.allSettled
] = await Promise . all ( [
2023-04-25 22:39:47 +00:00
prisma . booking . findMany ( {
where : {
OR : [
{
userId : user.id ,
} ,
2023-08-24 09:14:10 +00:00
] ,
AND : [ passedBookingsStatusFilter , . . . ( filtersCombined ? ? [ ] ) ] ,
} ,
orderBy ,
take : take + 1 ,
skip ,
} ) ,
prisma . booking . findMany ( {
where : {
OR : [
2023-04-25 22:39:47 +00:00
{
attendees : {
some : {
email : user.email ,
} ,
} ,
} ,
2023-08-24 09:14:10 +00:00
] ,
AND : [ passedBookingsStatusFilter , . . . ( filtersCombined ? ? [ ] ) ] ,
} ,
orderBy ,
take : take + 1 ,
skip ,
} ) ,
prisma . booking . findMany ( {
where : {
OR : [
2023-04-25 22:39:47 +00:00
{
eventType : {
team : {
members : {
some : {
userId : user.id ,
role : {
in : [ "ADMIN" , "OWNER" ] ,
} ,
} ,
} ,
} ,
} ,
} ,
2023-08-24 09:14:10 +00:00
] ,
AND : [ passedBookingsStatusFilter , . . . ( filtersCombined ? ? [ ] ) ] ,
} ,
orderBy ,
take : take + 1 ,
skip ,
} ) ,
prisma . booking . findMany ( {
where : {
OR : [
2023-04-25 22:39:47 +00:00
{
seatsReferences : {
some : {
attendee : {
email : user.email ,
} ,
} ,
} ,
} ,
] ,
AND : [ passedBookingsStatusFilter , . . . ( filtersCombined ? ? [ ] ) ] ,
} ,
orderBy ,
take : take + 1 ,
skip ,
} ) ,
prisma . booking . groupBy ( {
by : [ "recurringEventId" ] ,
_min : {
startTime : true ,
} ,
_count : {
recurringEventId : true ,
} ,
where : {
recurringEventId : {
not : { equals : null } ,
} ,
userId : user.id ,
} ,
} ) ,
prisma . booking . groupBy ( {
by : [ "recurringEventId" , "status" , "startTime" ] ,
_min : {
startTime : true ,
} ,
where : {
recurringEventId : {
not : { equals : null } ,
} ,
userId : user.id ,
} ,
} ) ,
] ) ;
const recurringInfo = recurringInfoBasic . map (
(
info : ( typeof recurringInfoBasic ) [ number ]
) : {
recurringEventId : string | null ;
count : number ;
firstDate : Date | null ;
bookings : {
[ key : string ] : Date [ ] ;
} ;
} = > {
const bookings = recurringInfoExtended . reduce (
( prev , curr ) = > {
if ( curr . recurringEventId === info . recurringEventId ) {
prev [ curr . status ] . push ( curr . startTime ) ;
}
return prev ;
} ,
{ ACCEPTED : [ ] , CANCELLED : [ ] , REJECTED : [ ] , PENDING : [ ] } as {
[ key in BookingStatus ] : Date [ ] ;
}
) ;
return {
recurringEventId : info.recurringEventId ,
count : info._count.recurringEventId ,
firstDate : info._min.startTime ,
bookings ,
} ;
}
) ;
2023-08-24 09:14:10 +00:00
const plainBookings = getUniqueBookings (
2023-08-25 09:36:57 +00:00
// It's going to mess up the orderBy as we are concatenating independent queries results
2023-08-24 09:14:10 +00:00
bookingsQueryUserId
. concat ( bookingsQueryAttendees )
. concat ( bookingsQueryTeamMember )
. concat ( bookingsQuerySeatReference )
) ;
// Now enrich bookings with relation data. We could have queried the relation data along with the bookings, but that would cause unnecessary queries to the database.
// Because Prisma is also going to query the select relation data sequentially, we are fine querying it separately here as it would be just 1 query instead of 4
const bookings = (
await prisma . booking . findMany ( {
where : {
id : {
in : plainBookings . map ( ( booking ) = > booking . id ) ,
} ,
} ,
select : bookingSelect ,
2023-08-25 09:36:57 +00:00
// We need to get the sorted bookings here as well because plainBookings array is not correctly sorted
orderBy ,
2023-08-24 09:14:10 +00:00
} )
) . map ( ( booking ) = > {
2023-07-11 15:11:08 +00:00
// If seats are enabled and the event is not set to show attendees, filter out attendees that are not the current user
if ( booking . seatsReferences . length && ! booking . eventType ? . seatsShowAttendees ) {
booking . attendees = booking . attendees . filter ( ( attendee ) = > attendee . email === user . email ) ;
}
2023-04-25 22:39:47 +00:00
return {
. . . booking ,
eventType : {
. . . booking . eventType ,
recurringEvent : parseRecurringEvent ( booking . eventType ? . recurringEvent ) ,
price : booking.eventType?.price || 0 ,
currency : booking.eventType?.currency || "usd" ,
metadata : EventTypeMetaDataSchema.parse ( booking . eventType ? . metadata || { } ) ,
} ,
startTime : booking.startTime.toISOString ( ) ,
endTime : booking.endTime.toISOString ( ) ,
} ;
} ) ;
2023-08-24 09:14:10 +00:00
return { bookings , recurringInfo } ;
}