@lautmaler/crm-connectors
Version:
Provides connectors to various CRM systems and calendar services.
345 lines (314 loc) • 10.6 kB
text/typescript
import axios from "axios";
import {
Appointment,
CRMBackend,
HubSpotApiResponse,
} from "../crm.interfaces.js";
import winston from "winston";
import { GoogleCalendarBackend } from "./GoogleCalendarBackend.js";
import { CalendlyBackend } from "./CalendlyBackend.js";
const HUBSPOT_API_BASE_URL = "https://api.hubspot.com";
export class HubSpotBackend implements CRMBackend {
private logger: winston.Logger;
private hubspotApiKey: string;
private calenderBackend: GoogleCalendarBackend | CalendlyBackend;
private hubspotOwners: any[] = [];
private employee: string;
constructor(backend?: string, hubspotOwners?: any[], employee?: string) {
this.hubspotOwners = hubspotOwners || [];
this.employee = employee || "";
this.logger = winston.createLogger({
level: "info",
format: winston.format.json(),
defaultMeta: { service: "crm-service" },
transports: [
new winston.transports.Console({
format: winston.format.simple(),
}),
],
});
this.hubspotApiKey = (process.env.HUBSPOT_API_KEY as string) || "";
switch (backend) {
case "google":
this.calenderBackend = new GoogleCalendarBackend();
break;
case "calendly":
this.calenderBackend = new CalendlyBackend();
break;
default:
throw new Error("Invalid backend specified");
}
}
private getHeaders() {
return {
headers: {
Authorization: `Bearer ${this.hubspotApiKey}`,
"Content-Type": "application/json",
},
};
}
async fetchAvailableSlots(
timestamp: string,
attendees: string[],
): Promise<string[]> {
return this.calenderBackend.fetchAvailableSlots(timestamp, attendees);
}
async fetchAppointmentTypes(): Promise<string[]> {
// TODO: Implement fetching appointment types from HubSpot
return ["Consultation", "Meeting", "Demo"];
}
async getContactIdByName(contactName: string): Promise<string | null> {
const url = `${HUBSPOT_API_BASE_URL}/crm/v3/objects/contacts/search`;
const [firstName, lastName] = contactName.split(" "); // Split the name into first and last name
const data = {
filterGroups: [
{
filters: [
{
propertyName: "firstname",
operator: "EQ",
value: firstName,
},
{
propertyName: "lastname",
operator: "EQ",
value: lastName,
},
],
},
],
};
try {
const response = await axios.post(url, data, {
headers: {
Authorization: `Bearer ${this.hubspotApiKey}`,
"Content-Type": "application/json",
},
});
// Check if the contact exists and return the ID
if ((response.data as { results: any[] }).results.length > 0) {
const contactId = (response.data as { results: any[] }).results[0].id; // Get the first matching contact's ID
this.logger.info(`Contact found. ID: ${contactId}`);
return contactId; // Return the contact ID
} else {
this.logger.info("Contact does not exist");
return null; // Contact not found, return null
}
} catch (error: any) {
this.logger.error(
"Error fetching contact:",
error.response?.data || error.message,
);
throw new Error("Failed to check if contact exists");
}
}
async createContact(contactName: string): Promise<string> {
const url = `${HUBSPOT_API_BASE_URL}/crm/v3/objects/contacts`;
const data = {
properties: {
firstname: contactName.split(" ")[0],
lastname: contactName.split(" ")[1], // Assumes name is in "First Last" format
},
};
try {
const response = (await axios.post(url, data, this.getHeaders())) as {
data: { id: string };
};
this.logger.info(response.data);
if (response.data?.id) {
return response.data.id;
} else {
throw new Error("Failed to create contact");
}
} catch (error: any) {
throw new Error("Failed to create contact: " + error.message);
}
}
async appointmentCreationCall(
appointment: Appointment,
contactId: string,
timestampMs: number,
employee: any,
): Promise<HubSpotApiResponse> {
const url = `${HUBSPOT_API_BASE_URL}/crm/v3/objects/meetings`;
const data = {
associations: [
{
types: [
{
associationCategory: "HUBSPOT_DEFINED",
associationTypeId: 200,
},
],
to: {
id: contactId,
},
},
],
properties: {
hs_timestamp: appointment.timestamp,
hs_meeting_start_time: timestampMs, // Start time in milliseconds
hs_meeting_end_time: timestampMs + 3600000, // End time (default: 1 hour later)
hubspot_owner_id: employee?.id,
hs_meeting_title:
appointment.meetingTitle ?? `Meeting w/ ${appointment.contactName}`,
hs_internal_meeting_notes: appointment.meetingNotes,
hs_meeting_location: "Remote",
hs_meeting_outcome: "SCHEDULED",
},
};
try {
const response = await axios.post(url, data, this.getHeaders());
return response.data as HubSpotApiResponse;
} catch (error: any) {
this.logger.error(
"Error booking appointment:",
error.response?.data || error.message,
);
throw new Error("Failed to book appointment.");
}
}
async bookAppointment(appointment: Appointment): Promise<string> {
const employee = this.hubspotOwners.find(
(owner) => owner.lastName === appointment.employeeName,
);
const timestampMs = new Date(appointment.timestamp).getTime(); // Convert to milliseconds
const contactId =
(await this.getContactIdByName(appointment.contactName)) ||
(await this.createContact(appointment.contactName));
const apiReponse = await this.appointmentCreationCall(
appointment,
contactId,
timestampMs,
employee,
);
const meetingUrl = `https://app.hubspot.com/meetings/${apiReponse.id}`;
return meetingUrl;
}
async modifyAppointment(
id: string,
updatedInfo: Partial<Appointment>,
): Promise<Appointment> {
const url = `${HUBSPOT_API_BASE_URL}/engagements/v1/engagements/${id}`;
// Prepare the data for the API request
const data: any = {
engagement: {
type: "MEETING",
},
metadata: {},
};
// Update timestamp if provided
if (updatedInfo.timestamp) {
data.engagement.timestamp = new Date(updatedInfo.timestamp).getTime();
data.metadata.startTime = new Date(updatedInfo.timestamp).getTime();
// Assuming a default 1-hour duration
data.metadata.endTime =
new Date(updatedInfo.timestamp).getTime() + 3600000;
}
// Update notes if provided
if (updatedInfo.meetingNotes) {
data.metadata.body = updatedInfo.meetingNotes;
}
// Update type if provided
if (updatedInfo.type) {
data.metadata.meetingType = updatedInfo.type;
}
// Update status if provided
if (updatedInfo.status) {
data.metadata.status = updatedInfo.status;
}
try {
// Make the API call to update the engagement
await axios.patch(url, data, this.getHeaders());
this.logger.info(`Modified appointment with ID: ${id}`);
// Fetch the updated appointment details
const updatedAppointment = await this.findAppointmentById(id);
if (!updatedAppointment) {
throw new Error("Failed to retrieve updated appointment");
}
return updatedAppointment;
} catch (error) {
this.logger.error("Error modifying appointment:", error);
throw new Error("Failed to modify appointment");
}
}
async findAppointmentByContactName(name: string): Promise<Appointment> {
const url = `${HUBSPOT_API_BASE_URL}/crm/v3/objects/meetings/search`; // Correct endpoint
const data = {
filterGroups: [
{
filters: [
{
propertyName: "hs_meeting_title",
operator: "CONTAINS_TOKEN",
value: name,
},
],
},
],
};
try {
// Ensure this is a POST request
const response = await axios.post(url, data, this.getHeaders());
const responseData = response.data as { results: any[] };
return responseData.results.map((meeting: any) => ({
id: meeting.id,
timestamp: meeting.properties.hs_meeting_start_time,
meetingNotes: meeting.properties.hs_meeting_body || "",
type: "Meeting",
status: "Scheduled",
contactName: name,
employeeName: this.employee,
}))[0] as Appointment;
} catch (error: any) {
this.logger.error(
"Error searching for appointments:",
error.response?.data || error.message,
);
throw new Error("Failed to search for appointments by name");
}
}
async findAppointmentByTimestamp(
timestamp: string,
): Promise<Appointment | null> {
const url = `${HUBSPOT_API_BASE_URL}/engagements/v1/engagements`;
const response = await axios.get(url, this.getHeaders());
const appointment = (response.data as { results: any[] }).results.find(
(meeting: any) =>
new Date(meeting.metadata.startTime).toISOString() === timestamp,
);
return {
id: appointment.engagement.id,
timestamp,
meetingNotes: appointment.metadata.body,
type: "Meeting",
status: "Scheduled",
contactName: "",
employeeName: this.employee,
} as Appointment;
}
async findAppointmentById(id: string): Promise<Appointment | null> {
const url = `${HUBSPOT_API_BASE_URL}/engagements/v1/engagements/${id}`;
try {
const response = await axios.get(url, this.getHeaders());
const meeting = response.data as {
metadata: { startTime: string; body: string };
};
return {
id,
timestamp: new Date(meeting.metadata.startTime).toISOString(),
meetingNotes: meeting.metadata.body,
type: "Meeting",
status: "Scheduled",
contactName: "",
employeeName: this.employee,
};
} catch (error: any) {
if (error.response?.status === 404) return null;
throw error;
}
}
async createEventToSms(appointment: Appointment): Promise<string> {
throw new Error("Method not implemented.");
}
}