diff --git a/apps/web/modules/bookings/components/BookerWebWrapper.tsx b/apps/web/modules/bookings/components/BookerWebWrapper.tsx index a252fcb0b4..2ba34777a7 100644 --- a/apps/web/modules/bookings/components/BookerWebWrapper.tsx +++ b/apps/web/modules/bookings/components/BookerWebWrapper.tsx @@ -126,6 +126,7 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps): JSX.Elemen name: bookerForm.formName, requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification, onVerifyEmail: bookerForm.beforeVerifyEmail, + eventTypeId: event?.data?.id, }); const slots = useSlots(event?.data ? { id: event.data.id, length: event.data.length } : null); diff --git a/apps/web/modules/bookings/hooks/useVerifyEmail.ts b/apps/web/modules/bookings/hooks/useVerifyEmail.ts index eada0222d3..8e048ac6a9 100644 --- a/apps/web/modules/bookings/hooks/useVerifyEmail.ts +++ b/apps/web/modules/bookings/hooks/useVerifyEmail.ts @@ -12,6 +12,7 @@ export interface IUseVerifyEmailProps { onVerifyEmail?: () => void; name?: string | { firstName: string; lastname?: string }; requiresBookerEmailVerification?: boolean; + eventTypeId?: number; } export type UseVerifyEmailReturnType = ReturnType; export const useVerifyEmail = ({ @@ -19,6 +20,7 @@ export const useVerifyEmail = ({ name, requiresBookerEmailVerification, onVerifyEmail, + eventTypeId, }: IUseVerifyEmailProps) => { const [isEmailVerificationModalVisible, setEmailVerificationModalVisible] = useState(false); const verifiedEmail = useBookerStore((state) => state.verifiedEmail); @@ -56,6 +58,7 @@ export const useVerifyEmail = ({ email, username: typeof name === "string" ? name : name?.firstName, language: i18n.language || "en", + eventTypeId, }); }; diff --git a/packages/app-store/_utils/payments/handlePaymentSuccess.ts b/packages/app-store/_utils/payments/handlePaymentSuccess.ts index 1bece58cae..c3fc6e33c0 100644 --- a/packages/app-store/_utils/payments/handlePaymentSuccess.ts +++ b/packages/app-store/_utils/payments/handlePaymentSuccess.ts @@ -228,7 +228,7 @@ export async function handlePaymentSuccess(params: { workflows, smsReminderNumber: booking.smsReminderNumber, calendarEvent: calendarEventForWorkflow, - hideBranding: !!booking.eventType?.owner?.hideBranding, + hideBranding: evt.hideBranding ?? false, triggers: [WorkflowTriggerEvents.BOOKING_PAID], creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); diff --git a/packages/emails/lib/types/email-types.ts b/packages/emails/lib/types/email-types.ts index f8e48dffaa..fec3d61ec9 100644 --- a/packages/emails/lib/types/email-types.ts +++ b/packages/emails/lib/types/email-types.ts @@ -20,4 +20,5 @@ export type EmailVerifyCode = { }; verificationEmailCode: string; isVerifyingEmail?: boolean; + hideLogo?: boolean; }; diff --git a/packages/emails/src/templates/BaseScheduledEmail.tsx b/packages/emails/src/templates/BaseScheduledEmail.tsx index 691f6ef8c0..40facf6239 100644 --- a/packages/emails/src/templates/BaseScheduledEmail.tsx +++ b/packages/emails/src/templates/BaseScheduledEmail.tsx @@ -1,19 +1,17 @@ -import type { TFunction } from "i18next"; - import dayjs from "@calcom/dayjs"; import { formatPrice } from "@calcom/lib/currencyConversions"; import { TimeFormat } from "@calcom/lib/timeFormat"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; - +import type { TFunction } from "i18next"; import { + AppsStatus, BaseEmailHtml, Info, LocationInfo, ManageLink, + UserFieldsResponses, WhenInfo, WhoInfo, - AppsStatus, - UserFieldsResponses, } from "../components"; import { PersonInfo } from "../components/WhoInfo"; @@ -65,7 +63,7 @@ export const BaseScheduledEmail = ( return ( { return ( + hideLogo={props.hideLogo} + subject={ + props.hideLogo + ? props.language("verify_email_subject_no_branding") + : props.language( + `verify_email_subject${props.isVerifyingEmail ? "_verifying_email" : ""}`, + { appName: APP_NAME } + ) + } + >

+ }} + > <>{props.language("verify_email_email_header")}

@@ -31,20 +42,27 @@ export const VerifyEmailByCode = (

-
-

- <> - {props.language("happy_scheduling")},
- - <>{props.language("the_calcom_team", { companyName: SENDER_NAME })} - - -

-
+ {!props.hideLogo && ( +
+

+ <> + {props.language("happy_scheduling")},
+ + <> + {props.language("the_calcom_team", { + companyName: SENDER_NAME, + })} + + + +

+
+ )}
); }; diff --git a/packages/emails/templates/attendee-verify-email.ts b/packages/emails/templates/attendee-verify-email.ts index be15cff051..c586680c1d 100644 --- a/packages/emails/templates/attendee-verify-email.ts +++ b/packages/emails/templates/attendee-verify-email.ts @@ -19,31 +19,39 @@ export default class AttendeeVerifyEmail extends BaseEmail { return { to: `${this.verifyAccountInput.user.name} <${this.verifyAccountInput.user.email}>`, from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`, - subject: this.verifyAccountInput.language( - `verify_email_subject${this.verifyAccountInput.isVerifyingEmail ? "_verifying_email" : ""}`, - { - appName: APP_NAME, - } - ), + subject: this.verifyAccountInput.hideLogo + ? this.verifyAccountInput.language("verify_email_subject_no_branding") + : this.verifyAccountInput.language( + `verify_email_subject${this.verifyAccountInput.isVerifyingEmail ? "_verifying_email" : ""}`, + { appName: APP_NAME } + ), html: await renderEmail("VerifyEmailByCode", this.verifyAccountInput), text: this.getTextBody(), }; } protected getTextBody(): string { + const subject = this.verifyAccountInput.hideLogo + ? this.verifyAccountInput.language("verify_email_subject_no_branding") + : this.verifyAccountInput.language( + `verify_email_subject${this.verifyAccountInput.isVerifyingEmail ? "_verifying_email" : ""}`, + { appName: APP_NAME } + ); + + const footer = this.verifyAccountInput.hideLogo + ? "" + : `${this.verifyAccountInput.language("happy_scheduling")} ${this.verifyAccountInput.language( + "the_calcom_team", + { companyName: COMPANY_NAME } + )}`; + return ` -${this.verifyAccountInput.language( - `verify_email_subject${this.verifyAccountInput.isVerifyingEmail ? "_verifying_email" : ""}`, - { appName: APP_NAME } -)} +${subject} ${this.verifyAccountInput.language("verify_email_email_header")} ${this.verifyAccountInput.language("hi_user_name", { name: this.verifyAccountInput.user.name })}, ${this.verifyAccountInput.language("verify_email_by_code_email_body")} ${this.verifyAccountInput.verificationEmailCode} -${this.verifyAccountInput.language("happy_scheduling")} ${this.verifyAccountInput.language( - "the_calcom_team", - { companyName: COMPANY_NAME } - )} +${footer} `.replace(/(<([^>]+)>)/gi, ""); } } diff --git a/packages/features/CalendarEventBuilder.ts b/packages/features/CalendarEventBuilder.ts index 4dce693bda..a419fb4ba8 100644 --- a/packages/features/CalendarEventBuilder.ts +++ b/packages/features/CalendarEventBuilder.ts @@ -1,18 +1,21 @@ -import type { TFunction } from "i18next"; - import { ALL_APPS } from "@calcom/app-store/utils"; import { getAssignmentReasonCategory } from "@calcom/features/bookings/lib/getAssignmentReasonCategory"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import type { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { getTranslation } from "@calcom/i18n/server"; import { getTimeFormatStringFromUserTimeFormat, type TimeFormat } from "@calcom/lib/timeFormat"; import type { Attendee, BookingSeat, DestinationCalendar, Prisma, User } from "@calcom/prisma/client"; -import { SchedulingType } from "@calcom/prisma/enums"; +import type { SchedulingType } from "@calcom/prisma/enums"; import { bookingResponses as bookingResponsesSchema } from "@calcom/prisma/zod-utils"; -import type { CalendarEvent, Person, CalEventResponses, AppsStatus } from "@calcom/types/Calendar"; +import type { AppsStatus, CalEventResponses, CalendarEvent, Person } from "@calcom/types/Calendar"; import type { VideoCallData } from "@calcom/types/VideoApiAdapter"; +import type { TFunction } from "i18next"; const APP_TYPE_TO_NAME_MAP = new Map(ALL_APPS.map((app) => [app.type, app.name])); @@ -198,6 +201,18 @@ export class CalendarEventBuilder { details: assignmentReason[0].reasonString ?? null, } : null + ) + .withHideBranding( + await getEventTypeService().shouldHideBrandingForEventType(eventType.id, { + team: eventType.team + ? { hideBranding: eventType.team.hideBranding, parent: eventType.team.parent } + : null, + owner: { + id: user.id, + hideBranding: user.hideBranding, + profiles: user.profiles ?? [], + }, + } satisfies EventTypeBrandingData) ); // Seats @@ -549,6 +564,14 @@ export class CalendarEventBuilder { return this; } + withHideBranding(hideBranding?: boolean) { + this.event = { + ...this.event, + hideBranding, + }; + return this; + } + build(): CalendarEvent | null { // Validate required fields if ( diff --git a/packages/features/auth/lib/verifyEmail.ts b/packages/features/auth/lib/verifyEmail.ts index f35b9d389a..9e186e9ce0 100644 --- a/packages/features/auth/lib/verifyEmail.ts +++ b/packages/features/auth/lib/verifyEmail.ts @@ -25,6 +25,7 @@ interface VerifyEmailType { secondaryEmailId?: number; isVerifyingEmail?: boolean; isPlatform?: boolean; + hideBranding?: boolean; } export const sendEmailVerification = async ({ @@ -90,6 +91,7 @@ export const sendEmailVerificationByCode = async ({ language, username, isVerifyingEmail, + hideBranding, }: VerifyEmailType) => { if (await checkIfEmailIsBlockedInWatchlistController({ email, organizationId: null, span: sentrySpan })) { log.warn("Email is blocked - not sending verification email", email); @@ -112,6 +114,7 @@ export const sendEmailVerificationByCode = async ({ name: username, }, isVerifyingEmail, + hideLogo: hideBranding, }); return { ok: true, skipped: false }; diff --git a/packages/features/bookings/lib/getBookingToDelete.ts b/packages/features/bookings/lib/getBookingToDelete.ts index c1f4189097..70476bdc96 100644 --- a/packages/features/bookings/lib/getBookingToDelete.ts +++ b/packages/features/bookings/lib/getBookingToDelete.ts @@ -24,9 +24,11 @@ export async function getBookingToDelete(id: number | undefined, uid: string | u destinationCalendar: true, locale: true, isPlatformManaged: true, + hideBranding: true, profiles: { select: { organizationId: true, + organization: { select: { hideBranding: true } }, }, }, }, @@ -63,6 +65,8 @@ export async function getBookingToDelete(id: number | undefined, uid: string | u id: true, name: true, parentId: true, + hideBranding: true, + parent: { select: { hideBranding: true } }, }, }, parentId: true, diff --git a/packages/features/bookings/lib/handleBookingRequested.ts b/packages/features/bookings/lib/handleBookingRequested.ts index f7fb0a60b1..f3f8d4a1c3 100644 --- a/packages/features/bookings/lib/handleBookingRequested.ts +++ b/packages/features/bookings/lib/handleBookingRequested.ts @@ -100,7 +100,7 @@ export async function handleBookingRequested(args: { await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({ workflows, smsReminderNumber: booking.smsReminderNumber, - hideBranding: !!booking.eventType?.owner?.hideBranding, + hideBranding: evt.hideBranding, calendarEvent: { ...evt, bookerUrl: evt.bookerUrl as string, diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 05bb8f3c3b..9273cabf41 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -23,6 +23,10 @@ import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBooke import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType"; import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; import { PrismaOrgMembershipRepository } from "@calcom/features/membership/repositories/PrismaOrgMembershipRepository"; import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; @@ -398,6 +402,23 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { customReplyToEmail: bookingToDelete.eventType?.customReplyToEmail, organizationId: ownerProfile?.organizationId ?? null, schedulingType: bookingToDelete.eventType?.schedulingType, + hideBranding: bookingToDelete.eventTypeId + ? await getEventTypeService().shouldHideBrandingForEventType(bookingToDelete.eventTypeId, { + team: bookingToDelete.eventType?.team + ? { + hideBranding: bookingToDelete.eventType.team.hideBranding, + parent: bookingToDelete.eventType.team.parent, + } + : null, + owner: bookingToDelete.user + ? { + id: bookingToDelete.user.id, + hideBranding: bookingToDelete.user.hideBranding, + profiles: bookingToDelete.user.profiles ?? [], + } + : null, + } satisfies EventTypeBrandingData) + : false, }; const dataForWebhooks = { evt, webhooks, eventTypeInfo }; diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 9269ae1f1b..8712660321 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -455,7 +455,7 @@ export async function handleConfirmation(args: { evt: evtOfBooking, workflows, requiresConfirmation: false, - hideBranding: !!updatedBookings[index].eventType?.owner?.hideBranding, + hideBranding: evtOfBooking.hideBranding ?? false, seatReferenceUid: evt.attendeeSeatId, isPlatformNoEmail: !emailsEnabled && Boolean(platformClientParams?.platformClientId), traceContext: spanContext, @@ -468,7 +468,7 @@ export async function handleConfirmation(args: { workflows, smsReminderNumber: updatedBookings[index].smsReminderNumber, calendarEvent: evtOfBooking, - hideBranding: !!updatedBookings[index].eventType?.owner?.hideBranding, + hideBranding: evtOfBooking.hideBranding, isConfirmedByDefault: true, isNormalBookingOrFirstRecurringSlot: isFirstBooking, isRescheduleEvent: false, diff --git a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts index 299859af13..f6130e8627 100644 --- a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts +++ b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts @@ -48,6 +48,8 @@ const getEventTypesFromDBSelect = { includeManagedEventsInLimits: true, rrResetInterval: true, rrTimestampBasis: true, + hideBranding: true, + parent: { select: { hideBranding: true } }, }, }, bookingFields: true, @@ -108,7 +110,13 @@ const getEventTypesFromDBSelect = { useEventTypeDestinationCalendarEmail: true, owner: { select: { + id: true, hideBranding: true, + profiles: { + select: { + organization: { select: { hideBranding: true } }, + }, + }, }, }, workflows: { diff --git a/packages/features/bookings/lib/payment/getBooking.ts b/packages/features/bookings/lib/payment/getBooking.ts index 53cacd33cf..e8de09a826 100644 --- a/packages/features/bookings/lib/payment/getBooking.ts +++ b/packages/features/bookings/lib/payment/getBooking.ts @@ -2,6 +2,10 @@ import { enrichUserWithDelegationCredentials } from "@calcom/app-store/delegatio import { workflowSelect } from "@calcom/ee/workflows/lib/getAllWorkflows"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; import { HttpError as HttpCode } from "@calcom/lib/http-error"; import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; @@ -86,6 +90,8 @@ export async function getBooking(bookingId: number) { id: true, name: true, parentId: true, + hideBranding: true, + parent: { select: { hideBranding: true } }, }, }, seatsPerTimeSlot: true, @@ -115,6 +121,12 @@ export async function getBooking(bookingId: number) { locale: true, destinationCalendar: true, isPlatformManaged: true, + hideBranding: true, + profiles: { + select: { + organization: { select: { hideBranding: true } }, + }, + }, }, }, }, @@ -202,6 +214,18 @@ export async function getBooking(bookingId: number) { customReplyToEmail: booking.eventType?.customReplyToEmail, seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot, seatsShowAttendees: booking.eventType?.seatsShowAttendees, + hideBranding: booking.eventTypeId + ? await getEventTypeService().shouldHideBrandingForEventType(booking.eventTypeId, { + team: booking.eventType?.team + ? { hideBranding: booking.eventType.team.hideBranding, parent: booking.eventType.team.parent } + : null, + owner: { + id: user.id, + hideBranding: userWithoutDelegationCredentials.hideBranding, + profiles: userWithoutDelegationCredentials.profiles ?? [], + }, + } satisfies EventTypeBrandingData) + : false, disableCancelling: booking.eventType?.disableCancelling ?? false, disableRescheduling: booking.eventType?.disableRescheduling ?? false, }; diff --git a/packages/features/bookings/lib/payment/handleNoShowFee.test.ts b/packages/features/bookings/lib/payment/handleNoShowFee.test.ts index 99d0b4d722..02585e0961 100644 --- a/packages/features/bookings/lib/payment/handleNoShowFee.test.ts +++ b/packages/features/bookings/lib/payment/handleNoShowFee.test.ts @@ -61,6 +61,7 @@ vi.mock("@calcom/features/ee/teams/repositories/TeamRepository", () => ({ vi.mock("@calcom/prisma", () => ({ default: {}, + prisma: {}, })); describe("handleNoShowFee", () => { diff --git a/packages/features/bookings/lib/payment/handleNoShowFee.ts b/packages/features/bookings/lib/payment/handleNoShowFee.ts index fd68034069..481dbec8a4 100644 --- a/packages/features/bookings/lib/payment/handleNoShowFee.ts +++ b/packages/features/bookings/lib/payment/handleNoShowFee.ts @@ -4,6 +4,10 @@ import dayjs from "@calcom/dayjs"; import { sendNoShowFeeChargedEmail } from "@calcom/emails/billing-email-service"; import { CredentialRepository } from "@calcom/features/credentials/repositories/CredentialRepository"; import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { ErrorWithCode } from "@calcom/lib/errors"; @@ -25,14 +29,18 @@ export const handleNoShowFee = async ({ startTime: Date; endTime: Date; userPrimaryEmail: string | null; + eventTypeId: number | null; userId: number | null; user?: { + id: number; email: string; name?: string | null; locale: string | null; timeZone: string; + hideBranding: boolean | null; profiles: { organizationId: number | null; + organization: { hideBranding: boolean | null } | null; }[]; } | null; eventType: { @@ -40,6 +48,11 @@ export const handleNoShowFee = async ({ hideOrganizerEmail: boolean; teamId: number | null; metadata?: Prisma.JsonValue; + team?: { + id: number; + hideBranding: boolean | null; + parent: { hideBranding: boolean | null } | null; + } | null; } | null; attendees: { name: string; @@ -101,6 +114,20 @@ export const handleNoShowFee = async ({ paymentOption: payment.paymentOption, }, organizationId: booking.user?.profiles?.[0]?.organizationId ?? null, + hideBranding: booking.eventTypeId + ? await getEventTypeService().shouldHideBrandingForEventType(booking.eventTypeId, { + team: booking.eventType?.team + ? { hideBranding: booking.eventType.team.hideBranding, parent: booking.eventType.team.parent } + : null, + owner: booking.user + ? { + id: booking.user.id, + hideBranding: booking.user.hideBranding, + profiles: booking.user.profiles ?? [], + } + : null, + } satisfies EventTypeBrandingData) + : false, }; if (teamId) { diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 819a0bc421..9b95eec6bc 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -45,6 +45,10 @@ import { BookingLocationService } from "@calcom/features/ee/round-robin/lib/book import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; import { getEventName, updateHostInEventName } from "@calcom/features/eventtypes/lib/eventNaming"; import type { FeaturesRepository } from "@calcom/features/flags/features.repository"; @@ -1483,6 +1487,11 @@ async function handler( where: { userId: organizerUser.id, }, + select: { + organizationId: true, + username: true, + organization: { select: { hideBranding: true } }, + }, }); const organizerOrganizationId = organizerOrganizationProfile?.organizationId; @@ -1571,6 +1580,20 @@ async function handler( }) .withOrganization(organizerOrganizationId) .withHashedLink(hasHashedBookingLink ? (reqBody.hashedLink ?? null) : null) + .withHideBranding( + await getEventTypeService().shouldHideBrandingForEventType(eventType.id, { + team: eventType.team + ? { hideBranding: eventType.team.hideBranding, parent: eventType.team.parent } + : null, + owner: { + id: organizerUser.id, + hideBranding: organizerUser.hideBranding, + profiles: organizerOrganizationProfile + ? [{ organization: organizerOrganizationProfile.organization }] + : [], + }, + } satisfies EventTypeBrandingData) + ) .build(); if (!builtEvt) { @@ -2617,7 +2640,7 @@ async function handler( workflows, smsReminderNumber: smsReminderNumber || null, calendarEvent: calendarEventForWorkflow, - hideBranding: !!eventType.owner?.hideBranding || !!platformClientId, + hideBranding: evt.hideBranding || !!platformClientId, seatReferenceUid: evt.attendeeSeatId, isDryRun, triggers: [WorkflowTriggerEvents.BOOKING_PAYMENT_INITIATED], @@ -2801,7 +2824,7 @@ async function handler( evt: evtWithMetadata, workflows, requiresConfirmation: !isConfirmedByDefault, - hideBranding: !!eventType.owner?.hideBranding || !!platformClientId, + hideBranding: (evt.hideBranding ?? false) || !!platformClientId, seatReferenceUid: evt.attendeeSeatId, isPlatformNoEmail: noEmail && Boolean(platformClientId), isDryRun, @@ -2816,7 +2839,7 @@ async function handler( workflows, smsReminderNumber: smsReminderNumber || null, calendarEvent: evtWithMetadata, - hideBranding: !!eventType.owner?.hideBranding || !!platformClientId, + hideBranding: evt.hideBranding || !!platformClientId, seatReferenceUid: evt.attendeeSeatId, isDryRun, isConfirmedByDefault, diff --git a/packages/features/bookings/repositories/BookingRepository.ts b/packages/features/bookings/repositories/BookingRepository.ts index 6de5476b73..e336f70d6a 100644 --- a/packages/features/bookings/repositories/BookingRepository.ts +++ b/packages/features/bookings/repositories/BookingRepository.ts @@ -249,8 +249,14 @@ const selectStatementToGetBookingForCalEventBuilder = { timeZone: true, locale: true, timeFormat: true, + hideBranding: true, destinationCalendar: true, - profiles: { select: { organizationId: true } }, + profiles: { + select: { + organizationId: true, + organization: { select: { hideBranding: true } }, + }, + }, }, }, // destination calendar of the Organizer @@ -284,6 +290,8 @@ const selectStatementToGetBookingForCalEventBuilder = { id: true, name: true, parentId: true, + hideBranding: true, + parent: { select: { hideBranding: true } }, members: { select: { user: { @@ -459,11 +467,17 @@ export class BookingRepository implements IBookingRepository { }, owner: { select: { + id: true, hideBranding: true, email: true, name: true, timeZone: true, locale: true, + profiles: { + select: { + organization: { select: { hideBranding: true } }, + }, + }, }, }, team: { @@ -471,6 +485,8 @@ export class BookingRepository implements IBookingRepository { parentId: true, name: true, id: true, + hideBranding: true, + parent: { select: { hideBranding: true } }, }, }, }, @@ -1549,17 +1565,27 @@ export class BookingRepository implements IBookingRepository { hideOrganizerEmail: true, teamId: true, metadata: true, + team: { + select: { + id: true, + hideBranding: true, + parent: { select: { hideBranding: true } }, + }, + }, }, }, user: { select: { + id: true, email: true, name: true, timeZone: true, locale: true, + hideBranding: true, profiles: { select: { organizationId: true, + organization: { select: { hideBranding: true } }, }, }, }, @@ -1679,7 +1705,28 @@ export class BookingRepository implements IBookingRepository { }, include: { attendees: true, - eventType: true, + eventType: { + select: { + teamId: true, + bookingFields: true, + title: true, + hideOrganizerEmail: true, + recurringEvent: true, + seatsPerTimeSlot: true, + seatsShowAttendees: true, + customReplyToEmail: true, + metadata: true, + schedulingType: true, + team: { + select: { + id: true, + name: true, + hideBranding: true, + parent: { select: { hideBranding: true } }, + }, + }, + }, + }, destinationCalendar: true, references: true, user: { @@ -1689,6 +1736,7 @@ export class BookingRepository implements IBookingRepository { profiles: { select: { organizationId: true, + organization: { select: { hideBranding: true } }, }, }, }, diff --git a/packages/features/di/tokens.ts b/packages/features/di/tokens.ts index 21a847ce5a..70d6155089 100644 --- a/packages/features/di/tokens.ts +++ b/packages/features/di/tokens.ts @@ -1,11 +1,12 @@ -import { BOOKING_DI_TOKENS } from "@calcom/features/bookings/di/tokens"; import { BOOKING_AUDIT_DI_TOKENS } from "@calcom/features/booking-audit/di/tokens"; +import { BOOKING_DI_TOKENS } from "@calcom/features/bookings/di/tokens"; import { ACTIVE_USER_BILLING_DI_TOKENS } from "@calcom/features/ee/billing/active-user/di/tokens"; +import { ORGANIZATION_DI_TOKENS } from "@calcom/features/ee/organizations/di/tokens"; +import { EVENT_TYPE_DI_TOKENS } from "@calcom/features/eventtypes/di/tokens"; import { FEATURE_OPT_IN_DI_TOKENS } from "@calcom/features/feature-opt-in/di/tokens"; import { FLAGS_DI_TOKENS } from "@calcom/features/flags/di/tokens"; import { HASHED_LINK_DI_TOKENS } from "@calcom/features/hashedLink/di/tokens"; import { OAUTH_DI_TOKENS } from "@calcom/features/oauth/di/tokens"; -import { ORGANIZATION_DI_TOKENS } from "@calcom/features/ee/organizations/di/tokens"; import { TRANSLATION_DI_TOKENS } from "@calcom/features/translation/di/tokens"; import { WATCHLIST_DI_TOKENS } from "./watchlist/Watchlist.tokens"; import { WEBHOOK_TOKENS } from "./webhooks/Webhooks.tokens"; @@ -89,4 +90,5 @@ export const DI_TOKENS = { ...ORGANIZATION_DI_TOKENS, ...TRANSLATION_DI_TOKENS, ...WEBHOOK_TOKENS, + ...EVENT_TYPE_DI_TOKENS, }; diff --git a/packages/features/ee/managed-event-types/reassignment/services/ManagedEventManualReassignmentService.ts b/packages/features/ee/managed-event-types/reassignment/services/ManagedEventManualReassignmentService.ts index 34928bae29..5546366140 100644 --- a/packages/features/ee/managed-event-types/reassignment/services/ManagedEventManualReassignmentService.ts +++ b/packages/features/ee/managed-event-types/reassignment/services/ManagedEventManualReassignmentService.ts @@ -8,37 +8,39 @@ import { import EventManager from "@calcom/features/bookings/lib/EventManager"; import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials"; import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB"; -import { +import type { BookingRepository, - type ManagedEventReassignmentCreatedBooking, - type ManagedEventCancellationResult, + ManagedEventCancellationResult, + ManagedEventReassignmentCreatedBooking, } from "@calcom/features/bookings/repositories/BookingRepository"; -import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder"; -import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; +import { CreditService } from "@calcom/features/ee/billing/credit-service"; +import { + type ManagedEventAssignmentReasonService, + ManagedEventReassignmentType, +} from "@calcom/features/ee/managed-event-types/reassignment/services/ManagedEventAssignmentReasonRecorder"; +import { + buildNewBookingPlan, + findTargetChildEventType, + validateManagedEventReassignment, +} from "@calcom/features/ee/managed-event-types/reassignment/utils"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { BookingLocationService } from "@calcom/features/ee/round-robin/lib/bookingLocationService"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; -import { CreditService } from "@calcom/features/ee/billing/credit-service"; -import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar"; -import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; +import type { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; +import type { UserRepository } from "@calcom/features/users/repositories/UserRepository"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; -import logger from "@calcom/lib/logger"; import type loggerType from "@calcom/lib/logger"; +import logger from "@calcom/lib/logger"; +import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import type { PrismaClient } from "@calcom/prisma"; - import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; - -import { - ManagedEventAssignmentReasonService, - ManagedEventReassignmentType, -} from "@calcom/features/ee/managed-event-types/reassignment/services/ManagedEventAssignmentReasonRecorder"; -import { - findTargetChildEventType, - validateManagedEventReassignment, - buildNewBookingPlan, -} from "@calcom/features/ee/managed-event-types/reassignment/utils"; +import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar"; interface ManagedEventManualReassignmentServiceDeps { prisma: PrismaClient; @@ -183,7 +185,24 @@ export class ManagedEventManualReassignmentService { bookerUrl, metadata: videoCallUrl ? { videoCallUrl, ...additionalInformation } : undefined, }, - hideBranding: !!targetEventTypeDetails.owner?.hideBranding, + hideBranding: await getEventTypeService().shouldHideBrandingForEventType( + targetEventTypeDetails.id, + { + team: targetEventTypeDetails.team + ? { + hideBranding: targetEventTypeDetails.team.hideBranding, + parent: targetEventTypeDetails.team.parent, + } + : null, + owner: targetEventTypeDetails.owner + ? { + id: targetEventTypeDetails.owner.id, + hideBranding: targetEventTypeDetails.owner.hideBranding, + profiles: targetEventTypeDetails.owner.profiles ?? [], + } + : null, + } satisfies EventTypeBrandingData + ), seatReferenceUid: undefined, isDryRun: false, isConfirmedByDefault: targetEventTypeDetails.requiresConfirmation ? false : true, @@ -451,7 +470,7 @@ export class ManagedEventManualReassignmentService { } let videoCallUrl: string | null = null; - let videoCallData: CalendarEvent["videoCallData"] = undefined; + let videoCallData: CalendarEvent["videoCallData"]; const additionalInformation: AdditionalInformation = {}; try { diff --git a/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts b/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts index cd12364da9..d4f209ec41 100644 --- a/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts +++ b/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts @@ -94,11 +94,13 @@ export class EmailWorkflowService { creditCheckFn, }); - const hideBranding = await this.shouldHideBranding({ - platformClientId: evt.platformClientId, - userId: workflow.userId, - teamId: workflow.teamId, - }); + const hideBranding = + evt.hideBranding ?? + (await this.shouldHideBranding({ + platformClientId: evt.platformClientId, + userId: workflow.userId, + teamId: evt.team?.id ?? workflow.teamId, + })); const emailWorkflowContentParams = await this.generateParametersToBuildEmailWorkflowContent({ evt, diff --git a/packages/features/eventtypes/di/EventTypeService.container.ts b/packages/features/eventtypes/di/EventTypeService.container.ts new file mode 100644 index 0000000000..c5748a9f49 --- /dev/null +++ b/packages/features/eventtypes/di/EventTypeService.container.ts @@ -0,0 +1,18 @@ +import { createContainer } from "@calcom/features/di/di"; +import { prismaModule } from "@calcom/features/di/modules/Prisma"; +import { DI_TOKENS } from "@calcom/features/di/tokens"; +import { + type EventTypeBrandingData, + type EventTypeService, + moduleLoader as eventTypeServiceModule, +} from "./EventTypeService.module"; + +const eventTypeServiceContainer = createContainer(); +eventTypeServiceContainer.load(DI_TOKENS.PRISMA_MODULE, prismaModule); + +export type { EventTypeBrandingData }; + +export function getEventTypeService(): EventTypeService { + eventTypeServiceModule.loadModule(eventTypeServiceContainer); + return eventTypeServiceContainer.get(eventTypeServiceModule.token); +} diff --git a/packages/features/eventtypes/di/EventTypeService.module.ts b/packages/features/eventtypes/di/EventTypeService.module.ts new file mode 100644 index 0000000000..cf81898270 --- /dev/null +++ b/packages/features/eventtypes/di/EventTypeService.module.ts @@ -0,0 +1,24 @@ +import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di"; +import { moduleLoader as eventTypeRepositoryModuleLoader } from "@calcom/features/di/modules/EventType"; +import { DI_TOKENS } from "@calcom/features/di/tokens"; +import { EventTypeService } from "../service/EventTypeService"; + +const thisModule = createModule(); +const token = DI_TOKENS.EVENT_TYPE_SERVICE; +const moduleToken = DI_TOKENS.EVENT_TYPE_SERVICE_MODULE; + +const loadModule = bindModuleToClassOnToken({ + module: thisModule, + moduleToken, + token, + classs: EventTypeService, + dep: eventTypeRepositoryModuleLoader, +}); + +export const moduleLoader = { + token, + loadModule, +} satisfies ModuleLoader; + +export type { EventTypeService }; +export type { EventTypeBrandingData } from "../service/EventTypeService"; diff --git a/packages/features/eventtypes/di/tokens.ts b/packages/features/eventtypes/di/tokens.ts new file mode 100644 index 0000000000..b0efbc931d --- /dev/null +++ b/packages/features/eventtypes/di/tokens.ts @@ -0,0 +1,4 @@ +export const EVENT_TYPE_DI_TOKENS = { + EVENT_TYPE_SERVICE: Symbol("EventTypeService"), + EVENT_TYPE_SERVICE_MODULE: Symbol("EventTypeServiceModule"), +}; diff --git a/packages/features/eventtypes/repositories/eventTypeRepository.ts b/packages/features/eventtypes/repositories/eventTypeRepository.ts index 53831fe9ca..a23fa121f7 100644 --- a/packages/features/eventtypes/repositories/eventTypeRepository.ts +++ b/packages/features/eventtypes/repositories/eventTypeRepository.ts @@ -1771,6 +1771,32 @@ export class EventTypeRepository implements IEventTypesRepository { }); } + async findByIdIncludeBrandingInfo({ id }: { id: number }) { + return await this.prismaClient.eventType.findUnique({ + where: { id }, + select: { + id: true, + team: { + select: { + hideBranding: true, + parent: { select: { hideBranding: true } }, + }, + }, + owner: { + select: { + id: true, + hideBranding: true, + profiles: { + select: { + organization: { select: { hideBranding: true } }, + }, + }, + }, + }, + }, + }); + } + async getEventTypeList({ teamId, userId, diff --git a/packages/features/eventtypes/service/EventTypeService.test.ts b/packages/features/eventtypes/service/EventTypeService.test.ts new file mode 100644 index 0000000000..688d3a1fe4 --- /dev/null +++ b/packages/features/eventtypes/service/EventTypeService.test.ts @@ -0,0 +1,141 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { EventTypeRepository } from "../repositories/eventTypeRepository"; +import type { EventTypeBrandingData } from "./EventTypeService"; +import { EventTypeService } from "./EventTypeService"; + +function createMockRepository(overrides: Partial = {}): EventTypeRepository { + return { + findByIdIncludeBrandingInfo: vi.fn().mockResolvedValue(null), + ...overrides, + } as unknown as EventTypeRepository; +} + +describe("EventTypeService", () => { + let service: EventTypeService; + let mockRepo: EventTypeRepository; + + beforeEach(() => { + mockRepo = createMockRepository(); + service = new EventTypeService(mockRepo); + }); + + describe("shouldHideBrandingForEventType", () => { + describe("hot path (prefetchedData provided)", () => { + it("returns false when prefetchedData has no team and no owner", async () => { + const prefetchedData: EventTypeBrandingData = { team: null, owner: null }; + const result = await service.shouldHideBrandingForEventType(1, prefetchedData); + expect(result).toBe(false); + expect(mockRepo.findByIdIncludeBrandingInfo).not.toHaveBeenCalled(); + }); + + it("returns true when team has hideBranding enabled", async () => { + const prefetchedData: EventTypeBrandingData = { + team: { hideBranding: true, parent: null }, + owner: null, + }; + const result = await service.shouldHideBrandingForEventType(1, prefetchedData); + expect(result).toBe(true); + expect(mockRepo.findByIdIncludeBrandingInfo).not.toHaveBeenCalled(); + }); + + it("returns true when team parent (organization) has hideBranding enabled", async () => { + const prefetchedData: EventTypeBrandingData = { + team: { hideBranding: false, parent: { hideBranding: true } }, + owner: null, + }; + const result = await service.shouldHideBrandingForEventType(1, prefetchedData); + expect(result).toBe(true); + }); + + it("returns false when team hideBranding is false and no parent", async () => { + const prefetchedData: EventTypeBrandingData = { + team: { hideBranding: false, parent: null }, + owner: null, + }; + const result = await service.shouldHideBrandingForEventType(1, prefetchedData); + expect(result).toBe(false); + }); + + it("returns true when owner has hideBranding enabled", async () => { + const prefetchedData: EventTypeBrandingData = { + team: null, + owner: { id: 42, hideBranding: true, profiles: [] }, + }; + const result = await service.shouldHideBrandingForEventType(1, prefetchedData); + expect(result).toBe(true); + }); + + it("returns true when owner's organization has hideBranding enabled", async () => { + const prefetchedData: EventTypeBrandingData = { + team: null, + owner: { + id: 42, + hideBranding: false, + profiles: [{ organization: { hideBranding: true } }], + }, + }; + const result = await service.shouldHideBrandingForEventType(1, prefetchedData); + expect(result).toBe(true); + }); + + it("returns false when owner hideBranding is false and no org branding", async () => { + const prefetchedData: EventTypeBrandingData = { + team: null, + owner: { + id: 42, + hideBranding: false, + profiles: [{ organization: { hideBranding: false } }], + }, + }; + const result = await service.shouldHideBrandingForEventType(1, prefetchedData); + expect(result).toBe(false); + }); + + it("handles owner with empty profiles array", async () => { + const prefetchedData: EventTypeBrandingData = { + team: null, + owner: { id: 42, hideBranding: false, profiles: [] }, + }; + const result = await service.shouldHideBrandingForEventType(1, prefetchedData); + expect(result).toBe(false); + }); + + it("prioritises team over owner when both provided", async () => { + const prefetchedData: EventTypeBrandingData = { + team: { hideBranding: true, parent: null }, + owner: { id: 42, hideBranding: false, profiles: [] }, + }; + const result = await service.shouldHideBrandingForEventType(1, prefetchedData); + expect(result).toBe(true); + }); + }); + + describe("cold path (no prefetchedData)", () => { + it("fetches from repository when no prefetchedData is provided", async () => { + const repoData = { + team: { hideBranding: true, parent: null }, + owner: null, + }; + mockRepo = createMockRepository({ + findByIdIncludeBrandingInfo: vi.fn().mockResolvedValue(repoData), + }); + service = new EventTypeService(mockRepo); + + const result = await service.shouldHideBrandingForEventType(99); + expect(result).toBe(true); + expect(mockRepo.findByIdIncludeBrandingInfo).toHaveBeenCalledWith({ id: 99 }); + }); + + it("returns false when repository returns null", async () => { + mockRepo = createMockRepository({ + findByIdIncludeBrandingInfo: vi.fn().mockResolvedValue(null), + }); + service = new EventTypeService(mockRepo); + + const result = await service.shouldHideBrandingForEventType(99); + expect(result).toBe(false); + expect(mockRepo.findByIdIncludeBrandingInfo).toHaveBeenCalledWith({ id: 99 }); + }); + }); + }); +}); diff --git a/packages/features/eventtypes/service/EventTypeService.ts b/packages/features/eventtypes/service/EventTypeService.ts new file mode 100644 index 0000000000..7ff06f55d3 --- /dev/null +++ b/packages/features/eventtypes/service/EventTypeService.ts @@ -0,0 +1,55 @@ +import { shouldHideBrandingForEventUsingProfile } from "@calcom/features/profile/lib/hideBranding"; +import type { EventTypeRepository } from "../repositories/eventTypeRepository"; + +/** + * Shape of pre-fetched branding data that callers can pass to avoid a DB query. + * Matches the raw DB shape returned by EventTypeRepository.findByIdIncludeBrandingInfo. + */ +export type EventTypeBrandingData = { + team: { + hideBranding: boolean | null; + parent: { hideBranding: boolean | null } | null; + } | null; + owner: { + id: number; + hideBranding: boolean | null; + profiles: Array<{ organization: { hideBranding: boolean | null } | null }>; + } | null; +}; + +export class EventTypeService { + constructor(private eventTypeRepository: EventTypeRepository) {} + + /** + * Determines whether branding should be hidden for the given event type. + * + * - **Hot path** (prefetchedData provided): uses the supplied data directly, no DB query. + * - **Cold path** (no prefetchedData): fetches from DB via the repository. + */ + async shouldHideBrandingForEventType( + eventTypeId: number, + prefetchedData?: EventTypeBrandingData + ): Promise { + const data = + prefetchedData ?? (await this.eventTypeRepository.findByIdIncludeBrandingInfo({ id: eventTypeId })); + + if (!data) return false; + + return shouldHideBrandingForEventUsingProfile({ + eventTypeId, + team: data.team + ? { + hideBranding: data.team.hideBranding, + parent: data.team.parent, + } + : null, + owner: data.owner + ? { + id: data.owner.id, + hideBranding: data.owner.hideBranding, + profile: data.owner.profiles?.[0] ? { organization: data.owner.profiles[0].organization } : null, + } + : null, + }); + } +} diff --git a/packages/features/handleMarkNoShow.ts b/packages/features/handleMarkNoShow.ts index 9ccc992240..1ca9bb67d6 100644 --- a/packages/features/handleMarkNoShow.ts +++ b/packages/features/handleMarkNoShow.ts @@ -8,15 +8,19 @@ import { } from "@calcom/features/booking-audit/lib/makeActor"; import type { ValidActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container"; -import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository"; import { AttendeeRepository } from "@calcom/features/bookings/repositories/AttendeeRepository"; import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; import { BookingAccessService } from "@calcom/features/bookings/services/BookingAccessService"; +import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository"; import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType"; import type { ExtendedCalendarEvent } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; import { WebhookService } from "@calcom/features/webhooks/lib/WebhookService"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { HttpError } from "@calcom/lib/http-error"; @@ -360,6 +364,25 @@ const handleMarkNoShow = async ({ } : undefined; + const hideBranding = await getEventTypeService().shouldHideBrandingForEventType( + booking.eventType.id, + { + team: booking.eventType.team + ? { + hideBranding: booking.eventType.team.hideBranding, + parent: booking.eventType.team.parent, + } + : null, + owner: booking.eventType.owner + ? { + id: booking.eventType.owner.id, + hideBranding: booking.eventType.owner.hideBranding, + profiles: booking.eventType.owner.profiles ?? [], + } + : null, + } satisfies EventTypeBrandingData + ); + const calendarEvent: ExtendedCalendarEvent = { type: booking.eventType.slug, title: booking.title, @@ -394,6 +417,7 @@ const handleMarkNoShow = async ({ eventTypeId: booking.eventType?.id, customReplyToEmail: booking.eventType?.customReplyToEmail, team, + hideBranding, }; const creditService = new CreditService(); @@ -401,7 +425,7 @@ const handleMarkNoShow = async ({ await WorkflowService.scheduleWorkflowsFilteredByTriggerEvent({ workflows, smsReminderNumber: booking.smsReminderNumber, - hideBranding: booking.eventType.owner?.hideBranding, + hideBranding: calendarEvent.hideBranding, calendarEvent, triggers: [WorkflowTriggerEvents.BOOKING_NO_SHOW_UPDATED], creditCheckFn: creditService.hasAvailableCredits.bind(creditService), diff --git a/packages/features/profile/lib/hideBranding.test.ts b/packages/features/profile/lib/hideBranding.test.ts new file mode 100644 index 0000000000..0a8183f3ad --- /dev/null +++ b/packages/features/profile/lib/hideBranding.test.ts @@ -0,0 +1,397 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; + +import { + shouldHideBrandingForEventUsingProfile, + shouldHideBrandingForTeamEvent, + shouldHideBrandingForUserEvent, +} from "./hideBranding"; + +// Mock the dependencies +vi.mock("@calcom/features/ee/teams/repositories/TeamRepository", () => { + return { + TeamRepository: class MockTeamRepository { + findTeamWithParentHideBranding = vi.fn(); + }, + }; +}); + +vi.mock("@calcom/features/users/repositories/UserRepository", () => { + return { + UserRepository: class MockUserRepository { + findUserWithHideBranding = vi.fn(); + }, + }; +}); + +vi.mock("@calcom/features/profile/repositories/ProfileRepository", () => ({ + ProfileRepository: { + findByUserIdAndOrgSlug: vi.fn(), + }, +})); + +vi.mock("@calcom/lib/logger", () => ({ + default: { + getSubLogger: () => ({ + error: vi.fn(), + }), + }, +})); + +vi.mock("@calcom/prisma", () => ({ + prisma: {}, +})); + +describe("hideBranding", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("shouldHideBrandingForEventUsingProfile", () => { + describe("team events", () => { + it("should return true when team has hideBranding enabled", () => { + const team = { + hideBranding: true, + parent: null, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner: null, + team, + }); + + expect(result).toBe(true); + }); + + it("should return true when parent organization has hideBranding enabled", () => { + const team = { + hideBranding: false, + parent: { + hideBranding: true, + }, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner: null, + team, + }); + + expect(result).toBe(true); + }); + + it("should return true when both team and parent have hideBranding enabled", () => { + const team = { + hideBranding: true, + parent: { + hideBranding: true, + }, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner: null, + team, + }); + + expect(result).toBe(true); + }); + + it("should return false when neither team nor parent has hideBranding enabled", () => { + const team = { + hideBranding: false, + parent: { + hideBranding: false, + }, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner: null, + team, + }); + + expect(result).toBe(false); + }); + + it("should return false when team has hideBranding disabled and no parent", () => { + const team = { + hideBranding: false, + parent: null, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner: null, + team, + }); + + expect(result).toBe(false); + }); + + it("should handle null hideBranding values", () => { + const team = { + hideBranding: null, + parent: { + hideBranding: null, + }, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner: null, + team, + }); + + expect(result).toBe(false); + }); + }); + + describe("user events", () => { + it("should return true when user has hideBranding enabled", () => { + const owner = { + id: 1, + hideBranding: true, + profile: null, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner, + team: null, + }); + + expect(result).toBe(true); + }); + + it("should return true when user's organization has hideBranding enabled", () => { + const owner = { + id: 1, + hideBranding: false, + profile: { + organization: { + hideBranding: true, + }, + }, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner, + team: null, + }); + + expect(result).toBe(true); + }); + + it("should return true when both user and organization have hideBranding enabled", () => { + const owner = { + id: 1, + hideBranding: true, + profile: { + organization: { + hideBranding: true, + }, + }, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner, + team: null, + }); + + expect(result).toBe(true); + }); + + it("should return false when neither user nor organization has hideBranding enabled", () => { + const owner = { + id: 1, + hideBranding: false, + profile: { + organization: { + hideBranding: false, + }, + }, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner, + team: null, + }); + + expect(result).toBe(false); + }); + + it("should return false when user has hideBranding disabled and no organization", () => { + const owner = { + id: 1, + hideBranding: false, + profile: { + organization: null, + }, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner, + team: null, + }); + + expect(result).toBe(false); + }); + + it("should handle null profile", () => { + const owner = { + id: 1, + hideBranding: false, + profile: null, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner, + team: null, + }); + + expect(result).toBe(false); + }); + + it("should handle null hideBranding values", () => { + const owner = { + id: 1, + hideBranding: null, + profile: { + organization: { + hideBranding: null, + }, + }, + }; + + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner, + team: null, + }); + + expect(result).toBe(false); + }); + }); + + describe("no owner or team", () => { + it("should return false when neither owner nor team is provided", () => { + const result = shouldHideBrandingForEventUsingProfile({ + eventTypeId: 1, + owner: null, + team: null, + }); + + expect(result).toBe(false); + }); + }); + }); + + describe("shouldHideBrandingForTeamEvent", () => { + it("should return true when team has hideBranding enabled", () => { + const team = { + hideBranding: true, + parent: null, + }; + + const result = shouldHideBrandingForTeamEvent({ + eventTypeId: 1, + team, + }); + + expect(result).toBe(true); + }); + + it("should return true when parent organization has hideBranding enabled", () => { + const team = { + hideBranding: false, + parent: { + hideBranding: true, + }, + }; + + const result = shouldHideBrandingForTeamEvent({ + eventTypeId: 1, + team, + }); + + expect(result).toBe(true); + }); + + it("should return false when neither team nor parent has hideBranding enabled", () => { + const team = { + hideBranding: false, + parent: { + hideBranding: false, + }, + }; + + const result = shouldHideBrandingForTeamEvent({ + eventTypeId: 1, + team, + }); + + expect(result).toBe(false); + }); + }); + + describe("shouldHideBrandingForUserEvent", () => { + it("should return true when user has hideBranding enabled", () => { + const owner = { + id: 1, + hideBranding: true, + profile: null, + }; + + const result = shouldHideBrandingForUserEvent({ + eventTypeId: 1, + owner, + }); + + expect(result).toBe(true); + }); + + it("should return true when user's organization has hideBranding enabled", () => { + const owner = { + id: 1, + hideBranding: false, + profile: { + organization: { + hideBranding: true, + }, + }, + }; + + const result = shouldHideBrandingForUserEvent({ + eventTypeId: 1, + owner, + }); + + expect(result).toBe(true); + }); + + it("should return false when neither user nor organization has hideBranding enabled", () => { + const owner = { + id: 1, + hideBranding: false, + profile: { + organization: { + hideBranding: false, + }, + }, + }; + + const result = shouldHideBrandingForUserEvent({ + eventTypeId: 1, + owner, + }); + + expect(result).toBe(false); + }); + }); +}); diff --git a/packages/i18n/locales/en/common.json b/packages/i18n/locales/en/common.json index a68201f4cc..1d14645076 100644 --- a/packages/i18n/locales/en/common.json +++ b/packages/i18n/locales/en/common.json @@ -20,6 +20,7 @@ "reset_password_subject": "{{appName}}: Reset password instructions", "verify_email_subject": "{{appName}}: Verify your account", "verify_email_subject_verifying_email": "{{appName}}: Verify your email", + "verify_email_subject_no_branding": "Verify your email", "check_your_email": "Check your email", "old_email_address": "Old email", "new_email_address": "New email", diff --git a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx index cbceb1ca05..a27ca2facc 100644 --- a/packages/platform/atoms/booker/BookerPlatformWrapper.tsx +++ b/packages/platform/atoms/booker/BookerPlatformWrapper.tsx @@ -401,6 +401,7 @@ const BookerPlatformWrapperComponent = ( name: bookerForm.formName, requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification, onVerifyEmail: bookerForm.beforeVerifyEmail, + eventTypeId: event?.data?.id, }); const verifyCode = useVerifyCode({ diff --git a/packages/platform/atoms/hooks/useVerifyEmail.ts b/packages/platform/atoms/hooks/useVerifyEmail.ts index 7b687c322b..844ffc9334 100644 --- a/packages/platform/atoms/hooks/useVerifyEmail.ts +++ b/packages/platform/atoms/hooks/useVerifyEmail.ts @@ -15,6 +15,7 @@ export interface IUseVerifyEmailProps { onVerifyEmail?: () => void; name?: string | { firstName: string; lastname?: string }; requiresBookerEmailVerification?: boolean; + eventTypeId?: number; } export type UseVerifyEmailReturnType = ReturnType; @@ -22,6 +23,7 @@ export type UseVerifyEmailReturnType = ReturnType; interface RequestEmailVerificationInput { email: string; username?: string; + eventTypeId?: number; } export const useVerifyEmail = ({ @@ -29,6 +31,7 @@ export const useVerifyEmail = ({ name, requiresBookerEmailVerification, onVerifyEmail, + eventTypeId, }: IUseVerifyEmailProps) => { const [isEmailVerificationModalVisible, setEmailVerificationModalVisible] = useState(false); const verifiedEmail = useBookerStore((state) => state.verifiedEmail); @@ -85,6 +88,7 @@ export const useVerifyEmail = ({ sendEmailVerificationMutation.mutate({ email, username: typeof name === "string" ? name : name?.firstName, + eventTypeId, }); }; diff --git a/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts b/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts index 7874c5f47b..51766b947d 100644 --- a/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts @@ -1,6 +1,10 @@ import { sendScheduledEmailsAndSMS } from "@calcom/emails/email-manager"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { scheduleNoShowTriggers } from "@calcom/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj"; import { getTranslation } from "@calcom/i18n/server"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; @@ -9,9 +13,7 @@ import { BookingStatus } from "@calcom/prisma/enums"; import { bookingMetadataSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import type { CalendarEvent } from "@calcom/types/Calendar"; - import { TRPCError } from "@trpc/server"; - import type { TConnectAndJoinInputSchema } from "./connectAndJoin.schema"; type Options = { @@ -32,6 +34,18 @@ export const Handler = async ({ ctx, input }: Options) => { const tOrganizer = await getTranslation(user?.locale ?? "en", "common"); + const userBrandingInfo = await prisma.user.findUnique({ + where: { id: user.id }, + select: { + hideBranding: true, + profiles: { + select: { + organization: { select: { hideBranding: true } }, + }, + }, + }, + }); + const instantMeetingToken = await prisma.instantMeetingToken.findUnique({ select: { expires: true, @@ -135,6 +149,8 @@ export const Handler = async ({ ctx, input }: Options) => { select: { id: true, name: true, + hideBranding: true, + parent: { select: { hideBranding: true } }, }, }, }, @@ -221,6 +237,21 @@ export const Handler = async ({ ctx, input }: Options) => { members: [], } : undefined, + hideBranding: updatedBooking.eventTypeId + ? await getEventTypeService().shouldHideBrandingForEventType(updatedBooking.eventTypeId, { + team: updatedBooking.eventType?.team + ? { + hideBranding: updatedBooking.eventType.team.hideBranding, + parent: updatedBooking.eventType.team.parent, + } + : null, + owner: { + id: user.id, + hideBranding: userBrandingInfo?.hideBranding ?? null, + profiles: userBrandingInfo?.profiles ?? [], + }, + } satisfies EventTypeBrandingData) + : false, }; const eventTypeMetadata = EventTypeMetaDataSchema.parse(updatedBooking?.eventType?.metadata); diff --git a/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts b/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts index b7d0bd7637..b89f273049 100644 --- a/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.handler.ts @@ -1,10 +1,9 @@ -import type { NextApiRequest } from "next"; - import { sendEmailVerificationByCode } from "@calcom/features/auth/lib/verifyEmail"; +import { getEventTypeService } from "@calcom/features/eventtypes/di/EventTypeService.container"; import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError"; import getIP from "@calcom/lib/getIP"; import { hashEmail, piiHasher } from "@calcom/lib/server/PiiHasher"; - +import type { NextApiRequest } from "next"; import type { TRPCContext } from "../../../createContext"; import type { TSendVerifyEmailCodeSchema } from "./sendVerifyEmailCode.schema"; @@ -30,10 +29,17 @@ export const sendVerifyEmailCode = async ({ identifier: `sendVerifyEmailCode:${identifier}`, }); + let hideBranding = false; + if (input.eventTypeId) { + const eventTypeService = getEventTypeService(); + hideBranding = await eventTypeService.shouldHideBrandingForEventType(input.eventTypeId); + } + return await sendEmailVerificationByCode({ email: input.email, username: input.username, language: input.language, isVerifyingEmail: input.isVerifyingEmail, + hideBranding, }); }; diff --git a/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.schema.ts b/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.schema.ts index 714272a6c2..bc79c1c766 100644 --- a/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.schema.ts +++ b/packages/trpc/server/routers/viewer/auth/sendVerifyEmailCode.schema.ts @@ -5,6 +5,7 @@ export type TSendVerifyEmailCodeSchema = { username?: string; language: string; isVerifyingEmail?: boolean; + eventTypeId?: number; }; export const ZSendVerifyEmailCodeSchema: z.ZodType = z.object({ @@ -12,4 +13,5 @@ export const ZSendVerifyEmailCodeSchema: z.ZodType = username: z.string().optional(), language: z.string(), isVerifyingEmail: z.boolean().optional(), + eventTypeId: z.number().optional(), }); diff --git a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts index 85ae52d13f..d99f95ef9c 100644 --- a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts @@ -8,6 +8,10 @@ import { BookingEmailSmsHandler } from "@calcom/features/bookings/lib/BookingEma import EventManager from "@calcom/features/bookings/lib/EventManager"; import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; import { extractBaseEmail } from "@calcom/lib/extract-base-email"; @@ -23,10 +27,7 @@ import { TRPCError } from "@trpc/server"; import type { TrpcSessionUser } from "../../../types"; import type { TAddGuestsInputSchema } from "./addGuests.schema"; -export type TUser = Pick< - NonNullable, - "id" | "email" | "organizationId" | "uuid" -> & +export type TUser = Pick, "id" | "email" | "organizationId" | "uuid"> & Partial, "profile">>; type AddGuestsOptions = { @@ -79,9 +80,7 @@ export const addGuestsHandler = async ({ ); // Capture new attendee emails after update for audit logging - const newAttendeeEmails = bookingAttendees.attendees.map( - (attendee) => attendee.email - ); + const newAttendeeEmails = bookingAttendees.attendees.map((attendee) => attendee.email); const attendeesList = await prepareAttendeesList(bookingAttendees.attendees); @@ -97,10 +96,7 @@ export const addGuestsHandler = async ({ const featuresRepository = getFeaturesRepository(); const organizationId = user.organizationId ?? null; const isBookingAuditEnabled = organizationId - ? await featuresRepository.checkIfTeamHasFeature( - organizationId, - "booking-audit" - ) + ? await featuresRepository.checkIfTeamHasFeature(organizationId, "booking-audit") : false; await bookingEventHandlerService.onAttendeeAdded({ @@ -119,9 +115,7 @@ export const addGuestsHandler = async ({ export async function getBooking(bookingId: number) { const bookingRepository = new BookingRepository(prisma); - const booking = await bookingRepository.findByIdIncludeDestinationCalendar( - bookingId - ); + const booking = await bookingRepository.findByIdIncludeDestinationCalendar(bookingId); if (!booking || !booking.user) { throw new TRPCError({ code: "NOT_FOUND", message: "booking_not_found" }); @@ -130,14 +124,9 @@ export async function getBooking(bookingId: number) { return booking; } -export async function validateUserPermissions( - booking: Booking, - user: TUser -): Promise { +export async function validateUserPermissions(booking: Booking, user: TUser): Promise { const isOrganizer = booking.userId === user.id; - const isAttendee = !!booking.attendees.find( - (attendee) => attendee.email === user.email - ); + const isAttendee = !!booking.attendees.find((attendee) => attendee.email === user.email); let hasBookingUpdatePermission = false; if (booking.eventType?.teamId) { @@ -163,9 +152,7 @@ export function validateGuestsFieldEnabled(booking: Booking): void { ? eventTypeBookingFields.parse(booking.eventType.bookingFields) : []; - const guestsBookingField = parsedBookingFields.find( - (field) => field.name === "guests" - ); + const guestsBookingField = parsedBookingFields.find((field) => field.name === "guests"); if (guestsBookingField?.hidden) { throw new TRPCError({ code: "BAD_REQUEST", @@ -184,10 +171,17 @@ export async function getOrganizerData(userId: number | null) { id: userId, }, select: { + id: true, name: true, email: true, timeZone: true, locale: true, + hideBranding: true, + profiles: { + select: { + organization: { select: { hideBranding: true } }, + }, + }, }, }); } @@ -206,30 +200,20 @@ function deduplicateGuestEmails(guests: string[]): string[] { function getBlacklistedEmails(): string[] { return process.env.BLACKLISTED_GUEST_EMAILS - ? process.env.BLACKLISTED_GUEST_EMAILS.split(",").map((email) => - email.toLowerCase() - ) + ? process.env.BLACKLISTED_GUEST_EMAILS.split(",").map((email) => email.toLowerCase()) : []; } -async function getEmailVerificationRequirements( - guestEmails: string[] -): Promise> { +async function getEmailVerificationRequirements(guestEmails: string[]): Promise> { const userRepo = new UserRepository(prisma); - const guestUsers = - await userRepo.findManyByEmailsWithEmailVerificationSettings({ - emails: guestEmails, - }); + const guestUsers = await userRepo.findManyByEmailsWithEmailVerificationSettings({ + emails: guestEmails, + }); const emailToRequiresVerification = new Map(); for (const user of guestUsers) { - const matchedBase = extractBaseEmail( - user.matchedEmail ?? user.email - ).toLowerCase(); - emailToRequiresVerification.set( - matchedBase, - user.requiresBookerEmailVerification === true - ); + const matchedBase = extractBaseEmail(user.matchedEmail ?? user.email).toLowerCase(); + emailToRequiresVerification.set(matchedBase, user.requiresBookerEmailVerification === true); } return emailToRequiresVerification; @@ -256,12 +240,8 @@ export async function sanitizeAndFilterGuests( const guestEmails = guests.map((guest) => guest.email); const deduplicatedGuests = deduplicateGuestEmails(guestEmails); const blacklistedGuestEmails = getBlacklistedEmails(); - const guestEmailsLowerCase = deduplicatedGuests.map((email) => - extractBaseEmail(email).toLowerCase() - ); - const emailToRequiresVerification = await getEmailVerificationRequirements( - guestEmailsLowerCase - ); + const guestEmailsLowerCase = deduplicatedGuests.map((email) => extractBaseEmail(email).toLowerCase()); + const emailToRequiresVerification = await getEmailVerificationRequirements(guestEmailsLowerCase); // Create a map of email to guest object for easy lookup const emailToGuestMap = new Map( @@ -272,8 +252,7 @@ export async function sanitizeAndFilterGuests( const baseGuestEmail = extractBaseEmail(email).toLowerCase(); return ( !booking.attendees.some( - (attendee) => - extractBaseEmail(attendee.email).toLowerCase() === baseGuestEmail + (attendee) => extractBaseEmail(attendee.email).toLowerCase() === baseGuestEmail ) && !blacklistedGuestEmails.includes(baseGuestEmail) && !emailToRequiresVerification.get(baseGuestEmail) @@ -339,9 +318,7 @@ export async function buildCalendarEvent( attendeesList: Awaited> ): Promise { const tOrganizer = await getTranslation(organizer.locale ?? "en", "common"); - const videoCallReference = booking.references.find((reference) => - reference.type.includes("_video") - ); + const videoCallReference = booking.references.find((reference) => reference.type.includes("_video")); const evt: CalendarEvent = { title: booking.title || "", @@ -364,12 +341,24 @@ export async function buildCalendarEvent( destinationCalendar: booking?.destinationCalendar ? [booking?.destinationCalendar] : booking?.user?.destinationCalendar - ? [booking?.user?.destinationCalendar] - : [], + ? [booking?.user?.destinationCalendar] + : [], seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot, seatsShowAttendees: booking.eventType?.seatsShowAttendees, customReplyToEmail: booking.eventType?.customReplyToEmail, organizationId: booking.user?.profiles?.[0]?.organizationId ?? null, + hideBranding: booking.eventTypeId + ? await getEventTypeService().shouldHideBrandingForEventType(booking.eventTypeId, { + team: booking.eventType?.team + ? { hideBranding: booking.eventType.team.hideBranding, parent: booking.eventType.team.parent } + : null, + owner: { + id: organizer.id, + hideBranding: organizer.hideBranding, + profiles: organizer.profiles ?? [], + }, + } satisfies EventTypeBrandingData) + : false, }; if (videoCallReference) { @@ -384,10 +373,7 @@ export async function buildCalendarEvent( return evt; } -export async function updateCalendarEvent( - booking: Booking, - evt: CalendarEvent -): Promise { +export async function updateCalendarEvent(booking: Booking, evt: CalendarEvent): Promise { if (!booking.user) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -395,9 +381,7 @@ export async function updateCalendarEvent( }); } - const credentials = await getUsersCredentialsIncludeServiceAccountKey( - booking.user - ); + const credentials = await getUsersCredentialsIncludeServiceAccountKey(booking.user); const eventManager = new EventManager({ ...booking.user, @@ -419,9 +403,7 @@ export async function sendGuestNotifications( await emailsAndSmsHandler.handleAddGuests({ evt, eventType: { - metadata: eventTypeMetaDataSchemaWithTypedApps.parse( - booking?.eventType?.metadata - ), + metadata: eventTypeMetaDataSchemaWithTypedApps.parse(booking?.eventType?.metadata), schedulingType: booking.eventType?.schedulingType || null, }, newGuests: uniqueGuests, diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index 852bb13058..d253e7dcbe 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -19,6 +19,10 @@ import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBooke import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows"; import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; +import { + type EventTypeBrandingData, + getEventTypeService, +} from "@calcom/features/eventtypes/di/EventTypeService.container"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; @@ -168,6 +172,8 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { id: true, name: true, parentId: true, + hideBranding: true, + parent: { select: { hideBranding: true } }, }, }, workflows: { @@ -198,6 +204,12 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { name: true, destinationCalendar: true, locale: true, + hideBranding: true, + profiles: { + select: { + organization: { select: { hideBranding: true } }, + }, + }, }, }, id: true, @@ -352,6 +364,18 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { details: booking.assignmentReason[0].reasonString ?? null, } : null, + hideBranding: booking.eventType?.id + ? await getEventTypeService().shouldHideBrandingForEventType(booking.eventType.id, { + team: booking.eventType.team + ? { hideBranding: booking.eventType.team.hideBranding, parent: booking.eventType.team.parent } + : null, + owner: { + id: user.id, + hideBranding: user.hideBranding, + profiles: user.profiles ?? [], + }, + } satisfies EventTypeBrandingData) + : false, }; const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent); @@ -564,7 +588,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { slug: booking.eventType?.slug as string, }, }, - hideBranding: !!booking.eventType?.owner?.hideBranding, + hideBranding: evt.hideBranding, triggers: [WorkflowTriggerEvents.BOOKING_REJECTED], creditCheckFn: creditService.hasAvailableCredits.bind(creditService), }); diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index 23e4cea36d..8438c5c36d 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -219,6 +219,7 @@ export interface CalendarEvent { platformRescheduleUrl?: string | null; platformCancelUrl?: string | null; platformBookingUrl?: string | null; + hideBranding?: boolean; oneTimePassword?: string | null; delegationCredentialId?: string | null; domainWideDelegationCredentialId?: string | null;