From b664857809c2bafe01b0ad2b3aa24b5d3b632fe3 Mon Sep 17 00:00:00 2001
From: Pallav <90088723+Pallava-Joshi@users.noreply.github.com>
Date: Mon, 22 Dec 2025 22:52:04 +0530
Subject: [PATCH] feat: set HubSpot meeting owner from cal's organizer (#25998)
* feat: add HubSpot owner(organizer in cal)
* refactor: improve logging and token handling in HubSpot integration
* Update packages/app-store/hubspot/lib/CrmService.ts
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
* feat: enhance error handling for missing scopes
* refactor
* remove unused
---------
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
---
packages/app-store/hubspot/api/add.ts | 2 +-
packages/app-store/hubspot/lib/CrmService.ts | 98 +++++++++++++++-----
2 files changed, 74 insertions(+), 26 deletions(-)
diff --git a/packages/app-store/hubspot/api/add.ts b/packages/app-store/hubspot/api/add.ts
index 40f23aa646..f0943dc7ad 100644
--- a/packages/app-store/hubspot/api/add.ts
+++ b/packages/app-store/hubspot/api/add.ts
@@ -6,7 +6,7 @@ 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.objects.owners.read"];
const hubspotClient = new hubspot.Client();
diff --git a/packages/app-store/hubspot/lib/CrmService.ts b/packages/app-store/hubspot/lib/CrmService.ts
index 438b390753..a3172a4d95 100644
--- a/packages/app-store/hubspot/lib/CrmService.ts
+++ b/packages/app-store/hubspot/lib/CrmService.ts
@@ -56,32 +56,40 @@ export default class HubspotCalendarService implements CRM {
})
.join("
");
- return `${event.organizer.language.translate("invitee_timezone")}: ${
- event.attendees[0].timeZone
- }
${
- event.additionalNotes
- ? `${event.organizer.language.translate("share_additional_notes")}
${
- event.additionalNotes
- }
`
+ const organizerName = event.organizer.name || event.organizer.email;
+ const organizerInfo = `${event.organizer.language.translate("organizer")}: ${organizerName} (${event.organizer.email
+ })`;
+
+ return `${organizerInfo}
${event.organizer.language.translate("invitee_timezone")}: ${event.attendees[0].timeZone
+ }
${event.additionalNotes
+ ? `${event.organizer.language.translate("share_additional_notes")}
${event.additionalNotes
+ }
`
: ""
- }
+ }
${userFieldsHtml}
${event.organizer.language.translate("where")}: ${location}
${plainText ? `${event.organizer.language.translate("description")}
${plainText}` : ""}
`;
};
- private hubspotCreateMeeting = async (event: CalendarEvent) => {
+ private hubspotCreateMeeting = async (event: CalendarEvent, hubspotOwnerId?: string) => {
+ const properties: Record = {
+ hs_timestamp: Date.now().toString(),
+ hs_meeting_title: event.title,
+ hs_meeting_body: this.getHubspotMeetingBody(event),
+ hs_meeting_location: getLocation(event),
+ hs_meeting_start_time: new Date(event.startTime).toISOString(),
+ hs_meeting_end_time: new Date(event.endTime).toISOString(),
+ hs_meeting_outcome: "SCHEDULED",
+ };
+
+ if (hubspotOwnerId) {
+ properties.hubspot_owner_id = hubspotOwnerId;
+ this.log.debug("hubspot:meeting:setting_owner", { hubspotOwnerId });
+ }
+
const simplePublicObjectInput: SimplePublicObjectInput = {
- properties: {
- hs_timestamp: Date.now().toString(),
- hs_meeting_title: event.title,
- hs_meeting_body: this.getHubspotMeetingBody(event),
- hs_meeting_location: getLocation(event),
- hs_meeting_start_time: new Date(event.startTime).toISOString(),
- hs_meeting_end_time: new Date(event.endTime).toISOString(),
- hs_meeting_outcome: "SCHEDULED",
- },
+ properties,
};
return this.hubspotClient.crm.objects.meetings.basicApi.create(simplePublicObjectInput);
@@ -132,6 +140,34 @@ export default class HubspotCalendarService implements CRM {
return this.hubspotClient.crm.objects.meetings.basicApi.archive(uid);
};
+ private getHubspotOwnerIdByEmail = async (email: string): Promise => {
+ try {
+ const ownersResponse = await this.hubspotClient.crm.owners.ownersApi.getPage(
+ email,
+ undefined,
+ 100,
+ false
+ );
+
+ if (ownersResponse.results && ownersResponse.results.length > 0) {
+ const matchingOwner = ownersResponse.results.find(
+ (owner) => owner.email?.toLowerCase() === email.toLowerCase()
+ );
+
+ if (matchingOwner) {
+ this.log.debug("hubspot:owner:found", { ownerId: matchingOwner.id });
+ return matchingOwner.id;
+ }
+ }
+
+ this.log.debug("hubspot:owner:not_found");
+ return undefined;
+ } catch (error) {
+ this.log.error("hubspot:owner:lookup_error", { error });
+ return undefined;
+ }
+ };
+
private hubspotAuth = async (credential: CredentialPayload) => {
const appKeys = await getAppKeysFromSlug("hubspot");
if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id;
@@ -139,13 +175,14 @@ export default class HubspotCalendarService implements CRM {
if (!this.client_id) throw new HttpError({ statusCode: 400, message: "Hubspot client_id missing." });
if (!this.client_secret)
throw new HttpError({ statusCode: 400, message: "Hubspot client_secret missing." });
- const credentialKey = credential.key as unknown as HubspotToken;
+ let currentToken = credential.key as unknown as HubspotToken;
+
const isTokenValid = (token: HubspotToken) =>
token &&
token.tokenType &&
token.accessToken &&
token.expiryDate &&
- (token.expiresIn || token.expiryDate) < Date.now();
+ token.expiryDate > Date.now();
const refreshAccessToken = async (refreshToken: string) => {
try {
@@ -162,7 +199,6 @@ export default class HubspotCalendarService implements CRM {
"hubspot",
credential.userId
);
- // set expiry date as offset from current time.
hubspotRefreshToken.expiryDate = Math.round(Date.now() + hubspotRefreshToken.expiresIn * 1000);
await prisma.credential.update({
where: {
@@ -174,22 +210,34 @@ export default class HubspotCalendarService implements CRM {
});
this.hubspotClient.setAccessToken(hubspotRefreshToken.accessToken);
+ currentToken = { ...currentToken, ...hubspotRefreshToken };
} catch (e: unknown) {
this.log.error(e);
}
};
return {
- getToken: () =>
- !isTokenValid(credentialKey) ? Promise.resolve([]) : refreshAccessToken(credentialKey.refreshToken),
+ getToken: async () => {
+ if (!isTokenValid(currentToken)) {
+ await refreshAccessToken(currentToken.refreshToken);
+ } else {
+ this.hubspotClient.setAccessToken(currentToken.accessToken);
+ }
+ },
};
};
async handleMeetingCreation(event: CalendarEvent, contacts: Contact[]) {
const contactIds: { id?: string }[] = contacts.map((contact) => ({ id: contact.id }));
- const meetingEvent = await this.hubspotCreateMeeting(event);
+
+ const organizerEmail = event.organizer.email;
+ this.log.debug("hubspot:meeting:fetching_owner");
+
+ const hubspotOwnerId = await this.getHubspotOwnerIdByEmail(organizerEmail);
+
+ const meetingEvent = await this.hubspotCreateMeeting(event, hubspotOwnerId);
if (meetingEvent) {
- this.log.debug("meeting:creation:ok", { meetingEvent });
+ this.log.debug("meeting:creation:ok", { meetingId: meetingEvent.id, hubspotOwnerId });
const associatedMeeting = await this.hubspotAssociate(meetingEvent, contactIds as any);
if (associatedMeeting) {
this.log.debug("association:creation:ok", { associatedMeeting });