UNPKG

@lautmaler/crm-connectors

Version:

Provides connectors to various CRM systems and calendar services.

345 lines (314 loc) 10.6 kB
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."); } }