Desenvolvimento com IA na prática: o CRM implementado
Da especificação ao sistema no ar: a stack, cada critério de aceite como teste e o defeito de desempenho que só existia com os limites de produção.

Neste artigo
O primeiro incremento do CRM de cobrança está no ar em crm.areacli.com. Ele importa a carteira por planilha, mostra devedores, títulos, faixas de atraso e o histórico de cada alteração, e exige login. Nesta parte estão as escolhas técnicas, feitas depois da especificação aprovada, e o que aconteceu quando o código encontrou o ambiente real. O código foi escrito pelo Claude Code, assistente de programação da Anthropic. As decisões, inclusive as que contrariaram a recomendação dele, foram minhas.
O resumo: as 29 regras e os 26 critérios de aceite da especificação funcional viraram testes automatizados, e todos passaram. Mesmo assim, três defeitos chegaram até a véspera da publicação, e um deles teria derrubado a demonstração na primeira planilha grande.
A stack, escolhida por último#
A linguagem, o banco e a arquitetura só foram discutidos depois de a especificação estar aprovada. A ordem não é capricho: as escolhas técnicas dependem do que o sistema precisa fazer. Login com perfis, vários credores mais adiante, importação de 200 mil linhas em até 2 minutos e telas acessíveis eram requisitos conhecidos antes de qualquer opção técnica aparecer.
| Decisão | Opções apresentadas | Escolha |
|---|---|---|
| Como as telas são montadas | HTML gerado no servidor; aplicação JavaScript com API; framework JavaScript completo | HTML no servidor, com htmx para atualizar partes da tela |
| Linguagem e framework | Python com Django; Go; C# com ASP.NET; TypeScript com Node | Python com Django |
| Linha do Django | 6.1, a mais nova; 5.2, de suporte longo | 6.1 |
| Fila para tarefas em segundo plano | Procrastinate, que usa o próprio PostgreSQL; um processo próprio; django-tasks | Procrastinate |
A favor do HTML gerado no servidor pesou a acessibilidade: HTML semântico funciona com leitor de tela e teclado sem esforço extra, e permite uma política de segurança de conteúdo (CSP) que proíbe qualquer script embutido na página. A favor do Django pesou o que ele já traz pronto: login, permissões, formulários e migrações de banco, que os próximos incrementos vão usar.
A primeira lista não tinha o Flask. A ferramenta de perguntas comporta quatro opções, e a vaga do Python foi para o Django sem aviso. Perguntei por quê. A comparação veio depois: o Flask é mais explícito, mas cada peça que o Django traz pronta viraria uma biblioteca a escolher, travar e auditar. Fiquei com o Django. A omissão ficou registrada na decisão técnica, porque uma lista de opções cortada sem aviso já embute uma escolha.
Duas regras do projeto em conflito#
O projeto tem uma regra antiga: só usar versões de software publicadas há pelo menos duas semanas. Ela protege contra o pacote adulterado ou defeituoso, que costuma ser descoberto e retirado nos primeiros dias.
Na hora de travar as versões, as mais novas do Django tinham 12 dias. Elas corrigiam quatro falhas de segurança, uma delas de negação de serviço. Seguir a regra à risca significaria publicar o sistema com falhas conhecidas e documentadas. A saída foi uma exceção escrita e com condições:
- só vale para correção da mesma linha de versão (6.1.1 para 6.1.2), nunca para versão nova de recursos;
- as notas oficiais precisam citar correção de segurança;
- o pacote vem da fonte oficial, com hash conferido;
- a adoção fica registrada no diário do projeto.
A regra foi posta à prova no mesmo dia. Pedi o Python 3.15. A verificação mostrou que ele tinha 9 dias e que o Django ainda não o declarava suportado. A exceção recém-criada não cobria o caso, porque é versão nova, não correção. Ficou o Python 3.14, e o 3.15 virou pendência com critério objetivo: entra quando o Django declarar suporte e a versão tiver duas semanas.
Cada critério de aceite virou um teste#
A especificação descreve cada critério no formato “dado, quando, então”. Na implementação, cada um virou um teste que abre um navegador de verdade (o Playwright), faz o que o usuário faria e confere o resultado na tela. O critério do batimento, por exemplo, diz que a conferência informa os títulos que serão retirados, com quantidade e valor, antes de qualquer gravação. O teste correspondente:
def test_c08_batimento_mostra_e_retira_os_ausentes(logado, live_server):
importar(logado, live_server.url, [linha("A", "100,00"), linha("B"), linha("C", "50,00")])
importar(logado, live_server.url, [linha("B"), linha("D")], modo="Batimento", confirmar=False)
expect(linha_tabela(logado, "Títulos que serão retirados")).to_contain_text("2 · R$ 150,00")
assert Titulo.objects.filter(situacao="retirado").count() == 0
Três cuidados tornaram essa correspondência confiável:
- Os exemplos da especificação são os dados do teste. “Hoje = 18/10/2026, vencimento 10/09/2026, 38 dias de atraso” está no teste com esses números. Para isso, o sistema ganhou uma data de referência configurável, usada só nos testes. Sem ela, os testes passariam num dia e quebrariam no seguinte.
- Acessibilidade verificada em toda tela. Cada tela visitada pelos testes passa pelo axe-core, que confere as regras da WCAG 2.2 nível AA. Qualquer violação reprova o teste.
- A CSP também é verificada. Se o navegador bloquear algum recurso por violar a política de segurança, o teste falha.
No total são 94 testes: 64 de regras isoladas, 26 critérios no navegador, 2 dos próprios instrumentos e 2 das telas no tamanho de um celular.
Testar o teste#
A suíte inteira passou de primeira, sem nenhuma violação de acessibilidade. Isso é motivo de desconfiança, não de comemoração. Um verificador de acessibilidade que não roda, ou uma política de segurança que não está ativa, também produzem zero erros.
Por isso, foram acrescentados dois testes que só passam se os instrumentos funcionarem: um planta uma imagem sem texto alternativo e exige que o axe acuse a violação; o outro tenta executar um script embutido e exige que a política de segurança o bloqueie. Os dois instrumentos funcionavam. Sem esses testes, eu não teria como saber.
O defeito que só existia com os limites de produção#
A especificação pede que uma planilha de 200 mil linhas seja conferida em até 2 minutos. No ambiente de testes, com 2 processadores, levou 12,9 segundos.
O plano técnico mandava medir também com os limites reais da demonstração, num ambiente descartável idêntico: 1 processador para o banco, num servidor que divide a máquina com outros serviços. Ali, a mesma conferência passou de 10 minutos e foi interrompida.
A causa estava numa única consulta, a que procura títulos repetidos no arquivo. As linhas entram numa tabela de preparação por carga em massa, e uma tabela recém-carregada não tem estatísticas. Sem elas, o PostgreSQL estimou que a tabela tinha uma linha e escolheu um plano que compara cada linha com todas as outras. Com 200 mil linhas, são bilhões de comparações. No ambiente de testes o plano tinha saído bom, e o motivo não ficou esclarecido; é justamente o tipo de diferença que só uma medição no ambiente real revela.
A correção é conhecida e está na documentação do PostgreSQL: rodar ANALYZE depois de qualquer carga em massa. A consulta também foi reescrita para não depender do plano escolhido. Com os mesmos limites da demonstração, o resultado ficou assim:
| Planilha | Conferência | Confirmação | Meta |
|---|---|---|---|
| 10 mil linhas | 2,0 s | 5,3 s | 30 s para conferir; lista em até 1 s (medido: 0,09 s) |
| 200 mil linhas | 23,8 s | 41,5 s | 2 minutos para cada etapa |
Este é o caso típico do defeito que passa batido num projeto comum. Os testes passam, a homologação usa uma planilha pequena, e o problema aparece quando o primeiro cliente grande envia o arquivo. Ele foi encontrado antes por duas razões: a especificação tinha uma meta de tempo que dava para medir, e o plano mandava medir com os recursos reais.
O que os testes não pegaram#
Dois defeitos passaram por todos os 94 testes e só apareceram em capturas de tela:
- As barras do gráfico de faixas de atraso não apareciam. Em português, o framework formata o número da largura com vírgula (“4,7”), e o desenho vetorial não aceita vírgula. A página continuava acessível, porque a tabela ao lado tem os mesmos números. Só estava errada.
- A ficha do devedor rolava para o lado no celular. Um texto escondido para leitores de tela escapava da tabela e alargava a página para 566 pixels numa tela de 390.
Os dois ganharam testes próprios. O segundo revelou ainda que as tabelas roláveis não podiam ser alcançadas pelo teclado, outro problema que o verificador automático só acusa na largura de um celular.
Dez lacunas devolvidas à especificação#
Uma especificação com 29 regras, revisada e aprovada, ainda deixou dez pontos sem definição. Alguns exemplos:
- Linhas em branco no meio da planilha contam como recusadas ou são ignoradas?
- Um telefone vazio na carga nova apaga o telefone que já estava cadastrado?
- O valor de cada faixa no resumo soma os títulos daquela faixa ou o total dos devedores que estão nela?
- A busca por “joão” deve encontrar “Joao”?
Num projeto comum, cada uma dessas perguntas seria respondida em silêncio por quem escreve o código. Aqui, o código implementou o comportamento mais conservador, e as dez voltaram para a especificação como decisões em aberto, com dono, prazo e a área que as decidiria numa empresa: operação, financeiro, jurídico ou TI.
Em números#
| Item | Resultado |
|---|---|
| Regras da especificação | 29 |
| Critérios de aceite automatizados no navegador | 26 de 26 |
| Testes no total | 94 |
| Violações de acessibilidade (WCAG 2.2 AA) nas telas testadas | 0 |
| Defeitos encontrados antes da publicação | 3, dos quais 2 visuais e 1 de desempenho |
| Lacunas devolvidas à especificação | 10 |
| Memória em uso na demonstração, em repouso | 210 MB, somando aplicação, fila e banco |
O que vem a seguir#
A demonstração está publicada, mas por enquanto o acesso é restrito. O próximo incremento traz os perfis de gestor e operador, a gestão de usuários pela tela e o usuário de demonstração público, com dados fictícios recriados todo dia. Antes dele, as dez lacunas precisam ser decididas.


