Andrade Systems
Todos os sistemas
ProduçãoPrefeitura municipal brasileira

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.

Caminho de uma requisição de escritaA requisição atravessa uma cadeia de guards em ordem fixa — contenção de abuso, verificação de origem, sessão e permissão — antes de alcançar o serviço de domínio, que escreve no PostgreSQL e enfileira o trabalho assíncrono. O motor de rateio é mostrado separado, fora do caminho da requisição, porque não depende de banco, relógio nem framework.Requisição de escritaCadeia de guardsrate limitorigemsessãopermissãoordem fixa, travada por testeServiço de domínioPostgreSQLnumeric, não floatFila → workerfora da requisiçãoMotor de rateiosem banco · sem relógio · sem framework
Toda requisição de escrita passa pela mesma cadeia, em ordem fixa: contenção de abuso, verificação de origem, autenticação e só então autorização. O motor de rateio fica fora desse caminho — ele é matemática pura, sem banco e sem relógio.

Decisões de engenharia

Decisões e trade-offs

Cada decisão registra a justificativa e o trade-off aceito.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

React 19ViteTypeScriptTailwind

Backend

NestJSNode.jsTypeScriptZod

Dados

PostgreSQLPrismadecimal.js

Assíncrono

BullMQRedis

Segurança

Argon2idAES-256-GCMTOTPRBAC

Infraestrutura

RailwayS3Docker

Testes

VitestTestes de propriedade

Próximo sistema

ORKEN

Um ERP modular para diferentes ramos de operação.

Produção