diff --git a/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx b/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx
index c6d37c7427a990..1e844706b86404 100644
--- a/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx
+++ b/packages/app-store/salesforce/components/EventTypeAppCardInterface.tsx
@@ -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({
) : null}
+
+
+
{
+ setAppData("sendNoShowAttendeeData", checked);
+ }}
+ />
+ {sendNoShowAttendeeData ? (
+
+
Field name to check (must be checkbox data type)
+
setAppData("sendNoShowAttendeeDataField", e.target.value)}
+ />
+
+ ) : null}
+
>
);
diff --git a/packages/app-store/salesforce/lib/CrmService.ts b/packages/app-store/salesforce/lib/CrmService.ts
index f6ec791f6136a8..a9be986419b685 100644
--- a/packages/app-store/salesforce/lib/CrmService.ts
+++ b/packages/app-store/salesforce/lib/CrmService.ts
@@ -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) {
diff --git a/packages/app-store/salesforce/zod.ts b/packages/app-store/salesforce/zod.ts
index ad3eb41e12e0db..a2c7e016ef170d 100644
--- a/packages/app-store/salesforce/zod.ts
+++ b/packages/app-store/salesforce/zod.ts
@@ -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({
diff --git a/packages/core/crmManager/crmManager.ts b/packages/core/crmManager/crmManager.ts
index 835142effe71d6..fd530b654fc590 100644
--- a/packages/core/crmManager/crmManager.ts
+++ b/packages/core/crmManager/crmManager.ts
@@ -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);
+ }
+ }
}
diff --git a/packages/features/handleMarkNoShow.ts b/packages/features/handleMarkNoShow.ts
index becf9724cd680c..784d3a70be8f14 100644
--- a/packages/features/handleMarkNoShow.ts
+++ b/packages/features/handleMarkNoShow.ts
@@ -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[]) => {
};
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) {
diff --git a/packages/features/noShow/handleSendingAttendeeNoShowDataToApps.ts b/packages/features/noShow/handleSendingAttendeeNoShowDataToApps.ts
new file mode 100644
index 00000000000000..f46de7b0416ccc
--- /dev/null
+++ b/packages/features/noShow/handleSendingAttendeeNoShowDataToApps.ts
@@ -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,
+ 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);
+}
diff --git a/packages/features/noShow/noShowEnabledApps.ts b/packages/features/noShow/noShowEnabledApps.ts
new file mode 100644
index 00000000000000..75bbb121f8e9c4
--- /dev/null
+++ b/packages/features/noShow/noShowEnabledApps.ts
@@ -0,0 +1,2 @@
+/** Slugs of apps that have the option to send no show data to */
+export const noShowEnabledApps = ["salesforce"];
diff --git a/packages/types/CrmService.d.ts b/packages/types/CrmService.d.ts
index 029bc59a600d13..a4275ca2bbbec2 100644
--- a/packages/types/CrmService.d.ts
+++ b/packages/types/CrmService.d.ts
@@ -39,4 +39,8 @@ export interface CRM {
}) => Promise;
createContacts: (contactsToCreate: ContactCreateInput[], organizerEmail?: string) => Promise;
getAppOptions: () => any;
+ handleAttendeeNoShow?: (
+ bookingUid: string,
+ attendees: { email: string; noShow: boolean }[]
+ ) => Promise;
}