feat: platform toggle calendar event creation (#21788)

* feat: add areCalendarEventsEnabled to PlatformOAuthClient table

* feat: areCalendarEvents enabled in handleNewBooking and v2

* feat: v2 handle areCalendarEventsEnabled

* feat: toggle areCalendarEventsEnabled on frontend

* feat: enable areCalendarEventsEnabled for recurring,confirm,paid

* refactor: use placeholder calendar event

* remove merge conflicts

* chore: bump libraries

* chore: bump libraries
This commit is contained in:
Lauris Skraucis
2025-06-13 14:15:47 +00:00
committed by GitHub
parent d7fd055171
commit 4a53ec0b74
33 changed files with 525 additions and 22 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.220",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.222",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -77,6 +77,7 @@ type OAuthRequestParams = {
platformBookingUrl: string;
platformBookingLocation?: string;
arePlatformEmailsEnabled: boolean;
areCalendarEventsEnabled: boolean;
};
const DEFAULT_PLATFORM_PARAMS = {
@@ -86,6 +87,7 @@ const DEFAULT_PLATFORM_PARAMS = {
platformBookingUrl: "",
arePlatformEmailsEnabled: false,
platformBookingLocation: undefined,
areCalendarEventsEnabled: false,
};
@Controller({
@@ -195,6 +197,7 @@ export class BookingsController_2024_04_15 {
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
});
if (booking.userId && booking.uid && booking.startTime) {
void (await this.billingService.increaseUsageByUserId(booking.userId, {
@@ -428,7 +431,7 @@ export class BookingsController_2024_04_15 {
if (isEmbed) {
// embed should ignore oauth client settings and enable emails by default
return { ...res, arePlatformEmailsEnabled: true };
return { ...res, arePlatformEmailsEnabled: true, areCalendarEventsEnabled: true };
}
try {
@@ -440,6 +443,7 @@ export class BookingsController_2024_04_15 {
res.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
res.platformBookingUrl = client.bookingRedirectUri ?? "";
res.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
res.areCalendarEventsEnabled = client.areCalendarEventsEnabled;
}
return res;
} catch (err) {
@@ -35,6 +35,7 @@ import {
X_CAL_CLIENT_ID,
} from "@calcom/platform-constants";
import { AttendeeScheduledEmail, OrganizerScheduledEmail } from "@calcom/platform-libraries/emails";
import { EventManager } from "@calcom/platform-libraries/event-types";
import {
CreateEventTypeInput_2024_06_14,
FAILED_EVENT_TYPE_IDENTIFICATION_ERROR_MESSAGE,
@@ -78,6 +79,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const eventTypeSlug = `user-bookings-event-type-${randomString()}`;
let recurringEventTypeId: number;
const recurringEventTypeSlug = `user-bookings-event-type-${randomString()}`;
let eventTypeRequiresConfirmationId: number;
const eventTypeRequiresConfirmationSlug = `user-bookings-event-type-requires-confirmation-${randomString()}`;
let createdBooking: BookingOutput_2024_08_13;
let rescheduledBooking: BookingOutput_2024_08_13;
@@ -153,6 +156,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
);
recurringEventTypeId = recurringEvent.id;
const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create(
{
title: `user-bookings-2024-08-13-event-type-requires-confirmation-${randomString()}`,
slug: eventTypeRequiresConfirmationSlug,
length: 60,
requiresConfirmation: true,
},
user.id
);
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
bookingInThePast = await bookingsRepositoryFixture.create({
user: {
connect: {
@@ -2539,6 +2553,285 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("calendar events", () => {
beforeEach(() => {
jest.restoreAllMocks();
jest
.spyOn(EventManager.prototype, "create")
.mockImplementation(() => Promise.resolve({ results: [], referencesToCreate: [] }));
});
describe("platform oAuth client has calendar events enabled", () => {
beforeAll(async () => {
await oauthClientRepositoryFixture.update(oAuthClient.id, {
areCalendarEventsEnabled: true,
});
});
it("should create calendar event when booking normal event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 9, 0, 0)).toISOString(),
eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(EventManager.prototype.create).toHaveBeenCalledTimes(1);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should create calendar events when booking recurring event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 10, 9, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(EventManager.prototype.create).toHaveBeenCalledTimes(3);
});
});
describe("event type requires confirmation", () => {
let bookingThatRequiresConfirmationUid: string;
it("should not create calendar event when booking event that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 12, 9, 0, 0)).toISOString(),
eventTypeId: eventTypeRequiresConfirmationId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
bookingThatRequiresConfirmationUid = data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should create calendar event when confirming event that requires confirmation", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${bookingThatRequiresConfirmationUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(EventManager.prototype.create).toHaveBeenCalledTimes(1);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
});
});
describe("platform oAuth client has calendar events disabled", () => {
beforeAll(async () => {
await oauthClientRepositoryFixture.update(oAuthClient.id, {
areCalendarEventsEnabled: false,
});
});
it("should not create calendar event when booking", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 10, 0, 0)).toISOString(),
eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
};
const beforeCreate = new Date();
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const afterCreate = new Date();
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should not create calendar events when booking recurring event", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 11, 10, 0, 0)).toISOString(),
eventTypeId: recurringEventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
});
});
describe("event type requires confirmation", () => {
let bookingThatRequiresConfirmationUid: string;
it("should not create calendar event when booking event that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 12, 10, 0, 0)).toISOString(),
eventTypeId: eventTypeRequiresConfirmationId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
bookingThatRequiresConfirmationUid = data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should not create calendar event when confirming event that requires confirmation", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${bookingThatRequiresConfirmationUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(EventManager.prototype.create).toHaveBeenCalledTimes(0);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
});
});
});
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
return (
!Array.isArray(data) &&
@@ -404,6 +404,7 @@ export class BookingsService_2024_08_13 {
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
noEmail: bookingRequest.noEmail,
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
});
const ids = bookings.map((booking) => booking.id || 0);
return this.outputService.getOutputRecurringBookings(ids);
@@ -424,6 +425,7 @@ export class BookingsService_2024_08_13 {
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
});
return this.outputService.getOutputCreateRecurringSeatedBookings(
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
@@ -445,6 +447,7 @@ export class BookingsService_2024_08_13 {
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
});
if (!booking.uid) {
@@ -475,6 +478,7 @@ export class BookingsService_2024_08_13 {
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
});
if (!booking.uid) {
@@ -652,6 +656,7 @@ export class BookingsService_2024_08_13 {
platformCancelUrl: bookingRequest.platformCancelUrl,
platformBookingUrl: bookingRequest.platformBookingUrl,
platformBookingLocation: bookingRequest.platformBookingLocation,
areCalendarEventsEnabled: bookingRequest.areCalendarEventsEnabled,
});
if (!booking.uid) {
throw new Error("Booking missing uid");
@@ -59,6 +59,7 @@ type OAuthRequestParams = {
platformBookingUrl: string;
platformBookingLocation?: string;
arePlatformEmailsEnabled: boolean;
areCalendarEventsEnabled: boolean;
};
export enum Frequency {
@@ -54,6 +54,7 @@ export class PlatformBookingsService {
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
areCalendarEventsEnabled: oAuthClient.areCalendarEventsEnabled,
};
}
@@ -78,6 +79,7 @@ export class PlatformBookingsService {
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
areCalendarEventsEnabled: oAuthClient.areCalendarEventsEnabled,
};
}
@@ -25,6 +25,7 @@ export class OAuthClientsOutputService {
bookingRescheduleRedirectUri: client.bookingRescheduleRedirectUri ?? undefined,
areEmailsEnabled: client.areEmailsEnabled,
areDefaultEventTypesEnabled: client.areDefaultEventTypesEnabled,
areCalendarEventsEnabled: client.areCalendarEventsEnabled,
};
}
+16 -1
View File
@@ -13912,6 +13912,11 @@
"type": "boolean",
"default": false,
"description": "If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user."
},
"areCalendarEventsEnabled": {
"type": "boolean",
"default": true,
"description": "If true and if managed user has calendar connected, calendar events will be created. Disable it if you manually create calendar events. Default to true."
}
},
"required": [
@@ -14034,6 +14039,11 @@
"example": true,
"description": "If enabled, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Leave this disabled if you want to create a managed user and then manually create event types for the user."
},
"areCalendarEventsEnabled": {
"type": "boolean",
"example": true,
"description": "If true and if managed user has calendar connected, calendar events will be created. Disable it if you manually create calendar events. Default to true."
},
"bookingRedirectUri": {
"type": "string",
"example": "https://example.com/booking-redirect"
@@ -14056,7 +14066,8 @@
"organizationId",
"createdAt",
"areEmailsEnabled",
"areDefaultEventTypesEnabled"
"areDefaultEventTypesEnabled",
"areCalendarEventsEnabled"
]
},
"GetOAuthClientsResponseDto": {
@@ -14132,6 +14143,10 @@
"areDefaultEventTypesEnabled": {
"type": "boolean",
"description": "If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user."
},
"areCalendarEventsEnabled": {
"type": "boolean",
"description": "If true and if managed user has calendar connected, calendar events will be created. Disable it if you manually create calendar events. Default to true."
}
}
},
@@ -47,6 +47,13 @@ export class OAuthClientRepositoryFixture {
});
}
async update(clientId: PlatformOAuthClient["id"], data: Prisma.PlatformOAuthClientUpdateInput) {
return this.prismaWriteClient.platformOAuthClient.update({
where: { id: clientId },
data,
});
}
async delete(clientId: PlatformOAuthClient["id"]) {
return this.prismaWriteClient.platformOAuthClient.delete({ where: { id: clientId } });
}
@@ -48,6 +48,7 @@ export const OAuthClientsList = ({ oauthClients, isDeleting, handleDelete }: OAu
onDelete={handleDelete}
areEmailsEnabled={client.areEmailsEnabled}
areDefaultEventTypesEnabled={client.areDefaultEventTypesEnabled}
areCalendarEventsEnabled={client.areCalendarEventsEnabled}
organizationId={client.organizationId}
/>
);
@@ -18,6 +18,7 @@ type OAuthClientCardProps = {
bookingRescheduleRedirectUri: string | null | undefined;
areEmailsEnabled: boolean | undefined;
areDefaultEventTypesEnabled: boolean;
areCalendarEventsEnabled: boolean;
permissions: Array<keyof typeof PERMISSION_MAP>;
lastItem: boolean;
id: string;
@@ -42,6 +43,7 @@ export const OAuthClientCard = ({
isLoading,
areEmailsEnabled,
areDefaultEventTypesEnabled,
areCalendarEventsEnabled,
organizationId,
}: OAuthClientCardProps) => {
const router = useRouter();
@@ -173,6 +175,10 @@ export const OAuthClientCard = ({
<span className="text-sm font-semibold">Default event types enabled:</span>{" "}
{areDefaultEventTypesEnabled ? "Yes" : "No"}
</div>
<div className="flex gap-1 text-sm">
<span className="text-sm font-semibold">Calendar events enabled:</span>{" "}
{areCalendarEventsEnabled ? "Yes" : "No"}
</div>
</div>
<div className="flex items-start gap-4">
<Button
@@ -37,6 +37,7 @@ export type FormValues = {
bookingRescheduleRedirectUri?: string;
areEmailsEnabled?: boolean;
areDefaultEventTypesEnabled?: boolean;
areCalendarEventsEnabled?: boolean;
};
export const OAuthClientForm = ({
@@ -236,6 +237,27 @@ export const OAuthClientForm = ({
Enable emails
</label>
</div>
<div className="mt-6">
<div className="flex items-center">
<input
{...register("areCalendarEventsEnabled")}
id="areCalendarEventsEnabled"
className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed"
type="checkbox"
disabled={isFormDisabled}
/>
<label htmlFor="areCalendarEventsEnabled" className="cursor-pointer px-2 text-base font-semibold">
Enable calendar events
</label>
<Tooltip
className="max-w-[400px] whitespace-normal"
content="If enabled and the managed user has calendar connected, an event in the calendar will be created. By default true. Disable it if you want to create events in the calendar manually.">
<div className="ml-1">
<Icon name="info" className="h-4 w-4 text-gray-500" aria-hidden="true" />
</div>
</Tooltip>
</div>
</div>
<div className="mt-6">
<div className="flex items-center">
<input
@@ -60,6 +60,7 @@ export default function EditOAuthClient() {
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
areEmailsEnabled: data.areEmailsEnabled,
areDefaultEventTypesEnabled: data.areDefaultEventTypesEnabled,
areCalendarEventsEnabled: data.areCalendarEventsEnabled,
});
};
@@ -87,6 +88,7 @@ export default function EditOAuthClient() {
name: data?.name ?? "",
areEmailsEnabled: data.areEmailsEnabled ?? false,
areDefaultEventTypesEnabled: data.areDefaultEventTypesEnabled ?? false,
areCalendarEventsEnabled: data.areCalendarEventsEnabled,
redirectUris: data?.redirectUris?.map((uri) => ({ uri })) ?? [{ uri: "" }],
bookingRedirectUri: data?.bookingRedirectUri ?? "",
bookingCancelRedirectUri: data?.bookingCancelRedirectUri ?? "",
@@ -56,6 +56,7 @@ export default function CreateOAuthClient() {
bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri,
areEmailsEnabled: data.areEmailsEnabled,
areDefaultEventTypesEnabled: data.areDefaultEventTypesEnabled,
areCalendarEventsEnabled: data.areCalendarEventsEnabled,
});
};
@@ -76,7 +77,24 @@ export default function CreateOAuthClient() {
</p>
</div>
</div>
<OAuthClientForm isPending={isSaving} onSubmit={onSubmit} />
<OAuthClientForm
isPending={isSaving}
onSubmit={onSubmit}
defaultValues={{
areCalendarEventsEnabled: true,
areEmailsEnabled: true,
eventTypeRead: true,
eventTypeWrite: true,
bookingRead: true,
bookingWrite: true,
scheduleRead: true,
scheduleWrite: true,
appsRead: true,
appsWrite: true,
profileRead: true,
profileWrite: true,
}}
/>
</div>
</Shell>
</div>
+16 -1
View File
@@ -13254,6 +13254,11 @@
"type": "boolean",
"default": false,
"description": "If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user."
},
"areCalendarEventsEnabled": {
"type": "boolean",
"default": true,
"description": "If true and if managed user has calendar connected, calendar events will be created. Disable it if you manually create calendar events. Default to true."
}
},
"required": ["name", "redirectUris", "permissions"]
@@ -13358,6 +13363,11 @@
"example": true,
"description": "If enabled, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Leave this disabled if you want to create a managed user and then manually create event types for the user."
},
"areCalendarEventsEnabled": {
"type": "boolean",
"example": true,
"description": "If true and if managed user has calendar connected, calendar events will be created. Disable it if you manually create calendar events. Default to true."
},
"bookingRedirectUri": {
"type": "string",
"example": "https://example.com/booking-redirect"
@@ -13380,7 +13390,8 @@
"organizationId",
"createdAt",
"areEmailsEnabled",
"areDefaultEventTypesEnabled"
"areDefaultEventTypesEnabled",
"areCalendarEventsEnabled"
]
},
"GetOAuthClientsResponseDto": {
@@ -13444,6 +13455,10 @@
"areDefaultEventTypesEnabled": {
"type": "boolean",
"description": "If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user."
},
"areCalendarEventsEnabled": {
"type": "boolean",
"description": "If true and if managed user has calendar connected, calendar events will be created. Disable it if you manually create calendar events. Default to true."
}
}
},
@@ -14,7 +14,7 @@ import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import type { EventManagerUser } from "@calcom/lib/EventManager";
import EventManager from "@calcom/lib/EventManager";
import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager";
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
@@ -88,7 +88,8 @@ export async function handleConfirmation(args: {
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});
const apps = eventTypeAppMetadataOptionalSchema.parse(eventTypeMetadata?.apps);
const eventManager = new EventManager(user, apps);
const scheduleResult = await eventManager.create(evt);
const areCalendarEventsEnabled = platformClientParams?.areCalendarEventsEnabled ?? true;
const scheduleResult = areCalendarEventsEnabled ? await eventManager.create(evt) : placeholderCreatedEvent;
const results = scheduleResult.results;
const metadata: AdditionalInformation = {};
const workflows = await getAllWorkflowsFromEventType(eventType, booking.userId);
@@ -43,7 +43,7 @@ import {
scheduleTrigger,
} from "@calcom/features/webhooks/lib/scheduleTrigger";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import EventManager from "@calcom/lib/EventManager";
import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager";
import { handleAnalyticsEvents } from "@calcom/lib/analyticsManager/handleAnalyticsEvents";
import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils";
import { getUsernameList } from "@calcom/lib/defaultEvents";
@@ -373,6 +373,7 @@ export type PlatformParams = {
platformBookingUrl?: string;
platformRescheduleUrl?: string;
platformBookingLocation?: string;
areCalendarEventsEnabled?: boolean;
};
export type BookingHandlerInput = {
@@ -397,6 +398,7 @@ async function handler(
platformBookingLocation,
hostname,
forcedSlug,
areCalendarEventsEnabled = true,
} = input;
const isPlatformBooking = !!platformClientId;
@@ -1737,7 +1739,7 @@ async function handler(
// Create a booking
} else if (isConfirmedByDefault) {
// Use EventManager to conditionally use all needed integrations.
const createManager = await eventManager.create(evt);
const createManager = areCalendarEventsEnabled ? await eventManager.create(evt) : placeholderCreatedEvent;
if (evt.location) {
booking.location = evt.location;
}
@@ -9,6 +9,7 @@ export type PlatformParams = {
platformBookingUrl?: string;
platformRescheduleUrl?: string;
platformBookingLocation?: string;
areCalendarEventsEnabled?: boolean;
};
export type BookingHandlerInput = {
@@ -45,6 +46,7 @@ export const handleNewRecurringBooking = async (input: BookingHandlerInput): Pro
platformCancelUrl: input.platformCancelUrl,
platformBookingUrl: input.platformBookingUrl,
platformBookingLocation: input.platformBookingLocation,
areCalendarEventsEnabled: input.areCalendarEventsEnabled,
};
if (isRoundRobin) {
+15 -3
View File
@@ -8,7 +8,9 @@ import { doesBookingRequireConfirmation } from "@calcom/features/bookings/lib/do
import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
import stripe from "@calcom/features/ee/payments/server/stripe";
import EventManager from "@calcom/lib/EventManager";
import { getPlatformParams } from "@calcom/features/platform-oauth-client/get-platform-params";
import { PlatformOAuthClientRepository } from "@calcom/features/platform-oauth-client/platform-oauth-client.repository";
import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
@@ -80,9 +82,18 @@ const handleSetupSuccess = async (event: Stripe.Event) => {
metadata,
});
const platformOAuthClientRepository = new PlatformOAuthClientRepository();
const platformOAuthClient = user.isPlatformManaged
? await platformOAuthClientRepository.getByUserId(user.id)
: null;
const areCalendarEventsEnabled = platformOAuthClient?.areCalendarEventsEnabled ?? true;
const areEmailsEnabled = platformOAuthClient?.areEmailsEnabled ?? true;
if (!requiresConfirmation) {
const eventManager = new EventManager({ ...user, credentials: allCredentials }, metadata?.apps);
const scheduleResult = await eventManager.create(evt);
const scheduleResult = areCalendarEventsEnabled
? await eventManager.create(evt)
: placeholderCreatedEvent;
bookingData.references = { create: scheduleResult.referencesToCreate };
bookingData.status = BookingStatus.ACCEPTED;
}
@@ -114,8 +125,9 @@ const handleSetupSuccess = async (event: Stripe.Event) => {
bookingId: booking.id,
booking,
paid: true,
platformClientParams: platformOAuthClient ? getPlatformParams(platformOAuthClient) : undefined,
});
} else {
} else if (areEmailsEnabled) {
await sendOrganizerRequestEmail({ ...evt }, eventType.metadata);
await sendAttendeeRequestEmailAndSMS({ ...evt }, evt.attendees[0], eventType.metadata);
}
@@ -0,0 +1,12 @@
import type { PlatformOAuthClient } from "@prisma/client";
export function getPlatformParams(platformOAuthClient: PlatformOAuthClient) {
return {
platformClientId: platformOAuthClient.id,
platformCancelUrl: platformOAuthClient.bookingCancelRedirectUri,
platformRescheduleUrl: platformOAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: platformOAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: platformOAuthClient.areEmailsEnabled,
areCalendarEventsEnabled: platformOAuthClient.areCalendarEventsEnabled,
};
}
@@ -0,0 +1,5 @@
import type { PlatformOAuthClient } from "@prisma/client";
export interface IPlatformOAuthClientRepository {
getByUserId(userId: number): Promise<PlatformOAuthClient | null>;
}
@@ -0,0 +1,24 @@
import { captureException } from "@sentry/nextjs";
import prisma from "@calcom/prisma";
import type { IPlatformOAuthClientRepository } from "./platform-oauth-client.repository.interface";
export class PlatformOAuthClientRepository implements IPlatformOAuthClientRepository {
async getByUserId(userId: number) {
try {
return prisma.platformOAuthClient.findFirst({
where: {
users: {
some: {
id: userId,
},
},
},
});
} catch (err) {
captureException(err);
throw err;
}
}
}
+2
View File
@@ -1142,3 +1142,5 @@ export default class EventManager {
return this.appOptions[credential.appId as keyof typeof this.appOptions];
}
}
export const placeholderCreatedEvent = { results: [], referencesToCreate: [] };
+1
View File
@@ -91,6 +91,7 @@ export async function getBooking(bookingId: number) {
name: true,
locale: true,
destinationCalendar: true,
isPlatformManaged: true,
},
},
},
+16 -3
View File
@@ -5,7 +5,9 @@ import { doesBookingRequireConfirmation } from "@calcom/features/bookings/lib/do
import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
import { handleBookingRequested } from "@calcom/features/bookings/lib/handleBookingRequested";
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
import EventManager from "@calcom/lib/EventManager";
import { getPlatformParams } from "@calcom/features/platform-oauth-client/get-platform-params";
import { PlatformOAuthClientRepository } from "@calcom/features/platform-oauth-client/platform-oauth-client.repository";
import EventManager, { placeholderCreatedEvent } from "@calcom/lib/EventManager";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import { getBooking } from "@calcom/lib/payment/getBooking";
import prisma from "@calcom/prisma";
@@ -33,10 +35,20 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number)
});
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
const platformOAuthClientRepository = new PlatformOAuthClientRepository();
const platformOAuthClient = userWithCredentials.isPlatformManaged
? await platformOAuthClientRepository.getByUserId(userWithCredentials.id)
: null;
const areCalendarEventsEnabled = platformOAuthClient?.areCalendarEventsEnabled ?? true;
const areEmailsEnabled = platformOAuthClient?.areEmailsEnabled ?? true;
if (isConfirmed) {
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
const eventManager = new EventManager({ ...userWithCredentials, credentials: allCredentials }, apps);
const scheduleResult = await eventManager.create(evt);
const scheduleResult = areCalendarEventsEnabled
? await eventManager.create(evt)
: placeholderCreatedEvent;
bookingData.references = { create: scheduleResult.referencesToCreate };
}
@@ -76,6 +88,7 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number)
bookingId: booking.id,
booking,
paid: true,
platformClientParams: platformOAuthClient ? getPlatformParams(platformOAuthClient) : undefined,
});
} else {
await handleBookingRequested({
@@ -84,7 +97,7 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number)
});
log.debug(`handling booking request for eventId ${eventType.id}`);
}
} else {
} else if (areEmailsEnabled) {
await sendScheduledEmailsAndSMS({ ...evt }, undefined, undefined, undefined, eventType.metadata);
}
+3 -1
View File
@@ -1,3 +1,5 @@
import EventManager from "@calcom/lib/EventManager";
export { getPublicEvent, type PublicEventType } from "@calcom/features/eventtypes/lib/getPublicEvent";
export { getBulkUserEventTypes, getBulkTeamEventTypes } from "@calcom/lib/event-types/getBulkEventTypes";
@@ -17,7 +19,7 @@ export {
} from "@calcom/prisma/zod-utils";
export { validateCustomEventName } from "@calcom/lib/event";
export { EventManager };
export { getEventTypeById } from "@calcom/lib/event-types/getEventTypeById";
export { getEventTypesByViewer } from "@calcom/lib/event-types/getEventTypesByViewer";
export type { EventType } from "@calcom/lib/event-types/getEventTypeById";
@@ -6,6 +6,10 @@ import { PERMISSION_MAP } from "@calcom/platform-constants";
export const ARE_DEFAULT_EVENT_TYPES_ENABLED_DOCS =
"If true, when creating a managed user the managed user will have 4 default event types: 30 and 60 minutes without Cal video, 30 and 60 minutes with Cal video. Set this as false if you want to create a managed user and then manually create event types for the user.";
export const ARE_CALENDAR_EVENTS_ENABLED_DOCS =
"If true and if managed user has calendar connected, calendar events will be created. Disable it if you manually create calendar events. Default to true.";
export class CreateOAuthClientInput {
@IsOptional()
@IsString()
@@ -60,4 +64,14 @@ export class CreateOAuthClientInput {
description: ARE_DEFAULT_EVENT_TYPES_ENABLED_DOCS,
})
areDefaultEventTypesEnabled? = false;
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value ?? true)
@ApiPropertyOptional({
type: Boolean,
default: true,
description: ARE_CALENDAR_EVENTS_ENABLED_DOCS,
})
areCalendarEventsEnabled? = true;
}
@@ -1,7 +1,10 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsArray, IsBoolean, IsOptional, IsString } from "class-validator";
import { ARE_DEFAULT_EVENT_TYPES_ENABLED_DOCS } from "./create-oauth-client.input";
import {
ARE_CALENDAR_EVENTS_ENABLED_DOCS,
ARE_DEFAULT_EVENT_TYPES_ENABLED_DOCS,
} from "./create-oauth-client.input";
export class UpdateOAuthClientInput {
@IsOptional()
@@ -47,4 +50,12 @@ export class UpdateOAuthClientInput {
description: ARE_DEFAULT_EVENT_TYPES_ENABLED_DOCS,
})
areDefaultEventTypesEnabled?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({
type: Boolean,
description: ARE_CALENDAR_EVENTS_ENABLED_DOCS,
})
areCalendarEventsEnabled?: boolean;
}
@@ -3,6 +3,8 @@ import { IsArray, IsEnum, IsString, IsNumber, IsOptional, IsDate, IsBoolean, IsU
import { PERMISSION_MAP } from "@calcom/platform-constants";
import { ARE_CALENDAR_EVENTS_ENABLED_DOCS } from "../inputs";
export class PlatformOAuthClientDto {
@ApiProperty({ example: "clsx38nbl0001vkhlwin9fmt0" })
@IsString()
@@ -56,6 +58,10 @@ export class PlatformOAuthClientDto {
})
areDefaultEventTypesEnabled!: boolean;
@IsBoolean()
@ApiProperty({ example: true, description: ARE_CALENDAR_EVENTS_ENABLED_DOCS })
areCalendarEventsEnabled!: boolean;
@ApiPropertyOptional({ example: "https://example.com/booking-redirect" })
@IsOptional()
@IsUrl()
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "PlatformOAuthClient" ADD COLUMN "areCalendarEventsEnabled" BOOLEAN NOT NULL DEFAULT true;
+1
View File
@@ -1786,6 +1786,7 @@ model PlatformOAuthClient {
bookingRescheduleRedirectUri String?
areEmailsEnabled Boolean @default(false)
areDefaultEventTypesEnabled Boolean @default(true)
areCalendarEventsEnabled Boolean @default(true)
createdAt DateTime @default(now())
}
+1
View File
@@ -259,6 +259,7 @@ const PlatformClientParamsSchema = z.object({
platformCancelUrl: z.string().nullable().optional(),
platformBookingUrl: z.string().nullable().optional(),
platformBookingLocation: z.string().optional(),
areCalendarEventsEnabled: z.boolean().optional(),
});
export type PlatformClientParams = z.infer<typeof PlatformClientParamsSchema>;
+5 -5
View File
@@ -2736,7 +2736,7 @@ __metadata:
"@axiomhq/winston": ^1.2.0
"@calcom/platform-constants": "*"
"@calcom/platform-enums": "*"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.218"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.222"
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2"
"@calcom/platform-types": "*"
"@calcom/platform-utils": "*"
@@ -3831,13 +3831,13 @@ __metadata:
languageName: node
linkType: hard
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.218":
version: 0.0.218
resolution: "@calcom/platform-libraries@npm:0.0.218"
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.222":
version: 0.0.222
resolution: "@calcom/platform-libraries@npm:0.0.222"
dependencies:
"@calcom/features": "*"
"@calcom/lib": "*"
checksum: 8f73aeffaec3b15707aeaab40d0f056e0156e162d22ba44a19e537d897f654adc32f4bafaa0f7bab846b94211cebeaadc7bb7180ce98204c646c24ed48bdf3ef
checksum: c02cc43e0726cb2587bac6424d69af1cb42f48f5de725ec5096c5c5397c85928409ecf0c7fbc23092ef6ccf6235501f59449c6f89383d2874c794bf2259c0c1b
languageName: node
linkType: hard