index.ts110 lines · main
1import 'server-only'
2
3import { CustomerIOClient, type CustomerIOConfig } from './customerio'
4import { HubSpotClient, type HubSpotConfig } from './hubspot'
5import { NotionClient, type NotionConfig } from './notion'
6
7export type { CustomerIOConfig } from './customerio'
8export type { HubSpotConfig } from './hubspot'
9export type { NotionConfig } from './notion'
10export { CustomerIOClient } from './customerio'
11export { HubSpotClient } from './hubspot'
12export { NotionClient } from './notion'
13
14export interface CRMConfig {
15 hubspot?: HubSpotConfig
16 customerio?: CustomerIOConfig
17 notion?: NotionConfig
18}
19
20export interface CRMEventOptions {
21 /** Email used as identifier */
22 email: string
23 /** Fields submitted to HubSpot form (key = HubSpot field name, value = field value) */
24 hubspotFields?: Record<string, string>
25 /** HubSpot form context */
26 context?: { pageUri?: string; pageName?: string }
27 /** Legal consent text for HubSpot */
28 consent?: string
29 /** Event name for Customer.io tracking */
30 event?: string
31 /** Properties attached to the Customer.io event */
32 properties?: Record<string, unknown>
33 /** Profile attributes to set on the Customer.io contact */
34 customerioProfile?: Record<string, unknown>
35 /** Notion page creation — values are keyed by Notion database column name */
36 notion?: {
37 databaseId: string
38 properties: Record<string, unknown>
39 }
40}
41
42export class CRMClient {
43 private hubspot: HubSpotClient | null
44 private customerio: CustomerIOClient | null
45 private notion: NotionClient | null
46
47 constructor(config: CRMConfig) {
48 this.hubspot = config.hubspot ? new HubSpotClient(config.hubspot) : null
49 this.customerio = config.customerio ? new CustomerIOClient(config.customerio) : null
50 this.notion = config.notion ? new NotionClient(config.notion) : null
51 }
52
53 /**
54 * Submit an event to configured providers in parallel.
55 * Each provider is invoked only when both its client and its payload are present.
56 */
57 async submitEvent(options: CRMEventOptions): Promise<{ errors: Error[] }> {
58 const promises: Promise<void>[] = []
59 const errors: Error[] = []
60
61 if (this.hubspot && options.hubspotFields) {
62 const fields = { ...options.hubspotFields }
63 if (!fields.email) fields.email = options.email
64
65 promises.push(
66 this.hubspot
67 .submitForm(fields, {
68 pageUri: options.context?.pageUri,
69 pageName: options.context?.pageName,
70 consent: options.consent,
71 })
72 .catch((err) => {
73 errors.push(new Error(`HubSpot: ${err.message}`))
74 })
75 )
76 }
77
78 if (this.customerio && options.event) {
79 promises.push(
80 (async () => {
81 try {
82 if (options.customerioProfile) {
83 await this.customerio!.identify(options.email, options.customerioProfile)
84 }
85 await this.customerio!.track(options.email, options.event!, {
86 ...options.properties,
87 email: options.email,
88 submitted_at: new Date().toISOString(),
89 })
90 } catch (err: any) {
91 errors.push(new Error(`Customer.io: ${err.message}`))
92 }
93 })()
94 )
95 }
96
97 if (this.notion && options.notion) {
98 const { databaseId, properties } = options.notion
99 promises.push(
100 this.notion.createDatabasePage(databaseId, properties).catch((err) => {
101 errors.push(new Error(`Notion: ${err.message}`))
102 })
103 )
104 }
105
106 await Promise.all(promises)
107
108 return { errors }
109 }
110}