539b922ce3
- Armazena payload WAMessage (serializado com _b64 para Uint8Array) em messages.mediaPayload no momento da recepção da mensagem - Nova rota POST /instances/:id/redownload-media/:msgId que usa sock.updateMediaMessage para renovar URL expirada + re-upload Wasabi - Frontend: placeholders clicáveis (spinner + texto) para imagem, vídeo, áudio, documento e figurinha sem media_url; atualização automática via socket message:media_ready existente Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
561 lines
18 KiB
Plaintext
561 lines
18 KiB
Plaintext
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Prisma Schema — NewWhats SaaS Omnichannel
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
binaryTargets = ["native", "debian-openssl-3.0.x", "debian-openssl-1.1.x"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ─── PLANOS ──────────────────────────────────────────────────────────────────
|
|
|
|
model Plan {
|
|
id String @id @default(uuid())
|
|
name String
|
|
maxInstances Int @default(1)
|
|
maxAgents Int @default(5)
|
|
priceMonthly Decimal @default(0) @db.Decimal(10, 2)
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
users User[]
|
|
}
|
|
|
|
// ─── USUÁRIOS (ADMIN + TENANTS) ───────────────────────────────────────────────
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
name String
|
|
email String @unique
|
|
passwordHash String
|
|
role UserRole @default(USER)
|
|
isActive Boolean @default(true)
|
|
planId String?
|
|
trialEndsAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
plan Plan? @relation(fields: [planId], references: [id])
|
|
instances Instance[]
|
|
sectors Sector[]
|
|
teams Team[]
|
|
apiKeys ApiKey[]
|
|
aiCredentials AICredential[]
|
|
aiBots AIBot[]
|
|
broadcasts Broadcast[]
|
|
pluginPairs PluginPair[]
|
|
extWebhooks ExtWebhook[]
|
|
messageTemplates MessageTemplate[]
|
|
scheduledMessages ScheduledMessage[]
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
|
|
enum UserRole {
|
|
ADMIN
|
|
USER
|
|
}
|
|
|
|
// ─── API KEYS ─────────────────────────────────────────────────────────────────
|
|
|
|
model ApiKey {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
key String @unique @default(uuid())
|
|
name String
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
expiresAt DateTime?
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("api_keys")
|
|
}
|
|
|
|
// ─── INSTÂNCIAS WHATSAPP ──────────────────────────────────────────────────────
|
|
|
|
model Instance {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
name String
|
|
phone String?
|
|
avatar String?
|
|
status InstanceStatus @default(DISCONNECTED)
|
|
engine String @default("infinite")
|
|
sessionPath String?
|
|
webhookUrl String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
contacts Contact[]
|
|
chats Chat[]
|
|
messages Message[]
|
|
broadcasts Broadcast[]
|
|
|
|
@@unique([tenantId, name])
|
|
@@map("instances")
|
|
}
|
|
|
|
enum InstanceStatus {
|
|
DISCONNECTED
|
|
CONNECTING
|
|
QR_PENDING
|
|
CONNECTED
|
|
BANNED
|
|
}
|
|
|
|
// ─── SETORES ─────────────────────────────────────────────────────────────────
|
|
|
|
model Sector {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
name String
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
teams Team[]
|
|
protocols Protocol[]
|
|
|
|
@@unique([tenantId, name])
|
|
@@map("sectors")
|
|
}
|
|
|
|
// ─── EQUIPES ─────────────────────────────────────────────────────────────────
|
|
|
|
model Team {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
sectorId String
|
|
name String
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
sector Sector @relation(fields: [sectorId], references: [id], onDelete: Cascade)
|
|
protocols Protocol[]
|
|
|
|
@@unique([tenantId, sectorId, name])
|
|
@@map("teams")
|
|
}
|
|
|
|
// ─── CONTATOS ─────────────────────────────────────────────────────────────────
|
|
|
|
model Contact {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
instanceId String
|
|
jid String
|
|
lidJid String?
|
|
// Prioridade: name (agenda) > verifiedName > notify (pushname)
|
|
name String?
|
|
verifiedName String?
|
|
notify String?
|
|
phone String?
|
|
avatarUrl String?
|
|
scoreReputacao Int @default(100)
|
|
flagRestricao Boolean @default(false)
|
|
isBlocked Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
|
chats Chat[]
|
|
protocols Protocol[]
|
|
|
|
@@unique([tenantId, instanceId, jid])
|
|
@@index([tenantId, instanceId, lidJid])
|
|
@@map("contacts")
|
|
}
|
|
|
|
// ─── CHATS ────────────────────────────────────────────────────────────────────
|
|
|
|
model Chat {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
instanceId String
|
|
contactId String?
|
|
jid String
|
|
name String? // Subject para grupos (@g.us); null para individuais
|
|
unreadCount Int @default(0)
|
|
lastMessageAt DateTime?
|
|
isPinned Boolean @default(false)
|
|
isArchived Boolean @default(false)
|
|
// IA Chatbot
|
|
botPaused Boolean @default(false) // human takeover — pausa o bot neste chat
|
|
botSummary String? // Cérebro: resumo de 2 frases da conversa
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
|
contact Contact? @relation(fields: [contactId], references: [id])
|
|
messages Message[]
|
|
protocols Protocol[]
|
|
|
|
@@unique([tenantId, instanceId, jid])
|
|
// Índice composto para a query principal da inbox:
|
|
// WHERE tenantId + instanceId + isArchived (filtro)
|
|
// ORDER BY isPinned DESC, lastMessageAt DESC (ordenação)
|
|
@@index([tenantId, instanceId, isArchived, isPinned(sort: Desc), lastMessageAt(sort: Desc)])
|
|
@@map("chats")
|
|
}
|
|
|
|
// ─── MENSAGENS ────────────────────────────────────────────────────────────────
|
|
|
|
model Message {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
instanceId String
|
|
chatId String
|
|
remoteJid String
|
|
messageId String
|
|
fromMe Boolean @default(false)
|
|
type MessageType @default(TEXT)
|
|
body String?
|
|
caption String?
|
|
mediaUrl String?
|
|
mediaPath String?
|
|
mimeType String?
|
|
fileName String?
|
|
duration Int?
|
|
pushName String? // Nome do remetente no momento do envio (grupos: quem enviou)
|
|
senderJid String? // JID do remetente real (grupos: key.participant)
|
|
replyToId String? // ID da mensagem citada (reply)
|
|
richPayload Json? // Payload rico original (buttons/list/carousel/poll)
|
|
mediaPayload Json? // Inner WAMessage payload para re-download de mídia
|
|
status MessageStatus @default(PENDING)
|
|
timestamp DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
|
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
|
replyTo Message? @relation("MessageReply", fields: [replyToId], references: [id])
|
|
replies Message[] @relation("MessageReply")
|
|
|
|
@@unique([instanceId, messageId])
|
|
// Índice para "última mensagem por chat" — usado no include da inbox
|
|
// ORDER BY timestamp DESC LIMIT 1 (por chatId)
|
|
@@index([chatId, timestamp(sort: Desc)])
|
|
// Índice para reconcile-names: DISTINCT ON remoteJid ORDER BY timestamp DESC
|
|
// WHERE instanceId = ? AND fromMe = false AND pushName IS NOT NULL
|
|
@@index([instanceId, fromMe, remoteJid, timestamp(sort: Desc)])
|
|
// Índice para obter a última mensagem da instância — usado na verificação de gap de sincronização
|
|
@@index([instanceId, timestamp(sort: Desc)])
|
|
@@map("messages")
|
|
}
|
|
|
|
enum MessageType {
|
|
TEXT
|
|
IMAGE
|
|
VIDEO
|
|
AUDIO
|
|
DOCUMENT
|
|
STICKER
|
|
LOCATION
|
|
CONTACT
|
|
REACTION
|
|
POLL
|
|
BUTTONS
|
|
LIST
|
|
CAROUSEL
|
|
INTERACTIVE
|
|
UNSUPPORTED
|
|
}
|
|
|
|
enum MessageStatus {
|
|
PENDING
|
|
SENT
|
|
DELIVERED
|
|
READ
|
|
FAILED
|
|
}
|
|
|
|
// ─── PROTOCOLOS DE ATENDIMENTO ────────────────────────────────────────────────
|
|
|
|
model Protocol {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
number String @unique // Número sequencial rastreável: #0001-2024
|
|
chatId String
|
|
contactId String
|
|
sectorId String?
|
|
teamId String?
|
|
agentId String?
|
|
status ProtocolStatus @default(OPEN)
|
|
summary String? // Resumo de 2 frases gerado pelo Temporal
|
|
deadline DateTime?
|
|
resolvedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
chat Chat @relation(fields: [chatId], references: [id])
|
|
contact Contact @relation(fields: [contactId], references: [id])
|
|
sector Sector? @relation(fields: [sectorId], references: [id])
|
|
team Team? @relation(fields: [teamId], references: [id])
|
|
|
|
// Índice para filtro de protocolos abertos por chat (usado no include da inbox)
|
|
// WHERE chatId = ? AND status IN ('OPEN', 'IN_PROGRESS', ...) ORDER BY createdAt DESC
|
|
@@index([chatId, status, createdAt(sort: Desc)])
|
|
@@map("protocols")
|
|
}
|
|
|
|
enum ProtocolStatus {
|
|
OPEN
|
|
IN_PROGRESS
|
|
WAITING_CLIENT
|
|
WAITING_AGENT
|
|
RESOLVED
|
|
CANCELLED
|
|
}
|
|
|
|
// ─── IA MULTI-AGENTE ──────────────────────────────────────────────────────────
|
|
|
|
model AICredential {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
instanceId String
|
|
name String
|
|
provider LLMProvider @default(GEMINI)
|
|
apiKey String
|
|
createdAt DateTime @default(now())
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
bots AIBot[]
|
|
|
|
@@unique([tenantId, instanceId, name])
|
|
@@map("ai_credentials")
|
|
}
|
|
|
|
model AIBot {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
instanceId String
|
|
credentialId String
|
|
name String
|
|
systemPrompt String @db.Text
|
|
model String @default("gemini-1.5-flash")
|
|
enabled Boolean @default(false)
|
|
triggerMode TriggerMode @default(ALL)
|
|
keywords String[]
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
credential AICredential @relation(fields: [credentialId], references: [id])
|
|
|
|
@@unique([tenantId, instanceId])
|
|
@@map("ai_bots")
|
|
}
|
|
|
|
enum LLMProvider {
|
|
GEMINI
|
|
OPENAI
|
|
}
|
|
|
|
enum TriggerMode {
|
|
ALL
|
|
KEYWORD
|
|
}
|
|
|
|
// ─── BROADCAST (DISPARO EM MASSA) ─────────────────────────────────────────────
|
|
|
|
model Broadcast {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
instanceId String
|
|
message String @db.Text
|
|
totalCount Int @default(0)
|
|
sentCount Int @default(0)
|
|
failedCount Int @default(0)
|
|
status String @default("RUNNING") // RUNNING | DONE | FAILED
|
|
createdAt DateTime @default(now())
|
|
finishedAt DateTime?
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
|
|
recipients BroadcastRecipient[]
|
|
|
|
@@map("broadcasts")
|
|
}
|
|
|
|
// ─── PLUGIN PAIRS ─────────────────────────────────────────────────────────────
|
|
// Registra sistemas externos pareados com uma conta NewWhats.
|
|
// Cada par tem uma integration_key exclusiva que autentica chamadas /nw/*.
|
|
|
|
model PluginPair {
|
|
id String @id @default(uuid())
|
|
userId String
|
|
clientSystem String // identificador técnico ex: "alemao-conveniencias"
|
|
clientName String // nome amigável ex: "Alemão Conveniências"
|
|
clientUrl String? // URL base do sistema cliente
|
|
integrationKey String @unique
|
|
lastSeenAt DateTime?
|
|
revokedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("plugin_pairs")
|
|
}
|
|
|
|
// ─── EXT WEBHOOKS ─────────────────────────────────────────────────────────────
|
|
// Endpoints externos registrados por tenant para receber push de eventos.
|
|
|
|
model ExtWebhook {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
url String
|
|
events String[]
|
|
secret String
|
|
active Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("ext_webhooks")
|
|
}
|
|
|
|
model BroadcastRecipient {
|
|
id String @id @default(uuid())
|
|
broadcastId String
|
|
jid String
|
|
status String @default("PENDING") // PENDING | SENT | FAILED
|
|
createdAt DateTime @default(now())
|
|
|
|
broadcast Broadcast @relation(fields: [broadcastId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("broadcast_recipients")
|
|
}
|
|
|
|
// ─── TEMPLATES DE MENSAGEM ────────────────────────────────────────────────────
|
|
|
|
model MessageTemplate {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
name String
|
|
description String?
|
|
type TemplateType @default(TEXT)
|
|
payload Json
|
|
tags String[]
|
|
usageCount Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
schedules ScheduledMessage[]
|
|
|
|
@@map("message_templates")
|
|
}
|
|
|
|
enum TemplateType {
|
|
TEXT
|
|
BUTTONS
|
|
INTERACTIVE
|
|
LIST
|
|
POLL
|
|
CAROUSEL
|
|
}
|
|
|
|
// ─── AGENDAMENTOS DE MENSAGEM ─────────────────────────────────────────────────
|
|
|
|
model ScheduledMessage {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
instanceId String
|
|
name String
|
|
templateId String?
|
|
payload Json
|
|
recipientMode RecipientMode @default(MANUAL)
|
|
recipients Json
|
|
scheduleType ScheduleType @default(ONCE)
|
|
cronExpr String?
|
|
scheduledAt DateTime?
|
|
eventType EventType?
|
|
timezone String @default("America/Sao_Paulo")
|
|
status ScheduleStatus @default(ACTIVE)
|
|
lastRunAt DateTime?
|
|
nextRunAt DateTime?
|
|
runCount Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenant User @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
template MessageTemplate? @relation(fields: [templateId], references: [id])
|
|
logs ScheduledMessageLog[]
|
|
|
|
@@map("scheduled_messages")
|
|
}
|
|
|
|
model ScheduledMessageLog {
|
|
id String @id @default(uuid())
|
|
scheduleId String
|
|
status String
|
|
sentCount Int @default(0)
|
|
failedCount Int @default(0)
|
|
error String?
|
|
runAt DateTime @default(now())
|
|
|
|
schedule ScheduledMessage @relation(fields: [scheduleId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("scheduled_message_logs")
|
|
}
|
|
|
|
enum RecipientMode {
|
|
MANUAL
|
|
ALL_CONTACTS
|
|
TAG_FILTER
|
|
}
|
|
|
|
enum ScheduleType {
|
|
ONCE
|
|
RECURRING
|
|
EVENT
|
|
}
|
|
|
|
enum EventType {
|
|
BIRTHDAY
|
|
ANNIVERSARY
|
|
SIGNUP_DAYS
|
|
}
|
|
|
|
enum ScheduleStatus {
|
|
ACTIVE
|
|
PAUSED
|
|
DONE
|
|
CANCELLED
|
|
}
|
|
// ─── FIGURINHAS (STICKERS) ────────────────────────────────────────────────────
|
|
// Armazenamento persistente de stickers usando fileSha256 do protocolo Baileys
|
|
// como chave de conteúdo. Garante deduplicação mesmo após reinstalação do WA.
|
|
|
|
model Sticker {
|
|
id String @id @default(uuid())
|
|
tenantId String
|
|
instanceId String
|
|
sha256 String // fileSha256 hex — identidade permanente do conteúdo
|
|
wasabiPath String // stickers/{tenantId}/{sha256}.webp
|
|
isAnimated Boolean @default(false)
|
|
isFavorite Boolean @default(false)
|
|
useCount Int @default(1)
|
|
lastSeenAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([tenantId, sha256])
|
|
@@index([tenantId, instanceId, isFavorite, lastSeenAt(sort: Desc)])
|
|
@@map("stickers")
|
|
}
|
|
|
|
// Webhook allowed host fix
|
|
// Webhook allowed host fix
|
|
// Webhook query string fix
|