Appearance
Schema do Banco de Dados
Documentação de todos os modelos Prisma do sistema. Fonte: backend/prisma/schema.prisma.
Diagrama de domínios
Organization (raiz de tudo)
├── Store[] (lojas)
│ ├── StoreWhatsappNumber[] (números WhatsApp por loja)
│ │ └── WhatsappTemplate[]
│ ├── EmailSettings
│ ├── Seller[]
│ └── Customer[] (clientes da loja)
│
├── Customer[] (clientes da org)
│ ├── Transaction[] (compras)
│ ├── CustomerEvent[]
│ ├── RfmHistory[]
│ ├── CampaignRedirectToken[]
│ └── PostSaleSurveyJob[]
│
├── Campaign[]
│ ├── CampaignContent[]
│ ├── CampaignMetric
│ ├── CampaignSchedule[]
│ ├── WhatsappMessage[]
│ ├── EmailMessage[]
│ └── CampaignRedirectToken[]
│
├── Segment[]
├── Role[]
├── User[]
├── AccessLog[]
├── Conversation[]
│ └── Message[]
├── ChatwootConfig
├── EmailTemplate[]
├── OrganizationSmtpSettings
├── PostSaleSurveyConfig[]
└── SurveyResponse[]Organization
Raiz de toda a hierarquia multi-tenant. Cada organização é um cliente do CRM.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
name | String | Nome da organização |
clerkOrgId | String? | ID no Clerk (único) |
syncStatus | String | PENDING | SYNCING | COMPLETED | ERROR |
lastSyncDate | DateTime? | Última vez que o ERP sync completou |
lastCustomerTransId | BigInt | Maior trans_id já sincronizado (sync incremental) |
campaignSettings | Json? | { replyWindowHours: 48 } |
logoUrl | String? | URL do logo |
Store
Representa uma filial física ou e-commerce.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
code | String? | Código da filial no ERP (único) |
name | String | Nome da loja |
tradeName | String? | Nome fantasia |
cnpj, ie | String? | Documentos fiscais |
email, phone | String? | Contato da filial |
cep, street, number, neighborhood, city, state | String? | Endereço |
isEcommerce | Boolean | Marca a filial de e-commerce (fallback de redirecionamento) |
primaryStoreId | String? | FK para loja principal (alias — ver Regras de Lojas) |
businessHours | Json? | Horário de funcionamento por dia da semana |
settings | Json? | Configurações extras |
isActive | Boolean | Se está ativa |
agentCredentialEmail | String? | Email para receber credenciais Chatwoot |
Índices: organizationId
BusinessHoursWaiting
Clientes que enviaram mensagem fora do horário comercial.
| Campo | Tipo | Descrição |
|---|---|---|
id | CUID | PK |
phone | String | Telefone do cliente |
storeId, organizationId | String | Tenant |
extraNote | String? | Nota extra para o atendente |
Unique: [phone, storeId]
User
Usuários do CRM (funcionários, admins). Autenticados via Clerk.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
clerkId | String? | ID no Clerk |
clerkInvitationId | String? | ID do convite pendente |
email | String | Único |
name | String? | Nome |
status | String | ACTIVE | INVITED |
roleId | String? | FK → Role |
organizationId | String | FK → Organization |
accessibleStores | Store[] | Lojas visíveis (vazio = todas) |
Role
Papel de usuário com permissões e nível hierárquico.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
name | String | Ex: "Super Admin", "Vendedor" |
level | Int | 0 = Super Admin, maior = menos poder |
permissions | String[] | Ex: ["app:campaigns", "app:customers"] |
readOnly | Boolean | Se true, só GET |
organizationId | String | FK → Organization |
AccessLog
Registro de auditoria de ações sensíveis.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
userId, organizationId | String | Quem fez |
storeId | String? | Em qual loja |
endpoint, method | String | HTTP |
actionType | String | Ex: CAMPAIGN_SENT, USER_INVITED |
ip, userAgent | String? | Origem (IP mascarado) |
metadata | Json? | IDs relevantes da ação |
status | Int? | HTTP status |
Índices: userId, actionType, createdAt, organizationId
Customer
Cliente sincronizado do ERP Millennium.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
externalId | String? | ID no ERP |
transId | BigInt? | trans_id do Millennium (sync incremental) |
name, email, phone | String? | PII |
phoneType | PhoneType | MOBILE | LANDLINE | INVALID | UNKNOWN |
cpfHash | String? | SHA-256 do CPF (busca/dedup) |
cpfEncrypted | String? | AES-256-GCM (uso interno) |
cpfMasked | String? | ***.***.XXX-** (display) |
cpfValid | Boolean? | Validez do CPF |
birthDate | DateTime? | Data de nascimento |
zipCode, city, state, address, neighborhood | String? | Endereço |
personType | String | PF | PJ |
rfmStatus | String? | Segmento atual (ex: champions) |
previousRfmStatus | String? | Segmento anterior |
tags | String[] | Labels livres |
totalSpent | Float | Total gasto |
orderCount | Int | Número de pedidos |
lastOrder | DateTime? | Data da última compra |
storeId | String | Loja principal |
preferredStoreId | String? | Loja preferida para roteamento WhatsApp |
preferredSellerId | String? | Vendedor preferido |
sellerAssignmentCycle | Json? | Estado do round-robin por loja |
whatsappOptOut | Boolean | Se descadastrou de WhatsApp |
emailOptOut | Boolean | Se descadastrou de email |
smsOptOut | Boolean | Se descadastrou de SMS |
whatsappUnsubscribeToken | String? | Token permanente para link de descadastro |
dataQualityIssues | Json? | Problemas de qualidade identificados |
Uniques: [storeId, externalId], cpfHash
Índices: organizationId, [organizationId, lastOrder], email, phone, tags
CustomerEvent
Eventos de comportamento do cliente (ex: abertura de email, clique em link).
| Campo | Tipo | Descrição |
|---|---|---|
customerId | String | FK → Customer |
type | String | Tipo do evento |
date | DateTime | Quando ocorreu |
metadata | Json? | Dados extras |
RfmHistory
Histórico de segmentação RFM do cliente.
| Campo | Tipo | Descrição |
|---|---|---|
customerId | String | FK → Customer |
score | Int | Score calculado |
segment | String | Segmento (ex: champions) |
date | DateTime | Data do cálculo |
Product
Produtos sincronizados do ERP.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
externalId | String | ID numérico do ERP (único) |
productCode | String? | Código zero-padded (join key com Transaction.items) |
name | String | descricao1 do ERP |
shortName | String? | descricao2 |
collection, department, group, brand, category, type | String? | Taxonomia |
colors, sizes, patterns | String[] | Variações |
price | Float? | Preço |
status | String | ACTIVE | INACTIVE |
Índices: productCode, collection, department, group, division, brand, category, type
Seller
Vendedores sincronizados do ERP.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
externalId | String | ID no ERP (único) |
codFuncionario | String? | Código formatado (ex: 000246) |
name, email, phone | String? | Dados do funcionário |
cpfHash, cpfEncrypted, cpfMasked | String? | CPF protegido |
storeId | String? | Loja atual |
branchCode | String? | cod_filial do ERP |
Transaction
Compras sincronizadas do ERP.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
externalId | String? | ID no ERP (único) |
totalValue | Float | Valor total |
date | DateTime | Data da compra |
status | String | PAID | CANCELLED |
channel | String? | Canal de venda |
isInfluenced | Boolean | Se foi atribuída a uma campanha |
campaignId | String? | FK → Campaign (atribuição) |
salespersonId | String? | FK → Seller |
vendorCode, vendorName | String? | Vendedor raw do ERP |
items | Json? | Array de produtos (id, qty, price) |
itemCount | Int? | Total de peças |
Índices: date, customerId, externalId, vendorCode, campaignId, [status, date], [customerId, status, date], [storeId, date]
Segment
Segmento de clientes com regras dinâmicas.
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
name, description | String | |
rules, conditions | Json? | Filtros (ver Módulo CRM) |
logic | String | custom | rfm |
isDynamic | Boolean | Se recalcula automaticamente |
lastCount | Int | Última contagem |
storeId | String? | Filtrado por loja |
Campaign
Campanha de comunicação (WhatsApp ou Email).
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
name | String | Nome da campanha |
type | String | email | whatsapp |
channel | String | Email | WHATSAPP |
status | String | draft | scheduled | sending | sent | cancelled |
scheduledAt | DateTime? | Agendamento |
sentAt | DateTime? | Quando foi enviada |
sent, delivered, opens, clicks | Int | Métricas básicas |
softBounces, hardBounces, spamReports, unsubscribes | Int | |
whatsappTemplateId | String? | FK → WhatsappTemplate |
whatsappNumberId | String? | FK → StoreWhatsappNumber |
attributionWindowDays | Int | Janela de atribuição de receita (padrão: 7 dias) |
behaviorRules | Json? | Filtros de comportamento (include/exclude) |
frequency | String | once | daily | weekly | monthly |
scheduleEndAt | DateTime? | Fim da recorrência |
scheduleDayOfWeek | Int? | 0–6 (semanal) |
scheduleDayOfMonth | Int? | 1–31 (mensal) |
Índices: organizationId, storeId, [status, scheduledAt]
CampaignContent
Conteúdo de uma campanha (corpo do email ou mensagem).
| Campo | Tipo | Descrição |
|---|---|---|
subject | String? | Assunto do email |
body | String | Corpo (HTML para email, texto para WhatsApp) |
mediaUrl | String? | URL de mídia (WhatsApp) |
designJson | Json? | Estado do editor visual |
CampaignMetric
Métricas agregadas da campanha (1:1 com Campaign).
| Campo | Tipo | Descrição |
|---|---|---|
sent, delivered, opens, clicks | Int | Básicas |
softBounces, hardBounces, spamReports, unsubscribes | Int | |
conversions, revenue | Int/Float | Atribuição |
reads, replies, failed, skipped | Int | WhatsApp-específicas |
cost | Float | Custo estimado (soma das tarifas Meta) |
StoreWhatsappNumber
Número WhatsApp de uma loja (Evolution ou Meta Cloud API).
| Campo | Tipo | Descrição |
|---|---|---|
id | UUID | PK |
storeId | String? | FK → Store (null para WABA do CRM) |
provider | String | meta_cloud ou evolution |
number | String | Número com DDD |
wabaId | String? | WhatsApp Business Account ID (Meta) |
phoneNumberId | String? | Phone Number ID (Meta Dashboard) |
accessToken | String? | Bearer token (criptografado AES-256-GCM) |
appSecret | String? | Meta App Secret (criptografado) |
webhookVerifyToken | String? | hub.verify_token (armazenado como SHA-256) |
messagingLimitTier | String? | TIER_50 | TIER_250 | TIER_1K | TIER_10K | TIER_100K | UNLIMITED |
qualityScore | String? | GREEN | YELLOW | RED |
evolutionInstanceName | String? | Nome da instância no Evolution API |
evolutionApiKey | String? | API key da instância (criptografada) |
chatwootInboxId | String? | ID do inbox no Chatwoot |
isDefault | Boolean | Se é o número padrão da org |
WhatsappTemplate
Template de mensagem WhatsApp aprovado pela Meta.
| Campo | Tipo | Descrição |
|---|---|---|
metaTemplateId | String? | ID retornado pela Meta |
name | String | snake_case, único dentro do WABA |
language | String | pt_BR |
category | String | MARKETING | UTILITY | AUTHENTICATION |
status | String | PENDING | APPROVED | REJECTED | DISABLED | PAUSED | DRAFT |
rejectionReason | String? | Preenchido via webhook da Meta |
components | Json | Array de componentes Meta: HEADER, BODY, FOOTER, BUTTONS |
variableMapping | Json? | {"1":"name","2":"phone"} — mapeamento CRM das variáveis |
buttonConfig | Json? | [{smartRedirect, isUnsubscribe}] — flags por botão |
hasDraftChanges | Boolean | true quando há alterações locais não enviadas à Meta |
WhatsappMessage
Mensagem individual enviada ou recebida via WhatsApp.
| Campo | Tipo | Descrição |
|---|---|---|
wamId | String? | ID único retornado pela Meta (único) |
campaignId | String? | FK → Campaign (null se inbox) |
conversationId | String? | FK → Conversation (null se campanha) |
direction | String | OUTBOUND | INBOUND |
recipientPhone | String | Número do destinatário |
status | String | SENT | DELIVERED | READ | FAILED |
errorCode, errorMessage | String? | Se falhou |
sentAt, deliveredAt, readAt | DateTime? | Timestamps dos status |
Índices: wamId, campaignId, conversationId, organizationId, [recipientPhone, sentAt]
WhatsappPricingRate
Tarifas por conversa da Meta (importadas manualmente ou via API).
| Campo | Tipo | Descrição |
|---|---|---|
phoneNumberId | String | FK para o número |
country | String | ISO alpha-2 (ex: BR) |
category | String | MARKETING | UTILITY | AUTHENTICATION | SERVICE |
costPerConversation | Float | Custo em USD |
referenceMonth | String | 2026-04 |
EmailSettings
Configuração SMTP por loja.
| Campo | Tipo | Descrição |
|---|---|---|
storeId | String | FK → Store (único) |
senderName, senderEmail | String | Remetente |
host, port, user | String/Int | SMTP |
pass | String | Senha (criptografada AES-256-GCM) |
secure | Boolean | TLS |
OrganizationSmtpSettings
Configuração SMTP padrão da organização (fallback quando a loja não tem EmailSettings).
Mesmos campos que EmailSettings, mas vinculado à Organization.
Conversation
Conversa no Chatwoot/WhatsApp.
| Campo | Tipo | Descrição |
|---|---|---|
storeId, customerId | String | Quem e onde |
provider | String | whatsapp_cloud | evolution |
status | String | OPEN | CLOSED | PENDING | RESOLVED |
assignedToId | String? | FK → User (vendedor responsável) |
contactPhone | String? | Número do contato |
chatwootConversationId | String? | ID no Chatwoot |
resolvedAt | DateTime? | Quando foi resolvida |
firstResponseSeconds | Int? | Tempo até primeira resposta |
resolutionSeconds | Int? | Tempo total até resolução |
Message
Mensagem individual dentro de uma Conversation.
| Campo | Tipo | Descrição |
|---|---|---|
wamId | String? | ID da Meta (único) |
direction | String | INBOUND | OUTBOUND |
type | String | text | template | image | document | audio |
content | Json | Estrutura da Meta |
status | String | SENT | DELIVERED | READ | FAILED |
ChatwootConfig
Configuração da integração Chatwoot por organização.
| Campo | Tipo | Descrição |
|---|---|---|
instanceUrl | String | URL da instância Chatwoot |
apiKey | String | Chave de API (criptografada) |
accountId | String | ID da conta no Chatwoot |
handoffMessage | String? | Mensagem ao transferir para atendente |
storeGreetingMessage | String? | Saudação da loja |
menuMessage | String? | Menu de seleção de loja |
activeConversationMessage | String? | Mensagem se já há conversa aberta |
connectingMessage | String? | Mensagem enquanto conecta |
whatsappMode | String | evolution | meta_cloud |
waitTimeoutEnabled | Boolean | Se ativa timeout de espera |
waitTimeoutMinutes | Int | Tempo antes de reassign (padrão: 30) |
surveyEnabled | Boolean | Se envia pesquisa ao fechar conversa |
CampaignRedirectToken
Token de redirecionamento gerado para cada cliente em cada campanha.
| Campo | Tipo | Descrição |
|---|---|---|
token | String | UUID aleatório (único) |
customerId, campaignId | String | FKs |
preferredStoreId | String? | Loja preferida para roteamento |
clickedAt | DateTime? | Quando o cliente clicou |
redirectedTo | String? | URL final do redirect |
expiresAt | DateTime | Validade (padrão: 30 dias) |
EmailTemplate
Template de email MJML reutilizável.
| Campo | Tipo | Descrição |
|---|---|---|
name | String | Nome do template |
category | String | Categoria |
mjml | String (Text) | Código MJML |
designJson | Json? | Estado do editor |
thumbnailUrl | String? | Preview |
usedCount | Int | Quantas vezes usado |
EmailMessage
Envio individual de email (tracking por destinatário).
| Campo | Tipo | Descrição |
|---|---|---|
campaignId | String? | FK → Campaign |
customerId, recipientEmail | String | Destinatário |
subject | String | Assunto |
status | String | QUEUED | SENT | BOUNCED | FAILED |
bounceType | String? | HARD | SOFT |
trackingId | String | UUID para pixel de abertura/clique |
openedAt, clickedAt, unsubscribedAt, sentAt | DateTime? | Eventos |
PostSaleSurveyConfig
Configuração da pesquisa de satisfação pós-venda.
| Campo | Tipo | Descrição |
|---|---|---|
type | String | CSAT | NPS |
enabled | Boolean | Se ativa |
delayHours | Int | Horas após a venda para enviar |
expirationDays | Int | Validade do link |
dedupeWindowDays | Int | Mínimo de dias entre pesquisas para o mesmo cliente |
whatsappEnabled, whatsappNumberId, whatsappTemplateId | — | Canal WhatsApp |
emailEnabled, emailSubject | — | Canal Email |
questions | Json? | Configuração das perguntas |
primaryColor, bgColor1, bgColor2 | String? | Visual personalizado |
PostSaleSurveyJob
Job de envio de pesquisa para uma transação específica.
| Campo | Tipo | Descrição |
|---|---|---|
transactionId, customerId | String | FKs |
channel | String | WHATSAPP | EMAIL |
surveyType | String | CSAT | NPS |
status | String | PENDING | SENT | SKIPPED | FAILED |
skipReason | String? | Por que foi pulado |
scheduledAt, sentAt, expiresAt | DateTime? | Ciclo de vida |
surveyTokenHash | String? | SHA-256 do token (dedup) |
Unique: [transactionId, channel, surveyType]
SurveyResponse / PostSaleSurveyResponse
Respostas de pesquisas de satisfação.
| Campo | Tipo | Descrição |
|---|---|---|
conversationId (Survey) / jobId (PostSale) | — | FK origem |
rating | Int | 1–5 (CSAT) ou 0–10 (NPS) |
comment | String? | Texto livre (max 500 chars) |
tokenHash | String | SHA-256 do token — dedup idempotente |
submittedAt | DateTime | Quando respondeu |
Modelos auxiliares de integração Chatwoot/n8n
| Modelo | Descrição |
|---|---|
SyncEvent | Deduplicação de webhooks Chatwoot por chatwootDeliveryId |
SyncFailure | Webhooks que falharam (ORPHAN_WAMID, CRM_UNREACHABLE, etc.) |
WindowExpiry | Conversa Chatwoot encerrada por expiração da janela 24h |
ConversationRedirect | Registro de redirects de campanha |
CampaignNoMenu | Cliente que respondeu sem selecionar loja no menu |
FailedWebhook | Webhooks externos que falharam e precisam de retry |
JobRun
Log de execução de jobs BullMQ.
| Campo | Tipo | Descrição |
|---|---|---|
queue | String | Nome da fila |
jobName, jobId | String | Identificadores |
status | String | completed | failed | active |
durationMs | Int? | Duração em ms |
error | String? | Mensagem de erro |
safePayload | Json? | Payload sem dados sensíveis |