// @ts-nocheck export function normalizeEmail(value?: string) { const normalized = String(value || "") .trim() .toLowerCase() return normalized || undefined } export function normalizePhone(value?: string) { const digits = String(value || "").replace(/\D/g, "") if (!digits) { return undefined } if (digits.length === 10) { return `+1${digits}` } if (digits.length === 11 && digits.startsWith("1")) { return `+${digits}` } return `+${digits}` } function trimOptional(value?: string) { const trimmed = String(value || "").trim() return trimmed || undefined } function isPlaceholderFirstName(value?: string) { const normalized = String(value || "") .trim() .toLowerCase() return normalized === "unknown" || normalized === "phone" } function isPlaceholderLastName(value?: string) { const normalized = String(value || "") .trim() .toLowerCase() return ( normalized === "contact" || normalized === "lead" || normalized === "caller" ) } function looksLikePhoneLabel(value?: string) { const normalized = trimOptional(value) if (!normalized) { return false } const digits = normalized.replace(/\D/g, "") return digits.length >= 7 && digits.length <= 15 } export function sanitizeContactNameParts(args: { firstName?: string lastName?: string fullName?: string }) { let firstName = trimOptional(args.firstName) let lastName = trimOptional(args.lastName) if (!firstName && !lastName) { const fullName = trimOptional(args.fullName) if (fullName && !looksLikePhoneLabel(fullName)) { const parts = fullName.split(/\s+/).filter(Boolean) if (parts.length === 1) { firstName = parts[0] } else if (parts.length > 1) { firstName = parts.shift() lastName = parts.join(" ") } } } if (isPlaceholderFirstName(firstName)) { firstName = undefined } if (isPlaceholderLastName(lastName)) { lastName = undefined } return { firstName, lastName, } } export function dedupeStrings(values?: string[]) { return Array.from( new Set( (values || []) .map((value) => String(value || "").trim()) .filter(Boolean) ) ) } export async function findContactByIdentity(ctx, args) { if (args.ghlContactId) { const byGhl = await ctx.db .query("contacts") .withIndex("by_ghlContactId", (q) => q.eq("ghlContactId", args.ghlContactId)) .unique() if (byGhl) { return byGhl } } const normalizedEmail = normalizeEmail(args.email) if (normalizedEmail) { const byEmail = await ctx.db .query("contacts") .withIndex("by_normalizedEmail", (q) => q.eq("normalizedEmail", normalizedEmail) ) .unique() if (byEmail) { return byEmail } } const normalizedPhone = normalizePhone(args.phone) if (normalizedPhone) { const byPhone = await ctx.db .query("contacts") .withIndex("by_normalizedPhone", (q) => q.eq("normalizedPhone", normalizedPhone) ) .unique() if (byPhone) { return byPhone } } return null } export async function upsertContactRecord(ctx, input) { const now = input.updatedAt ?? Date.now() const normalizedEmail = normalizeEmail(input.email) const normalizedPhone = normalizePhone(input.phone) const existing = await findContactByIdentity(ctx, { ghlContactId: input.ghlContactId, email: normalizedEmail, phone: normalizedPhone, }) const existingName = sanitizeContactNameParts({ firstName: existing?.firstName, lastName: existing?.lastName, }) const incomingName = sanitizeContactNameParts({ firstName: input.firstName, lastName: input.lastName, fullName: input.fullName, }) const patch = { firstName: incomingName.firstName ?? existingName.firstName ?? "", lastName: incomingName.lastName ?? existingName.lastName ?? "", email: input.email || existing?.email, normalizedEmail: normalizedEmail || existing?.normalizedEmail, phone: input.phone || existing?.phone, normalizedPhone: normalizedPhone || existing?.normalizedPhone, company: input.company ?? existing?.company, tags: dedupeStrings([...(existing?.tags || []), ...(input.tags || [])]), status: input.status || existing?.status || "lead", source: input.source || existing?.source, notes: input.notes ?? existing?.notes, ghlContactId: input.ghlContactId || existing?.ghlContactId, livekitIdentity: input.livekitIdentity || existing?.livekitIdentity, lastActivityAt: input.lastActivityAt ?? existing?.lastActivityAt ?? input.createdAt ?? now, updatedAt: now, } if (existing) { await ctx.db.patch(existing._id, patch) return await ctx.db.get(existing._id) } const id = await ctx.db.insert("contacts", { ...patch, createdAt: input.createdAt ?? now, }) return await ctx.db.get(id) } export async function upsertConversationRecord(ctx, input) { const now = input.updatedAt ?? Date.now() let existing = null if (input.ghlConversationId) { existing = await ctx.db .query("conversations") .withIndex("by_ghlConversationId", (q) => q.eq("ghlConversationId", input.ghlConversationId) ) .unique() } if (!existing && input.livekitRoomName) { existing = await ctx.db .query("conversations") .withIndex("by_livekitRoomName", (q) => q.eq("livekitRoomName", input.livekitRoomName) ) .unique() } if (!existing && input.voiceSessionId) { existing = await ctx.db .query("conversations") .withIndex("by_voiceSessionId", (q) => q.eq("voiceSessionId", input.voiceSessionId) ) .unique() } const patch = { contactId: input.contactId ?? existing?.contactId, title: input.title || existing?.title, channel: input.channel || existing?.channel || "unknown", source: input.source || existing?.source, status: input.status || existing?.status || "open", direction: input.direction || existing?.direction || "mixed", startedAt: input.startedAt ?? existing?.startedAt ?? now, endedAt: input.endedAt ?? existing?.endedAt, lastMessageAt: input.lastMessageAt ?? existing?.lastMessageAt, lastMessagePreview: input.lastMessagePreview ?? existing?.lastMessagePreview, unreadCount: input.unreadCount ?? existing?.unreadCount ?? 0, summaryText: input.summaryText ?? existing?.summaryText, ghlConversationId: input.ghlConversationId || existing?.ghlConversationId, livekitRoomName: input.livekitRoomName || existing?.livekitRoomName, voiceSessionId: input.voiceSessionId ?? existing?.voiceSessionId, updatedAt: now, } if (existing) { await ctx.db.patch(existing._id, patch) return await ctx.db.get(existing._id) } const id = await ctx.db.insert("conversations", { ...patch, createdAt: input.createdAt ?? now, }) return await ctx.db.get(id) } export async function ensureConversationParticipant(ctx, input) { const participants = await ctx.db .query("conversationParticipants") .withIndex("by_conversationId", (q) => q.eq("conversationId", input.conversationId) ) .collect() const normalizedEmail = normalizeEmail(input.email) const normalizedPhone = normalizePhone(input.phone) const existing = participants.find((participant) => { if (input.contactId && participant.contactId === input.contactId) { return true } if ( input.externalContactId && participant.externalContactId === input.externalContactId ) { return true } if (normalizedEmail && participant.normalizedEmail === normalizedEmail) { return true } if (normalizedPhone && participant.normalizedPhone === normalizedPhone) { return true } return false }) const patch = { contactId: input.contactId ?? existing?.contactId, role: input.role || existing?.role || "unknown", displayName: input.displayName || existing?.displayName, phone: input.phone || existing?.phone, normalizedPhone: normalizedPhone || existing?.normalizedPhone, email: input.email || existing?.email, normalizedEmail: normalizedEmail || existing?.normalizedEmail, externalContactId: input.externalContactId || existing?.externalContactId, updatedAt: Date.now(), } if (existing) { await ctx.db.patch(existing._id, patch) return await ctx.db.get(existing._id) } const id = await ctx.db.insert("conversationParticipants", { conversationId: input.conversationId, ...patch, createdAt: Date.now(), }) return await ctx.db.get(id) } export async function upsertMessageRecord(ctx, input) { let existing = null if (input.ghlMessageId) { existing = await ctx.db .query("messages") .withIndex("by_ghlMessageId", (q) => q.eq("ghlMessageId", input.ghlMessageId) ) .unique() } if (!existing && input.voiceTranscriptTurnId) { existing = await ctx.db .query("messages") .withIndex("by_voiceTranscriptTurnId", (q) => q.eq("voiceTranscriptTurnId", input.voiceTranscriptTurnId) ) .unique() } const now = input.updatedAt ?? Date.now() const patch = { conversationId: input.conversationId, contactId: input.contactId, direction: input.direction || existing?.direction || "system", channel: input.channel || existing?.channel || "unknown", source: input.source || existing?.source, messageType: input.messageType || existing?.messageType, body: String(input.body || existing?.body || "").trim(), status: input.status || existing?.status, sentAt: input.sentAt ?? existing?.sentAt ?? now, ghlMessageId: input.ghlMessageId || existing?.ghlMessageId, voiceTranscriptTurnId: input.voiceTranscriptTurnId ?? existing?.voiceTranscriptTurnId, voiceSessionId: input.voiceSessionId ?? existing?.voiceSessionId, livekitRoomName: input.livekitRoomName || existing?.livekitRoomName, metadata: input.metadata || existing?.metadata, updatedAt: now, } let message if (existing) { await ctx.db.patch(existing._id, patch) message = await ctx.db.get(existing._id) } else { const id = await ctx.db.insert("messages", { ...patch, createdAt: input.createdAt ?? now, }) message = await ctx.db.get(id) } await ctx.db.patch(input.conversationId, { lastMessageAt: patch.sentAt, lastMessagePreview: patch.body.slice(0, 240), updatedAt: now, }) return message } export async function upsertCallArtifactRecord(ctx, input) { let existing = null if (input.recordingId) { existing = await ctx.db .query("callArtifacts") .withIndex("by_recordingId", (q) => q.eq("recordingId", input.recordingId)) .unique() } if (!existing && input.voiceSessionId) { existing = await ctx.db .query("callArtifacts") .withIndex("by_voiceSessionId", (q) => q.eq("voiceSessionId", input.voiceSessionId) ) .unique() } if (!existing && input.ghlMessageId) { existing = await ctx.db .query("callArtifacts") .withIndex("by_ghlMessageId", (q) => q.eq("ghlMessageId", input.ghlMessageId) ) .unique() } const now = input.updatedAt ?? Date.now() const patch = { conversationId: input.conversationId, contactId: input.contactId ?? existing?.contactId, source: input.source || existing?.source, recordingId: input.recordingId || existing?.recordingId, recordingUrl: input.recordingUrl || existing?.recordingUrl, recordingStatus: input.recordingStatus || existing?.recordingStatus, transcriptionText: input.transcriptionText ?? existing?.transcriptionText, durationMs: input.durationMs ?? existing?.durationMs, startedAt: input.startedAt ?? existing?.startedAt, endedAt: input.endedAt ?? existing?.endedAt, ghlMessageId: input.ghlMessageId || existing?.ghlMessageId, voiceSessionId: input.voiceSessionId ?? existing?.voiceSessionId, livekitRoomName: input.livekitRoomName || existing?.livekitRoomName, metadata: input.metadata || existing?.metadata, updatedAt: now, } if (existing) { await ctx.db.patch(existing._id, patch) return await ctx.db.get(existing._id) } const id = await ctx.db.insert("callArtifacts", { ...patch, createdAt: input.createdAt ?? now, }) return await ctx.db.get(id) } export async function upsertExternalSyncState(ctx, input) { const existing = await ctx.db .query("externalSyncState") .withIndex("by_provider_entityType_entityId", (q) => q .eq("provider", input.provider) .eq("entityType", input.entityType) .eq("entityId", input.entityId) ) .unique() const patch = { cursor: input.cursor ?? existing?.cursor, checksum: input.checksum ?? existing?.checksum, status: input.status || existing?.status || "pending", lastAttemptAt: input.lastAttemptAt ?? existing?.lastAttemptAt ?? Date.now(), lastSyncedAt: input.lastSyncedAt ?? existing?.lastSyncedAt, error: input.error ?? existing?.error, metadata: input.metadata ?? existing?.metadata, updatedAt: Date.now(), } if (existing) { await ctx.db.patch(existing._id, patch) return await ctx.db.get(existing._id) } const id = await ctx.db.insert("externalSyncState", { provider: input.provider, entityType: input.entityType, entityId: input.entityId, ...patch, }) return await ctx.db.get(id) }