chore(ops): restore all source files in newwhats.clube67.com
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
This commit is contained in:
@@ -0,0 +1,560 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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
|
||||
Reference in New Issue
Block a user