feat: seed real agents and enable comment templates

This commit is contained in:
esdrasrenan 2025-10-06 20:35:40 -03:00
parent df8c4e29bb
commit 409cbea7b9
13 changed files with 1722 additions and 29 deletions

View file

@ -0,0 +1,292 @@
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: "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", "CUSTOMER"])
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)
}
async function upsertUsers(snapshotUsers) {
const map = new Map()
for (const user of snapshotUsers) {
const normalizedEmail = normalizeEmail(user.email)
if (!normalizedEmail) continue
const normalizedRole = (user.role ?? "CUSTOMER").toUpperCase()
const role = allowedRoles.has(normalizedRole) ? normalizedRole : "CUSTOMER"
const record = await prisma.user.upsert({
where: { email: normalizedEmail },
update: {
name: user.name ?? normalizedEmail,
role,
tenantId,
avatarUrl: user.avatarUrl ?? null,
},
create: {
email: normalizedEmail,
name: user.name ?? normalizedEmail,
role,
tenantId,
avatarUrl: user.avatarUrl ?? null,
},
})
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,
},
create: {
email: normalizedEmail,
name: staff.name,
role: staff.role,
tenantId,
avatarUrl: 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", "MANAGER", "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: "CUSTOMER",
},
})
}
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) {
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
const desiredAssigneeEmail = defaultAssigneeEmail || normalizeEmail(ticket.assigneeEmail)
const assigneeId = desiredAssigneeEmail ? userMap.get(desiredAssigneeEmail) || fallbackAssigneeId || null : fallbackAssigneeId || null
const existing = await prisma.ticket.findFirst({
where: {
tenantId,
reference: ticket.reference,
},
})
const data = {
subject: ticket.subject,
summary: ticket.summary ?? null,
status: (ticket.status ?? "NEW").toUpperCase(),
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(),
}
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(`Usuários recebidos: ${snapshot.users.length}`)
console.log(`Filas recebidas: ${snapshot.queues.length}`)
console.log(`Tickets recebidos: ${snapshot.tickets.length}`)
console.log("Sincronizando usuários no Prisma...")
const userMap = await upsertUsers(snapshot.users)
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)
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()
})

View file

@ -24,13 +24,6 @@ const defaultUsers = singleUserFromEnv ?? [
role: "admin",
tenantId,
},
{
email: "agente.demo@sistema.dev",
password: "agent123",
name: "Agente Demo",
role: "agent",
tenantId,
},
{
email: "cliente.demo@sistema.dev",
password: "cliente123",
@ -38,6 +31,62 @@ const defaultUsers = singleUserFromEnv ?? [
role: "customer",
tenantId,
},
{
email: "gabriel.oliveira@rever.com.br",
password: "agent123",
name: "Gabriel Oliveira",
role: "agent",
tenantId,
},
{
email: "george.araujo@rever.com.br",
password: "agent123",
name: "George Araujo",
role: "agent",
tenantId,
},
{
email: "hugo.soares@rever.com.br",
password: "agent123",
name: "Hugo Soares",
role: "agent",
tenantId,
},
{
email: "julio@rever.com.br",
password: "agent123",
name: "Julio Cesar",
role: "agent",
tenantId,
},
{
email: "lorena@rever.com.br",
password: "agent123",
name: "Lorena Magalhães",
role: "agent",
tenantId,
},
{
email: "renan.pac@paulicon.com.br",
password: "agent123",
name: "Rever",
role: "agent",
tenantId,
},
{
email: "thiago.medeiros@rever.com.br",
password: "agent123",
name: "Thiago Medeiros",
role: "agent",
tenantId,
},
{
email: "weslei@rever.com.br",
password: "agent123",
name: "Weslei Magalhães",
role: "agent",
tenantId,
},
]
async function upsertAuthUser({ email, password, name, role, tenantId: userTenant }: (typeof defaultUsers)[number]) {

View file

@ -0,0 +1,136 @@
import { PrismaClient } from "@prisma/client"
import { ConvexHttpClient } from "convex/browser"
const prisma = new PrismaClient()
function toMillis(date) {
return date instanceof Date ? date.getTime() : date ? new Date(date).getTime() : undefined
}
function normalizeString(value, fallback = "") {
if (!value) return fallback
return value.trim()
}
function slugify(value) {
return normalizeString(value)
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-") || undefined
}
async function main() {
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
if (!secret) {
console.error("CONVEX_SYNC_SECRET não configurado. Configure no .env.")
process.exit(1)
}
const [users, queues, tickets] = await Promise.all([
prisma.user.findMany({
include: {
teams: {
include: { team: true },
},
},
}),
prisma.queue.findMany(),
prisma.ticket.findMany({
include: {
requester: true,
assignee: true,
queue: true,
comments: {
include: {
author: true,
},
},
events: true,
},
orderBy: { createdAt: "asc" },
}),
])
const userSnapshot = users.map((user) => ({
email: user.email,
name: normalizeString(user.name, user.email),
role: user.role,
avatarUrl: user.avatarUrl ?? undefined,
teams: user.teams
.map((membership) => membership.team?.name)
.filter((name) => Boolean(name) && typeof name === "string"),
}))
const queueSnapshot = queues.map((queue) => ({
name: normalizeString(queue.name, queue.slug ?? queue.id),
slug: queue.slug ? queue.slug : normalizeString(queue.name, queue.id).toLowerCase().replace(/\s+/g, "-"),
}))
const referenceFallbackStart = 41000
let referenceCounter = referenceFallbackStart
const ticketSnapshot = tickets.map((ticket) => {
const reference = ticket.reference && ticket.reference > 0 ? ticket.reference : ++referenceCounter
const requesterEmail = ticket.requester?.email ?? userSnapshot[0]?.email ?? "unknown@example.com"
const assigneeEmail = ticket.assignee?.email ?? undefined
const queueSlug = ticket.queue?.slug ?? slugify(ticket.queue?.name)
return {
reference,
subject: normalizeString(ticket.subject, `Ticket ${reference}`),
summary: ticket.summary ?? undefined,
status: ticket.status,
priority: ticket.priority,
channel: ticket.channel,
queueSlug: queueSlug ?? undefined,
requesterEmail,
assigneeEmail,
dueAt: toMillis(ticket.dueAt) ?? undefined,
firstResponseAt: toMillis(ticket.firstResponseAt) ?? undefined,
resolvedAt: toMillis(ticket.resolvedAt) ?? undefined,
closedAt: toMillis(ticket.closedAt) ?? undefined,
createdAt: toMillis(ticket.createdAt) ?? Date.now(),
updatedAt: toMillis(ticket.updatedAt) ?? Date.now(),
tags: Array.isArray(ticket.tags) ? ticket.tags : undefined,
comments: ticket.comments.map((comment) => ({
authorEmail: comment.author?.email ?? requesterEmail,
visibility: comment.visibility,
body: comment.body,
createdAt: toMillis(comment.createdAt) ?? Date.now(),
updatedAt: toMillis(comment.updatedAt) ?? Date.now(),
})),
events: ticket.events.map((event) => ({
type: event.type,
payload: event.payload ?? {},
createdAt: toMillis(event.createdAt) ?? Date.now(),
})),
}
})
const client = new ConvexHttpClient(convexUrl)
const result = await client.mutation("migrations:importPrismaSnapshot", {
secret,
snapshot: {
tenantId,
users: userSnapshot,
queues: queueSnapshot,
tickets: ticketSnapshot,
},
})
console.log("Sincronização concluída:", result)
}
main()
.catch((error) => {
console.error(error)
process.exitCode = 1
})
.finally(async () => {
await prisma.$disconnect()
})