feat: Salesforce - Disable creating new contacts (#15780)
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
@@ -2497,6 +2497,7 @@
|
||||
"skip_writing_to_calendar": "Do not write to the ICS feed",
|
||||
"rescheduling_not_possible": "Rescheduling is not possible as the event has expired",
|
||||
"event_expired": "This event is expired",
|
||||
"skip_contact_creation": "Skip creating contacts if they do not exist in {{appName}} ",
|
||||
"skip_writing_to_calendar_note": "If your ICS link is read-only (e.g., Proton Calendar), check the box above to avoid errors. You'll also need to manually update your calendar for changes.",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
const isRoundRobinLeadSkipEnabled = getAppData("roundRobinLeadSkip");
|
||||
const isSkipContactCreationEnabled = getAppData("skipContactCreation");
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
@@ -30,24 +31,34 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
switchChecked={enabled}
|
||||
hideSettingsIcon>
|
||||
<>
|
||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN ? (
|
||||
<div>
|
||||
<Switch
|
||||
label={t("skip_rr_assignment_label")}
|
||||
labelOnLeading
|
||||
checked={isRoundRobinLeadSkipEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setAppData("roundRobinLeadSkip", checked);
|
||||
if (checked) {
|
||||
// temporary solution, enabled should always be already set
|
||||
setAppData("enabled", checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Alert className="mt-2" severity="neutral" title={t("skip_rr_description")} />
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<Switch
|
||||
label={t("skip_contact_creation", { appName: "Salesforce" })}
|
||||
labelOnLeading
|
||||
checked={isSkipContactCreationEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setAppData("skipContactCreation", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN ? (
|
||||
<div className="mt-4">
|
||||
<Switch
|
||||
label={t("skip_rr_assignment_label")}
|
||||
labelOnLeading
|
||||
checked={isRoundRobinLeadSkipEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setAppData("roundRobinLeadSkip", checked);
|
||||
if (checked) {
|
||||
// temporary solution, enabled should always be already set
|
||||
setAppData("enabled", checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Alert className="mt-2" severity="neutral" title={t("skip_rr_description")} />
|
||||
</div>
|
||||
) : null}
|
||||
</AppCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||
|
||||
export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
roundRobinLeadSkip: z.boolean().optional(),
|
||||
skipContactCreation: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
|
||||
@@ -21,6 +21,7 @@ import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import { createdEventSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { EventTypeAppMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { Event } from "@calcom/types/Event";
|
||||
@@ -44,6 +45,13 @@ interface HasId {
|
||||
id: number;
|
||||
}
|
||||
|
||||
// The options should have the slug of the apps the option is enabled for
|
||||
interface AppOptions {
|
||||
crm: {
|
||||
skipContactCreation: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const latestCredentialFirst = <T extends HasId>(a: T, b: T) => {
|
||||
return b.id - a.id;
|
||||
};
|
||||
@@ -92,13 +100,14 @@ export default class EventManager {
|
||||
calendarCredentials: CredentialPayload[];
|
||||
videoCredentials: CredentialPayload[];
|
||||
crmCredentials: CredentialPayload[];
|
||||
appOptions: AppOptions;
|
||||
|
||||
/**
|
||||
* Takes an array of credentials and initializes a new instance of the EventManager.
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
constructor(user: EventManagerUser) {
|
||||
constructor(user: EventManagerUser, eventTypeAppMetadata?: z.infer<typeof EventTypeAppMetadataSchema>) {
|
||||
log.silly("Initializing EventManager", safeStringify({ user: getPiiFreeUser(user) }));
|
||||
const appCredentials = getApps(user.credentials, true).flatMap((app) =>
|
||||
app.credentials.map((creds) => ({ ...creds, appName: app.name }))
|
||||
@@ -122,6 +131,8 @@ export default class EventManager {
|
||||
this.crmCredentials = appCredentials.filter(
|
||||
(cred) => cred.type.endsWith("_crm") || cred.type.endsWith("_other_calendar")
|
||||
);
|
||||
|
||||
this.appOptions = this.generateAppOptions(eventTypeAppMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -951,7 +962,8 @@ export default class EventManager {
|
||||
const crm = new CrmManager(credential);
|
||||
|
||||
let success = true;
|
||||
const createdEvent = await crm.createEvent(event).catch((error) => {
|
||||
const skipContactCreation = this.appOptions.crm.skipContactCreation.includes(credential.appId || "");
|
||||
const createdEvent = await crm.createEvent(event, skipContactCreation).catch((error) => {
|
||||
success = false;
|
||||
log.warn(`Error creating crm event for ${credential.type}`, error);
|
||||
});
|
||||
@@ -1008,4 +1020,21 @@ export default class EventManager {
|
||||
await crm.deleteEvent(reference.uid);
|
||||
}
|
||||
}
|
||||
|
||||
private generateAppOptions(eventTypeAppMetadata?: z.infer<typeof EventTypeAppMetadataSchema>) {
|
||||
const appOptions: AppOptions = {
|
||||
crm: {
|
||||
skipContactCreation: [],
|
||||
},
|
||||
};
|
||||
|
||||
if (eventTypeAppMetadata) {
|
||||
for (const key in eventTypeAppMetadata) {
|
||||
const app = eventTypeAppMetadata[key as keyof typeof eventTypeAppMetadata];
|
||||
if (app?.skipContactCreation) appOptions.crm.skipContactCreation.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return appOptions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class CrmManager {
|
||||
return crmService;
|
||||
}
|
||||
|
||||
public async createEvent(event: CalendarEvent) {
|
||||
public async createEvent(event: CalendarEvent, skipContactCreation?: boolean) {
|
||||
const crmService = await this.getCrmService(this.credential);
|
||||
// First see if the attendees already exist in the crm
|
||||
let contacts = (await this.getContacts(event.attendees.map((a) => a.email))) || [];
|
||||
@@ -33,6 +33,8 @@ export default class CrmManager {
|
||||
if (contacts.length == event.attendees.length) {
|
||||
return await crmService?.createEvent(event, contacts);
|
||||
}
|
||||
|
||||
if (skipContactCreation) return;
|
||||
// Figure out which contacts to create
|
||||
const contactsToCreate = event.attendees.filter(
|
||||
(attendee) => !contacts.some((contact) => contact.email === attendee.email)
|
||||
|
||||
@@ -63,12 +63,13 @@ export async function handleConfirmation(args: {
|
||||
paid?: boolean;
|
||||
}) {
|
||||
const { user, evt, recurringEventId, prisma, bookingId, booking, paid } = args;
|
||||
const eventManager = new EventManager(user);
|
||||
const eventType = booking.eventType;
|
||||
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});
|
||||
const eventManager = new EventManager(user, eventTypeMetadata?.apps);
|
||||
const scheduleResult = await eventManager.create(evt);
|
||||
const results = scheduleResult.results;
|
||||
const metadata: AdditionalInformation = {};
|
||||
|
||||
const eventType = booking.eventType;
|
||||
const workflows = await getAllWorkflowsFromEventType(eventType, booking.userId);
|
||||
|
||||
if (results.length > 0 && results.every((res) => !res.success)) {
|
||||
@@ -87,7 +88,7 @@ export async function handleConfirmation(args: {
|
||||
}
|
||||
try {
|
||||
const eventType = booking.eventType;
|
||||
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});
|
||||
|
||||
let isHostConfirmationEmailsDisabled = false;
|
||||
let isAttendeeConfirmationEmailDisabled = false;
|
||||
|
||||
|
||||
@@ -215,7 +215,6 @@ async function handler(
|
||||
!req.body.eventTypeId && !!req.body.eventTypeSlug
|
||||
? getDefaultEvent(req.body.eventTypeSlug)
|
||||
: await getEventTypesFromDB(req.body.eventTypeId);
|
||||
|
||||
eventType = {
|
||||
...eventType,
|
||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||
@@ -1116,7 +1115,7 @@ async function handler(
|
||||
|
||||
// After polling videoBusyTimes, credentials might have been changed due to refreshment, so query them again.
|
||||
const credentials = await refreshCredentials(allCredentials);
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials });
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials }, eventType?.metadata?.apps);
|
||||
|
||||
let videoCallUrl;
|
||||
|
||||
@@ -1153,7 +1152,6 @@ async function handler(
|
||||
changedOrganizer,
|
||||
previousHostDestinationCalendar
|
||||
);
|
||||
|
||||
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
||||
// to the default description when we are sending the emails.
|
||||
evt.description = eventType.description;
|
||||
|
||||
@@ -142,7 +142,7 @@ const createNewSeat = async (
|
||||
);
|
||||
}
|
||||
const credentials = await refreshCredentials(allCredentials);
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials });
|
||||
const eventManager = new EventManager({ ...organizerUser, credentials }, eventType?.metadata?.apps);
|
||||
await eventManager.updateCalendarAttendees(evt, seatedBooking);
|
||||
|
||||
const foundBooking = await findBookingQuery(seatedBooking.id);
|
||||
|
||||
@@ -72,7 +72,7 @@ const handleSetupSuccess = async (event: Stripe.Event) => {
|
||||
},
|
||||
});
|
||||
if (!requiresConfirmation) {
|
||||
const eventManager = new EventManager(user);
|
||||
const eventManager = new EventManager(user, eventType?.metadata?.apps);
|
||||
const scheduleResult = await eventManager.create(evt);
|
||||
bookingData.references = { create: scheduleResult.referencesToCreate };
|
||||
bookingData.status = BookingStatus.ACCEPTED;
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number)
|
||||
|
||||
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||
if (isConfirmed) {
|
||||
const eventManager = new EventManager(userWithCredentials);
|
||||
const eventManager = new EventManager(userWithCredentials, eventType?.metadata?.apps);
|
||||
const scheduleResult = await eventManager.create(evt);
|
||||
bookingData.references = { create: scheduleResult.referencesToCreate };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user