// ───────────────────────────────────────────────────────────────────────────── // 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) 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 } // Webhook allowed host fix // Webhook allowed host fix // Webhook query string fix