Pular para conteúdo

0025 — Organização da visualização da análise de crédito

Status CONCLUÍDA
Branch spec/0025-visualizacao-da-analise-de-credito
Criada em 2026-08-08
Apps afetados api · web · e2e

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

1. Problema

A tela de ver uma análise de crédito é onde o analista decide se a garantidora assume o risco. Hoje ela não está organizada em torno dessa decisão — está organizada em torno do cadastro. Quem abre a tela encontra, nesta ordem: inquilino, imóvel, situação financeira, imobiliária parceira, documentos, histórico, observações, parametrização financeira, contrato e, só então, o parecer.

Cinco dores concretas:

  1. Não dá para saber onde a análise está sem ler. O estado aparece como um chip solto ao lado do código. Um chip diz onde está; não diz o que já passou nem o que falta. Para responder "essa análise já foi para o analista?" o operador desce até o histórico, no meio da página, e lê a última linha.

  2. O veredito da política aparece depois de tudo. A leitura "dentro/fora da política" é um <Alert> no fim da página (show.tsx:569), e só existe quando a análise está em estado decidível. É a informação que decide o caso, e está atrás de sete painéis.

  3. A informação do risco está partida em três lugares. A razão renda/Pacote está numa linha do painel "Situação financeira"; a recomendação automática da API (credit_recommendation) está escondida dentro do painel de parecer, no fim; e o alerta de política está num terceiro ponto. São três leituras da mesma pergunta — "essa renda cobre esse aluguel?" — em três alturas da tela.

  4. O que falta de documento obrigatório só aparece na hora de aprovar. O checklist completo mostra tudo (entregue, faltando, vencido) misturado por área, e o bloqueio da aprovação (spec 0008, RN9) só se revela como alerta dentro do painel de parecer, quando o analista já está com o dedo no botão. A pendência devia ser a segunda coisa que ele lê, não a última.

  5. "Iniciar análise" está enterrado. Para uma análise SUBMITTED, a única ação possível é começá-la — e ela é um <Button> pequeno no meio do painel de parecer, no fim da página (DecisionPanel.tsx:215), enquanto o topo da tela oferece só "Cancelar".

Além disso, a tela tem dois painéis financeiros separados — "Situação financeira" e a "Parametrização financeira" colapsável — com um painel de imobiliária e um de observações entre eles.

2. Objetivo

A visualização passa a ser lida de cima para baixo na ordem da decisão: onde a análise está, se cabe na política, o que falta, quanto custa — depois quem é e o que aconteceu. As ações do momento ficam no topo, no cabeçalho, e não no meio da página.

3. Fora de escopo

  • A lista e o formulário de análise de crédito. Só a tela de ver (show.tsx) muda. O formulário de dois passos (spec 0015) fica como está.
  • A máquina de estados. Nenhuma transição nova, nenhuma regra de transição alterada. A linha do tempo desenha o que já existe.
  • min_income_ratio como parâmetro do tenant. Continua constante no web (MIN_INCOME_RATIO = 3), como a spec 0015 deixou. Virar TenantSettings é spec própria — e agora com mais motivo, porque o mínimo passa a ser o número mais visível da tela.
  • A regra de decisão. O que aprovar/reprovar/pendenciar faz, valida e envia não muda. Muda onde os botões ficam e onde os campos que os habilitam são preenchidos.
  • Consulta a bureau de crédito / score. Não há campo nem integração. O quadro de política mostra o que o sistema sabe: renda declarada e a recomendação da API.
  • Menu de estouro (⋮) no PageHeader. O teto de três ações da spec 0020 (RN7) fica de pé; a RN9 resolve o único estado que estoura sem criar o menu.
  • As telas de contrato e cobrança. O painel "Contrato vinculado" só muda de posição.

4. Regras de negócio

Linha do tempo (bloco 1)

  • RN1. A linha do tempo é um trilho fixo de quatro etapas — Em edição → Enviado → Em análise → Aprovado —, com a etapa atual destacada, as anteriores marcadas como cumpridas e as seguintes apagadas. Sem datas: a cronologia (quando, quem, por quê) é do histórico, no fim da página, e repeti-la no topo faria a mesma informação aparecer duas vezes com precisões diferentes.

  • RN2. Os três estados fora do trilho feliz não ganham nó próprio — ocupam o nó em que a análise de fato está:

  • PENDING_DOCS ocupa o nó 3 com o rótulo "Aguardando documentos" e cor de alerta. É onde a análise está, e ela volta para Em análise sem andar no trilho.
  • REJECTED ocupa o nó 4 com o rótulo "Reprovado" e cor de erro.
  • CANCELLED ocupa o nó 4 com o rótulo "Cancelado", neutro, e todo o trilho fica esmaecido — é o único estado em que nada mais vai acontecer por caminho nenhum.

  • RN3. No mobile (375px) os quatro rótulos não cabem lado a lado. O trilho mostra os quatro nós numerados e o rótulo apenas da etapa atual, numa linha abaixo, no formato Etapa 3 de 4 · Em análise. Sem rolagem horizontal — nem no trilho, nem na página.

Ordem da página

  • RN4. A visualização passa a ter esta ordem, de cima para baixo:
# Bloco Observação
1 Linha do tempo RN1–RN3
2 Parecer da análise RN12–RN13, RN16
3 Política de contratação RN5–RN7
4 Documentos obrigatórios faltando RN8
5 Resumo financeiro RN9
6 Inquilino · Imóvel · Imobiliária painéis de hoje, só reposicionados
7 Anexos · Checklist completo · Observações · Contrato vinculado idem
8 Histórico RN14

O parecer subiu para o bloco 2 num ajuste posterior à primeira implementação. Ele estava no 7, atrás de seis blocos de leitura — e é o que o analista escreve e o que o gestor abre a tela para ler. Os botões de decisão continuam no cabeçalho (RN10); o que mudou de lugar foi o texto.

Blocos que hoje não aparecem em certos estados continuam não aparecendo (o painel de contrato só existe com contrato; o de parecer só a partir de SUBMITTED). A ordem relativa dos que aparecem é sempre esta — nenhum bloco muda de posição conforme o estado.

Política de contratação (bloco 3)

  • RN5. O quadro mostra, sempre nesta ordem: renda declarada total (soma de inquilinos e corresponsáveis), Pacote Locatício, e a razão entre os dois com o mínimo ao lado (2,3× (mínimo 3,0×)). O veredito DENTRO / FORA fica no cabeçalho do quadro, e é razão ≥ 3,0×.

  • RN5a. O título do quadro acompanha o veredito, não só o selo: FORA pinta rótulo e selo de vermelho, DENTRO de verde. Mesma regra do quadro de documentos (RN8a) e pelo mesmo motivo — só o selo tingido deixava o bloco parecendo um painel neutro qualquer numa varredura rápida da página. Sem renda declarada não há veredito a dar, e o título fica neutro.

  • RN6. Quando existe Decision (isto é, de IN_ANALYSIS em diante), o quadro mostra também o chip da recomendação automática da API (credit_recommendation: Aprovação recomendada / Análise manual / Rejeição sugerida) com o recommendation_notes. Esse chip sai do painel de parecer, onde vive hoje (DecisionPanel.tsx:231) — a recomendação é leitura da política, não parte do parecer. Sem Decision (DRAFT, SUBMITTED), o quadro mostra só a razão local.

  • RN7. A razão da API passa a incidir sobre o Pacote Locatício. Hoje compute_credit_summary recebe credit_analysis.rental_value — só o aluguel — nos dois pontos que a chamam (decision_service.py:115 e credit_analysis_participant_service.py:214), enquanto o web calcula sobre o Pacote (aluguel + condomínio + IPTU), como a spec 0006 definiu para todo o resto da precificação. O serviço passa a usar financial_rental_package, com fallback para rental_value nas análises antigas sem snapshot. Os limiares (3× e 2×) não mudam. Decidido em 2026-08-08 (§9): alinhar.

Por que não dá para deixar como está: as RN5 e RN6 colocam as duas contas no mesmo quadro, e o divisor da API é sempre menor — a razão dela é sempre a mais otimista. Numa análise com aluguel R$ 2.500, condomínio R$ 450, IPTU R$ 150 e renda declarada de R$ 8.000, o quadro exibiria o veredito FORA (2,6×, sobre o Pacote de R$ 3.100) logo acima do chip "Aprovação recomendada" da API (3,2×, sobre o aluguel). A faixa em que os dois se contradizem é 3 × aluguel ≤ renda < 3 × Pacote — aqui, toda renda entre R$ 7.500 e R$ 9.300.

Qual das duas está certa: a do Pacote. É sobre ele que incidem mensalidade, LMI e Custos de Saída (pricing.py), e é ele que a garantidora cobre no sinistro. Medir o risco sobre uma base menor do que a que se cobre subestima a exposição. A API ficou para trás porque credit.py é anterior à spec 0006 e não foi revisitada — as docstrings ainda dizem "regra 3× aluguel"; o web foi ajustado na spec 0015 (RN8) e a API não.

Consequência: análises com encargos informados podem mudar de recomendação ao serem reabertas — de APPROVE para MANUAL_ANALYSIS, por exemplo. Como a recomendação é recalculada só na criação da Decision e na atualização de renda, as já decididas mantêm o valor gravado. Não há dado em produção (ver CLAUDE.md, "Estágio do produto").

  • RN8a. O título do quadro acompanha o tom do estado, não só o selo: com pendência, rótulo e selo ficam vermelhos. Só o selo tingido deixava o bloco parecendo um painel neutro qualquer numa varredura rápida da página.

  • RN8. O quadro de documentos lista apenas os obrigatórios não satisfeitos — o blocking do checklist (spec 0008) —, cada um com o nome do documento, de quem é e o motivo (Falta ou Vencido). Sem pendência, o quadro mostra o estado positivo ("Nenhum documento obrigatório pendente") em vez de sumir: a ausência do quadro seria ambígua entre "não falta nada" e "o tenant não parametrizou nada". Se o tenant não parametrizou exigência nenhuma (RN15 da spec 0008), o quadro diz isso.

O checklist completo por área — entregues, opcionais, vencidos — continua existindo, no bloco 7, junto dos anexos. O quadro do topo é o resumo do que impede.

Resumo financeiro (bloco 5)

  • RN9. Os dois painéis financeiros de hoje viram um: "Situação financeira" (aluguel, encargos, Pacote, prazo, vigência) com a parametrização da spec 0010 — taxa, LMI, Custos de Saída, pagamento inicial, carências — no mesmo quadro. As linhas de renda declarada e razão saem daqui: foram para o quadro de política (RN5) e não se repetem.

  • RN9a. O resumo financeiro não colapsa nesta tela. O recolhimento existe para o formulário no mobile (spec 0007, RN13), onde o resumo empurraria os campos para baixo. Numa tela de leitura ele só escondia a metade mais informativa do bloco — taxa, LMI, custos de saída — atrás de um clique que ninguém dava. O collapsible do componente continua existindo, para o formulário; a visualização não o usa.

  • RN9c. Nenhuma linha do quadro tem peso próprio. O "Pacote Locatício" chegou a sair em 16px/700 contra os 13px/600 das vizinhas e lia como um bloco preto no meio do quadro. Ele já é o número âncora pela repetição — reaparece no resumo logo abaixo e no quadro de política.

  • RN9b. O quadro fica em destaque, tingido de primary (navy). É a cor do dinheiro no app — o FinancialSummaryPanel já tingia o próprio cabeçalho de navy antes desta spec —, então o destaque não inventa uma cor: promove a que já estava lá.

Ações no cabeçalho

  • RN10. As ações do topo passam a ser, por estado:
Estado Principal Secundária Terciária
DRAFT Enviar para análise Editar Cancelar
SUBMITTED Iniciar análise Cancelar —
IN_ANALYSIS Aprovar Reprovar Solicitar documentos
PENDING_DOCS Aprovar Reprovar Resolver pendência
APPROVED Criar contrato (ou Visualizar contrato) — —
REJECTED / CANCELLED Visualizar contrato, se houver — —

DRAFT e os estados terminais ficam como estão hoje. O que muda é do SUBMITTED ao PENDING_DOCS.

  • RN11. PENDING_DOCS é o único estado com quatro candidatas (Aprovar, Reprovar, Resolver pendência, Cancelar) e o teto do PageHeader é três. "Cancelar" sai do topo e passa a viver no rodapé do painel de parecer, como ação de texto de baixo destaque, preservando o data-testid. É a de menor frequência das quatro: cancelar uma análise que já está com o analista, esperando documento, é o caso raro — e a destrutiva é justamente a que menos merece o alvo mais fácil do topo.

  • RN12. O botão "Aprovar" do topo abre o diálogo de aprovação, que passa a carregar o que habilita a decisão: taxa de setup arbitrada, a confirmação da cláusula obrigatória e — quando houver bloqueio documental e o usuário for gestor — a justificativa da exceção (spec 0008, RN10). O botão do topo fica sempre habilitado; quem valida é o confirmar do diálogo, que só habilita com a cláusula marcada (e com a justificativa preenchida, quando exigida).

A razão: um botão desabilitado no cabeçalho, com o motivo três telas abaixo, é pior que um clique a mais. Hoje o motivo está ao lado do botão porque os dois estão no mesmo painel; levando o botão ao topo sem levar o motivo, o analista veria "Aprovar" cinza sem saber por quê.

  • RN13. O painel de parecer (bloco 2) fica com o que é redação e registro: parecer do analista, notas internas, "Salvar rascunho", os dados de quem decidiu e quando, e a leitura do que foi decidido nos estados terminais. Sem o chip de recomendação, que foi para a política (RN6).

  • RN13b. As ações do momento se repetem no cabeçalho do painel, contornadas. É duplicação deliberada: o topo é onde elas têm hierarquia (RN10), e aqui elas são a alternativa ao alcance da mão de quem acabou de escrever o parecer e não quer voltar ao topo para decidir. Todas contornadas — inclusive a que é preenchida no cabeçalho —, porque um botão preenchido a meia página disputaria a hierarquia com o do topo. A lista é derivada da mesma que alimenta o cabeçalho, não montada de novo, para as duas filas não poderem divergir quando um estado ganhar ou perder ação; e cada cópia recebe o data-testid do original com sufixo -panel, porque dois elementos com o mesmo atributo violam o modo estrito do Playwright.

  • RN13a. O painel de parecer fica tingido de highlight (laranja da marca), para sair da fila de painéis neutros. Com ele no bloco 2, o destaque é o que separa "o que se lê primeiro" de "o que se consulta": a página passa a ter três cores de peso — laranja no parecer, navy no financeiro (RN9b) e o semáforo da política e dos documentos —, e todo o resto em teal neutro. O rótulo usa highlightInk (5,59:1), não highlight.main: o laranja vivo é cor de preenchimento e não passa como texto sobre o próprio tingimento.

  • RN13c. No mobile as ações do painel são ícone, com alvo de 44px — a mesma medida do topo e da gaveta. Com rótulo, três botões envolviam em duas linhas e empurravam o parecer para baixo logo no segundo bloco da página.

Sem rótulo, a cor deixa de ser enfeite: é ela que separa aprovar de reprovar de pendenciar. Por isso cada ação carrega um tom semântico — success em Aprovar, error em Reprovar, warning em Solicitar documentos —, aplicado por sx para vencer a regra do tema que pinta toda variant="outlined" de teal (design-system/theme/components.ts: "Secundário e ghost da marca são teal"). O tom vale nos dois viewports: teal no desktop e colorido no mobile seria a mesma ação com duas identidades.

A tinta é o token *Ink, não palette[tom].main — a mesma lição da spec 0019: a cor viva é de preenchimento e não alcança contraste como texto ou glifo. Medido sobre o laranja do cabeçalho, o warning.main (#F59E0B) dá 1,94:1 e o ícone de "Solicitar documentos" sumiria; warningInk dá 5,04:1.

O cabeçalho da página não muda. Lá a regra de forma é a da spec 0020 — principal preenchida em highlight, as outras contornadas —, e o tom é campo extra que o PageHeader ignora.

  • RN14. O histórico é sempre o último bloco, em largura inteira. Hoje ele divide um grid de duas colunas com os documentos (show.tsx:506), o que o espreme no desktop e o joga para o meio da página no mobile.

  • RN16. No checklist completo (bloco 7), obrigatório vem antes de opcional, e todo item opcional é pintado em info — qualquer que seja o status dele. A ordem que vinha da API era a da parametrização, misturando os dois; e um opcional faltando pintado de vermelho competia com o obrigatório que de fato bloqueia a aprovação, sendo que são duas conversas diferentes: uma impede a decisão, a outra não. A ordenação é estável, então a parametrização do tenant segue mandando no desempate dentro de cada grupo.

  • RN15. Permissões. As ações de decisão no topo só aparecem para quem tem a permissão correspondente: decisions:create para "Iniciar análise" e decisions:update para aprovar, reprovar e solicitar documentos — lidas de user.permissions, que o useCurrentUser já traz. Hoje a tela não checa nada e deixa a API recusar; com os botões no cabeçalho, um AGENCY_USER veria "Aprovar" como ação principal da tela. A API continua sendo a autoridade — a checagem no web é sobre o que se oferece.

5. Impacto por app

apps/api

Uma única mudança de comportamento, sem migration e sem mudança de contrato HTTP:

  • app/services/decision_service.py — as duas chamadas a compute_credit_summary passam a receber o Pacote Locatício no lugar de rental_value (RN7): credit_analysis.financial_rental_package or credit_analysis.rental_value. O mesmo em app/services/credit_analysis_participant_service.py, que recalcula o resumo quando a renda de um participante muda.
  • Docstrings — app/services/credit.py (módulo e função) e o model app/models/decision.py dizem "regra 3× aluguel"; passam a dizer Pacote Locatício, citando a spec 0006. O parâmetro rental_value da função vira package_value.
  • Swagger — nenhum endpoint muda de assinatura. A description de POST /credit-analyses/{id}/decision menciona o resumo automático; ajustar a base de cálculo no texto.
  • Testes — tests/ que exercitam compute_credit_summary com condomínio/IPTU passam a esperar a razão sobre o Pacote. Um teste novo cobre a diferença: mesma renda e mesmo aluguel, com e sem encargos, recomendações diferentes.

apps/web

Toda a mudança é em src/pages/credit-analyses/.

Componentes novos (em components/):

  • StatusTimeline.tsx — o trilho de quatro etapas (RN1–RN3).
  • PolicyPanel.tsx — o quadro de política (RN5–RN6), absorvendo o CreditRecommendationBadge que hoje vive dentro do DecisionPanel.
  • BlockingDocumentsPanel.tsx — o resumo do que falta (RN8).
  • ApproveDialog.tsx — o diálogo de aprovação com setup, cláusula e justificativa (RN12).

Componentes alterados:

  • show.tsx — reordenação (RN4), composição das ações do cabeçalho (RN10–RN11, RN15), fusão dos dois painéis financeiros (RN9), histórico em largura inteira (RN14).
  • DecisionPanel.tsx — perde os botões de decisão, o chip de recomendação e os campos de aprovação; ganha o "Cancelar" de PENDING_DOCS (RN11). Fica com parecer, notas, rascunho e a leitura do decidido (RN13).
  • FinancialSummaryPanel.tsx — deixa de ser painel próprio e vira o corpo colapsável do resumo financeiro (RN9).

data-testid — preservados (o e2e depende): credit-analysis-detail-financial, credit-analysis-financial-summary-toggle, credit-analysis-financial-summary-body, credit-analysis-doc-checklist, credit-analysis-doc-checklist-blocking, credit-analysis-history, credit-analysis-detail-renter, credit-analysis-detail-property, credit-analysis-submit-button, credit-analysis-edit-button, credit-analysis-cancel-button, credit-analysis-create-contract-button, credit-analysis-view-contract-button, decision-approve-button (agora no cabeçalho), decision-approve-setup-input e decision-approve-clause-checkbox (agora dentro do diálogo).

Novos:

data-testid Onde
credit-analysis-timeline o trilho (RN1)
credit-analysis-timeline-step-{n} cada nó, 1–4
credit-analysis-timeline-current o nó da etapa atual
credit-analysis-policy-panel o quadro de política (RN5)
credit-analysis-policy-verdict DENTRO / FORA
credit-analysis-policy-ratio a razão calculada
credit-analysis-policy-recommendation o chip da API (RN6)
credit-analysis-blocking-docs o quadro do que falta (RN8)
credit-analysis-blocking-doc-{i} cada pendência
credit-analysis-detail-documents contêiner do bloco 7 — âncora de ordem
credit-analysis-decision-panel o painel de parecer (RN13)
credit-analysis-start-analysis-button "Iniciar análise" no cabeçalho
decision-reject-button "Reprovar" no cabeçalho
decision-pend-button "Solicitar documentos" no cabeçalho
credit-analysis-approve-dialog o diálogo de aprovação (RN12)
credit-analysis-approve-confirm o confirmar do diálogo
decision-approve-justification-input migra do painel para o diálogo

O credit-analysis-policy-alert de hoje sai — o <Alert> do fim vira o PolicyPanel do topo. O e2e que o usa é ajustado (ver abaixo).

Sider: sem impacto — não há módulo novo.

Responsividade & PWA: a página já é coluna única no mobile; a reordenação vale para os dois viewports. O ponto novo é o trilho (RN3). Os grids de duas colunas dos blocos 5 e 6 continuam colapsando em uma. Sem impacto no PWA — nenhum asset novo, nenhuma rota nova, nenhum comportamento offline.

apps/e2e

  • credit-analyses.spec.ts — os dois testes de aprovação mudam de caminho: o botão decision-approve-button agora está no cabeçalho e abre o diálogo; a cláusula e a taxa de setup são preenchidas dentro dele, e quem fica desabilitado até a cláusula é o credit-analysis-approve-confirm. As asserções de resultado (status APPROVED, approved_initial_fee_value propagando ao contrato) não mudam.
  • credit-analyses-form-steps.spec.ts — a asserção sobre credit-analysis-policy-alert passa a mirar credit-analysis-policy-panel / credit-analysis-policy-verdict.
  • credit-analysis-documents.spec.ts — o checklist completo continua com o mesmo testid, agora no bloco 6; acrescentar asserção sobre credit-analysis-blocking-docs.
  • Spec nova credit-analysis-detail-order.spec.ts — cobre RN1–RN4: o trilho marca a etapa certa em cada estado, e os blocos aparecem na ordem da RN4 (comparando a posição vertical dos data-testid), nos dois viewports.
  • Seeds: nenhum novo. Os cenários usam o setupCreditAnalysisInDecision que já existe; para o caso da RN7, uma análise com condo_fee/property_tax preenchidos.

6. Contrato de API

Nenhuma assinatura muda. A única alteração é o valor calculado de dois campos da resposta de GET /api/v1/credit-analyses/{id}/decision:

GET /api/v1/credit-analyses/{id}/decision
Authorization: Bearer <token>
{
  "income_to_rent_ratio": "2.58",
  "credit_recommendation": "MANUAL_ANALYSIS",
  "recommendation_notes": "Renda total R$ 8.000,00 representa 2.58× o Pacote Locatício (mínimo recomendado: 3×)."
}

Antes da RN7, a mesma análise (aluguel R$ 2.500, condomínio R$ 450, IPTU R$ 150, renda R$ 8.000) devolvia "3.20" e APPROVE — o divisor era só o aluguel. É a inversão de recomendação que a RN7 corrige, e a razão de ela existir. O nome do campo (income_to_rent_ratio) é mantido para não quebrar o web sem ganho; o texto de recommendation_notes passa a dizer "Pacote Locatício".

Erros previstos: nenhum novo. Os da aprovação seguem os da spec 0008 (MISSING_REQUIRED_DOCUMENTS, JUSTIFICATION_REQUIRED, HTTP 403 para o analista) e da spec 0003 (MANDATORY_CLAUSE_NOT_CONFIRMED, RATE_OUT_OF_RANGE) — só passam a ser exibidos dentro do diálogo da RN12, não mais no painel do fim da página.

7. Critérios de aceite

  • [x] CA1. Dado uma análise em IN_ANALYSIS, quando abro a visualização, então o primeiro bloco da página é a linha do tempo, com quatro nós, o terceiro destacado como atual e os dois primeiros marcados como cumpridos.
  • [x] CA2. Dado uma análise em PENDING_DOCS, então o terceiro nó do trilho aparece com o rótulo "Aguardando documentos" em cor de alerta, e nenhum quinto nó é criado.
  • [x] CA3. Dado uma análise CANCELLED, então o trilho inteiro aparece esmaecido e o último nó diz "Cancelado".
  • [x] CA4. Em 375px, a linha do tempo cabe sem rolagem horizontal e mostra o rótulo só da etapa atual, no formato "Etapa N de 4 · "; a página inteira tem scrollWidth - clientWidth == 0.
  • [x] CA5. Em qualquer estado, a ordem vertical dos blocos é a da RN4 — verificável comparando a posição dos data-testid na página.
  • [x] CA6. Dado renda declarada de R$ 8.000 e Pacote Locatício de R$ 3.100 (aluguel R$ 2.500 + condomínio R$ 450 + IPTU R$ 150), então o quadro de política mostra "2,6× (mínimo 3,0×)" e o veredito FORA. É o mesmo cenário de CA18 e CA19 — um fixture só atravessa os três.
  • [x] CA7. Dado a mesma análise já em IN_ANALYSIS, então o quadro de política mostra também o chip da recomendação da API, e esse chip não aparece mais no painel de parecer.
  • [x] CA8. Dado uma análise em DRAFT (sem Decision), então o quadro de política aparece com a razão e sem o chip de recomendação.
  • [x] CA9. Dado dois documentos obrigatórios faltando, então o quadro de documentos lista os dois com nome, pessoa e motivo; dado nenhum faltando, o quadro diz que não há pendência em vez de sumir.
  • [x] CA10. A página tem um bloco de resumo financeiro, com a parametrização dentro dele, e nenhuma linha de renda ou razão — que estão só no quadro de política.
  • [x] CA21. O resumo financeiro aparece aberto, sem controle de colapsar, e o quadro está tingido de navy (RN9a/RN9b).
  • [x] CA22. Com pendência documental, o título do quadro de documentos obrigatórios está vermelho, não só o selo (RN8a).
  • [x] CA26. As ações do momento aparecem duas vezes — no cabeçalho da página e no cabeçalho do painel de parecer —, cada uma com um data-testid só, e a cópia do painel dispara a mesma coisa que a do topo (RN13b).
  • [x] CA28. Em 375px, as ações do painel de parecer são botões de ícone de 44px, uma única linha ao lado do título, com verde em Aprovar, vermelho em Reprovar e âmbar em Solicitar documentos; no desktop os mesmos tons acompanham os rótulos (RN13c).
  • [x] CA27. No quadro de situação financeira, nenhuma linha tem tamanho ou peso diferente das demais (RN9c).
  • [x] CA25. Com a análise fora da política, o título do quadro de política está vermelho; dentro, verde; sem renda declarada, neutro (RN5a).
  • [x] CA23. O parecer é o segundo bloco da página, logo abaixo do trilho, e está tingido de laranja (RN13a).
  • [x] CA24. No checklist completo, os itens obrigatórios aparecem antes dos opcionais, e todo opcional está em info — inclusive um opcional faltando (RN16).
  • [x] CA11. Dado uma análise em SUBMITTED, então "Iniciar análise" é a ação principal do cabeçalho, e o painel de parecer não oferece mais esse botão.
  • [x] CA12. Dado uma análise em IN_ANALYSIS, então o cabeçalho traz Aprovar, Reprovar e Solicitar documentos, e o painel de parecer não traz nenhum dos três.
  • [x] CA13. Dado uma análise em PENDING_DOCS, então o cabeçalho traz três ações (Aprovar, Reprovar, Resolver pendência) e "Cancelar" aparece no rodapé do painel de parecer.
  • [x] CA14. Clicar em "Aprovar" no cabeçalho abre o diálogo; o confirmar do diálogo está desabilitado até a cláusula obrigatória ser marcada; marcada, aprova e a API devolve APPROVED.
  • [x] CA15. Dado documento obrigatório faltando e usuário ANALYST, então o diálogo informa o impedimento e não deixa confirmar; dado ADMIN_TENANT, o diálogo pede a justificativa e confirma com ela preenchida.
  • [x] CA16. Dado um usuário sem decisions:update, então o cabeçalho não oferece Aprovar, Reprovar nem Solicitar documentos.
  • [x] CA17. O histórico é o último bloco da página, em largura inteira, em desktop e mobile.
  • [x] CA18. Dado aluguel R$ 2.500, condomínio R$ 450, IPTU R$ 150 e renda R$ 8.000, então income_to_rent_ratio devolvido pela API é 2.58 (Pacote R$ 3.100), não 3.20, e credit_recommendation é MANUAL_ANALYSIS, não APPROVE.
  • [x] CA19. Na mesma análise, o veredito do quadro de política (FORA) e o chip de recomendação da API não se contradizem.
  • [x] CA20. make test passa; make lint não ganha apontamento novo nos arquivos tocados.

8. Plano de implementação

  • [x] apps/api — decision_service.py e credit_analysis_participant_service.py passam o Pacote Locatício a compute_credit_summary (RN7), via o helper credit_base() novo em credit.py
  • [x] apps/api — migration Alembic: não se aplica (nenhuma mudança de schema)
  • [x] apps/api — docstrings (credit.py, decision.py, credit_analysis_participant.py, credit_analysis_participant_repo.py) e a description do endpoint de decisão dizendo Pacote Locatício
  • [x] apps/api — testes: tests/test_credit_ratio_pacote.py
  • [x] apps/web — StatusTimeline (RN1–RN3)
  • [x] apps/web — PolicyPanel (RN5–RN6), absorvendo o chip de recomendação
  • [x] apps/web — BlockingDocumentsPanel (RN8)
  • [x] apps/web — fusão dos painéis financeiros (RN9)
  • [x] apps/web — ApproveDialog e a limpeza do DecisionPanel (RN12–RN13)
  • [x] apps/web — ações do cabeçalho por estado, com permissão (RN10–RN11, RN15)
  • [x] apps/web — reordenação do show.tsx e histórico em largura inteira (RN4, RN14)
  • [x] apps/e2e — ajuste de credit-analyses.spec.ts, credit-analyses-form-steps.spec.ts e credit-analysis-documents.spec.ts
  • [x] apps/e2e — spec nova credit-analysis-detail-order.spec.ts
  • [x] make test e make lint — 216 passam (1 falha intermitente conhecida, alheia); ruff sem apontamento novo nos arquivos tocados; tsc limpo
  • [x] Validado no browser em desktop e mobile, contra o dev-remote depois do /devremote-rebuild

9. Decisões em aberto

Nenhuma. A única que havia foi decidida:

  • [x] RN7 — alinhar a razão da API ao Pacote Locatício. Decidido pelo usuário em 2026-08-08: alinhar. A API passa a calcular income_to_rent_ratio e credit_recommendation sobre financial_rental_package, e a spec deixa de ter duas contas para a mesma pergunta. A alternativa considerada — manter a API sobre o aluguel e o quadro de política exibir só a razão do web — foi descartada: esconderia o sintoma sem corrigir a base da recomendação automática, que é o parecer que o analista lê como sendo o do sistema.

10. Registro

Implementada em 2026-08-08. O que divergiu do planejado, e por quê:

Acréscimos não previstos na spec

  • credit_base() em app/services/credit.py. A spec dizia "as duas chamadas passam a receber o Pacote". Foram feitas as duas, mas através de um helper em vez de repetir a expressão nos dois arquivos — foi exatamente "cada ponto escolhe a base por si" que deixou a divergência da RN7 sobreviver desde a spec 0006. Com um helper, o próximo ponto que precisar da base de crédito não tem onde escolher errado.

  • _brl() e o texto de recommendation_notes em pt-BR. A RN6 trouxe o recommendation_notes da API para o quadro de política, no topo da página. Ali ele passou a aparecer ao lado de valores formatados pelo web (R$ 8.000,00), e o formato padrão do Python (R$ 8000.00 representa 2.58×) virou erro à vista. No fim da página, onde o texto ficava antes, ninguém comparava. Corrigido junto porque a RN6 é a causa da exposição — não é conserto oportunista de outra coisa.

  • data-testid="credit-analysis-detail-documents". O plano usava credit-analysis-doc-checklist como âncora do bloco 6 na aferição de ordem, mas o DocumentChecklist renderiza null quando o tenant não parametrizou exigência nenhuma (DocumentChecklist.tsx:63). A ordem da página não pode depender da parametrização do tenant para ser aferível, então o contêiner do bloco ganhou testid próprio. Descoberto na validação por screenshot, antes de o e2e rodar.

  • tolerate403 no getAnalysis. Não previsto, e é um defeito que a spec criou: ao mover a carga da decisão para a página (para alimentar RN6 e RN13 de uma fonte só), ela passou a acontecer em todos os estados, inclusive DRAFT. Um OPERATOR, que não tem decisions:read, receberia 403 — e o handle403 age antes de quem chamou, redirecionando a aplicação inteira. Antes o endpoint só era tocado quando o painel de parecer aparecia. A decisão é contexto opcional da tela, como /settings já era.

Confirmações da validação no dev-remote

Rebuild de api e web, containers recriados, screenshots nos dois viewports:

  • RN7 no ar: a API devolveu income_to_rent_ratio: "2.58" e MANUAL_ANALYSIS para o caso de referência — antes seria 3.20 e APPROVE.
  • RN1/RN2: trilho com quatro nós; PENDING_DOCS relabelou o nó 3 para "Aguardando documentos" em âmbar, sem criar um quinto.
  • RN3/CA4: em 375px, scrollWidth - clientWidth == 0 e só a etapa atual nomeada ("Etapa 3 de 4 · Em análise").
  • RN4/CA5: ordem vertical conferida pela posição real dos oito blocos — nenhum fora de lugar, nos dois viewports.
  • RN10/RN11: PENDING_DOCS mostrou no topo exatamente "Resolver pendência · Reprovar · Aprovar", com "Cancelar" ausente do cabeçalho.
  • Sem erro de console em nenhum dos quatro cenários.

Ferramental

  • make test não roda no host (sem uv); a suíte rodou por docker exec garantora-devremote-api-1 uv run pytest: 217 passam. Numa execução anterior, test_partner_agency_status_filter.py::test_alcanca_quem_esta_fora_da_primeira_pagina falhou — reproduzido 1 em 3 vezes com o código parado, mesma causa do flake que o CLAUDE.md já documenta para test_partner_agency_search.py (empate de created_at no SQLite, desempate por UUID aleatório). Vale registrar que o flake tem dois arquivos, não um.
  • Ruff: nos sete arquivos tocados, 21 apontamentos contra 22 do baseline — nenhum novo, e um a menos (uma linha longa preexistente em credit.py foi quebrada ao ser reescrita). credit.py e o teste novo estão limpos.
  • tsc: limpo. Use ./node_modules/.bin/tsc, não pnpm exec tsc: o corepack desta máquina roda pnpm 11, que ao ser invocado grava um apps/web/pnpm-workspace.yaml só com allowBuilds:. O arquivo não é do repositório, mas o COPY apps/web/ do web.Dockerfile o leva para dentro da imagem, e o pnpm build morre com ERROR packages field missing or empty. Custou um rebuild inteiro do web.

Ajustes pós-implementação (2026-08-08)

Cinco pedidos do usuário depois da primeira entrega, todos de apresentação — nenhuma regra de negócio, nenhuma mudança de API:

  1. Título do quadro de documentos obrigatórios em vermelho junto com o selo (RN8a); depois, a mesma regra para o quadro de política de contratação (RN5a) — os dois quadros com veredito passaram a tingir o rótulo, não só o selo.
  2. Situação financeira em destaque, tingida de primary/navy (RN9b).
  3. Resumo financeiro sem colapsar na visualização (RN9a) — o collapsible continua existindo para o formulário no mobile, que é onde ele foi criado.
  4. Checklist completo com obrigatórios antes dos opcionais, e opcional sempre em info (RN16).
  5. Parecer no bloco 2, logo abaixo do trilho, tingido de highlight/laranja (RN13a).

Efeito colateral do 5: a RN4, os cabeçalhos de seção e o ordem do e2e foram renumerados.

Três pedidos depois disso:

  1. Título do quadro de política acompanha o veredito (RN5a) — vermelho em FORA, verde em DENTRO, neutro sem renda declarada.
  2. Nenhuma linha com peso próprio na situação financeira (RN9c). A cor já era a mesma em todas (rgb(26,26,46)); o que destoava era o "Pacote Locatício" em 16px/700 contra 13px/600 — medido no dev-remote, não deduzido.
  3. As ações do momento repetidas no cabeçalho do painel de parecer (RN13b). Isso reverteu parte da RN13 original ("sem botões de decisão"): eles seguem no topo, com hierarquia, e ganham uma cópia contornada ao alcance de quem acabou de escrever o parecer.

  4. No mobile, as ações do painel viram ícone de 44px, com cor semântica (RN13c) — e a cor passou a valer também no desktop.

Sobre a 9, duas coisas medidas e não deduzidas:

  • O color do descritor é inerte em variant="outlined": o tema pinta toda ela de teal por regra de marca (design-system/theme/components.ts, "Secundário e ghost da marca são teal"). Era por isso que o Reprovar saía teal apesar de color: "error". O painel passa a aplicar a cor por sx, que vence a sobrescrita; o cabeçalho da página segue a regra do tema, inalterado.
  • A primeira versão usou palette[tom].main e o âmbar mediu 1,94:1 sobre o laranja do cabeçalho — o ícone de "Solicitar documentos" sumia. Trocado pelos tokens *Ink, que a spec 0019 criou justamente para "texto ou glifo colorido": agora 7,10 · 5,93 · 4,55. A medição virou parte do script de validação, para não depender de olhar.

A página passou a ter três pesos de cor: laranja no parecer, navy no financeiro e o semáforo (verde/vermelho) na política e nos documentos; todo o resto segue no teal neutro de painel.

Uma armadilha do ambiente compartilhado

Na validação por screenshot destes ajustes, o script parametrizou um documento obrigatório e um opcional para LESSEE_PF — e não desfez. A parametrização documental é do tenant, e o tenant de e2e é compartilhado: a rodada seguinte teve 3 falhas que não tinham nada a ver com o código (as duas aprovações da spec 0003 passaram a esbarrar em documento obrigatório faltando, e a contagem por área da spec 0010 virou "2 documentos"). Restaurada a configuração, 29 de 29 voltaram a passar.

Os specs de e2e já seguem essa disciplina (finally com setRequirement(..., null)); scripts de screenshot precisam da mesma.

Executado

A pedido do usuário, a suíte e2e dos quatro arquivos tocados foi executada contra o dev-remote reconstruído: 29 de 29 passaram — os 8 testes da spec nova e os 21 dos três specs ajustados, que cobrem as specs 0003, 0008, 0010 e 0015. Re-executada depois dos ajustes de apresentação: 29 de 29 de novo. Todos os 24 critérios de aceite estão verificados.

A suíte completa não foi executada: os quatro arquivos cobrem o que esta spec toca, mas PageHeader e o cliente da API são compartilhados com outras telas, e uma regressão fora da análise de crédito passaria despercebida.