perf: Move CRM event creation to tasker (#18370)
* 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 <zomars@me.com>
* detailed customer card (#18511)
Co-authored-by: Omar López <zomars@me.com>
* 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 <Head>
* 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 <peeroke@gmail.com>
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 <sldisek783@gmail.com>
* 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 <wollencarina@gmail.com>
* revert: "feat: bulk shorten links with dub.links.createMany (#18539)" (#18587)
This reverts commit 4902c6a0f8.
Co-authored-by: Alex van Andel <me@alexvanandel.com>
* 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 <me@alexvanandel.com>
* 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 <support@replexica.com>
* 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 <me@alexvanandel.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Omar López <zomars@me.com>
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 <sldisek783@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
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 <wollencarina@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: GitHub Actions <github-actions@github.com>
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 <support@replexica.com>
This commit is contained in:
13 files changed
+547
-2
No files matched your search
-1
@@ -387,7 +387,6 @@ describe("Organizations User OOO Endpoints", () => {
|
||||
}
|
||||
// test sort
|
||||
expect(data[1].id).toEqual(oooCreatedViaApiId);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -17,4 +17,5 @@ export type AppFlags = {
|
||||
attributes: boolean;
|
||||
"organizer-request-email-v2": boolean;
|
||||
"domain-wide-delegation": boolean;
|
||||
"salesforce-crm-tasker": boolean;
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -17,6 +17,7 @@ type TaskPayloads = {
|
||||
translateEventTypeData: z.infer<
|
||||
typeof import("./tasks/translateEventTypeData").ZTranslateEventDataPayloadSchema
|
||||
>;
|
||||
createCRMEvent: z.infer<typeof import("./tasks/crm/schema").createCRMEventSchema>;
|
||||
};
|
||||
export type TaskTypes = keyof TaskPayloads;
|
||||
export type TaskHandler = (payload: string) => Promise<void>;
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<CalendarEvent> = 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;
|
||||
@@ -0,0 +1,5 @@
|
||||
import z from "zod";
|
||||
|
||||
export const createCRMEventSchema = z.object({
|
||||
bookingUid: z.string(),
|
||||
});
|
||||
@@ -19,6 +19,7 @@ const tasks: Record<TaskTypes, () => Promise<TaskHandler>> = {
|
||||
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;
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user