feat: Salesforce - Disable creating new contacts (#15780)

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Joe Au-Yeung
2024-07-18 10:50:42 +00:00
committed by GitHub
co-authored by zomars
parent f0464e72c7
commit 218f6d6eaf
10 changed files with 72 additions and 29 deletions
@@ -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>
);
};
+1
View File
@@ -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({
+31 -2
View File
@@ -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;
}
}
+3 -1
View File
@@ -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);
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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 };
}