feat: Send Attendee No Show Data To Salesforce (#17423)
This commit is contained in:
@@ -30,6 +30,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
const createEventOnLeadCheckForContact = getAppData("createEventOnLeadCheckForContact") ?? false;
|
||||
const onBookingChangeRecordOwner = getAppData("onBookingChangeRecordOwner") ?? false;
|
||||
const onBookingChangeRecordOwnerName = getAppData("onBookingChangeRecordOwnerName") ?? [];
|
||||
const sendNoShowAttendeeData = getAppData("sendNoShowAttendeeData") ?? false;
|
||||
const sendNoShowAttendeeDataField = getAppData("sendNoShowAttendeeDataField") ?? "";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
const recordOptions = [
|
||||
@@ -278,6 +281,25 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
<Alert className="mt-2" severity="neutral" title={t("skip_rr_description")} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="ml-2 mt-4">
|
||||
<Switch
|
||||
label="Send no show attendee data to event object"
|
||||
checked={sendNoShowAttendeeData}
|
||||
onCheckedChange={(checked) => {
|
||||
setAppData("sendNoShowAttendeeData", checked);
|
||||
}}
|
||||
/>
|
||||
{sendNoShowAttendeeData ? (
|
||||
<div className="mt-2">
|
||||
<p className="mb-2">Field name to check (must be checkbox data type)</p>
|
||||
<InputField
|
||||
value={sendNoShowAttendeeDataField}
|
||||
onChange={(e) => setAppData("sendNoShowAttendeeDataField", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
</AppCard>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { CRM, Contact, CrmEvent } from "@calcom/types/CrmService";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import { default as appMeta } from "../config.json";
|
||||
import { SalesforceRecordEnum } from "./recordEnum";
|
||||
|
||||
type ExtendedTokenResponse = TokenResponse & {
|
||||
@@ -502,6 +503,80 @@ export default class SalesforceCRMService implements CRM {
|
||||
return createdContacts;
|
||||
}
|
||||
|
||||
async handleAttendeeNoShow(bookingUid: string, attendees: { email: string; noShow: boolean }[]) {
|
||||
const appOptions = this.getAppOptions();
|
||||
const { sendNoShowAttendeeData, sendNoShowAttendeeDataField } = appOptions;
|
||||
const conn = await this.conn;
|
||||
// Check that no show is enabled
|
||||
if (!sendNoShowAttendeeData && !sendNoShowAttendeeDataField) {
|
||||
this.log.warn(`No show settings not set for bookingUid ${bookingUid}`);
|
||||
return;
|
||||
}
|
||||
// Get all Salesforce events associated with the booking
|
||||
const salesforceEvents = await prisma.bookingReference.findMany({
|
||||
where: {
|
||||
type: appMeta.type,
|
||||
booking: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const salesforceEntity = await conn.describe("Event");
|
||||
const fields = salesforceEntity.fields;
|
||||
const noShowField = fields.find((field) => field.name === sendNoShowAttendeeDataField);
|
||||
|
||||
if (!noShowField || (!noShowField.type as unknown as string) !== "boolean") {
|
||||
this.log.warn(
|
||||
`No show field on Salesforce doesn't exist or is not of type boolean for bookingUid ${bookingUid}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of salesforceEvents) {
|
||||
const salesforceEvent = (await conn.query(`SELECT WhoId FROM Event WHERE Id = '${event.uid}'`)) as {
|
||||
records: { WhoId: string }[];
|
||||
};
|
||||
|
||||
let salesforceAttendeeEmail: string | undefined = undefined;
|
||||
// Figure out if the attendee is a contact or lead
|
||||
const contactQuery = (await conn.query(
|
||||
`SELECT Email FROM Contact WHERE Id = '${salesforceEvent.records[0].WhoId}'`
|
||||
)) as { records: { Email: string }[] };
|
||||
const leadQuery = (await conn.query(
|
||||
`SELECT Email FROM Lead WHERE Id = '${salesforceEvent.records[0].WhoId}'`
|
||||
)) as { records: { Email: string }[] };
|
||||
|
||||
// Prioritize contacts over leads
|
||||
if (contactQuery.records.length > 0) {
|
||||
salesforceAttendeeEmail = contactQuery.records[0].Email;
|
||||
} else if (leadQuery.records.length > 0) {
|
||||
salesforceAttendeeEmail = leadQuery.records[0].Email;
|
||||
} else {
|
||||
this.log.warn(
|
||||
`Could not find attendee for bookingUid ${bookingUid} and salesforce event id ${event.uid}`
|
||||
);
|
||||
}
|
||||
|
||||
if (salesforceAttendeeEmail) {
|
||||
// Find the attendee no show data
|
||||
const noShowData = attendees.find((attendee) => attendee.email === salesforceAttendeeEmail);
|
||||
|
||||
if (!noShowData) {
|
||||
this.log.warn(
|
||||
`No show data could not be found for ${salesforceAttendeeEmail} and bookingUid ${bookingUid}`
|
||||
);
|
||||
} else {
|
||||
// Update the event with the no show data
|
||||
await conn.sobject("Event").update({
|
||||
Id: event.uid,
|
||||
[sendNoShowAttendeeDataField]: noShowData.noShow,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getExistingIdFromDuplicateError(error: any): string | null {
|
||||
if (error.duplicateResult && error.duplicateResult.matchResults) {
|
||||
for (const matchResult of error.duplicateResult.matchResults) {
|
||||
|
||||
@@ -18,6 +18,8 @@ export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
createEventOnLeadCheckForContact: z.boolean().optional(),
|
||||
onBookingChangeRecordOwner: z.boolean().optional(),
|
||||
onBookingChangeRecordOwnerName: z.string().optional(),
|
||||
sendNoShowAttendeeData: z.boolean().optional(),
|
||||
sendNoShowAttendeeDataField: z.string().optional(),
|
||||
});
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
|
||||
@@ -72,4 +72,11 @@ export default class CrmManager {
|
||||
const createdContacts = (await crmService?.createContacts(contactsToCreate, organizerEmail)) || [];
|
||||
return createdContacts;
|
||||
}
|
||||
|
||||
public async handleAttendeeNoShow(bookingUid: string, attendees: { email: string; noShow: boolean }[]) {
|
||||
const crmService = await this.getCrmService(this.credential);
|
||||
if (crmService?.handleAttendeeNoShow) {
|
||||
await crmService.handleAttendeeNoShow(bookingUid, attendees);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ import { prisma } from "@calcom/prisma";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/client";
|
||||
import type { TNoShowInputSchema } from "@calcom/trpc/server/routers/loggedInViewer/markNoShow.schema";
|
||||
|
||||
import handleSendingAttendeeNoShowDataToApps from "./noShow/handleSendingAttendeeNoShowDataToApps";
|
||||
|
||||
export type NoShowAttendees = { email: string; noShow: boolean }[];
|
||||
|
||||
const buildResultPayload = async (
|
||||
bookingUid: string,
|
||||
attendeeEmails: string[],
|
||||
@@ -41,7 +45,7 @@ const logFailedResults = (results: PromiseSettledResult<any>[]) => {
|
||||
};
|
||||
|
||||
class ResponsePayload {
|
||||
attendees: { email: string; noShow: boolean }[];
|
||||
attendees: NoShowAttendees;
|
||||
noShowHost: boolean;
|
||||
message: string;
|
||||
|
||||
@@ -101,6 +105,8 @@ const handleMarkNoShow = async ({
|
||||
|
||||
responsePayload.setAttendees(payload.attendees);
|
||||
responsePayload.setMessage(payload.message);
|
||||
|
||||
await handleSendingAttendeeNoShowDataToApps(bookingUid, attendees);
|
||||
}
|
||||
|
||||
if (noShowHost) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
import type { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
|
||||
import CrmManager from "@calcom/core/crmManager/crmManager";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { NoShowAttendees } from "../handleMarkNoShow";
|
||||
import { noShowEnabledApps } from "./noShowEnabledApps";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["handleSendingNoShowDataToApps"] });
|
||||
|
||||
export default async function handleSendingAttendeeNoShowDataToApps(
|
||||
bookingUid: string,
|
||||
attendees: NoShowAttendees
|
||||
) {
|
||||
// Get event type metadata
|
||||
const eventTypeQuery = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
select: {
|
||||
eventType: {
|
||||
select: {
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!eventTypeQuery || !eventTypeQuery?.eventType?.metadata) {
|
||||
log.warn(`For no show, could not find eventType for bookingUid ${bookingUid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventTypeMetadataParse = EventTypeMetaDataSchema.safeParse(eventTypeQuery?.eventType?.metadata);
|
||||
if (!eventTypeMetadataParse.success) {
|
||||
log.error(`Malformed event type metadata for bookingUid ${bookingUid}`);
|
||||
return;
|
||||
}
|
||||
const eventTypeAppMetadata = eventTypeMetadataParse.data?.apps;
|
||||
|
||||
for (const slug in eventTypeAppMetadata) {
|
||||
if (noShowEnabledApps.includes(slug)) {
|
||||
const app = eventTypeAppMetadata[slug as keyof typeof eventTypeAppMetadata];
|
||||
|
||||
const appCategory = app.appCategories[0];
|
||||
|
||||
if (appCategory === "crm") {
|
||||
await handleCRMNoShow(bookingUid, app, attendees);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async function handleCRMNoShow(
|
||||
bookingUid: string,
|
||||
appData: z.infer<typeof eventTypeAppCardZod>,
|
||||
attendees: NoShowAttendees
|
||||
) {
|
||||
// Handle checking if no she in CrmService
|
||||
|
||||
const credential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
id: appData.credentialId,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!credential) {
|
||||
log.warn(`CRM credential not found for bookingUid ${bookingUid}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const crm = new CrmManager(credential, appData);
|
||||
await crm.handleAttendeeNoShow(bookingUid, attendees);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Slugs of apps that have the option to send no show data to */
|
||||
export const noShowEnabledApps = ["salesforce"];
|
||||
Vendored
+4
@@ -39,4 +39,8 @@ export interface CRM {
|
||||
}) => Promise<Contact[]>;
|
||||
createContacts: (contactsToCreate: ContactCreateInput[], organizerEmail?: string) => Promise<Contact[]>;
|
||||
getAppOptions: () => any;
|
||||
handleAttendeeNoShow?: (
|
||||
bookingUid: string,
|
||||
attendees: { email: string; noShow: boolean }[]
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user