Sistema de Gestão de Precatórios
Rateio, pagamento e auditoria de precatórios com precisão centesimal.
GovTech · Sistema financeiro auditável
- Produção
- Status
- 500+
- Testes automatizados
- Imutável
- Auditoria
Visão geral
O que este sistema resolve
Precatório é uma dívida judicial que o poder público é obrigado a pagar. Quando o município perde uma ação coletiva movida por servidores, o valor precisa ser dividido entre centenas de pessoas segundo uma regra oficial — e cada centavo dessa divisão pode ser questionado por qualquer um dos beneficiários, pelo Ministério Público ou pelo Tribunal de Contas.
O sistema cobre esse ciclo inteiro: o beneficiário se cadastra, envia documentos e dados bancários; a administração lança os salários que servem de base, publica os parâmetros financeiros, processa o rateio, monta os lotes de pagamento e registra cada pagamento realizado. Tudo o que acontece fica registrado de forma que não possa ser alterado depois.
O desafio
O que tornava o problema difícil
Estes são os pontos que mais influenciaram a arquitetura.
A conta precisa fechar exatamente
O rateio é proporcional à soma dos salários de cada pessoa num período de nove anos. A quota exata quase nunca cai num número redondo de centavos. Arredondar cada quota individualmente faz a soma divergir do total — sobra ou falta dinheiro público, e a diferença não tem dono.
Segregação de funções entre registro e reversão
Quem registra um pagamento não pode ser quem o reverte. Quem cria a regra de cálculo não pode ser quem a publica. Segregação de funções em papel é política; imposta pelo sistema, é controle.
Histórico crítico imutável
Regra publicada, parâmetro publicado, rateio publicado e pagamento registrado são fatos. Corrigir um fato significa criar uma nova versão ou registrar uma reversão justificada — nunca editar o original.
Proteção de dados bancários mesmo em caso de acesso ao banco
O sistema guarda CPF, conta bancária, valores a receber e documentos pessoais. Um backup extraviado não pode, sozinho, entregar nada disso.
O público é majoritariamente idoso
Toda barreira de acesso que parece boa no papel — código de ativação, exigência de e-mail confirmado, senha complexa — é uma pessoa de 70 anos que não consegue receber o que é dela. As decisões de segurança precisaram ser tomadas com esse peso na balança.
Arquitetura
Como o sistema é montado
- Frontend
React 19 · Vite · Tailwind
Portal do beneficiário e painel administrativo no mesmo build.
- API
NestJS · Prisma
Serve as rotas de API e o frontend compilado na mesma origem.
- Banco
PostgreSQL
Todo valor monetário em NUMERIC de precisão fixa. Nenhum float.
- Fila
BullMQ sobre Redis
E-mail, varredura de anexo e manutenção fora do ciclo da requisição.
- Worker
Processo separado
Consome a fila. Sem porta pública além de um health mínimo.
- Storage
Bucket privado
Documentos versionados, acesso por URL assinada de curta duração.
- Pacotes compartilhados
TypeScript
Permissões, schemas de validação, CPF, dinheiro e CSV — front e back leem o mesmo contrato.
Decisões de engenharia
Decisões e trade-offs
Cada decisão registra a justificativa e o trade-off aceito.
Uma origem pública só: a API serve também o frontend
Frontend e API no mesmo endereço eliminam a configuração cruzada de CORS, permitem o cookie de sessão mais restritivo disponível e tornam a verificação de origem uma defesa real contra requisição forjada. Um DNS, um certificado, uma superfície.
Custo aceito · Frontend e API escalam juntos. Colocar o front atrás de um CDN exigiria repensar a estratégia de cookie e de verificação de origem.
Valores monetários em decimal de precisão fixa
Ponto flutuante binário não representa 0,10 exatamente. Num sistema que soma milhares de parcelas, o erro acumulado deixa de ser teórico e vira divergência contábil. O valor nasce decimal no banco, transita como decimal no ORM, é serializado como texto na API e é formatado no frontend sem conversão numérica.
Custo aceito · Mais cerimônia em todo lugar: ninguém escreve uma soma sem pensar, e a serialização como texto surpreende quem chega ao projeto. Os testes verificam justamente que o atalho não foi tomado.
Fórmulas financeiras versionadas em código
Um campo de fórmula editável exigiria interpretar expressão em tempo de execução — isto é, executar código enviado pela interface com o privilégio do servidor. A fórmula oficial vive em código versionado e testado.
Custo aceito · Mudar a regra exige um deploy. Aceito: a regra é jurídica, muda por decisão publicada, e uma mudança dessas *deve* passar por revisão de código.
Unicidade garantida por constraint no banco
"No máximo uma versão vigente" verificado em código é um check-then-act: entre a leitura e a escrita cabe outra transação. Sob concorrência real — duplo-clique, retry de proxy, dois operadores — a regra falha exatamente quando importa. Índice único parcial resolve no único lugar que consegue.
Custo aceito · A violação chega como erro de constraint do banco, e o serviço precisa traduzi-la em uma resposta de conflito compreensível. É código a mais na borda, em troca de uma garantia que o serviço sozinho não consegue dar.
Segregação de funções validada pelo serviço
Esconder um botão é experiência de uso. A autoridade é o servidor, que resolve as permissões do banco e recusa por padrão quando não consegue identificar quem está pedindo.
Custo aceito · Existe uma saída de emergência para o perfil mais alto, porque uma regra sem exceção trava a operação num dia ruim. Ela exige motivo escrito e gera um evento de auditoria próprio.
Motor de rateio isolado da infraestrutura
Matemática pura é testável exaustivamente: entra parâmetro e lista de pesos, sai a distribuição. Sem dependência de infraestrutura, o mesmo teste roda em qualquer máquina e o resultado não depende do dia.
Custo aceito · Exige uma camada de tradução entre o banco e o motor. Em troca, a parte do sistema que ninguém pode errar é a mais simples de auditar.
Por dentro da engenharia
Os problemas em detalhe
Cada bloco abre com o problema, mostra a solução e explica por que ela foi escolhida. Expanda o que interessar.
Motor de rateio determinístico
Problema
Dividir um valor entre centenas de pessoas proporcionalmente ao salário produz quotas com infinitas casas decimais. Arredondar cada uma e somar dá um número diferente do total. Num rateio de dinheiro público, essa diferença é um erro contábil que alguém vai encontrar.
Solução
A aritmética roda em decimal de alta precisão. Cada quota é truncada ao centavo, e os centavos que sobram são distribuídos um a um pelos maiores restos fracionários, com desempate determinístico. Deságio e bônus são potes separados, e cada pote fecha sozinho. No fim, o motor confere: se a soma das partes não for exatamente igual ao total, ele lança um erro em vez de gravar.
Por que foi feito assim
O método do maior resto é a forma clássica de fechar uma distribuição proporcional sem criar nem perder unidades. O desempate determinístico é o que garante que reprocessar a mesma entrada — em outra máquina, em outro dia, com o banco devolvendo as linhas em outra ordem — produza exatamente os mesmos centavos para as mesmas pessoas. Um rateio que muda entre execuções é indefensável perante controle externo, mesmo quando as duas execuções fecham no total.
Testes de propriedade sobre a distribuição
Problema
Testar o rateio com três exemplos escolhidos à mão prova pouco. E verificar apenas que a soma fecha também não basta: uma distribuição grosseiramente injusta — em que uma pessoa leva quase tudo — fecharia no total e passaria no teste.
Solução
Duzentos cenários gerados por um gerador pseudo-aleatório de semente fixa, variando número de pessoas, quantidade de meses, deságio e bônus. Cada cenário verifica quatro invariantes: a soma fecha ao centavo; quem tem peso zero recebe exatamente zero; nenhum valor se afasta mais de dois centavos da quota matematicamente exata; e quem tem peso maior nunca recebe menos que quem tem peso menor. A cada vinte cenários, o mesmo caso é reexecutado com a lista embaralhada e comparado pessoa a pessoa.
Por que foi feito assim
A semente fixa dá o poder de cobertura do teste aleatório sem a instabilidade: a mesma sequência de cenários roda em qualquer máquina, em qualquer dia, e uma falha é sempre reproduzível. E escrever invariantes em vez de valores esperados obriga a declarar o que "correto" significa — foi assim que a verificação de alocação justa entrou, depois de ficar claro que fechar no total era condição necessária e não suficiente.
Auditoria imutável em duas camadas
Problema
Um sistema que move dinheiro público precisa preservar histórico contra dois adversários diferentes: o operador que quer apagar o próprio erro, e quem tem acesso direto ao banco de dados.
Solução
A trilha de auditoria não tem rota de alteração nem de exclusão na aplicação — e o banco recusa alteração, exclusão e limpeza da tabela por gatilho próprio. O conteúdo de cada evento passa por uma redação recursiva que reconhece campos sensíveis por nome, insensível a acento, caixa e separador, com limite de profundidade e tamanho.
Por que foi feito assim
Proteger só na camada da aplicação deixa a trilha à mercê de quem tem credencial de banco — que é exatamente o adversário contra quem a trilha existe. A redação está lá porque log de auditoria é o lugar mais fácil do sistema para vazar um segredo sem perceber: alguém registra "o objeto que mudou" e o objeto tinha um token dentro.
Idempotência no registro de pagamento
Problema
Registrar pagamento é a operação em que um duplo-clique, um retry automático de proxy ou um timeout de rede custa dinheiro de verdade. E é a operação que mais tende a ser repetida, porque quem opera fica na dúvida se funcionou.
Solução
A rota exige uma chave de idempotência. A chave e a resposta produzida ficam guardadas; repetir a mesma chave devolve a resposta original em vez de executar de novo. Abaixo disso, a transição de estado é feita com guarda condicional — a atualização só ocorre se a linha ainda estiver no estado esperado.
Por que foi feito assim
As duas defesas resolvem problemas diferentes e nenhuma substitui a outra. A chave de idempotência resolve a mesma requisição chegando duas vezes; a guarda de estado resolve duas requisições diferentes disputando a mesma linha. Implementar só a primeira deixa a corrida entre dois operadores em aberto.
Processamento fora do ciclo da requisição
Problema
Enviar e-mail, varrer um anexo em busca de código malicioso e gerar relatório são operações lentas e sujeitas a falha externa. Nenhuma delas pode prender a resposta ao usuário — e nenhuma pode falhar em silêncio.
Solução
Fila em Redis consumida por um processo separado, sem porta pública. Cada tipo de job foi classificado antes de ganhar política de retry, pela pergunta "o que acontece se isto rodar duas vezes?". A varredura de anexo é idempotente e pode repetir; o agendamento tem deduplicação por identificador; a importação de planilha não faz retry cego.
Por que foi feito assim
Retry é a configuração mais fácil de ligar e a mais fácil de errar. Numa importação de planilha, repetir cegamente duplicaria beneficiários — o defeito seria descoberto semanas depois, num relatório que não bate. Decidir a política por job, e não por padrão global, é o que torna a fila segura.
Unicidade garantida pelo banco
Problema
A regra "no máximo uma versão vigente por precatório" vivia numa verificação em código. Sob o nível de isolamento padrão do PostgreSQL, duas publicações simultâneas passam pela verificação antes de qualquer uma gravar — e o sistema fica com duas versões vigentes. A partir daí, a geração do arquivo de pagamento escolheria uma arbitrariamente: o beneficiário veria um valor na tela e a ordem bancária pagaria outro.
Solução
Índice único parcial sobre a condição de vigência, aplicado às tabelas versionadas. A segunda publicação concorrente falha na constraint, e o serviço traduz a falha em uma resposta de conflito. A leitura dos parâmetros passou a ter ordenação determinística, e a publicação retira explicitamente as outras versões.
Por que foi feito assim
Invariante que precisa valer sob concorrência não pertence ao código de aplicação — o banco é o único ponto por onde todas as transações passam obrigatoriamente. E o índice ser *parcial* é o que permite manter todo o histórico de versões na mesma tabela: a restrição vale só sobre as vigentes.
Segurança
Por que estas defesas, neste domínio
O princípio que organiza as decisões de segurança do sistema: um dump do banco, sozinho, é inútil. Todo dado sensível em repouso é cifrado com uma chave que vive apenas na configuração do processo, ou derivado com um segredo que vive apenas ali.
Autenticação que não responde perguntas
Todos os caminhos de falha de login — documento inexistente, conta inativa, senha errada, conta bloqueada — devolvem a mesma mensagem e pagam o mesmo custo de tempo. Sem isso, o formulário de login vira um serviço de consulta que confirma quem é beneficiário do precatório.
Senha com segredo fora do banco
Argon2id com parâmetros no piso recomendado, mais um segredo que vive apenas na configuração do processo e entra no cálculo do hash. Hashes gravados antes dessa mudança continuam válidos e são atualizados de forma transparente no login seguinte — ninguém precisou trocar de senha.
Sessão opaca do lado do servidor
O token é gerado no servidor após a autenticação e nunca aceito do cliente. O banco guarda apenas um derivado dele. Expiração absoluta, sem renovação silenciosa; logout, troca de senha e reset revogam de verdade, no servidor.
Segundo fator obrigatório para a administração
Segredo cifrado em repouso, com proteção contra reuso do mesmo código dentro da janela de validade, e bloqueio em contadores próprios — acertar a senha não zera o bloqueio do segundo fator.
Autorização resolvida do banco, falhando fechado
31 permissões distribuídas em 6 perfis. O guard exige todas as permissões declaradas na rota e recusa por padrão quando não consegue identificar o solicitante. Toda rota por identificador acessível ao beneficiário confere posse antes de responder; listagens coletivas devolvem documento mascarado.
Cifra dos dados mais sensíveis
Dados bancários e o segredo do segundo fator são cifrados com AES-256-GCM, com vetor de inicialização novo a cada operação e verificação de integridade na leitura — adulteração no banco falha, não passa despercebida. O formato é versionado, preparado para rotação de chave.
Configuração validada antes de subir
Faltando qualquer variável obrigatória, o processo encerra no boot com a lista do que falta, em vez de subir degradado. Em produção há exigências adicionais que não existem em desenvolvimento.
Qualidade
O que os testes protegem
A estratégia segue o risco, não a cobertura. O motor financeiro é matemática pura, então é testado exaustivamente por propriedade. O resto é testado nos pontos onde este sistema erraria: transição de estado, concorrência, autorização e serialização de dinheiro.
- O fechamento exato do rateio, sob 200 cenários gerados e um caso em escala real
- A ordem da cadeia de guards — reordená-la quebra um teste dedicado
- A inércia, em produção, da flag que dispensa o segundo fator
- Que dinheiro nunca vire número de ponto flutuante em nenhum ponto do trajeto
- Que o processo recuse subir sem os segredos obrigatórios
- Que a neutralização de fórmula em CSV cubra todas as gerações, e não só a exportação principal
Stack
Stack do projeto
O sistema em números
- 568 em 72 arquivos
- Testes automatizados
- 37
- Modelos de dados
- 31
- Permissões de acesso
- 6
- Perfis administrativos
- 22
- Módulos de domínio na API
- 15
- Migrations
conferido no repositório em 27/08/2026
Status
Produção
Frontend
Backend
Dados
Assíncrono
Segurança
Infraestrutura
Testes