Skip to content

Commit b664857

Browse files
pallava-joshicubic-dev-ai[bot]Amit91848
authored
feat: set HubSpot meeting owner from cal's organizer (calcom#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>
1 parent c746fdc commit b664857

2 files changed

Lines changed: 74 additions & 26 deletions

File tree

packages/app-store/hubspot/api/add.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
66
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
77
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
88

9-
const scopes = ["crm.objects.contacts.read", "crm.objects.contacts.write"];
9+
const scopes = ["crm.objects.contacts.read", "crm.objects.contacts.write", "crm.objects.owners.read"];
1010

1111
const hubspotClient = new hubspot.Client();
1212

packages/app-store/hubspot/lib/CrmService.ts

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -56,32 +56,40 @@ export default class HubspotCalendarService implements CRM {
5656
})
5757
.join("<br><br>");
5858

59-
return `<b>${event.organizer.language.translate("invitee_timezone")}:</b> ${
60-
event.attendees[0].timeZone
61-
}<br><br>${
62-
event.additionalNotes
63-
? `<b>${event.organizer.language.translate("share_additional_notes")}</b><br>${
64-
event.additionalNotes
65-
}<br><br>`
59+
const organizerName = event.organizer.name || event.organizer.email;
60+
const organizerInfo = `<b>${event.organizer.language.translate("organizer")}:</b> ${organizerName} (${event.organizer.email
61+
})`;
62+
63+
return `${organizerInfo}<br><br><b>${event.organizer.language.translate("invitee_timezone")}:</b> ${event.attendees[0].timeZone
64+
}<br><br>${event.additionalNotes
65+
? `<b>${event.organizer.language.translate("share_additional_notes")}</b><br>${event.additionalNotes
66+
}<br><br>`
6667
: ""
67-
}
68+
}
6869
${userFieldsHtml}<br><br>
6970
<b>${event.organizer.language.translate("where")}:</b> ${location}<br><br>
7071
${plainText ? `<b>${event.organizer.language.translate("description")}</b><br>${plainText}` : ""}
7172
`;
7273
};
7374

74-
private hubspotCreateMeeting = async (event: CalendarEvent) => {
75+
private hubspotCreateMeeting = async (event: CalendarEvent, hubspotOwnerId?: string) => {
76+
const properties: Record<string, string> = {
77+
hs_timestamp: Date.now().toString(),
78+
hs_meeting_title: event.title,
79+
hs_meeting_body: this.getHubspotMeetingBody(event),
80+
hs_meeting_location: getLocation(event),
81+
hs_meeting_start_time: new Date(event.startTime).toISOString(),
82+
hs_meeting_end_time: new Date(event.endTime).toISOString(),
83+
hs_meeting_outcome: "SCHEDULED",
84+
};
85+
86+
if (hubspotOwnerId) {
87+
properties.hubspot_owner_id = hubspotOwnerId;
88+
this.log.debug("hubspot:meeting:setting_owner", { hubspotOwnerId });
89+
}
90+
7591
const simplePublicObjectInput: SimplePublicObjectInput = {
76-
properties: {
77-
hs_timestamp: Date.now().toString(),
78-
hs_meeting_title: event.title,
79-
hs_meeting_body: this.getHubspotMeetingBody(event),
80-
hs_meeting_location: getLocation(event),
81-
hs_meeting_start_time: new Date(event.startTime).toISOString(),
82-
hs_meeting_end_time: new Date(event.endTime).toISOString(),
83-
hs_meeting_outcome: "SCHEDULED",
84-
},
92+
properties,
8593
};
8694

8795
return this.hubspotClient.crm.objects.meetings.basicApi.create(simplePublicObjectInput);
@@ -132,20 +140,49 @@ export default class HubspotCalendarService implements CRM {
132140
return this.hubspotClient.crm.objects.meetings.basicApi.archive(uid);
133141
};
134142

143+
private getHubspotOwnerIdByEmail = async (email: string): Promise<string | undefined> => {
144+
try {
145+
const ownersResponse = await this.hubspotClient.crm.owners.ownersApi.getPage(
146+
email,
147+
undefined,
148+
100,
149+
false
150+
);
151+
152+
if (ownersResponse.results && ownersResponse.results.length > 0) {
153+
const matchingOwner = ownersResponse.results.find(
154+
(owner) => owner.email?.toLowerCase() === email.toLowerCase()
155+
);
156+
157+
if (matchingOwner) {
158+
this.log.debug("hubspot:owner:found", { ownerId: matchingOwner.id });
159+
return matchingOwner.id;
160+
}
161+
}
162+
163+
this.log.debug("hubspot:owner:not_found");
164+
return undefined;
165+
} catch (error) {
166+
this.log.error("hubspot:owner:lookup_error", { error });
167+
return undefined;
168+
}
169+
};
170+
135171
private hubspotAuth = async (credential: CredentialPayload) => {
136172
const appKeys = await getAppKeysFromSlug("hubspot");
137173
if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id;
138174
if (typeof appKeys.client_secret === "string") this.client_secret = appKeys.client_secret;
139175
if (!this.client_id) throw new HttpError({ statusCode: 400, message: "Hubspot client_id missing." });
140176
if (!this.client_secret)
141177
throw new HttpError({ statusCode: 400, message: "Hubspot client_secret missing." });
142-
const credentialKey = credential.key as unknown as HubspotToken;
178+
let currentToken = credential.key as unknown as HubspotToken;
179+
143180
const isTokenValid = (token: HubspotToken) =>
144181
token &&
145182
token.tokenType &&
146183
token.accessToken &&
147184
token.expiryDate &&
148-
(token.expiresIn || token.expiryDate) < Date.now();
185+
token.expiryDate > Date.now();
149186

150187
const refreshAccessToken = async (refreshToken: string) => {
151188
try {
@@ -162,7 +199,6 @@ export default class HubspotCalendarService implements CRM {
162199
"hubspot",
163200
credential.userId
164201
);
165-
// set expiry date as offset from current time.
166202
hubspotRefreshToken.expiryDate = Math.round(Date.now() + hubspotRefreshToken.expiresIn * 1000);
167203
await prisma.credential.update({
168204
where: {
@@ -174,22 +210,34 @@ export default class HubspotCalendarService implements CRM {
174210
});
175211

176212
this.hubspotClient.setAccessToken(hubspotRefreshToken.accessToken);
213+
currentToken = { ...currentToken, ...hubspotRefreshToken };
177214
} catch (e: unknown) {
178215
this.log.error(e);
179216
}
180217
};
181218

182219
return {
183-
getToken: () =>
184-
!isTokenValid(credentialKey) ? Promise.resolve([]) : refreshAccessToken(credentialKey.refreshToken),
220+
getToken: async () => {
221+
if (!isTokenValid(currentToken)) {
222+
await refreshAccessToken(currentToken.refreshToken);
223+
} else {
224+
this.hubspotClient.setAccessToken(currentToken.accessToken);
225+
}
226+
},
185227
};
186228
};
187229

188230
async handleMeetingCreation(event: CalendarEvent, contacts: Contact[]) {
189231
const contactIds: { id?: string }[] = contacts.map((contact) => ({ id: contact.id }));
190-
const meetingEvent = await this.hubspotCreateMeeting(event);
232+
233+
const organizerEmail = event.organizer.email;
234+
this.log.debug("hubspot:meeting:fetching_owner");
235+
236+
const hubspotOwnerId = await this.getHubspotOwnerIdByEmail(organizerEmail);
237+
238+
const meetingEvent = await this.hubspotCreateMeeting(event, hubspotOwnerId);
191239
if (meetingEvent) {
192-
this.log.debug("meeting:creation:ok", { meetingEvent });
240+
this.log.debug("meeting:creation:ok", { meetingId: meetingEvent.id, hubspotOwnerId });
193241
const associatedMeeting = await this.hubspotAssociate(meetingEvent, contactIds as any);
194242
if (associatedMeeting) {
195243
this.log.debug("association:creation:ok", { associatedMeeting });

0 commit comments

Comments
 (0)