From b3d5a6edcab46e0232e8b7bc0ab2f825f0d32fe2 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:40:05 -0400 Subject: [PATCH] feat: Salesforce - on booking cancel, write to event object (#20601) * Type app options * Refactor `writeToPersonRecord` to `writeToRecord` * Abstract writing to salesforce record options component * Add option to write to event object on cancellation * Add on booking cancel write to event object to schema * Refactor writing to record on booking option to use `` * Pass event object to crm delete method * When cancelling booking write to event object - App data isn't being read * V1 pass app data to crm service * Undo .env commit * Fix passing event to cancel crm cancel event method * Pass event type metadata to `EventManager` in `handleCancelBooking` * Handle writing to event record * Type fix --- apps/web/public/static/locales/en/common.json | 2 + .../components/EventTypeAppCardInterface.tsx | 273 ++--------------- .../components/WriteToObjectSettings.tsx | 283 ++++++++++++++++++ .../app-store/salesforce/lib/CrmService.ts | 147 +++++---- packages/app-store/salesforce/lib/enums.ts | 2 + packages/app-store/salesforce/zod.ts | 4 +- .../bookings/lib/handleCancelBooking.ts | 13 +- packages/lib/EventManager.ts | 9 +- packages/lib/crmManager/crmManager.ts | 4 +- packages/types/CrmService.d.ts | 2 +- 10 files changed, 425 insertions(+), 314 deletions(-) create mode 100644 packages/app-store/salesforce/components/components/WriteToObjectSettings.tsx diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index a9d7bacbdf..609940cf57 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3076,5 +3076,7 @@ "enable_delegation_credential_description": "Grant Cal.com automatic access to the calendars of all organization members by enabling delegation credential.", "disable_delegation_credential": "Disable Delegation Credential", "disable_delegation_credential_description": "Once delegation credential is disabled, organization members who haven’t connected their calendars will need to do so manually.", + "salesforce_on_cancel_write_to_event": "On cancelled booking, write to event record instead of deleting event", + "salesforce_on_every_cancellation": "On every cancellation", "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 f7dcec18e7..c280d52f5f 100644 --- a/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx @@ -1,6 +1,5 @@ import { usePathname } from "next/navigation"; import { useState } from "react"; -import type z from "zod"; import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; @@ -16,13 +15,9 @@ import { Select } from "@calcom/ui/components/form"; import { Switch } from "@calcom/ui/components/form"; import { showToast } from "@calcom/ui/components/toast"; -import { - SalesforceRecordEnum, - WhenToWriteToRecord, - SalesforceFieldType, - DateFieldTypeData, -} from "../lib/enums"; -import type { appDataSchema, writeToRecordEntrySchema } from "../zod"; +import { SalesforceRecordEnum } from "../lib/enums"; +import type { appDataSchema } from "../zod"; +import WriteToObjectSettings, { BookingActionEnum } from "./components/WriteToObjectSettings"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const pathname = usePathname(); @@ -48,6 +43,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ const onBookingWriteToRecordFields = getAppData("onBookingWriteToRecordFields") ?? {}; const ignoreGuests = getAppData("ignoreGuests") ?? false; const roundRobinSkipFallbackToLeadOwner = getAppData("roundRobinSkipFallbackToLeadOwner") ?? false; + const onCancelWriteToEventRecord = getAppData("onCancelWriteToEventRecord") ?? false; + const onCancelWriteToEventRecordFields = getAppData("onCancelWriteToEventRecordFields") ?? {}; const { t } = useLocale(); @@ -60,49 +57,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ recordOptions.find((option) => option.value === createEventOn) ?? recordOptions[0] ); - const fieldTypeOptions = [ - { label: t("text"), value: SalesforceFieldType.TEXT }, - { label: t("date"), value: SalesforceFieldType.DATE }, - { label: t("phone").charAt(0).toUpperCase() + t("phone").slice(1), value: SalesforceFieldType.PHONE }, - { label: t("checkbox"), value: SalesforceFieldType.CHECKBOX }, - { label: t("picklist"), value: SalesforceFieldType.PICKLIST }, - { label: t("custom"), value: SalesforceFieldType.CUSTOM }, - ]; - - const [writeToPersonObjectFieldType, setWriteToPersonObjectFieldType] = useState(fieldTypeOptions[0]); - - const whenToWriteToRecordOptions = [ - { label: t("on_every_booking"), value: WhenToWriteToRecord.EVERY_BOOKING }, - { label: t("only_if_field_is_empty"), value: WhenToWriteToRecord.FIELD_EMPTY }, - ]; - - const [whenToWriteToPersonRecord, setWhenToWriteToPersonRecord] = useState( - whenToWriteToRecordOptions.find((option) => option.value === WhenToWriteToRecord.FIELD_EMPTY) ?? - whenToWriteToRecordOptions[0] - ); - - const dateFieldValueOptions = [ - { label: t("booking_start_date"), value: DateFieldTypeData.BOOKING_START_DATE }, - { label: t("booking_created_date"), value: DateFieldTypeData.BOOKING_CREATED_DATE }, - ]; - - const checkboxFieldValueOptions = [ - { label: t("true"), value: true }, - { label: t("false"), value: false }, - ]; - - const [dateFieldValue, setDateValue] = useState(dateFieldValueOptions[0]); - const [checkboxFieldValue, setCheckboxFieldValue] = useState(checkboxFieldValueOptions[0]); - - const [newOnBookingWriteToPersonObjectField, setNewOnBookingWriteToPersonObjectField] = useState< - z.infer - >({ - field: "", - fieldType: writeToPersonObjectFieldType.value, - value: "", - whenToWrite: WhenToWriteToRecord.FIELD_EMPTY, - }); - const checkOwnerOptions = [ { label: t("contact"), value: SalesforceRecordEnum.CONTACT }, { label: t("salesforce_lead"), value: SalesforceRecordEnum.LEAD }, @@ -306,206 +260,16 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
- { + { setAppData("onBookingWriteToRecord", checked); }} + updateWriteToObjectData={(data) => setAppData("onBookingWriteToRecordFields", data)} /> - {onBookingWriteToRecord ? ( -
-
-
{t("field_name")}
-
{t("field_type")}
-
{t("value")}
-
{t("when_to_write")}
-
-
- {Object.keys(onBookingWriteToRecordFields).map((key) => ( -
-
- -
-
- option.value === onBookingWriteToRecordFields[key].value - )} - isDisabled={true} - /> - ) : onBookingWriteToRecordFields[key].fieldType === SalesforceFieldType.CHECKBOX ? ( - option.value === onBookingWriteToRecordFields[key].whenToWrite - )} - isDisabled={true} - /> -
-
-
-
- ))} -
-
- - setNewOnBookingWriteToPersonObjectField({ - ...newOnBookingWriteToPersonObjectField, - field: e.target.value, - }) - } - /> -
-
- { - if (e) { - setDateValue(e); - setNewOnBookingWriteToPersonObjectField({ - ...newOnBookingWriteToPersonObjectField, - value: e.value, - }); - } - }} - /> - ) : writeToPersonObjectFieldType.value === SalesforceFieldType.CHECKBOX ? ( - { - if (e) { - setWhenToWriteToPersonRecord(e); - setNewOnBookingWriteToPersonObjectField({ - ...newOnBookingWriteToPersonObjectField, - whenToWrite: e.value, - }); - } - }} - /> -
-
-
- -
- ) : null}
@@ -588,6 +352,19 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
) : null} +
+ { + setAppData("onCancelWriteToEventRecord", checked); + }} + updateWriteToObjectData={(data) => setAppData("onCancelWriteToEventRecordFields", data)} + /> +
+
>; + +const WriteToObjectSettings = ({ + bookingAction, + optionLabel, + optionEnabled, + optionSwitchOnChange, + writeToObjectData, + updateWriteToObjectData, +}: { + bookingAction: BookingActionEnum; + optionLabel: string; + optionEnabled: boolean; + optionSwitchOnChange: (checked: boolean) => void; + writeToObjectData: iWriteToObjectData; + updateWriteToObjectData: (data: iWriteToObjectData) => void; +}) => { + const { t } = useLocale(); + + const fieldTypeOptions = [ + { label: t("text"), value: SalesforceFieldType.TEXT }, + { label: t("date"), value: SalesforceFieldType.DATE }, + { label: t("phone").charAt(0).toUpperCase() + t("phone").slice(1), value: SalesforceFieldType.PHONE }, + { label: t("checkbox"), value: SalesforceFieldType.CHECKBOX }, + { label: t("picklist"), value: SalesforceFieldType.PICKLIST }, + { label: t("custom"), value: SalesforceFieldType.CUSTOM }, + ]; + + const whenToWriteToRecordOptions = [ + { label: t("only_if_field_is_empty"), value: WhenToWriteToRecord.FIELD_EMPTY }, + ...(bookingAction === BookingActionEnum.ON_CANCEL + ? [{ label: t("salesforce_on_every_cancellation"), value: WhenToWriteToRecord.EVERY_BOOKING }] + : [{ label: t("on_every_booking"), value: WhenToWriteToRecord.EVERY_BOOKING }]), + ]; + + const checkboxFieldValueOptions = [ + { label: t("true"), value: true }, + { label: t("false"), value: false }, + ]; + + const dateFieldValueOptions = [ + ...(bookingAction === BookingActionEnum.ON_CANCEL + ? [{ label: t("booking_cancel_date"), value: DateFieldTypeData.BOOKING_CANCEL_DATE }] + : []), + { label: t("booking_start_date"), value: DateFieldTypeData.BOOKING_START_DATE }, + { label: t("booking_created_date"), value: DateFieldTypeData.BOOKING_CREATED_DATE }, + ]; + + const [fieldTypeSelectedOption, setFieldTypeSelectedOption] = useState(fieldTypeOptions[0]); + const [dateFieldSelectedOption, setDateFieldSelectedOption] = useState(dateFieldValueOptions[0]); + const [checkboxFieldSelectedOption, setCheckboxFieldSelectedOption] = useState( + checkboxFieldValueOptions[0] + ); + const [whenToWriteSelectedOption, setWhenToWriteSelectedOption] = useState(whenToWriteToRecordOptions[0]); + const [newOnWriteToRecordEntry, setNewOnWriteToRecordEntry] = useState< + z.infer + >({ + field: "", + fieldType: fieldTypeSelectedOption.value, + value: "", + whenToWrite: whenToWriteSelectedOption.value, + }); + + return ( + <> + + {optionEnabled ? ( +
+
+
{t("field_name")}
+
{t("field_type")}
+
{t("value")}
+
{t("when_to_write")}
+
+
+ {Object.keys(writeToObjectData).map((key) => ( +
+
+ +
+
+ option.value === writeToObjectData[key].value + )} + isDisabled={true} + /> + ) : writeToObjectData[key].fieldType === SalesforceFieldType.CHECKBOX ? ( + option.value === writeToObjectData[key].whenToWrite + )} + isDisabled={true} + /> +
+
+
+
+ ))} +
+
+ + setNewOnWriteToRecordEntry({ + ...newOnWriteToRecordEntry, + field: e.target.value, + }) + } + /> +
+
+ { + if (e) { + setDateFieldSelectedOption(e); + setNewOnWriteToRecordEntry({ + ...newOnWriteToRecordEntry, + value: e.value, + }); + } + }} + /> + ) : newOnWriteToRecordEntry.fieldType === SalesforceFieldType.CHECKBOX ? ( + { + if (e) { + setWhenToWriteSelectedOption(e); + setNewOnWriteToRecordEntry({ + ...newOnWriteToRecordEntry, + whenToWrite: e.value, + }); + } + }} + /> +
+
+
+ +
+ ) : null} + + ); +}; + +export default WriteToObjectSettings; diff --git a/packages/app-store/salesforce/lib/CrmService.ts b/packages/app-store/salesforce/lib/CrmService.ts index 208ab6ce52..5b85e96a90 100644 --- a/packages/app-store/salesforce/lib/CrmService.ts +++ b/packages/app-store/salesforce/lib/CrmService.ts @@ -18,7 +18,7 @@ import type { CRM, Contact, CrmEvent } from "@calcom/types/CrmService"; import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse"; import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse"; import { default as appMeta } from "../config.json"; -import type { writeToRecordDataSchema } from "../zod"; +import type { writeToRecordDataSchema, appDataSchema, writeToBookingEntry } from "../zod"; import { SalesforceRecordEnum, SalesforceFieldType, @@ -75,11 +75,11 @@ export default class SalesforceCRMService implements CRM { private conn!: Promise; private log: typeof logger; private calWarnings: string[] = []; - private appOptions: any; + private appOptions: z.infer; private doNotCreateEvent = false; private fallbackToContact = false; - constructor(credential: CredentialPayload, appOptions: any, testMode = false) { + constructor(credential: CredentialPayload, appOptions: z.infer, testMode = false) { this.integrationName = "salesforce_other_calendar"; if (!testMode) { this.conn = this.getClient(credential).then((c) => c); @@ -255,7 +255,7 @@ export default class SalesforceCRMService implements CRM { }); }); // Check to see if we also need to change the record owner - if (appOptions.onBookingChangeRecordOwner && appOptions.onBookingChangeRecordOwnerName) { + if (appOptions?.onBookingChangeRecordOwner && appOptions?.onBookingChangeRecordOwnerName) { if (ownerId) { // TODO: firstContact id is assumed to not be undefined. But current code doesn't check for it. await this.checkRecordOwnerNameFromRecordId(firstContact.id, ownerId); @@ -265,15 +265,16 @@ export default class SalesforceCRMService implements CRM { ); } } - if (appOptions.onBookingWriteToRecord && appOptions.onBookingWriteToRecordFields) { - await this.writeToPersonRecord( + if (appOptions?.onBookingWriteToRecord && appOptions?.onBookingWriteToRecordFields) { + await this.writeToRecord({ // TODO: firstContact id is assumed to not be undefined. But current code doesn't check for it. - firstContact.id, - event.startTime, - event.organizer?.email, - event.responses, - event?.uid - ); + recordId: firstContact.id, + fieldsToWriteTo: appOptions.onBookingWriteToRecordFields, + startTime: event.startTime, + organizerEmail: event.organizer?.email, + calEventResponses: event.responses, + bookingUid: event?.uid, + }); } return createdEvent; }; @@ -294,8 +295,27 @@ export default class SalesforceCRMService implements CRM { }); }; - private salesforceDeleteEvent = async (uid: string) => { + private salesforceDeleteEvent = async (uid: string, event: CalendarEvent) => { + const appOptions = this.getAppOptions(); const conn = await this.conn; + + if (appOptions?.onCancelWriteToEventRecord) { + const fieldsToWriteTo = appOptions?.onCancelWriteToEventRecordFields; + + // If the option is enabled then don't delete the event record + if (!fieldsToWriteTo || !Object.keys(fieldsToWriteTo)) { + return Promise.resolve(); + } + + return await this.writeToRecord({ + recordId: uid, + fieldsToWriteTo, + startTime: event.startTime, + organizerEmail: event.organizer?.email, + calEventResponses: event.responses, + bookingUid: event?.uid, + }); + } return await conn.sobject("Event").delete(uid); }; @@ -353,13 +373,15 @@ export default class SalesforceCRMService implements CRM { } } - public async deleteEvent(uid: string) { - const deletedEvent = await this.salesforceDeleteEvent(uid); - if (deletedEvent.success) { - Promise.resolve(); - } else { - Promise.reject({ calError: "Something went wrong when deleting the event in Salesforce" }); - } + public async deleteEvent(uid: string, event: CalendarEvent) { + await this.salesforceDeleteEvent(uid, event) + .then(() => { + return Promise.resolve(); + }) + .catch((error) => { + this.log.error(`Error canceling event ${uid} with error ${error}`); + return Promise.reject({ calError: "Something went wrong when deleting the event in Salesforce" }); + }); } async getContacts({ @@ -724,7 +746,7 @@ export default class SalesforceCRMService implements CRM { // Update the event with the no show data await conn.sobject("Event").update({ Id: event.uid, - [sendNoShowAttendeeDataField]: noShowData.noShow, + [sendNoShowAttendeeDataField as string]: noShowData.noShow, }); } } @@ -1045,84 +1067,91 @@ export default class SalesforceCRMService implements CRM { return this.fallbackToContact; } - private async writeToPersonRecord( - contactId: string, - startTime: string, - organizerEmail?: string, - calEventResponses?: CalEventResponses | null, - bookingUid?: string | null - ) { + private async writeToRecord({ + recordId, + startTime, + fieldsToWriteTo, + organizerEmail, + calEventResponses, + bookingUid, + }: { + recordId: string; + startTime: string; + fieldsToWriteTo: Record>; + organizerEmail?: string; + calEventResponses?: CalEventResponses | null; + bookingUid?: string | null; + }) { const conn = await this.conn; - const { onBookingWriteToRecordFields = {} } = this.getAppOptions(); - // Determine record type (Contact or Lead) - const personRecordType = this.determineRecordTypeById(contactId); + // Determine record type (Contact, Lead, etc) + const personRecordType = this.determineRecordTypeById(recordId); // Search the fields and ensure 1. they exist 2. they're the right type - const fieldsToWriteOn = Object.keys(onBookingWriteToRecordFields); + const fieldsToWriteOn = Object.keys(fieldsToWriteTo); const existingFields = await this.ensureFieldsExistOnObject(fieldsToWriteOn, personRecordType); if (!existingFields.length) { this.log.warn(`No fields found for record type ${personRecordType}`); return; } - const personRecord = await this.fetchPersonRecord(contactId, existingFields, personRecordType); + const personRecord = await this.fetchPersonRecord(recordId, existingFields, personRecordType); if (!personRecord) { - this.log.warn(`No personRecord found for contactId ${contactId}`); + this.log.warn(`No personRecord found for contactId ${recordId}`); return; } - this.log.info(`Writing to recordId ${contactId} on fields ${fieldsToWriteOn}`); + this.log.info(`Writing to recordId ${recordId} on fields ${fieldsToWriteOn}`); const writeOnRecordBody = await this.buildRecordUpdatePayload({ existingFields, personRecord, - onBookingWriteToRecordFields, + fieldsToWriteTo, startTime, bookingUid, organizerEmail, calEventResponses, - contactId, + recordId, }); this.log.info( - `Final writeOnRecordBody contains fields ${Object.keys(writeOnRecordBody)} for record ${contactId}` + `Final writeOnRecordBody contains fields ${Object.keys(writeOnRecordBody)} for record ${recordId}` ); // Update the person record await conn .sobject(personRecordType) .update({ - Id: contactId, + Id: recordId, ...writeOnRecordBody, }) .catch((e) => { - this.log.error(`Error updating person record for contactId ${contactId}`, e); + this.log.error(`Error updating person record for contactId ${recordId}`, e); }); } private async buildRecordUpdatePayload({ existingFields, personRecord, - onBookingWriteToRecordFields, + fieldsToWriteTo, startTime, bookingUid, organizerEmail, calEventResponses, - contactId, + recordId, }: { existingFields: Field[]; personRecord: Record; - onBookingWriteToRecordFields: Record; + fieldsToWriteTo: Record; startTime?: string; bookingUid?: string | null; organizerEmail?: string; calEventResponses?: CalEventResponses | null; - contactId: string; + recordId: string; }): Promise> { - const log = logger.getSubLogger({ prefix: [`[buildRecordUpdatePayload] ${contactId}`] }); + const log = logger.getSubLogger({ prefix: [`[buildRecordUpdatePayload] ${recordId}`] }); const writeOnRecordBody: Record = {}; for (const field of existingFields) { - const fieldConfig = onBookingWriteToRecordFields[field.name]; + const fieldConfig = fieldsToWriteTo[field.name]; if (!fieldConfig) { log.error(`No field config found for field ${field.name}`); @@ -1193,7 +1222,7 @@ export default class SalesforceCRMService implements CRM { salesforceField: field, calEventResponses, bookingUid, - contactId, + recordId, }); if (picklistValue) { writeOnRecordBody[field.name] = picklistValue; @@ -1223,8 +1252,10 @@ export default class SalesforceCRMService implements CRM { if (!customFieldInputsEnabled) return {}; + if (!appOptions?.onBookingWriteToEventObjectMap) return {}; + const customFieldInputs = customFieldInputsEnabled - ? await this.ensureFieldsExistOnObject(Object.keys(appOptions?.onBookingWriteToEventObjectMap), "Event") + ? await this.ensureFieldsExistOnObject(Object.keys(appOptions.onBookingWriteToEventObjectMap), "Event") : []; const confirmedCustomFieldInputs: { @@ -1342,6 +1373,10 @@ export default class SalesforceCRMService implements CRM { this.log.warn(`No uid for booking with organizer ${organizerEmail}`); } + if (fieldValue === DateFieldTypeData.BOOKING_CANCEL_DATE) { + return new Date().toISOString(); + } + return null; } @@ -1350,15 +1385,15 @@ export default class SalesforceCRMService implements CRM { salesforceField, calEventResponses, bookingUid, - contactId, + recordId, }: { fieldConfigValue: string; salesforceField: Field; calEventResponses?: CalEventResponses | null; bookingUid?: string | null; - contactId: string; + recordId: string; }) { - const log = logger.getSubLogger({ prefix: [`[getPicklistFieldValue] ${contactId}`] }); + const log = logger.getSubLogger({ prefix: [`[getPicklistFieldValue] ${recordId}`] }); const picklistOptions = salesforceField.picklistValues; if (!picklistOptions || !picklistOptions.length) { @@ -1498,12 +1533,12 @@ export default class SalesforceCRMService implements CRM { if (!(companyFieldName in onBookingWriteToRecordFields)) return; const companyValue = await this.getTextFieldValue({ - fieldValue: onBookingWriteToRecordFields[companyFieldName].value, + fieldValue: onBookingWriteToRecordFields[companyFieldName].value as string, fieldLength: defaultTextValueLength, calEventResponses, }); - if (companyValue === onBookingWriteToRecordFields[companyFieldName]) return; + if (companyValue && companyValue === onBookingWriteToRecordFields[companyFieldName].value) return; return companyValue; } @@ -1538,8 +1573,8 @@ export default class SalesforceCRMService implements CRM { const writeOnRecordBody = await this.buildRecordUpdatePayload({ existingFields, personRecord, - onBookingWriteToRecordFields: writeToRecordObject, - contactId: personRecord.Id, + fieldsToWriteTo: writeToRecordObject, + recordId: personRecord.Id, }); await conn .sobject(recordType) @@ -1568,6 +1603,8 @@ export default class SalesforceCRMService implements CRM { return SalesforceRecordEnum.ACCOUNT; case "00Q": return SalesforceRecordEnum.LEAD; + case "00U": + return SalesforceRecordEnum.EVENT; default: this.log.warn(`Unhandled record id type ${id}`); return SalesforceRecordEnum.CONTACT; diff --git a/packages/app-store/salesforce/lib/enums.ts b/packages/app-store/salesforce/lib/enums.ts index ba5936d817..e869cf6ba0 100644 --- a/packages/app-store/salesforce/lib/enums.ts +++ b/packages/app-store/salesforce/lib/enums.ts @@ -2,6 +2,7 @@ export enum SalesforceRecordEnum { CONTACT = "Contact", LEAD = "Lead", ACCOUNT = "Account", + EVENT = "Event", } export enum WhenToWriteToRecord { @@ -23,6 +24,7 @@ export enum SalesforceFieldType { export enum DateFieldTypeData { BOOKING_START_DATE = "booking_start_date", BOOKING_CREATED_DATE = "booking_created_date", + BOOKING_CANCEL_DATE = "booking_cancel_date", } export enum RoutingReasons { diff --git a/packages/app-store/salesforce/zod.ts b/packages/app-store/salesforce/zod.ts index 0329802db2..faefd42d07 100644 --- a/packages/app-store/salesforce/zod.ts +++ b/packages/app-store/salesforce/zod.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { eventTypeAppCardZod } from "../eventTypeAppCardZod"; import { SalesforceRecordEnum, WhenToWriteToRecord, SalesforceFieldType } from "./lib/enums"; -const writeToBookingEntry = z.object({ +export const writeToBookingEntry = z.object({ value: z.union([z.string(), z.boolean()]), fieldType: z.nativeEnum(SalesforceFieldType), whenToWrite: z.nativeEnum(WhenToWriteToRecord), @@ -56,6 +56,8 @@ export const appDataSchema = eventTypeAppCardZod.extend({ onBookingWriteToRecord: z.boolean().optional(), onBookingWriteToRecordFields: z.record(z.string(), writeToBookingEntry).optional(), ignoreGuests: z.boolean().optional(), + onCancelWriteToEventRecord: z.boolean().optional(), + onCancelWriteToEventRecordFields: z.record(z.string(), writeToBookingEntry).optional(), }); export const appKeysSchema = z.object({ diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 1027e5457a..2d0c4c979a 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -27,7 +27,11 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; -import { bookingMetadataSchema, EventTypeMetaDataSchema, bookingCancelInput } from "@calcom/prisma/zod-utils"; +import { + bookingMetadataSchema, + eventTypeMetaDataSchemaWithTypedApps, + bookingCancelInput, +} from "@calcom/prisma/zod-utils"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -444,7 +448,7 @@ async function handler(input: CancelBookingInput) { allRemainingBookings ); - const bookingToDeleteEventTypeMetadata = EventTypeMetaDataSchema.parse( + const bookingToDeleteEventTypeMetadata = eventTypeMetaDataSchemaWithTypedApps.parse( bookingToDelete.eventType?.metadata || null ); @@ -453,7 +457,10 @@ async function handler(input: CancelBookingInput) { metadata: bookingToDeleteEventTypeMetadata, }); - const eventManager = new EventManager({ ...bookingToDelete.user, credentials }); + const eventManager = new EventManager( + { ...bookingToDelete.user, credentials }, + bookingToDeleteEventTypeMetadata?.apps + ); await eventManager.cancelEvent(evt, bookingToDelete.references, isBookingInRecurringSeries); diff --git a/packages/lib/EventManager.ts b/packages/lib/EventManager.ts index 82bb4e9ba5..679e26b2d6 100644 --- a/packages/lib/EventManager.ts +++ b/packages/lib/EventManager.ts @@ -616,7 +616,7 @@ export default class EventManager { if (reference.type.includes("_crm") || reference.type.includes("other_calendar")) { crmReferences.push(reference); - allPromises.push(this.deleteCRMEvent({ reference })); + allPromises.push(this.deleteCRMEvent({ reference, event })); } } @@ -1100,11 +1100,12 @@ export default class EventManager { return updatedEvents; } - private async deleteCRMEvent({ reference }: { reference: PartialReference }) { + private async deleteCRMEvent({ reference, event }: { reference: PartialReference; event: CalendarEvent }) { const credential = this.crmCredentials.find((cred) => cred.id === reference.credentialId); if (credential) { - const crm = new CrmManager(credential); - await crm.deleteEvent(reference.uid); + const currentAppOption = this.getAppOptionsFromEventMetadata(credential); + const crm = new CrmManager(credential, currentAppOption); + await crm.deleteEvent(reference.uid, event); } } diff --git a/packages/lib/crmManager/crmManager.ts b/packages/lib/crmManager/crmManager.ts index baffa81693..9bb6bcfbbf 100644 --- a/packages/lib/crmManager/crmManager.ts +++ b/packages/lib/crmManager/crmManager.ts @@ -57,9 +57,9 @@ export default class CrmManager { return await crmService?.updateEvent(uid, event); } - public async deleteEvent(uid: string) { + public async deleteEvent(uid: string, event: CalendarEvent) { const crmService = await this.getCrmService(this.credential); - return await crmService?.deleteEvent(uid); + return await crmService?.deleteEvent(uid, event); } public async getContacts(params: { diff --git a/packages/types/CrmService.d.ts b/packages/types/CrmService.d.ts index 3b0b65678c..49d006307b 100644 --- a/packages/types/CrmService.d.ts +++ b/packages/types/CrmService.d.ts @@ -33,7 +33,7 @@ export interface CrmEvent { export interface CRM { createEvent: (event: CalendarEvent, contacts: Contact[]) => Promise; updateEvent: (uid: string, event: CalendarEvent) => Promise; - deleteEvent: (uid: string) => Promise; + deleteEvent: (uid: string, event: CalendarEvent) => Promise; getContacts: ({ emails, includeOwner,