feat: Salesforce skip ownership check if attendee email is a free domain (#17916)
* Add email domain array * Create numbered email domain object * Check email domain * Rename function * Add tests * Frontend enable skip ownership check if free email domain * Backend ignore adding ownership to return records if free email domain check is enabled * feat: Only require confirmation for free email domains (#17917) * Add requiresConfirmationForFreeEmail to db * Add option to event type settings * Get requiresConfirmationForFreeEmail for event type page * Include requiresConfirmationForFreeEmail in fetching event type * Pass bookerEmail to `getRequiresConfirmationFlags` * Add free email domain check to `determineRequiresConfirmation` * Add `requiresConfirmationForFreeEmail` to types * Add severity to Watchlist table * Add migration for watchlist severity * Add `getEmailDomainInWatchlist` method to watchlist repository * Use watchlist repository to check for free email domain * Mock watchlist repository in test * Update test * Rename method * Add severity to blocked list * Move check free email domain to async * Type checks * Adjust for promise returned * Fix tests * Fix * Fix tests
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { checkIfEmailInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
|
||||
export async function isLockedOrBlocked(req: NextApiRequest) {
|
||||
const user = req.user;
|
||||
if (!user?.email) return false;
|
||||
return user.locked || (await checkIfEmailInWatchlistController(user.email));
|
||||
return user.locked || (await checkIfEmailIsBlockedInWatchlistController(user.email));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, expect, it, beforeEach } from "vitest";
|
||||
|
||||
import { WatchlistSeverity } from "@calcom/prisma/enums";
|
||||
|
||||
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
|
||||
|
||||
describe("isLockedOrBlocked", () => {
|
||||
@@ -16,6 +18,7 @@ describe("isLockedOrBlocked", () => {
|
||||
{
|
||||
type: "DOMAIN",
|
||||
value: "blocked.com",
|
||||
severity: WatchlistSeverity.CRITICAL,
|
||||
createdById: 1,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2851,6 +2851,7 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
|
||||
"salesforce_option": "Salesforce Option",
|
||||
"lookup_field_name": "Lookup Field Name",
|
||||
"salesforce_if_free_email_domain_skip_owner_check": "If attendee has a free email domain, skip the ownership check and round robin as normal",
|
||||
"filter_operator_is": "Is",
|
||||
"filter_operator_is_not": "Is not",
|
||||
"filter_operator_contains": "Contains",
|
||||
@@ -2866,6 +2867,7 @@
|
||||
"rr_distribution_method_availability_description": "Allows bookers to book meetings whenever a host is available. Use this to maximize the number of potential meetings booked and when the even distribution of meetings across hosts is less important.",
|
||||
"rr_distribution_method_balanced_title": "Load balancing",
|
||||
"rr_distribution_method_balanced_description": "We will monitor how many bookings have been made with each host and compare this with others, disabling some hosts that are too far ahead so bookings are evenly distributed.",
|
||||
"require_confirmation_for_free_email": "Only require confirmation for free email providers (Ex. @gmail.com, @outlook.com)",
|
||||
"exclude_emails_that_contain": "Exclude emails that contain ...",
|
||||
"require_emails_that_contain": "Require emails that contain ...",
|
||||
"exclude_emails_match_found_error_message": "Please enter a valid work email address",
|
||||
|
||||
@@ -26,6 +26,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
const isRoundRobinLeadSkipEnabled = getAppData("roundRobinLeadSkip");
|
||||
const roundRobinSkipCheckRecordOn =
|
||||
getAppData("roundRobinSkipCheckRecordOn") ?? SalesforceRecordEnum.CONTACT;
|
||||
const ifFreeEmailDomainSkipOwnerCheck = getAppData("ifFreeEmailDomainSkipOwnerCheck") ?? false;
|
||||
const isSkipContactCreationEnabled = getAppData("skipContactCreation");
|
||||
const createLeadIfAccountNull = getAppData("createLeadIfAccountNull");
|
||||
const createNewContactUnderAccount = getAppData("createNewContactUnderAccount");
|
||||
@@ -497,22 +498,34 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
}}
|
||||
/>
|
||||
{isRoundRobinLeadSkipEnabled ? (
|
||||
<div className="my-4 ml-2">
|
||||
<label className="text-emphasis mb-2 align-text-top text-sm font-medium">
|
||||
{t("salesforce_check_owner_of")}
|
||||
</label>
|
||||
<Select
|
||||
className="mt-2 w-60"
|
||||
options={checkOwnerOptions}
|
||||
value={checkOwnerSelectedOption}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setCheckOwnerSelectedOption(e);
|
||||
setAppData("roundRobinSkipCheckRecordOn", e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="my-4 ml-2">
|
||||
<label className="text-emphasis mb-2 align-text-top text-sm font-medium">
|
||||
{t("salesforce_check_owner_of")}
|
||||
</label>
|
||||
<Select
|
||||
className="mt-2 w-60"
|
||||
options={checkOwnerOptions}
|
||||
value={checkOwnerSelectedOption}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setCheckOwnerSelectedOption(e);
|
||||
setAppData("roundRobinSkipCheckRecordOn", e.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="my-4">
|
||||
<Switch
|
||||
label={t("salesforce_if_free_email_domain_skip_owner_check")}
|
||||
labelOnLeading
|
||||
checked={ifFreeEmailDomainSkipOwnerCheck}
|
||||
onCheckedChange={(checked) => {
|
||||
setAppData("ifFreeEmailDomainSkipOwnerCheck", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
<Alert className="mt-2" severity="neutral" title={t("skip_rr_description")} />
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { getLocation } from "@calcom/lib/CalEventParser";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { checkIfFreeEmailDomain } from "@calcom/lib/freeEmailDomainCheck/checkIfFreeEmailDomain";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
@@ -431,7 +432,10 @@ export default class SalesforceCRMService implements CRM {
|
||||
}
|
||||
|
||||
// Handle owner information
|
||||
if (includeOwner || forRoundRobinSkip) {
|
||||
if (
|
||||
(includeOwner || forRoundRobinSkip) &&
|
||||
!(await this.shouldSkipAttendeeIfFreeEmailDomain(emailArray[0]))
|
||||
) {
|
||||
const ownerIds: Set<string> = new Set();
|
||||
|
||||
if (accountOwnerId) {
|
||||
@@ -1242,4 +1246,12 @@ export default class SalesforceCRMService implements CRM {
|
||||
if (companyValue === onBookingWriteToRecordFields[companyFieldName]) return;
|
||||
return companyValue;
|
||||
}
|
||||
|
||||
private async shouldSkipAttendeeIfFreeEmailDomain(attendeeEmail: string) {
|
||||
const appOptions = this.getAppOptions();
|
||||
if (!appOptions.ifFreeEmailDomainSkipOwnerCheck) return false;
|
||||
|
||||
const response = await checkIfFreeEmailDomain(attendeeEmail);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
.nativeEnum(SalesforceRecordEnum)
|
||||
.default(SalesforceRecordEnum.CONTACT)
|
||||
.optional(),
|
||||
ifFreeEmailDomainSkipOwnerCheck: z.boolean().optional(),
|
||||
skipContactCreation: z.boolean().optional(),
|
||||
createEventOn: z.nativeEnum(SalesforceRecordEnum).default(SalesforceRecordEnum.CONTACT).optional(),
|
||||
createNewContactUnderAccount: z.boolean().optional(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import { createOrUpdateMemberships } from "@calcom/features/auth/signup/utils/createOrUpdateMemberships";
|
||||
import { prefillAvatar } from "@calcom/features/auth/signup/utils/prefillAvatar";
|
||||
import { checkIfEmailInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getLocaleFromRequest } from "@calcom/lib/getLocaleFromRequest";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -39,7 +39,7 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
|
||||
})
|
||||
.parse(req.body);
|
||||
|
||||
const shouldLockByDefault = await checkIfEmailInWatchlistController(_email);
|
||||
const shouldLockByDefault = await checkIfEmailIsBlockedInWatchlistController(_email);
|
||||
|
||||
log.debug("handler", { email: _email });
|
||||
|
||||
|
||||
@@ -759,12 +759,13 @@ async function handler(
|
||||
const tOrganizer = await getTranslation(organizerUser?.locale ?? "en", "common");
|
||||
const allCredentials = await getAllCredentials(organizerUser, eventType);
|
||||
|
||||
const { userReschedulingIsOwner, isConfirmedByDefault } = getRequiresConfirmationFlags({
|
||||
const { userReschedulingIsOwner, isConfirmedByDefault } = await getRequiresConfirmationFlags({
|
||||
eventType,
|
||||
bookingStartTime: reqBody.start,
|
||||
userId,
|
||||
originalRescheduledBookingOrganizerId: originalRescheduledBooking?.user?.id,
|
||||
paymentAppData,
|
||||
bookerEmail,
|
||||
});
|
||||
|
||||
// If the Organizer himself is rescheduling, the booker should be sent the communication in his timezone and locale.
|
||||
|
||||
@@ -56,6 +56,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
periodCountCalendarDays: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
requiresConfirmation: true,
|
||||
requiresConfirmationForFreeEmail: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
maxLeadThreshold: true,
|
||||
minimumBookingNotice: true,
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { checkIfFreeEmailDomain } from "@calcom/lib/freeEmailDomainCheck/checkIfFreeEmailDomain";
|
||||
|
||||
import type { getEventTypeResponse } from "./getEventTypesFromDB";
|
||||
|
||||
type EventType = Pick<getEventTypeResponse, "metadata" | "requiresConfirmation">;
|
||||
type EventType = Pick<
|
||||
getEventTypeResponse,
|
||||
"metadata" | "requiresConfirmation" | "requiresConfirmationForFreeEmail"
|
||||
>;
|
||||
type PaymentAppData = { price: number };
|
||||
|
||||
export function getRequiresConfirmationFlags({
|
||||
export async function getRequiresConfirmationFlags({
|
||||
eventType,
|
||||
bookingStartTime,
|
||||
userId,
|
||||
paymentAppData,
|
||||
originalRescheduledBookingOrganizerId,
|
||||
bookerEmail,
|
||||
}: {
|
||||
eventType: EventType;
|
||||
bookingStartTime: string;
|
||||
userId: number | undefined;
|
||||
paymentAppData: PaymentAppData;
|
||||
originalRescheduledBookingOrganizerId: number | undefined;
|
||||
bookerEmail: string;
|
||||
}) {
|
||||
const requiresConfirmation = determineRequiresConfirmation(eventType, bookingStartTime);
|
||||
const requiresConfirmation = await determineRequiresConfirmation(eventType, bookingStartTime, bookerEmail);
|
||||
const userReschedulingIsOwner = isUserReschedulingOwner(userId, originalRescheduledBookingOrganizerId);
|
||||
const isConfirmedByDefault = determineIsConfirmedByDefault(
|
||||
requiresConfirmation,
|
||||
@@ -38,9 +44,18 @@ export function getRequiresConfirmationFlags({
|
||||
};
|
||||
}
|
||||
|
||||
function determineRequiresConfirmation(eventType: EventType, bookingStartTime: string): boolean {
|
||||
async function determineRequiresConfirmation(
|
||||
eventType: EventType,
|
||||
bookingStartTime: string,
|
||||
bookerEmail: string
|
||||
): Promise<boolean> {
|
||||
let requiresConfirmation = eventType?.requiresConfirmation;
|
||||
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
|
||||
const requiresConfirmationForFreeEmail = eventType?.requiresConfirmationForFreeEmail;
|
||||
|
||||
if (requiresConfirmationForFreeEmail) {
|
||||
requiresConfirmation = await checkIfFreeEmailDomain(bookerEmail);
|
||||
}
|
||||
|
||||
if (rcThreshold) {
|
||||
const timeDifference = dayjs(dayjs(bookingStartTime).utc().format()).diff(dayjs(), rcThreshold.unit);
|
||||
|
||||
+16
@@ -72,6 +72,7 @@ export default function RequiresConfirmationController({
|
||||
);
|
||||
|
||||
const requiresConfirmationWillBlockSlot = formMethods.getValues("requiresConfirmationWillBlockSlot");
|
||||
const requiresConfirmationForFreeEmail = formMethods.getValues("requiresConfirmationForFreeEmail");
|
||||
|
||||
return (
|
||||
<div className="block items-start sm:flex">
|
||||
@@ -241,6 +242,21 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<CheckboxField
|
||||
checked={requiresConfirmationForFreeEmail}
|
||||
descriptionAsLabel
|
||||
description={t("require_confirmation_for_free_email")}
|
||||
className={customClassNames?.conditionalConfirmationRadio?.checkbox}
|
||||
descriptionClassName={
|
||||
customClassNames?.conditionalConfirmationRadio?.checkboxDescription
|
||||
}
|
||||
onChange={(e) => {
|
||||
// We set should dirty to properly detect when we can submit the form
|
||||
formMethods.setValue("requiresConfirmationForFreeEmail", e.target.checked, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -81,6 +81,7 @@ export type FormValues = {
|
||||
lockTimeZoneToggleOnBookingPage: boolean;
|
||||
requiresConfirmation: boolean;
|
||||
requiresConfirmationWillBlockSlot: boolean;
|
||||
requiresConfirmationForFreeEmail: boolean;
|
||||
requiresBookerEmailVerification: boolean;
|
||||
recurringEvent: RecurringEvent | null;
|
||||
schedulingType: SchedulingType | null;
|
||||
|
||||
@@ -21,13 +21,13 @@ function presenter(watchlistedEmail: Watchlist | null) {
|
||||
* to the specific use cases. Controllers orchestrate Use Cases. They don't implement any
|
||||
* logic, but define the whole operations using use cases.
|
||||
*/
|
||||
export async function checkIfEmailInWatchlistController(
|
||||
export async function checkIfEmailIsBlockedInWatchlistController(
|
||||
email: string
|
||||
): Promise<ReturnType<typeof presenter>> {
|
||||
return await startSpan({ name: "checkIfEmailInWatchlist Controller" }, async () => {
|
||||
const lowercasedEmail = email.toLowerCase();
|
||||
const watchlistRepository = new WatchlistRepository();
|
||||
const watchlistedEmail = await watchlistRepository.getEmailInWatchlist(lowercasedEmail);
|
||||
const watchlistedEmail = await watchlistRepository.getBlockedEmailInWatchlist(lowercasedEmail);
|
||||
return presenter(watchlistedEmail);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Watchlist } from "./watchlist.model";
|
||||
|
||||
export interface IWatchlistRepository {
|
||||
getEmailInWatchlist(email: string): Promise<Watchlist | null>;
|
||||
getBlockedEmailInWatchlist(email: string): Promise<Watchlist | null>;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { captureException } from "@sentry/nextjs";
|
||||
|
||||
import db from "@calcom/prisma";
|
||||
import { WatchlistType } from "@calcom/prisma/enums";
|
||||
import { WatchlistType, WatchlistSeverity } from "@calcom/prisma/enums";
|
||||
|
||||
import type { IWatchlistRepository } from "./watchlist.repository.interface";
|
||||
|
||||
export class WatchlistRepository implements IWatchlistRepository {
|
||||
async getEmailInWatchlist(email: string) {
|
||||
async getBlockedEmailInWatchlist(email: string) {
|
||||
const [, domain] = email.split("@");
|
||||
try {
|
||||
const emailInWatchlist = await db.watchlist.findFirst({
|
||||
where: {
|
||||
severity: WatchlistSeverity.CRITICAL,
|
||||
OR: [
|
||||
{ type: WatchlistType.EMAIL, value: email },
|
||||
{ type: WatchlistType.DOMAIN, value: domain },
|
||||
@@ -23,4 +24,19 @@ export class WatchlistRepository implements IWatchlistRepository {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getFreeEmailDomainInWatchlist(emailDomain: string) {
|
||||
try {
|
||||
const domainInWatchWatchlist = await db.watchlist.findFirst({
|
||||
where: {
|
||||
type: WatchlistType.DOMAIN,
|
||||
value: emailDomain,
|
||||
},
|
||||
});
|
||||
return domainInWatchWatchlist;
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ const commons = {
|
||||
team: null,
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresConfirmation: false,
|
||||
requiresConfirmationForFreeEmail: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
bookingLimits: null,
|
||||
durationLimits: null,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { WatchlistRepository } from "@calcom/features/watchlist/watchlist.repository";
|
||||
|
||||
import { checkIfFreeEmailDomain } from "./checkIfFreeEmailDomain";
|
||||
|
||||
describe("checkIfFreeEmailDomain", () => {
|
||||
test("If gmail should return true", async () => {
|
||||
expect(await checkIfFreeEmailDomain("test@gmail.com")).toBe(true);
|
||||
});
|
||||
test("If outlook should return true", async () => {
|
||||
expect(await checkIfFreeEmailDomain("test@outlook.com")).toBe(true);
|
||||
});
|
||||
test("If work email, should return false", async () => {
|
||||
const spy = vi.spyOn(WatchlistRepository.prototype, "getFreeEmailDomainInWatchlist");
|
||||
spy.mockImplementation(() => {
|
||||
return null;
|
||||
});
|
||||
expect(await checkIfFreeEmailDomain("test@cal.com")).toBe(false);
|
||||
});
|
||||
test("If free email domain in watchlist, should return true", async () => {
|
||||
const spy = vi.spyOn(WatchlistRepository.prototype, "getFreeEmailDomainInWatchlist");
|
||||
spy.mockImplementation(() => {
|
||||
return { value: "freedomain.com" };
|
||||
});
|
||||
expect(await checkIfFreeEmailDomain("test@freedomain.com")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { WatchlistRepository } from "@calcom/features/watchlist/watchlist.repository";
|
||||
|
||||
export const checkIfFreeEmailDomain = async (email: string) => {
|
||||
const emailDomain = email.split("@")[1].toLowerCase();
|
||||
// If there's no email domain return as if it was a free email domain
|
||||
if (!emailDomain) return true;
|
||||
|
||||
// Gmail and Outlook are one of the most common email domains so we don't need to check the domains list
|
||||
if (emailDomain === "gmail.com" || emailDomain === "outlook.com") return true;
|
||||
|
||||
// Check if email domain is in the watchlist
|
||||
const watchlistRepository = new WatchlistRepository();
|
||||
return !!(await watchlistRepository.getFreeEmailDomainInWatchlist(emailDomain));
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ export const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
|
||||
length: true,
|
||||
title: true,
|
||||
requiresConfirmation: true,
|
||||
requiresConfirmationForFreeEmail: true,
|
||||
requiresConfirmationWillBlockSlot: true,
|
||||
autoTranslateDescriptionEnabled: true,
|
||||
position: true,
|
||||
|
||||
@@ -444,6 +444,7 @@ export class EventTypeRepository {
|
||||
periodCountCalendarDays: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
requiresConfirmation: true,
|
||||
requiresConfirmationForFreeEmail: true,
|
||||
requiresConfirmationWillBlockSlot: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
autoTranslateDescriptionEnabled: true,
|
||||
|
||||
@@ -109,6 +109,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
|
||||
recurringEvent: null,
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresConfirmation: false,
|
||||
requiresConfirmationForFreeEmail: false,
|
||||
requiresConfirmationWillBlockSlot: false,
|
||||
disableGuests: false,
|
||||
hideCalendarNotes: false,
|
||||
|
||||
@@ -88,6 +88,7 @@ export const useEventTypeForm = ({
|
||||
schedulingType: eventType.schedulingType,
|
||||
requiresConfirmation: eventType.requiresConfirmation,
|
||||
requiresConfirmationWillBlockSlot: eventType.requiresConfirmationWillBlockSlot,
|
||||
requiresConfirmationForFreeEmail: eventType.requiresConfirmationForFreeEmail,
|
||||
slotInterval: eventType.slotInterval,
|
||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||
metadata: eventType.metadata,
|
||||
@@ -188,6 +189,7 @@ export const useEventTypeForm = ({
|
||||
};
|
||||
|
||||
const getDirtyFields = (values: FormValues): Partial<FormValues> => {
|
||||
console.log("🚀 ~ getDirtyFields ~ values:", values);
|
||||
if (!isFormDirty) {
|
||||
return {};
|
||||
}
|
||||
@@ -368,6 +370,7 @@ export const useEventTypeForm = ({
|
||||
}, {}) as EventTypeUpdateInput;
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
console.log("🚀 ~ handleSubmit ~ filteredPayload:", filteredPayload);
|
||||
onSubmit({ ...filteredPayload, id: eventType.id });
|
||||
}
|
||||
};
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "EventType" ADD COLUMN "requiresConfirmationForFreeEmail" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "WatchlistSeverity" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Watchlist" ADD COLUMN "severity" "WatchlistSeverity" NOT NULL DEFAULT 'LOW';
|
||||
@@ -109,6 +109,7 @@ model EventType {
|
||||
lockTimeZoneToggleOnBookingPage Boolean @default(false)
|
||||
requiresConfirmation Boolean @default(false)
|
||||
requiresConfirmationWillBlockSlot Boolean @default(false)
|
||||
requiresConfirmationForFreeEmail Boolean @default(false)
|
||||
requiresBookerEmailVerification Boolean @default(false)
|
||||
autoTranslateDescriptionEnabled Boolean @default(false)
|
||||
/// @zod.custom(imports.recurringEventType)
|
||||
@@ -1738,18 +1739,26 @@ enum WatchlistType {
|
||||
USERNAME
|
||||
}
|
||||
|
||||
enum WatchlistSeverity {
|
||||
LOW
|
||||
MEDIUM
|
||||
HIGH
|
||||
CRITICAL
|
||||
}
|
||||
|
||||
model Watchlist {
|
||||
id String @id @unique @default(cuid())
|
||||
id String @id @unique @default(cuid())
|
||||
type WatchlistType
|
||||
// The identifier of the Watchlisted entity (email or domain)
|
||||
value String
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User @relation("CreatedWatchlists", onDelete: Cascade, fields: [createdById], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User @relation("CreatedWatchlists", onDelete: Cascade, fields: [createdById], references: [id])
|
||||
createdById Int
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation("UpdatedWatchlists", onDelete: SetNull, fields: [updatedById], references: [id])
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedBy User? @relation("UpdatedWatchlists", onDelete: SetNull, fields: [updatedById], references: [id])
|
||||
updatedById Int?
|
||||
severity WatchlistSeverity @default(LOW)
|
||||
|
||||
@@unique([type, value])
|
||||
@@index([type, value])
|
||||
|
||||
@@ -691,6 +691,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
|
||||
customInputs: true,
|
||||
disableGuests: true,
|
||||
requiresConfirmation: true,
|
||||
requiresConfirmationForFreeEmail: true,
|
||||
requiresConfirmationWillBlockSlot: true,
|
||||
eventName: true,
|
||||
metadata: true,
|
||||
|
||||
Reference in New Issue
Block a user