// @ts-nocheck import { action, mutation } from "./_generated/server"; import { v } from "convex/values"; const leadSyncStatus = v.union( v.literal("pending"), v.literal("sent"), v.literal("synced"), v.literal("failed"), v.literal("skipped"), ); const baseLead = { firstName: v.string(), lastName: v.string(), email: v.string(), phone: v.string(), company: v.optional(v.string()), intent: v.optional(v.string()), message: v.optional(v.string()), source: v.optional(v.string()), page: v.optional(v.string()), url: v.optional(v.string()), serviceTextConsent: v.optional(v.boolean()), marketingTextConsent: v.optional(v.boolean()), consentVersion: v.optional(v.string()), consentCapturedAt: v.optional(v.string()), consentSourcePage: v.optional(v.string()), }; function splitName(name: string) { const trimmed = String(name || "").trim(); if (!trimmed) { return { firstName: "Unknown", lastName: "Lead" }; } const parts = trimmed.split(/\s+/); const firstName = parts.shift() || "Unknown"; const lastName = parts.join(" ") || "Lead"; return { firstName, lastName }; } function mapServiceToType(service: string | undefined) { return String(service || "").toLowerCase() === "machine-request" ? "requestMachine" : "contact"; } function deriveSubmissionStatus(usesendStatus?: string, ghlStatus?: string) { if (usesendStatus === "sent" || ghlStatus === "synced") { return "delivered"; } if (usesendStatus === "failed" && ghlStatus === "failed") { return "failed"; } return "pending"; } async function sendWebhook(webhookUrl: string | undefined, payload: Record) { if (!webhookUrl) { return { success: false, error: "Webhook URL not configured" }; } const response = await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!response.ok) { return { success: false, error: `${response.status} ${response.statusText}`, }; } return { success: true }; } export const createLead = mutation({ args: { type: v.union(v.literal("contact"), v.literal("requestMachine")), idempotencyKey: v.optional(v.string()), firstName: v.string(), lastName: v.string(), email: v.string(), phone: v.string(), company: v.optional(v.string()), intent: v.optional(v.string()), message: v.optional(v.string()), source: v.optional(v.string()), page: v.optional(v.string()), url: v.optional(v.string()), employeeCount: v.optional(v.string()), machineType: v.optional(v.string()), machineCount: v.optional(v.string()), serviceTextConsent: v.optional(v.boolean()), marketingTextConsent: v.optional(v.boolean()), consentVersion: v.optional(v.string()), consentCapturedAt: v.optional(v.string()), consentSourcePage: v.optional(v.string()), marketingConsent: v.optional(v.boolean()), termsAgreement: v.optional(v.boolean()), usesendStatus: v.optional(leadSyncStatus), ghlStatus: v.optional(leadSyncStatus), status: v.union(v.literal("pending"), v.literal("delivered"), v.literal("failed")), error: v.optional(v.string()), }, handler: async (ctx, args) => { const now = Date.now(); return await ctx.db.insert("leadSubmissions", { ...args, createdAt: now, updatedAt: now, deliveredAt: args.status === "delivered" ? now : undefined, }); }, }); export const ingestLead = mutation({ args: { host: v.string(), tenantSlug: v.optional(v.string()), tenantName: v.optional(v.string()), tenantDomains: v.optional(v.array(v.string())), source: v.optional(v.string()), idempotencyKey: v.string(), name: v.string(), firstName: v.optional(v.string()), lastName: v.optional(v.string()), email: v.string(), phone: v.string(), company: v.optional(v.string()), service: v.optional(v.string()), intent: v.optional(v.string()), page: v.optional(v.string()), url: v.optional(v.string()), message: v.string(), employeeCount: v.optional(v.string()), machineType: v.optional(v.string()), machineCount: v.optional(v.string()), serviceTextConsent: v.optional(v.boolean()), marketingTextConsent: v.optional(v.boolean()), consentVersion: v.optional(v.string()), consentCapturedAt: v.optional(v.string()), consentSourcePage: v.optional(v.string()), }, handler: async (ctx, args) => { const existing = await ctx.db .query("leadSubmissions") .withIndex("by_idempotencyKey", (q) => q.eq("idempotencyKey", args.idempotencyKey)) .unique(); if (existing) { return { inserted: false, leadId: existing._id, idempotencyKey: args.idempotencyKey, tenantId: args.tenantSlug || args.host, }; } const fallbackName = splitName(args.name); const type = mapServiceToType(args.service); const now = Date.now(); const leadId = await ctx.db.insert("leadSubmissions", { type, status: "pending", idempotencyKey: args.idempotencyKey, firstName: args.firstName || fallbackName.firstName, lastName: args.lastName || fallbackName.lastName, email: args.email, phone: args.phone, company: args.company, intent: args.intent || args.service, message: args.message, source: args.source, page: args.page, url: args.url, employeeCount: args.employeeCount, machineType: args.machineType, machineCount: args.machineCount, serviceTextConsent: args.serviceTextConsent, marketingTextConsent: args.marketingTextConsent, consentVersion: args.consentVersion, consentCapturedAt: args.consentCapturedAt, consentSourcePage: args.consentSourcePage, usesendStatus: "pending", ghlStatus: "pending", createdAt: now, updatedAt: now, }); return { inserted: true, leadId, idempotencyKey: args.idempotencyKey, tenantId: args.tenantSlug || args.host, }; }, }); export const updateLeadSyncStatus = mutation({ args: { leadId: v.id("leadSubmissions"), usesendStatus: v.optional(leadSyncStatus), ghlStatus: v.optional(leadSyncStatus), error: v.optional(v.string()), }, handler: async (ctx, args) => { const lead = await ctx.db.get(args.leadId); if (!lead) { throw new Error("Lead not found"); } const usesendStatus = args.usesendStatus ?? lead.usesendStatus; const ghlStatus = args.ghlStatus ?? lead.ghlStatus; const status = deriveSubmissionStatus(usesendStatus, ghlStatus); const now = Date.now(); await ctx.db.patch(args.leadId, { usesendStatus, ghlStatus, status, error: args.error, deliveredAt: status === "delivered" ? lead.deliveredAt ?? now : lead.deliveredAt, updatedAt: now, }); return await ctx.db.get(args.leadId); }, }); export const submitContact = action({ args: { ...baseLead, }, handler: async (ctx, args) => { const payload = { firstName: args.firstName, lastName: args.lastName, email: args.email, phone: args.phone, company: args.company ?? "", custom1: args.message ?? "", custom2: args.source ?? "website", custom3: args.page ?? "", custom4: new Date().toISOString(), custom5: args.url ?? "", custom6: args.intent ?? "", custom7: args.serviceTextConsent ? "Consented" : "Not Consented", custom8: args.marketingTextConsent ? "Consented" : "Not Consented", custom9: args.consentVersion ?? "", custom10: args.consentCapturedAt ?? "", custom11: args.consentSourcePage ?? "", }; const result = await sendWebhook(process.env.GHL_CONTACT_WEBHOOK_URL, payload); await ctx.runMutation("leads:createLead", { type: "contact", ...args, usesendStatus: "skipped", ghlStatus: result.success ? "synced" : "failed", status: result.success ? "delivered" : "failed", error: result.error, }); return result; }, }); export const submitRequestMachine = action({ args: { ...baseLead, employeeCount: v.string(), machineType: v.string(), machineCount: v.string(), }, handler: async (ctx, args) => { const payload = { firstName: args.firstName, lastName: args.lastName, email: args.email, phone: args.phone, company: args.company ?? "", custom1: args.employeeCount, custom2: args.machineType, custom3: args.machineCount, custom4: args.message ?? "", custom5: args.source ?? "website", custom6: args.page ?? "", custom7: new Date().toISOString(), custom8: args.url ?? "", custom9: args.serviceTextConsent ? "Consented" : "Not Consented", custom10: args.marketingTextConsent ? "Consented" : "Not Consented", custom11: args.consentVersion ?? "", custom12: args.consentCapturedAt ?? "", custom13: args.consentSourcePage ?? "", custom14: "Free Consultation Request", tags: ["Machine Request"], }; const result = await sendWebhook(process.env.GHL_REQUEST_MACHINE_WEBHOOK_URL, payload); await ctx.runMutation("leads:createLead", { type: "requestMachine", ...args, usesendStatus: "skipped", ghlStatus: result.success ? "synced" : "failed", status: result.success ? "delivered" : "failed", error: result.error, }); return result; }, });