Skip to main content

Banco de Dados

📋 Visão Geral

O Portal FindUP utiliza PostgreSQL via Supabase, implementando um schema robusto com Row Level Security (RLS), triggers automáticos e funções customizadas para garantir integridade, segurança e performance.

🗄️ Schema do Banco

Diagrama de Relacionamentos

📊 Tabelas Principais

auth.users (Supabase Auth)

Sistema de autenticação nativo do Supabase.

-- Gerenciado pelo Supabase Auth
-- Contém: id, email, encrypted_password, etc.

profiles

Perfis estendidos dos usuários.

CREATE TABLE public.profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
full_name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
document_number TEXT, -- CPF/CNPJ
company_name TEXT,
commission_rate DECIMAL(5,4) DEFAULT 0.10,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);

user_roles

Sistema de roles hierárquico.

CREATE TYPE public.app_role AS ENUM ('admin', 'technician', 'user');

CREATE TABLE public.user_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role app_role NOT NULL,
UNIQUE (user_id, role)
);

products

Catálogo de produtos e serviços.

CREATE TYPE public.product_type AS ENUM (
'google_workspace',
'microsoft_365',
'other'
);

CREATE TABLE public.products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
product_type product_type NOT NULL,
price DECIMAL(10,2) NOT NULL,
monthly_price DECIMAL(10,2), -- Para assinaturas
commission_percentage DECIMAL(5,4) DEFAULT 0.10,
is_active BOOLEAN DEFAULT true,
image_url TEXT,
features TEXT[], -- Array de recursos
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);

customers

Base de clientes dos técnicos.

CREATE TABLE public.customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
technician_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE,
company_name TEXT NOT NULL,
contact_name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT NOT NULL,
cnpj TEXT,
address TEXT,
city TEXT,
state TEXT,
zip_code TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);

proposals

Propostas comerciais.

CREATE TYPE public.proposal_status AS ENUM (
'pending',
'sent',
'activated',
'email_sent',
'approved',
'rejected'
);

CREATE TABLE public.proposals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID REFERENCES public.customers(id) ON DELETE CASCADE,
technician_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE,
product_id UUID REFERENCES public.products(id) ON DELETE CASCADE,
monthly_value DECIMAL(10,2) NOT NULL,
contract_months INTEGER DEFAULT 12,
total_value DECIMAL(10,2) NOT NULL,
status proposal_status DEFAULT 'pending',
proposal_pdf_url TEXT,
email_sent_at TIMESTAMPTZ,
approval_token TEXT UNIQUE,
approved_at TIMESTAMPTZ,
approved_by_email TEXT,
activated_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);

orders

Pedidos gerados a partir das propostas.

CREATE TYPE public.order_status AS ENUM (
'pending',
'processing',
'completed',
'cancelled'
);

CREATE TABLE public.orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
proposal_id UUID REFERENCES public.proposals(id),
customer_id UUID REFERENCES public.customers(id),
technician_id UUID REFERENCES public.profiles(id),
product_id UUID REFERENCES public.products(id),
quantity INTEGER DEFAULT 1,
unit_price DECIMAL(10,2) NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
status order_status DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);

commissions

Comissões dos técnicos.

CREATE TABLE public.commissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID REFERENCES public.orders(id),
technician_id UUID REFERENCES public.profiles(id),
amount DECIMAL(10,2) NOT NULL,
commission_rate DECIMAL(5,4) NOT NULL,
vivo_payment_date DATE,
vivo_payment_received BOOLEAN DEFAULT false,
scheduled_payment_date DATE,
payment_status TEXT DEFAULT 'pending'
CHECK (payment_status IN ('pending', 'scheduled', 'paid')),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);

payment_calendar

Calendário de repasses.

CREATE TABLE public.payment_calendar (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
month_year TEXT UNIQUE NOT NULL,
vivo_payment_deadline DATE NOT NULL,
findup_payment_deadline DATE NOT NULL,
total_commissions_due DECIMAL(10,2) DEFAULT 0,
total_commissions_paid DECIMAL(10,2) DEFAULT 0,
status TEXT DEFAULT 'open'
CHECK (status IN ('open', 'processing', 'completed')),
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);

audit_logs

Logs de auditoria.

CREATE TABLE public.audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
action TEXT NOT NULL,
table_name TEXT,
record_id UUID,
old_values JSONB,
new_values JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);

🔐 Row Level Security (RLS)

Políticas por Tabela

profiles

-- Usuários podem ver apenas seu próprio perfil
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = user_id);

-- Admins podem ver todos os perfis
CREATE POLICY "Admins can view all profiles"
ON profiles FOR SELECT
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid()
AND role IN ('admin', 'super_admin')
)
);

customers

-- Técnicos veem apenas seus clientes
CREATE POLICY "Technicians view own customers"
ON customers FOR SELECT
USING (
technician_id IN (
SELECT id FROM profiles WHERE user_id = auth.uid()
) OR
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid() AND role IN ('admin', 'super_admin')
)
);

proposals

-- Técnicos veem apenas suas propostas
CREATE POLICY "Technicians view own proposals"
ON proposals FOR SELECT
USING (
technician_id IN (
SELECT id FROM profiles WHERE user_id = auth.uid()
) OR
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid() AND role IN ('admin', 'super_admin')
)
);

-- Acesso público via token para aprovação
CREATE POLICY "Public access via approval token"
ON proposals FOR SELECT
TO anon
USING (approval_token IS NOT NULL);

commissions

-- Técnicos veem apenas suas comissões
CREATE POLICY "Technicians view own commissions"
ON commissions FOR SELECT
USING (
technician_id IN (
SELECT id FROM profiles WHERE user_id = auth.uid()
) OR
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid() AND role IN ('admin', 'super_admin')
)
);

⚡ Funções Customizadas

has_role(user_id, role)

Verifica se um usuário tem determinado role.

CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role public.app_role)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
SELECT EXISTS (
SELECT 1
FROM public.user_roles
WHERE user_id = _user_id AND role = _role
)
$$;

calculate_payment_dates(sale_date)

Calcula datas de pagamento da Vivo e FindUP.

CREATE OR REPLACE FUNCTION public.calculate_payment_dates(sale_date DATE)
RETURNS TABLE (vivo_deadline DATE, findup_deadline DATE)
AS $$
DECLARE
next_month_start DATE;
vivo_date DATE;
findup_date DATE;
business_days INTEGER := 0;
current_date_calc DATE;
BEGIN
-- Primeiro dia do próximo mês
next_month_start := DATE_TRUNC('month', sale_date + INTERVAL '1 month');
current_date_calc := next_month_start;

-- Contar 15 dias úteis
WHILE business_days < 15 LOOP
IF EXTRACT(DOW FROM current_date_calc) NOT IN (0, 6) THEN
business_days := business_days + 1;
END IF;

IF business_days < 15 THEN
current_date_calc := current_date_calc + INTERVAL '1 day';
END IF;
END LOOP;

vivo_date := current_date_calc;
findup_date := vivo_date + INTERVAL '10 days';

RETURN QUERY SELECT vivo_date, findup_date;
END;
$$ LANGUAGE plpgsql;

generate_approval_token()

Gera token seguro para aprovação de propostas.

CREATE OR REPLACE FUNCTION public.generate_approval_token()
RETURNS TEXT
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
RETURN encode(gen_random_bytes(32), 'hex');
END;
$$;

🔄 Triggers

Fluxo de Triggers Automáticos

Atualização de updated_at

CREATE OR REPLACE FUNCTION public.update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Aplicar em todas as tabelas relevantes
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON public.profiles
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at();

Geração automática de tokens

CREATE OR REPLACE FUNCTION public.set_approval_token()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
IF NEW.approval_token IS NULL THEN
NEW.approval_token = public.generate_approval_token();
END IF;
RETURN NEW;
END;
$$;

CREATE TRIGGER set_approval_token_trigger
BEFORE INSERT ON public.proposals
FOR EACH ROW
EXECUTE FUNCTION public.set_approval_token();

Cálculo automático de comissões

CREATE OR REPLACE FUNCTION public.handle_proposal_activation()
RETURNS TRIGGER AS $$
DECLARE
month_key TEXT;
payment_dates RECORD;
commission_value DECIMAL(10,2);
BEGIN
IF NEW.status = 'activated' AND OLD.status != 'activated' THEN
-- Calcular datas de pagamento
SELECT * INTO payment_dates
FROM public.calculate_payment_dates(NEW.activated_at::DATE);

-- Chave do mês/ano
month_key := TO_CHAR(NEW.activated_at, 'YYYY-MM');

-- Calcular comissão: (mensalidade × 3) ÷ 2
commission_value := (NEW.monthly_value * 3) / 2;

-- Inserir/atualizar calendário de pagamento
INSERT INTO public.payment_calendar (
month_year,
vivo_payment_deadline,
findup_payment_deadline,
total_commissions_due
)
VALUES (
month_key,
payment_dates.vivo_deadline,
payment_dates.findup_deadline,
commission_value
)
ON CONFLICT (month_year)
DO UPDATE SET
total_commissions_due = payment_calendar.total_commissions_due + commission_value;

-- Atualizar comissão com as datas
UPDATE public.commissions
SET
scheduled_payment_date = payment_dates.findup_deadline,
amount = commission_value
WHERE order_id IN (
SELECT id FROM public.orders WHERE proposal_id = NEW.id
);
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER on_proposal_activation
AFTER UPDATE ON public.proposals
FOR EACH ROW EXECUTE FUNCTION public.handle_proposal_activation();

📈 Índices para Performance

-- Índices principais
CREATE INDEX idx_customers_technician_id ON public.customers(technician_id);
CREATE INDEX idx_proposals_customer_id ON public.proposals(customer_id);
CREATE INDEX idx_proposals_technician_id ON public.proposals(technician_id);
CREATE INDEX idx_proposals_status ON public.proposals(status);
CREATE INDEX idx_commissions_technician_id ON public.commissions(technician_id);
CREATE INDEX idx_payment_calendar_month_year ON public.payment_calendar(month_year);
CREATE INDEX idx_audit_logs_user_id ON public.audit_logs(user_id);
CREATE INDEX idx_audit_logs_created_at ON public.audit_logs(created_at);

-- Índices compostos
CREATE INDEX idx_proposals_tech_status ON public.proposals(technician_id, status);
CREATE INDEX idx_commissions_tech_status ON public.commissions(technician_id, payment_status);

🔒 Backup e Recuperação

Backup Automático (Supabase)

O Supabase realiza backups automáticos diários.

Backup Manual

# Backup via CLI do Supabase
supabase db dump --file backup.sql

# Restaurar backup
supabase db reset --from-file backup.sql

Exportação de Dados

-- Exportar dados específicos
COPY (SELECT * FROM proposals WHERE created_at >= '2025-01-01')
TO '/tmp/proposals_2025.csv' DELIMITER ',' CSV HEADER;

🔍 Monitoramento

Queries de Performance

-- Queries mais lentas
SELECT query, mean_time, calls, total_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

-- Tamanho das tabelas
SELECT
tablename,
pg_size_pretty(pg_total_relation_size(tablename::regclass)) as size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(tablename::regclass) DESC;

Logs de Auditoria

-- Ações recentes por usuário
SELECT
al.action,
al.table_name,
p.full_name,
al.created_at
FROM audit_logs al
JOIN profiles p ON p.user_id = al.user_id
ORDER BY al.created_at DESC
LIMIT 50;

🧹 Manutenção

Limpeza de Logs Antigos

-- Limpar logs de auditoria com mais de 1 ano
DELETE FROM audit_logs
WHERE created_at < now() - INTERVAL '1 year';

Otimização

-- Analisar estatísticas das tabelas
ANALYZE;

-- Reindexar se necessário
REINDEX TABLE proposals;

📊 Relatórios SQL

Vendas por Técnico

SELECT
p.full_name as tecnico,
COUNT(pr.id) as total_propostas,
COUNT(CASE WHEN pr.status = 'activated' THEN 1 END) as ativadas,
SUM(CASE WHEN pr.status = 'activated' THEN pr.total_value ELSE 0 END) as valor_total
FROM profiles p
LEFT JOIN proposals pr ON pr.technician_id = p.id
WHERE p.user_id IN (
SELECT user_id FROM user_roles WHERE role = 'technician'
)
GROUP BY p.id, p.full_name
ORDER BY valor_total DESC;

Comissões por Período

SELECT
DATE_TRUNC('month', c.created_at) as mes,
COUNT(*) as total_comissoes,
SUM(c.amount) as valor_total,
AVG(c.amount) as valor_medio
FROM commissions c
WHERE c.created_at >= '2025-01-01'
GROUP BY DATE_TRUNC('month', c.created_at)
ORDER BY mes;