Rocky_Mountain_Vending/scripts/import-ghl-contacts-to-contact-profiles.ts

166 lines
3.9 KiB
TypeScript

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<string, string> = {}
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 <contacts.json|contacts.csv>")
}
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)
})