diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 13c86760c4..e661634892 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx b/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx index 643d630d48..1597bc4f57 100644 --- a/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx @@ -17,6 +17,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ const { getAppData, setAppData, disabled } = useAppContextWithSchema(); 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 ? ( -
- { - setAppData("roundRobinLeadSkip", checked); - if (checked) { - // temporary solution, enabled should always be already set - setAppData("enabled", checked); - } - }} - /> - -
- ) : null} +
+ { + setAppData("skipContactCreation", checked); + }} + /> +
+ {eventType.schedulingType === SchedulingType.ROUND_ROBIN ? ( +
+ { + setAppData("roundRobinLeadSkip", checked); + if (checked) { + // temporary solution, enabled should always be already set + setAppData("enabled", checked); + } + }} + /> + +
+ ) : null} ); }; diff --git a/packages/app-store/salesforce/zod.ts b/packages/app-store/salesforce/zod.ts index 5228818b87..0d0e11352f 100644 --- a/packages/app-store/salesforce/zod.ts +++ b/packages/app-store/salesforce/zod.ts @@ -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({ diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index d6ddcc2839..1b4b587972 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -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 = (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) { 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) { + 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; + } } diff --git a/packages/core/crmManager/crmManager.ts b/packages/core/crmManager/crmManager.ts index 35a9977cc0..e97366ca49 100644 --- a/packages/core/crmManager/crmManager.ts +++ b/packages/core/crmManager/crmManager.ts @@ -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) diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index bdeb65fc9a..0f964dd50f 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -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; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 6888de8adb..a265239e61 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -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; diff --git a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts index b6f1bf354e..9a6fd8b17e 100644 --- a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts +++ b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts @@ -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); diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index 720b4c04dd..fa07ee1dc8 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -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; diff --git a/packages/lib/payment/handlePaymentSuccess.ts b/packages/lib/payment/handlePaymentSuccess.ts index 90264c7b4d..1baa03ac00 100644 --- a/packages/lib/payment/handlePaymentSuccess.ts +++ b/packages/lib/payment/handlePaymentSuccess.ts @@ -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 }; }