From 6561b92f4072923a579c11d3bc3782469db5d529 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Thu, 16 Jan 2025 22:07:30 -0500 Subject: [PATCH] perf: Move CRM event creation to tasker (#18370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Upgrade jsforce to 3.6.2 * Refactor connecting to Salesforce * Revert yarn.lock changes * Add `TASKER_ENABLE_CRM_EVENT_CREATION` to .env * Add createCRMEvent scheduler * Schedule CRM event creating in `EventManager` * Add calendar event builder for CRM tasks * Do not write to person record if fields don't exist * Change type to expect string from tasker * Create CRM event * Create booking references * Type fixes * Migrate callback endpoint * Add jsforce node dependency * Migrate add endpoint * Import * Import package into crmService * Use new package types * Type fix * Update vite config * Push updated lockfile * Attempt to bump platform/libraries to unlock jsforce * Also update lockfile, naturally * bump platform libraries * feat: salesforce to tasker improvements (#18419) * feat: salesforce to tasker * refactor: event manager * tests: add unit tests for create CRM Event * Update vite.config * Add jsforce to vite config * Revert mint.json changes * Default to not enabling * Revert yarn.lock changes * Remove `TASKER_ENABLE_CRM_EVENT_CREATION` variable * Revert yarn.lock changes * feat: Round Robin weights future members toggle (#17782) Co-authored-by: Omar López * detailed customer card (#18511) Co-authored-by: Omar López * chore: app router - all sub-pages in `/apps` (#16976) * chore: apps/[slug] remove pages router * remove apps/[slug] pages from /future * chore: apps/installed remove pages router * chore: apps/installation remove pages router * remove Head element * fix metadata * fix test * fix another test * chore: apps/categories remove pages router * revert unneeded changes * update middleware * Remove * remove unused import and code * remove unused import and code again * fix * fix category page * add split icon * add /routing paths to middleware matcher * wip * remove HeadSeo from App.tsx * clean up head-seo test * add generateAppMetadata * use generateAppMetadata in apps/[slug] page * delete file * remove log * fix * fix * fix apps/installed pages * fix cateogires pages * fix * fix imports * wip * fix * fix * fix metadata * fix * redirect /apps/routing-forms to /routing * replace all usages of /apps/routing-forms to /routing * better naming * /routing -> /routing/forms * fix * fix * fix * fix * remove backPath as it is irrelevant when withoutMain is true * fix type checks * fix type check in apps/[slug] * refactors * fix * fix test * fix * fix * fix * Replace multiple leading slashes with a single slash * migrate routing-forms too * add re routing * fix * add redirection --------- Co-authored-by: Peer Richelsen Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> * chore: app router 404 page (#18597) * wip * wip * fix not found page * render middleware for /settings pages * fix * remove global-error page * add metadata to not-found page * make not-found page static * remove 404 * adding not-found to middleware is not necessary * add every routes to config.matcher * fix test * fix style * use i18n string * fix tests * fix * fix * revert unneeded changes * fix * fix * fix * fix style * fix * remove 404 * remove log * fix * fix * fix * fix * better naming * parallel testing --------- Co-authored-by: Benny Joo * feat: render custom error page for unexpected sever error + remove` pages/_error` (#18606) * remove page/_error * refactor app/error * fix: app/not-found cannot be a static page (#18610) * Async False (#18611) * chore: redirect to /500 if pathname does not exist + better error handling (#18615) * fix lint error * fix booking page and better error handling * chore: gracefully handle 404s from pages router's dynamic pages + tests (#18618) * restore pages/_error * set custom header in pages/_error * handle it in middleware * add test * remove logs * better test description * chore: try using custom 404 in pages/_error (#18622) * fix: parsing teamId (#18623) * chore: restore error pages for pages router (#18625) * disable emails to all guests (#18628) Co-authored-by: CarinaWolli * revert: "feat: bulk shorten links with dub.links.createMany (#18539)" (#18587) This reverts commit 4902c6a0f8bdfcc2afaa5c5c227c806d7537398e. Co-authored-by: Alex van Andel * chore: release v4.8.18 * fix: disable sending sms when email is present (#18632) * fix: disable sending sms when email is present * fix: update test * fix: main lint errors (#18634) * fix: error-page.tsx related lint errors * fix: lint no continue-on-error * Adding more lint fixes * Bring back annotate code linting results * Bring back linting continue-on-error * Slimline lint * feat: do not show automation webhooks in webhook list (#18607) * fix: make ESLint work and fix lint errors that were undetected before (#18639) * fix eslint config * fix * add it to dev dep * fix * sync eslint version * force ts-node to compile our ESLint plugin's TS files into CommonJS (which ESLint requires) * fix some lint errors * fix lint errors * remove duplicate classname * make @typescript-eslint/ban-types a warn for packages/trpc files * fix lint errors in trpc * fix lint errors in trpc - 2 * fix * fix * fix lint warnings * chore: clean up config.matcher in middleware (#18638) Co-authored-by: Alex van Andel * Allow lint to error but continue (unblock pipeline) * fix: potential fix for flaky layout shift (#18651) * potential fix: layout shift * fix lint error * feat: update translations via @replexica (#18598) * chore: sync with main * feat: update translations via @replexica --------- Co-authored-by: Replexica * Update yarn.lock * nit: let tasker handle payload stringification --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Alex van Andel Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Morgan Vernay Co-authored-by: Omar López Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Nizzy <140507264+nizzyabi@users.noreply.github.com> Co-authored-by: Benny Joo Co-authored-by: Peer Richelsen Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli Co-authored-by: Keith Williams Co-authored-by: GitHub Actions Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com> Co-authored-by: Calcom Bot <109866826+calcom-bot@users.noreply.github.com> Co-authored-by: Replexica --- .../ooo/organizations-users-ooo.e2e-spec.ts | 1 - .../app-store/salesforce/lib/CrmService.ts | 5 + packages/core/EventManager.ts | 24 +- .../core/crmManager/tasker/crmScheduler.ts | 9 + packages/features/flags/config.ts | 1 + packages/features/flags/hooks/index.ts | 1 + packages/features/tasker/tasker.ts | 1 + .../crm/__tests__/createCRMEvent.test.ts | 258 ++++++++++++++++++ .../tasker/tasks/crm/createCRMEvent.ts | 144 ++++++++++ .../tasks/crm/lib/buildCalendarEvent.ts | 90 ++++++ packages/features/tasker/tasks/crm/schema.ts | 5 + packages/features/tasker/tasks/index.ts | 1 + .../migration.sql | 9 + 13 files changed, 547 insertions(+), 2 deletions(-) create mode 100644 packages/core/crmManager/tasker/crmScheduler.ts create mode 100644 packages/features/tasker/tasks/crm/__tests__/createCRMEvent.test.ts create mode 100644 packages/features/tasker/tasks/crm/createCRMEvent.ts create mode 100644 packages/features/tasker/tasks/crm/lib/buildCalendarEvent.ts create mode 100644 packages/features/tasker/tasks/crm/schema.ts create mode 100644 packages/prisma/migrations/20241230140747531_add_salesforce_crm_tasker/migration.sql diff --git a/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo.e2e-spec.ts index 1e3c511925aea9..d215803b9a97dc 100644 --- a/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/controllers/users/ooo/organizations-users-ooo.e2e-spec.ts @@ -387,7 +387,6 @@ describe("Organizations User OOO Endpoints", () => { } // test sort expect(data[1].id).toEqual(oooCreatedViaApiId); - }); }); diff --git a/packages/app-store/salesforce/lib/CrmService.ts b/packages/app-store/salesforce/lib/CrmService.ts index 8ecff65bebf560..766baefa5800eb 100644 --- a/packages/app-store/salesforce/lib/CrmService.ts +++ b/packages/app-store/salesforce/lib/CrmService.ts @@ -910,6 +910,11 @@ export default class SalesforceCRMService implements CRM { const fieldsToWriteOn = Object.keys(onBookingWriteToRecordFields); const existingFields = await this.ensureFieldsExistOnObject(fieldsToWriteOn, personRecordType); + if (!existingFields.length) { + this.log.warn(`No fields found for record type ${personRecordType}`); + return; + } + const personRecord = await this.fetchPersonRecord(contactId, existingFields, personRecordType); if (!personRecord) { this.log.warn(`No personRecord found for contactId ${contactId}`); diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index 1af0d6de314230..9c4280c4bebe27 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -9,6 +9,8 @@ import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApi import { appKeysSchema as calVideoKeysSchema } from "@calcom/app-store/dailyvideo/zod"; import { getLocationFromApp, MeetLocationType } from "@calcom/app-store/locations"; import getApps from "@calcom/app-store/utils"; +import CRMScheduler from "@calcom/core/crmManager/tasker/crmScheduler"; +import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { getUid } from "@calcom/lib/CalEventParser"; import logger from "@calcom/lib/logger"; import { @@ -215,7 +217,9 @@ export default class EventManager { return result.type.includes("_calendar"); }; - results.push(...(await this.createAllCRMEvents(clonedCalEvent))); + const createdCRMEvents = await this.createAllCRMEvents(clonedCalEvent); + + results.push(...createdCRMEvents); // References can be any type: calendar/video const referencesToCreate = results.map((result) => { @@ -970,8 +974,26 @@ export default class EventManager { private async createAllCRMEvents(event: CalendarEvent) { const createdEvents = []; + + const featureRepo = new FeaturesRepository(); + const isTaskerEnabledForSalesforceCrm = event.team?.id + ? await featureRepo.checkIfTeamHasFeature(event.team.id, "salesforce-crm-tasker") + : false; + const uid = getUid(event); for (const credential of this.crmCredentials) { + if (isTaskerEnabledForSalesforceCrm) { + if (!event.uid) { + console.error( + `Missing bookingId when scheduling CRM event creation on event type ${event?.eventTypeId}` + ); + continue; + } + + await CRMScheduler.createEvent({ bookingUid: event.uid }); + continue; + } + const currentAppOption = this.getAppOptionsFromEventMetadata(credential); const crm = new CrmManager(credential, currentAppOption); diff --git a/packages/core/crmManager/tasker/crmScheduler.ts b/packages/core/crmManager/tasker/crmScheduler.ts new file mode 100644 index 00000000000000..c37e23e6afdfa8 --- /dev/null +++ b/packages/core/crmManager/tasker/crmScheduler.ts @@ -0,0 +1,9 @@ +import tasker from "@calcom/features/tasker"; + +class CRMScheduler { + static async createEvent({ bookingUid }: { bookingUid: string }) { + return tasker.create("createCRMEvent", { bookingUid }); + } +} + +export default CRMScheduler; diff --git a/packages/features/flags/config.ts b/packages/features/flags/config.ts index a03ca9ed31aa33..bf4145e9279a43 100644 --- a/packages/features/flags/config.ts +++ b/packages/features/flags/config.ts @@ -17,4 +17,5 @@ export type AppFlags = { attributes: boolean; "organizer-request-email-v2": boolean; "domain-wide-delegation": boolean; + "salesforce-crm-tasker": boolean; }; diff --git a/packages/features/flags/hooks/index.ts b/packages/features/flags/hooks/index.ts index 1b248362efc2d7..ca2672d829d3dc 100644 --- a/packages/features/flags/hooks/index.ts +++ b/packages/features/flags/hooks/index.ts @@ -16,6 +16,7 @@ const initialData: AppFlags = { attributes: false, "organizer-request-email-v2": false, "domain-wide-delegation": false, + "salesforce-crm-tasker": false, }; if (process.env.NEXT_PUBLIC_IS_E2E) { diff --git a/packages/features/tasker/tasker.ts b/packages/features/tasker/tasker.ts index f1449473578c06..c326af343428a7 100644 --- a/packages/features/tasker/tasker.ts +++ b/packages/features/tasker/tasker.ts @@ -17,6 +17,7 @@ type TaskPayloads = { translateEventTypeData: z.infer< typeof import("./tasks/translateEventTypeData").ZTranslateEventDataPayloadSchema >; + createCRMEvent: z.infer; }; export type TaskTypes = keyof TaskPayloads; export type TaskHandler = (payload: string) => Promise; diff --git a/packages/features/tasker/tasks/crm/__tests__/createCRMEvent.test.ts b/packages/features/tasker/tasks/crm/__tests__/createCRMEvent.test.ts new file mode 100644 index 00000000000000..cf8f20d171b616 --- /dev/null +++ b/packages/features/tasker/tasks/crm/__tests__/createCRMEvent.test.ts @@ -0,0 +1,258 @@ +import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock"; + +import { describe, expect, it, beforeEach, vi } from "vitest"; + +import { BookingStatus } from "@calcom/prisma/enums"; + +import { createCRMEvent } from "../createCRMEvent"; + +interface User { + id: number; + email: string; + name: string; +} + +interface EventTypeMetadata { + apps: { + salesforce: { + enabled: boolean; + credentialId: number; + appCategories: string[]; + }; + }; +} + +interface Booking { + id: number; + uid: string; + status: BookingStatus; + title: string; + startTime: Date; + endTime: Date; + user: User; + eventType: { + metadata: EventTypeMetadata; + }; +} + +interface CRMCredential { + id: number; + type: string; + key: { + access_token?: string; + refresh_token?: string; + expiry_date?: number; + key1?: string; + }; + userId: number; +} + +interface CRMManagerInterface { + createEvent: (event: unknown) => Promise<{ id: string }>; +} + +vi.mock("../lib/buildCalendarEvent", () => ({ + default: vi.fn().mockResolvedValue({ + title: "Test Event", + description: "Test Description", + startTime: new Date(), + endTime: new Date(), + }), +})); + +const mockCreateEvent = vi.fn().mockResolvedValue({ id: "sf-event-123" }); +vi.mock("@calcom/core/crmManager/crmManager", () => ({ + default: class MockCrmManager { + private credential: CRMCredential; + + constructor(credential: CRMCredential) { + this.credential = credential; + } + + createEvent = mockCreateEvent; + + getManager(): CRMManagerInterface { + return { + createEvent: this.createEvent, + }; + } + }, +})); + +describe("createCRMEvent", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockCreateEvent.mockClear(); + // Clear Prisma mocks + prismaMock.booking.findUnique.mockReset(); + prismaMock.credential.findUnique.mockReset(); + prismaMock.bookingReference.createMany.mockReset(); + }); + + it("should successfully create a Salesforce CRM event", async () => { + const mockBooking: Booking = { + id: 1, + uid: "booking-123", + status: BookingStatus.ACCEPTED, + title: "Test Booking", + startTime: new Date(), + endTime: new Date(), + user: { + id: 1, + email: "test@example.com", + name: "Test User", + }, + eventType: { + metadata: { + apps: { + salesforce: { + enabled: true, + credentialId: 1, + appCategories: ["crm"], + }, + }, + }, + }, + }; + + const mockCredential: CRMCredential = { + id: 1, + type: "salesforce_crm", + key: { + access_token: "mock_token", + refresh_token: "mock_refresh", + expiry_date: new Date().getTime() + 3600000, + }, + userId: 1, + }; + + // Set up Prisma mocks with proper return values + prismaMock.booking.findUnique.mockResolvedValueOnce(mockBooking); + prismaMock.credential.findUnique.mockResolvedValueOnce(mockCredential); + prismaMock.bookingReference.createMany.mockResolvedValueOnce({ count: 1 }); + + const payload = JSON.stringify({ + bookingUid: "booking-123", + }); + + await createCRMEvent(payload); + + expect(mockCreateEvent).toHaveBeenCalled(); + expect(prismaMock.bookingReference.createMany).toHaveBeenCalledWith({ + data: [ + { + type: "salesforce_crm", + uid: "sf-event-123", + meetingId: "sf-event-123", + credentialId: 1, + bookingId: 1, + }, + ], + }); + }); + + it("should throw error for invalid payload", async () => { + const invalidPayload = JSON.stringify({ + invalidField: "test", + }); + + await expect(createCRMEvent(invalidPayload)).rejects.toThrow("malformed payload in createCRMEvent"); + }); + + it("should throw error when booking is not found", async () => { + prismaMock.booking.findUnique.mockResolvedValue(null); + + const payload = JSON.stringify({ + bookingUid: "non-existent-booking", + }); + + await expect(createCRMEvent(payload)).rejects.toThrow("booking not found"); + }); + + it("should handle case when Salesforce CRM is not enabled", async () => { + const mockBooking: Booking = { + id: 1, + uid: "booking-123", + status: BookingStatus.ACCEPTED, + title: "Test Booking", + startTime: new Date(), + endTime: new Date(), + user: { + id: 1, + email: "test@example.com", + name: "Test User", + }, + eventType: { + metadata: { + apps: { + salesforce: { + enabled: false, + credentialId: 1, + appCategories: ["crm"], + }, + }, + }, + }, + }; + + prismaMock.booking.findUnique.mockResolvedValue(mockBooking); + + const payload = JSON.stringify({ + bookingUid: "booking-123", + }); + + await createCRMEvent(payload); + + expect(prismaMock.bookingReference.createMany).toHaveBeenCalledWith({ + data: [], + }); + }); + + it("should handle CRM creation error gracefully", async () => { + const mockBooking: Booking = { + id: 1, + uid: "booking-123", + status: BookingStatus.ACCEPTED, + title: "Test Booking", + startTime: new Date(), + endTime: new Date(), + user: { + id: 1, + email: "test@example.com", + name: "Test User", + }, + eventType: { + metadata: { + apps: { + salesforce: { + enabled: true, + credentialId: 1, + appCategories: ["crm"], + }, + }, + }, + }, + }; + + const mockCredential: CRMCredential = { + id: 1, + type: "salesforce_crm", + key: { key1: "value1" }, + userId: 1, + }; + + prismaMock.booking.findUnique.mockResolvedValue(mockBooking); + prismaMock.credential.findFirst.mockResolvedValue(mockCredential); + mockCreateEvent.mockRejectedValue(new Error("Salesforce API error")); + + const payload = JSON.stringify({ + bookingUid: "booking-123", + }); + + await createCRMEvent(payload); + + expect(prismaMock.bookingReference.createMany).toHaveBeenCalledWith({ + data: [], + }); + }); +}); diff --git a/packages/features/tasker/tasks/crm/createCRMEvent.ts b/packages/features/tasker/tasks/crm/createCRMEvent.ts new file mode 100644 index 00000000000000..dc436191074ec8 --- /dev/null +++ b/packages/features/tasker/tasks/crm/createCRMEvent.ts @@ -0,0 +1,144 @@ +import type { Prisma } from "@prisma/client"; + +import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"; +import logger from "@calcom/lib/logger"; +import prisma from "@calcom/prisma"; +import { BookingStatus } from "@calcom/prisma/enums"; +import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; + +import buildCalendarEvent from "./lib/buildCalendarEvent"; +import { createCRMEventSchema } from "./schema"; + +const log = logger.getSubLogger({ prefix: [`[[tasker] createCRMEvent`] }); + +export async function createCRMEvent(payload: string): Promise { + try { + const parsedPayload = createCRMEventSchema.safeParse(JSON.parse(payload)); + + if (!parsedPayload.success) { + throw new Error(`malformed payload in createCRMEvent: ${parsedPayload.error}`); + } + const { bookingUid } = parsedPayload.data; + + const booking = await prisma.booking.findUnique({ + where: { + uid: bookingUid, + status: BookingStatus.ACCEPTED, + }, + include: { + user: { + select: { + name: true, + email: true, + locale: true, + username: true, + timeZone: true, + }, + }, + eventType: { + select: { + metadata: true, + }, + }, + references: { + select: { + type: true, + }, + }, + }, + }); + + if (!booking) { + throw new Error(`booking not found for uid: ${bookingUid}`); + } + + if (!booking.user) { + throw new Error(`user not found for uid: ${bookingUid}`); + } + + const eventTypeMetadata = EventTypeMetaDataSchema.safeParse(booking.eventType?.metadata); + + if (!eventTypeMetadata.success) { + throw new Error(`malformed event type metadata: ${eventTypeMetadata.error}`); + } + + const eventTypeAppMetadata = eventTypeMetadata.data?.apps; + + if (!eventTypeAppMetadata) { + throw new Error(`event type app metadata not found for booking ${bookingUid}`); + } + + const calendarEvent = await buildCalendarEvent(bookingUid); + + const bookingReferencesToCreate: Prisma.BookingReferenceUncheckedCreateInput[] = []; + + // Find enabled CRM apps for the event type + for (const appSlug of Object.keys(eventTypeAppMetadata)) { + try { + const appData = eventTypeAppMetadata[appSlug as keyof typeof eventTypeAppMetadata]; + const appParse = eventTypeAppCardZod.safeParse(appData); + + if (!appParse.success) { + log.error(`Error parsing event type app data for bookingUid ${bookingUid}`, appParse?.error); + continue; + } + + const app = appParse.data; + + if ( + !app.appCategories || + !app.appCategories.some((category: string) => category === "crm") || + !app.enabled || + !app.credentialId + ) + continue; + + const crmCredential = await prisma.credential.findUnique({ + where: { + id: app.credentialId, + }, + include: { + user: { + select: { + email: true, + }, + }, + }, + }); + + if (!crmCredential) { + throw new Error(`Credential not found for credentialId: ${app.credentialId}`); + } + + const CrmManager = (await import("@calcom/core/crmManager/crmManager")).default; + + const crm = new CrmManager(crmCredential, app); + + const results = await crm.createEvent(calendarEvent).catch((error) => { + log.error(`Error creating crm event for credentialId ${app.credentialId}`, error); + }); + + if (results) { + bookingReferencesToCreate.push({ + type: crmCredential.type, + uid: results.id, + meetingId: results.id, + credentialId: crmCredential.id, + bookingId: booking.id, + }); + } + } catch (error) { + log.error(`Error processing CRM app ${appSlug} for booking ${bookingUid}:`, error); + // Continue with next app even if one fails + continue; + } + } + + await prisma.bookingReference.createMany({ + data: bookingReferencesToCreate, + }); + } catch (error) { + log.error(`Error in createCRMEvent for payload: ${payload}:`, error); + throw error; + } +} diff --git a/packages/features/tasker/tasks/crm/lib/buildCalendarEvent.ts b/packages/features/tasker/tasks/crm/lib/buildCalendarEvent.ts new file mode 100644 index 00000000000000..d1ea41f6cc6692 --- /dev/null +++ b/packages/features/tasker/tasks/crm/lib/buildCalendarEvent.ts @@ -0,0 +1,90 @@ +import { addVideoCallDataToEvent } from "@calcom/features/bookings/lib/handleNewBooking/addVideoCallDataToEvent"; +import { getTranslation } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; +import type { CalendarEvent } from "@calcom/types/Calendar"; + +const buildCalendarEvent: (bookingUid: string) => Promise = async (bookingUid: string) => { + const booking = await prisma.booking.findFirst({ + where: { + uid: bookingUid, + }, + include: { + user: { + select: { + name: true, + email: true, + locale: true, + username: true, + timeZone: true, + timeFormat: true, + }, + }, + eventType: { + select: { + slug: true, + }, + }, + attendees: { + select: { + email: true, + locale: true, + name: true, + timeZone: true, + phoneNumber: true, + }, + }, + references: true, + }, + }); + + if (!booking) { + throw new Error(`booking not found for bookings: ${bookingUid}`); + } + + if (!booking.user) { + throw new Error(`organizer not found for booking: ${bookingUid}`); + } + + if (!booking.eventType) { + throw new Error(`event type not found for booking ${bookingUid}`); + } + + const organizerT = await getTranslation(booking.user?.locale ?? "en", "common"); + + const attendeePromises = []; + for (const attendee of booking.attendees) { + attendeePromises.push( + getTranslation(attendee.locale ?? "en", "common").then((tAttendee) => ({ + email: attendee.email, + name: attendee.name, + timeZone: attendee.timeZone, + language: { translate: tAttendee, locale: attendee.locale ?? "en" }, + phoneNumber: attendee.phoneNumber || undefined, + })) + ); + } + + const attendeeList = await Promise.all(attendeePromises); + + let calendarEvent: CalendarEvent = { + type: booking.eventType.slug, + title: booking.title, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), + organizer: { + email: booking.user.email, + name: booking.user.name || "Nameless", + username: booking.user?.username || "No username", + language: { translate: organizerT, locale: booking.user?.locale ?? "en" }, + timeZone: booking.user.timeZone, + }, + attendees: attendeeList, + location: booking.location, + }; + + calendarEvent = addVideoCallDataToEvent(booking.references, calendarEvent); + + return calendarEvent; +}; + +export default buildCalendarEvent; diff --git a/packages/features/tasker/tasks/crm/schema.ts b/packages/features/tasker/tasks/crm/schema.ts new file mode 100644 index 00000000000000..bf2a09567908a6 --- /dev/null +++ b/packages/features/tasker/tasks/crm/schema.ts @@ -0,0 +1,5 @@ +import z from "zod"; + +export const createCRMEventSchema = z.object({ + bookingUid: z.string(), +}); diff --git a/packages/features/tasker/tasks/index.ts b/packages/features/tasker/tasks/index.ts index e8adead8dc630c..1c9af9fd6dd6ad 100644 --- a/packages/features/tasker/tasks/index.ts +++ b/packages/features/tasker/tasks/index.ts @@ -19,6 +19,7 @@ const tasks: Record Promise> = { sendSms: () => Promise.resolve(() => Promise.reject(new Error("Not implemented"))), translateEventTypeData: () => import("./translateEventTypeData").then((module) => module.translateEventTypeData), + createCRMEvent: () => import("./crm/createCRMEvent").then((module) => module.createCRMEvent), }; export default tasks; diff --git a/packages/prisma/migrations/20241230140747531_add_salesforce_crm_tasker/migration.sql b/packages/prisma/migrations/20241230140747531_add_salesforce_crm_tasker/migration.sql new file mode 100644 index 00000000000000..c535803aab29f8 --- /dev/null +++ b/packages/prisma/migrations/20241230140747531_add_salesforce_crm_tasker/migration.sql @@ -0,0 +1,9 @@ +INSERT INTO + "Feature" (slug, enabled, description, "type") +VALUES + ( + 'salesforce-crm-tasker', + false, + 'Whether to use tasker for Salesforce CRM event creation or not on a team/user basis.', + 'OPERATIONAL' + ) ON CONFLICT (slug) DO NOTHING;