2023-03-14 04:19:05 +00:00
|
|
|
import type { App, Attendee, Credential, EventTypeCustomInput } from "@prisma/client";
|
2023-05-02 11:44:05 +00:00
|
|
|
import { Prisma } from "@prisma/client";
|
2022-10-12 13:04:51 +00:00
|
|
|
import async from "async";
|
2022-12-16 19:39:41 +00:00
|
|
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
2022-11-05 18:58:35 +00:00
|
|
|
import { cloneDeep } from "lodash";
|
2022-10-12 13:04:51 +00:00
|
|
|
import type { NextApiRequest } from "next";
|
2023-03-14 04:19:05 +00:00
|
|
|
import short, { uuid } from "short-uuid";
|
2022-10-12 13:04:51 +00:00
|
|
|
import { v5 as uuidv5 } from "uuid";
|
2022-10-14 22:45:02 +00:00
|
|
|
import z from "zod";
|
2022-10-12 13:04:51 +00:00
|
|
|
|
2023-03-14 04:19:05 +00:00
|
|
|
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
2023-01-10 02:01:57 +00:00
|
|
|
import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_metadata";
|
2023-02-19 07:43:36 +00:00
|
|
|
import type { LocationObject } from "@calcom/app-store/locations";
|
2023-08-07 22:08:13 +00:00
|
|
|
import {
|
|
|
|
getLocationValueForDB,
|
|
|
|
MeetLocationType,
|
|
|
|
OrganizerDefaultConferencingAppType,
|
|
|
|
} from "@calcom/app-store/locations";
|
2023-02-19 07:43:36 +00:00
|
|
|
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
2023-06-06 11:59:57 +00:00
|
|
|
import { getAppFromSlug } from "@calcom/app-store/utils";
|
2022-10-12 13:04:51 +00:00
|
|
|
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
|
|
|
import EventManager from "@calcom/core/EventManager";
|
|
|
|
import { getEventName } from "@calcom/core/event";
|
|
|
|
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
2023-03-14 04:19:05 +00:00
|
|
|
import { deleteMeeting } from "@calcom/core/videoClient";
|
2023-02-19 07:43:36 +00:00
|
|
|
import dayjs from "@calcom/dayjs";
|
2022-10-12 13:04:51 +00:00
|
|
|
import {
|
|
|
|
sendAttendeeRequestEmail,
|
|
|
|
sendOrganizerRequestEmail,
|
|
|
|
sendRescheduledEmails,
|
2023-03-14 04:19:05 +00:00
|
|
|
sendRescheduledSeatEmail,
|
2023-08-07 22:08:13 +00:00
|
|
|
sendScheduledEmails,
|
2022-10-12 13:04:51 +00:00
|
|
|
sendScheduledSeatsEmails,
|
|
|
|
} from "@calcom/emails";
|
2023-03-02 18:15:28 +00:00
|
|
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
2023-03-27 08:27:10 +00:00
|
|
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
2023-05-30 15:35:05 +00:00
|
|
|
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
|
2023-05-09 17:08:14 +00:00
|
|
|
import {
|
|
|
|
allowDisablingAttendeeConfirmationEmails,
|
|
|
|
allowDisablingHostConfirmationEmails,
|
|
|
|
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
2023-08-10 18:52:36 +00:00
|
|
|
import { isEventTypeOwnerKYCVerified } from "@calcom/features/ee/workflows/lib/isEventTypeOwnerKYCVerified";
|
2023-07-31 17:35:48 +00:00
|
|
|
import {
|
|
|
|
cancelWorkflowReminders,
|
2023-08-07 22:08:13 +00:00
|
|
|
scheduleWorkflowReminders,
|
2023-07-31 17:35:48 +00:00
|
|
|
} from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
2023-07-20 05:03:50 +00:00
|
|
|
import { getFullName } from "@calcom/features/form-builder/utils";
|
2023-05-30 15:35:05 +00:00
|
|
|
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
|
2022-10-12 13:04:51 +00:00
|
|
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
|
|
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
2023-04-13 19:03:08 +00:00
|
|
|
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
2023-06-26 19:44:58 +00:00
|
|
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
2023-08-07 22:08:13 +00:00
|
|
|
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
|
2022-10-12 13:04:51 +00:00
|
|
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
2023-06-26 19:44:58 +00:00
|
|
|
import getIP from "@calcom/lib/getIP";
|
2023-02-08 20:36:22 +00:00
|
|
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
2023-07-25 17:05:02 +00:00
|
|
|
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
2022-10-12 13:04:51 +00:00
|
|
|
import { HttpError } from "@calcom/lib/http-error";
|
|
|
|
import isOutOfBounds, { BookingDateInPastError } from "@calcom/lib/isOutOfBounds";
|
|
|
|
import logger from "@calcom/lib/logger";
|
2023-02-08 20:36:22 +00:00
|
|
|
import { handlePayment } from "@calcom/lib/payment/handlePayment";
|
2023-03-10 20:00:19 +00:00
|
|
|
import { checkBookingLimits, checkDurationLimits, getLuckyUser } from "@calcom/lib/server";
|
2023-07-13 13:52:21 +00:00
|
|
|
import { getBookerUrl } from "@calcom/lib/server/getBookerUrl";
|
2022-10-12 13:04:51 +00:00
|
|
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
2023-03-02 18:15:28 +00:00
|
|
|
import { slugify } from "@calcom/lib/slugify";
|
2022-10-12 13:04:51 +00:00
|
|
|
import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/SyncServiceManager";
|
2023-07-19 14:30:37 +00:00
|
|
|
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
2022-10-12 13:04:51 +00:00
|
|
|
import prisma, { userSelect } from "@calcom/prisma";
|
2023-03-14 04:19:05 +00:00
|
|
|
import type { BookingReference } from "@calcom/prisma/client";
|
2023-07-31 17:35:48 +00:00
|
|
|
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
2022-12-01 21:53:52 +00:00
|
|
|
import {
|
2023-03-02 18:15:28 +00:00
|
|
|
bookingCreateBodySchemaForApi,
|
2023-08-07 22:08:13 +00:00
|
|
|
bookingCreateSchemaLegacyPropsForApi,
|
2022-12-01 21:53:52 +00:00
|
|
|
customInputSchema,
|
|
|
|
EventTypeMetaDataSchema,
|
|
|
|
extendedBookingCreateBody,
|
2023-02-15 15:42:49 +00:00
|
|
|
userMetadata as userMetadataSchema,
|
2022-12-01 21:53:52 +00:00
|
|
|
} from "@calcom/prisma/zod-utils";
|
2022-10-12 13:04:51 +00:00
|
|
|
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
|
2023-06-06 11:59:57 +00:00
|
|
|
import type {
|
|
|
|
AdditionalInformation,
|
|
|
|
AppsStatus,
|
|
|
|
CalendarEvent,
|
|
|
|
IntervalLimit,
|
|
|
|
Person,
|
|
|
|
} from "@calcom/types/Calendar";
|
2022-10-12 13:04:51 +00:00
|
|
|
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
|
|
|
|
2023-02-19 07:43:36 +00:00
|
|
|
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
2023-03-02 18:15:28 +00:00
|
|
|
import getBookingResponsesSchema from "./getBookingResponsesSchema";
|
2022-10-12 13:04:51 +00:00
|
|
|
|
|
|
|
const translator = short();
|
|
|
|
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
|
|
|
|
|
|
|
type User = Prisma.UserGetPayload<typeof userSelect>;
|
|
|
|
type BufferedBusyTimes = BufferedBusyTime[];
|
2023-08-10 19:07:57 +00:00
|
|
|
type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
2022-10-12 13:04:51 +00:00
|
|
|
|
2023-02-08 20:36:22 +00:00
|
|
|
interface IEventTypePaymentCredentialType {
|
|
|
|
appId: EventTypeAppsList;
|
|
|
|
app: {
|
|
|
|
categories: App["categories"];
|
|
|
|
dirName: string;
|
|
|
|
};
|
|
|
|
key: Prisma.JsonValue;
|
|
|
|
}
|
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
/**
|
|
|
|
* Refreshes a Credential with fresh data from the database.
|
|
|
|
*
|
|
|
|
* @param credential
|
|
|
|
*/
|
|
|
|
async function refreshCredential(credential: Credential): Promise<Credential> {
|
|
|
|
const newCredential = await prisma.credential.findUnique({
|
|
|
|
where: {
|
|
|
|
id: credential.id,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!newCredential) {
|
|
|
|
return credential;
|
|
|
|
} else {
|
|
|
|
return newCredential;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Refreshes the given set of credentials.
|
|
|
|
*
|
|
|
|
* @param credentials
|
|
|
|
*/
|
|
|
|
async function refreshCredentials(credentials: Array<Credential>): Promise<Array<Credential>> {
|
|
|
|
return await async.mapLimit(credentials, 5, refreshCredential);
|
|
|
|
}
|
|
|
|
|
2023-07-11 07:41:21 +00:00
|
|
|
/**
|
|
|
|
* Gets credentials from the user, team, and org if applicable
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
const getAllCredentials = async (
|
|
|
|
user: User & { credentials: Credential[] },
|
|
|
|
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,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
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: {
|
|
|
|
credentials: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
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: {
|
|
|
|
credentials: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (org?.credentials) {
|
|
|
|
allCredentials.push(...org.credentials);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return allCredentials;
|
|
|
|
};
|
|
|
|
|
2022-11-04 11:36:11 +00:00
|
|
|
// if true, there are conflicts.
|
|
|
|
function checkForConflicts(busyTimes: BufferedBusyTimes, time: dayjs.ConfigType, length: number) {
|
2022-10-12 13:04:51 +00:00
|
|
|
// Early return
|
2022-11-04 11:36:11 +00:00
|
|
|
if (!Array.isArray(busyTimes) || busyTimes.length < 1) {
|
|
|
|
return false; // guaranteed no conflicts when there is no busy times.
|
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
|
2022-11-04 11:36:11 +00:00
|
|
|
for (const busyTime of busyTimes) {
|
2022-10-12 13:04:51 +00: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 18:03:49 +00:00
|
|
|
log.error(
|
|
|
|
`NAUF: start between a busy time slot ${JSON.stringify({
|
|
|
|
...busyTime,
|
|
|
|
time: dayjs(time).format(),
|
|
|
|
})}`
|
|
|
|
);
|
2022-11-04 11:36:11 +00:00
|
|
|
return true;
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
// Check if slot end time is between start and end time
|
|
|
|
if (dayjs(time).add(length, "minutes").isBetween(startTime, endTime)) {
|
2023-03-23 18:03:49 +00:00
|
|
|
log.error(
|
|
|
|
`NAUF: Ends between a busy time slot ${JSON.stringify({
|
|
|
|
...busyTime,
|
|
|
|
time: dayjs(time).add(length, "minutes").format(),
|
|
|
|
})}`
|
|
|
|
);
|
2022-11-04 11:36:11 +00:00
|
|
|
return true;
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
// Check if startTime is between slot
|
|
|
|
if (startTime.isBetween(dayjs(time), dayjs(time).add(length, "minutes"))) {
|
2022-11-04 11:36:11 +00:00
|
|
|
return true;
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
}
|
2022-11-04 11:36:11 +00:00
|
|
|
return false;
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const getEventTypesFromDB = async (eventTypeId: number) => {
|
|
|
|
const eventType = await prisma.eventType.findUniqueOrThrow({
|
|
|
|
where: {
|
|
|
|
id: eventTypeId,
|
|
|
|
},
|
|
|
|
select: {
|
|
|
|
id: true,
|
|
|
|
customInputs: true,
|
2023-03-02 18:15:28 +00:00
|
|
|
disableGuests: true,
|
2023-04-20 15:55:19 +00:00
|
|
|
users: {
|
|
|
|
select: {
|
|
|
|
credentials: true,
|
|
|
|
...userSelect.select,
|
|
|
|
},
|
|
|
|
},
|
2023-04-18 10:08:09 +00:00
|
|
|
slug: true,
|
2022-10-12 13:04:51 +00:00
|
|
|
team: {
|
|
|
|
select: {
|
|
|
|
id: true,
|
|
|
|
name: true,
|
2023-08-10 18:52:36 +00:00
|
|
|
metadata: true,
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
|
|
|
},
|
2023-03-02 18:15:28 +00:00
|
|
|
bookingFields: true,
|
2022-10-12 13:04:51 +00:00
|
|
|
title: true,
|
|
|
|
length: true,
|
|
|
|
eventName: true,
|
|
|
|
schedulingType: true,
|
|
|
|
description: true,
|
|
|
|
periodType: true,
|
|
|
|
periodStartDate: true,
|
|
|
|
periodEndDate: true,
|
|
|
|
periodDays: true,
|
|
|
|
periodCountCalendarDays: true,
|
|
|
|
requiresConfirmation: true,
|
2023-07-31 17:51:11 +00:00
|
|
|
requiresBookerEmailVerification: true,
|
2022-10-12 13:04:51 +00:00
|
|
|
userId: true,
|
|
|
|
price: true,
|
|
|
|
currency: true,
|
|
|
|
metadata: true,
|
|
|
|
destinationCalendar: true,
|
|
|
|
hideCalendarNotes: true,
|
|
|
|
seatsPerTimeSlot: true,
|
|
|
|
recurringEvent: true,
|
2022-10-18 19:41:50 +00:00
|
|
|
seatsShowAttendees: true,
|
2022-10-12 13:04:51 +00:00
|
|
|
bookingLimits: true,
|
2023-03-10 20:00:19 +00:00
|
|
|
durationLimits: true,
|
2023-07-11 07:41:21 +00:00
|
|
|
parentId: true,
|
2023-04-18 10:08:09 +00:00
|
|
|
owner: {
|
|
|
|
select: {
|
|
|
|
hideBranding: true,
|
2023-08-10 18:52:36 +00:00
|
|
|
metadata: true,
|
|
|
|
teams: {
|
|
|
|
select: {
|
|
|
|
accepted: true,
|
|
|
|
team: {
|
|
|
|
select: {
|
|
|
|
metadata: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2023-04-18 10:08:09 +00:00
|
|
|
},
|
|
|
|
},
|
2022-10-12 13:04:51 +00:00
|
|
|
workflows: {
|
|
|
|
include: {
|
|
|
|
workflow: {
|
|
|
|
include: {
|
|
|
|
steps: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
locations: true,
|
|
|
|
timeZone: true,
|
|
|
|
schedule: {
|
|
|
|
select: {
|
|
|
|
availability: true,
|
|
|
|
timeZone: true,
|
|
|
|
},
|
|
|
|
},
|
2023-01-12 21:09:12 +00:00
|
|
|
hosts: {
|
|
|
|
select: {
|
|
|
|
isFixed: true,
|
2023-04-20 15:55:19 +00:00
|
|
|
user: {
|
|
|
|
select: {
|
|
|
|
credentials: true,
|
|
|
|
...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-15 00:40:47 +00:00
|
|
|
organization: {
|
|
|
|
select: {
|
|
|
|
slug: true,
|
|
|
|
},
|
|
|
|
},
|
2023-04-20 15:55:19 +00:00
|
|
|
},
|
|
|
|
},
|
2023-01-12 21:09:12 +00:00
|
|
|
},
|
|
|
|
},
|
2022-10-12 13:04:51 +00:00
|
|
|
availability: {
|
|
|
|
select: {
|
2022-12-14 17:30:55 +00:00
|
|
|
date: true,
|
2022-10-12 13:04:51 +00:00
|
|
|
startTime: true,
|
|
|
|
endTime: true,
|
|
|
|
days: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
...eventType,
|
2023-05-24 23:35:44 +00: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 08:52:46 +00:00
|
|
|
isDynamic: false,
|
2022-10-12 13:04:51 +00: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-15 00:40:47 +00:00
|
|
|
type IsFixedAwareUser = User & {
|
|
|
|
isFixed: boolean;
|
|
|
|
credentials: Credential[];
|
|
|
|
organization: { slug: string };
|
|
|
|
};
|
2023-01-12 21:09:12 +00:00
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
async function ensureAvailableUsers(
|
|
|
|
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>> & {
|
2023-01-12 21:09:12 +00:00
|
|
|
users: IsFixedAwareUser[];
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
2023-08-10 19:07:57 +00:00
|
|
|
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
|
2022-11-08 20:59:44 +00:00
|
|
|
recurringDatesInfo?: {
|
|
|
|
allRecurringDates: string[] | undefined;
|
|
|
|
currentRecurringIndex: number | undefined;
|
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
) {
|
2023-01-12 21:09:12 +00:00
|
|
|
const availableUsers: IsFixedAwareUser[] = [];
|
2023-08-10 19:07:57 +00:00
|
|
|
|
|
|
|
const orginalBookingDuration = input.originalRescheduledBooking
|
|
|
|
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
|
|
|
dayjs(input.originalRescheduledBooking.startTime),
|
|
|
|
"minutes"
|
|
|
|
)
|
|
|
|
: undefined;
|
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
/** Let's start checking for availability */
|
|
|
|
for (const user of eventType.users) {
|
2023-07-05 16:47:41 +00:00
|
|
|
const { dateRanges, busy: bufferedBusyTimes } = await getUserAvailability(
|
2022-10-12 13:04:51 +00:00
|
|
|
{
|
|
|
|
userId: user.id,
|
|
|
|
eventTypeId: eventType.id,
|
2023-08-10 19:07:57 +00:00
|
|
|
duration: orginalBookingDuration,
|
2022-10-12 13:04:51 +00:00
|
|
|
...input,
|
|
|
|
},
|
2023-08-10 19:07:57 +00:00
|
|
|
{
|
|
|
|
user,
|
|
|
|
eventType,
|
|
|
|
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
|
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
);
|
|
|
|
|
2023-07-05 16:47:41 +00:00
|
|
|
if (!dateRanges.length) {
|
2022-11-04 11:36:11 +00:00
|
|
|
// user does not have availability at this time, skip user.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
console.log("calendarBusyTimes==>>>", bufferedBusyTimes);
|
|
|
|
|
2022-11-08 16:12:31 +00:00
|
|
|
let foundConflict = false;
|
2022-10-12 13:04:51 +00:00
|
|
|
try {
|
2022-11-08 20:59:44 +00:00
|
|
|
if (
|
|
|
|
eventType.recurringEvent &&
|
|
|
|
recurringDatesInfo?.currentRecurringIndex === 0 &&
|
|
|
|
recurringDatesInfo.allRecurringDates
|
|
|
|
) {
|
|
|
|
const allBookingDates = recurringDatesInfo.allRecurringDates.map((strDate) => new Date(strDate));
|
2022-10-12 13:04:51 +00:00
|
|
|
// Go through each date for the recurring event and check if each one's availability
|
|
|
|
// DONE: Decreased computational complexity from O(2^n) to O(n) by refactoring this loop to stop
|
|
|
|
// running at the first unavailable time.
|
|
|
|
let i = 0;
|
2022-11-04 11:36:11 +00:00
|
|
|
while (!foundConflict && i < allBookingDates.length) {
|
|
|
|
foundConflict = checkForConflicts(bufferedBusyTimes, allBookingDates[i++], eventType.length);
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
} else {
|
2022-11-04 11:36:11 +00:00
|
|
|
foundConflict = checkForConflicts(bufferedBusyTimes, input.dateFrom, eventType.length);
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
log.debug({
|
|
|
|
message: "Unable set isAvailableToBeBooked. Using true. ",
|
|
|
|
});
|
|
|
|
}
|
2022-11-04 11:36:11 +00:00
|
|
|
// no conflicts found, add to available users.
|
|
|
|
if (!foundConflict) {
|
2022-10-12 13:04:51 +00:00
|
|
|
availableUsers.push(user);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!availableUsers.length) {
|
|
|
|
throw new Error("No available users found.");
|
|
|
|
}
|
|
|
|
return availableUsers;
|
|
|
|
}
|
|
|
|
|
2023-03-14 04:19:05 +00: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,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
payment: true,
|
|
|
|
references: true,
|
|
|
|
workflowReminders: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
function getBookingData({
|
|
|
|
req,
|
|
|
|
isNotAnApiCall,
|
|
|
|
eventType,
|
|
|
|
}: {
|
|
|
|
req: NextApiRequest;
|
|
|
|
isNotAnApiCall: boolean;
|
|
|
|
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>;
|
|
|
|
}) {
|
2023-04-18 12:35:06 +00:00
|
|
|
const responsesSchema = getBookingResponsesSchema({
|
|
|
|
eventType: {
|
|
|
|
bookingFields: eventType.bookingFields,
|
|
|
|
},
|
|
|
|
view: req.body.rescheduleUid ? "reschedule" : "booking",
|
|
|
|
});
|
2023-03-02 18:15:28 +00:00
|
|
|
const bookingDataSchema = isNotAnApiCall
|
|
|
|
? extendedBookingCreateBody.merge(
|
|
|
|
z.object({
|
2023-04-18 12:35:06 +00:00
|
|
|
responses: responsesSchema,
|
2023-03-02 18:15:28 +00:00
|
|
|
})
|
|
|
|
)
|
2023-04-18 12:35:06 +00:00
|
|
|
: bookingCreateBodySchemaForApi
|
|
|
|
.merge(
|
|
|
|
z.object({
|
|
|
|
responses: responsesSchema.optional(),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
.superRefine((val, ctx) => {
|
|
|
|
if (val.responses && val.customInputs) {
|
|
|
|
ctx.addIssue({
|
|
|
|
code: "custom",
|
|
|
|
message:
|
|
|
|
"Don't use both customInputs and responses. `customInputs` is only there for legacy support.",
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const legacyProps = Object.keys(bookingCreateSchemaLegacyPropsForApi.shape);
|
|
|
|
|
|
|
|
if (val.responses) {
|
|
|
|
const unwantedProps: string[] = [];
|
|
|
|
legacyProps.forEach((legacyProp) => {
|
2023-06-02 19:29:52 +00:00
|
|
|
if (typeof val[legacyProp as keyof typeof val] !== "undefined") {
|
|
|
|
console.error(
|
|
|
|
`Deprecated: Unexpected falsy value for: ${unwantedProps.join(
|
|
|
|
","
|
|
|
|
)}. They can't be used with \`responses\`. This will become a 400 error in the future.`
|
|
|
|
);
|
|
|
|
}
|
2023-04-18 12:35:06 +00:00
|
|
|
if (val[legacyProp as keyof typeof val]) {
|
|
|
|
unwantedProps.push(legacyProp);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (unwantedProps.length) {
|
|
|
|
ctx.addIssue({
|
|
|
|
code: "custom",
|
|
|
|
message: `Legacy Props: ${unwantedProps.join(",")}. They can't be used with \`responses\``,
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else if (val.customInputs) {
|
|
|
|
const { success } = bookingCreateSchemaLegacyPropsForApi.safeParse(val);
|
|
|
|
if (!success) {
|
|
|
|
ctx.addIssue({
|
|
|
|
code: "custom",
|
|
|
|
message: `With \`customInputs\` you must specify legacy props ${legacyProps.join(",")}`,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2023-06-02 19:29:52 +00:00
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
const reqBody = bookingDataSchema.parse(req.body);
|
2023-06-02 19:29:52 +00:00
|
|
|
|
|
|
|
// Work with Typescript to require reqBody.end
|
|
|
|
type ReqBodyWithoutEnd = z.infer<typeof bookingDataSchema>;
|
|
|
|
type ReqBodyWithEnd = ReqBodyWithoutEnd & { end: string };
|
|
|
|
|
|
|
|
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)) {
|
|
|
|
throw new Error("Internal Error.");
|
|
|
|
}
|
|
|
|
// reqBody.end is no longer an optional property.
|
2023-04-18 12:35:06 +00: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 18:15:28 +00:00
|
|
|
const responses = reqBody.responses;
|
2023-07-20 05:03:50 +00:00
|
|
|
|
2023-04-04 04:59:09 +00:00
|
|
|
const { userFieldsResponses: calEventUserFieldsResponses, responses: calEventResponses } =
|
|
|
|
getCalEventResponses({
|
|
|
|
bookingFields: eventType.bookingFields,
|
|
|
|
responses,
|
|
|
|
});
|
2023-03-02 18:15:28 +00: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 17:50:54 +00:00
|
|
|
calEventUserFieldsResponses,
|
2023-03-02 18:15:28 +00:00
|
|
|
rescheduleReason: responses.rescheduleReason,
|
2023-03-07 17:50:54 +00:00
|
|
|
calEventResponses,
|
2023-03-02 18:15:28 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function getCustomInputsResponses(
|
|
|
|
reqBody: {
|
2023-06-06 11:59:57 +00:00
|
|
|
responses?: Record<string, object>;
|
2023-03-02 18:15:28 +00: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;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function handler(
|
|
|
|
req: NextApiRequest & { userId?: number | undefined },
|
|
|
|
{
|
|
|
|
isNotAnApiCall = false,
|
|
|
|
}: {
|
|
|
|
isNotAnApiCall?: boolean;
|
|
|
|
} = {
|
|
|
|
isNotAnApiCall: false,
|
|
|
|
}
|
|
|
|
) {
|
2022-10-12 13:04:51 +00:00
|
|
|
const { userId } = req;
|
|
|
|
|
2023-06-26 19:44:58 +00:00
|
|
|
const userIp = getIP(req);
|
|
|
|
|
|
|
|
await checkRateLimitAndThrowError({
|
|
|
|
rateLimitingType: "core",
|
|
|
|
identifier: userIp,
|
|
|
|
});
|
|
|
|
|
2023-03-02 18:15:28 +00: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 16:11:50 +00:00
|
|
|
const {
|
|
|
|
recurringCount,
|
2022-11-08 20:59:44 +00:00
|
|
|
allRecurringDates,
|
|
|
|
currentRecurringIndex,
|
2022-10-19 16:11:50 +00:00
|
|
|
noEmail,
|
|
|
|
eventTypeId,
|
2023-03-02 18:15:28 +00:00
|
|
|
eventTypeSlug,
|
2022-10-19 16:11:50 +00:00
|
|
|
hasHashedBookingLink,
|
|
|
|
language,
|
|
|
|
appsStatus: reqAppsStatus,
|
2023-03-02 18:15:28 +00:00
|
|
|
name: bookerName,
|
|
|
|
email: bookerEmail,
|
|
|
|
guests: reqGuests,
|
|
|
|
location,
|
|
|
|
notes: additionalNotes,
|
|
|
|
smsReminderNumber,
|
|
|
|
rescheduleReason,
|
2022-10-19 16:11:50 +00:00
|
|
|
...reqBody
|
2023-03-02 18:15:28 +00:00
|
|
|
} = getBookingData({
|
|
|
|
req,
|
|
|
|
isNotAnApiCall,
|
|
|
|
eventType,
|
|
|
|
});
|
2022-10-12 13:04:51 +00:00
|
|
|
|
2023-07-20 05:03:50 +00:00
|
|
|
const fullName = getFullName(bookerName);
|
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
const tGuests = await getTranslation("en", "common");
|
|
|
|
log.debug(`Booking eventType ${eventTypeId} started`);
|
2023-08-07 22:08:13 +00:00
|
|
|
const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user);
|
2022-10-12 13:04:51 +00:00
|
|
|
if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" });
|
|
|
|
|
2023-02-27 20:45:40 +00:00
|
|
|
const isTeamEventType =
|
|
|
|
eventType.schedulingType === SchedulingType.COLLECTIVE ||
|
|
|
|
eventType.schedulingType === SchedulingType.ROUND_ROBIN;
|
|
|
|
|
2023-02-08 20:36:22 +00:00
|
|
|
const paymentAppData = getPaymentAppData(eventType);
|
2022-10-14 16:24:43 +00:00
|
|
|
|
2022-10-12 13:04:51 +00: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-03-23 18:03:49 +00:00
|
|
|
log.warn({
|
|
|
|
message: "NewBooking: Unable set timeOutOfBounds. Using false. ",
|
|
|
|
});
|
2022-10-12 13:04:51 +00:00
|
|
|
if (error instanceof BookingDateInPastError) {
|
|
|
|
// TODO: HttpError should not bleed through to the console.
|
|
|
|
log.info(`Booking eventType ${eventTypeId} failed`, error);
|
|
|
|
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-03-23 18:03:49 +00:00
|
|
|
log.warn({
|
|
|
|
message: `NewBooking: EventType '${eventType.eventName}' cannot be booked at this time.`,
|
|
|
|
});
|
2022-10-12 13:04:51 +00:00
|
|
|
throw new HttpError({ statusCode: 400, message: error.message });
|
|
|
|
}
|
|
|
|
|
2023-01-12 21:09:12 +00:00
|
|
|
const loadUsers = async () =>
|
|
|
|
!eventTypeId
|
|
|
|
? await prisma.user.findMany({
|
|
|
|
where: {
|
|
|
|
username: {
|
|
|
|
in: dynamicUserList,
|
|
|
|
},
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
2023-02-15 15:42:49 +00:00
|
|
|
select: {
|
|
|
|
...userSelect.select,
|
2023-04-20 15:55:19 +00:00
|
|
|
credentials: true, // Don't leak to client
|
2023-02-15 15:42:49 +00:00
|
|
|
metadata: true,
|
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-15 00:40:47 +00:00
|
|
|
organization: {
|
|
|
|
select: {
|
|
|
|
slug: true,
|
|
|
|
},
|
|
|
|
},
|
2023-02-15 15:42:49 +00:00
|
|
|
},
|
2023-01-12 21:09:12 +00:00
|
|
|
})
|
2023-06-02 19:29:52 +00:00
|
|
|
: eventType.hosts?.length
|
2023-01-12 21:09:12 +00:00
|
|
|
? eventType.hosts.map(({ user, isFixed }) => ({
|
|
|
|
...user,
|
|
|
|
isFixed,
|
|
|
|
}))
|
2023-05-24 23:35:44 +00:00
|
|
|
: eventType.users || [];
|
2023-01-12 21:09:12 +00:00
|
|
|
// loadUsers allows type inferring
|
2023-02-15 15:42:49 +00:00
|
|
|
let users: (Awaited<ReturnType<typeof loadUsers>>[number] & {
|
|
|
|
isFixed?: boolean;
|
|
|
|
metadata?: Prisma.JsonValue;
|
|
|
|
})[] = await loadUsers();
|
2023-01-12 21:09:12 +00:00
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
|
|
|
|
if (!isDynamicAllowed && !eventTypeId) {
|
2023-03-23 18:03:49 +00:00
|
|
|
log.warn({ message: "NewBooking: Some of the users in this group do not allow dynamic booking" });
|
2022-10-12 13:04:51 +00: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 15:55:19 +00:00
|
|
|
select: {
|
|
|
|
credentials: true, // Don't leak to client
|
|
|
|
...userSelect.select,
|
|
|
|
},
|
2022-10-12 13:04:51 +00:00
|
|
|
});
|
2023-03-23 18:03:49 +00:00
|
|
|
if (!eventTypeUser) {
|
|
|
|
log.warn({ message: "NewBooking: eventTypeUser.notFound" });
|
|
|
|
throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
users.push(eventTypeUser);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!users) throw new HttpError({ statusCode: 404, message: "eventTypeUser.notFound" });
|
|
|
|
|
2023-01-12 21:09:12 +00:00
|
|
|
users = users.map((user) => ({
|
|
|
|
...user,
|
|
|
|
isFixed:
|
|
|
|
user.isFixed === false
|
|
|
|
? false
|
|
|
|
: user.isFixed || eventType.schedulingType !== SchedulingType.ROUND_ROBIN,
|
|
|
|
}));
|
|
|
|
|
2023-03-16 18:12:30 +00:00
|
|
|
let locationBodyString = location;
|
2023-06-23 17:04:34 +00:00
|
|
|
|
|
|
|
// TODO: It's definition should be moved to getLocationValueForDb
|
|
|
|
let organizerOrFirstDynamicGroupMemberDefaultLocationUrl = undefined;
|
2023-03-28 20:03:54 +00:00
|
|
|
|
2023-03-16 18:12:30 +00: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 11:38:57 +00:00
|
|
|
locationBodyString = firstUsersMetadata?.defaultConferencingApp?.appLink || locationBodyString;
|
2023-06-23 17:04:34 +00:00
|
|
|
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
|
|
|
|
firstUsersMetadata?.defaultConferencingApp?.appLink;
|
2023-03-16 18:12:30 +00:00
|
|
|
}
|
|
|
|
|
2023-06-02 19:29:52 +00:00
|
|
|
if (
|
|
|
|
Object.prototype.hasOwnProperty.call(eventType, "bookingLimits") ||
|
|
|
|
Object.prototype.hasOwnProperty.call(eventType, "durationLimits")
|
|
|
|
) {
|
2023-03-10 20:00:19 +00:00
|
|
|
const startAsDate = dayjs(reqBody.start).toDate();
|
2023-06-02 19:29:52 +00:00
|
|
|
if (eventType.bookingLimits) {
|
2023-06-06 11:59:57 +00:00
|
|
|
await checkBookingLimits(eventType.bookingLimits as IntervalLimit, startAsDate, eventType.id);
|
2023-06-02 19:29:52 +00:00
|
|
|
}
|
|
|
|
if (eventType.durationLimits) {
|
2023-06-06 11:59:57 +00:00
|
|
|
await checkDurationLimits(eventType.durationLimits as IntervalLimit, startAsDate, eventType.id);
|
2023-06-02 19:29:52 +00:00
|
|
|
}
|
2023-03-10 20:00:19 +00:00
|
|
|
}
|
|
|
|
|
2023-08-10 19:07:57 +00:00
|
|
|
let rescheduleUid = reqBody.rescheduleUid;
|
|
|
|
let bookingSeat: Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null = null;
|
|
|
|
|
|
|
|
let originalRescheduledBooking: BookingType = null;
|
|
|
|
|
|
|
|
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" });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
if (!eventType.seatsPerTimeSlot) {
|
|
|
|
const availableUsers = await ensureAvailableUsers(
|
|
|
|
{
|
|
|
|
...eventType,
|
2023-01-12 21:09:12 +00:00
|
|
|
users: users as IsFixedAwareUser[],
|
2022-11-08 20:59:44 +00:00
|
|
|
...(eventType.recurringEvent && {
|
|
|
|
recurringEvent: {
|
|
|
|
...eventType.recurringEvent,
|
|
|
|
count: recurringCount || eventType.recurringEvent.count,
|
|
|
|
},
|
|
|
|
}),
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
dateFrom: reqBody.start,
|
|
|
|
dateTo: reqBody.end,
|
2023-03-03 16:33:16 +00:00
|
|
|
timeZone: reqBody.timeZone,
|
2023-08-10 19:07:57 +00:00
|
|
|
originalRescheduledBooking,
|
2022-11-08 20:59:44 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
allRecurringDates,
|
|
|
|
currentRecurringIndex,
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2023-01-12 21:09:12 +00:00
|
|
|
const luckyUsers: typeof users = [];
|
|
|
|
const luckyUserPool = availableUsers.filter((user) => !user.isFixed);
|
|
|
|
// 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 13:04:51 +00:00
|
|
|
}
|
2023-01-12 21:09:12 +00:00
|
|
|
luckyUsers.push(newLuckyUser);
|
|
|
|
}
|
|
|
|
// ALL fixed users must be available
|
|
|
|
if (
|
|
|
|
availableUsers.filter((user) => user.isFixed).length !== users.filter((user) => user.isFixed).length
|
|
|
|
) {
|
|
|
|
throw new Error("Some users are unavailable for booking.");
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
2023-02-24 01:22:18 +00: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 13:04:51 +00:00
|
|
|
}
|
2023-01-12 21:09:12 +00:00
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
const [organizerUser] = users;
|
2023-03-14 04:19:05 +00:00
|
|
|
const tOrganizer = await getTranslation(organizerUser?.locale ?? "en", "common");
|
2023-06-23 17:04:34 +00:00
|
|
|
|
2023-07-11 07:41:21 +00:00
|
|
|
const allCredentials = await getAllCredentials(organizerUser, eventType);
|
|
|
|
|
2023-08-10 16:05:35 +00:00
|
|
|
const isOrganizerRescheduling = organizerUser.id === userId;
|
|
|
|
|
|
|
|
const attendeeInfoOnReschedule =
|
|
|
|
isOrganizerRescheduling && originalRescheduledBooking
|
|
|
|
? 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-03-28 20:03:54 +00:00
|
|
|
// use host default
|
2023-06-23 17:04:34 +00:00
|
|
|
if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) {
|
2023-03-28 20:03:54 +00:00
|
|
|
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
|
|
|
|
const organizerMetadata = metadataParseResult.success ? metadataParseResult.data : undefined;
|
|
|
|
if (organizerMetadata) {
|
|
|
|
const app = getAppFromSlug(organizerMetadata?.defaultConferencingApp?.appSlug);
|
|
|
|
locationBodyString = app?.appData?.location?.type || locationBodyString;
|
2023-06-23 17:04:34 +00:00
|
|
|
organizerOrFirstDynamicGroupMemberDefaultLocationUrl =
|
|
|
|
organizerMetadata?.defaultConferencingApp?.appLink;
|
2023-03-28 20:03:54 +00:00
|
|
|
} else {
|
|
|
|
locationBodyString = "";
|
|
|
|
}
|
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
|
|
|
|
const invitee = [
|
|
|
|
{
|
2023-03-02 18:15:28 +00:00
|
|
|
email: bookerEmail,
|
2023-07-20 05:03:50 +00:00
|
|
|
name: fullName,
|
2023-08-03 15:10:59 +00:00
|
|
|
firstName: (typeof bookerName === "object" && bookerName.firstName) || "",
|
|
|
|
lastName: (typeof bookerName === "object" && bookerName.lastName) || "",
|
2023-08-10 16:05:35 +00:00
|
|
|
timeZone: attendeeTimezone,
|
|
|
|
language: { translate: tAttendees, locale: attendeeLanguage ?? "en" },
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
|
|
|
];
|
2023-02-27 20:45:40 +00:00
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
const guests = (reqGuests || []).reduce((guestArray, guest) => {
|
2023-02-27 20:45:40 +00:00
|
|
|
// If it's a team event, remove the team member from guests
|
2023-03-16 09:04:17 +00:00
|
|
|
if (isTeamEventType && users.some((user) => user.email === guest)) {
|
|
|
|
return guestArray;
|
2023-02-27 20:45:40 +00:00
|
|
|
}
|
2023-03-16 09:04:17 +00:00
|
|
|
guestArray.push({
|
|
|
|
email: guest,
|
|
|
|
name: "",
|
2023-08-03 15:10:59 +00:00
|
|
|
firstName: "",
|
|
|
|
lastName: "",
|
2023-08-10 16:05:35 +00:00
|
|
|
timeZone: attendeeTimezone,
|
2023-03-16 09:04:17 +00:00
|
|
|
language: { translate: tGuests, locale: "en" },
|
|
|
|
});
|
2023-02-27 20:45:40 +00:00
|
|
|
return guestArray;
|
|
|
|
}, [] as typeof invitee);
|
2022-10-12 13:04:51 +00:00
|
|
|
|
|
|
|
const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`;
|
|
|
|
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
|
|
|
|
|
2023-06-23 17:04:34 +00: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 03:54:28 +00:00
|
|
|
const { bookingLocation, conferenceCredentialId } = organizerOrFirstDynamicGroupMemberDefaultLocationUrl
|
|
|
|
? {
|
|
|
|
bookingLocation: organizerOrFirstDynamicGroupMemberDefaultLocationUrl,
|
|
|
|
conferenceCredentialId: undefined,
|
|
|
|
}
|
2023-06-23 17:04:34 +00:00
|
|
|
: getLocationValueForDB(locationBodyString, eventType.locations);
|
2023-04-04 04:59:09 +00:00
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
const customInputs = getCustomInputsResponses(reqBody, eventType.customInputs);
|
2022-10-12 13:04:51 +00:00
|
|
|
const teamMemberPromises =
|
2023-01-12 21:09:12 +00:00
|
|
|
users.length > 1
|
2022-10-12 13:04:51 +00:00
|
|
|
? users.slice(1).map(async function (user) {
|
|
|
|
return {
|
|
|
|
email: user.email || "",
|
|
|
|
name: user.name || "",
|
|
|
|
timeZone: user.timeZone,
|
|
|
|
language: {
|
|
|
|
translate: await getTranslation(user.locale ?? "en", "common"),
|
|
|
|
locale: user.locale ?? "en",
|
|
|
|
},
|
|
|
|
};
|
|
|
|
})
|
|
|
|
: [];
|
|
|
|
|
|
|
|
const teamMembers = await Promise.all(teamMemberPromises);
|
|
|
|
|
2023-02-27 20:45:40 +00:00
|
|
|
const attendeesList = [...invitee, ...guests];
|
2022-10-12 13:04:51 +00:00
|
|
|
|
2023-03-09 15:11:16 +00:00
|
|
|
const responses = "responses" in reqBody ? reqBody.responses : null;
|
|
|
|
|
2023-07-27 08:52:46 +00:00
|
|
|
const evtName = !eventType?.isDynamic ? eventType.eventName : responses?.title;
|
2022-10-12 13:04:51 +00:00
|
|
|
const eventNameObject = {
|
2023-03-02 18:15:28 +00:00
|
|
|
//TODO: Can we have an unnamed attendee? If not, I would really like to throw an error here.
|
2023-07-20 05:03:50 +00:00
|
|
|
attendeeName: fullName || "Nameless",
|
2022-10-12 13:04:51 +00:00
|
|
|
eventType: eventType.title,
|
2023-07-27 08:52:46 +00:00
|
|
|
eventName: evtName,
|
2023-07-20 23:29:08 +00: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 18:15:28 +00:00
|
|
|
// TODO: Can we have an unnamed organizer? If not, I would really like to throw an error here.
|
2022-10-12 13:04:51 +00:00
|
|
|
host: organizerUser.name || "Nameless",
|
|
|
|
location: bookingLocation,
|
2023-03-09 15:11:16 +00:00
|
|
|
bookingFields: { ...responses },
|
2022-10-12 13:04:51 +00:00
|
|
|
t: tOrganizer,
|
|
|
|
};
|
|
|
|
|
2022-12-05 12:12:14 +00:00
|
|
|
let requiresConfirmation = eventType?.requiresConfirmation;
|
|
|
|
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
|
|
|
|
if (rcThreshold) {
|
|
|
|
if (dayjs(dayjs(reqBody.start).utc().format()).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) {
|
|
|
|
requiresConfirmation = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-07 17:50:54 +00:00
|
|
|
const calEventUserFieldsResponses =
|
|
|
|
"calEventUserFieldsResponses" in reqBody ? reqBody.calEventUserFieldsResponses : null;
|
2023-04-20 11:38:57 +00:00
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
let evt: CalendarEvent = {
|
2023-07-13 13:52:21 +00:00
|
|
|
bookerUrl: await getBookerUrl(organizerUser),
|
2022-10-12 13:04:51 +00:00
|
|
|
type: eventType.title,
|
|
|
|
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-22 00:15:51 +00:00
|
|
|
id: organizerUser.id,
|
2022-10-12 13:04:51 +00:00
|
|
|
name: organizerUser.name || "Nameless",
|
|
|
|
email: organizerUser.email || "Email-less",
|
2023-04-18 10:08:09 +00:00
|
|
|
username: organizerUser.username || undefined,
|
2022-10-12 13:04:51 +00:00
|
|
|
timeZone: organizerUser.timeZone,
|
|
|
|
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
|
2023-07-19 14:30:37 +00:00
|
|
|
timeFormat: getTimeFormatStringFromUserTimeFormat(organizerUser.timeFormat),
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
2023-03-07 17:50:54 +00:00
|
|
|
responses: "calEventResponses" in reqBody ? reqBody.calEventResponses : null,
|
|
|
|
userFieldsResponses: calEventUserFieldsResponses,
|
2022-10-12 13:04:51 +00:00
|
|
|
attendees: attendeesList,
|
|
|
|
location: bookingLocation, // Will be processed by the EventManager later.
|
2023-08-02 03:54:28 +00:00
|
|
|
conferenceCredentialId,
|
2022-10-12 13:04:51 +00:00
|
|
|
/** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */
|
|
|
|
destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar,
|
|
|
|
hideCalendarNotes: eventType.hideCalendarNotes,
|
2022-12-05 12:12:14 +00:00
|
|
|
requiresConfirmation: requiresConfirmation ?? false,
|
2022-10-12 13:04:51 +00:00
|
|
|
eventTypeId: eventType.id,
|
2023-02-24 19:52:25 +00:00
|
|
|
// if seats are not enabled we should default true
|
2023-06-02 19:29:52 +00:00
|
|
|
seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true,
|
2023-01-31 21:14:19 +00:00
|
|
|
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
|
2022-10-12 13:04:51 +00:00
|
|
|
};
|
|
|
|
|
2023-03-14 04:19:05 +00: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 08:52:58 +00:00
|
|
|
|
|
|
|
if (videoCallReference) {
|
|
|
|
evt.videoCallData = {
|
|
|
|
type: videoCallReference.type,
|
|
|
|
id: videoCallReference.meetingId,
|
|
|
|
password: videoCallReference?.meetingPassword,
|
|
|
|
url: videoCallReference.meetingUrl,
|
|
|
|
};
|
|
|
|
}
|
2023-03-14 04:19:05 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/* Check if the original booking has no more attendees, if so delete the booking
|
|
|
|
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,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
if (credential) {
|
|
|
|
if (reference.type.includes("_video")) {
|
|
|
|
integrationsToDelete.push(deleteMeeting(credential, reference.uid));
|
|
|
|
}
|
|
|
|
if (reference.type.includes("_calendar") && originalBookingEvt) {
|
2023-04-05 14:55:57 +00:00
|
|
|
const calendar = await getCalendar(credential);
|
2023-03-14 04:19:05 +00:00
|
|
|
if (calendar) {
|
|
|
|
integrationsToDelete.push(
|
|
|
|
calendar?.deleteEvent(reference.uid, originalBookingEvt, reference.externalCalendarId)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await Promise.all(integrationsToDelete).then(async () => {
|
2023-04-02 10:09:57 +00:00
|
|
|
await prisma.booking.update({
|
2023-03-14 04:19:05 +00:00
|
|
|
where: {
|
|
|
|
id: originalRescheduledBooking.id,
|
|
|
|
},
|
2023-04-02 10:09:57 +00:00
|
|
|
data: {
|
|
|
|
status: BookingStatus.CANCELLED,
|
|
|
|
},
|
2023-03-14 04:19:05 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
deletedReferences = true;
|
|
|
|
}
|
|
|
|
return deletedReferences;
|
|
|
|
};
|
|
|
|
|
2023-07-13 20:40:16 +00:00
|
|
|
// data needed for triggering webhooks
|
|
|
|
const eventTypeInfo: EventTypeInfo = {
|
|
|
|
eventTitle: eventType.title,
|
|
|
|
eventDescription: eventType.description,
|
|
|
|
requiresConfirmation: requiresConfirmation || null,
|
|
|
|
price: paymentAppData.price,
|
|
|
|
currency: eventType.currency,
|
|
|
|
length: eventType.length,
|
|
|
|
};
|
|
|
|
|
2023-07-25 17:05:02 +00:00
|
|
|
const teamId = await getTeamIdFromEventType({ eventType });
|
2023-07-13 20:40:16 +00:00
|
|
|
|
|
|
|
const subscriberOptions: GetSubscriberOptions = {
|
|
|
|
userId: organizerUser.id,
|
|
|
|
eventTypeId,
|
|
|
|
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
|
|
|
teamId,
|
|
|
|
};
|
|
|
|
|
|
|
|
const eventTrigger: WebhookTriggerEvents = rescheduleUid
|
|
|
|
? WebhookTriggerEvents.BOOKING_RESCHEDULED
|
|
|
|
: WebhookTriggerEvents.BOOKING_CREATED;
|
|
|
|
|
|
|
|
subscriberOptions.triggerEvent = eventTrigger;
|
|
|
|
|
|
|
|
const subscriberOptionsMeetingEnded = {
|
|
|
|
userId: organizerUser.id,
|
|
|
|
eventTypeId,
|
|
|
|
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
|
|
|
teamId,
|
|
|
|
};
|
|
|
|
|
|
|
|
const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);
|
|
|
|
|
2023-08-10 18:52:36 +00:00
|
|
|
const isKYCVerified = isEventTypeOwnerKYCVerified(eventType);
|
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const handleSeats = async () => {
|
|
|
|
let resultBooking:
|
|
|
|
| (Partial<Booking> & {
|
|
|
|
appsStatus?: AppsStatus[];
|
|
|
|
seatReferenceUid?: string;
|
|
|
|
paymentUid?: string;
|
|
|
|
message?: string;
|
|
|
|
})
|
|
|
|
| null = null;
|
2023-07-11 15:11:08 +00:00
|
|
|
|
2023-05-09 10:02:19 +00:00
|
|
|
const booking = await prisma.booking.findFirst({
|
2022-10-12 13:04:51 +00:00
|
|
|
where: {
|
2023-05-09 10:02:19 +00:00
|
|
|
OR: [
|
|
|
|
{
|
|
|
|
uid: rescheduleUid || reqBody.bookingUid,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
eventTypeId: eventType.id,
|
|
|
|
startTime: evt.startTime,
|
|
|
|
},
|
|
|
|
],
|
2023-07-11 15:11:08 +00:00
|
|
|
status: BookingStatus.ACCEPTED,
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
2023-03-14 04:19:05 +00:00
|
|
|
select: {
|
|
|
|
uid: true,
|
|
|
|
id: true,
|
|
|
|
attendees: { include: { bookingSeat: true } },
|
|
|
|
userId: true,
|
|
|
|
references: true,
|
|
|
|
startTime: true,
|
|
|
|
user: true,
|
|
|
|
status: true,
|
2023-07-13 20:40:16 +00:00
|
|
|
smsReminderNumber: true,
|
|
|
|
endTime: true,
|
|
|
|
scheduledJobs: true,
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
|
|
|
});
|
2023-05-09 10:02:19 +00:00
|
|
|
|
|
|
|
if (!booking) {
|
|
|
|
throw new HttpError({ statusCode: 404, message: "Could not find booking" });
|
|
|
|
}
|
|
|
|
|
2023-03-14 04:19:05 +00: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
|
|
|
|
) {
|
|
|
|
throw new HttpError({ statusCode: 409, message: "Already signed up for this booking." });
|
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
|
2023-03-14 04:19:05 +00: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 20:36:22 +00:00
|
|
|
where: {
|
2023-03-14 04:19:05 +00:00
|
|
|
startTime: evt.startTime,
|
|
|
|
eventTypeId: eventType.id,
|
2023-07-11 15:11:08 +00:00
|
|
|
status: BookingStatus.ACCEPTED,
|
2023-02-08 20:36:22 +00:00
|
|
|
},
|
|
|
|
select: {
|
2023-03-14 04:19:05 +00:00
|
|
|
id: true,
|
|
|
|
uid: true,
|
|
|
|
attendees: {
|
|
|
|
include: {
|
|
|
|
bookingSeat: true,
|
2023-02-08 20:36:22 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2023-07-11 07:41:21 +00:00
|
|
|
const credentials = await refreshCredentials(allCredentials);
|
2023-03-14 04:19:05 +00: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-28 01:16:07 +00: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 23:45:48 +00: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-28 01:16:07 +00: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 04:19:05 +00: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 15:11:08 +00:00
|
|
|
endTime: evt.endTime,
|
2023-03-14 04:19:05 +00: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 17:13:57 +00:00
|
|
|
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
evt.iCalUID = calendarResult?.updatedEvent.iCalUID || undefined;
|
2023-04-03 17:13:57 +00:00
|
|
|
|
2023-03-14 04:19:05 +00:00
|
|
|
if (results.length > 0 && results.some((res) => !res.success)) {
|
|
|
|
const error = {
|
|
|
|
errorCode: "BookingReschedulingMeetingFailed",
|
|
|
|
message: "Booking Rescheduling failed",
|
|
|
|
};
|
|
|
|
log.error(`Booking ${organizerUser.name} failed`, error, results);
|
|
|
|
} 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;
|
|
|
|
handleAppsStatus(results, newBooking);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (noEmail !== true) {
|
|
|
|
const copyEvent = cloneDeep(evt);
|
|
|
|
await sendRescheduledEmails({
|
|
|
|
...copyEvent,
|
|
|
|
additionalNotes, // Resets back to the additionalNote input and not the override value
|
2023-04-03 09:18:12 +00:00
|
|
|
cancellationReason: "$RCH$" + (rescheduleReason ? rescheduleReason : ""), // Removable code prefix to differentiate cancellation from rescheduling for email
|
2023-03-14 04:19:05 +00:00
|
|
|
});
|
|
|
|
}
|
2023-04-13 19:03:08 +00:00
|
|
|
const foundBooking = await findBookingQuery(newBooking.id);
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00: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 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
// Confirm that the new event will have enough available seats
|
2023-03-14 04:19:05 +00:00
|
|
|
if (
|
2023-04-13 19:03:08 +00:00
|
|
|
!eventType.seatsPerTimeSlot ||
|
|
|
|
attendeesToMove.length +
|
|
|
|
newTimeSlotBooking.attendees.filter((attendee) => attendee.bookingSeat).length >
|
|
|
|
eventType.seatsPerTimeSlot
|
2023-03-14 04:19:05 +00:00
|
|
|
) {
|
2023-04-13 19:03:08 +00:00
|
|
|
throw new HttpError({ statusCode: 409, message: "Booking does not have enough available seats" });
|
2023-03-14 04:19:05 +00:00
|
|
|
}
|
|
|
|
|
2023-04-13 19:03:08 +00: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 04:19:05 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2023-04-13 19:03:08 +00: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 04:19:05 +00:00
|
|
|
},
|
2023-04-13 19:03:08 +00:00
|
|
|
}),
|
|
|
|
]);
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const updatedNewBooking = await prisma.booking.findUnique({
|
2023-03-14 04:19:05 +00:00
|
|
|
where: {
|
2023-04-13 19:03:08 +00:00
|
|
|
id: newTimeSlotBooking.id,
|
2023-03-14 04:19:05 +00:00
|
|
|
},
|
2023-04-13 19:03:08 +00:00
|
|
|
include: {
|
|
|
|
attendees: true,
|
|
|
|
references: true,
|
|
|
|
},
|
|
|
|
});
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
if (!updatedNewBooking) {
|
|
|
|
throw new HttpError({ statusCode: 404, message: "Updated booking not found" });
|
|
|
|
}
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
// Update the evt object with the new attendees
|
|
|
|
const updatedBookingAttendees = updatedNewBooking.attendees.map((attendee) => {
|
|
|
|
const evtAttendee = {
|
|
|
|
...attendee,
|
2023-08-10 16:05:35 +00:00
|
|
|
language: { translate: tAttendees, locale: attendeeLanguage ?? "en" },
|
2023-04-13 19:03:08 +00:00
|
|
|
};
|
|
|
|
return evtAttendee;
|
|
|
|
});
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
evt.attendees = updatedBookingAttendees;
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
addVideoCallDataToEvt(updatedNewBooking.references);
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const copyEvent = cloneDeep(evt);
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const updateManager = await eventManager.reschedule(
|
|
|
|
copyEvent,
|
|
|
|
rescheduleUid,
|
|
|
|
newTimeSlotBooking.id
|
|
|
|
);
|
2023-04-03 17:13:57 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const results = updateManager.results;
|
2023-04-03 17:13:57 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
2023-04-03 17:13:57 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
|
|
|
|
? calendarResult?.updatedEvent[0]?.iCalUID
|
|
|
|
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
// TODO send reschedule emails to attendees of the old booking
|
|
|
|
await sendRescheduledEmails({
|
|
|
|
...copyEvent,
|
|
|
|
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-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
// Update the old booking with the cancelled status
|
|
|
|
await prisma.booking.update({
|
|
|
|
where: {
|
|
|
|
id: booking.id,
|
|
|
|
},
|
|
|
|
data: {
|
|
|
|
status: BookingStatus.CANCELLED,
|
|
|
|
},
|
|
|
|
});
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const foundBooking = await findBookingQuery(newTimeSlotBooking.id);
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
resultBooking = { ...foundBooking };
|
|
|
|
}
|
2023-03-14 04:19:05 +00:00
|
|
|
}
|
|
|
|
|
2023-03-28 01:16:07 +00:00
|
|
|
// seatAttendee is null when the organizer is rescheduling.
|
|
|
|
const seatAttendee: Partial<Person> | null = bookingSeat?.attendee || null;
|
2023-04-13 19:03:08 +00:00
|
|
|
if (seatAttendee) {
|
|
|
|
seatAttendee["language"] = { translate: tAttendees, locale: bookingSeat?.attendee.locale ?? "en" };
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-07-11 15:11:08 +00: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 04:19:05 +00:00
|
|
|
});
|
|
|
|
|
2023-07-11 15:11:08 +00: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 04:19:05 +00:00
|
|
|
}
|
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
// We don't want to trigger rescheduling logic of the original booking
|
|
|
|
originalRescheduledBooking = null;
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
return null;
|
|
|
|
}
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-07-11 15:11:08 +00: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 04:19:05 +00:00
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
const copyEvent = cloneDeep(evt);
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
|
2023-04-03 17:13:57 +00:00
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
const results = updateManager.results;
|
2023-04-03 17:13:57 +00:00
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
2023-04-03 17:13:57 +00:00
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
|
|
|
|
? calendarResult?.updatedEvent[0]?.iCalUID
|
|
|
|
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
await sendRescheduledSeatEmail(copyEvent, seatAttendee as Person);
|
|
|
|
const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => {
|
|
|
|
return attendee.email !== bookerEmail;
|
|
|
|
});
|
|
|
|
await lastAttendeeDeleteBooking(originalRescheduledBooking, filteredAttendees, originalBookingEvt);
|
2022-12-02 00:12:06 +00:00
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
const foundBooking = await findBookingQuery(newTimeSlotBooking.id);
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
resultBooking = { ...foundBooking, seatReferenceUid: bookingSeat?.referenceUid };
|
|
|
|
}
|
2023-03-14 04:19:05 +00: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 16:05:35 +00:00
|
|
|
return { ...attendee, language: { translate: tAttendees, locale: attendeeLanguage ?? "en" } };
|
2023-03-14 04:19:05 +00: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 20:36:22 +00:00
|
|
|
}
|
2023-03-14 04:19:05 +00: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 20:36:22 +00:00
|
|
|
}
|
2022-12-02 00:12:06 +00:00
|
|
|
|
2023-03-29 21:13:35 +00:00
|
|
|
const attendeeUniqueId = uuid();
|
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
await prisma.booking.update({
|
2023-03-14 04:19:05 +00: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 21:13:35 +00:00
|
|
|
bookingSeat: {
|
|
|
|
create: {
|
|
|
|
referenceUid: attendeeUniqueId,
|
|
|
|
data: {
|
|
|
|
description: additionalNotes,
|
|
|
|
},
|
|
|
|
booking: {
|
|
|
|
connect: {
|
|
|
|
id: booking.id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2023-03-14 04:19:05 +00: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 18:19:52 +00:00
|
|
|
if (!evt?.uid) {
|
|
|
|
evt.uid = booking?.uid ?? null;
|
|
|
|
}
|
2023-03-14 04:19:05 +00:00
|
|
|
const copyEvent = cloneDeep(evt);
|
2023-04-21 13:49:53 +00:00
|
|
|
copyEvent.uid = booking.uid;
|
2023-03-14 04:19:05 +00:00
|
|
|
await sendScheduledSeatsEmails(copyEvent, invitee[0], newSeat, !!eventType.seatsShowAttendees);
|
2022-12-02 00:12:06 +00:00
|
|
|
|
2023-07-11 07:41:21 +00:00
|
|
|
const credentials = await refreshCredentials(allCredentials);
|
2023-03-14 04:19:05 +00:00
|
|
|
const eventManager = new EventManager({ ...organizerUser, credentials });
|
|
|
|
await eventManager.updateCalendarAttendees(evt, booking);
|
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const foundBooking = await findBookingQuery(booking.id);
|
2023-03-14 04:19:05 +00: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 16:48:39 +00:00
|
|
|
...(paymentAppData.credentialId
|
|
|
|
? { id: paymentAppData.credentialId }
|
|
|
|
: { userId: organizerUser.id }),
|
2023-03-14 04:19:05 +00: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 21:44:14 +00:00
|
|
|
booking,
|
|
|
|
bookerEmail
|
2023-03-14 04:19:05 +00:00
|
|
|
);
|
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
resultBooking = { ...foundBooking };
|
|
|
|
resultBooking["message"] = "Payment required";
|
|
|
|
resultBooking["paymentUid"] = payment?.uid;
|
2023-07-11 15:11:08 +00:00
|
|
|
} else {
|
|
|
|
resultBooking = { ...foundBooking };
|
2023-03-14 04:19:05 +00:00
|
|
|
}
|
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
resultBooking["seatReferenceUid"] = evt.attendeeSeatId;
|
2023-04-13 19:03:08 +00: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 11:46:23 +00:00
|
|
|
const metadata = evt.videoCallData?.url ? { videoCallUrl: evt.videoCallData.url } : undefined;
|
2023-04-13 19:03:08 +00:00
|
|
|
try {
|
|
|
|
await scheduleWorkflowReminders({
|
|
|
|
workflows: eventType.workflows,
|
|
|
|
smsReminderNumber: smsReminderNumber || null,
|
2023-04-18 10:08:09 +00:00
|
|
|
calendarEvent: { ...evt, ...{ metadata, eventType: { slug: eventType.slug } } },
|
2023-08-10 18:52:36 +00:00
|
|
|
isNotConfirmed: evt.requiresConfirmation || false,
|
2023-04-13 19:03:08 +00:00
|
|
|
isRescheduleEvent: !!rescheduleUid,
|
|
|
|
isFirstRecurringEvent: true,
|
|
|
|
emailAttendeeSendToOverride: bookerEmail,
|
2023-08-01 14:13:28 +00:00
|
|
|
seatReferenceUid: evt.attendeeSeatId,
|
2023-08-10 18:52:36 +00:00
|
|
|
isKYCVerified,
|
2023-04-13 19:03:08 +00:00
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
log.error("Error while scheduling workflow reminders", error);
|
2023-03-14 04:19:05 +00:00
|
|
|
}
|
2023-04-13 19:03:08 +00:00
|
|
|
|
2023-07-13 20:40:16 +00:00
|
|
|
const webhookData = {
|
|
|
|
...evt,
|
|
|
|
...eventTypeInfo,
|
|
|
|
bookingId: booking?.id,
|
|
|
|
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 19:03:08 +00:00
|
|
|
return resultBooking;
|
2023-03-14 04:19:05 +00: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-02 00:12:06 +00:00
|
|
|
req.statusCode = 201;
|
2023-03-14 04:19:05 +00:00
|
|
|
return newBooking;
|
2022-12-02 00:12:06 +00:00
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
2023-02-27 20:45:40 +00:00
|
|
|
if (isTeamEventType) {
|
2022-10-12 13:04:51 +00:00
|
|
|
evt.team = {
|
2023-02-27 20:45:40 +00:00
|
|
|
members: teamMembers,
|
2022-10-12 13:04:51 +00:00
|
|
|
name: eventType.team?.name || "Nameless",
|
2023-02-27 20:45:40 +00:00
|
|
|
};
|
2022-10-12 13:04:51 +00: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;
|
|
|
|
}
|
|
|
|
|
2022-11-23 16:38:13 +00:00
|
|
|
// 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 && originalRescheduledBooking?.user?.id === userId;
|
2023-02-08 20:36:22 +00:00
|
|
|
const isConfirmedByDefault = (!requiresConfirmation && !paymentAppData.price) || userReschedulingIsOwner;
|
2022-10-12 13:04:51 +00:00
|
|
|
|
|
|
|
async function createBooking() {
|
|
|
|
if (originalRescheduledBooking) {
|
|
|
|
evt.title = originalRescheduledBooking?.title || evt.title;
|
2023-05-31 04:33:52 +00:00
|
|
|
evt.description = originalRescheduledBooking?.description || evt.description;
|
2023-03-14 04:19:05 +00:00
|
|
|
evt.location = originalRescheduledBooking?.location || evt.location;
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const eventTypeRel = !eventTypeId
|
|
|
|
? {}
|
|
|
|
: {
|
|
|
|
connect: {
|
|
|
|
id: eventTypeId,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null;
|
|
|
|
const dynamicGroupSlugRef = !eventTypeId ? (reqBody.user as string).toLowerCase() : null;
|
|
|
|
|
2023-03-14 04:19:05 +00:00
|
|
|
// 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 && originalRescheduledBooking?.user?.id === userId;
|
2023-03-23 18:50:39 +00:00
|
|
|
const isConfirmedByDefault = (!requiresConfirmation && !paymentAppData.price) || userReschedulingIsOwner;
|
2023-03-14 04:19:05 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2023-03-15 22:38:57 +00:00
|
|
|
if (evt.team?.members) {
|
|
|
|
attendeesData.push(
|
|
|
|
...evt.team.members.map((member) => ({
|
|
|
|
email: member.email,
|
|
|
|
name: member.name,
|
|
|
|
timeZone: member.timeZone,
|
|
|
|
locale: member.language.locale,
|
|
|
|
}))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
const newBookingData: Prisma.BookingCreateInput = {
|
|
|
|
uid,
|
2023-03-02 18:15:28 +00:00
|
|
|
responses: responses === null ? Prisma.JsonNull : responses,
|
2022-10-12 13:04:51 +00:00
|
|
|
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,
|
2023-03-02 18:15:28 +00:00
|
|
|
smsReminderNumber,
|
2023-02-06 18:05:30 +00:00
|
|
|
metadata: reqBody.metadata,
|
2022-10-12 13:04:51 +00:00
|
|
|
attendees: {
|
|
|
|
createMany: {
|
2023-03-14 04:19:05 +00:00
|
|
|
data: attendeesData,
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
dynamicEventSlugRef,
|
|
|
|
dynamicGroupSlugRef,
|
|
|
|
user: {
|
|
|
|
connect: {
|
|
|
|
id: organizerUser.id,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
destinationCalendar: evt.destinationCalendar
|
|
|
|
? {
|
|
|
|
connect: { id: evt.destinationCalendar.id },
|
|
|
|
}
|
|
|
|
: undefined,
|
|
|
|
};
|
2023-03-14 04:19:05 +00:00
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
if (reqBody.recurringEventId) {
|
|
|
|
newBookingData.recurringEventId = reqBody.recurringEventId;
|
|
|
|
}
|
|
|
|
if (originalRescheduledBooking) {
|
2023-03-22 10:17:19 +00:00
|
|
|
newBookingData.metadata = {
|
|
|
|
...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata),
|
|
|
|
};
|
2022-10-12 13:04:51 +00:00
|
|
|
newBookingData["paid"] = originalRescheduledBooking.paid;
|
|
|
|
newBookingData["fromReschedule"] = originalRescheduledBooking.uid;
|
2023-04-03 09:18:12 +00:00
|
|
|
if (originalRescheduledBooking.uid) {
|
|
|
|
newBookingData.cancellationReason = rescheduleReason;
|
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
if (newBookingData.attendees?.createMany?.data) {
|
2023-03-14 04:19:05 +00:00
|
|
|
// Reschedule logic with booking with seats
|
|
|
|
if (eventType?.seatsPerTimeSlot && bookerEmail) {
|
|
|
|
newBookingData.attendees.createMany.data = attendeesData.filter(
|
|
|
|
(attendee) => attendee.email === bookerEmail
|
|
|
|
);
|
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
if (originalRescheduledBooking.recurringEventId) {
|
|
|
|
newBookingData.recurringEventId = originalRescheduledBooking.recurringEventId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const createBookingObj = {
|
|
|
|
include: {
|
|
|
|
user: {
|
|
|
|
select: { email: true, name: true, timeZone: true },
|
|
|
|
},
|
|
|
|
attendees: true,
|
|
|
|
payment: true,
|
2023-03-14 04:19:05 +00:00
|
|
|
references: true,
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
|
|
|
data: newBookingData,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (originalRescheduledBooking?.paid && originalRescheduledBooking?.payment) {
|
|
|
|
const bookingPayment = originalRescheduledBooking?.payment?.find((payment) => payment.success);
|
|
|
|
|
|
|
|
if (bookingPayment) {
|
|
|
|
createBookingObj.data.payment = {
|
|
|
|
connect: { id: bookingPayment.id },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-08 20:36:22 +00:00
|
|
|
if (typeof paymentAppData.price === "number" && paymentAppData.price > 0) {
|
2023-04-13 05:27:54 +00:00
|
|
|
/* Validate if there is any payment app credential for this user */
|
2022-10-12 13:04:51 +00:00
|
|
|
await prisma.credential.findFirstOrThrow({
|
|
|
|
where: {
|
2023-04-13 05:27:54 +00:00
|
|
|
appId: paymentAppData.appId,
|
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 16:48:39 +00:00
|
|
|
...(paymentAppData.credentialId
|
|
|
|
? { id: paymentAppData.credentialId }
|
|
|
|
: { userId: organizerUser.id }),
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
|
|
|
select: {
|
|
|
|
id: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return prisma.booking.create(createBookingObj);
|
|
|
|
}
|
|
|
|
|
2023-04-03 17:13:57 +00:00
|
|
|
let results: EventResult<AdditionalInformation & { url?: string; iCalUID?: string }>[] = [];
|
2022-10-12 13:04:51 +00:00
|
|
|
let referencesToCreate: PartialReference[] = [];
|
|
|
|
|
|
|
|
type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
2022-10-19 16:11:50 +00:00
|
|
|
let booking: (Booking & { appsStatus?: AppsStatus[] }) | null = null;
|
2022-10-12 13:04:51 +00:00
|
|
|
try {
|
|
|
|
booking = await createBooking();
|
2023-03-14 04:19:05 +00:00
|
|
|
|
|
|
|
// @NOTE: Add specific try catch for all subsequent async calls to avoid error
|
2022-10-12 13:04:51 +00:00
|
|
|
// Sync Services
|
|
|
|
await syncServicesUpdateWebUser(
|
|
|
|
await prisma.user.findFirst({
|
|
|
|
where: { id: userId },
|
2022-12-08 23:20:24 +00:00
|
|
|
select: { id: true, email: true, name: true, username: true, createdDate: true },
|
2022-10-12 13:04:51 +00:00
|
|
|
})
|
|
|
|
);
|
|
|
|
evt.uid = booking?.uid ?? null;
|
2023-03-14 04:19:05 +00:00
|
|
|
|
|
|
|
if (booking && booking.id && eventType.seatsPerTimeSlot) {
|
|
|
|
const currentAttendee = booking.attendees.find(
|
|
|
|
(attendee) => attendee.email === req.body.responses.email
|
2023-06-06 11:59:57 +00:00
|
|
|
);
|
2023-03-14 04:19:05 +00: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 13:04:51 +00:00
|
|
|
} catch (_err) {
|
|
|
|
const err = getErrorFromUnknown(_err);
|
|
|
|
log.error(`Booking ${eventTypeId} failed`, "Error when saving booking to db", err.message);
|
|
|
|
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 07:41:21 +00:00
|
|
|
const credentials = await refreshCredentials(allCredentials);
|
2022-10-12 13:04:51 +00:00
|
|
|
const eventManager = new EventManager({ ...organizerUser, credentials });
|
|
|
|
|
2022-10-19 16:11:50 +00:00
|
|
|
function handleAppsStatus(
|
|
|
|
results: EventResult<AdditionalInformation>[],
|
|
|
|
booking: (Booking & { appsStatus?: AppsStatus[] }) | null
|
|
|
|
) {
|
|
|
|
// Taking care of apps status
|
|
|
|
const resultStatus: AppsStatus[] = results.map((app) => ({
|
|
|
|
appName: app.appName,
|
|
|
|
type: app.type,
|
|
|
|
success: app.success ? 1 : 0,
|
|
|
|
failures: !app.success ? 1 : 0,
|
2022-11-22 20:44:08 +00:00
|
|
|
errors: app.calError ? [app.calError] : [],
|
|
|
|
warnings: app.calWarnings,
|
2022-10-19 16:11:50 +00:00
|
|
|
}));
|
|
|
|
|
|
|
|
if (reqAppsStatus === undefined) {
|
|
|
|
if (booking !== null) {
|
|
|
|
booking.appsStatus = resultStatus;
|
|
|
|
}
|
|
|
|
evt.appsStatus = resultStatus;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// 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 20:44:08 +00: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 16:11:50 +00:00
|
|
|
} else {
|
|
|
|
prev[curr.type] = curr;
|
|
|
|
}
|
|
|
|
return prev;
|
|
|
|
}, {} as { [key: string]: AppsStatus });
|
|
|
|
evt.appsStatus = Object.values(calcAppsStatus);
|
|
|
|
}
|
|
|
|
|
2022-12-15 21:43:07 +00:00
|
|
|
let videoCallUrl;
|
2022-10-12 13:04:51 +00:00
|
|
|
if (originalRescheduledBooking?.uid) {
|
2023-02-20 17:40:08 +00:00
|
|
|
try {
|
|
|
|
// cancel workflow reminders from previous rescheduled booking
|
2023-07-31 17:35:48 +00:00
|
|
|
await cancelWorkflowReminders(originalRescheduledBooking.workflowReminders);
|
2023-02-20 17:40:08 +00:00
|
|
|
} catch (error) {
|
|
|
|
log.error("Error while canceling scheduled workflow reminders", error);
|
|
|
|
}
|
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
// Use EventManager to conditionally use all needed integrations.
|
2023-03-23 15:18:27 +00:00
|
|
|
addVideoCallDataToEvt(originalRescheduledBooking.references);
|
2023-03-22 14:23:09 +00:00
|
|
|
const updateManager = await eventManager.reschedule(evt, originalRescheduledBooking.uid);
|
2023-03-27 09:34:41 +00:00
|
|
|
|
2023-04-02 10:09:57 +00:00
|
|
|
//update original rescheduled booking (no seats event)
|
2023-03-27 09:34:41 +00:00
|
|
|
if (!eventType.seatsPerTimeSlot) {
|
2023-04-02 10:09:57 +00:00
|
|
|
await prisma.booking.update({
|
2023-03-27 09:34:41 +00:00
|
|
|
where: {
|
|
|
|
id: originalRescheduledBooking.id,
|
|
|
|
},
|
2023-04-02 10:09:57 +00:00
|
|
|
data: {
|
|
|
|
status: BookingStatus.CANCELLED,
|
|
|
|
},
|
2023-03-27 09:34:41 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-10-12 13:04:51 +00: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;
|
|
|
|
if (results.length > 0 && results.some((res) => !res.success)) {
|
|
|
|
const error = {
|
|
|
|
errorCode: "BookingReschedulingMeetingFailed",
|
|
|
|
message: "Booking Rescheduling failed",
|
|
|
|
};
|
|
|
|
|
|
|
|
log.error(`Booking ${organizerUser.name} failed`, error, results);
|
|
|
|
} else {
|
|
|
|
const metadata: AdditionalInformation = {};
|
2023-04-03 17:13:57 +00:00
|
|
|
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
|
|
|
|
|
|
|
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
|
|
|
|
? calendarResult?.updatedEvent[0]?.iCalUID
|
|
|
|
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
2022-10-12 13:04:51 +00:00
|
|
|
|
|
|
|
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;
|
2022-10-19 16:11:50 +00:00
|
|
|
handleAppsStatus(results, booking);
|
2023-03-17 08:24:48 +00:00
|
|
|
videoCallUrl = metadata.hangoutLink || videoCallUrl || updatedEvent?.url;
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (noEmail !== true) {
|
2023-03-14 04:19:05 +00:00
|
|
|
const copyEvent = cloneDeep(evt);
|
2022-10-12 13:04:51 +00:00
|
|
|
await sendRescheduledEmails({
|
2023-03-14 04:19:05 +00:00
|
|
|
...copyEvent,
|
2022-10-12 13:04:51 +00:00
|
|
|
additionalInformation: metadata,
|
|
|
|
additionalNotes, // Resets back to the additionalNote input and not the override value
|
2023-04-03 09:18:12 +00:00
|
|
|
cancellationReason: "$RCH$" + (rescheduleReason ? rescheduleReason : ""), // Removable code prefix to differentiate cancellation from rescheduling for email
|
2022-10-12 13:04:51 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If it's not a reschedule, doesn't require confirmation and there's no price,
|
|
|
|
// Create a booking
|
2023-02-08 20:36:22 +00:00
|
|
|
} else if (!requiresConfirmation && !paymentAppData.price) {
|
2022-10-12 13:04:51 +00: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 21:43:07 +00:00
|
|
|
videoCallUrl = evt.videoCallData && evt.videoCallData.url ? evt.videoCallData.url : null;
|
|
|
|
|
2022-10-12 13:04:51 +00:00
|
|
|
if (results.length > 0 && results.every((res) => !res.success)) {
|
|
|
|
const error = {
|
|
|
|
errorCode: "BookingCreatingMeetingFailed",
|
|
|
|
message: "Booking failed",
|
|
|
|
};
|
|
|
|
|
|
|
|
log.error(`Booking ${organizerUser.username} failed`, error, results);
|
|
|
|
} else {
|
|
|
|
const metadata: AdditionalInformation = {};
|
|
|
|
|
|
|
|
if (results.length) {
|
2023-01-10 02:01:57 +00: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 22:49:10 +00: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-10 02:01:57 +00:00
|
|
|
|
|
|
|
if (!googleCalResult) {
|
|
|
|
results.push({
|
|
|
|
...googleMeetResult,
|
|
|
|
success: false,
|
|
|
|
calWarnings: [tOrganizer("google_meet_warning")],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (googleCalResult?.createdEvent?.hangoutLink) {
|
|
|
|
results.push({
|
|
|
|
...googleMeetResult,
|
|
|
|
success: true,
|
|
|
|
});
|
2023-05-11 22:49:10 +00: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-10 02:01:57 +00:00
|
|
|
} else if (googleCalResult && !googleCalResult.createdEvent?.hangoutLink) {
|
|
|
|
results.push({
|
|
|
|
...googleMeetResult,
|
|
|
|
success: false,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2022-10-12 13:04:51 +00: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;
|
2022-10-19 16:11:50 +00:00
|
|
|
handleAppsStatus(results, booking);
|
2023-06-23 17:04:34 +00:00
|
|
|
videoCallUrl =
|
|
|
|
metadata.hangoutLink || organizerOrFirstDynamicGroupMemberDefaultLocationUrl || videoCallUrl;
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
if (noEmail !== true) {
|
2023-05-09 17:08:14 +00: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-03-10 14:38:29 +00:00
|
|
|
await sendScheduledEmails(
|
|
|
|
{
|
|
|
|
...evt,
|
|
|
|
additionalInformation: metadata,
|
|
|
|
additionalNotes,
|
|
|
|
customInputs,
|
|
|
|
},
|
2023-05-09 17:08:14 +00:00
|
|
|
eventNameObject,
|
|
|
|
isHostConfirmationEmailsDisabled,
|
|
|
|
isAttendeeConfirmationEmailDisabled
|
2023-03-10 14:38:29 +00:00
|
|
|
);
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-11 21:44:14 +00:00
|
|
|
const bookingRequiresPayment =
|
|
|
|
!Number.isNaN(paymentAppData.price) &&
|
|
|
|
paymentAppData.price > 0 &&
|
|
|
|
!originalRescheduledBooking?.paid &&
|
|
|
|
!!booking;
|
|
|
|
|
|
|
|
if (!isConfirmedByDefault && noEmail !== true && !bookingRequiresPayment) {
|
2022-10-12 13:04:51 +00:00
|
|
|
await sendOrganizerRequestEmail({ ...evt, additionalNotes });
|
|
|
|
await sendAttendeeRequestEmail({ ...evt, additionalNotes }, attendeesList[0]);
|
|
|
|
}
|
|
|
|
|
2023-04-11 21:44:14 +00:00
|
|
|
if (bookingRequiresPayment) {
|
2023-02-08 20:36:22 +00: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 16:48:39 +00:00
|
|
|
...(paymentAppData.credentialId ? { id: paymentAppData.credentialId } : { userId: organizerUser.id }),
|
2023-02-08 20:36:22 +00: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 13:04:51 +00:00
|
|
|
|
2023-02-08 20:36:22 +00:00
|
|
|
if (!eventTypePaymentAppCredential) {
|
2022-10-12 13:04:51 +00:00
|
|
|
throw new HttpError({ statusCode: 400, message: "Missing payment credentials" });
|
2023-02-08 20:36:22 +00:00
|
|
|
}
|
2022-10-12 13:04:51 +00:00
|
|
|
|
2023-02-08 20:36:22 +00:00
|
|
|
// Convert type of eventTypePaymentAppCredential to appId: EventTypeAppList
|
2022-10-12 13:04:51 +00:00
|
|
|
if (!booking.user) booking.user = organizerUser;
|
2023-02-08 20:36:22 +00:00
|
|
|
const payment = await handlePayment(
|
|
|
|
evt,
|
|
|
|
eventType,
|
|
|
|
eventTypePaymentAppCredential as IEventTypePaymentCredentialType,
|
2023-04-11 21:44:14 +00:00
|
|
|
booking,
|
|
|
|
bookerEmail
|
2023-02-08 20:36:22 +00:00
|
|
|
);
|
2022-10-12 13:04:51 +00:00
|
|
|
|
|
|
|
req.statusCode = 201;
|
2023-02-08 20:36:22 +00:00
|
|
|
return { ...booking, message: "Payment required", paymentUid: payment?.uid };
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
log.debug(`Booking ${organizerUser.username} completed`);
|
2023-02-20 18:52:07 +00:00
|
|
|
|
|
|
|
if (booking.location?.startsWith("http")) {
|
|
|
|
videoCallUrl = booking.location;
|
|
|
|
}
|
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const metadata = videoCallUrl
|
|
|
|
? {
|
|
|
|
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
|
|
|
|
}
|
|
|
|
: undefined;
|
2023-05-30 15:35:05 +00:00
|
|
|
|
|
|
|
const webhookData = {
|
|
|
|
...evt,
|
|
|
|
...eventTypeInfo,
|
|
|
|
bookingId: booking?.id,
|
|
|
|
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-11-23 16:38:13 +00:00
|
|
|
if (isConfirmedByDefault) {
|
|
|
|
try {
|
|
|
|
const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);
|
2022-10-12 13:04:51 +00:00
|
|
|
|
2022-11-23 16:38:13 +00:00
|
|
|
subscribersMeetingEnded.forEach((subscriber) => {
|
|
|
|
if (rescheduleUid && originalRescheduledBooking) {
|
|
|
|
cancelScheduledJobs(originalRescheduledBooking, undefined, true);
|
|
|
|
}
|
|
|
|
if (booking && booking.status === BookingStatus.ACCEPTED) {
|
|
|
|
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
log.error("Error while running scheduledJobs for booking", error);
|
|
|
|
}
|
2022-10-25 00:32:14 +00:00
|
|
|
|
2023-05-30 15:35:05 +00:00
|
|
|
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
|
|
|
|
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
|
|
|
|
} else if (eventType.requiresConfirmation) {
|
|
|
|
// 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-25 00:32:14 +00:00
|
|
|
}
|
2022-11-23 16:38:13 +00:00
|
|
|
|
2022-10-12 13:04:51 +00: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-25 00:32:14 +00:00
|
|
|
try {
|
|
|
|
if (hasHashedBookingLink) {
|
|
|
|
await prisma.hashedLink.update({
|
|
|
|
where: {
|
|
|
|
link: reqBody.hashedLink as string,
|
|
|
|
},
|
|
|
|
data: {
|
|
|
|
link: hashedUid,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
log.error("Error while updating hashed link", error);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!booking) throw new HttpError({ statusCode: 400, message: "Booking failed" });
|
|
|
|
|
|
|
|
try {
|
|
|
|
await prisma.booking.update({
|
2022-10-12 13:04:51 +00:00
|
|
|
where: {
|
2022-10-25 00:32:14 +00:00
|
|
|
uid: booking.uid,
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
|
|
|
data: {
|
2023-02-06 18:05:30 +00:00
|
|
|
metadata: { ...(typeof booking.metadata === "object" && booking.metadata), ...metadata },
|
2022-10-25 00:32:14 +00:00
|
|
|
references: {
|
|
|
|
createMany: {
|
|
|
|
data: referencesToCreate,
|
|
|
|
},
|
|
|
|
},
|
2022-10-12 13:04:51 +00:00
|
|
|
},
|
|
|
|
});
|
2022-10-25 00:32:14 +00:00
|
|
|
} catch (error) {
|
|
|
|
log.error("Error while creating booking references", error);
|
|
|
|
}
|
|
|
|
|
2023-04-21 11:46:23 +00:00
|
|
|
const metadataFromEvent = videoCallUrl ? { videoCallUrl } : undefined;
|
2023-04-13 19:03:08 +00:00
|
|
|
|
2022-10-25 00:32:14 +00:00
|
|
|
try {
|
2023-04-13 19:03:08 +00:00
|
|
|
await scheduleWorkflowReminders({
|
|
|
|
workflows: eventType.workflows,
|
|
|
|
smsReminderNumber: smsReminderNumber || null,
|
2023-04-18 10:08:09 +00:00
|
|
|
calendarEvent: {
|
|
|
|
...evt,
|
|
|
|
...{ metadata: metadataFromEvent, eventType: { slug: eventType.slug } },
|
|
|
|
},
|
2023-08-10 18:52:36 +00:00
|
|
|
isNotConfirmed: evt.requiresConfirmation || false,
|
2023-04-13 19:03:08 +00:00
|
|
|
isRescheduleEvent: !!rescheduleUid,
|
|
|
|
isFirstRecurringEvent: true,
|
2023-04-18 10:08:09 +00:00
|
|
|
hideBranding: !!eventType.owner?.hideBranding,
|
2023-08-01 14:13:28 +00:00
|
|
|
seatReferenceUid: evt.attendeeSeatId,
|
2023-08-10 18:52:36 +00:00
|
|
|
isKYCVerified,
|
2023-04-13 19:03:08 +00:00
|
|
|
});
|
2022-10-25 00:32:14 +00:00
|
|
|
} catch (error) {
|
|
|
|
log.error("Error while scheduling workflow reminders", error);
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// booking successful
|
|
|
|
req.statusCode = 201;
|
2023-03-14 04:19:05 +00:00
|
|
|
return {
|
|
|
|
...booking,
|
|
|
|
seatReferenceUid: evt.attendeeSeatId,
|
|
|
|
};
|
2022-10-12 13:04:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default handler;
|
2022-10-14 22:45:02 +00: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 19:39:41 +00: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 22:45:02 +00: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 04:19:05 +00:00
|
|
|
|
2023-04-13 19:03:08 +00:00
|
|
|
const findBookingQuery = async (bookingId: number) => {
|
2023-03-14 04:19:05 +00: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,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
eventType: {
|
|
|
|
select: {
|
|
|
|
title: true,
|
|
|
|
description: true,
|
|
|
|
currency: true,
|
|
|
|
length: true,
|
|
|
|
requiresConfirmation: true,
|
2023-07-31 17:51:11 +00:00
|
|
|
requiresBookerEmailVerification: true,
|
2023-03-14 04:19:05 +00:00
|
|
|
price: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
// This should never happen but it's just typescript safe
|
|
|
|
if (!foundBooking) {
|
|
|
|
throw new Error("Internal Error.");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't leak any sensitive data
|
|
|
|
return foundBooking;
|
|
|
|
};
|