feat: Send Attendee No Show Data To Salesforce (#17423)

This commit is contained in:
Joe Au-Yeung
2024-10-31 16:39:58 +00:00
committed by GitHub
parent ebd5ca6ceb
commit 06f83e6d0d
8 changed files with 204 additions and 1 deletions
@@ -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) {
+2
View File
@@ -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({
+7
View File
@@ -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);
}
}
}
+7 -1
View File
@@ -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"];
+4
View File
@@ -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>;
}