0007 — Documento do cadastro do inquilino aparece como "Entregue" numa análise recém-criada, sem nenhuma pista de que veio de antes¶
| Status | CORRIGIDO |
| Branch | fix/0007-cpf-ja-vem-entregue |
| Aberto em | 2026-08-14 |
| Severidade | MÉDIA |
| Apps afetados | api · web · e2e |
| Nota do diagnóstico | 8,7 (tentativa 2 de 3) |
| Nota da correção | 8,7 (tentativa 2 de 3) |
Status:
RASCUNHO→DIAGNOSTICADO→EM CORREÇÃO→CORRIGIDO
O fix atravessa dois portões, cada um com nota de 0 a 10 dada por um subagente verificador de contexto limpo, em até 3 tentativas, passando a partir de 7,0:
- O diagnóstico (seções 1 a 4) é validado na seção 5 (reprodução × causa raiz). É o portão barato: sem ele, o segundo mede o conserto contra um palpite.
- A correção (o diff e os testes) é validada na seção 11 (correção × regressão), com duas perguntas — atacou a causa ou o sintoma, e o que prova que não volta.
1. Sintoma¶
Ao criar uma Análise de Crédito e avançar para o passo Documentos, o item CPF já aparece marcado como Entregue — ícone verde de check-circle preenchido e a legenda "Anexado em 12/08/2026" — sem que nada tenha sido anexado nesta análise.
Não há mensagem de erro: a tela está funcionando. O que ela não faz é dizer que aquele documento veio do cadastro do inquilino, anexado dois dias antes, noutro contexto. A única pista disponível é a data — e a data sozinha não distingue "anexei agora" de "já estava lá", porque a mesma linha exibe as duas situações com exatamente o mesmo texto.
O efeito prático é que o operador não sabe se pode confiar no que está marcado. Numa tela cuja função é responder "o que ainda falta?", um "Entregue" de origem desconhecida é pior que um "Falta": o "Falta" manda anexar, e o "Entregue" opaco convida a seguir sem conferir — e a exigência documental existe justamente para ser conferida antes da aprovação (spec 0008, RN9).
2. Reprodução¶
- Entrar no app como usuário da garantidora com
credit_analyses:create. - Análise de Crédito → Nova, escolhendo como inquilino uma pessoa que já tem documento anexado no cadastro dela — no dev-remote, José da Silva.
- Salvar e avançar para o passo Documentos.
- Olhar a linha CPF no bloco
INQUILINO PF · José da Silva.
Observado agora: ícone verde, "Anexado em 12/08/2026", botões de baixar/substituir.
- Ambiente: dev-remote (
https://dev.garantora.com.br/app), garantidora "Garantidora", usuáriosocio@garantora.test. A ocorrência que originou este registro está no banco: análise45880313-e2f7-4618-a589-45d2cfc0e699,DRAFT, criada em 2026-08-14 07:58. O mesmo usuário havia anexado o CPF ao cadastro do José em 2026-08-12 22:16 — dois dias antes, noutra sessão. - Superfície: ambos. É a mesma legenda nos dois viewports —
DocumentsStep.tsx:224monta o texto antes de qualquer decisão de layout, e oflexDirectionsó empilha o que já está escrito. Não é agravado pelo PWA: o dado vem doGET /api/v1/credit-analyses/{id}/document-checklist(credit_analysis_documents.py:91) a cada abertura, e não do bundle cacheado. - Dados/pré-condições: o inquilino escolhido precisa ter, no cadastro dele, um documento vivo (
deleted_at IS NULL) comdocument_type_idclassificado de um tipo que a matriz exige na área da natureza dele. No dev-remote hoje: documents→ um único registro de tipo CPF,original_filename = almoco.pdf,renter_id= José da Silva,created_at = 2026-08-12;credit_analysis_document_requirements→ CPF éREQUIREDemLESSEE_PFcomvalidity_daysnulo.
Com qualquer outro inquilino da base o CPF aparece como "Falta" — o que confirma que o gatilho é o documento do cadastro, e não a criação da análise.
3. Esperado × Atual¶
- Esperado: o documento reaproveitado do cadastro continua valendo (é a decisão da spec 0008, confirmada pelo usuário em 2026-08-14), mas a linha diz de onde ele veio — que é do cadastro do inquilino e não desta análise. Quem lê a tela consegue separar, sem abrir o arquivo, "isto foi anexado aqui" de "isto veio junto com a pessoa".
- Atual: as duas origens são renderizadas com o mesmo ícone, a mesma cor e a mesma frase — "Anexado em DD/MM/AAAA". Não há como distinguir na tela, e a resposta da API não carrega o que permitiria distinguir.
4. Causa raiz¶
Não é o reaproveitamento em si. Ele é deliberado e está registrado na spec 0008 ("Conferência por pessoa — sim (RN8). Viabilizada por guardar o documento no cadastro da pessoa" e "Prender à análise custaria a autoria do documento, que é o que fecha o furo da renda somada"). O defeito é que a procedência do documento nunca é registrada em lugar nenhum — não é um dado que se perde no caminho até a tela; é um dado que nunca existiu.
Onde¶
Quatro pontos, em duas camadas:
apps/web/src/pages/credit-analyses/components/DocumentsStep.tsx:84— anexar pelo próprio passo Documentos chamadocumentsApi.upload(file, t.entity.type, …), e para as áreas de pessoat.entity.typeé"renter". Ou seja: o arquivo que o usuário anexa dentro da análise vai para o cadastro do inquilino, gravando uma linha indistinguível da de qualquer outra origem — mesmorenter_id, mesmocredit_analysis_idnulo, mesmosuploaded_byecreated_at. É a citação que sustenta a frase acima: não há campo a preservar.apps/api/app/repositories/credit_analysis_document_requirement_repo.py:109-110— a busca dos documentos da área de pessoa filtra porDocument.renter_ide nunca porcredit_analysis_id. É aqui que documento de qualquer época entra no checklist da análise nova. Correto pela RN8.apps/api/app/schemas/credit_analysis_document_requirement.py:104-124—ChecklistItemStatustransportadocument_ideuploaded_at, e nenhum campo de procedência. Não há o que a tela possa ler para decidir.apps/web/src/pages/credit-analyses/components/DocumentsStep.tsx:224eapps/web/src/pages/credit-analyses/components/DocumentChecklist.tsx:133-135— os dois componentes formatamuploaded_atcomo data e mais nada. ODocumentsStepescreve "Anexado em …", que afirma um ato desta tela; oDocumentChecklistmostra só a data solta.
Não é regressão. O comportamento existe desde a spec 0008, que criou o checklist; a spec 0037 mexeu em uploaded_at (passou a preferir content_updated_at), o que muda qual data aparece, não o fato de a origem não aparecer.
O mecanismo¶
A cadeia, do dado ao pixel:
build_checklistmonta a áreaLESSEE_PFchamandodocuments_for_entity(tenant_id, renter_id=renter.id)— o recorte é a pessoa. A análise em que o documento foi anexado nunca entra na consulta, e nem poderia: pela regra doDocument(as quatro FKs são mutuamente exclusivas), um documento de pessoa não temcredit_analysis_id._resolvepega o mais recente de cada tipo e pergunta_within_validity. Para o CPF,validity_dayséNone→ a função devolveTrueincondicionalmente. Um documento de 2026-08-12 é tão válido quanto um de hoje, e continuará sendo em 2030.- O status vira
SATISFIED, comuploaded_at = content_updated_at or created_at. DocumentsStep.tsx:182derivaattached = status !== "MISSING" && !!document_ide a linha 224 escreve "Anexado em 12/08/2026".
O passo 2 é o que faz o sintoma aparecer agora e neste item: sem prazo configurado, o documento nunca envelhece, então um arquivo anexado em qualquer momento do passado chega à análise nova como se tivesse acabado de ser entregue. E o passo 4 é o que fecha o defeito: a frase escolhida — "Anexado" — descreve um ato do usuário, quando o que houve foi um reaproveitamento. Não há informação errada na tela; há informação faltando, e o texto preenche a lacuna com a leitura mais otimista.
Por isso o bug aparece ali e não em toda parte: só na área de pessoa e de imóvel, que são as que resolvem documento por entidade externa à análise. Em ADDITIONAL o documento é resolvido por credit_analysis_id (credit_analysis_document_service.py:209-211) e o problema não existe — ali "Anexado" é literalmente verdade.
O que pode separar as duas origens¶
Como a procedência não é gravada, nenhuma leitura do Document distingue um arquivo anexado dentro desta análise de um que já estava no cadastro: as duas gravações produzem a mesma linha. Sobram dois caminhos, e a diferença entre eles decide o tamanho da correção:
- Comparar tempos.
build_checklistjá recebe aCreditAnalysis(credit_analysis_document_service.py:141), entãocredit_analysis.created_ate ouploaded_atdo item estão os dois em mãos no mesmo escopo. Documento cujo anexo é anterior à criação da análise não pode ter vindo dela — é dedução, não heurística. Não custa coluna nem migration. O limite, que precisa ser dito: o inverso não é garantido. Um documento anexado ao cadastro do inquilino por outra tela, depois que esta análise já existia, contaria como "desta análise". No caso relatado a distância é de dois dias, mas a fronteira é real e a correção não deve afirmar mais do que ela sustenta. - Passar a gravar a procedência — uma coluna nova em
documentsdizendo em que análise o arquivo foi anexado. Responde com exatidão, e custa migration, mudança noDocumentService.createe retrofit do que já existe (que ficaria nulo, isto é, sem resposta, justamente para os documentos antigos que motivaram este bug).
A escolha entre os dois é do implement; o que a causa raiz estabelece é que não existe um terceiro caminho pela leitura do dado atual.
O alcance¶
A mesma causa morde em quatro frentes, e não só no CPF:
DocumentsStep(passo 2 da criação) — onde o bug foi visto.DocumentChecklist(show.tsx, análise já criada) — mesma omissão, agravada por mostrar só a data, sem sequer a palavra "anexado".- Qualquer tipo exigido em
LESSEE_PF,LESSEE_PJ,CO_RESPONSIBLEouPROPERTY, não apenas o CPF. Hoje o CPF é o únicoREQUIREDda matriz do dev-remote, e por isso é o que salta aos olhos; parametrizar RG ou CNH como obrigatório reproduz o mesmo. A base já tem 174 documentos de "Comprovante de Renda" em 174 inquilinos distintos — todos invisíveis no checklist só porque esse tipo não está parametrizado. Basta parametrizar para o sintoma se multiplicar por 174. - Não é só a garantidora que lê essa legenda.
AGENCY_USERtemcredit_analyses:create,credit_analyses:readecredit_analyses:update(rbac_seed.py:227-229), e o checklist é lido comcredit_analyses:read— então a imobiliária percorre o mesmo passo Documentos, na criação e na correção de uma análise devolvida, e vê o mesmo "Entregue" sem procedência. Para ela o opaco pesa mais: o documento pode ter sido anexado por outra pessoa, e ela não tem por que saber que aquele CPF já morava no cadastro do inquilino. Isso sobe a prioridade do CA3 — a correção não pode ficar só no passo de criação. - Os outros consumidores do checklist leem só
blocking, e por isso nenhum deles é afetado — a marca de procedência só faz sentido em item satisfeito:BlockingDocumentsPanel,CreditAnalysisForm.tsx:668(conta quantos faltam para liberar o envio),show.tsx:1213(PendDialog) eshow.tsx:1274(ApproveDialog).
5. Validação do diagnóstico¶
Preenchido pelo laço de validação do /fix new (subagente fix-diagnosis-validator). Registrar todas as tentativas, inclusive as reprovadas.
| Tentativa | Reprodução | Causa raiz | Final | Veredicto |
|---|---|---|---|---|
| 1 | 8,3 | 7,0 | 7,0 | APROVADO (no piso) |
| 2 | 8,7 | 9,0 | 8,7 | APROVADO |
Resultado: aprovado na tentativa 2 com nota 8,7 (mínimo: 7,0).
Pendências aceitas: —
A tentativa 1 aprovou em 7,0 — o piso —, limitada pelo mecanismo (D5). O apontamento era procedente e foi atacado antes de seguir: o registro dizia que a procedência "se perde" e o código mostra que ela nunca é gravada, porque anexar pelo passo Documentos escreve no cadastro do inquilino como qualquer outra porta (
DocumentsStep.tsx:84). Sem isso escrito, o CA2 ficaria pedindo uma distinção que o dado não sustenta — que é exatamente o tipo de lacuna que reapareceria no portão da correção. Também foram corrigidos o caminho da rota, a lista de consumidores do checklist e a identificação do tenant e do usuário.
6. Correção¶
Aditiva, e só na apresentação. Nenhum status muda, nenhuma exigência muda, nenhuma aprovação que hoje passa deixa de passar. O que entra é um campo e uma frase.
Dos dois caminhos que a seção 4 levantou, foi escolhido o temporal — comparar a data do arquivo com a de criação da análise. O outro (gravar a procedência numa coluna nova) responderia com exatidão, mas custaria migration, mudança no DocumentService.create e deixaria null justamente nos documentos antigos que motivaram o bug — isto é, não responderia para o caso relatado.
apps/api¶
- Onde: schema + service
- O quê:
ChecklistItemStatusganhapredates_analysis: bool(defaultFalse), com o Swagger explicando por que ele existe e qual é o limite dele.CreditAnalysisDocumentService._predates(doc, analysis_created_at)— estático, uma comparação. Usacontent_updated_at or created_at, a mesma data da validade e a mesma que a tela mostra: a procedência é do arquivo, não do registro, então uma via nova anexada no curso da análise deixa de contar como herdada. Normalizatzinfoporque o SQLite dos testes devolvedatetimeingênuo._resolvepassa a receberanalysis_created_at;build_checklistjá tinha aCreditAnalysisem mãos nas três chamadas, então não houve consulta a mais.- Sem migration. Nenhuma coluna nova.
apps/web¶
- Onde:
components/procedencia.ts(novo),DocumentsStep.tsx,DocumentChecklist.tsx, e o tipo emsettings/credit-analysis-documents/api.ts - O quê: a montagem da frase saiu dos componentes para um módulo próprio, porque as duas telas precisam da mesma regra e escreviam textos diferentes para o mesmo dado.
legendaDoDocumento— a forma longa do passo Documentos:Já no cadastro de José da Silva · 12/08/2026no lugar deAnexado em 12/08/2026.legendaCompacta— a forma curta do checklist da análise (show.tsx), onde a situação já é dita por um chip ao lado e a linha mostrava só a data.donoDoCadastro— sai do bloco, não do item: é a área que define a entidade dona (PROPERTY→ o imóvel; as demais → a pessoa). Usa o nome quando existe, que é o que desfaz a ambiguidade numa análise com corresponsáveis.- Ícone e cor não mudam, por decisão do usuário: o documento herdado vale tanto quanto o anexado agora, e um check mais fraco diria o contrário — competindo, ainda por cima, com o amarelo de Vencido.
data-testid:credit-analysis-doc-origin-{key}noDocumentsStepecredit-analysis-doc-origin-{document_type_id}noDocumentChecklist.- Responsividade: o container da legenda no
DocumentChecklisttrocouflexShrink: 0porminWidth: 0, e o chip ganhouflexShrink: 0. A legenda cresceu; fixa, empurraria a linha para fora em 375px. Quem encolhe é o texto — o chip, nunca. - PWA: sem impacto; nenhum asset mudou e o dado vem da API a cada abertura.
apps/e2e¶
- Teste de regressão:
tests/e2e/procedencia-documento.spec.ts, três casos — o documento anexado antes da análise (rótulo presente, e sem bloqueio), o anexado depois (rótulo ausente), e a legenda mais longa em 375px sem rolagem horizontal. A ordem dos passos é o teste: invertida, não há procedência a declarar.
7. Risco / impacto colateral¶
Não toca dinheiro: o checklist não entra em pricing, billing nem sinistro.
O risco real é mexer na regra sem querer. A RN8 (conferência por pessoa) e a RN9 (bloqueio da aprovação por documento faltando) dependem de SATISFIED continuar significando o que significa hoje. Uma correção que mudasse o status do documento reaproveitado passaria a bloquear aprovações que hoje passam — exatamente o que o usuário decidiu não fazer. A correção precisa ser aditiva.
8. Critérios de aceite¶
- [x] CA1. Seguindo os passos da seção 2, a linha do CPF continua marcada como entregue e informa que o documento veio do cadastro do inquilino, não desta análise.
- [x] CA2. Um documento anexado depois que esta análise foi criada não recebe a marca de procedência — as duas situações ficam distinguíveis na tela. O CA se escreve pelo tempo, e não por "pelo passo Documentos", porque o upload do passo grava no cadastro do inquilino como qualquer outro (seção 4): a tela não tem como saber por qual porta o arquivo entrou, só se ele é anterior à análise.
- [x] CA3. A mesma distinção aparece no checklist da análise já criada (
show.tsx), não só no passo de criação. - [x] CA4. O status continua
SATISFIEDe a aprovação segue desbloqueada: nenhuma análise que hoje aprova passa a bloquear (RN9 intacta). - [x] CA5. A legenda não estoura nem gera rolagem horizontal em 375px.
9. Plano¶
- [x] Reproduzir o bug — feito pelo dado, sem criar nada: a análise
45880313do dev-remote é a ocorrência, e o CPF do José da Silva de 12/08 é a causa. - [x] Corrigir a causa raiz —
api(campo + comparação) →web(módulo de legenda + as duas telas) →e2e(spec de regressão) - [x] Teste de regressão que falharia se a correção fosse revertida — verificado de verdade, não presumido: com
predates_analysis=Falsefixo no lugar da chamada a_predates, os dois testes quebram (assert False is True). Restaurado em seguida. - [x]
make preflight— limpo - [x]
make test— 364 passaram, 2 falharam, e as duas são as intermitentes registradas noCLAUDE.md(empate decreated_atem SQLite desempatado por UUID aleatório). Evidência, sem reconstruir amain: os dois arquivos de teste não mudaram nesta branch (git diff mainvazio); esta branch tocou só o checklist documental e o web; e rodando sozinha, atest_partner_agency_status_filterpassou em 3 de 4 execuções. Não é regressão desta branch, por estes motivos. - [x]
make lint—ruff checklimpo nos três arquivos da API tocados;tsc --noEmitdo web sem erro. - [x] Validar no browser em desktop e mobile — script de screenshot contra o dev-remote reconstruído, com o nome longo (
Maria Aparecida da Conceição Fonseca):scrollWidth - clientWidth == 0nos dois viewports, zero erro de console, chip Entregue intacto e a legenda quebrando em duas linhas sem empurrar nada. - [x] e2e executado —
procedencia-documento.spec.ts, 4 casos, todos verdes (23,2s) contrahttps://dev.garantora.com.br/appdepois do/devremote-rebuild.
10. Decisões em aberto¶
- [x] O documento do cadastro deve valer para uma análise nova? — Sim, e a tela precisa dizer de onde veio. Decidido pelo usuário em 2026-08-14. Mantém a decisão da spec 0008; o defeito é de comunicação, não de regra. As outras duas saídas consideradas — exigir reanexo por análise, ou dar prazo ao CPF — foram descartadas: a primeira reverteria a RN8 e seria
/spec, não/fix; a segunda é parametrização (validity_days), que o usuário pode mudar sem código a qualquer momento.
11. Validação da correção¶
| Tentativa | Correção | Regressão | Final | Veredicto |
|---|---|---|---|---|
| 1 | 8,7 | 8,0 | 8,0 | APROVADO |
| 2 | 8,7 | 8,7 | 8,7 | APROVADO |
Resultado: aprovado na tentativa 2 com nota 8,7 (mínimo: 7,0).
A tentativa 1 já aprovava, e mesmo assim valeu a segunda: o apontamento mais grave era que a tela onde o bug foi relatado não tinha prova nenhuma. Os três casos e2e abriam
show.tsx, que usa a legenda curta; a frase longa do passo Documentos — a que dizia "Anexado em" — podia ser revertida sozinha com a suíte inteira verde. É exatamente a segunda maneira de um fix falhar parecendo pronto. Foi acrescentado o caso do passo Documentos, comnot.toContainText('Anexado em').Da lista da tentativa 2, foram fechados mais três: a área
PROPERTYe a combinaçãoEXPIRED+ herdado, que produzem frases diferentes e não eram exercitadas, e a âncora dos itens do e2e pordocument_type_idem vez deitems[0]— que num tenant compartilhado é vermelho falso esperando acontecer.
Defeitos aceitos:
- Nenhum teste percorre o
AGENCY_USER. O diagnóstico elegeu a imobiliária como quem mais sofre com o "Entregue" opaco, e o código não ramifica por papel — o checklist é o mesmo endpoint e os mesmos componentes para todos. O risco é de regressão futura, não de defeito presente. Fica registrado em vez de coberto. - O limite temporal do
predates_analysis— documento anexado ao cadastro por outra tela depois que a análise já existia conta como "desta análise". É inerente ao caminho escolhido (seção 4), está escrito na docstring de_predates, e erra para o lado seguro: some um rótulo, não aparece um falso.
12. Registro¶
O que era: as duas origens de um documento — herdado do cadastro e anexado agora — renderizadas com o mesmo ícone, a mesma cor e a mesma frase ("Anexado em DD/MM/AAAA").
O que virou: um campo derivado no contrato (predates_analysis) e uma frase que diz o cadastro de onde o arquivo veio, nas duas telas. Ícone e cor não mudaram.
O que divergiu do plano:
- O portão da correção aprovou na primeira rodada (8,0) e mesmo assim houve uma segunda. O apontamento era que a tela onde o bug foi relatado — o passo Documentos — não tinha prova nenhuma: os três casos e2e abriam o
show.tsx, que usa a legenda curta, e a frase longa podia ser revertida sozinha com a suíte verde. Aprovar e seguir teria fechado o fix com a metade mais importante descoberta. build_snapshotvirou decisão escrita, não silêncio. O retrato de auditoria da aprovação (RN11) segue sempredates_analysis, porque guarda fatos e a procedência é derivada deles — está na docstring, com o porquê.- O screenshot pegou o que nem o e2e nem o type check pegariam. No
DocumentChecklista legenda começa a própria linha, e ela saía em minúscula ("já no cadastro de…") — lê-se como erro de digitação. Corrigido, e a asserção do e2e foi ajustada junto. - Um rebuild falhou em silêncio no meio disso. O
cd apps/webantes do script quebrou o caminho relativo; o comando saiu com 0 (era otscque passara) e o dev-remote seguiu servindo o bundle anterior. Só o screenshot mostrou — a legenda continuava minúscula depois do "rebuild". Refeito com caminho absoluto.