Skip to content

Commit

Permalink
feat: Hubspot - create new contacts under an account (#18723)
Browse files Browse the repository at this point in the history
* Add new scopes

* Add FE option to create contacts under a company

* Find company and associate with contact
  • Loading branch information
joeauyeung authored Jan 21, 2025
1 parent 276f7e5 commit c62394b
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 15 deletions.
1 change: 1 addition & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
7 changes: 6 additions & 1 deletion packages/app-store/hubspot/api/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof appDataSchema>();

const createContactUnderCompany = getAppData("createContactUnderCompany");

return (
<AppCard
Expand All @@ -19,8 +28,20 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
updateEnabled(e);
}}
switchChecked={enabled}
hideAppCardOptions
/>
hideSettingsIcon>
<>
<div>
<Switch
label={t("hubspot_create_contact_under_company")}
labelOnLeading
checked={createContactUnderCompany}
onCheckedChange={(checked) => {
setAppData("createContactUnderCompany", checked);
}}
/>
</div>
</>
</AppCard>
);
};

Expand Down
79 changes: 68 additions & 11 deletions packages/app-store/hubspot/lib/CrmService.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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();

Expand All @@ -30,13 +38,15 @@ export default class HubspotCalendarService implements CRM {
private log: typeof logger;
private client_id = "";
private client_secret = "";
private appOptions: z.infer<typeof appDataSchema>;

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 => {
Expand All @@ -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,
Expand All @@ -67,7 +77,7 @@ export default class HubspotCalendarService implements CRM {
},
types: [
{
associationCategory: AssociationSpecAssociationCategoryEnum.HubspotDefined,
associationCategory: MeetingAssociationCategoryEnum.HubspotDefined,
associationTypeId: AssociationTypes.meetingToContact,
},
],
Expand Down Expand Up @@ -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,
},
],
})),
Expand All @@ -232,14 +242,61 @@ 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: {
firstname,
lastname,
email: attendee.email,
},
...(companyId && {
associations: [
{
to: {
id: companyId,
},
types: [
{
associationCategory: ContactAssociationCategoryEnum.HubspotDefined,
associationTypeId: AssociationTypes.contactToCompany,
},
],
},
],
}),
};
});
const createdContacts = await Promise.all(
Expand Down Expand Up @@ -267,6 +324,6 @@ export default class HubspotCalendarService implements CRM {
}

getAppOptions() {
console.log("No options implemented");
return this.appOptions;
}
}
4 changes: 3 additions & 1 deletion packages/app-store/hubspot/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit c62394b

Please sign in to comment.