From c62394bfe4f02e30450a7193f966e7501fa4db86 Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Tue, 21 Jan 2025 04:41:18 -0500 Subject: [PATCH] feat: Hubspot - create new contacts under an account (#18723) * Add new scopes * Add FE option to create contacts under a company * Find company and associate with contact --- apps/web/public/static/locales/en/common.json | 1 + packages/app-store/hubspot/api/add.ts | 7 +- .../components/EventTypeAppCardInterface.tsx | 25 +++++- packages/app-store/hubspot/lib/CrmService.ts | 79 ++++++++++++++++--- packages/app-store/hubspot/zod.ts | 4 +- 5 files changed, 101 insertions(+), 15 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 8133ae2c6e8178..f9898ef016634a 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2926,5 +2926,6 @@ "managed_users": "Managed Users", "managed_users_description": "See all the managed users created by your OAuth client", "select_oAuth_client": "Select Oauth Client", + "hubspot_create_contact_under_company": "Create new contacts under a company where the website matches the contact email domain", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/app-store/hubspot/api/add.ts b/packages/app-store/hubspot/api/add.ts index 350bb153c2a319..b81e2d8fb44dac 100644 --- a/packages/app-store/hubspot/api/add.ts +++ b/packages/app-store/hubspot/api/add.ts @@ -6,7 +6,12 @@ import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState"; -const scopes = ["crm.objects.contacts.read", "crm.objects.contacts.write"]; +const scopes = [ + "crm.objects.contacts.read", + "crm.objects.contacts.write", + "crm.schemas.companies.read", + "crm.objects.companies.read", +]; let client_id = ""; const oauth = new Client().oauth; diff --git a/packages/app-store/hubspot/components/EventTypeAppCardInterface.tsx b/packages/app-store/hubspot/components/EventTypeAppCardInterface.tsx index 53a8297d6fca40..165d0d6d0425ac 100644 --- a/packages/app-store/hubspot/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/hubspot/components/EventTypeAppCardInterface.tsx @@ -1,14 +1,23 @@ import { usePathname } from "next/navigation"; +import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Switch } from "@calcom/ui"; + +import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const pathname = usePathname(); + const { t } = useLocale(); const { enabled, updateEnabled } = useIsAppEnabled(app); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); + + const createContactUnderCompany = getAppData("createContactUnderCompany"); return ( + hideSettingsIcon> + <> +
+ { + setAppData("createContactUnderCompany", checked); + }} + /> +
+ +
); }; diff --git a/packages/app-store/hubspot/lib/CrmService.ts b/packages/app-store/hubspot/lib/CrmService.ts index 38f1b67394570e..2693759ee247b5 100644 --- a/packages/app-store/hubspot/lib/CrmService.ts +++ b/packages/app-store/hubspot/lib/CrmService.ts @@ -1,13 +1,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Client, AssociationTypes } from "@hubspot/api-client"; -import type { PublicObjectSearchRequest } from "@hubspot/api-client/lib/codegen/crm/contacts"; -import { FilterOperatorEnum } from "@hubspot/api-client/lib/codegen/crm/contacts"; +import { FilterOperatorEnum as CompanyFilterOperatorEnum } from "@hubspot/api-client/lib/codegen/crm/companies"; +import type { + PublicObjectSearchRequest as ContactSearchInput, + SimplePublicObjectInputForCreate as HubspotContactCreateInput, + PublicObjectSearchRequest as CompanySearchInput, +} from "@hubspot/api-client/lib/codegen/crm/contacts"; +import { FilterOperatorEnum as ContactFilterOperatorEnum } from "@hubspot/api-client/lib/codegen/crm/contacts"; +import { AssociationSpecAssociationCategoryEnum as ContactAssociationCategoryEnum } from "@hubspot/api-client/lib/codegen/crm/contacts"; import type { SimplePublicObjectInput, - SimplePublicObjectInputForCreate, + SimplePublicObjectInputForCreate as MeetingCreateInput, PublicAssociationsForObject, } from "@hubspot/api-client/lib/codegen/crm/objects/meetings"; -import { AssociationSpecAssociationCategoryEnum } from "@hubspot/api-client/lib/codegen/crm/objects/meetings"; +import { AssociationSpecAssociationCategoryEnum as MeetingAssociationCategoryEnum } from "@hubspot/api-client/lib/codegen/crm/objects/meetings"; +import type z from "zod"; import { getLocation } from "@calcom/lib/CalEventParser"; import { WEBAPP_URL } from "@calcom/lib/constants"; @@ -21,6 +28,7 @@ import type { CRM, ContactCreateInput, Contact, CrmEvent } from "@calcom/types/C import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens"; import type { HubspotToken } from "../api/callback"; +import type { appDataSchema } from "../zod"; const hubspotClient = new Client(); @@ -30,13 +38,15 @@ export default class HubspotCalendarService implements CRM { private log: typeof logger; private client_id = ""; private client_secret = ""; + private appOptions: z.infer; - constructor(credential: CredentialPayload) { + constructor(credential: CredentialPayload, appOptions: any) { this.integrationName = "hubspot_other_calendar"; this.auth = this.hubspotAuth(credential).then((r) => r); this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] }); + this.appOptions = appOptions; } private getHubspotMeetingBody = (event: CalendarEvent): string => { @@ -49,7 +59,7 @@ export default class HubspotCalendarService implements CRM { private hubspotCreateMeeting = async (event: CalendarEvent, contacts: Contact[]) => { try { - const simplePublicObjectInput: SimplePublicObjectInputForCreate = { + const simplePublicObjectInput: MeetingCreateInput = { properties: { hs_timestamp: Date.now().toString(), hs_meeting_title: event.title, @@ -67,7 +77,7 @@ export default class HubspotCalendarService implements CRM { }, types: [ { - associationCategory: AssociationSpecAssociationCategoryEnum.HubspotDefined, + associationCategory: MeetingAssociationCategoryEnum.HubspotDefined, associationTypeId: AssociationTypes.meetingToContact, }, ], @@ -201,13 +211,13 @@ export default class HubspotCalendarService implements CRM { const emailArray = Array.isArray(emails) ? emails : [emails]; - const publicObjectSearchRequest: PublicObjectSearchRequest = { + const publicObjectSearchRequest: ContactSearchInput = { filterGroups: emailArray.map((attendeeEmail) => ({ filters: [ { value: attendeeEmail, propertyName: "email", - operator: FilterOperatorEnum.Eq, + operator: ContactFilterOperatorEnum.Eq, }, ], })), @@ -232,7 +242,39 @@ export default class HubspotCalendarService implements CRM { const auth = await this.auth; await auth.getToken(); - const simplePublicObjectInputs = contactsToCreate.map((attendee) => { + const appOptions = this.getAppOptions(); + let companyId: string; + + // Check for a company to associate the contact with + if (appOptions?.createContactUnderCompany) { + const emailDomain = contactsToCreate[0].email.split("@")[1]; + + const companySearchInput: CompanySearchInput = { + filterGroups: [ + { + filters: [ + { + propertyName: "website", + operator: CompanyFilterOperatorEnum.ContainsToken, + value: emailDomain, + }, + ], + }, + ], + properties: ["id"], + limit: 1, + }; + + const companyQuery = await hubspotClient.crm.companies.searchApi + .doSearch(companySearchInput) + .then((apiResponse) => apiResponse.results); + if (companyQuery.length > 0) { + const company = companyQuery[0]; + companyId = company.id; + } + } + + const simplePublicObjectInputs: HubspotContactCreateInput[] = contactsToCreate.map((attendee) => { const [firstname, lastname] = attendee.name ? attendee.name.split(" ") : [attendee.email, ""]; return { properties: { @@ -240,6 +282,21 @@ export default class HubspotCalendarService implements CRM { lastname, email: attendee.email, }, + ...(companyId && { + associations: [ + { + to: { + id: companyId, + }, + types: [ + { + associationCategory: ContactAssociationCategoryEnum.HubspotDefined, + associationTypeId: AssociationTypes.contactToCompany, + }, + ], + }, + ], + }), }; }); const createdContacts = await Promise.all( @@ -267,6 +324,6 @@ export default class HubspotCalendarService implements CRM { } getAppOptions() { - console.log("No options implemented"); + return this.appOptions; } } diff --git a/packages/app-store/hubspot/zod.ts b/packages/app-store/hubspot/zod.ts index 8cfdc5ee22810c..56297b05d26db3 100644 --- a/packages/app-store/hubspot/zod.ts +++ b/packages/app-store/hubspot/zod.ts @@ -2,7 +2,9 @@ import { z } from "zod"; import { eventTypeAppCardZod } from "../eventTypeAppCardZod"; -export const appDataSchema = eventTypeAppCardZod; +export const appDataSchema = eventTypeAppCardZod.extend({ + createContactUnderCompany: z.boolean().optional(), +}); export const appKeysSchema = z.object({ client_id: z.string().min(1),