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:
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
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
+41
-22
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
Vendored
+1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user