refactor: v2 OAuth webhooks & workflows (#17959)

* refactor: handleMarkNoShow OAuth webhook handling

* refactor: confirm / decline booking OAuth webhook handling

* refactor: pass oauth client to calendar event

* refactor: pass oauth params

* fix import

* fix tests

* chore: bump libraries

* chore: bump libraries

* chore: republish platform libraries

* chore: republish platform libraries

* fix: use replexica key in v2 and unit test CI

* lock file

* fixup! Merge branch 'main' into lauris/cal-4807-platform-refactor-oauth-webhooks

* Revert "fix: use replexica key in v2 and unit test CI"

This reverts commit 0bd6c364535f9e11a4d1497a6bd8909885562080.

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
Lauris Skraucis
2024-12-04 11:25:20 +01:00
committed by GitHub
co-authored by Morgan Morgan Vernay
parent b92db55380
commit e2938ac39a
13 changed files with 5834 additions and 107 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.63",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.67",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -34,6 +34,7 @@ import {
OrganizerCancelledEmail,
AttendeeCancelledEmail,
OrganizerReassignedEmail,
AttendeeUpdatedEmail,
} from "@calcom/platform-libraries";
import {
CreateBookingInput_2024_08_13,
@@ -61,10 +62,12 @@ jest
jest
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(OrganizerReassignedEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
jest
.spyOn(AttendeeUpdatedEmail.prototype, "getHtml")
.mockImplementation(() => Promise.resolve("<p>email</p>"));
type EmailSetup = {
team: Team;
@@ -681,6 +684,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
});
});
@@ -697,6 +701,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerReassignedEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).not.toHaveBeenCalled();
});
});
});
@@ -898,6 +903,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
expect(responseBody.data).toBeDefined();
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
});
});
@@ -914,6 +920,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerReassignedEmail.prototype.getHtml).toHaveBeenCalled();
expect(AttendeeUpdatedEmail.prototype.getHtml).toHaveBeenCalled();
});
});
});
@@ -363,12 +363,17 @@ export class BookingsService_2024_08_13 {
async markAbsent(bookingUid: string, bookingOwnerId: number, body: MarkAbsentBookingInput_2024_08_13) {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
const platformClientParams = bookingBefore?.eventTypeId
? await this.inputService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
await handleMarkNoShow({
bookingUid,
attendees: bodyTransformed.attendees,
noShowHost: bodyTransformed.noShowHost,
userId: bookingOwnerId,
platformClientParams,
});
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
@@ -423,7 +428,11 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
const profile = this.usersService.getUserMainProfile(requestUser);
@@ -431,6 +440,7 @@ export class BookingsService_2024_08_13 {
bookingId: booking.id,
orgId: profile?.organizationId || null,
emailsEnabled,
platformClientParams,
});
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
@@ -441,12 +451,6 @@ export class BookingsService_2024_08_13 {
return this.outputService.getOutputReassignedBooking(reassigned);
}
async getEmailsEnabled(eventTypeId: number) {
const oAuthParams = await this.inputService.getOAuthClientParams(eventTypeId);
const emailsEnabled = oAuthParams ? oAuthParams.arePlatformEmailsEnabled : true;
return emailsEnabled;
}
async reassignBookingToUser(
bookingUid: string,
newUserId: number,
@@ -463,7 +467,11 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`User with id=${newUserId} was not found in the database`);
}
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
const profile = this.usersService.getUserMainProfile(user);
@@ -474,6 +482,7 @@ export class BookingsService_2024_08_13 {
reassignReason: body.reason,
reassignedById,
emailsEnabled,
platformClientParams,
});
return this.outputService.getOutputReassignedBooking(reassigned);
@@ -485,7 +494,11 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
await confirmBookingHandler({
ctx: {
@@ -496,6 +509,7 @@ export class BookingsService_2024_08_13 {
confirmed: true,
recurringEventId: booking.recurringEventId,
emailsEnabled,
platformClientParams,
},
});
@@ -508,7 +522,11 @@ export class BookingsService_2024_08_13 {
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
}
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
await confirmBookingHandler({
ctx: {
@@ -520,6 +538,7 @@ export class BookingsService_2024_08_13 {
recurringEventId: booking.recurringEventId,
reason,
emailsEnabled,
platformClientParams,
},
});
@@ -24,10 +24,10 @@ import { withNextAuth } from "test/utils/withNextAuth";
import { PlatformOAuthClient, Team, Webhook } from "@calcom/prisma/client";
describe("EventTypes WebhooksController (e2e)", () => {
describe("OAuth client WebhooksController (e2e)", () => {
let app: INestApplication;
const userEmail = "event-types-webhook-controller-e2e@api.com";
const otherUserEmail = "other-event-types-webhook-controller-e2e@api.com";
const userEmail = "oauth-client-webhook-controller-e2e@api.com";
const otherUserEmail = "other-oauth-client-webhook-controller-e2e@api.com";
let user: UserWithProfile;
let otherUser: UserWithProfile;
let oAuthClient: PlatformOAuthClient;
@@ -23,6 +23,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
import type { PrismaClient } from "@calcom/prisma";
import type { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { PlatformClientParams } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
@@ -68,8 +69,19 @@ export async function handleConfirmation(args: {
};
paid?: boolean;
emailsEnabled?: boolean;
platformClientParams?: PlatformClientParams;
}) {
const { user, evt, recurringEventId, prisma, bookingId, booking, paid, emailsEnabled = true } = args;
const {
user,
evt,
recurringEventId,
prisma,
bookingId,
booking,
paid,
emailsEnabled = true,
platformClientParams,
} = args;
const eventType = booking.eventType;
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});
const eventManager = new EventManager(user, eventTypeMetadata?.apps);
@@ -355,6 +367,7 @@ export async function handleConfirmation(args: {
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
teamId,
orgId,
oAuthClientId: platformClientParams?.platformClientId,
});
const subscribersMeetingStarted = await getWebhooks({
userId,
@@ -362,6 +375,7 @@ export async function handleConfirmation(args: {
triggerEvent: WebhookTriggerEvents.MEETING_STARTED,
teamId: eventType?.teamId,
orgId,
oAuthClientId: platformClientParams?.platformClientId,
});
const subscribersMeetingEnded = await getWebhooks({
userId,
@@ -369,6 +383,7 @@ export async function handleConfirmation(args: {
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
teamId: eventType?.teamId,
orgId,
oAuthClientId: platformClientParams?.platformClientId,
});
const scheduleTriggerPromises: Promise<unknown>[] = [];
@@ -410,6 +425,7 @@ export async function handleConfirmation(args: {
eventTypeId: booking.eventTypeId,
teamId,
orgId,
oAuthClientId: platformClientParams?.platformClientId,
});
const eventTypeInfo: EventTypeInfo = {
@@ -429,6 +445,7 @@ export async function handleConfirmation(args: {
status: "ACCEPTED",
smsReminderNumber: booking.smsReminderNumber || undefined,
metadata: meetingUrl ? { videoCallUrl: meetingUrl } : undefined,
...(platformClientParams ? platformClientParams : {}),
};
const promises = subscribersBookingCreated.map((sub) =>
@@ -440,7 +457,7 @@ export async function handleConfirmation(args: {
payload
).catch((e) => {
log.error(
`Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_CREATED}, URL: ${sub.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,
`Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_CREATED}, URL: ${sub.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}, platformClientId: ${platformClientParams?.platformClientId}`,
safeStringify(e)
);
})
@@ -456,6 +473,7 @@ export async function handleConfirmation(args: {
triggerEvent: WebhookTriggerEvents.BOOKING_PAID,
teamId: eventType?.teamId,
orgId,
oAuthClientId: platformClientParams?.platformClientId,
});
const bookingWithPayment = await prisma.booking.findFirst({
where: {
@@ -65,6 +65,7 @@ import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { PlatformClientParams } from "@calcom/prisma/zod-utils";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
import type { AdditionalInformation, AppsStatus, CalendarEvent, Person } from "@calcom/types/Calendar";
@@ -339,14 +340,10 @@ function buildTroubleshooterData({
}
async function handler(
req: NextApiRequest & {
userId?: number | undefined;
platformClientId?: string;
platformRescheduleUrl?: string;
platformCancelUrl?: string;
platformBookingUrl?: string;
platformBookingLocation?: string;
},
req: NextApiRequest &
PlatformClientParams & {
userId?: number | undefined;
},
bookingDataSchemaGetter: BookingDataSchemaGetter = getBookingDataSchema
) {
const {
@@ -13,10 +13,11 @@ type ScheduleNoShowTriggersArgs = {
eventTypeId: number | null;
teamId?: number | null;
orgId?: number | null;
oAuthClientId?: string | null;
};
export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) => {
const { booking, triggerForUser, organizerUser, eventTypeId, teamId, orgId } = args;
const { booking, triggerForUser, organizerUser, eventTypeId, teamId, orgId, oAuthClientId } = args;
// Add task for automatic no show in cal video
const noShowPromises: Promise<any>[] = [];
@@ -27,6 +28,7 @@ export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) =
triggerEvent: WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
teamId,
orgId,
oAuthClientId,
});
noShowPromises.push(
@@ -56,6 +58,7 @@ export const scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) =
triggerEvent: WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
teamId,
orgId,
oAuthClientId,
});
noShowPromises.push(
@@ -28,7 +28,7 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import { prisma } from "@calcom/prisma";
import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import type { EventTypeMetadata, PlatformClientParams } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { handleRescheduleEventManager } from "./handleRescheduleEventManager";
@@ -49,6 +49,7 @@ export const roundRobinManualReassignment = async ({
reassignReason,
reassignedById,
emailsEnabled = true,
platformClientParams,
}: {
bookingId: number;
newUserId: number;
@@ -56,6 +57,7 @@ export const roundRobinManualReassignment = async ({
reassignReason?: string;
reassignedById: number;
emailsEnabled?: boolean;
platformClientParams?: PlatformClientParams;
}) => {
const roundRobinReassignLogger = logger.getSubLogger({
prefix: ["roundRobinManualReassign", `${bookingId}`],
@@ -264,6 +266,7 @@ export const roundRobinManualReassignment = async ({
booking,
}),
location: bookingLocation,
...(platformClientParams ? platformClientParams : {}),
};
const credentials = await prisma.credential.findMany({
@@ -30,7 +30,7 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import { prisma } from "@calcom/prisma";
import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import type { EventTypeMetadata, PlatformClientParams } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { handleRescheduleEventManager } from "./handleRescheduleEventManager";
@@ -42,10 +42,12 @@ export const roundRobinReassignment = async ({
bookingId,
orgId,
emailsEnabled = true,
platformClientParams,
}: {
bookingId: number;
orgId: number | null;
emailsEnabled?: boolean;
platformClientParams?: PlatformClientParams;
}) => {
const roundRobinReassignLogger = logger.getSubLogger({
prefix: ["roundRobinReassign", `${bookingId}`],
@@ -294,6 +296,7 @@ export const roundRobinReassignment = async ({
booking,
}),
location: bookingLocation,
...(platformClientParams ? platformClientParams : {}),
};
const credentials = await prisma.credential.findMany({
+14 -3
View File
@@ -8,6 +8,7 @@ import { getTranslation } from "@calcom/lib/server/i18n";
import { BookingRepository } from "@calcom/lib/server/repository/booking";
import { prisma } from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/client";
import type { PlatformClientParams } from "@calcom/prisma/zod-utils";
import type { TNoShowInputSchema } from "@calcom/trpc/server/routers/loggedInViewer/markNoShow.schema";
import handleSendingAttendeeNoShowDataToApps from "./noShow/handleSendingAttendeeNoShowDataToApps";
@@ -82,7 +83,12 @@ const handleMarkNoShow = async ({
noShowHost,
userId,
locale,
}: TNoShowInputSchema & { userId?: number; locale?: string }) => {
platformClientParams,
}: TNoShowInputSchema & {
userId?: number;
locale?: string;
platformClientParams?: PlatformClientParams;
}) => {
const responsePayload = new ResponsePayload();
const t = await getTranslation(locale ?? "en", "common");
@@ -94,13 +100,17 @@ const handleMarkNoShow = async ({
const payload = await buildResultPayload(bookingUid, attendeeEmails, attendees, t);
const { webhooks, bookingId } = await getWebhooksService(bookingUid);
const { webhooks, bookingId } = await getWebhooksService(
bookingUid,
platformClientParams?.platformClientId
);
await webhooks.sendPayload({
...payload,
/** We send webhook message pre-translated, on client we already handle this */
bookingUid,
bookingId,
...(platformClientParams ? platformClientParams : {}),
});
responsePayload.setAttendees(payload.attendees);
@@ -179,7 +189,7 @@ const updateAttendees = async (
.map((x) => ({ email: x.email, noShow: x.noShow }));
};
const getWebhooksService = async (bookingUid: string) => {
const getWebhooksService = async (bookingUid: string, platformClientId?: string) => {
const booking = await prisma.booking.findUnique({
where: { uid: bookingUid },
select: {
@@ -204,6 +214,7 @@ const getWebhooksService = async (bookingUid: string) => {
eventTypeId: booking?.eventType?.id,
orgId,
triggerEvent: WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED,
oAuthClientId: platformClientId,
});
return { webhooks, bookingId: booking?.id };
+11
View File
@@ -276,12 +276,23 @@ export const requiredCustomInputSchema = z.union([
export type BookingCreateBody = z.input<typeof bookingCreateBodySchema>;
const PlatformClientParamsSchema = z.object({
platformClientId: z.string().optional(),
platformRescheduleUrl: z.string().nullable().optional(),
platformCancelUrl: z.string().nullable().optional(),
platformBookingUrl: z.string().nullable().optional(),
platformBookingLocation: z.string().optional(),
});
export type PlatformClientParams = z.infer<typeof PlatformClientParamsSchema>;
export const bookingConfirmPatchBodySchema = z.object({
bookingId: z.number(),
confirmed: z.boolean(),
recurringEventId: z.string().optional(),
reason: z.string().optional(),
emailsEnabled: z.boolean().default(true),
platformClientParams: PlatformClientParamsSchema.optional(),
});
// `responses` is merged with it during handleNewBooking call because `responses` schema is dynamic and depends on eventType
@@ -8,6 +8,7 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
@@ -41,7 +42,14 @@ type ConfirmOptions = {
export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
const { user } = ctx;
const { bookingId, recurringEventId, reason: rejectionReason, confirmed, emailsEnabled } = input;
const {
bookingId,
recurringEventId,
reason: rejectionReason,
confirmed,
emailsEnabled,
platformClientParams,
} = input;
const tOrganizer = await getTranslation(user.locale ?? "en", "common");
@@ -214,6 +222,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
members: [],
}
: undefined,
...(platformClientParams ? platformClientParams : {}),
};
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
@@ -270,6 +279,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
bookingId,
booking,
emailsEnabled,
platformClientParams,
});
} else {
evt.rejectionReason = rejectionReason;
@@ -383,12 +393,13 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
const orgId = await getOrgIdFromMemberOrTeamId({ memberId: booking.userId, teamId });
// send BOOKING_REJECTED webhooks
const subscriberOptions = {
const subscriberOptions: GetSubscriberOptions = {
userId: booking.userId,
eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_REJECTED,
teamId,
orgId,
oAuthClientId: platformClientParams?.platformClientId,
};
const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REJECTED;
const eventTypeInfo: EventTypeInfo = {
+5718 -74
View File
File diff suppressed because it is too large Load Diff