Pular para conteúdo

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:

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

  1. Entrar no app como usuário da garantidora com credit_analyses:create.
  2. 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.
  3. Salvar e avançar para o passo Documentos.
  4. 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ário socio@garantora.test. A ocorrência que originou este registro está no banco: análise 45880313-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:224 monta o texto antes de qualquer decisão de layout, e o flexDirection só empilha o que já está escrito. Não é agravado pelo PWA: o dado vem do GET /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) com document_type_id classificado 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 é REQUIRED em LESSEE_PF com validity_days nulo.

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 chama documentsApi.upload(file, t.entity.type, …), e para as áreas de pessoa t.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 — mesmo renter_id, mesmo credit_analysis_id nulo, mesmos uploaded_by e created_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 por Document.renter_id e nunca por credit_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 — ChecklistItemStatus transporta document_id e uploaded_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:224 e apps/web/src/pages/credit-analyses/components/DocumentChecklist.tsx:133-135 — os dois componentes formatam uploaded_at como data e mais nada. O DocumentsStep escreve "Anexado em …", que afirma um ato desta tela; o DocumentChecklist mostra 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:

  1. build_checklist monta a área LESSEE_PF chamando documents_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 do Document (as quatro FKs são mutuamente exclusivas), um documento de pessoa não tem credit_analysis_id.
  2. _resolve pega o mais recente de cada tipo e pergunta _within_validity. Para o CPF, validity_days é None → a função devolve True incondicionalmente. Um documento de 2026-08-12 é tão válido quanto um de hoje, e continuará sendo em 2030.
  3. O status vira SATISFIED, com uploaded_at = content_updated_at or created_at.
  4. DocumentsStep.tsx:182 deriva attached = status !== "MISSING" && !!document_id e 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_checklist já recebe a CreditAnalysis (credit_analysis_document_service.py:141), então credit_analysis.created_at e o uploaded_at do 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 documents dizendo em que análise o arquivo foi anexado. Responde com exatidão, e custa migration, mudança no DocumentService.create e 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_RESPONSIBLE ou PROPERTY, não apenas o CPF. Hoje o CPF é o único REQUIRED da 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_USER tem credit_analyses:create, credit_analyses:read e credit_analyses:update (rbac_seed.py:227-229), e o checklist é lido com credit_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) e show.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ê:
  • ChecklistItemStatus ganha predates_analysis: bool (default False), com o Swagger explicando por que ele existe e qual é o limite dele.
  • CreditAnalysisDocumentService._predates(doc, analysis_created_at) — estático, uma comparação. Usa content_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. Normaliza tzinfo porque o SQLite dos testes devolve datetime ingênuo.
  • _resolve passa a receber analysis_created_at; build_checklist já tinha a CreditAnalysis em 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 em settings/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/2026 no lugar de Anexado 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} no DocumentsStep e credit-analysis-doc-origin-{document_type_id} no DocumentChecklist.
  • Responsividade: o container da legenda no DocumentChecklist trocou flexShrink: 0 por minWidth: 0, e o chip ganhou flexShrink: 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 SATISFIED e 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 45880313 do 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=False fixo 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 no CLAUDE.md (empate de created_at em SQLite desempatado por UUID aleatório). Evidência, sem reconstruir a main: os dois arquivos de teste não mudaram nesta branch (git diff main vazio); esta branch tocou só o checklist documental e o web; e rodando sozinha, a test_partner_agency_status_filter passou em 3 de 4 execuções. Não é regressão desta branch, por estes motivos.
  • [x] make lint — ruff check limpo nos três arquivos da API tocados; tsc --noEmit do 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 == 0 nos 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) contra https://dev.garantora.com.br/app depois 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, com not.toContainText('Anexado em').

Da lista da tentativa 2, foram fechados mais três: a área PROPERTY e a combinação EXPIRED + herdado, que produzem frases diferentes e não eram exercitadas, e a âncora dos itens do e2e por document_type_id em vez de items[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_snapshot virou decisão escrita, não silêncio. O retrato de auditoria da aprovação (RN11) segue sem predates_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 DocumentChecklist a 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/web antes do script quebrou o caminho relativo; o comando saiu com 0 (era o tsc que 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.