import { readFile } from "node:fs/promises" import { resolve } from "node:path" import { config as loadEnv } from "dotenv" import { fetchMutation } from "convex/nextjs" import { api } from "@/convex/_generated/api" loadEnv({ path: ".env.local" }) type ImportRecord = { firstName?: string lastName?: string name?: string email?: string phone?: string company?: string notes?: string } function normalizePhone(value?: string | null) { const digits = String(value || "").replace(/\D/g, "") if (!digits) { return "" } if (digits.length === 10) { return `+1${digits}` } if (digits.length === 11 && digits.startsWith("1")) { return `+${digits}` } if (digits.length >= 11) { return `+${digits}` } return "" } function splitCsvLine(line: string) { const values: string[] = [] let current = "" let inQuotes = false for (let index = 0; index < line.length; index += 1) { const char = line[index] const next = line[index + 1] if (char === '"' && inQuotes && next === '"') { current += '"' index += 1 continue } if (char === '"') { inQuotes = !inQuotes continue } if (char === "," && !inQuotes) { values.push(current.trim()) current = "" continue } current += char } values.push(current.trim()) return values } function parseCsv(content: string): ImportRecord[] { const lines = content .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) if (lines.length < 2) { return [] } const headers = splitCsvLine(lines[0]).map((header) => header.toLowerCase()) return lines.slice(1).map((line) => { const values = splitCsvLine(line) const record: Record = {} headers.forEach((header, index) => { record[header] = values[index] || "" }) return { firstName: record.firstname || record["first name"] || record.first_name, lastName: record.lastname || record["last name"] || record.last_name, name: record.name || record.fullname || record["full name"], email: record.email || record["email address"], phone: record.phone || record["phone number"] || record.mobile, company: record.company || record["company name"], notes: record.notes || record.note, } }) } function parseJson(content: string): ImportRecord[] { const value = JSON.parse(content) if (!Array.isArray(value)) { throw new Error("JSON import file must contain an array of contacts.") } return value } async function loadRecords(pathname: string) { const absolutePath = resolve(pathname) const content = await readFile(absolutePath, "utf8") if (absolutePath.endsWith(".json")) { return parseJson(content) } return parseCsv(content) } async function main() { const inputPath = process.argv[2] if (!inputPath) { throw new Error("Usage: tsx scripts/import-ghl-contacts-to-contact-profiles.ts ") } const records = await loadRecords(inputPath) let imported = 0 let skipped = 0 for (const record of records) { const normalizedPhone = normalizePhone(record.phone) if (!normalizedPhone) { skipped += 1 continue } const displayName = record.name?.trim() || [record.firstName, record.lastName].filter(Boolean).join(" ").trim() await fetchMutation(api.contactProfiles.upsertByPhone, { normalizedPhone, displayName: displayName || undefined, firstName: record.firstName?.trim() || undefined, lastName: record.lastName?.trim() || undefined, email: record.email?.trim() || undefined, company: record.company?.trim() || undefined, reminderNotes: record.notes?.trim() || undefined, source: "ghl-import", }) imported += 1 } console.log( JSON.stringify( { imported, skipped, total: records.length, }, null, 2 ) ) } main().catch((error) => { console.error(error) process.exit(1) })