Skip to content

Commit 06f83e6

Browse files
authored
feat: Send Attendee No Show Data To Salesforce (#17423)
1 parent ebd5ca6 commit 06f83e6

File tree

8 files changed

+204
-1
lines changed

8 files changed

+204
-1
lines changed

packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
3030
const createEventOnLeadCheckForContact = getAppData("createEventOnLeadCheckForContact") ?? false;
3131
const onBookingChangeRecordOwner = getAppData("onBookingChangeRecordOwner") ?? false;
3232
const onBookingChangeRecordOwnerName = getAppData("onBookingChangeRecordOwnerName") ?? [];
33+
const sendNoShowAttendeeData = getAppData("sendNoShowAttendeeData") ?? false;
34+
const sendNoShowAttendeeDataField = getAppData("sendNoShowAttendeeDataField") ?? "";
35+
3336
const { t } = useLocale();
3437

3538
const recordOptions = [
@@ -278,6 +281,25 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
278281
<Alert className="mt-2" severity="neutral" title={t("skip_rr_description")} />
279282
</div>
280283
) : null}
284+
285+
<div className="ml-2 mt-4">
286+
<Switch
287+
label="Send no show attendee data to event object"
288+
checked={sendNoShowAttendeeData}
289+
onCheckedChange={(checked) => {
290+
setAppData("sendNoShowAttendeeData", checked);
291+
}}
292+
/>
293+
{sendNoShowAttendeeData ? (
294+
<div className="mt-2">
295+
<p className="mb-2">Field name to check (must be checkbox data type)</p>
296+
<InputField
297+
value={sendNoShowAttendeeDataField}
298+
onChange={(e) => setAppData("sendNoShowAttendeeDataField", e.target.value)}
299+
/>
300+
</div>
301+
) : null}
302+
</div>
281303
</>
282304
</AppCard>
283305
);

packages/app-store/salesforce/lib/CrmService.ts

+75
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { CRM, Contact, CrmEvent } from "@calcom/types/CrmService";
1515
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
1616
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
1717
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
18+
import { default as appMeta } from "../config.json";
1819
import { SalesforceRecordEnum } from "./recordEnum";
1920

2021
type ExtendedTokenResponse = TokenResponse & {
@@ -502,6 +503,80 @@ export default class SalesforceCRMService implements CRM {
502503
return createdContacts;
503504
}
504505

506+
async handleAttendeeNoShow(bookingUid: string, attendees: { email: string; noShow: boolean }[]) {
507+
const appOptions = this.getAppOptions();
508+
const { sendNoShowAttendeeData, sendNoShowAttendeeDataField } = appOptions;
509+
const conn = await this.conn;
510+
// Check that no show is enabled
511+
if (!sendNoShowAttendeeData && !sendNoShowAttendeeDataField) {
512+
this.log.warn(`No show settings not set for bookingUid ${bookingUid}`);
513+
return;
514+
}
515+
// Get all Salesforce events associated with the booking
516+
const salesforceEvents = await prisma.bookingReference.findMany({
517+
where: {
518+
type: appMeta.type,
519+
booking: {
520+
uid: bookingUid,
521+
},
522+
},
523+
});
524+
525+
const salesforceEntity = await conn.describe("Event");
526+
const fields = salesforceEntity.fields;
527+
const noShowField = fields.find((field) => field.name === sendNoShowAttendeeDataField);
528+
529+
if (!noShowField || (!noShowField.type as unknown as string) !== "boolean") {
530+
this.log.warn(
531+
`No show field on Salesforce doesn't exist or is not of type boolean for bookingUid ${bookingUid}`
532+
);
533+
return;
534+
}
535+
536+
for (const event of salesforceEvents) {
537+
const salesforceEvent = (await conn.query(`SELECT WhoId FROM Event WHERE Id = '${event.uid}'`)) as {
538+
records: { WhoId: string }[];
539+
};
540+
541+
let salesforceAttendeeEmail: string | undefined = undefined;
542+
// Figure out if the attendee is a contact or lead
543+
const contactQuery = (await conn.query(
544+
`SELECT Email FROM Contact WHERE Id = '${salesforceEvent.records[0].WhoId}'`
545+
)) as { records: { Email: string }[] };
546+
const leadQuery = (await conn.query(
547+
`SELECT Email FROM Lead WHERE Id = '${salesforceEvent.records[0].WhoId}'`
548+
)) as { records: { Email: string }[] };
549+
550+
// Prioritize contacts over leads
551+
if (contactQuery.records.length > 0) {
552+
salesforceAttendeeEmail = contactQuery.records[0].Email;
553+
} else if (leadQuery.records.length > 0) {
554+
salesforceAttendeeEmail = leadQuery.records[0].Email;
555+
} else {
556+
this.log.warn(
557+
`Could not find attendee for bookingUid ${bookingUid} and salesforce event id ${event.uid}`
558+
);
559+
}
560+
561+
if (salesforceAttendeeEmail) {
562+
// Find the attendee no show data
563+
const noShowData = attendees.find((attendee) => attendee.email === salesforceAttendeeEmail);
564+
565+
if (!noShowData) {
566+
this.log.warn(
567+
`No show data could not be found for ${salesforceAttendeeEmail} and bookingUid ${bookingUid}`
568+
);
569+
} else {
570+
// Update the event with the no show data
571+
await conn.sobject("Event").update({
572+
Id: event.uid,
573+
[sendNoShowAttendeeDataField]: noShowData.noShow,
574+
});
575+
}
576+
}
577+
}
578+
}
579+
505580
private getExistingIdFromDuplicateError(error: any): string | null {
506581
if (error.duplicateResult && error.duplicateResult.matchResults) {
507582
for (const matchResult of error.duplicateResult.matchResults) {

packages/app-store/salesforce/zod.ts

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const appDataSchema = eventTypeAppCardZod.extend({
1818
createEventOnLeadCheckForContact: z.boolean().optional(),
1919
onBookingChangeRecordOwner: z.boolean().optional(),
2020
onBookingChangeRecordOwnerName: z.string().optional(),
21+
sendNoShowAttendeeData: z.boolean().optional(),
22+
sendNoShowAttendeeDataField: z.string().optional(),
2123
});
2224

2325
export const appKeysSchema = z.object({

packages/core/crmManager/crmManager.ts

+7
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,11 @@ export default class CrmManager {
7272
const createdContacts = (await crmService?.createContacts(contactsToCreate, organizerEmail)) || [];
7373
return createdContacts;
7474
}
75+
76+
public async handleAttendeeNoShow(bookingUid: string, attendees: { email: string; noShow: boolean }[]) {
77+
const crmService = await this.getCrmService(this.credential);
78+
if (crmService?.handleAttendeeNoShow) {
79+
await crmService.handleAttendeeNoShow(bookingUid, attendees);
80+
}
81+
}
7582
}

packages/features/handleMarkNoShow.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import { prisma } from "@calcom/prisma";
1010
import { WebhookTriggerEvents } from "@calcom/prisma/client";
1111
import type { TNoShowInputSchema } from "@calcom/trpc/server/routers/loggedInViewer/markNoShow.schema";
1212

13+
import handleSendingAttendeeNoShowDataToApps from "./noShow/handleSendingAttendeeNoShowDataToApps";
14+
15+
export type NoShowAttendees = { email: string; noShow: boolean }[];
16+
1317
const buildResultPayload = async (
1418
bookingUid: string,
1519
attendeeEmails: string[],
@@ -41,7 +45,7 @@ const logFailedResults = (results: PromiseSettledResult<any>[]) => {
4145
};
4246

4347
class ResponsePayload {
44-
attendees: { email: string; noShow: boolean }[];
48+
attendees: NoShowAttendees;
4549
noShowHost: boolean;
4650
message: string;
4751

@@ -101,6 +105,8 @@ const handleMarkNoShow = async ({
101105

102106
responsePayload.setAttendees(payload.attendees);
103107
responsePayload.setMessage(payload.message);
108+
109+
await handleSendingAttendeeNoShowDataToApps(bookingUid, attendees);
104110
}
105111

106112
if (noShowHost) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { z } from "zod";
2+
3+
import type { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
4+
import CrmManager from "@calcom/core/crmManager/crmManager";
5+
import logger from "@calcom/lib/logger";
6+
import prisma from "@calcom/prisma";
7+
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
8+
9+
import type { NoShowAttendees } from "../handleMarkNoShow";
10+
import { noShowEnabledApps } from "./noShowEnabledApps";
11+
12+
const log = logger.getSubLogger({ prefix: ["handleSendingNoShowDataToApps"] });
13+
14+
export default async function handleSendingAttendeeNoShowDataToApps(
15+
bookingUid: string,
16+
attendees: NoShowAttendees
17+
) {
18+
// Get event type metadata
19+
const eventTypeQuery = await prisma.booking.findFirst({
20+
where: {
21+
uid: bookingUid,
22+
},
23+
select: {
24+
eventType: {
25+
select: {
26+
metadata: true,
27+
},
28+
},
29+
},
30+
});
31+
if (!eventTypeQuery || !eventTypeQuery?.eventType?.metadata) {
32+
log.warn(`For no show, could not find eventType for bookingUid ${bookingUid}`);
33+
return;
34+
}
35+
36+
const eventTypeMetadataParse = EventTypeMetaDataSchema.safeParse(eventTypeQuery?.eventType?.metadata);
37+
if (!eventTypeMetadataParse.success) {
38+
log.error(`Malformed event type metadata for bookingUid ${bookingUid}`);
39+
return;
40+
}
41+
const eventTypeAppMetadata = eventTypeMetadataParse.data?.apps;
42+
43+
for (const slug in eventTypeAppMetadata) {
44+
if (noShowEnabledApps.includes(slug)) {
45+
const app = eventTypeAppMetadata[slug as keyof typeof eventTypeAppMetadata];
46+
47+
const appCategory = app.appCategories[0];
48+
49+
if (appCategory === "crm") {
50+
await handleCRMNoShow(bookingUid, app, attendees);
51+
}
52+
}
53+
}
54+
55+
return;
56+
}
57+
58+
async function handleCRMNoShow(
59+
bookingUid: string,
60+
appData: z.infer<typeof eventTypeAppCardZod>,
61+
attendees: NoShowAttendees
62+
) {
63+
// Handle checking if no she in CrmService
64+
65+
const credential = await prisma.credential.findFirst({
66+
where: {
67+
id: appData.credentialId,
68+
},
69+
include: {
70+
user: {
71+
select: {
72+
email: true,
73+
},
74+
},
75+
},
76+
});
77+
78+
if (!credential) {
79+
log.warn(`CRM credential not found for bookingUid ${bookingUid}`);
80+
return;
81+
}
82+
83+
const crm = new CrmManager(credential, appData);
84+
await crm.handleAttendeeNoShow(bookingUid, attendees);
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** Slugs of apps that have the option to send no show data to */
2+
export const noShowEnabledApps = ["salesforce"];

packages/types/CrmService.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,8 @@ export interface CRM {
3939
}) => Promise<Contact[]>;
4040
createContacts: (contactsToCreate: ContactCreateInput[], organizerEmail?: string) => Promise<Contact[]>;
4141
getAppOptions: () => any;
42+
handleAttendeeNoShow?: (
43+
bookingUid: string,
44+
attendees: { email: string; noShow: boolean }[]
45+
) => Promise<void>;
4246
}

0 commit comments

Comments
 (0)