Pular para conteúdo

0015 — Telas de Análise de Crédito no design system

Status CONCLUÍDA
Branch spec/0015-telas-de-analise-de-credito
Criada em 2026-08-03
Apps afetados web · e2e (api: sem mudança de contrato)

Status: RASCUNHO → APROVADA → EM IMPLEMENTAÇÃO → CONCLUÍDA

1. Problema

A lista de análise de crédito já segue o design system (spec 0012), mas as três telas onde o trabalho de verdade acontece — incluir, editar e ver — ficaram atrás. Quatro dores, todas do ponto de vista de quem opera:

  1. Cadastrar uma análise exige que inquilino e imóvel já existam. O caso mais comum é justamente o cliente novo: a imobiliária manda a ficha, o operador começa a análise, descobre que o inquilino não está na base, abandona a tela, cadastra, volta e redigita o que já havia digitado. Os dois campos hoje são Autocomplete de escolha — não há caminho de "cadastrar aqui".
  2. Os documentos entram tarde. O formulário grava e navega para o detalhe; o anexo só acontece depois, em outra tela. Ninguém avisa, no momento da inclusão, que faltam documentos obrigatórios — e a análise sai para a fila incompleta.
  3. No detalhe, a decisão concorre com o resto. Aprovar ou reprovar uma garantia é o ato mais consequente do sistema, e está no meio de painéis de igual peso visual.
  4. A regra de 3× é invisível. A renda declarada por participante é coletada, e o modelo documenta que ela alimenta o "cálculo automático de crédito (regra 3× aluguel)" — mas nada disso aparece enquanto o operador digita. A conta é feita de cabeça, depois.

O design system v2.10.0 resolve os quatro em ui_kits/desktop/AnaliseCredito.jsx: inclusão em dois passos, BlocoSelecao com quatro estados, indicador de renda ao digitar e a decisão isolada no fim da visualização.

2. Objetivo

As três telas de análise de crédito passam a seguir o design system: inclusão em dois passos com o anexo dentro do próprio fluxo, inquilino e imóvel escolhidos da base ou cadastrados ali mesmo, indicador de renda sobre o pacote locatício em tempo de digitação e visualização em painéis com histórico em timeline e a decisão sozinha no fim.

3. Fora de escopo

  • A lista — já convergiu na spec 0012; só é tocada se um testid mudar de nome.
  • Restrições em consulta de crédito. O DS mostra a linha; o modelo não tem o campo e não há integração com bureau. Sem dado, sem linha.
  • min_income_ratio como política do tenant. O mínimo de 3× fica constante no web (decisão da entrevista). Virar TenantSettings é spec própria.
  • Rebaixar a submissão para PENDING_DOCS quando falta documento obrigatório — ver RN10. O submit continua indo para SUBMITTED.
  • Regra de decisão. O DecisionPanel muda de lugar e de aparência; o que ele faz, valida e envia fica igual.
  • Contrato e cobranças. O botão de criar contrato segue como está.
  • Participantes além do inquilino principal. O BlocoSelecao do DS cobre um inquilino; a análise tem inquilinos e corresponsáveis. O bloco governa o principal; os demais seguem no formato atual (lista com renda declarada por pessoa).
  • Kit mobile do DS. Ele só define a lista de análise; as telas de formulário e detalhe não têm referência mobile e serão traduzidas para coluna única (ver §5).

4. Regras de negócio

  • RN1. O passo 2 (Documentos) fica trancado até o passo 1 gravar. Não é enfeite: o documento é criado contra o credit_analysis_id, que só existe depois do POST. O passo trancado mostra cadeado e o título "Disponível depois de gravar os dados".
  • RN2. "Gravar e anexar documentos" valida o passo 1, cria a análise em DRAFT (POST /credit-analyses) e abre o passo 2 exibindo o código (credit_analysis_number) no cabeçalho. A partir daí o botão vira "Ir para documentos" e não cria nada de novo.
  • RN3. Em edição o registro já existe: os dois passos ficam livres desde o começo. Se a análise está em PENDING_DOCS, a tela abre no passo 2 — é onde está a pendência — e o passo 1 exibe o aviso de que ela volta para a fila ao reenviar.
  • RN4. Cada BlocoSelecao (Inquilino, Imóvel) tem quatro estados: vazio (escolher da base / cadastrar novo), buscando (campo de busca + resultados), escolhido (resumo com ação "Trocar") e novo (formulário inline, com volta para "Escolher da base"). Trocar limpa a escolha e cai em buscando.
  • RN5. A imobiliária é escolhida antes do imóvel inline. PropertyCreate exige partner_agency_id, e o imóvel criado dentro da análise herda a imobiliária da própria análise. Sem imobiliária escolhida, o modo novo do bloco de imóvel fica indisponível, com o motivo à vista. Para AGENCY_USER a imobiliária é a do próprio usuário.
  • RN6. O formulário inline de inquilino exige nome e CPF/CNPJ conforme a natureza (PF/PJ) — mais estrito que a API, que só exige name. O de imóvel exige logradouro, cidade e UF (o que PropertyCreate já exige).
  • RN7. O registro criado inline entra no bloco já como escolhido. Se a criação falhar, o erro aparece no bloco e nada do que foi digitado se perde — nem no bloco, nem no resto do formulário. Esta é a razão de existir do bloco.
  • RN8. A razão renda/pacote é calculada enquanto se digita: soma das declared_income dos inquilinos ÷ pacote locatício (aluguel + condomínio + IPTU). ≥ 3× mostra reforço verde; < 3× mostra alerta âmbar dizendo que exigirá arbitragem manual. É indicador, não bloqueio: não impede gravar nem enviar.
  • RN9. O passo 2 lista os tipos exigidos vindos do checklist parametrizado (GET /credit-analyses/{id}/document-checklist, spec 0010) — por área e por pessoa — e não uma lista fixa. Cada linha mostra obrigatório/opcional, o que já está anexado (nome e data) e as ações de anexar, baixar e remover.
  • RN10. Enviar com documento obrigatório faltando é permitido, com aviso antes: quantos faltam e que a análise segue para a fila incompleta. A análise vai para SUBMITTED — divergência consciente do DS, que anuncia "Aguardando documentos". No app o PENDING_DOCS é decisão do analista, não efeito do envio, e mudar isso seria alterar a máquina de estados. O texto do aviso diz a verdade do app.
  • RN11. Permissões. Ver, criar, editar, submeter e decidir seguem as de hoje (credit_analyses:*). O cadastro inline exige renters:create / properties:create: sem a permissão, o bloco não oferece o modo novo — só a escolha da base. AGENCY_USER enxerga na busca apenas o que já lhe é visível (o escopo é da API, não da tela).
  • RN12. O formulário só abre em DRAFT e PENDING_DOCS, como hoje. A decisão só aparece nos status decidíveis; nos demais, a visualização é leitura.
  • RN13. O histórico vem de GET /credit-analyses/{id}/history e é apresentado em timeline vertical: ícone do status, o que aconteceu, quando e por quem.
  • RN14. "Salvar rascunho" mantém o destino atual (o detalhe da análise), não a lista como no protótipo do DS — ver §9. O rótulo em edição é "Salvar alterações".

5. Impacto por app

apps/api

Sem mudança de contrato. Tudo que as telas precisam já existe: criar, atualizar, submeter, cancelar, histórico, checklist de documentos, upload de documento, criar inquilino e criar imóvel. Duas correções pontuais, ambas de documentação/teste:

  • Docstring desatualizada em credit_analysis_service.submit — ela afirma validar "ao menos 1 participante com role=LANDLORD". O código não valida isso, e o modelo CreditAnalysisParticipant diz o contrário ("o proprietário não é participante"). Corrigir o texto; o Swagger herda dele.
  • Teste fixando a validação real do submit (LESSEE, um principal, ao menos uma renda declarada) — para que a linha removida da docstring não volte por engano.

apps/web

Páginas

  • pages/credit-analyses/create.tsx e edit.tsx convergem para um CreditAnalysisForm compartilhado (modo: "novo" | "editar"), espelhando o FormularioAnalise do DS: os campos e a validação são um só código; o modo muda cabeçalho, rótulos e ações.
  • pages/credit-analyses/show.tsx reorganizado no layout do AnaliseVer: Cliente e Imóvel em duas colunas, Situação financeira, Documentos + Histórico, Observações, e a decisão sozinha no fim.

Componentes novos (em pages/credit-analyses/components/, salvo indicação)

Componente Papel
AnalysisSteps os dois passos em pílula, com cadeado no 2 até gravar (RN1)
SelectionBlock o BlocoSelecao genérico, quatro estados (RN4)
DocumentsStep passo 2 sobre o checklist parametrizado (RN9)
IncomeRatioBadge indicador de renda sobre o pacote (RN8)
HistoryTimeline histórico em timeline (RN13)
DataRow / FinancialRow em components/shared/ linha rótulo→valor e linha financeira do DS

DocumentChecklist, FinancialSummaryPanel e DecisionPanel são reaproveitados; DecisionPanel muda de posição e de peso visual, não de comportamento.

data-testid novos

credit-analysis-step-1 | credit-analysis-step-2
credit-analysis-form-save-continue     (Gravar e anexar documentos / Ir para documentos)
credit-analysis-form-back-to-data
credit-analysis-renter-block  | credit-analysis-property-block
credit-analysis-{renter,property}-choose | -new | -search | -change | -back-to-base
credit-analysis-{renter,property}-option-{id}
credit-analysis-renter-{name,document,type,birth,phone,email}-input
credit-analysis-property-{street,number,city,state,type,area,owner}-input
credit-analysis-{renter,property}-inline-error
credit-analysis-income-ratio
credit-analysis-documents-step | credit-analysis-documents-pending-alert
credit-analysis-document-attach-{requirementKey} | credit-analysis-document-remove-{docId}
credit-analysis-history

Preservados: credit-analysis-form, -form-draft, -form-submit, -form-cancel, credit-analysis-rental-value-input, credit-analysis-tenant-{n}-income-input, credit-analysis-coresponsible-add, e os do DecisionPanel. Substituídos: credit-analysis-tenants-input e credit-analysis-property-input (os Autocomplete que o SelectionBlock aposenta) — usados em credit-analyses-mobile.spec.ts.

Sider: sem impacto — nenhum módulo novo.

Responsividade (o kit mobile do DS não cobre estas telas; a tradução é nossa)

  • Passos: pílula compacta, rótulos abreviados quando não couberem, sem rolagem horizontal.
  • Painéis de duas colunas (Cliente/Imóvel, Documentos/Histórico) → coluna única.
  • Grades de campos (1.4fr 1fr, 1fr 1fr 1.4fr, 1.4fr 100px 1fr 1fr) → uma coluna, com UF e área lado a lado quando cabem.
  • Lista de resultados do bloco: altura menor que os 232px do desktop, com o campo de busca fixo acima — o teclado virtual come metade da tela.
  • Decisão: os dois botões empilhados, largura cheia, altura mínima de 48px, o de aprovar em primeiro plano.
  • Campos em 16px (piso do fix 0002 — não regredir, sob pena de zoom no iOS).

PWA: sem impacto. Nenhum asset novo, nenhuma rota nova, nada de offline.

apps/e2e

  • Spec nova credit-analyses-form-steps.spec.ts — o fluxo de dois passos, o bloco de seleção nos quatro estados, o cadastro inline e o envio com pendência.
  • Ajustes em credit-analyses-mobile.spec.ts (3 usos dos testids aposentados) e em credit-analyses.spec.ts / credit-analysis-documents.spec.ts se algum seletor de detalhe mudar de lugar.
  • Seeds: nenhum novo. O cadastro inline cria os registros que usa; a busca da base usa os seeds existentes de inquilino e imóvel.

6. Contrato de API

Nenhum endpoint novo. Os que as telas consomem, na ordem do fluxo:

POST   /api/v1/credit-analyses                                  credit_analyses:create
PATCH  /api/v1/credit-analyses/{id}                             credit_analyses:update
GET    /api/v1/credit-analyses/{id}                             credit_analyses:read
GET    /api/v1/credit-analyses/{id}/history                     credit_analyses:read
GET    /api/v1/credit-analyses/{id}/document-checklist          credit_analyses:read
POST   /api/v1/credit-analyses/{id}/submit                      credit_analyses:submit
POST   /api/v1/documents                (credit_analysis_id)    documents:create
POST   /api/v1/renters                  (cadastro inline)       renters:create
POST   /api/v1/properties               (cadastro inline)       properties:create

Erros de negócio que as telas precisam mostrar no lugar certo:

Código Onde aparece
NO_LESSEE, NO_PRIMARY_LESSEE, LESSEE_NO_INCOME no envio, junto ao bloco/campo que falta
erro de validação do POST /renters / POST /properties dentro do bloco, sem perder o formulário (RN7)
BusinessRuleError de transição (análise fora de DRAFT) toast, e a tela recarrega o status

7. Critérios de aceite

  • [x] CA1. Em /credit-analyses/create, o passo 2 aparece trancado, com cadeado, e clicar nele não navega — só depois de gravar (RN1).
  • [x] CA2. "Gravar e anexar documentos" cria a análise em DRAFT, o código passa a aparecer no cabeçalho e o passo 2 abre. Clicar de novo no botão (agora "Ir para documentos") não cria uma segunda análise (RN2).
  • [x] CA3. Abrir a edição de uma análise em PENDING_DOCS cai no passo 2, e o passo 1 mostra o aviso de reenvio (RN3).
  • [x] CA4. No bloco Inquilino: buscar por nome acha o registro da base, escolher mostra o resumo com "Trocar", e "Trocar" volta para a busca (RN4).
  • [x] CA5. "Cadastrar novo" no bloco Inquilino cria o inquilino sem sair da tela, e ele entra já escolhido. Os valores digitados nos outros painéis continuam lá (RN7).
  • [x] CA6. Sem imobiliária escolhida, o modo "cadastrar novo" do bloco Imóvel está indisponível e diz por quê. Escolhida a imobiliária, o imóvel criado inline nasce vinculado a ela (RN5).
  • [x] CA7. Nome vazio no inquilino inline e logradouro vazio no imóvel inline barram a gravação com erro no próprio bloco (RN6).
  • [x] CA8. Com renda somada ≥ 3× o pacote locatício o indicador é de reforço; abaixo de 3× é de alerta e cita a arbitragem manual. Nos dois casos gravar e enviar seguem permitidos (RN8).
  • [x] CA9. O passo 2 lista os tipos vindos do checklist parametrizado — mudar a exigência em Configurações muda o que o passo 2 cobra (RN9).
  • [x] CA10. Enviar com obrigatório faltando mostra quantos faltam antes e, confirmado, a análise fica em SUBMITTED (RN10).
  • [x] CA11. No detalhe, o histórico aparece em timeline com quem e quando, e a decisão (aprovar/reprovar) está sozinha no fim da página, sem painel competindo (RN13).
  • [x] CA12. Em 375px as três telas não têm rolagem horizontal (scrollWidth - clientWidth == 0), os painéis viram coluna única, os botões de decisão ficam empilhados com ≥48px e nenhum campo tem fonte menor que 16px.
  • [x] CA13. Papel sem renters:create não vê "Cadastrar novo" no bloco Inquilino, e segue conseguindo escolher da base (RN11).
  • [x] CA14. A docstring do submit não afirma mais exigir LANDLORD, e há teste fixando o que ele valida de fato.

8. Plano de implementação

  • [x] apps/api — corrigir a docstring do submit (sem LANDLORD) e conferir o Swagger
  • [x] apps/api — teste fixando as validações reais do submit
  • [x] apps/api — sem model, sem migration, sem endpoint novo (registrar que foi conferido)
  • [x] apps/web — components/shared/DataRow e FinancialRow
  • [x] apps/web — SelectionBlock com os quatro estados, sem ainda ligar no formulário
  • [x] apps/web — CreditAnalysisForm unificado + AnalysisSteps (RN1–RN3)
  • [x] apps/web — ligar os dois blocos, com cadastro inline e herança de imobiliária (RN5–RN7)
  • [x] apps/web — IncomeRatioBadge (RN8)
  • [x] apps/web — DocumentsStep sobre o checklist parametrizado (RN9, RN10)
  • [x] apps/web — show.tsx no layout do AnaliseVer, com HistoryTimeline e a decisão no fim
  • [x] apps/web — responsivo no mesmo passo: coluna única, botões empilhados, 16px
  • [x] apps/e2e — credit-analyses-form-steps.spec.ts e ajuste dos testids aposentados
  • [x] make test (202 passando) e make lint sem apontamento novo — o do web caiu de 37 para 33
  • [x] Validado no browser em desktop e mobile, nos dois temas — 84 verificações + o passo 2 com upload real

9. Decisões em aberto

Nenhuma — as três ambiguidades foram resolvidas na entrevista (escopo das três telas, cadastro inline dos dois registros, e traduzir a regra de 3× para o que o modelo já tem).

Ficam registradas três divergências conscientes em relação ao .jsx do design system, para não parecerem esquecimento na revisão:

  1. Sem "Restrições em consulta de crédito" — o campo não existe e não há bureau.
  2. Enviar com pendência vai para SUBMITTED, não para "Aguardando documentos" (RN10).
  3. "Salvar rascunho" continua indo para o detalhe, não para a lista (RN14). O destino do protótipo é encanamento do FluxoAnalise, não regra de design — e o operador que acabou de criar quer ver o que criou. Mudar isso quebraria credit-analyses-mobile.spec.ts sem ganho.

10. Registro

2026-08-03 — as três telas entregues. A API não mudou de contrato, como previsto.

O FormularioAnalise do design system virou um componente (CreditAnalysisForm) com mode: "create" | "edit", e create.tsx/edit.tsx viraram três linhas cada. A unificação já se pagou: as duas cópias antigas guardavam a renda em formatos diferentes (formatada no create, normalizada no edit), e o buildParticipants do create carregava um comentário explicando o NaN que isso produzia.

A imobiliária não é campo da análise — é escopo. O DS mostra "Imobiliária parceira" como dado do formulário; no app o partner_agency_id da análise vem do current_user.partner_agency_id (credit_analysis_service.create), e quem carrega o vínculo real é o imóvel. Então o seletor faz duas coisas concretas: filtra a busca de imóvel e fornece o partner_agency_id obrigatório do imóvel criado inline (RN5). Com uma imobiliária só na resposta — o caso do AGENCY_USER — ela já vem escolhida.

Cinco defeitos encontrados, três deles pré-existentes:

  1. data-testid duplicado no detalhe. Renderizei as ações duas vezes (cabeçalho e rodapé) escondendo por breakpoint CSS. Dois elementos com o mesmo testid violam o modo estrito do Playwright — o credit-analyses-mobile.spec.ts teria falhado. Voltou a useIsMobile(), renderizando num lugar só. Só o browser pegou: tsc e build passaram limpos.
  2. onFocus que reescrevia o valor do campo de dinheiro. O padrão herdado (onFocus={() => setDisplay(raw)}) re-renderiza no foco e descarta a seleção que o fill do Playwright — e o "selecionar tudo e digitar" de um humano — acabou de fazer: o texto novo é inserido em vez de substituir. Preencher 3000 sobre "R$ 9.000,00" dava R$ 90.003.000,00. Trocado por e.target.select(), que faz o mesmo trabalho sem re-render (o parseBRLInput já aceita o valor formatado de volta).
  3. Renda dos participantes extras formatava a cada tecla. fmtBRLInput("3") é "R$ 3,00", então digitar "3000" era impossível. Cada linha ganhou incomeDisplay próprio, formatando no blur — como os demais campos de dinheiro.
  4. persons:update_sensitive nunca existiu (pages/renters/form.tsx). A API concede renters:update_sensitive; o flag era sempre falso, e CPF/CNPJ/RG ficavam somente-leitura na edição até para quem tem a permissão. Terceira ocorrência do mesmo defeito de prefixo (a spec 0013 achou persons:delete duas vezes) — vale uma varredura por persons: em spec própria.
  5. O analista era expulso da tela de análise de crédito. GET /settings exige o papel ADMIN_TENANT, e o handle403 do api-fetch faz window.location.href = "/unauthorized" em qualquer GET que dê 403 — antes de quem chamou, então nem o .catch(() => {}) que já existia impedia. Resultado: ANALYST e OPERATOR batiam em /unauthorized ao abrir criar, editar e o detalhe — a tela em que o analista decide. Pré-existente (o create.tsx antigo fazia a mesma chamada), e invisível ao e2e porque os specs de decisão dirigem a tela como gestor/socio. Corrigido com apiReq(..., { tolerate403: true }): as configurações alimentam só o resumo financeiro, e um painel acessório não pode evitar que o dono da tarefa entre na tela.

Instrumentação: data-testid em select do MUI não pode ir no htmlInput. Num TextField select esse slot é o espelho escondido (aria-hidden, tabindex=-1) — um elemento que nenhum clique alcança — e o MUI 9 recusa data-* em slotProps.select no nível do tipo. Os cinco selects passaram por um SelectField local (FormControl + InputLabel + Select), com o testid na raiz clicável: é o que o FilterSelect das listas já fazia.

O passo 2 é o checklist da spec 0010, não a lista fixa do .jsx. E cada documento vai para a entidade que o possui (inquilino, imóvel) — só a área ADDITIONAL grava contra a análise, como a spec 0008 fixou. Validado com upload real: exigir um tipo em Configurações fez a linha aparecer, anexar zerou a pendência, remover a devolveu.

Ícones: cinco entraram no registro (file-dashed, floppy-disk, lock-simple, paperclip, plus-circle) — os que o DS usa nestas telas.

Um bug de interface corrigido de passagem: o cabeçalho do detalhe mostrava Análise {{code}} literal, resto da remoção de i18n da spec 0009. A variável code existia e nunca era usada.

Validação. tsc limpo; build de produção passando. 84 verificações num script Playwright cobrindo desktop (1440) e mobile (375) nos dois temas: passo trancado, busca no servidor, indicador de renda nos dois tons, edição com os passos livres, detalhe sem placeholder, ações dentro da largura, zero rolagem horizontal, nenhum campo focável abaixo de 16px (o piso do fix 0002 sobreviveu). Mais dois roteiros: o passo de documentos com upload real e o cadastro inline ponta a ponta (inquilino + imóvel + análise em uma gravação, com a observação digitada antes intacta).

Um detalhe de ambiente que custou tempo e vale registrar: o Caddy do dev-remote roda em 127.0.0.1:8180 (modo co-hospedado) e roteia por Host (dev.garantora.com.br) — com Host: localhost ele responde 200 com corpo vazio, o que parece bug da API e não é. A :8080 da máquina é de outro projeto.

A suíte e2e pegou quatro coisas que a validação no browser não pegou — três minhas, uma latente:

  1. Adicionar participante precisava persistir na hora. Eu deixei a linha só na tela até o "Salvar alterações"; o credit-analyses.spec.ts (spec 0003) afirma a persistência logo depois de confirmar, e estava certo: quem adiciona um corresponsável, navega e volta perderia o trabalho sem aviso. Adicionar e remover voltaram a valer na hora quando a análise já existe — em inclusão não há para onde gravar, então a linha fica local e entra no POST junto. A fila de remoções pendentes deixou de existir.
  2. POST /decision exige corpo — sem ele a API devolve 422 (VALIDATION_ERROR), e o meu cenário de PENDING_DOCS falhava nos três browsers antes de chegar à tela.
  3. Apagar o PDF temporário logo depois do setFiles sobe arquivo vazio no webkit (que lê o arquivo de forma tardia). Chromium e firefox passavam; só o webkit reprovava, e o sintoma — linha "Não anexado" — parecia bug da tela. O spec agora espera o 201 do POST /documents antes de remover o temporário.
  4. A contagem da lista de imóveis nunca foi sensível a separador de milhar. O tenant de e2e cruzou 1.054 imóveis e o rodapé passou a formatar em pt-BR, enquanto o teste montava de 1054. Defeito latente do spec 0013, revelado agora porque esta spec (e os roteiros de validação) criaram imóveis. Corrigido comparando sem separador dos dois lados. Vale registrar que o tenant de e2e acumulou lixo suficiente para cruzar mil registros — uma limpeza é decisão do usuário, não desta spec.

Uma quinta falha, properties.spec.ts no firefox (opção "SP" do seletor de UF não aparecendo em 30s), não reproduziu na segunda rodada: flake, não regressão.

A rodada de fechamento (279 passando, 1 falha) expôs uma corrida real da API — anterior a esta spec e fora do escopo dela. O POST /credit-analyses devolveu 500 com ForeignKeyViolationError: "Key (property_id)=(56347edd…) is not present in table properties". O imóvel existe no banco (created_at 18:15:00.045, deleted_at nulo) — ele só não estava visível no instante da chamada seguinte. A causa está no get_db:

yield session
await session.commit()   # roda no teardown da dependência

Em FastAPI o código depois do yield executa depois de a resposta ser entregue. Ou seja: a API responde 201 com o id antes de a transação comitar, e um cliente que emenda a escrita dependente (criar imóvel → criar análise, exatamente o que o setup do e2e faz) pode não achar a linha. Não é flake de teste — é o contrato da API. O teste passou sozinho no reteste, o que confirma a natureza de corrida.

Corrigir isso significa mover a fronteira transacional para antes da resposta, o que atravessa todos os endpoints; é /fix próprio, não emenda desta spec.

make test: 202 passando (eram 185 na última aferição do CLAUDE.md; +2 desta spec). Lint sem apontamento novo nos arquivos tocados — o do web caiu de 37 para 33, porque os arquivos antigos levaram os deles.

Cobertura de e2e: credit-analyses-form-steps.spec.ts com 7 cenários (CA1/CA2, CA5–CA7, CA8, CA13, CA3/CA9/CA10, CA11, CA3-PENDING_DOCS) e um cenário mobile novo (CA12) no credit-analyses-mobile.spec.ts. Os três usos dos testids aposentados (credit-analysis-tenants-input, credit-analysis-property-input) foram traduzidos para os blocos de seleção. O cenário que mexe na configuração de documentos do tenant — que é compartilhada — desfaz no finally.