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 `<WriteToObjectSettings />`

* 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
This commit is contained in:
Joe Au-Yeung
2025-04-08 11:40:05 -04:00
committed by GitHub
parent 4753bd785a
commit b3d5a6edca
10 changed files with 425 additions and 314 deletions
@@ -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 havent 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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -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<typeof writeToRecordEntrySchema>
>({
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({
</div>
<div className="mt-4">
<Switch
label={t("salesforce_on_booking_write_to_record", { record: createEventOn })}
labelOnLeading
checked={onBookingWriteToRecord}
onCheckedChange={(checked) => {
<WriteToObjectSettings
bookingAction={BookingActionEnum.ON_BOOKING}
optionLabel={t("salesforce_on_booking_write_to_record", { record: createEventOn })}
optionEnabled={onBookingWriteToRecord}
writeToObjectData={onBookingWriteToRecordFields}
optionSwitchOnChange={(checked) => {
setAppData("onBookingWriteToRecord", checked);
}}
updateWriteToObjectData={(data) => setAppData("onBookingWriteToRecordFields", data)}
/>
{onBookingWriteToRecord ? (
<div className="ml-2 mt-2">
<div className="grid grid-cols-5 gap-4">
<div>{t("field_name")}</div>
<div>{t("field_type")}</div>
<div>{t("value")}</div>
<div>{t("when_to_write")}</div>
</div>
<div>
{Object.keys(onBookingWriteToRecordFields).map((key) => (
<div className="mt-2 grid grid-cols-5 gap-4" key={key}>
<div>
<InputField value={key} readOnly />
</div>
<div>
<Select
value={fieldTypeOptions.find(
(option) => option.value === onBookingWriteToRecordFields[key].fieldType
)}
isDisabled={true}
/>
</div>
<div>
{onBookingWriteToRecordFields[key].fieldType === SalesforceFieldType.DATE ? (
<Select
value={dateFieldValueOptions.find(
(option) => option.value === onBookingWriteToRecordFields[key].value
)}
isDisabled={true}
/>
) : onBookingWriteToRecordFields[key].fieldType === SalesforceFieldType.CHECKBOX ? (
<Select
value={checkboxFieldValueOptions.find(
(option) => option.value === onBookingWriteToRecordFields[key].value
)}
isDisabled={true}
/>
) : (
<InputField value={onBookingWriteToRecordFields[key].value as string} readOnly />
)}
</div>
<div>
<Select
value={whenToWriteToRecordOptions.find(
(option) => option.value === onBookingWriteToRecordFields[key].whenToWrite
)}
isDisabled={true}
/>
</div>
<div>
<Button
StartIcon="trash"
variant="icon"
color="destructive"
onClick={() => {
const newObject = onBookingWriteToRecordFields;
delete onBookingWriteToRecordFields[key];
setAppData("onBookingWriteToRecordFields", newObject);
}}
/>
</div>
</div>
))}
<div className="mt-2 grid grid-cols-5 gap-4">
<div>
<InputField
value={newOnBookingWriteToPersonObjectField.field}
onChange={(e) =>
setNewOnBookingWriteToPersonObjectField({
...newOnBookingWriteToPersonObjectField,
field: e.target.value,
})
}
/>
</div>
<div>
<Select
options={fieldTypeOptions}
value={writeToPersonObjectFieldType}
onChange={(e) => {
if (e) {
setWriteToPersonObjectFieldType(e);
setNewOnBookingWriteToPersonObjectField({
...newOnBookingWriteToPersonObjectField,
fieldType: e.value,
...(e.value === SalesforceFieldType.DATE && { value: dateFieldValue.value }),
...(e.value === SalesforceFieldType.CHECKBOX && {
value: checkboxFieldValue.value,
}),
});
}
}}
/>
</div>
<div>
{writeToPersonObjectFieldType.value === SalesforceFieldType.DATE ? (
<Select
options={dateFieldValueOptions}
value={dateFieldValue}
onChange={(e) => {
if (e) {
setDateValue(e);
setNewOnBookingWriteToPersonObjectField({
...newOnBookingWriteToPersonObjectField,
value: e.value,
});
}
}}
/>
) : writeToPersonObjectFieldType.value === SalesforceFieldType.CHECKBOX ? (
<Select
options={checkboxFieldValueOptions}
value={checkboxFieldValue}
onChange={(e) => {
if (e) {
setCheckboxFieldValue(e);
setNewOnBookingWriteToPersonObjectField({
...newOnBookingWriteToPersonObjectField,
value: e.value,
});
}
}}
/>
) : (
<InputField
value={newOnBookingWriteToPersonObjectField.value as string}
onChange={(e) =>
setNewOnBookingWriteToPersonObjectField({
...newOnBookingWriteToPersonObjectField,
value: e.target.value,
})
}
/>
)}
</div>
<div>
<Select
options={whenToWriteToRecordOptions}
value={whenToWriteToPersonRecord}
onChange={(e) => {
if (e) {
setWhenToWriteToPersonRecord(e);
setNewOnBookingWriteToPersonObjectField({
...newOnBookingWriteToPersonObjectField,
whenToWrite: e.value,
});
}
}}
/>
</div>
</div>
</div>
<Button
className="mt-2"
size="sm"
disabled={
!(
newOnBookingWriteToPersonObjectField.field &&
newOnBookingWriteToPersonObjectField.fieldType &&
newOnBookingWriteToPersonObjectField.value !== "" &&
newOnBookingWriteToPersonObjectField.whenToWrite
)
}
onClick={() => {
if (
Object.keys(onBookingWriteToRecordFields).includes(
newOnBookingWriteToEventObjectField.field.trim()
)
) {
showToast("Field already exists", "error");
return;
}
setAppData("onBookingWriteToRecordFields", {
...onBookingWriteToRecordFields,
[newOnBookingWriteToPersonObjectField.field.trim()]: {
fieldType: newOnBookingWriteToPersonObjectField.fieldType,
value: newOnBookingWriteToPersonObjectField.value,
whenToWrite: newOnBookingWriteToPersonObjectField.whenToWrite,
},
});
setNewOnBookingWriteToPersonObjectField({
field: "",
fieldType: writeToPersonObjectFieldType.value,
value: "",
whenToWrite: WhenToWriteToRecord.FIELD_EMPTY,
});
}}>
{t("add_new_field")}
</Button>
</div>
) : null}
</div>
<div className="mt-4">
@@ -588,6 +352,19 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
</div>
) : null}
<div className="mt-4">
<WriteToObjectSettings
bookingAction={BookingActionEnum.ON_CANCEL}
optionLabel={t("salesforce_on_cancel_write_to_event")}
optionEnabled={onCancelWriteToEventRecord}
writeToObjectData={onCancelWriteToEventRecordFields}
optionSwitchOnChange={(checked) => {
setAppData("onCancelWriteToEventRecord", checked);
}}
updateWriteToObjectData={(data) => setAppData("onCancelWriteToEventRecordFields", data)}
/>
</div>
<div className="ml-2 mt-4">
<Switch
label="Send no show attendee data to event object"
@@ -0,0 +1,283 @@
import { useState } from "react";
import type z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { Switch } from "@calcom/ui/components/form";
import { InputField } from "@calcom/ui/components/form";
import { Select } from "@calcom/ui/components/form";
import { showToast } from "@calcom/ui/components/toast";
import { WhenToWriteToRecord, SalesforceFieldType, DateFieldTypeData } from "../../lib/enums";
import type { writeToRecordEntrySchema, writeToBookingEntry } from "../../zod";
export enum BookingActionEnum {
ON_BOOKING = "on_booking",
ON_CANCEL = "on_cancel",
}
type iWriteToObjectData = Record<string, z.infer<typeof writeToBookingEntry>>;
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<typeof writeToRecordEntrySchema>
>({
field: "",
fieldType: fieldTypeSelectedOption.value,
value: "",
whenToWrite: whenToWriteSelectedOption.value,
});
return (
<>
<Switch
label={optionLabel}
labelOnLeading
checked={optionEnabled}
onCheckedChange={optionSwitchOnChange}
/>
{optionEnabled ? (
<div className="ml-2 mt-2">
<div className="grid grid-cols-5 gap-4">
<div>{t("field_name")}</div>
<div>{t("field_type")}</div>
<div>{t("value")}</div>
<div>{t("when_to_write")}</div>
</div>
<div>
{Object.keys(writeToObjectData).map((key) => (
<div className="mt-2 grid grid-cols-5 gap-4" key={key}>
<div>
<InputField value={key} readOnly />
</div>
<div>
<Select
value={fieldTypeOptions.find(
(option) => option.value === writeToObjectData[key].fieldType
)}
isDisabled={true}
/>
</div>
<div>
{writeToObjectData[key].fieldType === SalesforceFieldType.DATE ? (
<Select
value={dateFieldValueOptions.find(
(option) => option.value === writeToObjectData[key].value
)}
isDisabled={true}
/>
) : writeToObjectData[key].fieldType === SalesforceFieldType.CHECKBOX ? (
<Select
value={checkboxFieldValueOptions.find(
(option) => option.value === writeToObjectData[key].value
)}
isDisabled={true}
/>
) : (
<InputField value={writeToObjectData[key].value as string} readOnly />
)}
</div>
<div>
<Select
value={whenToWriteToRecordOptions.find(
(option) => option.value === writeToObjectData[key].whenToWrite
)}
isDisabled={true}
/>
</div>
<div>
<Button
StartIcon="trash"
variant="icon"
color="destructive"
onClick={() => {
const newObject = writeToObjectData;
delete writeToObjectData[key];
updateWriteToObjectData(newObject);
}}
/>
</div>
</div>
))}
<div className="mt-2 grid grid-cols-5 gap-4">
<div>
<InputField
value={newOnWriteToRecordEntry.field}
onChange={(e) =>
setNewOnWriteToRecordEntry({
...newOnWriteToRecordEntry,
field: e.target.value,
})
}
/>
</div>
<div>
<Select
options={fieldTypeOptions}
value={fieldTypeSelectedOption}
onChange={(e) => {
if (e) {
setFieldTypeSelectedOption(e);
setNewOnWriteToRecordEntry({
...newOnWriteToRecordEntry,
fieldType: e.value,
...(e.value === SalesforceFieldType.DATE && { value: dateFieldSelectedOption.value }),
...(e.value === SalesforceFieldType.CHECKBOX && {
value: checkboxFieldSelectedOption.value,
}),
});
}
}}
/>
</div>
<div>
{newOnWriteToRecordEntry.fieldType === SalesforceFieldType.DATE ? (
<Select
options={dateFieldValueOptions}
value={dateFieldSelectedOption}
onChange={(e) => {
if (e) {
setDateFieldSelectedOption(e);
setNewOnWriteToRecordEntry({
...newOnWriteToRecordEntry,
value: e.value,
});
}
}}
/>
) : newOnWriteToRecordEntry.fieldType === SalesforceFieldType.CHECKBOX ? (
<Select
options={checkboxFieldValueOptions}
value={checkboxFieldSelectedOption}
onChange={(e) => {
if (e) {
setCheckboxFieldSelectedOption(e);
setNewOnWriteToRecordEntry({
...newOnWriteToRecordEntry,
value: e.value,
});
}
}}
/>
) : (
<InputField
value={newOnWriteToRecordEntry.value as string}
onChange={(e) =>
setNewOnWriteToRecordEntry({
...newOnWriteToRecordEntry,
value: e.target.value,
})
}
/>
)}
</div>
<div>
<Select
options={whenToWriteToRecordOptions}
value={whenToWriteSelectedOption}
onChange={(e) => {
if (e) {
setWhenToWriteSelectedOption(e);
setNewOnWriteToRecordEntry({
...newOnWriteToRecordEntry,
whenToWrite: e.value,
});
}
}}
/>
</div>
</div>
</div>
<Button
className="mt-2"
size="sm"
disabled={
!(
newOnWriteToRecordEntry.field &&
newOnWriteToRecordEntry.fieldType &&
newOnWriteToRecordEntry.value !== "" &&
newOnWriteToRecordEntry.whenToWrite
)
}
onClick={() => {
if (Object.keys(writeToObjectData).includes(newOnWriteToRecordEntry.field.trim())) {
showToast("Field already exists", "error");
return;
}
updateWriteToObjectData({
...writeToObjectData,
[newOnWriteToRecordEntry.field.trim()]: {
fieldType: newOnWriteToRecordEntry.fieldType,
value: newOnWriteToRecordEntry.value,
whenToWrite: newOnWriteToRecordEntry.whenToWrite,
},
});
setNewOnWriteToRecordEntry({
field: "",
fieldType: fieldTypeOptions[0].value,
value: "",
whenToWrite: whenToWriteToRecordOptions[0].value,
});
}}>
{t("add_new_field")}
</Button>
</div>
) : null}
</>
);
};
export default WriteToObjectSettings;
+92 -55
View File
@@ -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<Connection>;
private log: typeof logger;
private calWarnings: string[] = [];
private appOptions: any;
private appOptions: z.infer<typeof appDataSchema>;
private doNotCreateEvent = false;
private fallbackToContact = false;
constructor(credential: CredentialPayload, appOptions: any, testMode = false) {
constructor(credential: CredentialPayload, appOptions: z.infer<typeof appDataSchema>, 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<string, z.infer<typeof writeToBookingEntry>>;
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<string, any>;
onBookingWriteToRecordFields: Record<string, any>;
fieldsToWriteTo: Record<string, any>;
startTime?: string;
bookingUid?: string | null;
organizerEmail?: string;
calEventResponses?: CalEventResponses | null;
contactId: string;
recordId: string;
}): Promise<Record<string, any>> {
const log = logger.getSubLogger({ prefix: [`[buildRecordUpdatePayload] ${contactId}`] });
const log = logger.getSubLogger({ prefix: [`[buildRecordUpdatePayload] ${recordId}`] });
const writeOnRecordBody: Record<string, any> = {};
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;
@@ -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 {
+3 -1
View File
@@ -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({
@@ -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);
+5 -4
View File
@@ -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);
}
}
+2 -2
View File
@@ -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: {
+1 -1
View File
@@ -33,7 +33,7 @@ export interface CrmEvent {
export interface CRM {
createEvent: (event: CalendarEvent, contacts: Contact[]) => Promise<CrmEvent | undefined>;
updateEvent: (uid: string, event: CalendarEvent) => Promise<CrmEvent>;
deleteEvent: (uid: string) => Promise<void>;
deleteEvent: (uid: string, event: CalendarEvent) => Promise<void>;
getContacts: ({
emails,
includeOwner,