Files
clube67_newwhats.local/clube67/newwhats.local/backend/prisma/schema.prisma
T
VPS 4 Deploy Agent 539b922ce3 feat(media): re-download de mídia não baixada ao clicar
- 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>
2026-05-11 18:58:01 +02:00

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