fix: hide cal branding for orgs/teams (#27643)

* fix: hide branding for teams

* fix: remove unused organizationId and username fields from profiles select

Addresses Cubic AI review feedback (confidence 9/10) to select only
the profile fields that are actually used. The organizationId and
username fields were fetched but never referenced in this function.

Co-Authored-By: unknown <>

* fix: unit tests

* fix: add prisma named export to test mock

Co-Authored-By: rajiv@cal.com <sahalrajiv6900@gmail.com>

* Add tests: packages/features/profile/lib/hideBranding.test.ts

Generated by Paragon from proposal for PR #27643

* chore: implement cubic feedback

* fix: merge conflicts

* fix: unit tests

* fixup

* refactor: implement DI pattern for event type service

* fix: atoms build

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Rajiv Sahal
2026-02-25 20:53:01 +09:00
committed by GitHub
co-authored by unknown <> rajiv@cal.com <sahalrajiv6900@gmail.com> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 4081d11fbe
commit 5d65a0f091
39 changed files with 1129 additions and 154 deletions
@@ -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);
@@ -12,6 +12,7 @@ export interface IUseVerifyEmailProps {
onVerifyEmail?: () => void;
name?: string | { firstName: string; lastname?: string };
requiresBookerEmailVerification?: boolean;
eventTypeId?: number;
}
export type UseVerifyEmailReturnType = ReturnType<typeof useVerifyEmail>;
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,
});
};
@@ -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),
});
+1
View File
@@ -20,4 +20,5 @@ export type EmailVerifyCode = {
};
verificationEmailCode: string;
isVerifyingEmail?: boolean;
hideLogo?: boolean;
};
@@ -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 (
<BaseEmailHtml
hideLogo={Boolean(props.calEvent.platformClientId)}
hideLogo={Boolean(props.calEvent.platformClientId) || Boolean(props.calEvent.hideBranding)}
headerType={props.headerType || "checkCircle"}
subject={props.subject || subject}
title={t(
@@ -1,5 +1,8 @@
import { APP_NAME, SENDER_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import {
APP_NAME,
SENDER_NAME,
SUPPORT_MAIL_ADDRESS,
} from "@calcom/lib/constants";
import type { EmailVerifyCode } from "../../lib/types/email-types";
import { BaseEmailHtml } from "../components";
@@ -8,15 +11,23 @@ export const VerifyEmailByCode = (
) => {
return (
<BaseEmailHtml
subject={props.language(`verify_email_subject${props.isVerifyingEmail ? "_verifying_email" : ""}`, {
appName: APP_NAME,
})}>
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 }
)
}
>
<p
style={{
fontWeight: 600,
fontSize: "32px",
lineHeight: "38px",
}}>
}}
>
<>{props.language("verify_email_email_header")}</>
</p>
<p style={{ fontWeight: 400 }}>
@@ -31,20 +42,27 @@ export const VerifyEmailByCode = (
</>
</p>
</div>
<div style={{ lineHeight: "6px" }}>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<>
{props.language("happy_scheduling")}, <br />
<a
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
style={{ color: "#3E3E3E" }}
target="_blank"
rel="noreferrer">
<>{props.language("the_calcom_team", { companyName: SENDER_NAME })}</>
</a>
</>
</p>
</div>
{!props.hideLogo && (
<div style={{ lineHeight: "6px" }}>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<>
{props.language("happy_scheduling")}, <br />
<a
href={`mailto:${SUPPORT_MAIL_ADDRESS}`}
style={{ color: "#3E3E3E" }}
target="_blank"
rel="noreferrer"
>
<>
{props.language("the_calcom_team", {
companyName: SENDER_NAME,
})}
</>
</a>
</>
</p>
</div>
)}
</BaseEmailHtml>
);
};
@@ -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, "");
}
}
+27 -4
View File
@@ -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<string, string>(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 (
@@ -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 };
@@ -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,
@@ -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,
@@ -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 };
@@ -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,
@@ -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: {
@@ -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,
};
@@ -61,6 +61,7 @@ vi.mock("@calcom/features/ee/teams/repositories/TeamRepository", () => ({
vi.mock("@calcom/prisma", () => ({
default: {},
prisma: {},
}));
describe("handleNoShowFee", () => {
@@ -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) {
@@ -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,
@@ -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 } },
},
},
},
+4 -2
View File
@@ -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,
};
@@ -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 {
@@ -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,
@@ -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<EventTypeService>(eventTypeServiceModule.token);
}
@@ -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";
@@ -0,0 +1,4 @@
export const EVENT_TYPE_DI_TOKENS = {
EVENT_TYPE_SERVICE: Symbol("EventTypeService"),
EVENT_TYPE_SERVICE_MODULE: Symbol("EventTypeServiceModule"),
};
@@ -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,
@@ -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> = {}): 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 });
});
});
});
});
@@ -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<boolean> {
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,
});
}
}
+26 -2
View File
@@ -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),
@@ -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);
});
});
});
+1
View File
@@ -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",
@@ -401,6 +401,7 @@ const BookerPlatformWrapperComponent = (
name: bookerForm.formName,
requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification,
onVerifyEmail: bookerForm.beforeVerifyEmail,
eventTypeId: event?.data?.id,
});
const verifyCode = useVerifyCode({
@@ -15,6 +15,7 @@ export interface IUseVerifyEmailProps {
onVerifyEmail?: () => void;
name?: string | { firstName: string; lastname?: string };
requiresBookerEmailVerification?: boolean;
eventTypeId?: number;
}
export type UseVerifyEmailReturnType = ReturnType<typeof useVerifyEmail>;
@@ -22,6 +23,7 @@ export type UseVerifyEmailReturnType = ReturnType<typeof useVerifyEmail>;
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,
});
};
@@ -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);
@@ -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,
});
};
@@ -5,6 +5,7 @@ export type TSendVerifyEmailCodeSchema = {
username?: string;
language: string;
isVerifyingEmail?: boolean;
eventTypeId?: number;
};
export const ZSendVerifyEmailCodeSchema: z.ZodType<TSendVerifyEmailCodeSchema> = z.object({
@@ -12,4 +13,5 @@ export const ZSendVerifyEmailCodeSchema: z.ZodType<TSendVerifyEmailCodeSchema> =
username: z.string().optional(),
language: z.string(),
isVerifyingEmail: z.boolean().optional(),
eventTypeId: z.number().optional(),
});
@@ -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<TrpcSessionUser>,
"id" | "email" | "organizationId" | "uuid"
> &
export type TUser = Pick<NonNullable<TrpcSessionUser>, "id" | "email" | "organizationId" | "uuid"> &
Partial<Pick<NonNullable<TrpcSessionUser>, "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<void> {
export async function validateUserPermissions(booking: Booking, user: TUser): Promise<void> {
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<Map<string, boolean>> {
async function getEmailVerificationRequirements(guestEmails: string[]): Promise<Map<string, boolean>> {
const userRepo = new UserRepository(prisma);
const guestUsers =
await userRepo.findManyByEmailsWithEmailVerificationSettings({
emails: guestEmails,
});
const guestUsers = await userRepo.findManyByEmailsWithEmailVerificationSettings({
emails: guestEmails,
});
const emailToRequiresVerification = new Map<string, boolean>();
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<ReturnType<typeof prepareAttendeesList>>
): Promise<CalendarEvent> {
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<void> {
export async function updateCalendarEvent(booking: Booking, evt: CalendarEvent): Promise<void> {
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,
@@ -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),
});
+1
View File
@@ -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;