O problema
Uma factory financeira tocava a operação de empréstimos em planilhas e ferramentas improvisadas. O volume já era expressivo, com centenas de tomadores, milhares de contratos, ciclos mensais de fatura e conciliação manual de mora e cobrança. Cada passo manual era um ponto onde dinheiro escapava ou o compliance falhava.
O sistema precisava cobrir o ciclo completo: onboarding do cliente, aprovação do pedido, geração automática de parcelas, acúmulo diário de juros e mora, cobrança por WhatsApp, renegociação e uma trilha de auditoria por tomador, no estilo de um CRM. Com o tempo, passou também a atender mais de uma empresa na mesma base, o que trouxe a exigência de isolar os dados de cada uma.
Arquitetura
Monorepo com a API em NestJS 11 e o frontend em Next.js 15 com React 19, compartilhando o banco PostgreSQL via Prisma 6. O trabalho em segundo plano roda em BullMQ com Redis, e as mensagens de WhatsApp saem a partir das filas.
O sistema nasceu para uma empresa e evoluiu para multi-tenant. Cada empresa tem seus próprios clientes, contratos, modelos de mensagem e credenciais, sendo que as chaves do gateway de pagamento e da instância de WhatsApp ficam guardadas criptografadas. O isolamento acontece em duas camadas: um guard valida se o usuário pertence à empresa da requisição, e o Prisma injeta o filtro por companyId em cada consulta, de modo que uma empresa nunca enxerga os dados da outra. Um usuário pode pertencer a mais de uma empresa, através de uma tabela de associação, e um perfil SUPER_ADMIN transita entre todas.
┌─ Next.js 15 + React 19 ─────────────────────────┐
│ TanStack Query · Radix UI · Tailwind 4 │
│ JWT em cookies HTTP-only (withCredentials) │
└──────────────────┬──────────────────────────────┘
│ axios (tipado via Zod)
┌─ API NestJS 11 ──┴──────────────────────────────┐
│ Auth · Clientes · Pedidos · Faturas · Pagtos │
│ Renegociações · Comunicação · Dashboard │
│ escopo por companyId (multi-tenant) │
└──────┬───────────────────────────────┬──────────┘
│ │
PostgreSQL BullMQ + Redis ─→ WhatsApp
(Prisma 6) (fila whatsapp-messages)
├─ atraso aleatório 5 a 15s
├─ 3 tentativas, backoff exp.
└─ CommunicationLog
Os jobs diários (fuso do Brasil)
Três rotinas rodam em America/Sao_Paulo, nesta ordem:
- 05:00, processamento de inadimplência: faturas vencidas recebem o
lateFee%configurado, mudam paraUNPAIDe atualizam os totais do pedido. - 05:01, acúmulo de mora:
(lateFeeMora% / 30) × valor_original × dias_em_atraso, somado diariamente. As renegociações sempre incidem sobre o valor original do pedido, não sobre o renegociado. É uma regra financeira pouco óbvia, mas que importa para o compliance. - 06:00, apenas em dias úteis, mensagens de cobrança: agrupa por pedido as faturas em atraso e as que vencem no dia e enfileira os lembretes de WhatsApp.
Cada rotina percorre todas as empresas configuradas e tem um ponto de entrada manual de recuperação, para reprocessar um dia sem disparar efeitos colaterais acidentais.
Modelagem de domínio
A máquina de estados financeira é mapeada em um enum tipado do Prisma: OPEN → APPROVED → ACTIVE → PAID / RENEGOTIATED / UNAPPROVED / LOST. Cada pedido tem uma modalidade de recorrência (diária, semanal, quinzenal ou mensal) que define a frequência das parcelas. O dashboard lê agregados em cache, com paginação e filtros por modalidade e tipo de contrato, o que mantém as visões de portfólio rápidas mesmo com milhares de contratos ativos.
O controle de acesso é por papel e por departamento, com perfis como LOAN_MANAGEMENT, STREET_SUPERVISOR e RENEGOTIATION_ANALYST, e cada rota passa por guards de autenticação e de papel.
Um ponto que rendeu trabalho recente foi a liquidação de faturas. Pagamento parcial, quitação e estorno precisam manter o saldo do pedido exato em qualquer ordem de operação. Um bug de dupla contagem corrompia o amountDue em certos cenários de estorno, e a correção exigiu tornar a liquidação atômica. Hoje a baixa de pagamento é registrada dentro do próprio sistema, e a integração com a AbacatePay, para confirmar pagamentos de forma automática, está em adoção. Fica o aprendizado: em sistema financeiro, a ordem e a atomicidade das operações de saldo não são detalhe, são o produto.
Cobrança no WhatsApp: da Evolution API para a API oficial
O canal de cobrança é o WhatsApp, e quem inicia a conversa é sempre a empresa: lembrete de vencimento, aviso de atraso, cobrança. A primeira versão usou a Evolution API, uma ponte não oficial sobre o WhatsApp. Ela serviu bem para validar o produto, mas tem um teto claro. Como o contato parte sempre da empresa, e em volume, esse padrão esbarra nas regras do WhatsApp e deixa o número em risco constante de bloqueio. Para uma operação que depende da cobrança diária, isso é inviável.
Por isso a migração em andamento é para a API oficial do WhatsApp, a Cloud API da Meta. Ela existe justamente para esse caso: mensagens iniciadas pela empresa passam por modelos aprovados com antecedência, o que torna o envio em escala legítimo e estável. A modelagem atual já facilita a transição, porque as mensagens de cobrança e lembrete já são tratadas como modelos por empresa, e não como texto solto, o que mapeia direto para o conceito de template aprovado da API oficial.
O aprendizado aqui é de produto, não só técnico: uma integração não oficial é ótima para provar uma ideia, mas qualquer canal de comunicação que vira dependência do negócio precisa ir para a via oficial antes de escalar.
Confiabilidade
Conforme o sistema virou a base de uma operação real, a malha de testes e a documentação acompanharam. Hoje são mais de 300 testes, entre unitários e de integração, cobrindo os serviços financeiros, os guards e os jobs, além de documentação OpenAPI em todos os controllers. Não é vaidade de cobertura: em cálculo de juros, mora e liquidação, o teste é o que permite mexer no código sem medo de quebrar o saldo de alguém.
Resultado
Em produção e em manutenção contínua. A factory opera inteiramente dentro do sistema, sem planilhas, e as filas dão conta do ciclo diário sem intervenção humana. A base evoluiu de uma empresa para várias na mesma instância, e sigo dando manutenção e evoluindo o produto. Os números abaixo são contagens vivas, não estimativas.