425 lines
13 KiB
JavaScript
425 lines
13 KiB
JavaScript
import "dotenv/config"
|
|
import { PrismaClient } from "@prisma/client"
|
|
import { ConvexHttpClient } from "convex/browser"
|
|
|
|
const prisma = new PrismaClient()
|
|
|
|
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()
|
|
})
|