diff --git a/.vscode/settings.json b/.vscode/settings.json index a41a7f454..ae99b09d2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,6 +38,7 @@ "EHRC", "estree", "estruyf", + "firstname", "fkey", "fontsource", "Geist", @@ -64,6 +65,7 @@ "keyv", "langchain", "languagedetect", + "lastname", "Lewandowski", "Lexend", "likert", diff --git a/packages/api/src/router/auth.ts b/packages/api/src/router/auth.ts index a8725d1e1..c72ca8aff 100644 --- a/packages/api/src/router/auth.ts +++ b/packages/api/src/router/auth.ts @@ -1,5 +1,6 @@ import { clerkClient } from "@clerk/nextjs/server"; import { demoUsers } from "@oakai/core"; +import { createHubspotCustomer } from "@oakai/core/src/analytics/hubspotClient"; import { posthogServerClient } from "@oakai/core/src/analytics/posthogServerClient"; import { z } from "zod"; @@ -80,6 +81,20 @@ export const authRouter = router({ }, }); + const email = updatedUser.emailAddresses[0]?.emailAddress; + if (!email) { + throw new Error("Email address is expected on clerk user"); + } + + await createHubspotCustomer({ + email, + firstName: updatedUser.firstName, + lastName: updatedUser.lastName, + marketingAccepted: Boolean( + updatedUser.privateMetadata.acceptedPrivacyPolicy, + ), + }); + const { acceptedPrivacyPolicy, acceptedTermsOfUse } = updatedUser.privateMetadata; diff --git a/packages/core/package.json b/packages/core/package.json index bf5376ec3..c558324a2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -36,6 +36,7 @@ "@googleapis/docs": "^3.0.0", "@googleapis/drive": "^8.7.0", "@googleapis/slides": "^1.0.5", + "@hubspot/api-client": "^11.2.0", "@oakai/db": "*", "@oakai/logger": "*", "@slack/web-api": "^7.3.1", diff --git a/packages/core/src/analytics/hubspotClient.ts b/packages/core/src/analytics/hubspotClient.ts new file mode 100644 index 000000000..60994a39e --- /dev/null +++ b/packages/core/src/analytics/hubspotClient.ts @@ -0,0 +1,63 @@ +import { Client } from "@hubspot/api-client"; +import { ApiException } from "@hubspot/api-client/lib/codegen/crm/contacts/apis/exception"; + +const accessToken = process.env.HUBSPOT_ACCESS_TOKEN; +if (!accessToken) { + throw new Error("Missing HUBSPOT_ACCESS_TOKEN"); +} + +const hubspotClient = new Client({ accessToken }); + +interface CreateHubspotCustomerInput { + email: string; + firstName: string | null; + lastName: string | null; + marketingAccepted: boolean; +} + +export const createHubspotCustomer = async ({ + email, + firstName, + lastName, + marketingAccepted, +}: CreateHubspotCustomerInput) => { + let id: string | undefined; + try { + const result = await hubspotClient.crm.contacts.basicApi.getById( + email, + undefined, + undefined, + undefined, + undefined, + "email", + ); + id = result.id; + } catch (e) { + const isNotFoundError = e instanceof ApiException && e.code === 404; + if (!isNotFoundError) { + throw e; + } + } + + const properties = { + email, + ...(firstName && { + firstname: firstName, + }), + ...(lastName && { + lastname: lastName, + }), + email_consent_on_ai_account_creation: marketingAccepted ? "true" : "false", + }; + + if (id) { + return await hubspotClient.crm.contacts.basicApi.update(id, { + properties, + }); + } + + return await hubspotClient.crm.contacts.basicApi.create({ + properties, + associations: [], + }); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fbe4c2c0..37d06ff65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -568,6 +568,9 @@ importers: '@googleapis/slides': specifier: ^1.0.5 version: 1.0.5 + '@hubspot/api-client': + specifier: ^11.2.0 + version: 11.2.0 '@oakai/db': specifier: '*' version: link:../db @@ -2448,7 +2451,7 @@ packages: '@clerk/clerk-react': 5.2.0(react-dom@18.2.0)(react@18.2.0) '@clerk/shared': 2.2.0(react-dom@18.2.0)(react@18.2.0) crypto-js: 4.2.0 - next: 14.2.5(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.5(react-dom@18.2.0)(react@18.2.0) path-to-regexp: 6.2.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -3005,6 +3008,21 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@hubspot/api-client@11.2.0: + resolution: {integrity: sha512-OWf/vwuOw3lMRywdyBprAl5nt3a3kRVpLcIwz6hT7uxobNOPBElFMbp3nv0dM5JZqMD78NMT7VUTArKjKhRQKg==} + dependencies: + '@types/node-fetch': 2.6.4 + bottleneck: 2.19.5 + es6-promise: 4.2.8 + form-data: 2.5.1 + lodash.get: 4.4.2 + lodash.merge: 4.6.2 + node-fetch: 2.7.0 + url-parse: 1.5.10 + transitivePeerDependencies: + - encoding + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -11264,6 +11282,10 @@ packages: is-date-object: 1.0.5 is-symbol: 1.0.4 + /es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + dev: false + /esbuild-register@3.6.0(esbuild@0.21.5): resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -15072,6 +15094,10 @@ packages: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} dev: false + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: false + /lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} dev: true @@ -16114,7 +16140,7 @@ packages: postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.24.5)(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) optionalDependencies: '@next/swc-darwin-arm64': 14.2.5 '@next/swc-darwin-x64': 14.2.5 @@ -19415,6 +19441,23 @@ packages: client-only: 0.0.1 react: 18.2.0 + /styled-jsx@5.1.1(react@18.2.0): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + client-only: 0.0.1 + react: 18.2.0 + dev: false + /sucrase@3.34.0: resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} engines: {node: '>=8'}