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
This commit is contained in:
Lauris Skraucis
2026-02-09 13:46:52 -03:00
committed by GitHub
parent f66fffd13b
commit edf9cd70dd
9 changed files with 90 additions and 32 deletions
@@ -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);
@@ -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<unknown[]>
>;
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"
@@ -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,
@@ -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,
@@ -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};
@@ -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<boolean> {
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,
};
}
}
}
@@ -89,6 +89,7 @@ export type BookingInfo = {
videoCallData?: {
url?: string;
};
platformClientId?: string | null;
};
export type WorkflowContextData =
@@ -49,14 +49,17 @@ export async function sendWorkflowEmails(payload: string): Promise<void> {
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,
+1
View File
@@ -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()