feat: hubspot rr skip (#27310)
* feat: hubspot show owner availability for round robin * fix: translations * fix: use app name variable for text
This commit is contained in:
@@ -4465,6 +4465,8 @@
|
||||
"timestamp": "Timestamp",
|
||||
"json": "JSON",
|
||||
"hubspot_ignore_guests": "Do not create new records for guests added to the booking",
|
||||
"crm_book_directly_with_attendee_owner": "If attendee exists in {{appName}}, book directly with the owner",
|
||||
"crm_if_free_email_domain_skip_owner_check": "If attendee has a free email domain, skip the ownership check and round robin as normal",
|
||||
"saml_idp_not_authoritative_error": "This SAML identity provider is not authorized to manage your account. Please sign in with your original login method.",
|
||||
"audit_logs_organization_required": "You must be part of an organization to view audit logs.",
|
||||
"audit_logs_booking_not_found_or_permission_denied": "Booking not found or you do not have permission to view its audit logs.",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import process from "node:process";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
export const metadata = {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import * as hubspot from "@hubspot/api-client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import * as hubspot from "@hubspot/api-client";
|
||||
import type { TokenResponseIF } from "@hubspot/api-client/lib/codegen/oauth/models/TokenResponseIF";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
import { CrmFieldType } from "@calcom/app-store/_lib/crm-enums";
|
||||
import WriteToObjectSettings, {
|
||||
BookingActionEnum,
|
||||
} from "@calcom/app-store/_components/crm/WriteToObjectSettings";
|
||||
import { CrmFieldType } from "@calcom/app-store/_lib/crm-enums";
|
||||
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import { Section } from "@calcom/ui/components/section";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { appDataSchema } from "../zod";
|
||||
import { WhenToWrite } from "../zod";
|
||||
|
||||
@@ -32,6 +32,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
const overwriteContactOwner = getAppData("overwriteContactOwner") ?? false;
|
||||
const onBookingWriteToEventObject = getAppData("onBookingWriteToEventObject") ?? false;
|
||||
const onBookingWriteToEventObjectFields = getAppData("onBookingWriteToEventObjectFields") ?? {};
|
||||
const roundRobinLeadSkip = getAppData("roundRobinLeadSkip") ?? false;
|
||||
const ifFreeEmailDomainSkipOwnerCheck = getAppData("ifFreeEmailDomainSkipOwnerCheck") ?? false;
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
@@ -131,6 +133,47 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
supportedWriteTriggers={[WhenToWrite.EVERY_BOOKING]}
|
||||
/>
|
||||
</Section.SubSection>
|
||||
|
||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN ? (
|
||||
<>
|
||||
<Section.SubSection>
|
||||
<Section.SubSectionHeader
|
||||
icon="users"
|
||||
title={t("crm_book_directly_with_attendee_owner", { appName: app.name })}
|
||||
labelFor="book-directly-with-attendee-owner">
|
||||
<Switch
|
||||
size="sm"
|
||||
id="book-directly-with-attendee-owner"
|
||||
checked={roundRobinLeadSkip}
|
||||
onCheckedChange={(checked) => {
|
||||
setAppData("roundRobinLeadSkip", checked);
|
||||
if (checked) {
|
||||
setAppData("enabled", checked);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Section.SubSectionHeader>
|
||||
</Section.SubSection>
|
||||
{roundRobinLeadSkip && (
|
||||
<Section.SubSection>
|
||||
<Section.SubSectionHeader
|
||||
icon="users"
|
||||
title={t("crm_if_free_email_domain_skip_owner_check")}
|
||||
labelFor="if-free-email-domain-skip-owner-check">
|
||||
<Switch
|
||||
size="sm"
|
||||
id="if-free-email-domain-skip-owner-check"
|
||||
checked={ifFreeEmailDomainSkipOwnerCheck}
|
||||
onCheckedChange={(checked) => {
|
||||
setAppData("ifFreeEmailDomainSkipOwnerCheck", checked);
|
||||
}}
|
||||
/>
|
||||
</Section.SubSectionHeader>
|
||||
<Alert severity="info" title={t("skip_rr_description")} />
|
||||
</Section.SubSection>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</Section.Content>
|
||||
</AppCard>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { metadata } from "./_metadata";
|
||||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
||||
export { metadata } from "./_metadata";
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
import { setupAndTeardown } from "@calcom/testing/lib/bookingScenario/setupAndTeardown";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { CRM } from "@calcom/types/CrmService";
|
||||
import { setupAndTeardown } from "@calcom/testing/lib/bookingScenario/setupAndTeardown";
|
||||
import type { TFunction } from "i18next";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { CrmFieldType, DateFieldType, WhenToWrite } from "../../_lib/crm-enums";
|
||||
import type { appDataSchema } from "../zod";
|
||||
|
||||
@@ -18,6 +16,7 @@ const {
|
||||
mockGetAppKeysFromSlug,
|
||||
mockTrackingRepository,
|
||||
mockBookingRepository,
|
||||
mockCheckIfFreeEmailDomain,
|
||||
}: {
|
||||
mockHubspotClient: {
|
||||
crm: {
|
||||
@@ -28,7 +27,7 @@ const {
|
||||
create: ReturnType<typeof vi.fn>;
|
||||
getById: ReturnType<typeof vi.fn>;
|
||||
update: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
};
|
||||
};
|
||||
objects: {
|
||||
meetings: {
|
||||
@@ -40,7 +39,7 @@ const {
|
||||
};
|
||||
};
|
||||
associations: { batchApi: { create: ReturnType<typeof vi.fn> } };
|
||||
owners: { ownersApi: { getPage: ReturnType<typeof vi.fn> } };
|
||||
owners: { ownersApi: { getPage: ReturnType<typeof vi.fn>; getById: ReturnType<typeof vi.fn> } };
|
||||
};
|
||||
oauth: { tokensApi: { createToken: ReturnType<typeof vi.fn> } };
|
||||
setAccessToken: ReturnType<typeof vi.fn>;
|
||||
@@ -48,6 +47,7 @@ const {
|
||||
mockGetAppKeysFromSlug: ReturnType<typeof vi.fn>;
|
||||
mockTrackingRepository: { findByBookingUid: ReturnType<typeof vi.fn> };
|
||||
mockBookingRepository: { findBookingByUid: ReturnType<typeof vi.fn> };
|
||||
mockCheckIfFreeEmailDomain: ReturnType<typeof vi.fn>;
|
||||
} = vi.hoisted(() => {
|
||||
const mockHubspotClient = {
|
||||
crm: {
|
||||
@@ -83,6 +83,7 @@ const {
|
||||
owners: {
|
||||
ownersApi: {
|
||||
getPage: vi.fn(),
|
||||
getById: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -107,7 +108,15 @@ const {
|
||||
findBookingByUid: vi.fn(),
|
||||
};
|
||||
|
||||
return { mockHubspotClient, mockGetAppKeysFromSlug, mockTrackingRepository, mockBookingRepository };
|
||||
const mockCheckIfFreeEmailDomain = vi.fn();
|
||||
|
||||
return {
|
||||
mockHubspotClient,
|
||||
mockGetAppKeysFromSlug,
|
||||
mockTrackingRepository,
|
||||
mockBookingRepository,
|
||||
mockCheckIfFreeEmailDomain,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@hubspot/api-client", () => {
|
||||
@@ -144,6 +153,10 @@ vi.mock("@calcom/features/bookings/repositories/BookingRepository", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain", () => ({
|
||||
checkIfFreeEmailDomain: mockCheckIfFreeEmailDomain,
|
||||
}));
|
||||
|
||||
import BuildCrmService from "./CrmService";
|
||||
|
||||
describe("HubspotCalendarService", () => {
|
||||
@@ -284,6 +297,198 @@ describe("HubspotCalendarService", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
describe("forRoundRobinSkip param is passed", () => {
|
||||
it("should return contact with owner info when forRoundRobinSkip is true", async () => {
|
||||
mockAppOptions({ roundRobinLeadSkip: true });
|
||||
|
||||
mockHubspotClient.crm.contacts.searchApi.doSearch.mockResolvedValueOnce({
|
||||
results: [
|
||||
{
|
||||
id: "contact-1",
|
||||
properties: {
|
||||
email: "test@example.com",
|
||||
hubspot_owner_id: "12345",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockHubspotClient.crm.owners.ownersApi.getById.mockResolvedValueOnce({
|
||||
id: "12345",
|
||||
email: "owner@example.com",
|
||||
});
|
||||
|
||||
const result = await service.getContacts({
|
||||
emails: "test@example.com",
|
||||
forRoundRobinSkip: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "contact-1",
|
||||
email: "test@example.com",
|
||||
ownerId: "12345",
|
||||
ownerEmail: "owner@example.com",
|
||||
recordType: "CONTACT",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockHubspotClient.crm.contacts.searchApi.doSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
properties: ["hs_object_id", "email", "hubspot_owner_id"],
|
||||
})
|
||||
);
|
||||
|
||||
expect(mockHubspotClient.crm.owners.ownersApi.getById).toHaveBeenCalledWith(12345);
|
||||
});
|
||||
|
||||
it("should return contact without ownerEmail when contact has no owner", async () => {
|
||||
mockAppOptions({ roundRobinLeadSkip: true });
|
||||
|
||||
mockHubspotClient.crm.contacts.searchApi.doSearch.mockResolvedValueOnce({
|
||||
results: [
|
||||
{
|
||||
id: "contact-1",
|
||||
properties: {
|
||||
email: "test@example.com",
|
||||
hubspot_owner_id: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await service.getContacts({
|
||||
emails: "test@example.com",
|
||||
forRoundRobinSkip: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "contact-1",
|
||||
email: "test@example.com",
|
||||
ownerId: null,
|
||||
ownerEmail: undefined,
|
||||
recordType: "CONTACT",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockHubspotClient.crm.owners.ownersApi.getById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should skip owner lookup for free email domains when ifFreeEmailDomainSkipOwnerCheck is enabled", async () => {
|
||||
mockAppOptions({ roundRobinLeadSkip: true, ifFreeEmailDomainSkipOwnerCheck: true });
|
||||
mockCheckIfFreeEmailDomain.mockResolvedValueOnce(true);
|
||||
|
||||
mockHubspotClient.crm.contacts.searchApi.doSearch.mockResolvedValueOnce({
|
||||
results: [
|
||||
{
|
||||
id: "contact-1",
|
||||
properties: {
|
||||
email: "user@gmail.com",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await service.getContacts({
|
||||
emails: "user@gmail.com",
|
||||
forRoundRobinSkip: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "contact-1",
|
||||
email: "user@gmail.com",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockCheckIfFreeEmailDomain).toHaveBeenCalledWith({ email: "user@gmail.com" });
|
||||
expect(mockHubspotClient.crm.contacts.searchApi.doSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
properties: ["hs_object_id", "email"],
|
||||
})
|
||||
);
|
||||
expect(mockHubspotClient.crm.owners.ownersApi.getById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should include owner lookup for business email domains even when ifFreeEmailDomainSkipOwnerCheck is enabled", async () => {
|
||||
mockAppOptions({ roundRobinLeadSkip: true, ifFreeEmailDomainSkipOwnerCheck: true });
|
||||
mockCheckIfFreeEmailDomain.mockResolvedValueOnce(false);
|
||||
|
||||
mockHubspotClient.crm.contacts.searchApi.doSearch.mockResolvedValueOnce({
|
||||
results: [
|
||||
{
|
||||
id: "contact-1",
|
||||
properties: {
|
||||
email: "user@company.com",
|
||||
hubspot_owner_id: "45678",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockHubspotClient.crm.owners.ownersApi.getById.mockResolvedValueOnce({
|
||||
id: "45678",
|
||||
email: "owner@company.com",
|
||||
});
|
||||
|
||||
const result = await service.getContacts({
|
||||
emails: "user@company.com",
|
||||
forRoundRobinSkip: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "contact-1",
|
||||
email: "user@company.com",
|
||||
ownerId: "45678",
|
||||
ownerEmail: "owner@company.com",
|
||||
recordType: "CONTACT",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockCheckIfFreeEmailDomain).toHaveBeenCalledWith({ email: "user@company.com" });
|
||||
expect(mockHubspotClient.crm.contacts.searchApi.doSearch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
properties: ["hs_object_id", "email", "hubspot_owner_id"],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle owner lookup API error gracefully", async () => {
|
||||
mockAppOptions({ roundRobinLeadSkip: true });
|
||||
|
||||
mockHubspotClient.crm.contacts.searchApi.doSearch.mockResolvedValueOnce({
|
||||
results: [
|
||||
{
|
||||
id: "contact-1",
|
||||
properties: {
|
||||
email: "test@example.com",
|
||||
hubspot_owner_id: "99999",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockHubspotClient.crm.owners.ownersApi.getById.mockRejectedValueOnce(new Error("Owner not found"));
|
||||
|
||||
const result = await service.getContacts({
|
||||
emails: "test@example.com",
|
||||
forRoundRobinSkip: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "contact-1",
|
||||
email: "test@example.com",
|
||||
ownerId: "99999",
|
||||
ownerEmail: undefined,
|
||||
recordType: "CONTACT",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createContacts", () => {
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { checkIfFreeEmailDomain } from "@calcom/features/watchlist/lib/freeEmailDomainCheck/checkIfFreeEmailDomain";
|
||||
import getLabelValueMapFromResponses from "@calcom/lib/bookings/getLabelValueMapFromResponses";
|
||||
import { getLocation } from "@calcom/lib/CalEventParser";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { PrismaTrackingRepository } from "@calcom/lib/server/repository/PrismaTrackingRepository";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CalEventResponses, CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { Contact, ContactCreateInput, CRM, CrmEvent } from "@calcom/types/CrmService";
|
||||
import * as hubspot from "@hubspot/api-client";
|
||||
import type { BatchInputPublicAssociation } from "@hubspot/api-client/lib/codegen/crm/associations";
|
||||
import type { PublicObjectSearchRequest } from "@hubspot/api-client/lib/codegen/crm/contacts";
|
||||
@@ -7,22 +20,9 @@ import type {
|
||||
SimplePublicObjectInput,
|
||||
} from "@hubspot/api-client/lib/codegen/crm/objects/meetings";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getLocation } from "@calcom/lib/CalEventParser";
|
||||
import getLabelValueMapFromResponses from "@calcom/lib/bookings/getLabelValueMapFromResponses";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CalendarEvent, CalEventResponses } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { CRM, ContactCreateInput, Contact, CrmEvent } from "@calcom/types/CrmService";
|
||||
|
||||
import { CrmFieldType, DateFieldType } from "../../_lib/crm-enums";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
|
||||
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { PrismaTrackingRepository } from "@calcom/lib/server/repository/PrismaTrackingRepository";
|
||||
import type { HubspotToken } from "../api/callback";
|
||||
import type { appDataSchema } from "../zod";
|
||||
|
||||
@@ -336,11 +336,11 @@ class HubspotCalendarService implements CRM {
|
||||
hs_meeting_title: event.title,
|
||||
hs_meeting_body: this.getHubspotMeetingBody(event),
|
||||
hs_meeting_location: getLocation({
|
||||
videoCallData: event.videoCallData,
|
||||
additionalInformation: event.additionalInformation,
|
||||
location: event.location,
|
||||
uid: event.uid,
|
||||
}),
|
||||
videoCallData: event.videoCallData,
|
||||
additionalInformation: event.additionalInformation,
|
||||
location: event.location,
|
||||
uid: event.uid,
|
||||
}),
|
||||
hs_meeting_start_time: new Date(event.startTime).toISOString(),
|
||||
hs_meeting_end_time: new Date(event.endTime).toISOString(),
|
||||
hs_meeting_outcome: "RESCHEDULED",
|
||||
@@ -364,7 +364,6 @@ class HubspotCalendarService implements CRM {
|
||||
return this.hubspotClient.crm.objects.meetings.basicApi.archive(uid);
|
||||
};
|
||||
|
||||
|
||||
private hubspotAuth = async (credential: CredentialPayload) => {
|
||||
const appKeys = await getAppKeysFromSlug("hubspot");
|
||||
if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id;
|
||||
@@ -375,11 +374,7 @@ class HubspotCalendarService implements CRM {
|
||||
let currentToken = credential.key as unknown as HubspotToken;
|
||||
|
||||
const isTokenValid = (token: HubspotToken) =>
|
||||
token &&
|
||||
token.tokenType &&
|
||||
token.accessToken &&
|
||||
token.expiryDate &&
|
||||
token.expiryDate > Date.now();
|
||||
token && token.tokenType && token.accessToken && token.expiryDate && token.expiryDate > Date.now();
|
||||
|
||||
const refreshAccessToken = async (refreshToken: string) => {
|
||||
try {
|
||||
@@ -488,12 +483,25 @@ class HubspotCalendarService implements CRM {
|
||||
await this.hubspotCancelMeeting(uid);
|
||||
}
|
||||
|
||||
async getContacts({ emails }: { emails: string | string[] }): Promise<Contact[]> {
|
||||
async getContacts({
|
||||
emails,
|
||||
includeOwner,
|
||||
forRoundRobinSkip,
|
||||
}: {
|
||||
emails: string | string[];
|
||||
includeOwner?: boolean;
|
||||
forRoundRobinSkip?: boolean;
|
||||
}): Promise<Contact[]> {
|
||||
const auth = await this.auth;
|
||||
await auth.getToken();
|
||||
|
||||
const emailArray = Array.isArray(emails) ? emails : [emails];
|
||||
|
||||
const shouldIncludeOwner = includeOwner || forRoundRobinSkip;
|
||||
|
||||
const skipDueToFreeEmail =
|
||||
forRoundRobinSkip && (await this.shouldSkipAttendeeIfFreeEmailDomain(emailArray[0]));
|
||||
|
||||
const publicObjectSearchRequest: PublicObjectSearchRequest = {
|
||||
filterGroups: emailArray.map((attendeeEmail) => ({
|
||||
filters: [
|
||||
@@ -505,7 +513,10 @@ class HubspotCalendarService implements CRM {
|
||||
],
|
||||
})),
|
||||
sorts: ["hs_object_id"],
|
||||
properties: ["hs_object_id", "email"],
|
||||
properties:
|
||||
shouldIncludeOwner && !skipDueToFreeEmail
|
||||
? ["hs_object_id", "email", "hubspot_owner_id"]
|
||||
: ["hs_object_id", "email"],
|
||||
limit: 10,
|
||||
after: 0,
|
||||
};
|
||||
@@ -514,6 +525,27 @@ class HubspotCalendarService implements CRM {
|
||||
.doSearch(publicObjectSearchRequest)
|
||||
.then((apiResponse) => apiResponse.results);
|
||||
|
||||
if (shouldIncludeOwner && !skipDueToFreeEmail) {
|
||||
return await Promise.all(
|
||||
contacts.map(async (contact) => {
|
||||
const ownerId = contact.properties.hubspot_owner_id;
|
||||
let ownerEmail: string | undefined;
|
||||
|
||||
if (ownerId) {
|
||||
ownerEmail = await this.getOwnerEmailFromId(ownerId);
|
||||
}
|
||||
|
||||
return {
|
||||
id: contact.id,
|
||||
email: contact.properties.email,
|
||||
ownerId,
|
||||
ownerEmail,
|
||||
recordType: "CONTACT",
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return contacts.map((contact) => {
|
||||
return {
|
||||
id: contact.id,
|
||||
@@ -598,9 +630,7 @@ class HubspotCalendarService implements CRM {
|
||||
}
|
||||
|
||||
private async getContactOwnerId(contactId: string): Promise<string | null> {
|
||||
const contact = await this.hubspotClient.crm.contacts.basicApi.getById(contactId, [
|
||||
"hubspot_owner_id",
|
||||
]);
|
||||
const contact = await this.hubspotClient.crm.contacts.basicApi.getById(contactId, ["hubspot_owner_id"]);
|
||||
return contact.properties.hubspot_owner_id ?? null;
|
||||
}
|
||||
|
||||
@@ -623,6 +653,24 @@ class HubspotCalendarService implements CRM {
|
||||
this.log.error("Error fetching contact owner, skipping owner update:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async getOwnerEmailFromId(ownerId: string): Promise<string | undefined> {
|
||||
try {
|
||||
const owner = await this.hubspotClient.crm.owners.ownersApi.getById(parseInt(ownerId, 10));
|
||||
return owner.email ?? undefined;
|
||||
} catch (error) {
|
||||
this.log.error("Error fetching owner by ID:", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async shouldSkipAttendeeIfFreeEmailDomain(attendeeEmail: string): Promise<boolean> {
|
||||
const appOptions = this.getAppOptions();
|
||||
if (!appOptions.ifFreeEmailDomainSkipOwnerCheck) return false;
|
||||
|
||||
const response = await checkIfFreeEmailDomain({ email: attendeeEmail });
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { writeToBookingEntry } from "../_lib/crm-schemas";
|
||||
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||
|
||||
export { CrmFieldType, DateFieldType, WhenToWrite } from "../_lib/crm-enums";
|
||||
export { writeToBookingEntry, writeToRecordEntrySchema } from "../_lib/crm-schemas";
|
||||
export { CrmFieldType, WhenToWrite, DateFieldType } from "../_lib/crm-enums";
|
||||
|
||||
export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
ignoreGuests: z.boolean().optional(),
|
||||
@@ -13,6 +12,8 @@ export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
overwriteContactOwner: z.boolean().optional(),
|
||||
onBookingWriteToEventObject: z.boolean().optional(),
|
||||
onBookingWriteToEventObjectFields: z.record(z.string(), writeToBookingEntry).optional(),
|
||||
roundRobinLeadSkip: z.boolean().optional(),
|
||||
ifFreeEmailDomainSkipOwnerCheck: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user