Skip to content

Commit

Permalink
feat: Send Attendee No Show Data To Salesforce (#17423)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeauyeung authored Oct 31, 2024
1 parent ebd5ca6 commit 06f83e6
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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>
);
Expand Down
75 changes: 75 additions & 0 deletions packages/app-store/salesforce/lib/CrmService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/salesforce/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
7 changes: 7 additions & 0 deletions packages/core/crmManager/crmManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
8 changes: 7 additions & 1 deletion packages/features/handleMarkNoShow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down Expand Up @@ -41,7 +45,7 @@ const logFailedResults = (results: PromiseSettledResult<any>[]) => {
};

class ResponsePayload {
attendees: { email: string; noShow: boolean }[];
attendees: NoShowAttendees;
noShowHost: boolean;
message: string;

Expand Down Expand Up @@ -101,6 +105,8 @@ const handleMarkNoShow = async ({

responsePayload.setAttendees(payload.attendees);
responsePayload.setMessage(payload.message);

await handleSendingAttendeeNoShowDataToApps(bookingUid, attendees);
}

if (noShowHost) {
Expand Down
85 changes: 85 additions & 0 deletions packages/features/noShow/handleSendingAttendeeNoShowDataToApps.ts
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 2 additions & 0 deletions packages/features/noShow/noShowEnabledApps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Slugs of apps that have the option to send no show data to */
export const noShowEnabledApps = ["salesforce"];
4 changes: 4 additions & 0 deletions packages/types/CrmService.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}

0 comments on commit 06f83e6

Please sign in to comment.