2023-02-25 03:57:49 +00:00
import { BookingStatus , MembershipRole , Prisma , SchedulingType , WorkflowMethods } from "@prisma/client" ;
2023-04-13 19:07:10 +00:00
import type { BookingReference , EventType , User , WebhookTriggerEvents } from "@prisma/client" ;
2022-10-18 19:47:36 +00:00
import type { TFunction } from "next-i18next" ;
2022-05-27 23:27:41 +00:00
import { z } from "zod" ;
2023-02-08 20:36:22 +00:00
import appStore from "@calcom/app-store" ;
2022-10-18 19:47:36 +00:00
import { getCalendar } from "@calcom/app-store/_utils/getCalendar" ;
2022-08-26 00:48:50 +00:00
import { DailyLocationType } from "@calcom/app-store/locations" ;
2023-02-13 12:39:06 +00:00
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler" ;
2022-05-27 23:27:41 +00:00
import EventManager from "@calcom/core/EventManager" ;
2022-10-18 19:47:36 +00:00
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder" ;
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director" ;
import { deleteMeeting } from "@calcom/core/videoClient" ;
2022-06-28 20:40:58 +00:00
import dayjs from "@calcom/dayjs" ;
2023-02-13 12:39:06 +00:00
import { deleteScheduledEmailReminder } from "@calcom/ee/workflows/lib/reminders/emailReminderManager" ;
import { deleteScheduledSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager" ;
2023-02-25 03:57:49 +00:00
import { sendDeclinedEmails , sendLocationChangeEmails , sendRequestRescheduleEmail } from "@calcom/emails" ;
2023-03-27 08:27:10 +00:00
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses" ;
2023-02-25 03:57:49 +00:00
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation" ;
2022-10-12 13:04:51 +00:00
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks" ;
2023-02-16 22:39:57 +00:00
import sendPayload from "@calcom/features/webhooks/lib/sendPayload" ;
2022-08-26 21:58:08 +00:00
import { isPrismaObjOrUndefined , parseRecurringEvent } from "@calcom/lib" ;
2022-05-27 23:27:41 +00:00
import logger from "@calcom/lib/logger" ;
2022-08-26 21:58:08 +00:00
import { getTranslation } from "@calcom/lib/server" ;
2022-11-10 23:40:01 +00:00
import { bookingMinimalSelect } from "@calcom/prisma" ;
2022-08-26 21:58:08 +00:00
import { bookingConfirmPatchBodySchema } from "@calcom/prisma/zod-utils" ;
2022-10-18 19:47:36 +00:00
import type { AdditionalInformation , CalendarEvent , Person } from "@calcom/types/Calendar" ;
2022-05-27 23:27:41 +00:00
import { TRPCError } from "@trpc/server" ;
2023-02-25 03:57:49 +00:00
import { authedProcedure , router } from "../../trpc" ;
2022-07-22 17:27:06 +00:00
2022-10-18 19:47:36 +00:00
export type PersonAttendeeCommonFields = Pick <
User ,
"id" | "email" | "name" | "locale" | "timeZone" | "username"
> ;
2022-05-27 23:27:41 +00:00
// Common data for all endpoints under webhook
const commonBookingSchema = z . object ( {
bookingId : z.number ( ) ,
} ) ;
2022-11-10 23:40:01 +00:00
const bookingsProcedure = authedProcedure . input ( commonBookingSchema ) . use ( async ( { ctx , input , next } ) = > {
// Endpoints that just read the logged in user's data - like 'list' don't necessary have any input
const { bookingId } = input ;
const booking = await ctx . prisma . booking . findFirst ( {
where : {
id : bookingId ,
AND : [
{
OR : [
/* If user is organizer */
{ userId : ctx.user.id } ,
/* Or part of a collective booking */
{
eventType : {
schedulingType : SchedulingType.COLLECTIVE ,
users : {
some : {
id : ctx.user.id ,
} ,
} ,
} ,
} ,
] ,
} ,
] ,
} ,
include : {
attendees : true ,
eventType : true ,
destinationCalendar : true ,
references : true ,
user : {
include : {
destinationCalendar : true ,
credentials : true ,
} ,
} ,
} ,
} ) ;
if ( ! booking ) throw new TRPCError ( { code : "UNAUTHORIZED" } ) ;
return next ( { ctx : { booking } } ) ;
} ) ;
export const bookingsRouter = router ( {
get : authedProcedure
. input (
z . object ( {
2022-12-22 12:35:01 +00:00
filters : z.object ( {
teamIds : z.number ( ) . array ( ) . optional ( ) ,
userIds : z.number ( ) . array ( ) . optional ( ) ,
status : z.enum ( [ "upcoming" , "recurring" , "past" , "cancelled" , "unconfirmed" ] ) ,
eventTypeIds : z.number ( ) . array ( ) . optional ( ) ,
} ) ,
2022-11-10 23:40:01 +00:00
limit : z.number ( ) . min ( 1 ) . max ( 100 ) . nullish ( ) ,
cursor : z.number ( ) . nullish ( ) , // <-- "cursor" needs to exist when using useInfiniteQuery, but can be any type
} )
)
. query ( async ( { ctx , input } ) = > {
// 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 ;
2022-12-22 12:35:01 +00:00
const bookingListingByStatus = input . filters . status ;
2022-11-10 23:40:01 +00:00
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 } ,
2023-04-17 19:54:14 +00:00
status : { equals : BookingStatus.ACCEPTED } ,
2022-11-10 23:40:01 +00:00
} ,
{
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 ( ) } ,
2023-04-14 18:03:24 +00:00
status : { equals : BookingStatus.PENDING } ,
2022-11-10 23:40:01 +00:00
} ,
} ;
const bookingListingOrderby : Record <
typeof bookingListingByStatus ,
Prisma . BookingOrderByWithAggregationInput
> = {
upcoming : { startTime : "asc" } ,
recurring : { startTime : "asc" } ,
past : { startTime : "desc" } ,
cancelled : { startTime : "desc" } ,
unconfirmed : { startTime : "asc" } ,
} ;
2022-12-22 12:35:01 +00:00
// TODO: Fix record typing
const bookingWhereInputFilters : Record < string , Prisma.BookingWhereInput > = {
teamIds : {
AND : [
{
eventType : {
team : {
id : {
in : input . filters ? . teamIds ,
} ,
} ,
} ,
} ,
] ,
} ,
userIds : {
AND : [
{
eventType : {
users : {
some : {
id : {
in : input . filters ? . userIds ,
} ,
} ,
} ,
} ,
} ,
] ,
} ,
} ;
const filtersCombined : Prisma.BookingWhereInput [ ] =
input . filters &&
Object . keys ( input . filters ) . map ( ( key ) = > {
return bookingWhereInputFilters [ key ] ;
} ) ;
const passedBookingsStatusFilter = bookingListingFilters [ bookingListingByStatus ] ;
2022-11-10 23:40:01 +00:00
const orderBy = bookingListingOrderby [ bookingListingByStatus ] ;
2023-04-14 21:27:10 +00:00
const [ bookingsQuery , recurringInfoBasic , recurringInfoExtended ] = await Promise . all ( [
prisma . booking . findMany ( {
where : {
OR : [
{
userId : user.id ,
} ,
{
attendees : {
some : {
email : user.email ,
} ,
2022-11-10 23:40:01 +00:00
} ,
} ,
2023-04-14 21:27:10 +00:00
{
eventType : {
team : {
members : {
some : {
userId : user.id ,
role : {
in : [ "ADMIN" , "OWNER" ] ,
} ,
2023-01-19 18:23:58 +00:00
} ,
2022-11-10 23:40:01 +00:00
} ,
} ,
} ,
} ,
2023-04-14 21:27:10 +00:00
{
seatsReferences : {
some : {
attendee : {
email : user.email ,
} ,
2023-03-14 04:19:05 +00:00
} ,
} ,
} ,
2023-04-14 21:27:10 +00:00
] ,
AND : [ passedBookingsStatusFilter , . . . ( filtersCombined ? ? [ ] ) ] ,
} ,
select : {
. . . bookingMinimalSelect ,
uid : true ,
recurringEventId : true ,
location : true ,
eventType : {
select : {
slug : true ,
id : true ,
eventName : true ,
price : true ,
recurringEvent : true ,
team : {
select : {
name : true ,
} ,
2022-11-10 23:40:01 +00:00
} ,
} ,
} ,
2023-04-14 21:27:10 +00:00
status : true ,
paid : true ,
payment : {
select : {
paymentOption : true ,
amount : true ,
currency : true ,
success : true ,
} ,
2022-11-10 23:40:01 +00:00
} ,
2023-04-14 21:27:10 +00:00
user : {
select : {
id : true ,
name : true ,
email : true ,
2023-03-14 04:19:05 +00:00
} ,
} ,
2023-04-14 21:27:10 +00:00
rescheduled : true ,
references : true ,
isRecorded : true ,
seatsReferences : {
where : {
attendee : {
email : user.email ,
} ,
} ,
select : {
referenceUid : true ,
attendee : {
select : {
email : true ,
} ,
2023-03-14 04:19:05 +00:00
} ,
} ,
} ,
} ,
2023-04-14 21:27:10 +00:00
orderBy ,
take : take + 1 ,
skip ,
} ) ,
prisma . booking . groupBy ( {
by : [ "recurringEventId" ] ,
_min : {
startTime : true ,
2022-11-10 23:40:01 +00:00
} ,
2023-04-14 21:27:10 +00:00
_count : {
recurringEventId : true ,
2022-11-10 23:40:01 +00:00
} ,
2023-04-14 21:27:10 +00:00
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 ,
} ,
} ) ,
] ) ;
2022-11-10 23:40:01 +00:00
const recurringInfo = recurringInfoBasic . map (
(
2023-03-02 18:23:33 +00:00
info : ( typeof recurringInfoBasic ) [ number ]
2022-11-10 23:40:01 +00:00
) : {
recurringEventId : string | null ;
count : number ;
firstDate : Date | null ;
bookings : {
[ key : string ] : Date [ ] ;
} ;
} = > {
2023-04-14 21:29:43 +00:00
const bookings = recurringInfoExtended . reduce (
( prev , curr ) = > {
if ( curr . recurringEventId === info . recurringEventId ) {
2022-11-10 23:40:01 +00:00
prev [ curr . status ] . push ( curr . startTime ) ;
}
2023-04-14 21:29:43 +00:00
return prev ;
} ,
{ ACCEPTED : [ ] , CANCELLED : [ ] , REJECTED : [ ] , PENDING : [ ] } as {
[ key in BookingStatus ] : Date [ ] ;
}
) ;
2022-11-10 23:40:01 +00:00
return {
recurringEventId : info.recurringEventId ,
count : info._count.recurringEventId ,
firstDate : info._min.startTime ,
bookings ,
} ;
}
) ;
const bookings = bookingsQuery . map ( ( booking ) = > {
return {
. . . booking ,
eventType : {
. . . booking . eventType ,
recurringEvent : parseRecurringEvent ( booking . eventType ? . recurringEvent ) ,
} ,
startTime : booking.startTime.toISOString ( ) ,
endTime : booking.endTime.toISOString ( ) ,
} ;
} ) ;
const bookingsFetched = bookings . length ;
let nextCursor : typeof skip | null = skip ;
if ( bookingsFetched > take ) {
nextCursor += bookingsFetched ;
} else {
nextCursor = null ;
}
return {
bookings ,
recurringInfo ,
nextCursor ,
} ;
2022-10-18 19:47:36 +00:00
} ) ,
2022-11-10 23:40:01 +00:00
requestReschedule : authedProcedure
. input (
z . object ( {
bookingId : z.string ( ) ,
rescheduleReason : z.string ( ) . optional ( ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
2022-10-18 19:47:36 +00:00
const { user , prisma } = ctx ;
const { bookingId , rescheduleReason : cancellationReason } = input ;
const bookingToReschedule = await prisma . booking . findFirstOrThrow ( {
select : {
id : true ,
uid : true ,
userId : true ,
title : true ,
description : true ,
startTime : true ,
endTime : true ,
eventTypeId : true ,
eventType : true ,
location : true ,
attendees : true ,
references : true ,
customInputs : true ,
dynamicEventSlugRef : true ,
dynamicGroupSlugRef : true ,
destinationCalendar : true ,
2022-12-01 19:51:59 +00:00
smsReminderNumber : true ,
2023-02-13 12:39:06 +00:00
scheduledJobs : true ,
workflowReminders : true ,
2023-04-04 04:59:09 +00:00
responses : true ,
2022-10-18 19:47:36 +00:00
} ,
where : {
uid : bookingId ,
NOT : {
status : {
in : [ BookingStatus . CANCELLED , BookingStatus . REJECTED ] ,
} ,
} ,
} ,
} ) ;
if ( ! bookingToReschedule . userId ) {
throw new TRPCError ( { code : "FORBIDDEN" , message : "Booking to reschedule doesn't have an owner" } ) ;
}
if ( ! bookingToReschedule . eventType ) {
throw new TRPCError ( { code : "FORBIDDEN" , message : "EventType not found for current booking." } ) ;
}
const bookingBelongsToTeam = ! ! bookingToReschedule . eventType ? . teamId ;
const userTeams = await prisma . user . findUniqueOrThrow ( {
where : {
id : user.id ,
} ,
select : {
teams : true ,
} ,
} ) ;
if ( bookingBelongsToTeam && bookingToReschedule . eventType ? . teamId ) {
const userTeamIds = userTeams . teams . map ( ( item ) = > item . teamId ) ;
if ( userTeamIds . indexOf ( bookingToReschedule ? . eventType ? . teamId ) === - 1 ) {
throw new TRPCError ( { code : "FORBIDDEN" , message : "User isn't a member on the team" } ) ;
}
}
if ( ! bookingBelongsToTeam && bookingToReschedule . userId !== user . id ) {
throw new TRPCError ( { code : "FORBIDDEN" , message : "User isn't owner of the current booking" } ) ;
}
if ( bookingToReschedule ) {
let event : Partial < EventType > = { } ;
if ( bookingToReschedule . eventTypeId ) {
event = await prisma . eventType . findFirstOrThrow ( {
select : {
title : true ,
users : true ,
schedulingType : true ,
recurringEvent : true ,
} ,
where : {
id : bookingToReschedule.eventTypeId ,
} ,
} ) ;
}
await prisma . booking . update ( {
where : {
id : bookingToReschedule.id ,
} ,
data : {
rescheduled : true ,
cancellationReason ,
status : BookingStatus.CANCELLED ,
updatedAt : dayjs ( ) . toISOString ( ) ,
} ,
} ) ;
2023-02-20 17:40:08 +00:00
// delete scheduled jobs of previous booking
2023-02-13 12:39:06 +00:00
cancelScheduledJobs ( bookingToReschedule ) ;
2023-02-20 17:40:08 +00:00
//cancel workflow reminders of previous booking
2023-02-13 12:39:06 +00:00
bookingToReschedule . workflowReminders . forEach ( ( reminder ) = > {
2023-02-20 17:40:08 +00:00
if ( reminder . method === WorkflowMethods . EMAIL ) {
deleteScheduledEmailReminder ( reminder . id , reminder . referenceId ) ;
} else if ( reminder . method === WorkflowMethods . SMS ) {
deleteScheduledSMSReminder ( reminder . id , reminder . referenceId ) ;
2023-02-13 12:39:06 +00:00
}
} ) ;
2022-10-18 19:47:36 +00:00
const [ mainAttendee ] = bookingToReschedule . attendees ;
// @NOTE: Should we assume attendees language?
const tAttendees = await getTranslation ( mainAttendee . locale ? ? "en" , "common" ) ;
const usersToPeopleType = (
users : PersonAttendeeCommonFields [ ] ,
selectedLanguage : TFunction
) : Person [ ] = > {
return users ? . map ( ( user ) = > {
return {
email : user.email || "" ,
name : user.name || "" ,
username : user?.username || "" ,
language : { translate : selectedLanguage , locale : user.locale || "en" } ,
timeZone : user?.timeZone ,
} ;
} ) ;
} ;
const userTranslation = await getTranslation ( user . locale ? ? "en" , "common" ) ;
const [ userAsPeopleType ] = usersToPeopleType ( [ user ] , userTranslation ) ;
const builder = new CalendarEventBuilder ( ) ;
builder . init ( {
title : bookingToReschedule.title ,
type : event && event . title ? event.title : bookingToReschedule.title ,
startTime : bookingToReschedule.startTime.toISOString ( ) ,
endTime : bookingToReschedule.endTime.toISOString ( ) ,
attendees : usersToPeopleType (
// username field doesn't exists on attendee but could be in the future
bookingToReschedule . attendees as unknown as PersonAttendeeCommonFields [ ] ,
tAttendees
) ,
organizer : userAsPeopleType ,
} ) ;
const director = new CalendarEventDirector ( ) ;
director . setBuilder ( builder ) ;
director . setExistingBooking ( bookingToReschedule ) ;
cancellationReason && director . setCancellationReason ( cancellationReason ) ;
if ( event ) {
await director . buildForRescheduleEmail ( ) ;
} else {
await director . buildWithoutEventTypeForRescheduleEmail ( ) ;
}
// Handling calendar and videos cancellation
// This can set previous time as available, until virtual calendar is done
const credentialsMap = new Map ( ) ;
user . credentials . forEach ( ( credential ) = > {
credentialsMap . set ( credential . type , credential ) ;
} ) ;
2023-04-14 21:29:43 +00:00
const bookingRefsFiltered : BookingReference [ ] = bookingToReschedule . references . filter ( ( ref ) = >
credentialsMap . has ( ref . type )
2022-10-18 19:47:36 +00:00
) ;
2023-04-05 14:55:57 +00:00
bookingRefsFiltered . forEach ( async ( bookingRef ) = > {
2022-10-18 19:47:36 +00:00
if ( bookingRef . uid ) {
if ( bookingRef . type . endsWith ( "_calendar" ) ) {
2023-04-05 14:55:57 +00:00
const calendar = await getCalendar ( credentialsMap . get ( bookingRef . type ) ) ;
2022-10-18 19:47:36 +00:00
return calendar ? . deleteEvent (
bookingRef . uid ,
builder . calendarEvent ,
bookingRef . externalCalendarId
) ;
} else if ( bookingRef . type . endsWith ( "_video" ) ) {
return deleteMeeting ( credentialsMap . get ( bookingRef . type ) , bookingRef . uid ) ;
}
}
} ) ;
// Send emails
await sendRequestRescheduleEmail ( builder . calendarEvent , {
rescheduleLink : builder.rescheduleLink ,
} ) ;
const evt : CalendarEvent = {
title : bookingToReschedule?.title ,
type : event && event . title ? event.title : bookingToReschedule.title ,
description : bookingToReschedule?.description || "" ,
customInputs : isPrismaObjOrUndefined ( bookingToReschedule . customInputs ) ,
2023-04-04 04:59:09 +00:00
. . . getCalEventResponses ( {
booking : bookingToReschedule ,
bookingFields : bookingToReschedule.eventType?.bookingFields ? ? null ,
} ) ,
2022-10-18 19:47:36 +00:00
startTime : bookingToReschedule?.startTime ? dayjs ( bookingToReschedule . startTime ) . format ( ) : "" ,
endTime : bookingToReschedule?.endTime ? dayjs ( bookingToReschedule . endTime ) . format ( ) : "" ,
organizer : userAsPeopleType ,
attendees : usersToPeopleType (
// username field doesn't exists on attendee but could be in the future
bookingToReschedule . attendees as unknown as PersonAttendeeCommonFields [ ] ,
tAttendees
) ,
uid : bookingToReschedule?.uid ,
location : bookingToReschedule?.location ,
destinationCalendar :
bookingToReschedule ? . destinationCalendar || bookingToReschedule ? . destinationCalendar ,
cancellationReason : ` Please reschedule. ${ cancellationReason } ` , // TODO::Add i18-next for this
} ;
// Send webhook
const eventTrigger : WebhookTriggerEvents = "BOOKING_CANCELLED" ;
// Send Webhook call if hooked to BOOKING.CANCELLED
const subscriberOptions = {
userId : bookingToReschedule.userId ,
eventTypeId : ( bookingToReschedule . eventTypeId as number ) || 0 ,
triggerEvent : eventTrigger ,
} ;
const webhooks = await getWebhooks ( subscriberOptions ) ;
const promises = webhooks . map ( ( webhook ) = >
2022-12-01 19:51:59 +00:00
sendPayload ( webhook . secret , eventTrigger , new Date ( ) . toISOString ( ) , webhook , {
. . . evt ,
smsReminderNumber : bookingToReschedule.smsReminderNumber || undefined ,
} ) . catch ( ( e ) = > {
2022-10-18 19:47:36 +00:00
console . error (
` Error executing webhook for event: ${ eventTrigger } , URL: ${ webhook . subscriberUrl } ` ,
e
) ;
} )
) ;
await Promise . all ( promises ) ;
}
2022-05-27 23:27:41 +00:00
} ) ,
2022-11-10 23:40:01 +00:00
editLocation : bookingsProcedure
. input (
commonBookingSchema . extend ( {
newLocation : z.string ( ) . transform ( ( val ) = > val || DailyLocationType ) ,
} )
)
. mutation ( async ( { ctx , input } ) = > {
2022-05-27 23:27:41 +00:00
const { bookingId , newLocation : location } = input ;
const { booking } = ctx ;
try {
2022-08-31 19:44:47 +00:00
const organizer = await ctx . prisma . user . findFirstOrThrow ( {
2022-05-27 23:27:41 +00:00
where : {
id : booking.userId || 0 ,
} ,
select : {
name : true ,
email : true ,
timeZone : true ,
locale : true ,
} ,
} ) ;
const tOrganizer = await getTranslation ( organizer . locale ? ? "en" , "common" ) ;
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 = {
title : booking.title || "" ,
type : ( booking . eventType ? . title as string ) || booking ? . title || "" ,
description : booking.description || "" ,
startTime : booking.startTime ? dayjs ( booking . startTime ) . format ( ) : "" ,
endTime : booking.endTime ? dayjs ( booking . endTime ) . format ( ) : "" ,
organizer : {
email : organizer.email ,
name : organizer.name ? ? "Nameless" ,
timeZone : organizer.timeZone ,
language : { translate : tOrganizer , locale : organizer.locale ? ? "en" } ,
} ,
attendees : attendeesList ,
uid : booking.uid ,
2022-06-10 00:32:34 +00:00
recurringEvent : parseRecurringEvent ( booking . eventType ? . recurringEvent ) ,
2022-05-27 23:27:41 +00:00
location ,
destinationCalendar : booking?.destinationCalendar || booking ? . user ? . destinationCalendar ,
2023-03-30 23:45:48 +00:00
seatsPerTimeSlot : booking.eventType?.seatsPerTimeSlot ,
seatsShowAttendees : booking.eventType?.seatsShowAttendees ,
2022-05-27 23:27:41 +00:00
} ;
const eventManager = new EventManager ( ctx . user ) ;
2022-08-08 19:38:02 +00:00
const updatedResult = await eventManager . updateLocation ( evt , booking ) ;
const results = updatedResult . results ;
2022-05-27 23:27:41 +00:00
if ( results . length > 0 && results . every ( ( res ) = > ! res . success ) ) {
const error = {
errorCode : "BookingUpdateLocationFailed" ,
message : "Updating location failed" ,
} ;
logger . error ( ` Booking ${ ctx . user . username } failed ` , error , results ) ;
} else {
2022-08-08 19:38:02 +00:00
await ctx . prisma . booking . update ( {
where : {
id : bookingId ,
} ,
data : {
location ,
references : {
create : updatedResult.referencesToCreate ,
} ,
} ,
} ) ;
2022-06-06 19:49:00 +00:00
const metadata : AdditionalInformation = { } ;
2022-05-27 23:27:41 +00:00
if ( results . length ) {
2022-08-08 19:38:02 +00:00
metadata . hangoutLink = results [ 0 ] . updatedEvent ? . hangoutLink ;
metadata . conferenceData = results [ 0 ] . updatedEvent ? . conferenceData ;
metadata . entryPoints = results [ 0 ] . updatedEvent ? . entryPoints ;
2022-05-27 23:27:41 +00:00
}
try {
2022-06-06 19:49:00 +00:00
await sendLocationChangeEmails ( { . . . evt , additionalInformation : metadata } ) ;
2022-05-27 23:27:41 +00:00
} catch ( error ) {
console . log ( "Error sending LocationChangeEmails" ) ;
}
}
} catch {
throw new TRPCError ( { code : "INTERNAL_SERVER_ERROR" } ) ;
}
return { message : "Location updated" } ;
2022-11-10 23:40:01 +00:00
} ) ,
2022-12-22 00:15:51 +00:00
confirm : bookingsProcedure.input ( bookingConfirmPatchBodySchema ) . mutation ( async ( { ctx , input } ) = > {
const { user , prisma } = ctx ;
const { bookingId , recurringEventId , reason : rejectionReason , confirmed } = input ;
const tOrganizer = await getTranslation ( user . locale ? ? "en" , "common" ) ;
2023-04-04 04:59:09 +00:00
const booking = await prisma . booking . findUniqueOrThrow ( {
2022-12-22 00:15:51 +00:00
where : {
id : bookingId ,
} ,
select : {
title : true ,
description : true ,
customInputs : true ,
startTime : true ,
endTime : true ,
attendees : true ,
eventTypeId : true ,
2023-03-27 08:27:10 +00:00
responses : true ,
2022-12-22 00:15:51 +00:00
eventType : {
select : {
id : true ,
2023-02-08 20:36:22 +00:00
owner : true ,
teamId : true ,
2022-12-22 00:15:51 +00:00
recurringEvent : true ,
title : true ,
requiresConfirmation : true ,
currency : true ,
length : true ,
description : true ,
price : true ,
2023-03-27 08:27:10 +00:00
bookingFields : true ,
disableGuests : true ,
metadata : true ,
2022-12-22 00:15:51 +00:00
workflows : {
include : {
workflow : {
include : {
steps : true ,
} ,
} ,
} ,
} ,
2023-03-27 08:27:10 +00:00
customInputs : true ,
2022-12-22 00:15:51 +00:00
} ,
} ,
location : true ,
userId : true ,
id : true ,
uid : true ,
payment : true ,
destinationCalendar : true ,
paid : true ,
recurringEventId : true ,
status : true ,
smsReminderNumber : true ,
scheduledJobs : true ,
} ,
} ) ;
2023-03-27 08:27:10 +00:00
2023-04-18 23:48:53 +00:00
if ( booking . userId !== user . id && booking . eventTypeId ) {
// Only query database when it is explicitly required.
2023-04-17 23:50:34 +00:00
const eventType = await prisma . eventType . findFirst ( {
2022-12-22 00:15:51 +00:00
where : {
2023-04-18 23:48:53 +00:00
id : booking.eventTypeId ,
2023-04-17 23:50:34 +00:00
schedulingType : SchedulingType.COLLECTIVE ,
2022-12-22 00:15:51 +00:00
} ,
select : {
users : true ,
} ,
} ) ;
2023-04-17 23:50:34 +00:00
2023-04-18 23:48:53 +00:00
if ( eventType && ! eventType . users . find ( ( user ) = > booking . userId === user . id ) ) {
throw new TRPCError ( { code : "UNAUTHORIZED" , message : "UNAUTHORIZED" } ) ;
}
2023-04-17 23:50:34 +00:00
}
2022-12-22 00:15:51 +00:00
2023-04-18 23:48:53 +00:00
// Do not move this before authorization check.
// This is done to avoid exposing extra information to the requester.
if ( booking . status === BookingStatus . ACCEPTED ) {
throw new TRPCError ( { code : "BAD_REQUEST" , message : "Booking already confirmed" } ) ;
}
2022-12-22 00:15:51 +00:00
2023-02-13 12:26:28 +00:00
// If booking requires payment and is not paid, we don't allow confirmation
if ( confirmed && booking . payment . length > 0 && ! booking . paid ) {
2022-12-22 00:15:51 +00:00
await prisma . booking . update ( {
where : {
id : bookingId ,
} ,
data : {
status : BookingStatus.ACCEPTED ,
} ,
} ) ;
return { message : "Booking confirmed" , status : BookingStatus.ACCEPTED } ;
}
2023-02-13 12:26:28 +00:00
2023-04-18 23:48:53 +00:00
// Cache translations to avoid requesting multiple times.
const translations = new Map ( ) ;
2022-12-22 00:15:51 +00:00
const attendeesListPromises = booking . attendees . map ( async ( attendee ) = > {
2023-04-18 23:48:53 +00:00
const locale = attendee . locale ? ? "en" ;
let translate = translations . get ( locale ) ;
if ( ! translate ) {
translate = await getTranslation ( locale , "common" ) ;
translations . set ( locale , translate ) ;
}
2022-12-22 00:15:51 +00:00
return {
name : attendee.name ,
email : attendee.email ,
timeZone : attendee.timeZone ,
language : {
2023-04-18 23:48:53 +00:00
translate ,
locale ,
2022-12-22 00:15:51 +00:00
} ,
} ;
} ) ;
const attendeesList = await Promise . all ( attendeesListPromises ) ;
const evt : CalendarEvent = {
type : booking . eventType ? . title || booking . title ,
title : booking.title ,
description : booking.description ,
2023-04-04 04:59:09 +00:00
// TODO: Remove the usage of `bookingFields` in computing responses. We can do that by storing `label` with the response. Also, this would allow us to correctly show the label for a field even after the Event Type has been deleted.
. . . getCalEventResponses ( {
bookingFields : booking.eventType?.bookingFields ? ? null ,
booking ,
} ) ,
2022-12-22 00:15:51 +00:00
customInputs : isPrismaObjOrUndefined ( booking . customInputs ) ,
startTime : booking.startTime.toISOString ( ) ,
endTime : booking.endTime.toISOString ( ) ,
organizer : {
email : user.email ,
name : user.name || "Unnamed" ,
2023-04-18 10:08:09 +00:00
username : user.username || undefined ,
2022-12-22 00:15:51 +00:00
timeZone : user.timeZone ,
language : { translate : tOrganizer , locale : user.locale ? ? "en" } ,
} ,
attendees : attendeesList ,
location : booking.location ? ? "" ,
uid : booking.uid ,
destinationCalendar : booking?.destinationCalendar || user . destinationCalendar ,
requiresConfirmation : booking?.eventType?.requiresConfirmation ? ? false ,
eventTypeId : booking.eventType?.id ,
} ;
const recurringEvent = parseRecurringEvent ( booking . eventType ? . recurringEvent ) ;
2023-01-05 19:55:55 +00:00
if ( recurringEventId ) {
if (
! ( await prisma . booking . findFirst ( {
where : {
recurringEventId ,
id : booking.id ,
} ,
2023-04-18 23:48:53 +00:00
select : {
id : true ,
} ,
2023-01-05 19:55:55 +00:00
} ) )
) {
// FIXME: It might be best to retrieve recurringEventId from the booking itself.
throw new TRPCError ( {
code : "UNAUTHORIZED" ,
message : "Recurring event id doesn't belong to the booking" ,
} ) ;
}
}
2022-12-22 00:15:51 +00:00
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 ;
// count changed, parsing again to get the new value in
evt . recurringEvent = parseRecurringEvent ( recurringEvent ) ;
}
if ( confirmed ) {
2023-02-25 03:57:49 +00:00
await handleConfirmation ( { user , evt , recurringEventId , prisma , bookingId , booking } ) ;
2022-12-22 00:15:51 +00:00
} 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.
await prisma . booking . updateMany ( {
where : {
recurringEventId ,
status : BookingStatus.PENDING ,
} ,
data : {
status : BookingStatus.REJECTED ,
rejectionReason ,
} ,
} ) ;
} else {
2023-02-09 15:43:28 +00:00
// handle refunds
if ( ! ! booking . payment . length ) {
const successPayment = booking . payment . find ( ( payment ) = > payment . success ) ;
if ( ! successPayment ) {
2023-02-13 12:26:28 +00:00
// Disable paymentLink for this booking
} else {
let eventTypeOwnerId ;
if ( booking . eventType ? . owner ) {
eventTypeOwnerId = booking . eventType . owner . id ;
} else if ( booking . eventType ? . teamId ) {
const teamOwner = await prisma . membership . findFirst ( {
where : {
teamId : booking.eventType.teamId ,
role : MembershipRole.OWNER ,
} ,
select : {
userId : true ,
} ,
} ) ;
eventTypeOwnerId = teamOwner ? . userId ;
}
if ( ! eventTypeOwnerId ) {
throw new Error ( "Event Type owner not found for obtaining payment app credentials" ) ;
}
2023-02-09 15:43:28 +00:00
2023-02-13 12:26:28 +00:00
const paymentAppCredentials = await prisma . credential . findMany ( {
2023-02-09 15:43:28 +00:00
where : {
2023-02-13 12:26:28 +00:00
userId : eventTypeOwnerId ,
appId : successPayment.appId ,
2023-02-09 15:43:28 +00:00
} ,
select : {
2023-02-13 12:26:28 +00:00
key : true ,
appId : true ,
app : {
select : {
categories : true ,
dirName : true ,
} ,
2023-02-09 15:43:28 +00:00
} ,
2023-02-08 20:36:22 +00:00
} ,
2023-02-13 12:26:28 +00:00
} ) ;
2023-02-08 20:36:22 +00:00
2023-02-13 12:26:28 +00:00
const paymentAppCredential = paymentAppCredentials . find ( ( credential ) = > {
return credential . appId === successPayment . appId ;
} ) ;
2023-02-08 20:36:22 +00:00
2023-02-13 12:26:28 +00:00
if ( ! paymentAppCredential ) {
throw new Error ( "Payment app credentials not found" ) ;
}
2023-02-08 20:36:22 +00:00
2023-02-13 12:26:28 +00:00
// Posible to refactor TODO:
2023-04-05 14:55:57 +00:00
const paymentApp = await appStore [ paymentAppCredential ? . app ? . dirName as keyof typeof appStore ] ;
2023-02-13 12:26:28 +00:00
if ( ! ( paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp . lib ) ) {
console . warn ( ` payment App service of type ${ paymentApp } is not implemented ` ) ;
return null ;
}
2023-02-08 20:36:22 +00:00
2023-02-13 12:26:28 +00:00
const PaymentService = paymentApp . lib . PaymentService ;
const paymentInstance = new PaymentService ( paymentAppCredential ) ;
const paymentData = await paymentInstance . refund ( successPayment . id ) ;
if ( ! paymentData . refunded ) {
throw new Error ( "Payment could not be refunded" ) ;
}
2023-02-09 15:43:28 +00:00
}
2023-02-08 20:36:22 +00:00
}
2023-02-09 15:43:28 +00:00
// end handle refunds.
2023-02-08 20:36:22 +00:00
2022-12-22 00:15:51 +00:00
await prisma . booking . update ( {
where : {
id : bookingId ,
} ,
data : {
status : BookingStatus.REJECTED ,
rejectionReason ,
} ,
} ) ;
}
await sendDeclinedEmails ( evt ) ;
}
const message = "Booking " + confirmed ? "confirmed" : "rejected" ;
const status = confirmed ? BookingStatus.ACCEPTED : BookingStatus.REJECTED ;
return { message , status } ;
} ) ,
2023-03-14 04:19:05 +00:00
getBookingAttendees : authedProcedure
. input ( z . object ( { seatReferenceUid : z.string ( ) . uuid ( ) } ) )
. query ( async ( { ctx , input } ) = > {
const bookingSeat = await ctx . prisma . bookingSeat . findUniqueOrThrow ( {
where : {
referenceUid : input.seatReferenceUid ,
} ,
select : {
booking : {
select : {
_count : {
select : {
seatsReferences : true ,
} ,
} ,
} ,
} ,
} ,
} ) ;
if ( ! bookingSeat ) {
throw new Error ( "Booking not found" ) ;
}
return bookingSeat . booking . _count . seatsReferences ;
} ) ,
2022-11-10 23:40:01 +00:00
} ) ;