-- ============================================================ -- 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 = ''; -- 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