2023-09-14 13:53:58 -03:00
import type { App , Attendee , DestinationCalendar , EventTypeCustomInput } from "@prisma/client" ;
2023-05-02 08:44:05 -03:00
import { Prisma } from "@prisma/client" ;
2022-10-12 10:04:51 -03:00
import async from "async" ;
2023-12-19 14:42:40 -03:00
import type { IncomingMessage } from "http" ;
2022-12-16 16:39:41 -03:00
import { isValidPhoneNumber } from "libphonenumber-js" ;
2023-08-18 15:13:21 -03:00
// eslint-disable-next-line no-restricted-imports
2022-11-05 15:58:35 -03:00
import { cloneDeep } from "lodash" ;
2022-10-12 10:04:51 -03:00
import type { NextApiRequest } from "next" ;
2023-03-14 01:19:05 -03:00
import short , { uuid } from "short-uuid" ;
2023-11-15 16:52:19 -03:00
import type { Logger } from "tslog" ;
2022-10-12 10:04:51 -03:00
import { v5 as uuidv5 } from "uuid" ;
2022-10-14 19:45:02 -03:00
import z from "zod" ;
2022-10-12 10:04:51 -03:00
2023-03-14 01:19:05 -03:00
import { getCalendar } from "@calcom/app-store/_utils/getCalendar" ;
2023-01-09 23:01:57 -03:00
import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata" ;
2023-02-19 04:43:36 -03:00
import type { LocationObject } from "@calcom/app-store/locations" ;
2023-08-07 19:08:13 -03:00
import {
getLocationValueForDB ,
MeetLocationType ,
OrganizerDefaultConferencingAppType ,
} from "@calcom/app-store/locations" ;
2023-02-19 04:43:36 -03:00
import type { EventTypeAppsList } from "@calcom/app-store/utils" ;
2023-06-06 08:59:57 -03:00
import { getAppFromSlug } from "@calcom/app-store/utils" ;
2022-10-12 10:04:51 -03:00
import EventManager from "@calcom/core/EventManager" ;
import { getEventName } from "@calcom/core/event" ;
import { getUserAvailability } from "@calcom/core/getUserAvailability" ;
2023-03-14 01:19:05 -03:00
import { deleteMeeting } from "@calcom/core/videoClient" ;
2023-02-19 04:43:36 -03:00
import dayjs from "@calcom/dayjs" ;
2023-12-12 23:23:48 -03:00
import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/scheduleMandatoryReminder" ;
2022-10-12 10:04:51 -03:00
import {
sendAttendeeRequestEmail ,
sendOrganizerRequestEmail ,
sendRescheduledEmails ,
2023-03-14 01:19:05 -03:00
sendRescheduledSeatEmail ,
2023-11-27 15:09:33 -03:00
sendRoundRobinCancelledEmails ,
sendRoundRobinRescheduledEmails ,
sendRoundRobinScheduledEmails ,
2023-08-07 19:08:13 -03:00
sendScheduledEmails ,
2022-10-12 10:04:51 -03:00
sendScheduledSeatsEmails ,
} from "@calcom/emails" ;
2023-12-15 12:28:32 -03:00
import getICalUID from "@calcom/emails/lib/getICalUID" ;
2023-03-02 15:15:28 -03:00
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields" ;
2023-03-27 05:27:10 -03:00
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses" ;
2023-05-30 12:35:05 -03:00
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger" ;
2023-10-02 07:51:04 -03:00
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled" ;
2023-09-26 18:50:10 -03:00
import { userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains" ;
2023-05-09 14:08:14 -03:00
import {
allowDisablingAttendeeConfirmationEmails ,
allowDisablingHostConfirmationEmails ,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails" ;
2023-07-31 14:35:48 -03:00
import {
cancelWorkflowReminders ,
2023-08-07 19:08:13 -03:00
scheduleWorkflowReminders ,
2023-07-31 14:35:48 -03:00
} from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler" ;
2023-07-20 02:03:50 -03:00
import { getFullName } from "@calcom/features/form-builder/utils" ;
2023-05-30 12:35:05 -03:00
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks" ;
2022-10-12 10:04:51 -03:00
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks" ;
2023-08-30 20:17:42 -03:00
import { cancelScheduledJobs , scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger" ;
2022-10-12 10:04:51 -03:00
import { isPrismaObjOrUndefined , parseRecurringEvent } from "@calcom/lib" ;
2023-04-13 16:03:08 -03:00
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser" ;
2023-08-07 19:08:13 -03:00
import { getDefaultEvent , getUsernameList } from "@calcom/lib/defaultEvents" ;
2023-11-15 16:52:19 -03:00
import { ErrorCode } from "@calcom/lib/errorCodes" ;
2022-10-12 10:04:51 -03:00
import { getErrorFromUnknown } from "@calcom/lib/errors" ;
2023-12-19 06:33:30 -03:00
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server" ;
2023-02-08 17:36:22 -03:00
import getPaymentAppData from "@calcom/lib/getPaymentAppData" ;
2023-07-25 14:05:02 -03:00
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType" ;
2022-10-12 10:04:51 -03:00
import { HttpError } from "@calcom/lib/http-error" ;
import isOutOfBounds , { BookingDateInPastError } from "@calcom/lib/isOutOfBounds" ;
import logger from "@calcom/lib/logger" ;
2023-02-08 17:36:22 -03:00
import { handlePayment } from "@calcom/lib/payment/handlePayment" ;
2023-10-02 07:51:04 -03:00
import { getPiiFreeCalendarEvent , getPiiFreeEventType , getPiiFreeUser } from "@calcom/lib/piiFreeData" ;
2023-09-30 10:28:52 -03:00
import { safeStringify } from "@calcom/lib/safeStringify" ;
2023-03-10 17:00:19 -03:00
import { checkBookingLimits , checkDurationLimits , getLuckyUser } from "@calcom/lib/server" ;
2022-10-12 10:04:51 -03:00
import { getTranslation } from "@calcom/lib/server/i18n" ;
2023-03-02 15:15:28 -03:00
import { slugify } from "@calcom/lib/slugify" ;
2022-10-12 10:04:51 -03:00
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager" ;
2023-07-19 11:30:37 -03:00
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat" ;
2022-10-12 10:04:51 -03:00
import prisma , { userSelect } from "@calcom/prisma" ;
2023-03-14 01:19:05 -03:00
import type { BookingReference } from "@calcom/prisma/client" ;
2023-07-31 14:35:48 -03:00
import { BookingStatus , SchedulingType , WebhookTriggerEvents } from "@calcom/prisma/enums" ;
2023-09-14 13:53:58 -03:00
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential" ;
2022-12-01 18:53:52 -03:00
import {
2023-08-07 19:08:13 -03:00
bookingCreateSchemaLegacyPropsForApi ,
2022-12-01 18:53:52 -03:00
customInputSchema ,
EventTypeMetaDataSchema ,
2023-02-15 12:42:49 -03:00
userMetadata as userMetadataSchema ,
2022-12-01 18:53:52 -03:00
} from "@calcom/prisma/zod-utils" ;
2022-10-12 10:04:51 -03:00
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime" ;
2023-06-06 08:59:57 -03:00
import type {
AdditionalInformation ,
AppsStatus ,
CalendarEvent ,
IntervalLimit ,
Person ,
} from "@calcom/types/Calendar" ;
2023-09-14 13:53:58 -03:00
import type { CredentialPayload } from "@calcom/types/Credential" ;
2022-10-12 10:04:51 -03:00
import type { EventResult , PartialReference } from "@calcom/types/EventManager" ;
2023-02-19 04:43:36 -03:00
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload" ;
2023-11-29 12:11:09 -03:00
import getBookingDataSchema from "./getBookingDataSchema" ;
2022-10-12 10:04:51 -03:00
const translator = short ( ) ;
2023-10-17 16:00:48 -03:00
const log = logger . getSubLogger ( { prefix : [ "[api] book:user" ] } ) ;
2022-10-12 10:04:51 -03:00
type User = Prisma . UserGetPayload < typeof userSelect > ;
type BufferedBusyTimes = BufferedBusyTime [ ] ;
2023-08-10 16:07:57 -03:00
type BookingType = Prisma . PromiseReturnType < typeof getOriginalRescheduledBooking > ;
2023-11-29 12:11:09 -03:00
type Booking = Prisma . PromiseReturnType < typeof createBooking > ;
export type NewBookingEventType =
| Awaited < ReturnType < typeof getDefaultEvent > >
| Awaited < ReturnType < typeof getEventTypesFromDB > > ;
// Work with Typescript to require reqBody.end
type ReqBodyWithoutEnd = z . infer < ReturnType < typeof getBookingDataSchema > > ;
type ReqBodyWithEnd = ReqBodyWithoutEnd & { end : string } ;
2022-10-12 10:04:51 -03:00
2023-02-08 17:36:22 -03:00
interface IEventTypePaymentCredentialType {
appId : EventTypeAppsList ;
app : {
categories : App [ "categories" ] ;
dirName : string ;
} ;
key : Prisma.JsonValue ;
}
2022-10-12 10:04:51 -03:00
/ * *
* Refreshes a Credential with fresh data from the database .
*
* @param credential
* /
2023-09-14 13:53:58 -03:00
async function refreshCredential ( credential : CredentialPayload ) : Promise < CredentialPayload > {
2022-10-12 10:04:51 -03:00
const newCredential = await prisma . credential . findUnique ( {
where : {
id : credential.id ,
} ,
2023-09-14 13:53:58 -03:00
select : credentialForCalendarServiceSelect ,
2022-10-12 10:04:51 -03:00
} ) ;
if ( ! newCredential ) {
return credential ;
} else {
return newCredential ;
}
}
/ * *
* Refreshes the given set of credentials .
*
* @param credentials
* /
2023-09-14 13:53:58 -03:00
async function refreshCredentials ( credentials : Array < CredentialPayload > ) : Promise < Array < CredentialPayload > > {
2022-10-12 10:04:51 -03:00
return await async . mapLimit ( credentials , 5 , refreshCredential ) ;
}
2023-07-11 04:41:21 -03:00
/ * *
* Gets credentials from the user , team , and org if applicable
*
* /
const getAllCredentials = async (
2023-09-14 13:53:58 -03:00
user : User & { credentials : CredentialPayload [ ] } ,
2023-07-11 04:41:21 -03:00
eventType : Awaited < ReturnType < typeof getEventTypesFromDB > >
) = > {
const allCredentials = user . credentials ;
// If it's a team event type query for team credentials
if ( eventType . team ? . id ) {
const teamCredentialsQuery = await prisma . credential . findMany ( {
where : {
teamId : eventType.team.id ,
} ,
2023-09-14 13:53:58 -03:00
select : credentialForCalendarServiceSelect ,
2023-07-11 04:41:21 -03:00
} ) ;
allCredentials . push ( . . . teamCredentialsQuery ) ;
}
// If it's a managed event type, query for the parent team's credentials
if ( eventType . parentId ) {
const teamCredentialsQuery = await prisma . team . findFirst ( {
where : {
eventTypes : {
some : {
id : eventType.parentId ,
} ,
} ,
} ,
select : {
2023-09-14 13:53:58 -03:00
credentials : {
select : credentialForCalendarServiceSelect ,
} ,
2023-07-11 04:41:21 -03:00
} ,
} ) ;
if ( teamCredentialsQuery ? . credentials ) {
allCredentials . push ( . . . teamCredentialsQuery ? . credentials ) ;
}
}
// If the user is a part of an organization, query for the organization's credentials
if ( user ? . organizationId ) {
const org = await prisma . team . findUnique ( {
where : {
id : user.organizationId ,
} ,
select : {
2023-09-14 13:53:58 -03:00
credentials : {
select : credentialForCalendarServiceSelect ,
} ,
2023-07-11 04:41:21 -03:00
} ,
} ) ;
if ( org ? . credentials ) {
allCredentials . push ( . . . org . credentials ) ;
}
}
return allCredentials ;
} ;
2022-11-04 08:36:11 -03:00
// if true, there are conflicts.
function checkForConflicts ( busyTimes : BufferedBusyTimes , time : dayjs.ConfigType , length : number ) {
2022-10-12 10:04:51 -03:00
// Early return
2022-11-04 08:36:11 -03:00
if ( ! Array . isArray ( busyTimes ) || busyTimes . length < 1 ) {
return false ; // guaranteed no conflicts when there is no busy times.
}
2022-10-12 10:04:51 -03:00
2022-11-04 08:36:11 -03:00
for ( const busyTime of busyTimes ) {
2022-10-12 10:04:51 -03:00
const startTime = dayjs ( busyTime . start ) ;
const endTime = dayjs ( busyTime . end ) ;
// Check if time is between start and end times
if ( dayjs ( time ) . isBetween ( startTime , endTime , null , "[)" ) ) {
2023-03-23 15:03:49 -03:00
log . error (
2023-09-30 10:28:52 -03:00
` NAUF: start between a busy time slot ${ safeStringify ( {
2023-03-23 15:03:49 -03:00
. . . busyTime ,
time : dayjs ( time ) . format ( ) ,
} ) } `
) ;
2022-11-04 08:36:11 -03:00
return true ;
2022-10-12 10:04:51 -03:00
}
// Check if slot end time is between start and end time
if ( dayjs ( time ) . add ( length , "minutes" ) . isBetween ( startTime , endTime ) ) {
2023-03-23 15:03:49 -03:00
log . error (
2023-09-30 10:28:52 -03:00
` NAUF: Ends between a busy time slot ${ safeStringify ( {
2023-03-23 15:03:49 -03:00
. . . busyTime ,
time : dayjs ( time ) . add ( length , "minutes" ) . format ( ) ,
} ) } `
) ;
2022-11-04 08:36:11 -03:00
return true ;
2022-10-12 10:04:51 -03:00
}
// Check if startTime is between slot
if ( startTime . isBetween ( dayjs ( time ) , dayjs ( time ) . add ( length , "minutes" ) ) ) {
2022-11-04 08:36:11 -03:00
return true ;
2022-10-12 10:04:51 -03:00
}
}
2022-11-04 08:36:11 -03:00
return false ;
2022-10-12 10:04:51 -03:00
}
2023-11-29 12:11:09 -03:00
export const getEventTypesFromDB = async ( eventTypeId : number ) = > {
2022-10-12 10:04:51 -03:00
const eventType = await prisma . eventType . findUniqueOrThrow ( {
where : {
id : eventTypeId ,
} ,
select : {
id : true ,
customInputs : true ,
2023-03-02 15:15:28 -03:00
disableGuests : true ,
2023-04-20 12:55:19 -03:00
users : {
select : {
2023-09-14 13:53:58 -03:00
credentials : {
select : credentialForCalendarServiceSelect ,
} ,
2023-04-20 12:55:19 -03:00
. . . userSelect . select ,
} ,
} ,
2023-04-18 07:08:09 -03:00
slug : true ,
2022-10-12 10:04:51 -03:00
team : {
select : {
id : true ,
name : true ,
2023-12-19 06:33:30 -03:00
parentId : true ,
2022-10-12 10:04:51 -03:00
} ,
} ,
2023-03-02 15:15:28 -03:00
bookingFields : true ,
2022-10-12 10:04:51 -03:00
title : true ,
length : true ,
eventName : true ,
schedulingType : true ,
description : true ,
periodType : true ,
periodStartDate : true ,
periodEndDate : true ,
periodDays : true ,
periodCountCalendarDays : true ,
2023-10-25 15:16:01 -03:00
lockTimeZoneToggleOnBookingPage : true ,
2022-10-12 10:04:51 -03:00
requiresConfirmation : true ,
2023-07-31 14:51:11 -03:00
requiresBookerEmailVerification : true ,
2022-10-12 10:04:51 -03:00
userId : true ,
price : true ,
currency : true ,
metadata : true ,
destinationCalendar : true ,
hideCalendarNotes : true ,
seatsPerTimeSlot : true ,
recurringEvent : true ,
2022-10-18 16:41:50 -03:00
seatsShowAttendees : true ,
2023-09-08 12:37:26 -03:00
seatsShowAvailabilityCount : true ,
2022-10-12 10:04:51 -03:00
bookingLimits : true ,
2023-03-10 17:00:19 -03:00
durationLimits : true ,
2023-07-11 04:41:21 -03:00
parentId : true ,
2023-04-18 07:08:09 -03:00
owner : {
select : {
hideBranding : true ,
} ,
} ,
2022-10-12 10:04:51 -03:00
workflows : {
include : {
workflow : {
include : {
steps : true ,
} ,
} ,
} ,
} ,
locations : true ,
timeZone : true ,
schedule : {
select : {
availability : true ,
timeZone : true ,
} ,
} ,
2023-01-12 18:09:12 -03:00
hosts : {
select : {
isFixed : true ,
2023-04-20 12:55:19 -03:00
user : {
select : {
2023-09-14 13:53:58 -03:00
credentials : {
select : credentialForCalendarServiceSelect ,
} ,
2023-04-20 12:55:19 -03:00
. . . userSelect . select ,
feat: Organizations users calendar cache (#9498)
* Initial commit
* Adding feature flag
* feat: Orgs Schema Changing `scopedMembers` to `orgUsers` (#9209)
* Change scopedMembers to orgMembers
* Change to orgUsers
* Letting duplicate slugs for teams to support orgs
* Covering null on unique clauses
* Supporting having the orgId in the session cookie
* feat: organization event type filter (#9253)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* Missing changes to support orgs schema changes
* feat: Onboarding process to create an organization (#9184)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Making sure we check requestedSlug now
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* feat: [CAL-1816] Organization subdomain support (#9345)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* Covering users and subteams, excluding non-org users
* Unpublished teams shows correctly
* Create subdomain in Vercel
* feedback
* Renaming Vercel env vars
* Vercel domain check before creation
* Supporting cal-staging.com
* Change to have vercel detect it
* vercel domain check data message error
* Remove check domain
* Making sure we check requestedSlug now
* Feedback and unneeded code
* Reverting unneeded changes
* Unneeded changes
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Vercel subdomain creation in PROD only
* Making sure we let localhost still work
* Feedback
* Type check fixes
* feat: Organization branding in side menu (#9279)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Making sure we show the set up profile on org only
* Profile username availability rely on org hook
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* feat: Organization support for event types page (#9449)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Processing orgs and children as profile options
* Reverting change not belonging to this PR
* Making sure we show the set up profile on org only
* Removing console.log
* Comparing memberships to choose the highest one
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Type errors
* Refactor and type fixes
* Update orgDomains.ts
* Feedback
* Reverting
* NIT
* fix issue getting org slug from domain
* Improving orgDomains util
* Host comes with port
* Update useRouterQuery.ts
* add org users cache-calendar
* fix typo
* re-use the simple user calendar-cache page
* Apply suggestions from code review
* Update packages/core/CalendarManager.ts
* type fixes
---------
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2023-06-14 21:40:47 -03:00
organization : {
select : {
slug : true ,
} ,
} ,
2023-04-20 12:55:19 -03:00
} ,
} ,
2023-01-12 18:09:12 -03:00
} ,
} ,
2022-10-12 10:04:51 -03:00
availability : {
select : {
2022-12-14 14:30:55 -03:00
date : true ,
2022-10-12 10:04:51 -03:00
startTime : true ,
endTime : true ,
days : true ,
} ,
} ,
} ,
} ) ;
return {
. . . eventType ,
2023-05-24 20:35:44 -03:00
metadata : EventTypeMetaDataSchema.parse ( eventType ? . metadata || { } ) ,
recurringEvent : parseRecurringEvent ( eventType ? . recurringEvent ) ,
customInputs : customInputSchema.array ( ) . parse ( eventType ? . customInputs || [ ] ) ,
locations : ( eventType ? . locations ? ? [ ] ) as LocationObject [ ] ,
bookingFields : getBookingFieldsWithSystemFields ( eventType || { } ) ,
2023-07-27 05:52:46 -03:00
isDynamic : false ,
2022-10-12 10:04:51 -03:00
} ;
} ;
feat: Organizations users calendar cache (#9498)
* Initial commit
* Adding feature flag
* feat: Orgs Schema Changing `scopedMembers` to `orgUsers` (#9209)
* Change scopedMembers to orgMembers
* Change to orgUsers
* Letting duplicate slugs for teams to support orgs
* Covering null on unique clauses
* Supporting having the orgId in the session cookie
* feat: organization event type filter (#9253)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* Missing changes to support orgs schema changes
* feat: Onboarding process to create an organization (#9184)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Making sure we check requestedSlug now
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* feat: [CAL-1816] Organization subdomain support (#9345)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* Covering users and subteams, excluding non-org users
* Unpublished teams shows correctly
* Create subdomain in Vercel
* feedback
* Renaming Vercel env vars
* Vercel domain check before creation
* Supporting cal-staging.com
* Change to have vercel detect it
* vercel domain check data message error
* Remove check domain
* Making sure we check requestedSlug now
* Feedback and unneeded code
* Reverting unneeded changes
* Unneeded changes
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Vercel subdomain creation in PROD only
* Making sure we let localhost still work
* Feedback
* Type check fixes
* feat: Organization branding in side menu (#9279)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Making sure we show the set up profile on org only
* Profile username availability rely on org hook
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* feat: Organization support for event types page (#9449)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Processing orgs and children as profile options
* Reverting change not belonging to this PR
* Making sure we show the set up profile on org only
* Removing console.log
* Comparing memberships to choose the highest one
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Type errors
* Refactor and type fixes
* Update orgDomains.ts
* Feedback
* Reverting
* NIT
* fix issue getting org slug from domain
* Improving orgDomains util
* Host comes with port
* Update useRouterQuery.ts
* add org users cache-calendar
* fix typo
* re-use the simple user calendar-cache page
* Apply suggestions from code review
* Update packages/core/CalendarManager.ts
* type fixes
---------
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2023-06-14 21:40:47 -03:00
type IsFixedAwareUser = User & {
isFixed : boolean ;
2023-09-14 13:53:58 -03:00
credentials : CredentialPayload [ ] ;
feat: Organizations users calendar cache (#9498)
* Initial commit
* Adding feature flag
* feat: Orgs Schema Changing `scopedMembers` to `orgUsers` (#9209)
* Change scopedMembers to orgMembers
* Change to orgUsers
* Letting duplicate slugs for teams to support orgs
* Covering null on unique clauses
* Supporting having the orgId in the session cookie
* feat: organization event type filter (#9253)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* Missing changes to support orgs schema changes
* feat: Onboarding process to create an organization (#9184)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Making sure we check requestedSlug now
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* feat: [CAL-1816] Organization subdomain support (#9345)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* Covering users and subteams, excluding non-org users
* Unpublished teams shows correctly
* Create subdomain in Vercel
* feedback
* Renaming Vercel env vars
* Vercel domain check before creation
* Supporting cal-staging.com
* Change to have vercel detect it
* vercel domain check data message error
* Remove check domain
* Making sure we check requestedSlug now
* Feedback and unneeded code
* Reverting unneeded changes
* Unneeded changes
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Vercel subdomain creation in PROD only
* Making sure we let localhost still work
* Feedback
* Type check fixes
* feat: Organization branding in side menu (#9279)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Making sure we show the set up profile on org only
* Profile username availability rely on org hook
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* feat: Organization support for event types page (#9449)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Processing orgs and children as profile options
* Reverting change not belonging to this PR
* Making sure we show the set up profile on org only
* Removing console.log
* Comparing memberships to choose the highest one
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Type errors
* Refactor and type fixes
* Update orgDomains.ts
* Feedback
* Reverting
* NIT
* fix issue getting org slug from domain
* Improving orgDomains util
* Host comes with port
* Update useRouterQuery.ts
* add org users cache-calendar
* fix typo
* re-use the simple user calendar-cache page
* Apply suggestions from code review
* Update packages/core/CalendarManager.ts
* type fixes
---------
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2023-06-14 21:40:47 -03:00
organization : { slug : string } ;
} ;
2023-01-12 18:09:12 -03:00
2023-12-19 14:42:40 -03:00
const loadUsers = async ( eventType : NewBookingEventType , dynamicUserList : string [ ] , req : IncomingMessage ) = > {
2023-11-29 12:11:09 -03:00
try {
if ( ! eventType . id ) {
if ( ! Array . isArray ( dynamicUserList ) || dynamicUserList . length === 0 ) {
throw new Error ( "dynamicUserList is not properly defined or empty." ) ;
}
const users = await prisma . user . findMany ( {
where : {
username : { in : dynamicUserList } ,
2023-12-19 14:42:40 -03:00
organization : userOrgQuery ( req ) ,
2023-11-29 12:11:09 -03:00
} ,
select : {
. . . userSelect . select ,
credentials : {
select : credentialForCalendarServiceSelect ,
} ,
metadata : true ,
} ,
} ) ;
return users ;
}
const hosts = eventType . hosts || [ ] ;
if ( ! Array . isArray ( hosts ) ) {
throw new Error ( "eventType.hosts is not properly defined." ) ;
}
const users = hosts . map ( ( { user , isFixed } ) = > ( {
. . . user ,
isFixed ,
} ) ) ;
return users . length ? users : eventType.users ;
} catch ( error ) {
if ( error instanceof HttpError || error instanceof Prisma . PrismaClientKnownRequestError ) {
throw new HttpError ( { statusCode : 400 , message : error.message } ) ;
}
throw new HttpError ( { statusCode : 500 , message : "Unable to load users" } ) ;
}
} ;
2023-12-20 16:28:24 -03:00
export async function ensureAvailableUsers (
2022-10-12 10:04:51 -03:00
eventType : Awaited < ReturnType < typeof getEventTypesFromDB > > & {
2023-01-12 18:09:12 -03:00
users : IsFixedAwareUser [ ] ;
2022-10-12 10:04:51 -03:00
} ,
2023-11-15 16:52:19 -03:00
input : { dateFrom : string ; dateTo : string ; timeZone : string ; originalRescheduledBooking? : BookingType } ,
loggerWithEventDetails : Logger < unknown >
2022-10-12 10:04:51 -03:00
) {
2023-01-12 18:09:12 -03:00
const availableUsers : IsFixedAwareUser [ ] = [ ] ;
2023-09-21 03:22:05 -03:00
const duration = dayjs ( input . dateTo ) . diff ( input . dateFrom , "minute" ) ;
2023-08-10 16:07:57 -03:00
2023-08-31 14:47:02 -03:00
const originalBookingDuration = input . originalRescheduledBooking
2023-08-10 16:07:57 -03:00
? dayjs ( input . originalRescheduledBooking . endTime ) . diff (
dayjs ( input . originalRescheduledBooking . startTime ) ,
"minutes"
)
: undefined ;
2022-10-12 10:04:51 -03:00
/** Let's start checking for availability */
for ( const user of eventType . users ) {
2023-07-05 13:47:41 -03:00
const { dateRanges , busy : bufferedBusyTimes } = await getUserAvailability (
2022-10-12 10:04:51 -03:00
{
userId : user.id ,
eventTypeId : eventType.id ,
2023-08-31 14:47:02 -03:00
duration : originalBookingDuration ,
2022-10-12 10:04:51 -03:00
. . . input ,
} ,
2023-08-10 16:07:57 -03:00
{
user ,
eventType ,
rescheduleUid : input.originalRescheduledBooking?.uid ? ? null ,
}
2022-10-12 10:04:51 -03:00
) ;
2023-09-28 10:01:24 -03:00
log . debug (
"calendarBusyTimes==>>>" ,
JSON . stringify ( { bufferedBusyTimes , dateRanges , isRecurringEvent : eventType.recurringEvent } )
) ;
2023-07-05 13:47:41 -03:00
if ( ! dateRanges . length ) {
2022-11-04 08:36:11 -03:00
// user does not have availability at this time, skip user.
continue ;
}
2022-11-08 13:12:31 -03:00
let foundConflict = false ;
2023-11-15 16:49:03 -03:00
let dateRangeForBooking = false ;
//check if event time is within the date range
for ( const dateRange of dateRanges ) {
if (
( dayjs . utc ( input . dateFrom ) . isAfter ( dateRange . start ) ||
dayjs . utc ( input . dateFrom ) . isSame ( dateRange . start ) ) &&
( dayjs . utc ( input . dateTo ) . isBefore ( dateRange . end ) || dayjs . utc ( input . dateTo ) . isSame ( dateRange . end ) )
) {
dateRangeForBooking = true ;
break ;
}
}
if ( ! dateRangeForBooking ) {
continue ;
}
2022-10-12 10:04:51 -03:00
try {
2023-11-07 12:09:56 -03:00
foundConflict = checkForConflicts ( bufferedBusyTimes , input . dateFrom , duration ) ;
2022-10-12 10:04:51 -03:00
} catch {
log . debug ( {
message : "Unable set isAvailableToBeBooked. Using true. " ,
} ) ;
}
2022-11-04 08:36:11 -03:00
// no conflicts found, add to available users.
if ( ! foundConflict ) {
2022-10-12 10:04:51 -03:00
availableUsers . push ( user ) ;
}
}
if ( ! availableUsers . length ) {
2023-11-15 16:52:19 -03:00
loggerWithEventDetails . error ( ` No available users found. ` ) ;
throw new Error ( ErrorCode . NoAvailableUsersFound ) ;
2022-10-12 10:04:51 -03:00
}
return availableUsers ;
}
2023-03-14 01:19:05 -03:00
async function getOriginalRescheduledBooking ( uid : string , seatsEventType? : boolean ) {
return 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 ,
. . . ( seatsEventType && { bookingSeat : true , id : true } ) ,
} ,
} ,
user : {
select : {
id : true ,
name : true ,
email : true ,
locale : true ,
timeZone : true ,
2023-11-27 15:09:33 -03:00
destinationCalendar : true ,
credentials : {
select : {
id : true ,
userId : true ,
key : true ,
type : true ,
teamId : true ,
appId : true ,
invalid : true ,
user : {
select : {
email : true ,
} ,
} ,
} ,
} ,
2023-03-14 01:19:05 -03:00
} ,
} ,
2023-11-27 15:09:33 -03:00
destinationCalendar : true ,
2023-03-14 01:19:05 -03:00
payment : true ,
references : true ,
workflowReminders : true ,
} ,
} ) ;
}
2023-12-19 16:01:42 -03:00
export async function getBookingData ( {
2023-03-02 15:15:28 -03:00
req ,
isNotAnApiCall ,
eventType ,
} : {
req : NextApiRequest ;
isNotAnApiCall : boolean ;
eventType : Awaited < ReturnType < typeof getEventTypesFromDB > > ;
} ) {
2023-11-29 12:11:09 -03:00
const bookingDataSchema = getBookingDataSchema ( req . body ? . rescheduleUid , isNotAnApiCall , eventType ) ;
2023-06-02 16:29:52 -03:00
2023-08-17 14:57:30 -03:00
const reqBody = await bookingDataSchema . parseAsync ( req . body ) ;
2023-06-02 16:29:52 -03:00
const reqBodyWithEnd = ( reqBody : ReqBodyWithoutEnd ) : reqBody is ReqBodyWithEnd = > {
// Use the event length to auto-set the event end time.
if ( ! Object . prototype . hasOwnProperty . call ( reqBody , "end" ) ) {
reqBody . end = dayjs . utc ( reqBody . start ) . add ( eventType . length , "minutes" ) . format ( ) ;
}
return true ;
} ;
if ( ! reqBodyWithEnd ( reqBody ) ) {
2023-11-15 16:52:19 -03:00
throw new Error ( ErrorCode . RequestBodyWithouEnd ) ;
2023-06-02 16:29:52 -03:00
}
// reqBody.end is no longer an optional property.
2023-04-18 09:35:06 -03:00
if ( "customInputs" in reqBody ) {
if ( reqBody . customInputs ) {
// Check if required custom inputs exist
handleCustomInputs ( eventType . customInputs as EventTypeCustomInput [ ] , reqBody . customInputs ) ;
}
const reqBodyWithLegacyProps = bookingCreateSchemaLegacyPropsForApi . parse ( reqBody ) ;
return {
. . . reqBody ,
name : reqBodyWithLegacyProps.name ,
email : reqBodyWithLegacyProps.email ,
guests : reqBodyWithLegacyProps.guests ,
location : reqBodyWithLegacyProps.location || "" ,
smsReminderNumber : reqBodyWithLegacyProps.smsReminderNumber ,
notes : reqBodyWithLegacyProps.notes ,
rescheduleReason : reqBodyWithLegacyProps.rescheduleReason ,
} ;
} else {
if ( ! reqBody . responses ) {
throw new Error ( "`responses` must not be nullish" ) ;
}
2023-03-02 15:15:28 -03:00
const responses = reqBody . responses ;
2023-07-20 02:03:50 -03:00
2023-04-04 01:59:09 -03:00
const { userFieldsResponses : calEventUserFieldsResponses , responses : calEventResponses } =
getCalEventResponses ( {
bookingFields : eventType.bookingFields ,
responses ,
} ) ;
2023-03-02 15:15:28 -03:00
return {
. . . reqBody ,
name : responses.name ,
email : responses.email ,
guests : responses.guests ? responses . guests : [ ] ,
location : responses.location?.optionValue || responses . location ? . value || "" ,
smsReminderNumber : responses.smsReminderNumber ,
notes : responses.notes || "" ,
2023-03-07 14:50:54 -03:00
calEventUserFieldsResponses ,
2023-03-02 15:15:28 -03:00
rescheduleReason : responses.rescheduleReason ,
2023-03-07 14:50:54 -03:00
calEventResponses ,
2023-03-02 15:15:28 -03:00
} ;
}
}
2023-11-29 12:11:09 -03:00
async function createBooking ( {
originalRescheduledBooking ,
evt ,
eventTypeId ,
eventTypeSlug ,
reqBodyUser ,
reqBodyMetadata ,
reqBodyRecurringEventId ,
uid ,
responses ,
isConfirmedByDefault ,
smsReminderNumber ,
organizerUser ,
rescheduleReason ,
eventType ,
bookerEmail ,
paymentAppData ,
changedOrganizer ,
} : {
originalRescheduledBooking : Awaited < ReturnType < typeof getOriginalRescheduledBooking > > ;
evt : CalendarEvent ;
eventType : NewBookingEventType ;
eventTypeId : Awaited < ReturnType < typeof getBookingData > > [ "eventTypeId" ] ;
eventTypeSlug : Awaited < ReturnType < typeof getBookingData > > [ "eventTypeSlug" ] ;
reqBodyUser : ReqBodyWithEnd [ "user" ] ;
reqBodyMetadata : ReqBodyWithEnd [ "metadata" ] ;
reqBodyRecurringEventId : ReqBodyWithEnd [ "recurringEventId" ] ;
uid : short.SUUID ;
responses : ReqBodyWithEnd [ "responses" ] | null ;
isConfirmedByDefault : ReturnType < typeof getRequiresConfirmationFlags > [ "isConfirmedByDefault" ] ;
smsReminderNumber : Awaited < ReturnType < typeof getBookingData > > [ "smsReminderNumber" ] ;
organizerUser : Awaited < ReturnType < typeof loadUsers > > [ number ] & {
isFixed? : boolean ;
metadata? : Prisma.JsonValue ;
} ;
rescheduleReason : Awaited < ReturnType < typeof getBookingData > > [ "rescheduleReason" ] ;
bookerEmail : Awaited < ReturnType < typeof getBookingData > > [ "email" ] ;
paymentAppData : ReturnType < typeof getPaymentAppData > ;
changedOrganizer : boolean ;
} ) {
if ( originalRescheduledBooking ) {
evt . title = originalRescheduledBooking ? . title || evt . title ;
evt . description = originalRescheduledBooking ? . description || evt . description ;
evt . location = originalRescheduledBooking ? . location || evt . location ;
evt . location = changedOrganizer ? evt.location : originalRescheduledBooking?.location || evt . location ;
}
const eventTypeRel = ! eventTypeId
? { }
: {
connect : {
id : eventTypeId ,
} ,
} ;
const dynamicEventSlugRef = ! eventTypeId ? eventTypeSlug : null ;
const dynamicGroupSlugRef = ! eventTypeId ? ( reqBodyUser as string ) . toLowerCase ( ) : null ;
const attendeesData = evt . attendees . map ( ( attendee ) = > {
//if attendee is team member, it should fetch their locale not booker's locale
//perhaps make email fetch request to see if his locale is stored, else
return {
name : attendee.name ,
email : attendee.email ,
timeZone : attendee.timeZone ,
locale : attendee.language.locale ,
} ;
} ) ;
if ( evt . team ? . members ) {
attendeesData . push (
. . . evt . team . members . map ( ( member ) = > ( {
email : member.email ,
name : member.name ,
timeZone : member.timeZone ,
locale : member.language.locale ,
} ) )
) ;
}
const newBookingData : Prisma.BookingCreateInput = {
uid ,
responses : responses === null ? Prisma.JsonNull : responses ,
title : evt.title ,
startTime : dayjs.utc ( evt . startTime ) . toDate ( ) ,
endTime : dayjs.utc ( evt . endTime ) . toDate ( ) ,
description : evt.additionalNotes ,
customInputs : isPrismaObjOrUndefined ( evt . customInputs ) ,
status : isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING ,
location : evt.location ,
eventType : eventTypeRel ,
smsReminderNumber ,
metadata : reqBodyMetadata ,
attendees : {
createMany : {
data : attendeesData ,
} ,
} ,
dynamicEventSlugRef ,
dynamicGroupSlugRef ,
2023-12-15 12:28:32 -03:00
iCalUID : evt.iCalUID ? ? "" ,
2023-11-29 12:11:09 -03:00
user : {
connect : {
id : organizerUser.id ,
} ,
} ,
destinationCalendar :
evt . destinationCalendar && evt . destinationCalendar . length > 0
? {
connect : { id : evt.destinationCalendar [ 0 ] . id } ,
}
: undefined ,
} ;
if ( reqBodyRecurringEventId ) {
newBookingData . recurringEventId = reqBodyRecurringEventId ;
}
if ( originalRescheduledBooking ) {
newBookingData . metadata = {
. . . ( typeof originalRescheduledBooking . metadata === "object" && originalRescheduledBooking . metadata ) ,
} ;
newBookingData [ "paid" ] = originalRescheduledBooking . paid ;
newBookingData [ "fromReschedule" ] = originalRescheduledBooking . uid ;
if ( originalRescheduledBooking . uid ) {
newBookingData . cancellationReason = rescheduleReason ;
}
if ( newBookingData . attendees ? . createMany ? . data ) {
// Reschedule logic with booking with seats
if ( eventType ? . seatsPerTimeSlot && bookerEmail ) {
newBookingData . attendees . createMany . data = attendeesData . filter (
( attendee ) = > attendee . email === bookerEmail
) ;
}
}
if ( originalRescheduledBooking . recurringEventId ) {
newBookingData . recurringEventId = originalRescheduledBooking . recurringEventId ;
}
}
const createBookingObj = {
include : {
user : {
select : { email : true , name : true , timeZone : true , username : true } ,
} ,
attendees : true ,
payment : true ,
references : true ,
} ,
data : newBookingData ,
} ;
if ( originalRescheduledBooking ? . paid && originalRescheduledBooking ? . payment ) {
const bookingPayment = originalRescheduledBooking ? . payment ? . find ( ( payment ) = > payment . success ) ;
if ( bookingPayment ) {
createBookingObj . data . payment = {
connect : { id : bookingPayment.id } ,
} ;
}
}
if ( typeof paymentAppData . price === "number" && paymentAppData . price > 0 ) {
/* Validate if there is any payment app credential for this user */
await prisma . credential . findFirstOrThrow ( {
where : {
appId : paymentAppData.appId ,
. . . ( paymentAppData . credentialId ? { id : paymentAppData.credentialId } : { userId : organizerUser.id } ) ,
} ,
select : {
id : true ,
} ,
} ) ;
}
return prisma . booking . create ( createBookingObj ) ;
}
2023-12-19 16:01:42 -03:00
export function getCustomInputsResponses (
2023-03-02 15:15:28 -03:00
reqBody : {
2023-06-06 08:59:57 -03:00
responses? : Record < string , object > ;
2023-03-02 15:15:28 -03:00
customInputs? : z.infer < typeof bookingCreateSchemaLegacyPropsForApi > [ "customInputs" ] ;
} ,
eventTypeCustomInputs : Awaited < ReturnType < typeof getEventTypesFromDB > > [ "customInputs" ]
) {
const customInputsResponses = { } as NonNullable < CalendarEvent [ " customInputs " ] > ;
if ( "customInputs" in reqBody ) {
const reqCustomInputsResponses = reqBody . customInputs || [ ] ;
if ( reqCustomInputsResponses ? . length > 0 ) {
reqCustomInputsResponses . forEach ( ( { label , value } ) = > {
customInputsResponses [ label ] = value ;
} ) ;
}
} else {
const responses = reqBody . responses || { } ;
// Backward Compatibility: Map new `responses` to old `customInputs` format so that webhooks can still receive same values.
for ( const [ fieldName , fieldValue ] of Object . entries ( responses ) ) {
const foundACustomInputForTheResponse = eventTypeCustomInputs . find (
( input ) = > slugify ( input . label ) === fieldName
) ;
if ( foundACustomInputForTheResponse ) {
customInputsResponses [ foundACustomInputForTheResponse . label ] = fieldValue ;
}
}
}
return customInputsResponses ;
}
2023-12-15 12:28:32 -03:00
function getICalSequence ( originalRescheduledBooking : BookingType | null ) {
// If new booking set the sequence to 0
if ( ! originalRescheduledBooking ) {
return 0 ;
}
// If rescheduling and there is no sequence set, assume sequence should be 1
if ( ! originalRescheduledBooking . iCalSequence ) {
return 1 ;
}
// If rescheduling then increment sequence by 1
return originalRescheduledBooking . iCalSequence + 1 ;
}
2023-03-02 15:15:28 -03:00
async function handler (
req : NextApiRequest & { userId? : number | undefined } ,
{
isNotAnApiCall = false ,
2023-11-09 08:30:51 -03:00
skipAvailabilityCheck = false ,
2023-03-02 15:15:28 -03:00
} : {
isNotAnApiCall? : boolean ;
2023-11-09 08:30:51 -03:00
skipAvailabilityCheck? : boolean ;
2023-03-02 15:15:28 -03:00
} = {
isNotAnApiCall : false ,
2023-11-09 08:30:51 -03:00
skipAvailabilityCheck : false ,
2023-03-02 15:15:28 -03:00
}
) {
2022-10-12 10:04:51 -03:00
const { userId } = req ;
2023-06-26 16:44:58 -03:00
2023-03-02 15:15:28 -03:00
// handle dynamic user
let eventType =
! req . body . eventTypeId && ! ! req . body . eventTypeSlug
? getDefaultEvent ( req . body . eventTypeSlug )
: await getEventTypesFromDB ( req . body . eventTypeId ) ;
eventType = {
. . . eventType ,
bookingFields : getBookingFieldsWithSystemFields ( eventType ) ,
} ;
2022-10-19 13:11:50 -03:00
const {
recurringCount ,
noEmail ,
eventTypeId ,
2023-03-02 15:15:28 -03:00
eventTypeSlug ,
2022-10-19 13:11:50 -03:00
hasHashedBookingLink ,
language ,
appsStatus : reqAppsStatus ,
2023-03-02 15:15:28 -03:00
name : bookerName ,
email : bookerEmail ,
guests : reqGuests ,
location ,
notes : additionalNotes ,
smsReminderNumber ,
rescheduleReason ,
2022-10-19 13:11:50 -03:00
. . . reqBody
2023-08-17 14:57:30 -03:00
} = await getBookingData ( {
2023-03-02 15:15:28 -03:00
req ,
isNotAnApiCall ,
eventType ,
} ) ;
2022-10-12 10:04:51 -03:00
2023-10-17 16:00:48 -03:00
const loggerWithEventDetails = logger . getSubLogger ( {
2023-09-28 10:01:24 -03:00
prefix : [ "book:user" , ` ${ eventTypeId } : ${ reqBody . user } / ${ eventTypeSlug } ` ] ,
} ) ;
2023-10-02 07:51:04 -03:00
if ( isEventTypeLoggingEnabled ( { eventTypeId , usernameOrTeamName : reqBody.user } ) ) {
2023-10-17 16:00:48 -03:00
logger . settings . minLevel = 0 ;
2023-09-28 10:01:24 -03:00
}
2023-07-20 02:03:50 -03:00
const fullName = getFullName ( bookerName ) ;
2023-11-15 16:52:19 -03:00
// Why are we only using "en" locale
2022-10-12 10:04:51 -03:00
const tGuests = await getTranslation ( "en" , "common" ) ;
2023-10-02 07:51:04 -03:00
2023-08-07 19:08:13 -03:00
const dynamicUserList = Array . isArray ( reqBody . user ) ? reqBody.user : getUsernameList ( reqBody . user ) ;
2023-11-15 16:52:19 -03:00
if ( ! eventType ) throw new HttpError ( { statusCode : 404 , message : "event_type_not_found" } ) ;
2022-10-12 10:04:51 -03:00
2023-02-27 17:45:40 -03:00
const isTeamEventType =
2023-08-31 14:47:02 -03:00
! ! eventType . schedulingType && [ "COLLECTIVE" , "ROUND_ROBIN" ] . includes ( eventType . schedulingType ) ;
2023-02-27 17:45:40 -03:00
2023-02-08 17:36:22 -03:00
const paymentAppData = getPaymentAppData ( eventType ) ;
2023-10-02 07:51:04 -03:00
loggerWithEventDetails . debug (
` Booking eventType ${ eventTypeId } started ` ,
safeStringify ( {
reqBody : {
user : reqBody.user ,
eventTypeId ,
eventTypeSlug ,
startTime : reqBody.start ,
endTime : reqBody.end ,
rescheduleUid : reqBody.rescheduleUid ,
location : location ,
} ,
isTeamEventType ,
eventType : getPiiFreeEventType ( eventType ) ,
dynamicUserList ,
2023-11-09 08:30:51 -03:00
skipAvailabilityCheck ,
2023-10-02 07:51:04 -03:00
paymentAppData : {
enabled : paymentAppData.enabled ,
price : paymentAppData.price ,
paymentOption : paymentAppData.paymentOption ,
currency : paymentAppData.currency ,
appId : paymentAppData.appId ,
} ,
} )
) ;
2022-10-14 13:24:43 -03:00
2022-10-12 10:04:51 -03:00
let timeOutOfBounds = false ;
try {
timeOutOfBounds = isOutOfBounds ( reqBody . start , {
periodType : eventType.periodType ,
periodDays : eventType.periodDays ,
periodEndDate : eventType.periodEndDate ,
periodStartDate : eventType.periodStartDate ,
periodCountCalendarDays : eventType.periodCountCalendarDays ,
} ) ;
} catch ( error ) {
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . warn ( {
2023-03-23 15:03:49 -03:00
message : "NewBooking: Unable set timeOutOfBounds. Using false. " ,
} ) ;
2022-10-12 10:04:51 -03:00
if ( error instanceof BookingDateInPastError ) {
// TODO: HttpError should not bleed through to the console.
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . info ( ` Booking eventType ${ eventTypeId } failed ` , JSON . stringify ( { error } ) ) ;
2022-10-12 10:04:51 -03:00
throw new HttpError ( { statusCode : 400 , message : error.message } ) ;
}
}
if ( timeOutOfBounds ) {
const error = {
errorCode : "BookingTimeOutOfBounds" ,
message : ` EventType ' ${ eventType . eventName } ' cannot be booked at this time. ` ,
} ;
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . warn ( {
2023-03-23 15:03:49 -03:00
message : ` NewBooking: EventType ' ${ eventType . eventName } ' cannot be booked at this time. ` ,
} ) ;
2022-10-12 10:04:51 -03:00
throw new HttpError ( { statusCode : 400 , message : error.message } ) ;
}
2023-01-12 18:09:12 -03:00
// loadUsers allows type inferring
2023-02-15 12:42:49 -03:00
let users : ( Awaited < ReturnType < typeof loadUsers > > [ number ] & {
isFixed? : boolean ;
metadata? : Prisma.JsonValue ;
2023-12-19 14:42:40 -03:00
} ) [ ] = await loadUsers ( eventType , dynamicUserList , req ) ;
2023-01-12 18:09:12 -03:00
2022-10-12 10:04:51 -03:00
const isDynamicAllowed = ! users . some ( ( user ) = > ! user . allowDynamicBooking ) ;
if ( ! isDynamicAllowed && ! eventTypeId ) {
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . warn ( {
message : "NewBooking: Some of the users in this group do not allow dynamic booking" ,
} ) ;
2022-10-12 10:04:51 -03:00
throw new HttpError ( {
message : "Some of the users in this group do not allow dynamic booking" ,
statusCode : 400 ,
} ) ;
}
// If this event was pre-relationship migration
// TODO: Establish whether this is dead code.
if ( ! users . length && eventType . userId ) {
const eventTypeUser = await prisma . user . findUnique ( {
where : {
id : eventType.userId ,
} ,
2023-04-20 12:55:19 -03:00
select : {
2023-09-14 13:53:58 -03:00
credentials : {
select : credentialForCalendarServiceSelect ,
} , // Don't leak to client
2023-04-20 12:55:19 -03:00
. . . userSelect . select ,
} ,
2022-10-12 10:04:51 -03:00
} ) ;
2023-03-23 15:03:49 -03:00
if ( ! eventTypeUser ) {
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . warn ( { message : "NewBooking: eventTypeUser.notFound" } ) ;
2023-03-23 15:03:49 -03:00
throw new HttpError ( { statusCode : 404 , message : "eventTypeUser.notFound" } ) ;
}
2022-10-12 10:04:51 -03:00
users . push ( eventTypeUser ) ;
}
if ( ! users ) throw new HttpError ( { statusCode : 404 , message : "eventTypeUser.notFound" } ) ;
2023-01-12 18:09:12 -03:00
users = users . map ( ( user ) = > ( {
. . . user ,
isFixed :
user . isFixed === false
? false
: user . isFixed || eventType . schedulingType !== SchedulingType . ROUND_ROBIN ,
} ) ) ;
2023-10-02 07:51:04 -03:00
loggerWithEventDetails . debug (
"Concerned users" ,
safeStringify ( {
users : users.map ( getPiiFreeUser ) ,
} )
) ;
2023-03-16 15:12:30 -03:00
let locationBodyString = location ;
2023-06-23 14:04:34 -03:00
// TODO: It's definition should be moved to getLocationValueForDb
let organizerOrFirstDynamicGroupMemberDefaultLocationUrl = undefined ;
2023-03-28 17:03:54 -03:00
2023-03-16 15:12:30 -03:00
if ( dynamicUserList . length > 1 ) {
users = users . sort ( ( a , b ) = > {
const aIndex = ( a . username && dynamicUserList . indexOf ( a . username ) ) || 0 ;
const bIndex = ( b . username && dynamicUserList . indexOf ( b . username ) ) || 0 ;
return aIndex - bIndex ;
} ) ;
const firstUsersMetadata = userMetadataSchema . parse ( users [ 0 ] . metadata ) ;
2023-04-20 08:38:57 -03:00
locationBodyString = firstUsersMetadata ? . defaultConferencingApp ? . appLink || locationBodyString ;
2023-06-23 14:04:34 -03:00
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
firstUsersMetadata ? . defaultConferencingApp ? . appLink ;
2023-03-16 15:12:30 -03:00
}
2023-06-02 16:29:52 -03:00
if (
Object . prototype . hasOwnProperty . call ( eventType , "bookingLimits" ) ||
Object . prototype . hasOwnProperty . call ( eventType , "durationLimits" )
) {
2023-03-10 17:00:19 -03:00
const startAsDate = dayjs ( reqBody . start ) . toDate ( ) ;
2023-06-02 16:29:52 -03:00
if ( eventType . bookingLimits ) {
2023-11-07 14:17:05 -03:00
await checkBookingLimits (
eventType . bookingLimits as IntervalLimit ,
startAsDate ,
eventType . id ,
eventType . schedule ? . timeZone
) ;
2023-06-02 16:29:52 -03:00
}
if ( eventType . durationLimits ) {
2023-06-06 08:59:57 -03:00
await checkDurationLimits ( eventType . durationLimits as IntervalLimit , startAsDate , eventType . id ) ;
2023-06-02 16:29:52 -03:00
}
2023-03-10 17:00:19 -03:00
}
2023-08-10 16:07:57 -03:00
let rescheduleUid = reqBody . rescheduleUid ;
let bookingSeat : Prisma.BookingSeatGetPayload < { include : { booking : true ; attendee : true } } > | null = null ;
let originalRescheduledBooking : BookingType = null ;
2023-11-27 15:09:33 -03:00
//this gets the orginal rescheduled booking
2023-08-10 16:07:57 -03:00
if ( rescheduleUid ) {
// rescheduleUid can be bookingUid and bookingSeatUid
bookingSeat = await prisma . bookingSeat . findUnique ( {
where : {
referenceUid : rescheduleUid ,
} ,
include : {
booking : true ,
attendee : true ,
} ,
} ) ;
if ( bookingSeat ) {
rescheduleUid = bookingSeat . booking . uid ;
}
originalRescheduledBooking = await getOriginalRescheduledBooking (
rescheduleUid ,
! ! eventType . seatsPerTimeSlot
) ;
if ( ! originalRescheduledBooking ) {
throw new HttpError ( { statusCode : 404 , message : "Could not find original booking" } ) ;
}
}
2023-11-27 15:09:33 -03:00
//checks what users are available
2023-11-09 08:30:51 -03:00
if ( ! eventType . seatsPerTimeSlot && ! skipAvailabilityCheck ) {
2022-10-12 10:04:51 -03:00
const availableUsers = await ensureAvailableUsers (
{
. . . eventType ,
2023-01-12 18:09:12 -03:00
users : users as IsFixedAwareUser [ ] ,
2022-11-08 17:59:44 -03:00
. . . ( eventType . recurringEvent && {
recurringEvent : {
. . . eventType . recurringEvent ,
count : recurringCount || eventType . recurringEvent . count ,
} ,
} ) ,
2022-10-12 10:04:51 -03:00
} ,
{
2023-11-07 14:17:05 -03:00
dateFrom : dayjs ( reqBody . start ) . tz ( reqBody . timeZone ) . format ( ) ,
dateTo : dayjs ( reqBody . end ) . tz ( reqBody . timeZone ) . format ( ) ,
2023-03-03 13:33:16 -03:00
timeZone : reqBody.timeZone ,
2023-08-10 16:07:57 -03:00
originalRescheduledBooking ,
2023-11-15 16:52:19 -03:00
} ,
loggerWithEventDetails
2022-10-12 10:04:51 -03:00
) ;
2023-01-12 18:09:12 -03:00
const luckyUsers : typeof users = [ ] ;
const luckyUserPool = availableUsers . filter ( ( user ) = > ! user . isFixed ) ;
2023-10-02 07:51:04 -03:00
loggerWithEventDetails . debug (
"Computed available users" ,
safeStringify ( {
availableUsers : availableUsers.map ( ( user ) = > user . id ) ,
luckyUserPool : luckyUserPool.map ( ( user ) = > user . id ) ,
} )
) ;
2023-01-12 18:09:12 -03:00
// loop through all non-fixed hosts and get the lucky users
while ( luckyUserPool . length > 0 && luckyUsers . length < 1 /* TODO: Add variable */ ) {
const newLuckyUser = await getLuckyUser ( "MAXIMIZE_AVAILABILITY" , {
// find a lucky user that is not already in the luckyUsers array
availableUsers : luckyUserPool.filter (
( user ) = > ! luckyUsers . find ( ( existing ) = > existing . id === user . id )
) ,
eventTypeId : eventType.id ,
} ) ;
if ( ! newLuckyUser ) {
break ; // prevent infinite loop
2022-10-12 10:04:51 -03:00
}
2023-01-12 18:09:12 -03:00
luckyUsers . push ( newLuckyUser ) ;
}
// ALL fixed users must be available
if (
availableUsers . filter ( ( user ) = > user . isFixed ) . length !== users . filter ( ( user ) = > user . isFixed ) . length
) {
2023-11-15 16:52:19 -03:00
throw new Error ( ErrorCode . HostsUnavailableForBooking ) ;
2022-10-12 10:04:51 -03:00
}
2023-02-23 22:22:18 -03:00
// Pushing fixed user before the luckyUser guarantees the (first) fixed user as the organizer.
users = [ . . . availableUsers . filter ( ( user ) = > user . isFixed ) , . . . luckyUsers ] ;
2022-10-12 10:04:51 -03:00
}
2023-01-12 18:09:12 -03:00
2022-10-12 10:04:51 -03:00
const [ organizerUser ] = users ;
2023-10-02 07:51:04 -03:00
2023-03-14 01:19:05 -03:00
const tOrganizer = await getTranslation ( organizerUser ? . locale ? ? "en" , "common" ) ;
2023-06-23 14:04:34 -03:00
2023-07-11 04:41:21 -03:00
const allCredentials = await getAllCredentials ( organizerUser , eventType ) ;
2023-10-13 14:22:57 -03:00
const { userReschedulingIsOwner , isConfirmedByDefault } = getRequiresConfirmationFlags ( {
eventType ,
bookingStartTime : reqBody.start ,
userId ,
originalRescheduledBookingOrganizerId : originalRescheduledBooking?.user?.id ,
paymentAppData ,
} ) ;
2023-08-10 13:05:35 -03:00
2023-10-13 14:22:57 -03:00
// If the Organizer himself is rescheduling, the booker should be sent the communication in his timezone and locale.
2023-08-10 13:05:35 -03:00
const attendeeInfoOnReschedule =
2023-10-13 14:22:57 -03:00
userReschedulingIsOwner && originalRescheduledBooking
2023-08-10 13:05:35 -03:00
? originalRescheduledBooking . attendees . find ( ( attendee ) = > attendee . email === bookerEmail )
: null ;
const attendeeLanguage = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.locale : language ;
const attendeeTimezone = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.timeZone : reqBody.timeZone ;
const tAttendees = await getTranslation ( attendeeLanguage ? ? "en" , "common" ) ;
2023-11-22 14:43:25 -03:00
const isManagedEventType = ! ! eventType . parentId ;
2023-03-28 17:03:54 -03:00
// use host default
2023-11-22 14:43:25 -03:00
if ( ( isManagedEventType || isTeamEventType ) && locationBodyString === OrganizerDefaultConferencingAppType ) {
2023-03-28 17:03:54 -03:00
const metadataParseResult = userMetadataSchema . safeParse ( organizerUser . metadata ) ;
const organizerMetadata = metadataParseResult . success ? metadataParseResult.data : undefined ;
2023-10-06 10:43:28 -03:00
if ( organizerMetadata ? . defaultConferencingApp ? . appSlug ) {
2023-03-28 17:03:54 -03:00
const app = getAppFromSlug ( organizerMetadata ? . defaultConferencingApp ? . appSlug ) ;
locationBodyString = app ? . appData ? . location ? . type || locationBodyString ;
2023-06-23 14:04:34 -03:00
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
organizerMetadata ? . defaultConferencingApp ? . appLink ;
2023-03-28 17:03:54 -03:00
} else {
locationBodyString = "" ;
}
}
2022-10-12 10:04:51 -03:00
const invitee = [
{
2023-03-02 15:15:28 -03:00
email : bookerEmail ,
2023-07-20 02:03:50 -03:00
name : fullName ,
2023-08-03 12:10:59 -03:00
firstName : ( typeof bookerName === "object" && bookerName . firstName ) || "" ,
lastName : ( typeof bookerName === "object" && bookerName . lastName ) || "" ,
2023-08-10 13:05:35 -03:00
timeZone : attendeeTimezone ,
language : { translate : tAttendees , locale : attendeeLanguage ? ? "en" } ,
2022-10-12 10:04:51 -03:00
} ,
] ;
2023-02-27 17:45:40 -03:00
2023-03-02 15:15:28 -03:00
const guests = ( reqGuests || [ ] ) . reduce ( ( guestArray , guest ) = > {
2023-02-27 17:45:40 -03:00
// If it's a team event, remove the team member from guests
2023-03-16 06:04:17 -03:00
if ( isTeamEventType && users . some ( ( user ) = > user . email === guest ) ) {
return guestArray ;
2023-02-27 17:45:40 -03:00
}
2023-03-16 06:04:17 -03:00
guestArray . push ( {
email : guest ,
name : "" ,
2023-08-03 12:10:59 -03:00
firstName : "" ,
lastName : "" ,
2023-08-10 13:05:35 -03:00
timeZone : attendeeTimezone ,
2023-03-16 06:04:17 -03:00
language : { translate : tGuests , locale : "en" } ,
} ) ;
2023-02-27 17:45:40 -03:00
return guestArray ;
} , [ ] as typeof invitee ) ;
2022-10-12 10:04:51 -03:00
const seed = ` ${ organizerUser . username } : ${ dayjs ( reqBody . start ) . utc ( ) . format ( ) } : ${ new Date ( ) . getTime ( ) } ` ;
const uid = translator . fromUUID ( uuidv5 ( seed , uuidv5 . URL ) ) ;
2023-06-23 14:04:34 -03:00
// For static link based video apps, it would have the static URL value instead of it's type(e.g. integrations:campfire_video)
// This ensures that createMeeting isn't called for static video apps as bookingLocation becomes just a regular value for them.
2023-08-02 00:54:28 -03:00
const { bookingLocation , conferenceCredentialId } = organizerOrFirstDynamicGroupMemberDefaultLocationUrl
? {
bookingLocation : organizerOrFirstDynamicGroupMemberDefaultLocationUrl ,
conferenceCredentialId : undefined ,
}
2023-06-23 14:04:34 -03:00
: getLocationValueForDB ( locationBodyString , eventType . locations ) ;
2023-04-04 01:59:09 -03:00
2023-03-02 15:15:28 -03:00
const customInputs = getCustomInputsResponses ( reqBody , eventType . customInputs ) ;
2023-08-31 14:47:02 -03:00
const teamDestinationCalendars : DestinationCalendar [ ] = [ ] ;
// Organizer or user owner of this event type it's not listed as a team member.
const teamMemberPromises = users . slice ( 1 ) . map ( async ( user ) = > {
// push to teamDestinationCalendars if it's a team event but collective only
if ( isTeamEventType && eventType . schedulingType === "COLLECTIVE" && user . destinationCalendar ) {
teamDestinationCalendars . push ( user . destinationCalendar ) ;
}
return {
2023-09-14 13:53:58 -03:00
id : user.id ,
2023-08-31 14:47:02 -03:00
email : user.email ? ? "" ,
name : user.name ? ? "" ,
firstName : "" ,
lastName : "" ,
timeZone : user.timeZone ,
language : {
translate : await getTranslation ( user . locale ? ? "en" , "common" ) ,
locale : user.locale ? ? "en" ,
} ,
} ;
} ) ;
2022-10-12 10:04:51 -03:00
const teamMembers = await Promise . all ( teamMemberPromises ) ;
2023-02-27 17:45:40 -03:00
const attendeesList = [ . . . invitee , . . . guests ] ;
2022-10-12 10:04:51 -03:00
2023-03-09 12:11:16 -03:00
const responses = "responses" in reqBody ? reqBody.responses : null ;
2023-07-27 05:52:46 -03:00
const evtName = ! eventType ? . isDynamic ? eventType.eventName : responses?.title ;
2022-10-12 10:04:51 -03:00
const eventNameObject = {
2023-03-02 15:15:28 -03:00
//TODO: Can we have an unnamed attendee? If not, I would really like to throw an error here.
2023-07-20 02:03:50 -03:00
attendeeName : fullName || "Nameless" ,
2022-10-12 10:04:51 -03:00
eventType : eventType.title ,
2023-07-27 05:52:46 -03:00
eventName : evtName ,
2023-07-20 20:29:08 -03:00
// we send on behalf of team if >1 round robin attendee | collective
teamName : eventType.schedulingType === "COLLECTIVE" || users . length > 1 ? eventType.team?.name : null ,
2023-03-02 15:15:28 -03:00
// TODO: Can we have an unnamed organizer? If not, I would really like to throw an error here.
2022-10-12 10:04:51 -03:00
host : organizerUser.name || "Nameless" ,
location : bookingLocation ,
2023-03-09 12:11:16 -03:00
bookingFields : { . . . responses } ,
2022-10-12 10:04:51 -03:00
t : tOrganizer ,
} ;
2023-03-07 14:50:54 -03:00
const calEventUserFieldsResponses =
"calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null ;
2023-04-20 08:38:57 -03:00
2023-12-15 12:28:32 -03:00
const iCalUID = getICalUID ( {
event : { iCalUID : originalRescheduledBooking?.iCalUID , uid : originalRescheduledBooking?.uid } ,
uid ,
} ) ;
// For bookings made before introducing iCalSequence, assume that the sequence should start at 1. For new bookings start at 0.
const iCalSequence = getICalSequence ( originalRescheduledBooking ) ;
2022-10-12 10:04:51 -03:00
let evt : CalendarEvent = {
2023-12-19 06:33:30 -03:00
bookerUrl : eventType.team
? await getBookerBaseUrl ( { organizationId : eventType.team.parentId } )
: await getBookerBaseUrl ( organizerUser ) ,
2023-11-20 12:39:21 -03:00
type : eventType . slug ,
2022-10-12 10:04:51 -03:00
title : getEventName ( eventNameObject ) , //this needs to be either forced in english, or fetched for each attendee and organizer separately
description : eventType.description ,
additionalNotes ,
customInputs ,
startTime : dayjs ( reqBody . start ) . utc ( ) . format ( ) ,
endTime : dayjs ( reqBody . end ) . utc ( ) . format ( ) ,
organizer : {
2022-12-21 21:15:51 -03:00
id : organizerUser.id ,
2022-10-12 10:04:51 -03:00
name : organizerUser.name || "Nameless" ,
email : organizerUser.email || "Email-less" ,
2023-04-18 07:08:09 -03:00
username : organizerUser.username || undefined ,
2022-10-12 10:04:51 -03:00
timeZone : organizerUser.timeZone ,
language : { translate : tOrganizer , locale : organizerUser.locale ? ? "en" } ,
2023-07-19 11:30:37 -03:00
timeFormat : getTimeFormatStringFromUserTimeFormat ( organizerUser . timeFormat ) ,
2022-10-12 10:04:51 -03:00
} ,
2023-03-07 14:50:54 -03:00
responses : "calEventResponses" in reqBody ? reqBody.calEventResponses : null ,
userFieldsResponses : calEventUserFieldsResponses ,
2022-10-12 10:04:51 -03:00
attendees : attendeesList ,
location : bookingLocation , // Will be processed by the EventManager later.
2023-08-02 00:54:28 -03:00
conferenceCredentialId ,
2023-08-31 14:47:02 -03:00
destinationCalendar : eventType.destinationCalendar
? [ eventType . destinationCalendar ]
: organizerUser . destinationCalendar
? [ organizerUser . destinationCalendar ]
: null ,
2022-10-12 10:04:51 -03:00
hideCalendarNotes : eventType.hideCalendarNotes ,
2023-10-13 14:22:57 -03:00
requiresConfirmation : ! isConfirmedByDefault ,
2022-10-12 10:04:51 -03:00
eventTypeId : eventType.id ,
2023-02-24 16:52:25 -03:00
// if seats are not enabled we should default true
2023-06-02 16:29:52 -03:00
seatsShowAttendees : eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true ,
2023-01-31 18:14:19 -03:00
seatsPerTimeSlot : eventType.seatsPerTimeSlot ,
2023-09-08 12:37:26 -03:00
seatsShowAvailabilityCount : eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true ,
2023-08-31 14:47:02 -03:00
schedulingType : eventType.schedulingType ,
2023-12-15 12:28:32 -03:00
iCalUID ,
iCalSequence ,
2022-10-12 10:04:51 -03:00
} ;
2023-08-31 14:47:02 -03:00
if ( isTeamEventType && eventType . schedulingType === "COLLECTIVE" ) {
evt . destinationCalendar ? . push ( . . . teamDestinationCalendars ) ;
}
2023-03-14 01:19:05 -03:00
/* Used for seats bookings to update evt object with video data */
const addVideoCallDataToEvt = ( bookingReferences : BookingReference [ ] ) = > {
const videoCallReference = bookingReferences . find ( ( reference ) = > reference . type . includes ( "_video" ) ) ;
2023-01-25 05:52:58 -03:00
if ( videoCallReference ) {
evt . videoCallData = {
type : videoCallReference . type ,
id : videoCallReference.meetingId ,
password : videoCallReference?.meetingPassword ,
url : videoCallReference.meetingUrl ,
} ;
}
2023-03-14 01:19:05 -03:00
} ;
/ * C h e c k i f t h e o r i g i n a l b o o k i n g h a s n o m o r e a t t e n d e e s , i f s o d e l e t e t h e b o o k i n g
and any calendar or video integrations * /
const lastAttendeeDeleteBooking = async (
originalRescheduledBooking : Awaited < ReturnType < typeof getOriginalRescheduledBooking > > ,
filteredAttendees : Partial < Attendee > [ ] ,
originalBookingEvt? : CalendarEvent
) = > {
let deletedReferences = false ;
if ( filteredAttendees && filteredAttendees . length === 0 && originalRescheduledBooking ) {
const integrationsToDelete = [ ] ;
for ( const reference of originalRescheduledBooking . references ) {
if ( reference . credentialId ) {
const credential = await prisma . credential . findUnique ( {
where : {
id : reference.credentialId ,
} ,
2023-09-14 13:53:58 -03:00
select : credentialForCalendarServiceSelect ,
2023-03-14 01:19:05 -03:00
} ) ;
if ( credential ) {
if ( reference . type . includes ( "_video" ) ) {
integrationsToDelete . push ( deleteMeeting ( credential , reference . uid ) ) ;
}
if ( reference . type . includes ( "_calendar" ) && originalBookingEvt ) {
2023-04-05 11:55:57 -03:00
const calendar = await getCalendar ( credential ) ;
2023-03-14 01:19:05 -03:00
if ( calendar ) {
integrationsToDelete . push (
calendar ? . deleteEvent ( reference . uid , originalBookingEvt , reference . externalCalendarId )
) ;
}
}
}
}
}
await Promise . all ( integrationsToDelete ) . then ( async ( ) = > {
2023-04-02 07:09:57 -03:00
await prisma . booking . update ( {
2023-03-14 01:19:05 -03:00
where : {
id : originalRescheduledBooking.id ,
} ,
2023-04-02 07:09:57 -03:00
data : {
status : BookingStatus.CANCELLED ,
} ,
2023-03-14 01:19:05 -03:00
} ) ;
} ) ;
deletedReferences = true ;
}
return deletedReferences ;
} ;
2023-07-13 17:40:16 -03:00
// data needed for triggering webhooks
const eventTypeInfo : EventTypeInfo = {
eventTitle : eventType.title ,
eventDescription : eventType.description ,
price : paymentAppData.price ,
currency : eventType.currency ,
length : eventType.length ,
} ;
2023-07-25 14:05:02 -03:00
const teamId = await getTeamIdFromEventType ( { eventType } ) ;
2023-07-13 17:40:16 -03:00
2023-08-25 21:27:05 -03:00
const triggerForUser = ! teamId || ( teamId && eventType . parentId ) ;
2023-07-13 17:40:16 -03:00
const subscriberOptions : GetSubscriberOptions = {
2023-08-25 21:27:05 -03:00
userId : triggerForUser ? organizerUser.id : null ,
2023-07-13 17:40:16 -03:00
eventTypeId ,
triggerEvent : WebhookTriggerEvents.BOOKING_CREATED ,
teamId ,
} ;
const eventTrigger : WebhookTriggerEvents = rescheduleUid
? WebhookTriggerEvents . BOOKING_RESCHEDULED
: WebhookTriggerEvents . BOOKING_CREATED ;
subscriberOptions . triggerEvent = eventTrigger ;
const subscriberOptionsMeetingEnded = {
2023-08-25 21:27:05 -03:00
userId : triggerForUser ? organizerUser.id : null ,
2023-07-13 17:40:16 -03:00
eventTypeId ,
triggerEvent : WebhookTriggerEvents.MEETING_ENDED ,
teamId ,
} ;
2023-12-19 11:52:35 -03:00
const subscriberOptionsMeetingStarted = {
userId : triggerForUser ? organizerUser.id : null ,
eventTypeId ,
triggerEvent : WebhookTriggerEvents.MEETING_STARTED ,
teamId ,
} ;
2023-04-13 16:03:08 -03:00
const handleSeats = async ( ) = > {
let resultBooking :
| ( Partial < Booking > & {
appsStatus? : AppsStatus [ ] ;
seatReferenceUid? : string ;
paymentUid? : string ;
message? : string ;
2023-09-30 01:52:32 -03:00
paymentId? : number ;
2023-04-13 16:03:08 -03:00
} )
| null = null ;
2023-07-11 12:11:08 -03:00
2023-05-09 07:02:19 -03:00
const booking = await prisma . booking . findFirst ( {
2022-10-12 10:04:51 -03:00
where : {
2023-05-09 07:02:19 -03:00
OR : [
{
uid : rescheduleUid || reqBody . bookingUid ,
} ,
{
eventTypeId : eventType.id ,
startTime : evt.startTime ,
} ,
] ,
2023-07-11 12:11:08 -03:00
status : BookingStatus.ACCEPTED ,
2022-10-12 10:04:51 -03:00
} ,
2023-03-14 01:19:05 -03:00
select : {
uid : true ,
id : true ,
attendees : { include : { bookingSeat : true } } ,
userId : true ,
references : true ,
startTime : true ,
user : true ,
status : true ,
2023-07-13 17:40:16 -03:00
smsReminderNumber : true ,
endTime : true ,
scheduledJobs : true ,
2022-10-12 10:04:51 -03:00
} ,
} ) ;
2023-05-09 07:02:19 -03:00
if ( ! booking ) {
throw new HttpError ( { statusCode : 404 , message : "Could not find booking" } ) ;
}
2023-03-14 01:19:05 -03:00
// See if attendee is already signed up for timeslot
if (
booking . attendees . find ( ( attendee ) = > attendee . email === invitee [ 0 ] . email ) &&
dayjs . utc ( booking . startTime ) . format ( ) === evt . startTime
) {
2023-11-15 16:52:19 -03:00
throw new HttpError ( { statusCode : 409 , message : ErrorCode.AlreadySignedUpForBooking } ) ;
2023-03-14 01:19:05 -03:00
}
2022-10-12 10:04:51 -03:00
2023-03-14 01:19:05 -03:00
// There are two paths here, reschedule a booking with seats and booking seats without reschedule
if ( rescheduleUid ) {
// See if the new date has a booking already
const newTimeSlotBooking = await prisma . booking . findFirst ( {
2023-02-08 17:36:22 -03:00
where : {
2023-03-14 01:19:05 -03:00
startTime : evt.startTime ,
eventTypeId : eventType.id ,
2023-07-11 12:11:08 -03:00
status : BookingStatus.ACCEPTED ,
2023-02-08 17:36:22 -03:00
} ,
select : {
2023-03-14 01:19:05 -03:00
id : true ,
uid : true ,
attendees : {
include : {
bookingSeat : true ,
2023-02-08 17:36:22 -03:00
} ,
} ,
} ,
} ) ;
2023-07-11 04:41:21 -03:00
const credentials = await refreshCredentials ( allCredentials ) ;
2023-03-14 01:19:05 -03:00
const eventManager = new EventManager ( { . . . organizerUser , credentials } ) ;
if ( ! originalRescheduledBooking ) {
// typescript isn't smart enough;
throw new Error ( "Internal Error." ) ;
}
const updatedBookingAttendees = originalRescheduledBooking . attendees . reduce (
( filteredAttendees , attendee ) = > {
if ( attendee . email === bookerEmail ) {
return filteredAttendees ; // skip current booker, as we know the language already.
}
filteredAttendees . push ( {
name : attendee.name ,
email : attendee.email ,
timeZone : attendee.timeZone ,
language : { translate : tAttendees , locale : attendee.locale ? ? "en" } ,
} ) ;
return filteredAttendees ;
} ,
[ ] as Person [ ]
) ;
// If original booking has video reference we need to add the videoCallData to the new evt
const videoReference = originalRescheduledBooking . references . find ( ( reference ) = >
reference . type . includes ( "_video" )
) ;
const originalBookingEvt = {
. . . evt ,
title : originalRescheduledBooking.title ,
startTime : dayjs ( originalRescheduledBooking . startTime ) . utc ( ) . format ( ) ,
endTime : dayjs ( originalRescheduledBooking . endTime ) . utc ( ) . format ( ) ,
attendees : updatedBookingAttendees ,
// If the location is a video integration then include the videoCallData
. . . ( videoReference && {
videoCallData : {
type : videoReference . type ,
id : videoReference.meetingId ,
password : videoReference.meetingPassword ,
url : videoReference.meetingUrl ,
} ,
} ) ,
} ;
2023-03-27 22:16:07 -03:00
if ( ! bookingSeat ) {
// if no bookingSeat is given and the userId != owner, 401.
// TODO: Next step; Evaluate ownership, what about teams?
if ( booking . user ? . id !== req . userId ) {
throw new HttpError ( { statusCode : 401 } ) ;
}
2023-03-30 20:45:48 -03:00
// Moving forward in this block is the owner making changes to the booking. All attendees should be affected
evt . attendees = originalRescheduledBooking . attendees . map ( ( attendee ) = > {
return {
name : attendee.name ,
email : attendee.email ,
timeZone : attendee.timeZone ,
language : { translate : tAttendees , locale : attendee.locale ? ? "en" } ,
} ;
} ) ;
2023-03-27 22:16:07 -03:00
// If owner reschedules the event we want to update the entire booking
// Also if owner is rescheduling there should be no bookingSeat
2023-03-14 01:19:05 -03:00
// If there is no booking during the new time slot then update the current booking to the new date
if ( ! newTimeSlotBooking ) {
const newBooking : ( Booking & { appsStatus? : AppsStatus [ ] } ) | null = await prisma . booking . update ( {
where : {
id : booking.id ,
} ,
data : {
startTime : evt.startTime ,
2023-07-11 12:11:08 -03:00
endTime : evt.endTime ,
2023-03-14 01:19:05 -03:00
cancellationReason : rescheduleReason ,
} ,
include : {
user : true ,
references : true ,
payment : true ,
attendees : true ,
} ,
} ) ;
addVideoCallDataToEvt ( newBooking . references ) ;
const copyEvent = cloneDeep ( evt ) ;
const updateManager = await eventManager . reschedule ( copyEvent , rescheduleUid , newBooking . id ) ;
// @NOTE: This code is duplicated and should be moved to a function
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt . description = eventType . description ;
const results = updateManager . results ;
2023-04-03 14:13:57 -03:00
const calendarResult = results . find ( ( result ) = > result . type . includes ( "_calendar" ) ) ;
2023-07-11 12:11:08 -03:00
evt . iCalUID = calendarResult ? . updatedEvent . iCalUID || undefined ;
2023-04-03 14:13:57 -03:00
2023-03-14 01:19:05 -03:00
if ( results . length > 0 && results . some ( ( res ) = > ! res . success ) ) {
const error = {
errorCode : "BookingReschedulingMeetingFailed" ,
message : "Booking Rescheduling failed" ,
} ;
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . error (
` Booking ${ organizerUser . name } failed ` ,
JSON . stringify ( { error , results } )
) ;
2023-03-14 01:19:05 -03:00
} else {
const metadata : AdditionalInformation = { } ;
if ( results . length ) {
// TODO: Handle created event metadata more elegantly
const [ updatedEvent ] = Array . isArray ( results [ 0 ] . updatedEvent )
? results [ 0 ] . updatedEvent
: [ results [ 0 ] . updatedEvent ] ;
if ( updatedEvent ) {
metadata . hangoutLink = updatedEvent . hangoutLink ;
metadata . conferenceData = updatedEvent . conferenceData ;
metadata . entryPoints = updatedEvent . entryPoints ;
2023-10-17 08:16:24 -03:00
evt . appsStatus = handleAppsStatus ( results , newBooking ) ;
2023-03-14 01:19:05 -03:00
}
}
}
2023-10-13 14:22:57 -03:00
if ( noEmail !== true && isConfirmedByDefault ) {
2023-03-14 01:19:05 -03:00
const copyEvent = cloneDeep ( evt ) ;
2023-10-13 14:22:57 -03:00
loggerWithEventDetails . debug ( "Emails: Sending reschedule emails - handleSeats" ) ;
2023-03-14 01:19:05 -03:00
await sendRescheduledEmails ( {
. . . copyEvent ,
additionalNotes , // Resets back to the additionalNote input and not the override value
2023-10-03 15:52:19 -03:00
cancellationReason : ` $ RCH $ ${ rescheduleReason ? rescheduleReason : "" } ` , // Removable code prefix to differentiate cancellation from rescheduling for email
2023-03-14 01:19:05 -03:00
} ) ;
}
2023-04-13 16:03:08 -03:00
const foundBooking = await findBookingQuery ( newBooking . id ) ;
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
resultBooking = { . . . foundBooking , appsStatus : newBooking.appsStatus } ;
} else {
// Merge two bookings together
const attendeesToMove = [ ] ,
attendeesToDelete = [ ] ;
for ( const attendee of booking . attendees ) {
// If the attendee already exists on the new booking then delete the attendee record of the old booking
if (
newTimeSlotBooking . attendees . some (
( newBookingAttendee ) = > newBookingAttendee . email === attendee . email
)
) {
attendeesToDelete . push ( attendee . id ) ;
// If the attendee does not exist on the new booking then move that attendee record to the new booking
} else {
attendeesToMove . push ( { id : attendee.id , seatReferenceId : attendee.bookingSeat?.id } ) ;
}
}
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
// Confirm that the new event will have enough available seats
2023-03-14 01:19:05 -03:00
if (
2023-04-13 16:03:08 -03:00
! eventType . seatsPerTimeSlot ||
attendeesToMove . length +
newTimeSlotBooking . attendees . filter ( ( attendee ) = > attendee . bookingSeat ) . length >
eventType . seatsPerTimeSlot
2023-03-14 01:19:05 -03:00
) {
2023-04-13 16:03:08 -03:00
throw new HttpError ( { statusCode : 409 , message : "Booking does not have enough available seats" } ) ;
2023-03-14 01:19:05 -03:00
}
2023-04-13 16:03:08 -03:00
const moveAttendeeCalls = [ ] ;
for ( const attendeeToMove of attendeesToMove ) {
moveAttendeeCalls . push (
prisma . attendee . update ( {
where : {
id : attendeeToMove.id ,
} ,
data : {
bookingId : newTimeSlotBooking.id ,
bookingSeat : {
upsert : {
create : {
referenceUid : uuid ( ) ,
bookingId : newTimeSlotBooking.id ,
} ,
update : {
bookingId : newTimeSlotBooking.id ,
} ,
2023-03-14 01:19:05 -03:00
} ,
} ,
} ,
2023-04-13 16:03:08 -03:00
} )
) ;
}
await Promise . all ( [
. . . moveAttendeeCalls ,
// Delete any attendees that are already a part of that new time slot booking
prisma . attendee . deleteMany ( {
where : {
id : {
in : attendeesToDelete ,
} ,
2023-03-14 01:19:05 -03:00
} ,
2023-04-13 16:03:08 -03:00
} ) ,
] ) ;
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
const updatedNewBooking = await prisma . booking . findUnique ( {
2023-03-14 01:19:05 -03:00
where : {
2023-04-13 16:03:08 -03:00
id : newTimeSlotBooking.id ,
2023-03-14 01:19:05 -03:00
} ,
2023-04-13 16:03:08 -03:00
include : {
attendees : true ,
references : true ,
} ,
} ) ;
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
if ( ! updatedNewBooking ) {
throw new HttpError ( { statusCode : 404 , message : "Updated booking not found" } ) ;
}
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
// Update the evt object with the new attendees
const updatedBookingAttendees = updatedNewBooking . attendees . map ( ( attendee ) = > {
const evtAttendee = {
. . . attendee ,
2023-08-10 13:05:35 -03:00
language : { translate : tAttendees , locale : attendeeLanguage ? ? "en" } ,
2023-04-13 16:03:08 -03:00
} ;
return evtAttendee ;
} ) ;
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
evt . attendees = updatedBookingAttendees ;
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
addVideoCallDataToEvt ( updatedNewBooking . references ) ;
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
const copyEvent = cloneDeep ( evt ) ;
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
const updateManager = await eventManager . reschedule (
copyEvent ,
rescheduleUid ,
newTimeSlotBooking . id
) ;
2023-04-03 14:13:57 -03:00
2023-04-13 16:03:08 -03:00
const results = updateManager . results ;
2023-04-03 14:13:57 -03:00
2023-04-13 16:03:08 -03:00
const calendarResult = results . find ( ( result ) = > result . type . includes ( "_calendar" ) ) ;
2023-04-03 14:13:57 -03:00
2023-04-13 16:03:08 -03:00
evt . iCalUID = Array . isArray ( calendarResult ? . updatedEvent )
? calendarResult ? . updatedEvent [ 0 ] ? . iCalUID
: calendarResult ? . updatedEvent ? . iCalUID || undefined ;
2023-03-14 01:19:05 -03:00
2023-10-13 14:22:57 -03:00
if ( noEmail !== true && isConfirmedByDefault ) {
2023-09-05 14:52:47 -03:00
// TODO send reschedule emails to attendees of the old booking
2023-10-13 14:22:57 -03:00
loggerWithEventDetails . debug ( "Emails: Sending reschedule emails - handleSeats" ) ;
2023-09-05 14:52:47 -03:00
await sendRescheduledEmails ( {
. . . copyEvent ,
additionalNotes , // Resets back to the additionalNote input and not the override value
2023-10-03 15:52:19 -03:00
cancellationReason : ` $ RCH $ ${ rescheduleReason ? rescheduleReason : "" } ` , // Removable code prefix to differentiate cancellation from rescheduling for email
2023-09-05 14:52:47 -03:00
} ) ;
}
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
// Update the old booking with the cancelled status
await prisma . booking . update ( {
where : {
id : booking.id ,
} ,
data : {
status : BookingStatus.CANCELLED ,
} ,
} ) ;
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
const foundBooking = await findBookingQuery ( newTimeSlotBooking . id ) ;
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
resultBooking = { . . . foundBooking } ;
}
2023-03-14 01:19:05 -03:00
}
2023-03-27 22:16:07 -03:00
// seatAttendee is null when the organizer is rescheduling.
const seatAttendee : Partial < Person > | null = bookingSeat ? . attendee || null ;
2023-04-13 16:03:08 -03:00
if ( seatAttendee ) {
seatAttendee [ "language" ] = { translate : tAttendees , locale : bookingSeat?.attendee.locale ? ? "en" } ;
2023-03-14 01:19:05 -03:00
2023-07-11 12:11:08 -03:00
// If there is no booking then remove the attendee from the old booking and create a new one
if ( ! newTimeSlotBooking ) {
await prisma . attendee . delete ( {
where : {
id : seatAttendee?.id ,
} ,
2023-03-14 01:19:05 -03:00
} ) ;
2023-07-11 12:11:08 -03:00
// Update the original calendar event by removing the attendee that is rescheduling
if ( originalBookingEvt && originalRescheduledBooking ) {
// Event would probably be deleted so we first check than instead of updating references
const filteredAttendees = originalRescheduledBooking ? . attendees . filter ( ( attendee ) = > {
return attendee . email !== bookerEmail ;
} ) ;
const deletedReference = await lastAttendeeDeleteBooking (
originalRescheduledBooking ,
filteredAttendees ,
originalBookingEvt
) ;
if ( ! deletedReference ) {
await eventManager . updateCalendarAttendees ( originalBookingEvt , originalRescheduledBooking ) ;
}
2023-03-14 01:19:05 -03:00
}
2023-07-11 12:11:08 -03:00
// We don't want to trigger rescheduling logic of the original booking
originalRescheduledBooking = null ;
2023-03-14 01:19:05 -03:00
2023-07-11 12:11:08 -03:00
return null ;
}
2023-03-14 01:19:05 -03:00
2023-07-11 12:11:08 -03:00
// Need to change the new seat reference and attendee record to remove it from the old booking and add it to the new booking
// https://stackoverflow.com/questions/4980963/database-insert-new-rows-or-update-existing-ones
if ( seatAttendee ? . id && bookingSeat ? . id ) {
await Promise . all ( [
await prisma . attendee . update ( {
where : {
id : seatAttendee.id ,
} ,
data : {
bookingId : newTimeSlotBooking.id ,
} ,
} ) ,
await prisma . bookingSeat . update ( {
where : {
id : bookingSeat.id ,
} ,
data : {
bookingId : newTimeSlotBooking.id ,
} ,
} ) ,
] ) ;
}
2023-03-14 01:19:05 -03:00
2023-07-11 12:11:08 -03:00
const copyEvent = cloneDeep ( evt ) ;
2023-03-14 01:19:05 -03:00
2023-07-11 12:11:08 -03:00
const updateManager = await eventManager . reschedule ( copyEvent , rescheduleUid , newTimeSlotBooking . id ) ;
2023-04-03 14:13:57 -03:00
2023-07-11 12:11:08 -03:00
const results = updateManager . results ;
2023-04-03 14:13:57 -03:00
2023-07-11 12:11:08 -03:00
const calendarResult = results . find ( ( result ) = > result . type . includes ( "_calendar" ) ) ;
2023-04-03 14:13:57 -03:00
2023-07-11 12:11:08 -03:00
evt . iCalUID = Array . isArray ( calendarResult ? . updatedEvent )
? calendarResult ? . updatedEvent [ 0 ] ? . iCalUID
: calendarResult ? . updatedEvent ? . iCalUID || undefined ;
2023-03-14 01:19:05 -03:00
2023-07-11 12:11:08 -03:00
await sendRescheduledSeatEmail ( copyEvent , seatAttendee as Person ) ;
const filteredAttendees = originalRescheduledBooking ? . attendees . filter ( ( attendee ) = > {
return attendee . email !== bookerEmail ;
} ) ;
await lastAttendeeDeleteBooking ( originalRescheduledBooking , filteredAttendees , originalBookingEvt ) ;
2022-12-01 21:12:06 -03:00
2023-07-11 12:11:08 -03:00
const foundBooking = await findBookingQuery ( newTimeSlotBooking . id ) ;
2023-03-14 01:19:05 -03:00
2023-07-11 12:11:08 -03:00
resultBooking = { . . . foundBooking , seatReferenceUid : bookingSeat?.referenceUid } ;
}
2023-03-14 01:19:05 -03:00
} else {
// Need to add translation for attendees to pass type checks. Since these values are never written to the db we can just use the new attendee language
const bookingAttendees = booking . attendees . map ( ( attendee ) = > {
2023-08-10 13:05:35 -03:00
return { . . . attendee , language : { translate : tAttendees , locale : attendeeLanguage ? ? "en" } } ;
2023-03-14 01:19:05 -03:00
} ) ;
evt = { . . . evt , attendees : [ . . . bookingAttendees , invitee [ 0 ] ] } ;
if ( eventType . seatsPerTimeSlot && eventType . seatsPerTimeSlot <= booking . attendees . length ) {
throw new HttpError ( { statusCode : 409 , message : "Booking seats are full" } ) ;
2023-02-08 17:36:22 -03:00
}
2023-03-14 01:19:05 -03:00
const videoCallReference = booking . references . find ( ( reference ) = > reference . type . includes ( "_video" ) ) ;
if ( videoCallReference ) {
evt . videoCallData = {
type : videoCallReference . type ,
id : videoCallReference.meetingId ,
password : videoCallReference?.meetingPassword ,
url : videoCallReference.meetingUrl ,
} ;
2023-02-08 17:36:22 -03:00
}
2022-12-01 21:12:06 -03:00
2023-03-29 18:13:35 -03:00
const attendeeUniqueId = uuid ( ) ;
2023-04-13 16:03:08 -03:00
await prisma . booking . update ( {
2023-03-14 01:19:05 -03:00
where : {
uid : reqBody.bookingUid ,
} ,
include : {
attendees : true ,
} ,
data : {
attendees : {
create : {
email : invitee [ 0 ] . email ,
name : invitee [ 0 ] . name ,
timeZone : invitee [ 0 ] . timeZone ,
locale : invitee [ 0 ] . language . locale ,
2023-03-29 18:13:35 -03:00
bookingSeat : {
create : {
referenceUid : attendeeUniqueId ,
data : {
description : additionalNotes ,
} ,
booking : {
connect : {
id : booking.id ,
} ,
} ,
} ,
} ,
2023-03-14 01:19:05 -03:00
} ,
} ,
. . . ( booking . status === BookingStatus . CANCELLED && { status : BookingStatus.ACCEPTED } ) ,
} ,
} ) ;
evt . attendeeSeatId = attendeeUniqueId ;
const newSeat = booking . attendees . length !== 0 ;
/ * *
* Remember objects are passed into functions as references
* so if you modify it in a inner function it will be modified in the outer function
* deep cloning evt to avoid this
* /
2023-05-23 15:19:52 -03:00
if ( ! evt ? . uid ) {
evt . uid = booking ? . uid ? ? null ;
}
2023-03-14 01:19:05 -03:00
const copyEvent = cloneDeep ( evt ) ;
2023-04-21 10:49:53 -03:00
copyEvent . uid = booking . uid ;
2023-10-12 11:57:35 -03:00
if ( noEmail !== true ) {
let isHostConfirmationEmailsDisabled = false ;
let isAttendeeConfirmationEmailDisabled = false ;
2022-12-01 21:12:06 -03:00
2023-10-12 11:57:35 -03:00
const workflows = eventType . workflows . map ( ( workflow ) = > workflow . workflow ) ;
if ( eventType . workflows ) {
isHostConfirmationEmailsDisabled =
eventType . metadata ? . disableStandardEmails ? . confirmation ? . host || false ;
isAttendeeConfirmationEmailDisabled =
eventType . metadata ? . disableStandardEmails ? . confirmation ? . attendee || false ;
if ( isHostConfirmationEmailsDisabled ) {
isHostConfirmationEmailsDisabled = allowDisablingHostConfirmationEmails ( workflows ) ;
}
if ( isAttendeeConfirmationEmailDisabled ) {
isAttendeeConfirmationEmailDisabled = allowDisablingAttendeeConfirmationEmails ( workflows ) ;
}
}
await sendScheduledSeatsEmails (
copyEvent ,
invitee [ 0 ] ,
newSeat ,
! ! eventType . seatsShowAttendees ,
isHostConfirmationEmailsDisabled ,
isAttendeeConfirmationEmailDisabled
) ;
}
2023-07-11 04:41:21 -03:00
const credentials = await refreshCredentials ( allCredentials ) ;
2023-03-14 01:19:05 -03:00
const eventManager = new EventManager ( { . . . organizerUser , credentials } ) ;
await eventManager . updateCalendarAttendees ( evt , booking ) ;
2023-04-13 16:03:08 -03:00
const foundBooking = await findBookingQuery ( booking . id ) ;
2023-03-14 01:19:05 -03:00
if ( ! Number . isNaN ( paymentAppData . price ) && paymentAppData . price > 0 && ! ! booking ) {
const credentialPaymentAppCategories = await prisma . credential . findMany ( {
where : {
feat: Enable Apps for Teams & Orgs [CAL-1782] (#9337)
* Initial commit
* Adding feature flag
* Add schema relation for teams and credentials
* feat: Orgs Schema Changing `scopedMembers` to `orgUsers` (#9209)
* Change scopedMembers to orgMembers
* Change to orgUsers
* Create getUserAdminTeams function & tRPC endpoint
* Get user admin teams on app store page
* Create UserAdminTeams type
* Add user query to getUserAdminTeams
* Letting duplicate slugs for teams to support orgs
* Covering null on unique clauses
* Add dropdown to install button on app store
* Supporting having the orgId in the session cookie
* On app page, only show dropdown if there are teams
* Add teamId to OAuth state
* Create team credential for OAuth flow
* Create team credential for GCal
* Add create user or team credential for Stripe
* Create webex credentials for users or teams
* Fix type error on useAddAppMutation
* Hubspot create credential on user or team
* Zoho create create credential for user or team
* Zoom create credentials on user or team
* Salesforce create credential on user or teams
* OAuth create credentials for user or teams
* Revert Outlook changes
* Revert GCal changes
* Default app instal, create credential on user or team
* Add teamId to credential creation
* Disable installing for teams for calendars
* Include teams when querying installed apps
* Render team credentials on installed page
* Uninstall team apps
* Type fix on app card
* Add input to include user in teams query
* Add dropdown to install app page for user or team
* Type fixes on category page
* Install app from eventType page to user or team
* Render user and team apps on event type app page
* feat: organization event type filter (#9253)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* Missing changes to support orgs schema changes
* Render user and team apps on event type app page
* Add credentialOwner to eventTypeAppCard types
* Type fixes
* Create hook to check if app is enabled
* Clean up console.logs
* Fix useIsAppEnabled by returning not an array
* Convert event type apps to useIsAppEnabled
* Abstract credential owner type
* Remove console.logs
* On installed app page, show apps if only team credential is installed
* Clean up commented lines
* Handle installing app to just an team event from event type page
* Fix early return when creating team app credential
* Zoom add state to callback
* Get team location credentials and save credential id to location
* feat: Onboarding process to create an organization (#9184)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Making sure we check requestedSlug now
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Type fix
* Grab team location credentials
* Add isInstalled to eventType apps query
* feat: [CAL-1816] Organization subdomain support (#9345)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* Covering users and subteams, excluding non-org users
* Unpublished teams shows correctly
* Create subdomain in Vercel
* feedback
* Renaming Vercel env vars
* Vercel domain check before creation
* Supporting cal-staging.com
* Change to have vercel detect it
* vercel domain check data message error
* Remove check domain
* Making sure we check requestedSlug now
* Feedback and unneeded code
* Reverting unneeded changes
* Unneeded changes
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Vercel subdomain creation in PROD only
* Enable payment apps for team credentials
* Fix for team-user apps for event types
* Fix layout and add teamId to app card
* Disable apps on managed event types
* Add managed event type fields to event type apps
* Include organizations in query
* Change createAppCredential to createOAuthAppCredential
* Show app installed on teams
* Making sure we let localhost still work
* UI show installed for which team
* Type fixes
* For team events move use host location to top
* Add around to appStore
* New team event types organizer default conf app
* Fix app card bug
* Clean up
* Search for teamId or userId when deleting credential
* Type fixes
* Type fixes
* Type fixes
* Type fixes
* Address feedback
* Feedback
* Type check fixes
* feat: Organization branding in side menu (#9279)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Making sure we show the set up profile on org only
* Profile username availability rely on org hook
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* feat: Organization support for event types page (#9449)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Processing orgs and children as profile options
* Reverting change not belonging to this PR
* Making sure we show the set up profile on org only
* Removing console.log
* Comparing memberships to choose the highest one
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Type errors
* Refactor and type fixes
* Update orgDomains.ts
* Feedback
* Reverting
* NIT
* Address feedback
* fix issue getting org slug from domain
* Improving orgDomains util
* Host comes with port
* Update useRouterQuery.ts
* Fix app card bug
* Fix schema
* Type fixes
* Revert changes to location apps
* Remove console.log
* Fix app store test
* Handle install app dropdown
* Add CalendarApp to `getCalendar`
* Add PaymentApp type fix
* Payment type fix
* Type fixes
* Match with main
* Change type to account for team
* Fix app count for team events
* Type fixes
* More type fixes
* Type fix?
* Fix the type fix
* Remove UserAdminTeams empty array union
* Type fix
* Type fix
* Type fix
* Uses type predicates
* Use teamId. Fixes installation for teams after user installation
* Fix Team Events not working
* Get embed for org events working
* Fix rewrites
* Address feedback
* Type fix
* Fixes
* Add useAppContextWithSchema in useIsAppEnabled
* Type fix for apps using useIsAppEnabled
* Integrations.handler change credentialIds to userCredentialIds
* Remove apps endpoint
* Add LockedIcon and disabled props to event type app context
* Type fixes
* Type fix
* Type fixes
* Show team installed apps for members
* Type fix
* Reverting findFirst
* Revert findFirst
* Avoid a possible 500
* Fix missing tanslation
* Avoid possible 500
* Undo default app for teams
* Type fix
* Fix test
* Update package.json
* feat: Fix invite bug - added tests (#9945)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
* chore: Button Component Tidy up (#9888)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
* feat: Make Team Private
## What does this PR do?
Fixes https://github.com/calcom/cal.com/issues/8974
1) When user is admin
<img width="1440" alt="Screenshot 2023-07-03 at 6 45 50 PM" src="https://github.com/calcom/cal.com/assets/53316345/ce15158f-d278-4f1a-ba2e-8b63e4274793">
2) When user is not admin and team is private
<img width="1440" alt="Screenshot 2023-07-03 at 6 47 15 PM" src="https://github.com/calcom/cal.com/assets/53316345/ce23560e-690a-4c42-a76d-49691260aa4d">
3)
<img width="1440" alt="Screenshot 2023-07-03 at 6 51 56 PM" src="https://github.com/calcom/cal.com/assets/53316345/13af38f8-5618-4dae-b359-b24dc91e4eb4">
## Type of change
<!-- Please delete bullets that are not relevant. -->
- New feature (non-breaking change which adds functionality)
## How should this be tested?
1) go to Team members page and turn on switch Make Team Private.
Now after making the team private only admin would be able to see all the members list in the settings. There will not be a button to Book a team member instead on the team page like before.
## Mandatory Tasks
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
---------
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-06 13:48:39 -03:00
. . . ( paymentAppData . credentialId
? { id : paymentAppData.credentialId }
: { userId : organizerUser.id } ) ,
2023-03-14 01:19:05 -03:00
app : {
categories : {
hasSome : [ "payment" ] ,
} ,
} ,
} ,
select : {
key : true ,
appId : true ,
app : {
select : {
categories : true ,
dirName : true ,
} ,
} ,
} ,
} ) ;
const eventTypePaymentAppCredential = credentialPaymentAppCategories . find ( ( credential ) = > {
return credential . appId === paymentAppData . appId ;
} ) ;
if ( ! eventTypePaymentAppCredential ) {
throw new HttpError ( { statusCode : 400 , message : "Missing payment credentials" } ) ;
}
if ( ! eventTypePaymentAppCredential ? . appId ) {
throw new HttpError ( { statusCode : 400 , message : "Missing payment app id" } ) ;
}
const payment = await handlePayment (
evt ,
eventType ,
eventTypePaymentAppCredential as IEventTypePaymentCredentialType ,
2023-04-11 18:44:14 -03:00
booking ,
2023-09-14 01:10:42 -03:00
fullName ,
2023-04-11 18:44:14 -03:00
bookerEmail
2023-03-14 01:19:05 -03:00
) ;
2023-04-13 16:03:08 -03:00
resultBooking = { . . . foundBooking } ;
resultBooking [ "message" ] = "Payment required" ;
resultBooking [ "paymentUid" ] = payment ? . uid ;
2023-09-30 01:52:32 -03:00
resultBooking [ "id" ] = payment ? . id ;
2023-07-11 12:11:08 -03:00
} else {
resultBooking = { . . . foundBooking } ;
2023-03-14 01:19:05 -03:00
}
2023-07-11 12:11:08 -03:00
resultBooking [ "seatReferenceUid" ] = evt . attendeeSeatId ;
2023-04-13 16:03:08 -03:00
}
// Here we should handle every after action that needs to be done after booking creation
// Obtain event metadata that includes videoCallUrl
2023-04-21 08:46:23 -03:00
const metadata = evt . videoCallData ? . url ? { videoCallUrl : evt.videoCallData.url } : undefined ;
2023-04-13 16:03:08 -03:00
try {
await scheduleWorkflowReminders ( {
workflows : eventType.workflows ,
smsReminderNumber : smsReminderNumber || null ,
2023-04-18 07:08:09 -03:00
calendarEvent : { . . . evt , . . . { metadata , eventType : { slug : eventType.slug } } } ,
2023-08-10 15:52:36 -03:00
isNotConfirmed : evt.requiresConfirmation || false ,
2023-04-13 16:03:08 -03:00
isRescheduleEvent : ! ! rescheduleUid ,
isFirstRecurringEvent : true ,
emailAttendeeSendToOverride : bookerEmail ,
2023-08-01 11:13:28 -03:00
seatReferenceUid : evt.attendeeSeatId ,
2023-09-21 03:22:05 -03:00
eventTypeRequiresConfirmation : eventType.requiresConfirmation ,
2023-04-13 16:03:08 -03:00
} ) ;
} catch ( error ) {
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . error ( "Error while scheduling workflow reminders" , JSON . stringify ( { error } ) ) ;
2023-03-14 01:19:05 -03:00
}
2023-04-13 16:03:08 -03:00
2023-07-13 17:40:16 -03:00
const webhookData = {
. . . evt ,
. . . eventTypeInfo ,
2023-09-07 13:27:46 -03:00
uid : resultBooking?.uid || uid ,
2023-07-13 17:40:16 -03:00
bookingId : booking?.id ,
2023-10-25 14:26:22 -03:00
rescheduleId : originalRescheduledBooking?.id || undefined ,
2023-07-13 17:40:16 -03:00
rescheduleUid ,
rescheduleStartTime : originalRescheduledBooking?.startTime
? dayjs ( originalRescheduledBooking ? . startTime ) . utc ( ) . format ( )
: undefined ,
rescheduleEndTime : originalRescheduledBooking?.endTime
? dayjs ( originalRescheduledBooking ? . endTime ) . utc ( ) . format ( )
: undefined ,
metadata : { . . . metadata , . . . reqBody . metadata } ,
eventTypeId ,
status : "ACCEPTED" ,
smsReminderNumber : booking?.smsReminderNumber || undefined ,
} ;
await handleWebhookTrigger ( { subscriberOptions , eventTrigger , webhookData } ) ;
2023-04-13 16:03:08 -03:00
return resultBooking ;
2023-03-14 01:19:05 -03:00
} ;
// For seats, if the booking already exists then we want to add the new attendee to the existing booking
if ( eventType . seatsPerTimeSlot && ( reqBody . bookingUid || rescheduleUid ) ) {
const newBooking = await handleSeats ( ) ;
if ( newBooking ) {
2022-12-01 21:12:06 -03:00
req . statusCode = 201 ;
2023-03-14 01:19:05 -03:00
return newBooking ;
2022-12-01 21:12:06 -03:00
}
2022-10-12 10:04:51 -03:00
}
2023-02-27 17:45:40 -03:00
if ( isTeamEventType ) {
2022-10-12 10:04:51 -03:00
evt . team = {
2023-02-27 17:45:40 -03:00
members : teamMembers ,
2022-10-12 10:04:51 -03:00
name : eventType.team?.name || "Nameless" ,
2023-11-22 16:54:18 -03:00
id : eventType.team?.id ? ? 0 ,
2023-02-27 17:45:40 -03:00
} ;
2022-10-12 10:04:51 -03:00
}
if ( reqBody . recurringEventId && eventType . recurringEvent ) {
// 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)
eventType . recurringEvent = Object . assign ( { } , eventType . recurringEvent , { count : recurringCount } ) ;
evt . recurringEvent = eventType . recurringEvent ;
}
2023-11-27 15:09:33 -03:00
const changedOrganizer =
! ! originalRescheduledBooking &&
eventType . schedulingType === SchedulingType . ROUND_ROBIN &&
originalRescheduledBooking . userId !== evt . organizer . id ;
2023-04-03 14:13:57 -03:00
let results : EventResult < AdditionalInformation & { url ? : string ; iCalUID ? : string } > [ ] = [ ] ;
2022-10-12 10:04:51 -03:00
let referencesToCreate : PartialReference [ ] = [ ] ;
2022-10-19 13:11:50 -03:00
let booking : ( Booking & { appsStatus? : AppsStatus [ ] } ) | null = null ;
2023-10-02 07:51:04 -03:00
loggerWithEventDetails . debug (
"Going to create booking in DB now" ,
safeStringify ( {
organizerUser : organizerUser.id ,
attendeesList : attendeesList.map ( ( guest ) = > ( { timeZone : guest.timeZone } ) ) ,
2023-10-13 14:22:57 -03:00
requiresConfirmation : evt.requiresConfirmation ,
2023-10-02 07:51:04 -03:00
isConfirmedByDefault ,
userReschedulingIsOwner ,
} )
) ;
2022-10-12 10:04:51 -03:00
try {
2023-11-29 12:11:09 -03:00
booking = await createBooking ( {
originalRescheduledBooking ,
evt ,
eventTypeId ,
eventTypeSlug ,
reqBodyUser : reqBody.user ,
reqBodyMetadata : reqBody.metadata ,
reqBodyRecurringEventId : reqBody.recurringEventId ,
uid ,
responses ,
isConfirmedByDefault ,
smsReminderNumber ,
organizerUser ,
rescheduleReason ,
eventType ,
bookerEmail ,
paymentAppData ,
changedOrganizer ,
} ) ;
2023-03-14 01:19:05 -03:00
// @NOTE: Add specific try catch for all subsequent async calls to avoid error
2022-10-12 10:04:51 -03:00
// Sync Services
await syncServicesUpdateWebUser (
await prisma . user . findFirst ( {
where : { id : userId } ,
2022-12-08 20:20:24 -03:00
select : { id : true , email : true , name : true , username : true , createdDate : true } ,
2022-10-12 10:04:51 -03:00
} )
) ;
evt . uid = booking ? . uid ? ? null ;
2023-03-14 01:19:05 -03:00
if ( booking && booking . id && eventType . seatsPerTimeSlot ) {
const currentAttendee = booking . attendees . find (
( attendee ) = > attendee . email === req . body . responses . email
2023-06-06 08:59:57 -03:00
) ;
2023-03-14 01:19:05 -03:00
// Save description to bookingSeat
const uniqueAttendeeId = uuid ( ) ;
await prisma . bookingSeat . create ( {
data : {
referenceUid : uniqueAttendeeId ,
data : {
description : evt.additionalNotes ,
} ,
booking : {
connect : {
id : booking.id ,
} ,
} ,
attendee : {
connect : {
id : currentAttendee?.id ,
} ,
} ,
} ,
} ) ;
evt . attendeeSeatId = uniqueAttendeeId ;
}
2022-10-12 10:04:51 -03:00
} catch ( _err ) {
const err = getErrorFromUnknown ( _err ) ;
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . error (
` Booking ${ eventTypeId } failed ` ,
"Error when saving booking to db" ,
err . message
) ;
2022-10-12 10:04:51 -03:00
if ( err . code === "P2002" ) {
throw new HttpError ( { statusCode : 409 , message : "booking.conflict" } ) ;
}
throw err ;
}
// After polling videoBusyTimes, credentials might have been changed due to refreshment, so query them again.
2023-07-11 04:41:21 -03:00
const credentials = await refreshCredentials ( allCredentials ) ;
2022-10-12 10:04:51 -03:00
const eventManager = new EventManager ( { . . . organizerUser , credentials } ) ;
2022-10-19 13:11:50 -03:00
function handleAppsStatus (
results : EventResult < AdditionalInformation > [ ] ,
booking : ( Booking & { appsStatus? : AppsStatus [ ] } ) | null
) {
// Taking care of apps status
2023-10-17 08:16:24 -03:00
let resultStatus : AppsStatus [ ] = results . map ( ( app ) = > ( {
2022-10-19 13:11:50 -03:00
appName : app.appName ,
type : app . type ,
success : app.success ? 1 : 0 ,
failures : ! app . success ? 1 : 0 ,
2022-11-22 17:44:08 -03:00
errors : app.calError ? [ app . calError ] : [ ] ,
warnings : app.calWarnings ,
2022-10-19 13:11:50 -03:00
} ) ) ;
if ( reqAppsStatus === undefined ) {
if ( booking !== null ) {
booking . appsStatus = resultStatus ;
}
2023-10-17 08:16:24 -03:00
return resultStatus ;
2022-10-19 13:11:50 -03:00
}
// From down here we can assume reqAppsStatus is not undefined anymore
// Other status exist, so this is the last booking of a series,
// proceeding to prepare the info for the event
const calcAppsStatus = reqAppsStatus . concat ( resultStatus ) . reduce ( ( prev , curr ) = > {
if ( prev [ curr . type ] ) {
prev [ curr . type ] . success += curr . success ;
2022-11-22 17:44:08 -03:00
prev [ curr . type ] . errors = prev [ curr . type ] . errors . concat ( curr . errors ) ;
prev [ curr . type ] . warnings = prev [ curr . type ] . warnings ? . concat ( curr . warnings || [ ] ) ;
2022-10-19 13:11:50 -03:00
} else {
prev [ curr . type ] = curr ;
}
return prev ;
} , { } as { [ key : string ] : AppsStatus } ) ;
2023-10-17 08:16:24 -03:00
resultStatus = Object . values ( calcAppsStatus ) ;
return resultStatus ;
2022-10-19 13:11:50 -03:00
}
2022-12-15 18:43:07 -03:00
let videoCallUrl ;
2023-09-30 01:52:32 -03:00
2023-11-27 15:09:33 -03:00
//this is the actual rescheduling logic
2022-10-12 10:04:51 -03:00
if ( originalRescheduledBooking ? . uid ) {
2023-09-30 01:52:32 -03:00
log . silly ( "Rescheduling booking" , originalRescheduledBooking . uid ) ;
2023-02-20 14:40:08 -03:00
try {
// cancel workflow reminders from previous rescheduled booking
2023-07-31 14:35:48 -03:00
await cancelWorkflowReminders ( originalRescheduledBooking . workflowReminders ) ;
2023-02-20 14:40:08 -03:00
} catch ( error ) {
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . error (
"Error while canceling scheduled workflow reminders" ,
JSON . stringify ( { error } )
) ;
2023-02-20 14:40:08 -03:00
}
2023-03-23 12:18:27 -03:00
addVideoCallDataToEvt ( originalRescheduledBooking . references ) ;
2023-03-27 06:34:41 -03:00
2023-04-02 07:09:57 -03:00
//update original rescheduled booking (no seats event)
2023-03-27 06:34:41 -03:00
if ( ! eventType . seatsPerTimeSlot ) {
2023-04-02 07:09:57 -03:00
await prisma . booking . update ( {
2023-03-27 06:34:41 -03:00
where : {
id : originalRescheduledBooking.id ,
} ,
2023-04-02 07:09:57 -03:00
data : {
2023-10-04 16:10:10 -03:00
rescheduled : true ,
2023-04-02 07:09:57 -03:00
status : BookingStatus.CANCELLED ,
} ,
2023-03-27 06:34:41 -03:00
} ) ;
}
2023-11-27 15:09:33 -03:00
const newDesinationCalendar = evt . destinationCalendar ;
evt . destinationCalendar = originalRescheduledBooking ? . destinationCalendar
? [ originalRescheduledBooking ? . destinationCalendar ]
: originalRescheduledBooking ? . user ? . destinationCalendar
? [ originalRescheduledBooking ? . user . destinationCalendar ]
: evt . destinationCalendar ;
2023-11-28 15:37:31 -03:00
if ( changedOrganizer ) {
evt . title = getEventName ( eventNameObject ) ;
// location might changed and will be new created in eventManager.create (organizer default location)
evt . videoCallData = undefined ;
}
2023-11-27 15:09:33 -03:00
const updateManager = await eventManager . reschedule (
evt ,
originalRescheduledBooking . uid ,
undefined ,
2023-11-28 15:37:31 -03:00
changedOrganizer ,
newDesinationCalendar
2023-11-27 15:09:33 -03:00
) ;
2022-10-12 10:04:51 -03:00
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt . description = eventType . description ;
results = updateManager . results ;
referencesToCreate = updateManager . referencesToCreate ;
2023-11-28 15:37:31 -03:00
videoCallUrl = evt . videoCallData && evt . videoCallData . url ? evt.videoCallData.url : null ;
2023-04-03 14:13:57 -03:00
2023-11-28 15:37:31 -03:00
// This gets overridden when creating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt . description = eventType . description ;
2022-10-12 10:04:51 -03:00
2023-11-27 15:09:33 -03:00
const { metadata : videoMetadata , videoCallUrl : _videoCallUrl } = getVideoCallDetails ( {
2023-10-17 08:16:24 -03:00
results ,
} ) ;
2023-11-27 15:09:33 -03:00
let metadata : AdditionalInformation = { } ;
metadata = videoMetadata ;
2023-10-17 08:16:24 -03:00
videoCallUrl = _videoCallUrl ;
2023-11-27 15:09:33 -03:00
2023-11-28 15:37:31 -03:00
const isThereAnIntegrationError = results && results . some ( ( res ) = > ! res . success ) ;
2023-11-27 15:09:33 -03:00
2023-11-28 15:37:31 -03:00
if ( isThereAnIntegrationError ) {
const error = {
errorCode : "BookingReschedulingMeetingFailed" ,
message : "Booking Rescheduling failed" ,
} ;
2023-11-27 15:09:33 -03:00
2023-11-28 15:37:31 -03:00
loggerWithEventDetails . error (
` EventManager.reschedule failure in some of the integrations ${ organizerUser . username } ` ,
safeStringify ( { error , results } )
) ;
} else {
if ( results . length ) {
// Handle Google Meet results
// We use the original booking location since the evt location changes to daily
if ( bookingLocation === MeetLocationType ) {
const googleMeetResult = {
appName : GoogleMeetMetadata.name ,
type : "conferencing" ,
uid : results [ 0 ] . uid ,
originalEvent : results [ 0 ] . originalEvent ,
} ;
2023-11-27 15:09:33 -03:00
2023-11-28 15:37:31 -03:00
// Find index of google_calendar inside createManager.referencesToCreate
const googleCalIndex = updateManager . referencesToCreate . findIndex (
( ref ) = > ref . type === "google_calendar"
) ;
const googleCalResult = results [ googleCalIndex ] ;
if ( ! googleCalResult ) {
loggerWithEventDetails . warn ( "Google Calendar not installed but using Google Meet as location" ) ;
results . push ( {
. . . googleMeetResult ,
success : false ,
calWarnings : [ tOrganizer ( "google_meet_warning" ) ] ,
} ) ;
}
2023-11-27 15:09:33 -03:00
2023-11-28 15:37:31 -03:00
if ( googleCalResult ? . createdEvent ? . hangoutLink ) {
results . push ( {
. . . googleMeetResult ,
success : true ,
} ) ;
2023-11-27 15:09:33 -03:00
2023-11-28 15:37:31 -03:00
// Add google_meet to referencesToCreate in the same index as google_calendar
updateManager . referencesToCreate [ googleCalIndex ] = {
. . . updateManager . referencesToCreate [ googleCalIndex ] ,
meetingUrl : googleCalResult.createdEvent.hangoutLink ,
2023-11-27 15:09:33 -03:00
} ;
2023-11-28 15:37:31 -03:00
// Also create a new referenceToCreate with type video for google_meet
updateManager . referencesToCreate . push ( {
type : "google_meet_video" ,
meetingUrl : googleCalResult.createdEvent.hangoutLink ,
uid : googleCalResult.uid ,
credentialId : updateManager.referencesToCreate [ googleCalIndex ] . credentialId ,
} ) ;
} else if ( googleCalResult && ! googleCalResult . createdEvent ? . hangoutLink ) {
results . push ( {
. . . googleMeetResult ,
success : false ,
} ) ;
2023-11-27 15:09:33 -03:00
}
}
2023-11-28 15:37:31 -03:00
metadata . hangoutLink = results [ 0 ] . createdEvent ? . hangoutLink ;
metadata . conferenceData = results [ 0 ] . createdEvent ? . conferenceData ;
metadata . entryPoints = results [ 0 ] . createdEvent ? . entryPoints ;
evt . appsStatus = handleAppsStatus ( results , booking ) ;
videoCallUrl =
metadata . hangoutLink ||
results [ 0 ] . createdEvent ? . url ||
organizerOrFirstDynamicGroupMemberDefaultLocationUrl ||
videoCallUrl ;
2023-11-27 15:09:33 -03:00
}
2023-11-28 15:37:31 -03:00
const calendarResult = results . find ( ( result ) = > result . type . includes ( "_calendar" ) ) ;
evt . iCalUID = Array . isArray ( calendarResult ? . updatedEvent )
? calendarResult ? . updatedEvent [ 0 ] ? . iCalUID
: calendarResult ? . updatedEvent ? . iCalUID || undefined ;
2023-11-27 15:09:33 -03:00
}
2023-10-17 08:16:24 -03:00
evt . appsStatus = handleAppsStatus ( results , booking ) ;
// If there is an integration error, we don't send successful rescheduling email, instead broken integration email should be sent that are handled by either CalendarManager or videoClient
if ( noEmail !== true && isConfirmedByDefault && ! isThereAnIntegrationError ) {
const copyEvent = cloneDeep ( evt ) ;
2023-11-27 15:09:33 -03:00
const copyEventAdditionalInfo = {
2023-10-17 08:16:24 -03:00
. . . copyEvent ,
additionalInformation : metadata ,
additionalNotes , // Resets back to the additionalNote input and not the override value
cancellationReason : ` $ RCH $ ${ rescheduleReason ? rescheduleReason : "" } ` , // Removable code prefix to differentiate cancellation from rescheduling for email
2023-11-27 15:09:33 -03:00
} ;
loggerWithEventDetails . debug ( "Emails: Sending rescheduled emails for booking confirmation" ) ;
/ *
handle emails for round robin
- if booked rr host is the same , then rescheduling email
- if new rr host is booked , then cancellation email to old host and confirmation email to new host
* /
if ( eventType . schedulingType === SchedulingType . ROUND_ROBIN ) {
const originalBookingMemberEmails : Person [ ] = [ ] ;
for ( const user of originalRescheduledBooking . attendees ) {
const translate = await getTranslation ( user . locale ? ? "en" , "common" ) ;
originalBookingMemberEmails . push ( {
name : user.name ,
email : user.email ,
timeZone : user.timeZone ,
language : { translate , locale : user.locale ? ? "en" } ,
} ) ;
}
if ( originalRescheduledBooking . user ) {
const translate = await getTranslation ( originalRescheduledBooking . user . locale ? ? "en" , "common" ) ;
originalBookingMemberEmails . push ( {
. . . originalRescheduledBooking . user ,
name : originalRescheduledBooking.user.name || "" ,
language : { translate , locale : originalRescheduledBooking.user.locale ? ? "en" } ,
} ) ;
}
const newBookingMemberEmails : Person [ ] =
copyEvent . team ? . members
. map ( ( member ) = > member )
. concat ( copyEvent . organizer )
. concat ( copyEvent . attendees ) || [ ] ;
// scheduled Emails
const newBookedMembers = newBookingMemberEmails . filter (
( member ) = >
! originalBookingMemberEmails . find ( ( originalMember ) = > originalMember . email === member . email )
) ;
// cancelled Emails
const cancelledMembers = originalBookingMemberEmails . filter (
( member ) = > ! newBookingMemberEmails . find ( ( newMember ) = > newMember . email === member . email )
) ;
// rescheduled Emails
const rescheduledMembers = newBookingMemberEmails . filter ( ( member ) = >
originalBookingMemberEmails . find ( ( orignalMember ) = > orignalMember . email === member . email )
) ;
sendRoundRobinRescheduledEmails ( copyEventAdditionalInfo , rescheduledMembers ) ;
sendRoundRobinScheduledEmails ( copyEventAdditionalInfo , newBookedMembers ) ;
sendRoundRobinCancelledEmails ( copyEventAdditionalInfo , cancelledMembers ) ;
} else {
// send normal rescheduled emails (non round robin event, where organizers stay the same)
await sendRescheduledEmails ( {
. . . copyEvent ,
additionalInformation : metadata ,
additionalNotes , // Resets back to the additionalNote input and not the override value
cancellationReason : ` $ RCH $ ${ rescheduleReason ? rescheduleReason : "" } ` , // Removable code prefix to differentiate cancellation from rescheduling for email
} ) ;
}
2022-10-12 10:04:51 -03:00
}
// If it's not a reschedule, doesn't require confirmation and there's no price,
// Create a booking
2023-10-13 14:22:57 -03:00
} else if ( isConfirmedByDefault ) {
2022-10-12 10:04:51 -03:00
// Use EventManager to conditionally use all needed integrations.
const createManager = await eventManager . create ( evt ) ;
// This gets overridden when creating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt . description = eventType . description ;
results = createManager . results ;
referencesToCreate = createManager . referencesToCreate ;
2022-12-15 18:43:07 -03:00
videoCallUrl = evt . videoCallData && evt . videoCallData . url ? evt.videoCallData.url : null ;
2022-10-12 10:04:51 -03:00
if ( results . length > 0 && results . every ( ( res ) = > ! res . success ) ) {
const error = {
errorCode : "BookingCreatingMeetingFailed" ,
message : "Booking failed" ,
} ;
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . error (
2023-10-17 08:16:24 -03:00
` EventManager.create failure in some of the integrations ${ organizerUser . username } ` ,
2023-09-30 10:28:52 -03:00
safeStringify ( { error , results } )
2023-09-28 10:01:24 -03:00
) ;
2022-10-12 10:04:51 -03:00
} else {
const metadata : AdditionalInformation = { } ;
if ( results . length ) {
2023-01-09 23:01:57 -03:00
// Handle Google Meet results
// We use the original booking location since the evt location changes to daily
if ( bookingLocation === MeetLocationType ) {
const googleMeetResult = {
appName : GoogleMeetMetadata.name ,
type : "conferencing" ,
uid : results [ 0 ] . uid ,
originalEvent : results [ 0 ] . originalEvent ,
} ;
2023-05-11 19:49:10 -03:00
// Find index of google_calendar inside createManager.referencesToCreate
const googleCalIndex = createManager . referencesToCreate . findIndex (
( ref ) = > ref . type === "google_calendar"
) ;
const googleCalResult = results [ googleCalIndex ] ;
2023-01-09 23:01:57 -03:00
if ( ! googleCalResult ) {
2023-10-02 07:51:04 -03:00
loggerWithEventDetails . warn ( "Google Calendar not installed but using Google Meet as location" ) ;
2023-01-09 23:01:57 -03:00
results . push ( {
. . . googleMeetResult ,
success : false ,
calWarnings : [ tOrganizer ( "google_meet_warning" ) ] ,
} ) ;
}
if ( googleCalResult ? . createdEvent ? . hangoutLink ) {
results . push ( {
. . . googleMeetResult ,
success : true ,
} ) ;
2023-05-11 19:49:10 -03:00
// Add google_meet to referencesToCreate in the same index as google_calendar
createManager . referencesToCreate [ googleCalIndex ] = {
. . . createManager . referencesToCreate [ googleCalIndex ] ,
meetingUrl : googleCalResult.createdEvent.hangoutLink ,
} ;
// Also create a new referenceToCreate with type video for google_meet
createManager . referencesToCreate . push ( {
type : "google_meet_video" ,
meetingUrl : googleCalResult.createdEvent.hangoutLink ,
uid : googleCalResult.uid ,
credentialId : createManager.referencesToCreate [ googleCalIndex ] . credentialId ,
} ) ;
2023-01-09 23:01:57 -03:00
} else if ( googleCalResult && ! googleCalResult . createdEvent ? . hangoutLink ) {
results . push ( {
. . . googleMeetResult ,
success : false ,
} ) ;
}
}
2022-10-12 10:04:51 -03:00
// 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 ;
2023-10-17 08:16:24 -03:00
evt . appsStatus = handleAppsStatus ( results , booking ) ;
2023-06-23 14:04:34 -03:00
videoCallUrl =
metadata . hangoutLink || organizerOrFirstDynamicGroupMemberDefaultLocationUrl || videoCallUrl ;
2023-12-15 12:28:32 -03:00
if ( evt . iCalUID !== booking . iCalUID ) {
// The eventManager could change the iCalUID. At this point we can update the DB record
await prisma . booking . update ( {
where : {
id : booking.id ,
} ,
data : {
iCalUID : evt.iCalUID || booking . iCalUID ,
} ,
} ) ;
}
2022-10-12 10:04:51 -03:00
}
if ( noEmail !== true ) {
2023-05-09 14:08:14 -03:00
let isHostConfirmationEmailsDisabled = false ;
let isAttendeeConfirmationEmailDisabled = false ;
const workflows = eventType . workflows . map ( ( workflow ) = > workflow . workflow ) ;
if ( eventType . workflows ) {
isHostConfirmationEmailsDisabled =
eventType . metadata ? . disableStandardEmails ? . confirmation ? . host || false ;
isAttendeeConfirmationEmailDisabled =
eventType . metadata ? . disableStandardEmails ? . confirmation ? . attendee || false ;
if ( isHostConfirmationEmailsDisabled ) {
isHostConfirmationEmailsDisabled = allowDisablingHostConfirmationEmails ( workflows ) ;
}
if ( isAttendeeConfirmationEmailDisabled ) {
isAttendeeConfirmationEmailDisabled = allowDisablingAttendeeConfirmationEmails ( workflows ) ;
}
}
2023-10-02 07:51:04 -03:00
loggerWithEventDetails . debug (
2023-10-13 14:22:57 -03:00
"Emails: Sending scheduled emails for booking confirmation" ,
2023-10-02 07:51:04 -03:00
safeStringify ( {
calEvent : getPiiFreeCalendarEvent ( evt ) ,
} )
) ;
2023-03-10 11:38:29 -03:00
await sendScheduledEmails (
{
. . . evt ,
additionalInformation : metadata ,
additionalNotes ,
customInputs ,
} ,
2023-05-09 14:08:14 -03:00
eventNameObject ,
isHostConfirmationEmailsDisabled ,
isAttendeeConfirmationEmailDisabled
2023-03-10 11:38:29 -03:00
) ;
2022-10-12 10:04:51 -03:00
}
}
2023-10-13 14:22:57 -03:00
} else {
// If isConfirmedByDefault is false, then booking can't be considered ACCEPTED and thus EventManager has no role to play. Booking is created as PENDING
loggerWithEventDetails . debug (
` EventManager doesn't need to create or reschedule event for booking ${ organizerUser . username } ` ,
safeStringify ( {
calEvent : getPiiFreeCalendarEvent ( evt ) ,
isConfirmedByDefault ,
paymentValue : paymentAppData.price ,
} )
) ;
2022-10-12 10:04:51 -03:00
}
2023-04-11 18:44:14 -03:00
const bookingRequiresPayment =
! Number . isNaN ( paymentAppData . price ) &&
paymentAppData . price > 0 &&
! originalRescheduledBooking ? . paid &&
! ! booking ;
if ( ! isConfirmedByDefault && noEmail !== true && ! bookingRequiresPayment ) {
2023-10-02 07:51:04 -03:00
loggerWithEventDetails . debug (
2023-10-13 14:22:57 -03:00
` Emails: Booking ${ organizerUser . username } requires confirmation, sending request emails ` ,
2023-10-02 07:51:04 -03:00
safeStringify ( {
calEvent : getPiiFreeCalendarEvent ( evt ) ,
} )
) ;
2022-10-12 10:04:51 -03:00
await sendOrganizerRequestEmail ( { . . . evt , additionalNotes } ) ;
await sendAttendeeRequestEmail ( { . . . evt , additionalNotes } , attendeesList [ 0 ] ) ;
}
2023-10-02 07:51:04 -03:00
2023-09-30 01:52:32 -03:00
const metadata = videoCallUrl
? {
2023-11-27 15:09:33 -03:00
videoCallUrl : getVideoCallUrlFromCalEvent ( evt ) || videoCallUrl ,
2023-09-30 01:52:32 -03:00
}
: undefined ;
2023-10-17 08:16:24 -03:00
2023-09-30 01:52:32 -03:00
const webhookData = {
. . . evt ,
. . . eventTypeInfo ,
bookingId : booking?.id ,
2023-10-25 14:26:22 -03:00
rescheduleId : originalRescheduledBooking?.id || undefined ,
2023-09-30 01:52:32 -03:00
rescheduleUid ,
rescheduleStartTime : originalRescheduledBooking?.startTime
? dayjs ( originalRescheduledBooking ? . startTime ) . utc ( ) . format ( )
: undefined ,
rescheduleEndTime : originalRescheduledBooking?.endTime
? dayjs ( originalRescheduledBooking ? . endTime ) . utc ( ) . format ( )
: undefined ,
metadata : { . . . metadata , . . . reqBody . metadata } ,
eventTypeId ,
status : "ACCEPTED" ,
smsReminderNumber : booking?.smsReminderNumber || undefined ,
} ;
2022-10-12 10:04:51 -03:00
2023-04-11 18:44:14 -03:00
if ( bookingRequiresPayment ) {
2023-10-02 07:51:04 -03:00
loggerWithEventDetails . debug ( ` Booking ${ organizerUser . username } requires payment ` ) ;
2023-02-08 17:36:22 -03:00
// Load credentials.app.categories
const credentialPaymentAppCategories = await prisma . credential . findMany ( {
where : {
feat: Enable Apps for Teams & Orgs [CAL-1782] (#9337)
* Initial commit
* Adding feature flag
* Add schema relation for teams and credentials
* feat: Orgs Schema Changing `scopedMembers` to `orgUsers` (#9209)
* Change scopedMembers to orgMembers
* Change to orgUsers
* Create getUserAdminTeams function & tRPC endpoint
* Get user admin teams on app store page
* Create UserAdminTeams type
* Add user query to getUserAdminTeams
* Letting duplicate slugs for teams to support orgs
* Covering null on unique clauses
* Add dropdown to install button on app store
* Supporting having the orgId in the session cookie
* On app page, only show dropdown if there are teams
* Add teamId to OAuth state
* Create team credential for OAuth flow
* Create team credential for GCal
* Add create user or team credential for Stripe
* Create webex credentials for users or teams
* Fix type error on useAddAppMutation
* Hubspot create credential on user or team
* Zoho create create credential for user or team
* Zoom create credentials on user or team
* Salesforce create credential on user or teams
* OAuth create credentials for user or teams
* Revert Outlook changes
* Revert GCal changes
* Default app instal, create credential on user or team
* Add teamId to credential creation
* Disable installing for teams for calendars
* Include teams when querying installed apps
* Render team credentials on installed page
* Uninstall team apps
* Type fix on app card
* Add input to include user in teams query
* Add dropdown to install app page for user or team
* Type fixes on category page
* Install app from eventType page to user or team
* Render user and team apps on event type app page
* feat: organization event type filter (#9253)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* Missing changes to support orgs schema changes
* Render user and team apps on event type app page
* Add credentialOwner to eventTypeAppCard types
* Type fixes
* Create hook to check if app is enabled
* Clean up console.logs
* Fix useIsAppEnabled by returning not an array
* Convert event type apps to useIsAppEnabled
* Abstract credential owner type
* Remove console.logs
* On installed app page, show apps if only team credential is installed
* Clean up commented lines
* Handle installing app to just an team event from event type page
* Fix early return when creating team app credential
* Zoom add state to callback
* Get team location credentials and save credential id to location
* feat: Onboarding process to create an organization (#9184)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Making sure we check requestedSlug now
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Type fix
* Grab team location credentials
* Add isInstalled to eventType apps query
* feat: [CAL-1816] Organization subdomain support (#9345)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* Covering users and subteams, excluding non-org users
* Unpublished teams shows correctly
* Create subdomain in Vercel
* feedback
* Renaming Vercel env vars
* Vercel domain check before creation
* Supporting cal-staging.com
* Change to have vercel detect it
* vercel domain check data message error
* Remove check domain
* Making sure we check requestedSlug now
* Feedback and unneeded code
* Reverting unneeded changes
* Unneeded changes
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Vercel subdomain creation in PROD only
* Enable payment apps for team credentials
* Fix for team-user apps for event types
* Fix layout and add teamId to app card
* Disable apps on managed event types
* Add managed event type fields to event type apps
* Include organizations in query
* Change createAppCredential to createOAuthAppCredential
* Show app installed on teams
* Making sure we let localhost still work
* UI show installed for which team
* Type fixes
* For team events move use host location to top
* Add around to appStore
* New team event types organizer default conf app
* Fix app card bug
* Clean up
* Search for teamId or userId when deleting credential
* Type fixes
* Type fixes
* Type fixes
* Type fixes
* Address feedback
* Feedback
* Type check fixes
* feat: Organization branding in side menu (#9279)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Making sure we show the set up profile on org only
* Profile username availability rely on org hook
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Update apps/web/pages/team/[slug].tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* feat: Organization support for event types page (#9449)
* Desktop first banner, mobile pending
* Removing dead code and img
* WIP
* Adds Email verification template+translations for organizations (#9202)
* First step done
* Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding
* Step 2 done, avatar not working
* Covering null on unique clauses
* Onboarding admins step
* Last step to create teams
* Moving change password handler, improving verifying code flow
* Clearing error before submitting
* Reverting email testing api changes
* Reverting having the banner for now
* Consistent exported components
* Remove unneeded files from banner
* Removing uneeded code
* Fixing avatar selector
* Org branding provider used in shell sidebar
* Using meta component for head/descr
* Missing i18n strings
* Feedback
* Making an org avatar (temp)
* Using org avatar (temp)
* Not showing org logo if not set
* User onboarding with org branding (slug)
* Check for subteams slug clashes with usernames
* Fixing create teams onsuccess
* feedback
* Feedback
* Org public profile
* Public profiles for team event types
* Added setup profile alert
* Using org avatar on subteams avatar
* Processing orgs and children as profile options
* Reverting change not belonging to this PR
* Making sure we show the set up profile on org only
* Removing console.log
* Comparing memberships to choose the highest one
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Type errors
* Refactor and type fixes
* Update orgDomains.ts
* Feedback
* Reverting
* NIT
* Address feedback
* fix issue getting org slug from domain
* Improving orgDomains util
* Host comes with port
* Update useRouterQuery.ts
* Fix app card bug
* Fix schema
* Type fixes
* Revert changes to location apps
* Remove console.log
* Fix app store test
* Handle install app dropdown
* Add CalendarApp to `getCalendar`
* Add PaymentApp type fix
* Payment type fix
* Type fixes
* Match with main
* Change type to account for team
* Fix app count for team events
* Type fixes
* More type fixes
* Type fix?
* Fix the type fix
* Remove UserAdminTeams empty array union
* Type fix
* Type fix
* Type fix
* Uses type predicates
* Use teamId. Fixes installation for teams after user installation
* Fix Team Events not working
* Get embed for org events working
* Fix rewrites
* Address feedback
* Type fix
* Fixes
* Add useAppContextWithSchema in useIsAppEnabled
* Type fix for apps using useIsAppEnabled
* Integrations.handler change credentialIds to userCredentialIds
* Remove apps endpoint
* Add LockedIcon and disabled props to event type app context
* Type fixes
* Type fix
* Type fixes
* Show team installed apps for members
* Type fix
* Reverting findFirst
* Revert findFirst
* Avoid a possible 500
* Fix missing tanslation
* Avoid possible 500
* Undo default app for teams
* Type fix
* Fix test
* Update package.json
* feat: Fix invite bug - added tests (#9945)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
* chore: Button Component Tidy up (#9888)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
* feat: Make Team Private
## What does this PR do?
Fixes https://github.com/calcom/cal.com/issues/8974
1) When user is admin
<img width="1440" alt="Screenshot 2023-07-03 at 6 45 50 PM" src="https://github.com/calcom/cal.com/assets/53316345/ce15158f-d278-4f1a-ba2e-8b63e4274793">
2) When user is not admin and team is private
<img width="1440" alt="Screenshot 2023-07-03 at 6 47 15 PM" src="https://github.com/calcom/cal.com/assets/53316345/ce23560e-690a-4c42-a76d-49691260aa4d">
3)
<img width="1440" alt="Screenshot 2023-07-03 at 6 51 56 PM" src="https://github.com/calcom/cal.com/assets/53316345/13af38f8-5618-4dae-b359-b24dc91e4eb4">
## Type of change
<!-- Please delete bullets that are not relevant. -->
- New feature (non-breaking change which adds functionality)
## How should this be tested?
1) go to Team members page and turn on switch Make Team Private.
Now after making the team private only admin would be able to see all the members list in the settings. There will not be a button to Book a team member instead on the team page like before.
## Mandatory Tasks
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
---------
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-07-06 13:48:39 -03:00
. . . ( paymentAppData . credentialId ? { id : paymentAppData.credentialId } : { userId : organizerUser.id } ) ,
2023-02-08 17:36:22 -03:00
app : {
categories : {
hasSome : [ "payment" ] ,
} ,
} ,
} ,
select : {
key : true ,
appId : true ,
app : {
select : {
categories : true ,
dirName : true ,
} ,
} ,
} ,
} ) ;
const eventTypePaymentAppCredential = credentialPaymentAppCategories . find ( ( credential ) = > {
return credential . appId === paymentAppData . appId ;
} ) ;
2022-10-12 10:04:51 -03:00
2023-02-08 17:36:22 -03:00
if ( ! eventTypePaymentAppCredential ) {
2022-10-12 10:04:51 -03:00
throw new HttpError ( { statusCode : 400 , message : "Missing payment credentials" } ) ;
2023-02-08 17:36:22 -03:00
}
2022-10-12 10:04:51 -03:00
2023-02-08 17:36:22 -03:00
// Convert type of eventTypePaymentAppCredential to appId: EventTypeAppList
2022-10-12 10:04:51 -03:00
if ( ! booking . user ) booking . user = organizerUser ;
2023-02-08 17:36:22 -03:00
const payment = await handlePayment (
evt ,
eventType ,
eventTypePaymentAppCredential as IEventTypePaymentCredentialType ,
2023-04-11 18:44:14 -03:00
booking ,
2023-09-14 01:10:42 -03:00
fullName ,
2023-04-11 18:44:14 -03:00
bookerEmail
2023-02-08 17:36:22 -03:00
) ;
2023-09-30 01:52:32 -03:00
const subscriberOptionsPaymentInitiated : GetSubscriberOptions = {
userId : triggerForUser ? organizerUser.id : null ,
eventTypeId ,
triggerEvent : WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED ,
teamId ,
} ;
await handleWebhookTrigger ( {
subscriberOptions : subscriberOptionsPaymentInitiated ,
eventTrigger : WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED ,
webhookData : {
. . . webhookData ,
paymentId : payment?.id ,
} ,
} ) ;
2022-10-12 10:04:51 -03:00
req . statusCode = 201 ;
2023-09-30 01:52:32 -03:00
return { . . . booking , message : "Payment required" , paymentUid : payment?.uid , paymentId : payment?.id } ;
2022-10-12 10:04:51 -03:00
}
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . debug ( ` Booking ${ organizerUser . username } completed ` ) ;
2023-02-20 15:52:07 -03:00
if ( booking . location ? . startsWith ( "http" ) ) {
videoCallUrl = booking . location ;
}
2023-10-13 14:22:57 -03:00
// We are here so, booking doesn't require payment and booking is also created in DB already, through createBooking call
2022-11-23 13:38:13 -03:00
if ( isConfirmedByDefault ) {
try {
const subscribersMeetingEnded = await getWebhooks ( subscriberOptionsMeetingEnded ) ;
2023-12-19 11:52:35 -03:00
const subscribersMeetingStarted = await getWebhooks ( subscriberOptionsMeetingStarted ) ;
2022-10-12 10:04:51 -03:00
2022-11-23 13:38:13 -03:00
subscribersMeetingEnded . forEach ( ( subscriber ) = > {
if ( rescheduleUid && originalRescheduledBooking ) {
cancelScheduledJobs ( originalRescheduledBooking , undefined , true ) ;
}
if ( booking && booking . status === BookingStatus . ACCEPTED ) {
2023-12-19 11:52:35 -03:00
scheduleTrigger ( booking , subscriber . subscriberUrl , subscriber , WebhookTriggerEvents . MEETING_ENDED ) ;
}
} ) ;
subscribersMeetingStarted . forEach ( ( subscriber ) = > {
if ( rescheduleUid && originalRescheduledBooking ) {
cancelScheduledJobs ( originalRescheduledBooking , undefined , true ) ;
}
if ( booking && booking . status === BookingStatus . ACCEPTED ) {
scheduleTrigger (
booking ,
subscriber . subscriberUrl ,
subscriber ,
WebhookTriggerEvents . MEETING_STARTED
) ;
2022-11-23 13:38:13 -03:00
}
} ) ;
} catch ( error ) {
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . error (
"Error while running scheduledJobs for booking" ,
JSON . stringify ( { error } )
) ;
2022-11-23 13:38:13 -03:00
}
2022-10-24 21:32:14 -03:00
2023-05-30 12:35:05 -03:00
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
await handleWebhookTrigger ( { subscriberOptions , eventTrigger , webhookData } ) ;
2023-10-13 14:22:57 -03:00
} else {
2023-05-30 12:35:05 -03:00
// if eventType requires confirmation we will trigger the BOOKING REQUESTED Webhook
const eventTrigger : WebhookTriggerEvents = WebhookTriggerEvents . BOOKING_REQUESTED ;
subscriberOptions . triggerEvent = eventTrigger ;
webhookData . status = "PENDING" ;
await handleWebhookTrigger ( { subscriberOptions , eventTrigger , webhookData } ) ;
2022-10-24 21:32:14 -03:00
}
2022-11-23 13:38:13 -03:00
2022-10-12 10:04:51 -03:00
// Avoid passing referencesToCreate with id unique constrain values
// refresh hashed link if used
const urlSeed = ` ${ organizerUser . username } : ${ dayjs ( reqBody . start ) . utc ( ) . format ( ) } ` ;
const hashedUid = translator . fromUUID ( uuidv5 ( urlSeed , uuidv5 . URL ) ) ;
2022-10-24 21:32:14 -03:00
try {
if ( hasHashedBookingLink ) {
await prisma . hashedLink . update ( {
where : {
link : reqBody.hashedLink as string ,
} ,
data : {
link : hashedUid ,
} ,
} ) ;
}
} catch ( error ) {
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . error ( "Error while updating hashed link" , JSON . stringify ( { error } ) ) ;
2022-10-24 21:32:14 -03:00
}
if ( ! booking ) throw new HttpError ( { statusCode : 400 , message : "Booking failed" } ) ;
try {
await prisma . booking . update ( {
2022-10-12 10:04:51 -03:00
where : {
2022-10-24 21:32:14 -03:00
uid : booking.uid ,
2022-10-12 10:04:51 -03:00
} ,
data : {
2023-02-06 15:05:30 -03:00
metadata : { . . . ( typeof booking . metadata === "object" && booking . metadata ) , . . . metadata } ,
2022-10-24 21:32:14 -03:00
references : {
createMany : {
data : referencesToCreate ,
} ,
} ,
2022-10-12 10:04:51 -03:00
} ,
} ) ;
2022-10-24 21:32:14 -03:00
} catch ( error ) {
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . error ( "Error while creating booking references" , JSON . stringify ( { error } ) ) ;
2022-10-24 21:32:14 -03:00
}
2023-04-21 08:46:23 -03:00
const metadataFromEvent = videoCallUrl ? { videoCallUrl } : undefined ;
2023-12-12 23:23:48 -03:00
const evtWithMetadata = { . . . evt , metadata : metadataFromEvent , eventType : { slug : eventType.slug } } ;
await scheduleMandatoryReminder (
evtWithMetadata ,
eventType . workflows || [ ] ,
! isConfirmedByDefault ,
! ! eventType . owner ? . hideBranding ,
evt . attendeeSeatId
) ;
2023-04-13 16:03:08 -03:00
2022-10-24 21:32:14 -03:00
try {
2023-04-13 16:03:08 -03:00
await scheduleWorkflowReminders ( {
workflows : eventType.workflows ,
smsReminderNumber : smsReminderNumber || null ,
2023-12-12 23:23:48 -03:00
calendarEvent : evtWithMetadata ,
2023-11-13 11:28:15 -03:00
isNotConfirmed : ! isConfirmedByDefault ,
2023-04-13 16:03:08 -03:00
isRescheduleEvent : ! ! rescheduleUid ,
isFirstRecurringEvent : true ,
2023-04-18 07:08:09 -03:00
hideBranding : ! ! eventType . owner ? . hideBranding ,
2023-08-01 11:13:28 -03:00
seatReferenceUid : evt.attendeeSeatId ,
2023-09-21 03:22:05 -03:00
eventTypeRequiresConfirmation : eventType.requiresConfirmation ,
2023-04-13 16:03:08 -03:00
} ) ;
2022-10-24 21:32:14 -03:00
} catch ( error ) {
2023-09-28 10:01:24 -03:00
loggerWithEventDetails . error ( "Error while scheduling workflow reminders" , JSON . stringify ( { error } ) ) ;
2022-10-12 10:04:51 -03:00
}
// booking successful
req . statusCode = 201 ;
2023-03-14 01:19:05 -03:00
return {
. . . booking ,
seatReferenceUid : evt.attendeeSeatId ,
} ;
2022-10-12 10:04:51 -03:00
}
export default handler ;
2022-10-14 19:45:02 -03:00
2023-10-17 08:16:24 -03:00
function getVideoCallDetails ( {
results ,
} : {
results : EventResult < AdditionalInformation & { url ? : string | undefined ; iCalUID ? : string | undefined } > [ ] ;
} ) {
const firstVideoResult = results . find ( ( result ) = > result . type . includes ( "_video" ) ) ;
const metadata : AdditionalInformation = { } ;
let updatedVideoEvent = null ;
if ( firstVideoResult && firstVideoResult . success ) {
updatedVideoEvent = Array . isArray ( firstVideoResult . updatedEvent )
? firstVideoResult . updatedEvent [ 0 ]
: firstVideoResult . updatedEvent ;
if ( updatedVideoEvent ) {
metadata . hangoutLink = updatedVideoEvent . hangoutLink ;
metadata . conferenceData = updatedVideoEvent . conferenceData ;
metadata . entryPoints = updatedVideoEvent . entryPoints ;
}
}
const videoCallUrl = metadata . hangoutLink || updatedVideoEvent ? . url ;
return { videoCallUrl , metadata , updatedVideoEvent } ;
}
2023-10-13 14:22:57 -03:00
function getRequiresConfirmationFlags ( {
eventType ,
bookingStartTime ,
userId ,
paymentAppData ,
originalRescheduledBookingOrganizerId ,
} : {
eventType : Pick < Awaited < ReturnType < typeof getEventTypesFromDB > > , "metadata" | "requiresConfirmation" > ;
bookingStartTime : string ;
userId : number | undefined ;
paymentAppData : { price : number } ;
originalRescheduledBookingOrganizerId : number | undefined ;
} ) {
let requiresConfirmation = eventType ? . requiresConfirmation ;
const rcThreshold = eventType ? . metadata ? . requiresConfirmationThreshold ;
if ( rcThreshold ) {
if ( dayjs ( dayjs ( bookingStartTime ) . utc ( ) . format ( ) ) . diff ( dayjs ( ) , rcThreshold . unit ) > rcThreshold . time ) {
requiresConfirmation = false ;
}
}
// If the user is not the owner of the event, new booking should be always pending.
// Otherwise, an owner rescheduling should be always accepted.
// Before comparing make sure that userId is set, otherwise undefined === undefined
const userReschedulingIsOwner = ! ! ( userId && originalRescheduledBookingOrganizerId === userId ) ;
const isConfirmedByDefault = ( ! requiresConfirmation && ! paymentAppData . price ) || userReschedulingIsOwner ;
return {
/ * *
* Organizer of the booking is rescheduling
* /
userReschedulingIsOwner ,
/ * *
* Booking won ' t need confirmation to be ACCEPTED
* /
isConfirmedByDefault ,
} ;
}
2022-10-14 19:45:02 -03:00
function handleCustomInputs (
eventTypeCustomInputs : EventTypeCustomInput [ ] ,
reqCustomInputs : {
value : string | boolean ;
label : string ;
} [ ]
) {
eventTypeCustomInputs . forEach ( ( etcInput ) = > {
if ( etcInput . required ) {
const input = reqCustomInputs . find ( ( i ) = > i . label === etcInput . label ) ;
if ( etcInput . type === "BOOL" ) {
z . literal ( true , {
errorMap : ( ) = > ( { message : ` Missing ${ etcInput . type } customInput: ' ${ etcInput . label } ' ` } ) ,
} ) . parse ( input ? . value ) ;
2022-12-16 16:39:41 -03:00
} else if ( etcInput . type === "PHONE" ) {
z . string ( {
errorMap : ( ) = > ( {
message : ` Missing ${ etcInput . type } customInput: ' ${ etcInput . label } ' ` ,
} ) ,
} )
. refine ( ( val ) = > isValidPhoneNumber ( val ) , {
message : "Phone number is invalid" ,
} )
. parse ( input ? . value ) ;
2022-10-14 19:45:02 -03:00
} else {
// type: NUMBER are also passed as string
z . string ( {
errorMap : ( ) = > ( { message : ` Missing ${ etcInput . type } customInput: ' ${ etcInput . label } ' ` } ) ,
} )
. min ( 1 )
. parse ( input ? . value ) ;
}
}
} ) ;
}
2023-03-14 01:19:05 -03:00
2023-04-13 16:03:08 -03:00
const findBookingQuery = async ( bookingId : number ) = > {
2023-03-14 01:19:05 -03:00
const foundBooking = await prisma . booking . findUnique ( {
where : {
id : bookingId ,
} ,
select : {
uid : true ,
location : true ,
startTime : true ,
endTime : true ,
title : true ,
description : true ,
status : true ,
responses : true ,
user : {
select : {
name : true ,
email : true ,
timeZone : true ,
2023-09-14 01:10:42 -03:00
username : true ,
2023-03-14 01:19:05 -03:00
} ,
} ,
eventType : {
select : {
title : true ,
description : true ,
currency : true ,
length : true ,
2023-10-25 15:16:01 -03:00
lockTimeZoneToggleOnBookingPage : true ,
2023-03-14 01:19:05 -03:00
requiresConfirmation : true ,
2023-07-31 14:51:11 -03:00
requiresBookerEmailVerification : true ,
2023-03-14 01:19:05 -03:00
price : true ,
} ,
} ,
} ,
} ) ;
// This should never happen but it's just typescript safe
if ( ! foundBooking ) {
2023-11-15 16:52:19 -03:00
throw new Error ( "Internal Error. Couldn't find booking" ) ;
2023-03-14 01:19:05 -03:00
}
// Don't leak any sensitive data
return foundBooking ;
} ;