import { type HttpClient, http } from '@services' import { omyHttp } from '../httpExternal/omyClient' import { AvailabilitySchema, type HttpClientType, type HubspotLinkAvailabilityResponse, type HubspotMeeting, type HubspotMeetingBookSuccess, type HubspotMeetingLink, } from './types' export class Hubspot { private httpOptions = {} private hubspotApiDomain = '' private hubspotMeetingSchedulerToken = '' private http = {} as HttpClient constructor( private provider: { hubspotApiDomain: string hubspotMeetingSchedulerToken: string httpClient: HttpClientType }, ) { this.hubspotApiDomain = this.provider.hubspotApiDomain this.hubspotMeetingSchedulerToken = this.provider.hubspotMeetingSchedulerToken this.http = this.setHttp(this.provider.httpClient) } private setHttp(httpClientType: HttpClientType) { const api = { name: 'hubspot', url: this.hubspotApiDomain } as const return httpClientType === 'axios' ? http({ api, unserialized: true }) : omyHttp({ api }) } async postBooking(bookData: { email: string firstName: string lastName: string slug: string startTime: number timezone?: string | undefined phone: string message: string }): Promise { const url = new URL( '/scheduler/v3/meetings/meeting-links/book', this.hubspotApiDomain, ) // Use the class' token for authorization const response = await this.http.post( url.toString(), { ...bookData, formFields: [ { name: 'mobilephone', value: bookData.phone }, { name: 'message', value: bookData.message }, ], duration: 900000, }, { headers: { Authorization: `Bearer ${this.hubspotMeetingSchedulerToken}`, }, }, ) return response } async getMeetings(): Promise { const response = await this.http.get<{ data: { total: number results: HubspotMeeting[] } }>(`${this.hubspotApiDomain}/scheduler/v3/meetings/meeting-links`, { headers: { Authorization: `Bearer ${this.hubspotMeetingSchedulerToken}`, }, }) return response.data.results } async getMeeting( meetingSlug: string, timezone: string, ): Promise { if (!meetingSlug) { throw new Error('Meeting ID is required') } const url = new URL( `/scheduler/v3/meetings/meeting-links/book/${encodeURIComponent(meetingSlug)}?timezone=${encodeURIComponent(timezone)}`, this.hubspotApiDomain, ) const response = await this.http.get(url.toString(), { headers: { Authorization: `Bearer ${this.hubspotMeetingSchedulerToken}`, }, }) return response } async getAvailabilities( meetingSlug: string, timezone: string, monthOffset: string | undefined = '0', ) { if (!meetingSlug) { throw new Error('Meeting Slug is required') } const monthOffsetParam = monthOffset ? `&monthOffset=${monthOffset}` : '' const url = new URL( `/scheduler/v3/meetings/meeting-links/book/availability-page/${encodeURIComponent(meetingSlug)}?timezone=${encodeURIComponent(timezone)}&montOffset=${monthOffsetParam}`, this.hubspotApiDomain, ) const response = await this.http.get<{ data: unknown }>(url.toString(), { ...this.httpOptions, headers: { Authorization: `Bearer ${this.hubspotMeetingSchedulerToken}`, }, }) const parsedData = AvailabilitySchema.safeParse(response.data) return parsedData.data } async getAvailableDurations(meetingSlug: string, timezone: string) { const response = await this.getAvailabilities(meetingSlug, timezone) const availableDurations = Object.keys( response?.linkAvailability.linkAvailabilityByDuration ?? {}, ).map(Number) return availableDurations } async getComputedAvailabilities( availabilitesData: HubspotLinkAvailabilityResponse, duration: number, ) { const availabilities = ( availabilitesData.linkAvailability.linkAvailabilityByDuration[ duration.toString() ]?.availabilities ?? [] ) .filter((c) => c.startMillisUtc > Date.now()) .reduce( (acc, c) => { const date = new Date(c.startMillisUtc) const day = new Date( date.getFullYear(), date.getMonth(), date.getDate(), ).getTime() return { ...acc, [day]: [...(acc[day] ?? []), c.startMillisUtc], } }, {} as Record, ) return { availabilities, hasMore: availabilitesData.linkAvailability.hasMore, } } async getAllComputedAvailabilities( availabilitesData: HubspotLinkAvailabilityResponse, ) { const allAvailabilities: Record = {} const availableDurations = Object.keys( availabilitesData.linkAvailability.linkAvailabilityByDuration || {}, ).map((durationStr) => parseInt(durationStr)) for (const duration of availableDurations) { const availabilities = ( availabilitesData.linkAvailability.linkAvailabilityByDuration[ duration.toString() ]?.availabilities ?? [] ) .filter((c) => c.startMillisUtc > Date.now()) .reduce( (acc, c) => { const date = new Date(c.startMillisUtc) const day = new Date( date.getFullYear(), date.getMonth(), date.getDate(), ).getTime() if (!acc[day]) { acc[day] = [] } acc[day].push(c.startMillisUtc) return acc }, {} as Record, ) Object.entries(availabilities).forEach( ([dayTimestamp, availabilityTimes]) => { const dayKey = parseInt(dayTimestamp) if (!allAvailabilities[dayKey]) { allAvailabilities[dayKey] = [] } allAvailabilities[dayKey] = [ ...allAvailabilities[dayKey], ...availabilityTimes, ] }, ) } return allAvailabilities } async getMeetingDates( meetingSlug: string, timezone: string, monthOffset: string, ): Promise<{ dates: string[]; hasMore: boolean } | null> { const availabilities = await this.getAvailabilities( meetingSlug, timezone, monthOffset, ) if (!availabilities) { return null } const computedAvailabilitiesAllDurations: Record = {} const computedAvailabilities = await this.getAllComputedAvailabilities(availabilities) Object.entries(computedAvailabilities).forEach( ([dayTimestamp, availabilityTimes]) => { const dayKey = parseInt(dayTimestamp) if (!computedAvailabilitiesAllDurations[dayKey]) { computedAvailabilitiesAllDurations[dayKey] = [] } computedAvailabilitiesAllDurations[dayKey] = [ ...computedAvailabilitiesAllDurations[dayKey], ...availabilityTimes, ] }, ) const availableDatesMS = Object.keys(computedAvailabilitiesAllDurations) .map((key) => parseInt(key)) .sort((a, b) => a - b) const dates = availableDatesMS.map((date) => { const computedDate = new Date(date) const tzOffset = computedDate.getTimezoneOffset() * 60000 const ts = computedDate.getTime() - tzOffset return new Date(ts).toISOString() }) return { dates, hasMore: availabilities.linkAvailability.hasMore, } } async getAvailabilitiesForDate( date: string, meetingSlug: string, timezone: string, ) { const availableTimestampForDate: string[] = [] const now = new Date(Date.now()) const computableDate = new Date(date) const monthOffset = computableDate.getMonth() - now.getMonth() const availabilities = await this.getAvailabilities( meetingSlug, timezone, monthOffset.toString(), ) if (!availabilities) { return null } const computedAvailabilities = await this.getAllComputedAvailabilities(availabilities) Object.entries(computedAvailabilities).forEach( ([dayTimestamp, availabilityTimes]) => { const availableDate = new Date(Number(dayTimestamp)) if (computableDate.toDateString() === availableDate.toDateString()) { availableTimestampForDate.push( ...availabilityTimes.map((dateMs) => new Date(dateMs).toISOString(), ), ) } }, ) return availableTimestampForDate } }