import "dotenv/config" import { ConvexHttpClient } from "convex/browser" import { createPrismaClient } from "./utils/prisma-client.mjs" const prisma = createPrismaClient() const tenantId = process.env.SYNC_TENANT_ID || "tenant-atlas" const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL || "http://127.0.0.1:3210" const secret = process.env.CONVEX_SYNC_SECRET const STAFF_ROSTER = [ { email: "admin@sistema.dev", name: "Administrador", role: "ADMIN" }, { email: "gabriel.oliveira@rever.com.br", name: "Gabriel Oliveira", role: "AGENT" }, { email: "george.araujo@rever.com.br", name: "George Araujo", role: "AGENT" }, { email: "hugo.soares@rever.com.br", name: "Hugo Soares", role: "AGENT" }, { email: "julio@rever.com.br", name: "Julio Cesar", role: "AGENT" }, { email: "lorena@rever.com.br", name: "Lorena Magalhães", role: "AGENT" }, { email: "renan.pac@paulicon.com.br", name: "Rever", role: "AGENT" }, { email: "suporte@rever.com.br", name: "Telão", role: "AGENT" }, { email: "thiago.medeiros@rever.com.br", name: "Thiago Medeiros", role: "AGENT" }, { email: "weslei@rever.com.br", name: "Weslei Magalhães", role: "AGENT" }, ] const rawDefaultAssigneeEmail = process.env.SYNC_DEFAULT_ASSIGNEE || "gabriel.oliveira@rever.com.br" if (!secret) { console.error("CONVEX_SYNC_SECRET não configurado. Configure no .env.") process.exit(1) } const allowedRoles = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"]) const client = new ConvexHttpClient(convexUrl) function normalizeEmail(email) { if (!email) return null return email.trim().toLowerCase() } const defaultAssigneeEmail = normalizeEmail(rawDefaultAssigneeEmail) function toDate(value) { if (!value && value !== 0) return null return new Date(value) } function slugify(value) { return value .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^\w\s-]/g, "") .trim() .replace(/\s+/g, "-") .replace(/-+/g, "-") .toLowerCase() } const STATUS_MAP = { NEW: "PENDING", PENDING: "PENDING", OPEN: "AWAITING_ATTENDANCE", AWAITING_ATTENDANCE: "AWAITING_ATTENDANCE", ON_HOLD: "PAUSED", PAUSED: "PAUSED", RESOLVED: "RESOLVED", CLOSED: "RESOLVED", } function normalizeStatus(status) { if (!status) return "PENDING" const key = String(status).toUpperCase() return STATUS_MAP[key] ?? "PENDING" } function serializeConvexSlaSnapshot(snapshot) { if (!snapshot || typeof snapshot !== "object") return null const categoryIdRaw = snapshot.categoryId let categoryId if (typeof categoryIdRaw === "string") { categoryId = categoryIdRaw } else if (categoryIdRaw && typeof categoryIdRaw === "object" && "_id" in categoryIdRaw) { categoryId = categoryIdRaw._id } const pauseStatuses = Array.isArray(snapshot.pauseStatuses) && snapshot.pauseStatuses.length > 0 ? snapshot.pauseStatuses.filter((value) => typeof value === "string") : undefined const normalized = { categoryId, categoryName: typeof snapshot.categoryName === "string" ? snapshot.categoryName : undefined, priority: typeof snapshot.priority === "string" ? snapshot.priority : undefined, responseTargetMinutes: typeof snapshot.responseTargetMinutes === "number" ? snapshot.responseTargetMinutes : undefined, responseMode: typeof snapshot.responseMode === "string" ? snapshot.responseMode : undefined, solutionTargetMinutes: typeof snapshot.solutionTargetMinutes === "number" ? snapshot.solutionTargetMinutes : undefined, solutionMode: typeof snapshot.solutionMode === "string" ? snapshot.solutionMode : undefined, alertThreshold: typeof snapshot.alertThreshold === "number" ? snapshot.alertThreshold : undefined, pauseStatuses, } const hasValues = Object.values(normalized).some((value) => value !== undefined) return hasValues ? normalized : null } async function upsertCompanies(snapshotCompanies) { const map = new Map() for (const company of snapshotCompanies) { const slug = company.slug || slugify(company.name) const record = await prisma.company.upsert({ where: { tenantId_slug: { tenantId, slug, }, }, update: { name: company.name, isAvulso: Boolean(company.isAvulso ?? false), cnpj: company.cnpj ?? null, domain: company.domain ?? null, phone: company.phone ?? null, description: company.description ?? null, address: company.address ?? null, }, create: { tenantId, name: company.name, slug, isAvulso: Boolean(company.isAvulso ?? false), cnpj: company.cnpj ?? null, domain: company.domain ?? null, phone: company.phone ?? null, description: company.description ?? null, address: company.address ?? null, createdAt: toDate(company.createdAt) ?? new Date(), updatedAt: toDate(company.updatedAt) ?? new Date(), }, }) map.set(slug, record.id) } return map } async function upsertUsers(snapshotUsers, companyMap) { const map = new Map() for (const user of snapshotUsers) { const normalizedEmail = normalizeEmail(user.email) if (!normalizedEmail) continue const normalizedRole = (user.role ?? "MANAGER").toUpperCase() const role = allowedRoles.has(normalizedRole) ? normalizedRole : "MANAGER" const companyId = user.companySlug ? companyMap.get(user.companySlug) ?? null : null const record = await prisma.user.upsert({ where: { email: normalizedEmail }, update: { name: user.name ?? normalizedEmail, role, tenantId, avatarUrl: user.avatarUrl ?? null, companyId, }, create: { email: normalizedEmail, name: user.name ?? normalizedEmail, role, tenantId, avatarUrl: user.avatarUrl ?? null, companyId, }, }) map.set(normalizedEmail, record.id) } for (const staff of STAFF_ROSTER) { const normalizedEmail = normalizeEmail(staff.email) if (!normalizedEmail) continue const record = await prisma.user.upsert({ where: { email: normalizedEmail }, update: { name: staff.name, role: staff.role, tenantId, companyId: null, }, create: { email: normalizedEmail, name: staff.name, role: staff.role, tenantId, avatarUrl: null, companyId: null, }, }) map.set(normalizedEmail, record.id) } const allowedStaffEmails = new Set(STAFF_ROSTER.map((staff) => normalizeEmail(staff.email)).filter(Boolean)) const removableStaff = await prisma.user.findMany({ where: { tenantId, role: { in: ["ADMIN", "AGENT", "COLLABORATOR"] }, email: { notIn: Array.from(allowedStaffEmails), }, }, }) const fallbackAssigneeId = defaultAssigneeEmail ? map.get(defaultAssigneeEmail) ?? null : null for (const staff of removableStaff) { if (fallbackAssigneeId) { await prisma.ticket.updateMany({ where: { tenantId, assigneeId: staff.id }, data: { assigneeId: fallbackAssigneeId }, }) await prisma.ticketComment.updateMany({ where: { authorId: staff.id }, data: { authorId: fallbackAssigneeId }, }) } await prisma.user.update({ where: { id: staff.id }, data: { role: "MANAGER", }, }) } return map } async function upsertQueues(snapshotQueues) { const map = new Map() for (const queue of snapshotQueues) { if (!queue.slug) continue const record = await prisma.queue.upsert({ where: { tenantId_slug: { tenantId, slug: queue.slug, }, }, update: { name: queue.name, }, create: { tenantId, name: queue.name, slug: queue.slug, }, }) map.set(queue.slug, record.id) } return map } async function upsertTickets(snapshotTickets, userMap, queueMap, companyMap) { let created = 0 let updated = 0 const fallbackAssigneeId = defaultAssigneeEmail ? userMap.get(defaultAssigneeEmail) ?? null : null for (const ticket of snapshotTickets) { if (!ticket.requesterEmail) continue const requesterId = userMap.get(normalizeEmail(ticket.requesterEmail)) if (!requesterId) continue const queueId = ticket.queueSlug ? queueMap.get(ticket.queueSlug) ?? null : null let companyId = ticket.companySlug ? companyMap.get(ticket.companySlug) ?? null : null if (!companyId && requesterId) { const requester = await prisma.user.findUnique({ where: { id: requesterId }, select: { companyId: true }, }) companyId = requester?.companyId ?? null } const desiredAssigneeEmail = defaultAssigneeEmail || normalizeEmail(ticket.assigneeEmail) const assigneeId = desiredAssigneeEmail ? userMap.get(desiredAssigneeEmail) || fallbackAssigneeId || null : fallbackAssigneeId || null const slaSnapshot = serializeConvexSlaSnapshot(ticket.slaSnapshot) const existing = await prisma.ticket.findFirst({ where: { tenantId, reference: ticket.reference, }, }) const data = { subject: ticket.subject, summary: ticket.summary ?? null, status: normalizeStatus(ticket.status), priority: (ticket.priority ?? "MEDIUM").toUpperCase(), channel: (ticket.channel ?? "MANUAL").toUpperCase(), queueId, requesterId, assigneeId, dueAt: toDate(ticket.dueAt), firstResponseAt: toDate(ticket.firstResponseAt), resolvedAt: toDate(ticket.resolvedAt), closedAt: toDate(ticket.closedAt), createdAt: toDate(ticket.createdAt) ?? new Date(), updatedAt: toDate(ticket.updatedAt) ?? new Date(), companyId, slaSnapshot: slaSnapshot ?? null, slaResponseDueAt: toDate(ticket.slaResponseDueAt), slaSolutionDueAt: toDate(ticket.slaSolutionDueAt), slaResponseStatus: typeof ticket.slaResponseStatus === "string" ? ticket.slaResponseStatus : null, slaSolutionStatus: typeof ticket.slaSolutionStatus === "string" ? ticket.slaSolutionStatus : null, slaPausedAt: toDate(ticket.slaPausedAt), slaPausedBy: typeof ticket.slaPausedBy === "string" ? ticket.slaPausedBy : null, slaPausedMs: typeof ticket.slaPausedMs === "number" ? ticket.slaPausedMs : null, } let ticketRecord if (existing) { ticketRecord = await prisma.ticket.update({ where: { id: existing.id }, data, }) updated += 1 } else { ticketRecord = await prisma.ticket.create({ data: { tenantId, reference: ticket.reference, ...data, }, }) created += 1 } await prisma.ticketComment.deleteMany({ where: { ticketId: ticketRecord.id } }) await prisma.ticketEvent.deleteMany({ where: { ticketId: ticketRecord.id } }) const commentsData = ticket.comments .map((comment) => { const authorId = comment.authorEmail ? userMap.get(normalizeEmail(comment.authorEmail)) : null if (!authorId) { return null } return { ticketId: ticketRecord.id, authorId, visibility: (comment.visibility ?? "INTERNAL").toUpperCase(), body: comment.body ?? "", attachments: null, createdAt: toDate(comment.createdAt) ?? new Date(), updatedAt: toDate(comment.updatedAt) ?? new Date(), } }) .filter(Boolean) if (commentsData.length > 0) { await prisma.ticketComment.createMany({ data: commentsData }) } const eventsData = ticket.events.map((event) => ({ ticketId: ticketRecord.id, type: event.type ?? "UNKNOWN", payload: event.payload ?? {}, createdAt: toDate(event.createdAt) ?? new Date(), })) if (eventsData.length > 0) { await prisma.ticketEvent.createMany({ data: eventsData }) } } return { created, updated } } async function run() { console.log("Baixando snapshot do Convex...") const snapshot = await client.query("migrations:exportTenantSnapshot", { secret, tenantId, }) console.log(`Empresas recebidas: ${snapshot.companies.length}`) console.log(`Usuários recebidos: ${snapshot.users.length}`) console.log(`Filas recebidas: ${snapshot.queues.length}`) console.log(`Tickets recebidos: ${snapshot.tickets.length}`) console.log("Sincronizando empresas no Prisma...") const companyMap = await upsertCompanies(snapshot.companies) console.log(`Empresas ativas no mapa: ${companyMap.size}`) console.log("Sincronizando usuários no Prisma...") const userMap = await upsertUsers(snapshot.users, companyMap) console.log(`Usuários ativos no mapa: ${userMap.size}`) console.log("Sincronizando filas no Prisma...") const queueMap = await upsertQueues(snapshot.queues) console.log(`Filas ativas no mapa: ${queueMap.size}`) console.log("Sincronizando tickets no Prisma...") const ticketStats = await upsertTickets(snapshot.tickets, userMap, queueMap, companyMap) console.log(`Tickets criados: ${ticketStats.created}`) console.log(`Tickets atualizados: ${ticketStats.updated}`) } run() .catch((error) => { console.error("Falha ao importar dados do Convex para Prisma", error) process.exitCode = 1 }) .finally(async () => { await prisma.$disconnect() })