From edf9cd70dd0270481beb05bb2aa315abe5941960 Mon Sep 17 00:00:00 2001 From: Lauris Skraucis Date: Mon, 9 Feb 2026 17:46:52 +0100 Subject: [PATCH] fix: hide cal branding on platform workflows (#27385) * fix: hide cal branding on platform workflows * refactor: rely on existing with platform variables code * revert: comment * fix e2e * chore: remove unit results --- .../controllers/bookings.controller.ts | 4 +++ .../controllers/e2e/user-bookings.e2e-spec.ts | 30 +++++++++------- .../2024-08-13/services/input.service.ts | 5 ++- .../lib/service/RegularBookingService.ts | 6 ++-- .../workflows/api/scheduleEmailReminders.ts | 36 +++++++++++++------ .../lib/service/EmailWorkflowService.ts | 32 ++++++++++++++--- packages/features/ee/workflows/lib/types.ts | 1 + .../tasker/tasks/sendWorkflowEmails.ts | 7 ++-- packages/prisma/zod-utils.ts | 1 + 9 files changed, 90 insertions(+), 32 deletions(-) diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 694ae06737..738092fa0f 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -611,6 +611,10 @@ export class BookingsController_2024_04_15 { ...clone.body, noEmail: oAuthParams === undefined ? false : !oAuthParams.arePlatformEmailsEnabled, creationSource: CreationSource.API_V2, + metadata: { + ...(clone.body.metadata || {}), + ...(oAuthClientId && { platformClientId: oAuthClientId }), + }, }; if (oAuthClientId) { await this.setPlatformAttendeesEmails(clone.body, oAuthClientId); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts index 2792214c8d..27252cf7e2 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts @@ -515,7 +515,7 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(updatedAtDate?.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime()); expect(updatedAtDate?.getTime()).toBeLessThanOrEqual(afterCreate.getTime()); - expect(data.metadata).toEqual(body.metadata); + expect(data.metadata).toEqual({...body.metadata, platformClientId: oAuthClient.id}); createdBooking = data; } else { throw new Error( @@ -2115,7 +2115,7 @@ describe("Bookings Endpoints 2024-08-13", () => { expect(updatedAtDate?.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime()); expect(updatedAtDate?.getTime()).toBeLessThanOrEqual(afterCreate.getTime()); - expect(data.metadata).toEqual(body.metadata); + expect(data.metadata).toEqual({...body.metadata, platformClientId: oAuthClient.id}); createdBooking = data; } else { throw new Error( @@ -2662,16 +2662,22 @@ describe("Bookings Endpoints 2024-08-13", () => { }); describe("calendar events", () => { + // Type assertion to access private methods for test spying + const eventManagerProto = EventManager.prototype as unknown as Record< + "createAllCalendarEvents" | "createAllCRMEvents", + (...args: unknown[]) => Promise + >; + beforeEach(() => { jest.restoreAllMocks(); jest .spyOn(EventManager.prototype, "create") .mockImplementation(() => Promise.resolve({ results: [], referencesToCreate: [] })); jest - .spyOn(EventManager.prototype, "createAllCalendarEvents") + .spyOn(eventManagerProto, "createAllCalendarEvents") .mockImplementation(() => Promise.resolve([])); jest - .spyOn(EventManager.prototype, "createAllCRMEvents") + .spyOn(eventManagerProto, "createAllCRMEvents") .mockImplementation(() => Promise.resolve([])); }); @@ -2843,8 +2849,8 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0); - expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0); + expect(eventManagerProto.createAllCalendarEvents).toHaveBeenCalledTimes(0); + expect(eventManagerProto.createAllCRMEvents).toHaveBeenCalledTimes(0); } else { throw new Error( "Invalid response data - expected booking but received array of possibly recurring bookings" @@ -2874,8 +2880,8 @@ describe("Bookings Endpoints 2024-08-13", () => { const responseBody: CreateBookingOutput_2024_08_13 = response.body; expect(responseBody.status).toEqual(SUCCESS_STATUS); expect(responseBody.data).toBeDefined(); - expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0); - expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0); + expect(eventManagerProto.createAllCalendarEvents).toHaveBeenCalledTimes(0); + expect(eventManagerProto.createAllCRMEvents).toHaveBeenCalledTimes(0); }); }); @@ -2909,8 +2915,8 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0); - expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0); + expect(eventManagerProto.createAllCalendarEvents).toHaveBeenCalledTimes(0); + expect(eventManagerProto.createAllCRMEvents).toHaveBeenCalledTimes(0); bookingThatRequiresConfirmationUid = data.uid; } else { throw new Error( @@ -2935,8 +2941,8 @@ describe("Bookings Endpoints 2024-08-13", () => { const data: BookingOutput_2024_08_13 = responseBody.data; expect(data.id).toBeDefined(); expect(data.uid).toBeDefined(); - expect(EventManager.prototype.createAllCalendarEvents).toHaveBeenCalledTimes(0); - expect(EventManager.prototype.createAllCRMEvents).toHaveBeenCalledTimes(0); + expect(eventManagerProto.createAllCalendarEvents).toHaveBeenCalledTimes(0); + expect(eventManagerProto.createAllCRMEvents).toHaveBeenCalledTimes(0); } else { throw new Error( "Invalid response data - expected booking but received array of possibly recurring bookings" diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 9deb349e13..3cea8fafcc 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -190,7 +190,10 @@ export class InputBookingsService_2024_08_13 { eventTypeId: inputBooking.eventTypeId, timeZone: inputBooking.attendee.timeZone, language: inputBooking.attendee.language || "en", - metadata: inputBooking.metadata || {}, + metadata: { + ...(inputBooking.metadata || {}), + ...(platformClientId && { platformClientId }), + }, hasHashedBookingLink: false, guests, verificationCode: inputBooking.emailVerificationCode, diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 156f19ad48..f71f6a2b88 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -2615,7 +2615,7 @@ async function handler( workflows, smsReminderNumber: smsReminderNumber || null, calendarEvent: calendarEventForWorkflow, - hideBranding: !!eventType.owner?.hideBranding, + hideBranding: !!eventType.owner?.hideBranding || !!platformClientId, seatReferenceUid: evt.attendeeSeatId, isDryRun, triggers: [WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED], @@ -2790,7 +2790,7 @@ async function handler( evt: evtWithMetadata, workflows, requiresConfirmation: !isConfirmedByDefault, - hideBranding: !!eventType.owner?.hideBranding, + hideBranding: !!eventType.owner?.hideBranding || !!platformClientId, seatReferenceUid: evt.attendeeSeatId, isPlatformNoEmail: noEmail && Boolean(platformClientId), isDryRun, @@ -2805,7 +2805,7 @@ async function handler( workflows, smsReminderNumber: smsReminderNumber || null, calendarEvent: evtWithMetadata, - hideBranding: !!eventType.owner?.hideBranding, + hideBranding: !!eventType.owner?.hideBranding || !!platformClientId, seatReferenceUid: evt.attendeeSeatId, isDryRun, isConfirmedByDefault, diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index f7c6ffb5ef..b680b62a48 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -36,7 +36,7 @@ import customTemplate from "../lib/reminders/templates/customTemplate"; import emailRatingTemplate from "../lib/reminders/templates/emailRatingTemplate"; import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate"; -export async function handler(req: NextRequest) { +async function handler(req: NextRequest) { const apiKey = req.headers.get("authorization") || req.nextUrl.searchParams.get("apiKey"); if (process.env.CRON_API_KEY !== apiKey) { @@ -243,9 +243,7 @@ export async function handler(req: NextRequest) { eventEndTimeInAttendeeTimezone: dayjs(reminder.booking?.endTime).tz(targetAttendee?.timeZone), }; const emailLocale = locale || "en"; - const brandingDisabled = reminder.booking.eventType?.team - ? !!reminder.booking.eventType?.team?.hideBranding - : !!reminder.booking.user?.hideBranding; + const brandingDisabled = shouldHideBranding(reminder.booking); const emailSubject = customTemplate( reminder.workflowStep.emailSubject || "", @@ -271,9 +269,7 @@ export async function handler(req: NextRequest) { getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat) ).text.length === 0; } else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) { - const brandingDisabled = reminder.booking.eventType?.team - ? !!reminder.booking.eventType?.team?.hideBranding - : !!reminder.booking.user?.hideBranding; + const brandingDisabled = shouldHideBranding(reminder.booking); emailContent = emailReminderTemplate({ isEditingMode: false, locale: reminder.booking.user?.locale || "en", @@ -301,6 +297,7 @@ export async function handler(req: NextRequest) { const bookerUrl = await getBookerBaseUrl( reminder.booking.eventType?.team?.parentId ?? organizerOrganizationId ?? null ); + const brandingDisabled = shouldHideBranding(reminder.booking); emailContent = emailRatingTemplate({ isEditingMode: true, locale: reminder.booking.user?.locale || "en", @@ -313,6 +310,7 @@ export async function handler(req: NextRequest) { timeZone: timeZone || "", organizer: reminder.booking.user?.name || "", name: name || "", + isBrandingDisabled: brandingDisabled, ratingUrl: `${bookerUrl}/booking/${reminder.booking.uid}?rating`, noShowUrl: `${bookerUrl}/booking/${reminder.booking.uid}?noShow=true`, }); @@ -430,9 +428,7 @@ export async function handler(req: NextRequest) { const emailBodyEmpty = false; - const brandingDisabled = reminder.booking.eventType?.team - ? !!reminder.booking.eventType?.team?.hideBranding - : !!reminder.booking.user?.hideBranding; + const brandingDisabled = shouldHideBranding(reminder.booking); emailContent = emailReminderTemplate({ isEditingMode: false, @@ -516,3 +512,23 @@ export async function handler(req: NextRequest) { return NextResponse.json({ message: `${unscheduledReminders.length} Emails to schedule` }, { status: 200 }); } + +function shouldHideBranding(booking: { + metadata: unknown; + eventType?: { team?: { hideBranding?: boolean } | null } | null; + user?: { hideBranding?: boolean } | null; +}): boolean { + const bookingMetadata = bookingMetadataSchema.parse(booking.metadata || {}); + + if (bookingMetadata?.platformClientId) { + return true; + } + + if (booking.eventType?.team) { + return !!booking.eventType.team.hideBranding; + } + + return !!booking.user?.hideBranding; +} + +export {handler}; \ No newline at end of file diff --git a/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts b/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts index 4070f77325..93b92e3d20 100644 --- a/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts +++ b/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts @@ -87,9 +87,10 @@ export class EmailWorkflowService { creditCheckFn, }); - const hideBranding = await getHideBranding({ - userId: workflow.userId ?? undefined, - teamId: workflow.teamId ?? undefined, + const hideBranding = await this.shouldHideBranding({ + platformClientId: evt.platformClientId, + userId: workflow.userId, + teamId: workflow.teamId, }); const emailWorkflowContentParams = await this.generateParametersToBuildEmailWorkflowContent({ @@ -237,6 +238,27 @@ export class EmailWorkflowService { } as const; } + private async shouldHideBranding({ + platformClientId, + userId, + teamId, + }: { + platformClientId?: string | null; + userId?: number | null; + teamId?: number | null; + }): Promise { + if (platformClientId) { + return true; + } + + const hideBranding = await getHideBranding({ + userId: userId ?? undefined, + teamId: teamId ?? undefined, + }); + + return hideBranding; + } + async generateEmailPayloadForEvtWorkflow({ evt, sendTo, @@ -412,6 +434,7 @@ export class EmailWorkflowService { meetingUrl, otherPerson: attendeeName, name, + isBrandingDisabled: hideBranding, }); } else if (matchedTemplate === WorkflowTemplates.RATING) { emailContent = emailRatingTemplate({ @@ -426,6 +449,7 @@ export class EmailWorkflowService { timeZone, organizer: evt.organizer.name, name, + isBrandingDisabled: hideBranding, ratingUrl: `${bookerUrl}/booking/${evt.uid}?rating`, noShowUrl: `${bookerUrl}/booking/${evt.uid}?noShow=true`, }); @@ -554,4 +578,4 @@ export class EmailWorkflowService { sender, }; } -} +} \ No newline at end of file diff --git a/packages/features/ee/workflows/lib/types.ts b/packages/features/ee/workflows/lib/types.ts index 6abff70c4f..3f11b3e3b8 100644 --- a/packages/features/ee/workflows/lib/types.ts +++ b/packages/features/ee/workflows/lib/types.ts @@ -89,6 +89,7 @@ export type BookingInfo = { videoCallData?: { url?: string; }; + platformClientId?: string | null; }; export type WorkflowContextData = diff --git a/packages/features/tasker/tasks/sendWorkflowEmails.ts b/packages/features/tasker/tasks/sendWorkflowEmails.ts index 2520bdbf0e..fec3d7158d 100644 --- a/packages/features/tasker/tasks/sendWorkflowEmails.ts +++ b/packages/features/tasker/tasks/sendWorkflowEmails.ts @@ -49,14 +49,17 @@ export async function sendWorkflowEmails(payload: string): Promise { throw new Error("Booking not found"); } - const calendarEvent = (await CalendarEventBuilder.fromBooking(booking, {})).build(); + const bookingMetadata = bookingMetadataSchema.parse(booking.metadata || {}); + + const calendarEvent = (await CalendarEventBuilder.fromBooking(booking, { + platformClientId: bookingMetadata?.platformClientId, + })).build(); if (!calendarEvent) { throw new Error("Calendar event could not be built"); } // Check if videoCallUrl exists in booking metadata and add it to evt.metadata - const bookingMetadata = bookingMetadataSchema.parse(booking.metadata || {}); const metadata = bookingMetadata?.videoCallUrl ? { videoCallUrl: bookingMetadata.videoCallUrl, diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 53ac8fe40c..e817ad7f68 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -523,6 +523,7 @@ export const teamMetadataStrictSchema = baseTeamMetadataSchema export const bookingMetadataSchema = z .object({ videoCallUrl: z.string().optional(), + platformClientId: z.string().optional(), }) .and(z.record(z.string())) .nullable()