Files
SeConectai_Vendas/Etapas/seconectai-vendas-schema.sql
2026-08-14 17:35:24 -03:00

452 lines
19 KiB
SQL

-- ============================================================
-- SeConectAI Vendas — Schema PostgreSQL (multi-tenant CRM/Vendas)
-- ============================================================
-- Estratégia de multi-tenancy: banco único, isolado por tenant_id
-- + Row Level Security (RLS). Mais simples de operar em Docker/
-- Docker Compose do que "schema por tenant" e escala bem até
-- centenas de tenants num único Postgres.
-- ============================================================
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid()
-- ============================================================
-- 1. NÍVEL MASTER (você — dono da plataforma)
-- ============================================================
CREATE TABLE master_admins (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(150) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
senha_hash TEXT NOT NULL, -- argon2id ou bcrypt cost>=12
ativo BOOLEAN NOT NULL DEFAULT TRUE,
mfa_secret TEXT, -- TOTP opcional, recomendado p/ master
ultimo_login TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Conexão(ões) com a Evolution API 2.3, cadastrada pelo master.
-- Pode haver mais de um servidor Evolution (ex: para balancear carga).
CREATE TABLE evolution_api_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(100) NOT NULL, -- "Servidor Principal"
base_url VARCHAR(255) NOT NULL,
api_key_cripto TEXT NOT NULL, -- criptografar em repouso (app-level, ex: AES-256-GCM)
versao VARCHAR(20) NOT NULL DEFAULT '2.3',
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Config SMTP padrão da plataforma (fallback quando o tenant não
-- tem SMTP próprio cadastrado)
CREATE TABLE master_email_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
smtp_host VARCHAR(150) NOT NULL,
smtp_porta INTEGER NOT NULL,
smtp_usuario VARCHAR(150) NOT NULL,
smtp_senha_cripto TEXT NOT NULL,
email_remetente VARCHAR(150) NOT NULL,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE planos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome VARCHAR(100) NOT NULL,
descricao TEXT,
valor_mensal NUMERIC(10,2) NOT NULL,
limite_customers INTEGER, -- NULL = ilimitado
limite_usuarios INTEGER,
limite_instancias_whatsapp INTEGER NOT NULL DEFAULT 1,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ============================================================
-- 2. TENANTS — seus clientes que compram/usam a plataforma
-- ============================================================
CREATE TYPE tenant_status AS ENUM ('trial', 'ativo', 'inadimplente', 'suspenso', 'cancelado');
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
razao_social VARCHAR(200) NOT NULL,
nome_fantasia VARCHAR(200),
documento VARCHAR(20), -- CNPJ/CPF
email_contato VARCHAR(150) NOT NULL,
telefone VARCHAR(20),
logo_url TEXT, -- personalização visual (whitelabel)
cor_primaria VARCHAR(7), -- "#1a73e8"
plano_id UUID REFERENCES planos(id),
status tenant_status NOT NULL DEFAULT 'trial',
dia_vencimento_mensalidade SMALLINT NOT NULL DEFAULT 10
CHECK (dia_vencimento_mensalidade BETWEEN 1 AND 28),
trial_expira_em TIMESTAMPTZ,
criado_por UUID REFERENCES master_admins(id),
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_tenants_status ON tenants(status);
-- Faturas da mensalidade (você cobrando o tenant)
CREATE TYPE cobranca_status AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado');
CREATE TABLE tenant_faturas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
referencia VARCHAR(7) NOT NULL, -- '2026-08'
valor NUMERIC(10,2) NOT NULL,
vencimento DATE NOT NULL,
pago_em TIMESTAMPTZ,
status cobranca_status NOT NULL DEFAULT 'pendente',
alerta_enviado_em TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, referencia)
);
CREATE INDEX idx_tenant_faturas_venc
ON tenant_faturas(vencimento) WHERE status IN ('pendente', 'atrasado');
-- ============================================================
-- 3. USUÁRIOS E AUTENTICAÇÃO
-- ============================================================
CREATE TYPE user_role AS ENUM ('tenant_admin', 'vendedor', 'financeiro', 'visualizador');
CREATE TABLE tenant_users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
nome VARCHAR(150) NOT NULL,
email VARCHAR(150) NOT NULL,
senha_hash TEXT NOT NULL,
role user_role NOT NULL DEFAULT 'vendedor',
ativo BOOLEAN NOT NULL DEFAULT TRUE,
mfa_secret TEXT,
ultimo_login TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, email)
);
CREATE INDEX idx_tenant_users_tenant ON tenant_users(tenant_id);
-- Refresh tokens (JWT access token curto + refresh token com rotação)
CREATE TABLE auth_refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_tipo VARCHAR(20) NOT NULL, -- 'master' | 'tenant_user'
usuario_id UUID NOT NULL,
token_hash TEXT NOT NULL, -- nunca guardar o token em texto puro
expira_em TIMESTAMPTZ NOT NULL,
revogado BOOLEAN NOT NULL DEFAULT FALSE,
ip_origem INET,
user_agent TEXT,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_refresh_tokens_usuario ON auth_refresh_tokens(usuario_tipo, usuario_id);
CREATE TABLE password_reset_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_tipo VARCHAR(20) NOT NULL,
usuario_id UUID NOT NULL,
token_hash TEXT NOT NULL,
expira_em TIMESTAMPTZ NOT NULL,
usado BOOLEAN NOT NULL DEFAULT FALSE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Registro de tentativas de login (para rate-limit/bloqueio de força bruta)
CREATE TABLE login_attempts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(150) NOT NULL,
usuario_tipo VARCHAR(20) NOT NULL,
sucesso BOOLEAN NOT NULL,
ip_origem INET,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_login_attempts_email_data ON login_attempts(email, criado_em);
-- ============================================================
-- 4. INTEGRAÇÕES POR TENANT (WhatsApp / E-mail / Templates)
-- ============================================================
CREATE TABLE tenant_whatsapp_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
evolution_config_id UUID REFERENCES evolution_api_config(id),
instance_name VARCHAR(100) NOT NULL UNIQUE, -- nome único na Evolution API
numero_conectado VARCHAR(20),
status VARCHAR(30) NOT NULL DEFAULT 'desconectado',
-- desconectado | conectando | conectado | erro
qr_code_gerado_em TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE tenant_email_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL UNIQUE REFERENCES tenants(id) ON DELETE CASCADE,
smtp_host VARCHAR(150),
smtp_porta INTEGER,
smtp_usuario VARCHAR(150),
smtp_senha_cripto TEXT,
email_remetente VARCHAR(150),
usar_config_padrao BOOLEAN NOT NULL DEFAULT TRUE, -- usa master_email_config se true
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Templates de mensagens (nível master = padrão global; nível tenant = customizado)
CREATE TABLE alert_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL = template padrão da plataforma
tipo VARCHAR(50) NOT NULL,
-- 'mensalidade_tenant' | 'cobranca_cliente' | 'venda_confirmada' | 'lembrete_vencimento'
canal VARCHAR(20) NOT NULL, -- 'whatsapp' | 'email'
assunto VARCHAR(200), -- só para email
corpo TEXT NOT NULL, -- placeholders: {{nome}}, {{valor}}, {{vencimento}}
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Log/auditoria de todo alerta disparado (master->tenant e tenant->customer)
CREATE TABLE alert_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
destinatario_tipo VARCHAR(20) NOT NULL, -- 'tenant' | 'customer'
destinatario_id UUID NOT NULL, -- id do tenant ou do customer
canal VARCHAR(20) NOT NULL, -- whatsapp | email
template_id UUID REFERENCES alert_templates(id),
conteudo_enviado TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'enviado', -- enviado | falhou | entregue | lido
erro TEXT,
enviado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_alert_logs_tenant ON alert_logs(tenant_id, enviado_em);
-- ============================================================
-- 5. CLIENTES DO TENANT ("customers" — empresas atendidas)
-- ============================================================
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
razao_social VARCHAR(200) NOT NULL,
nome_fantasia VARCHAR(200),
documento VARCHAR(20),
email VARCHAR(150),
telefone_whatsapp VARCHAR(20),
endereco TEXT,
cidade VARCHAR(100),
uf CHAR(2),
observacoes TEXT,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_customers_tenant ON customers(tenant_id);
CREATE TABLE customer_contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
nome VARCHAR(150) NOT NULL,
cargo VARCHAR(100),
email VARCHAR(150),
telefone_whatsapp VARCHAR(20),
principal BOOLEAN NOT NULL DEFAULT FALSE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ============================================================
-- 6. PRODUTOS E SERVIÇOS DO TENANT
-- ============================================================
CREATE TYPE item_tipo AS ENUM ('produto', 'servico');
CREATE TABLE products_services (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
tipo item_tipo NOT NULL,
nome VARCHAR(200) NOT NULL,
descricao TEXT,
sku VARCHAR(50),
valor_venda NUMERIC(12,2) NOT NULL,
valor_custo NUMERIC(12,2),
unidade VARCHAR(20) NOT NULL DEFAULT 'un', -- un, hora, mês...
recorrente BOOLEAN NOT NULL DEFAULT FALSE, -- ex: serviço de mensalidade
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(tenant_id, sku)
);
CREATE INDEX idx_products_tenant ON products_services(tenant_id);
-- ============================================================
-- 7. VENDAS
-- ============================================================
CREATE TYPE venda_status AS ENUM ('orcamento', 'confirmada', 'cancelada', 'concluida');
CREATE TABLE sales (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
customer_id UUID NOT NULL REFERENCES customers(id),
vendedor_id UUID REFERENCES tenant_users(id),
numero_venda SERIAL,
status venda_status NOT NULL DEFAULT 'confirmada',
valor_total NUMERIC(12,2) NOT NULL DEFAULT 0,
desconto NUMERIC(12,2) NOT NULL DEFAULT 0,
observacoes TEXT,
data_venda DATE NOT NULL DEFAULT CURRENT_DATE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_sales_tenant_data ON sales(tenant_id, data_venda);
CREATE INDEX idx_sales_customer ON sales(customer_id);
CREATE TABLE sale_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sale_id UUID NOT NULL REFERENCES sales(id) ON DELETE CASCADE,
product_service_id UUID NOT NULL REFERENCES products_services(id),
quantidade NUMERIC(10,2) NOT NULL DEFAULT 1,
valor_unitario NUMERIC(12,2) NOT NULL,
valor_total NUMERIC(12,2) NOT NULL
);
CREATE INDEX idx_sale_items_sale ON sale_items(sale_id);
-- Parcelas/recebimentos: contas a receber do tenant junto ao customer
CREATE TYPE parcela_status AS ENUM ('pendente', 'pago', 'atrasado', 'cancelado');
CREATE TABLE sale_payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sale_id UUID NOT NULL REFERENCES sales(id) ON DELETE CASCADE,
numero_parcela SMALLINT NOT NULL DEFAULT 1,
valor NUMERIC(12,2) NOT NULL,
vencimento DATE NOT NULL,
pago_em TIMESTAMPTZ,
forma_pagamento VARCHAR(30), -- pix, boleto, cartao, dinheiro
status parcela_status NOT NULL DEFAULT 'pendente',
alerta_enviado_em TIMESTAMPTZ,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_sale_payments_venc
ON sale_payments(vencimento) WHERE status IN ('pendente', 'atrasado');
-- ============================================================
-- 8. AUDITORIA GERAL
-- ============================================================
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL,
usuario_tipo VARCHAR(20) NOT NULL, -- master | tenant_user
usuario_id UUID,
acao VARCHAR(100) NOT NULL, -- login, criar_venda, excluir_cliente...
entidade VARCHAR(50),
entidade_id UUID,
detalhes JSONB,
ip_origem INET,
criado_em TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_tenant_data ON audit_logs(tenant_id, criado_em);
-- ============================================================
-- 9. VIEWS DE DASHBOARD (usadas no painel do tenant)
-- ============================================================
CREATE VIEW vw_resumo_vendas_semanal AS
SELECT
s.tenant_id,
date_trunc('week', s.data_venda)::date AS semana,
COUNT(*) AS qtd_vendas,
SUM(s.valor_total) AS total_vendido
FROM sales s
WHERE s.status IN ('confirmada', 'concluida')
GROUP BY s.tenant_id, date_trunc('week', s.data_venda);
CREATE VIEW vw_resumo_vendas_mensal AS
SELECT
s.tenant_id,
date_trunc('month', s.data_venda)::date AS mes,
COUNT(*) AS qtd_vendas,
SUM(s.valor_total) AS total_vendido
FROM sales s
WHERE s.status IN ('confirmada', 'concluida')
GROUP BY s.tenant_id, date_trunc('month', s.data_venda);
CREATE VIEW vw_recebimentos AS
SELECT
sp.id,
s.tenant_id,
s.customer_id,
sp.valor,
sp.vencimento,
sp.status,
sp.pago_em
FROM sale_payments sp
JOIN sales s ON s.id = sp.sale_id;
-- ============================================================
-- 10. ROW LEVEL SECURITY — isolamento entre tenants
-- ============================================================
-- A aplicação deve definir, por conexão/transação:
-- SET app.current_tenant_id = '<uuid-do-tenant-logado>';
-- Usuários master (que enxergam todos os tenants) usam uma role
-- de banco separada (ex: app_master) que faz BYPASS RLS, ou uma
-- policy adicional que libera quando app.is_master = 'true'.
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
ALTER TABLE customer_contacts ENABLE ROW LEVEL SECURITY;
ALTER TABLE products_services ENABLE ROW LEVEL SECURITY;
ALTER TABLE sales ENABLE ROW LEVEL SECURITY;
ALTER TABLE sale_items ENABLE ROW LEVEL SECURITY;
ALTER TABLE sale_payments ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_users ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_whatsapp_instances ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_email_config ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_customers ON customers
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_products ON products_services
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_sales ON sales
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_tenant_users ON tenant_users
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_whatsapp ON tenant_whatsapp_instances
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
CREATE POLICY tenant_isolation_email_config ON tenant_email_config
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- sale_items e customer_contacts não têm tenant_id direto: isolar via join
CREATE POLICY tenant_isolation_sale_items ON sale_items
USING (sale_id IN (
SELECT id FROM sales
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
CREATE POLICY tenant_isolation_sale_payments ON sale_payments
USING (sale_id IN (
SELECT id FROM sales
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
CREATE POLICY tenant_isolation_customer_contacts ON customer_contacts
USING (customer_id IN (
SELECT id FROM customers
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
-- Fim do schema