Pular para conteúdo

0036 — O passo de analisar, e o caminho de volta para a imobiliária

Status CONCLUÍDA
Branch spec/0036-retorno-da-analise-a-imobiliaria
Criada em 2026-08-12
Apps afetados api · web · e2e
Nota da necessidade 9,0 (tentativa 7)
Nota do detalhamento 8,5 (tentativa 2)
Nota da implementação 8,5 (tentativa 4)

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

A spec tem duas partes, escritas em ordem, e três portões — cada um com nota de 0 a 10 dada por um subagente verificador de contexto limpo, em até 5 tentativas, passando acima de 8:

  1. A Parte I diz por que isto precisa existir e como se usa. Validada na seção 6 (negócio × usabilidade). Detalhar sobre necessidade vaga é o jeito mais caro de descobrir que era outra coisa.
  2. A Parte II diz como se constrói. Validada na seção 14 (segurança × clareza técnica) — autorização, isolamento, e se sobrou decisão para quem vai codar.
  3. A implementação é medida contra as duas, na seção 15 (aderência × qualidade × testes), antes de a spec fechar.

Parte I — Necessidade

Vocabulário, antes de tudo. Esta spec usa "devolver à imobiliária" para a ação que a interface hoje chama "Solicitar documentos", e "análise devolvida" para o estado PENDING_DOCS, rotulado "Aguardando documentos" no selo e na lista — o rótulo do selo não muda. Não há estado novo: há um renome de ação e uma transição de volta que não existia. O renome é parte da entrega, porque "solicitar documentos" descreve um dos motivos da devolução, não o que a ação faz.

"Linhas de situação" é o nome, nesta spec, das duas linhas de texto que passam a existir dentro do bloco da linha do tempo de status. Não é bloco novo, não é faixa, não é componente à parte — é texto acrescentado a um bloco que já está na tela.

Nem toda análise tem imobiliária. Quando quem cadastra é da própria garantidora, a análise nasce sem parceiro — é a carteira própria. O fluxo é o mesmo e o destinatário muda: a devolução volta para quem cadastrou. O rótulo da ação acompanha a origem — "Devolver à imobiliária" quando há parceiro, "Devolver para correção" na carteira própria —, e as linhas de situação dizem de quem é a vez pelo nome. Onde esta spec disser "imobiliária" sem ressalva, leia "a imobiliária, ou quem cadastrou, se não houver".

1. Dor

Uma análise enviada parece não ter próximo passo, e devolvê-la à imobiliária parece impossível.

O analista abre uma análise que a imobiliária acabou de enviar. A tela oferece uma ação: uma lupa. No computador ela vem com o texto "Iniciar análise"; no celular o texto some e sobra só o ícone. Nem uma coisa nem outra diz o que aquele passo faz — e o que ele faz é destrancar tudo o mais: aprovar, reprovar e devolver para a imobiliária. Nenhuma dessas três aparece antes de a lupa ser acionada.

Quem precisa devolver — faltou o comprovante de renda, o valor do aluguel veio errado, o inquilino cadastrado não é o do contrato — procura na tela e não acha. Conclui, com razão a partir do que vê, que o sistema não permite voltar.

O que se faz hoje, então: manda WhatsApp ou e-mail para a imobiliária pedindo o que falta, e deixa a análise parada como enviada. A consequência é dupla e cara:

  • O pedido não fica em lugar nenhum. O sistema tem onde guardar o motivo — o histórico da análise grava motivo, autor e data em toda transição —, mas quem devolve por WhatsApp não passa por transição nenhuma. O caminho que registra é justamente o que ninguém encontra. Quem pegar a análise depois — outro analista, o gestor, a auditoria — vê uma análise enviada há duas semanas sem nenhuma explicação para a parada.
  • A fila mente. A análise continua contada como "aguardando análise" quando na verdade aguarda a imobiliária. Ninguém consegue separar o que depende da garantidora do que depende do parceiro, que é a primeira pergunta de qualquer cobrança de prazo.

E o caminho certo, quando alguém o descobre, termina em dois becos.

  1. A imobiliária não pode corrigir o que ela mesma errou. Devolvida, a análise chega até ela, mas lá só dá para anexar documento. Se o problema era um dado — o valor do aluguel, o inquilino trocado —, quem cadastrou precisa pedir a alguém da garantidora que edite por ela. A devolução, que existia para tirar trabalho da garantidora, devolve trabalho para ela.
  2. Não há volta. Quando a imobiliária termina, nada avisa o analista e nada muda de lugar: a análise devolvida não tem caminho de retorno para a fila de análise. Ela fica devolvida para sempre, até alguém da garantidora lembrar de abri-la e conferir. Não é falta de aviso por e-mail — é ausência do passo: o retorno não existe como transição possível.

2. Quem sente

  • Analista de crédito (ANALYST, e o ADMIN_TENANT que também analisa) — em toda análise que chega. Não é caso de exceção: iniciar a análise é o primeiro passo de 100% delas, e é justamente esse passo que está escondido atrás de um ícone. Sente de novo, e pior, quando precisa devolver.
  • Usuário da imobiliária (AGENCY_USER) — quando a análise volta. Ele é quem cadastrou os dados e quem tem o documento na mão, e é quem hoje não consegue corrigir o que ele mesmo digitou errado nem sinalizar que terminou.
  • Gestor da garantidora (ADMIN_TENANT) — ao olhar a fila e não conseguir dizer o que está parado por culpa de quem.
  • OPERATOR — cadastra e envia as análises da carteira própria, aquelas sem imobiliária. Hoje ele recebe o pedido de correção por fora, sem registro, exatamente como o parceiro. Ele já pode editar a análise devolvida; o que lhe falta é a mesma coisa que falta a todo mundo — a tela dizer que ela voltou, por quê, e como devolvê-la ao analista.

3. Resultado esperado

Um analista que nunca viu o sistema abre uma análise enviada e entende, lendo a tela, em que passo ela está e o que fazer — inclusive que devolver à imobiliária é uma das saídas, e que ela fica disponível ao iniciar a análise. A imobiliária resolve sozinha o que foi pedido e devolve a análise para a fila de análise, onde ela reaparece sem intervenção de ninguém da garantidora.

Isso não é o mesmo que avisar: não há notificação, e o analista continua olhando a lista dele. O que muda é o que a lista diz — a análise resolvida volta a aparecer no filtro "Em análise", em vez de ficar parada num estado que ninguém sabia interpretar. Trocar varredura por leitura de fila é o ganho; ausência de vigilância não é prometida aqui.

Como se sabe que resolveu — seis sinais que hoje dão vermelho:

  • O texto aponta para algo que dá para ver. Hoje, na análise enviada, a tela já traz duas palavras sobre o passo: o selo diz a etapa e o painel de parecer diz "Esta análise de crédito está aguardando início da análise. Use 'Iniciar análise' no topo da página." O problema é que em 375px não existe nada chamado "Iniciar análise" no topo — existe uma lupa sem rótulo. A instrução nomeia um botão que a pessoa não consegue localizar. Verificável: abrir a análise enviada em 375px e procurar, na tela, o texto citado pela instrução.
  • O texto diz o que o passo destranca. Nenhuma frase hoje menciona que analisar é o que torna possível devolver à imobiliária — que é justamente a saída que ninguém encontra. Verificável: procurar a palavra "devolver" na tela de uma análise enviada.
  • A imobiliária tem o que ler. Hoje o bloco que explica o passo é o painel de parecer, e ele não renderiza para ela — exige decisions:read, que o AGENCY_USER não tem. A tela dela é muda sobre o passo e sobre o motivo da devolução. Verificável: abrir, como usuário de imobiliária, uma análise devolvida e procurar o motivo sem abrir o histórico.
  • A ida e a volta cabem no histórico. Uma análise devolvida e resolvida percorre o caminho inteiro dentro do sistema: sai da análise, volta para a análise, e o histórico mostra as duas transições com autor, data e motivo. Hoje a segunda transição não existe na máquina de estados — só se sai de devolvida para aprovada, reprovada ou cancelada.
  • A fila para de mentir. O analista distingue na lista o que espera a garantidora do que espera o parceiro, e o que o parceiro devolveu reaparece sozinho na fila de análise. Verificável assim: uma análise devolvida e reenviada pela imobiliária aparece no filtro "Em análise" sem ninguém da garantidora ter tocado nela.
  • A imobiliária se resolve sozinha. Um AGENCY_USER corrige o dado de uma análise devolvida e a reenvia sem pedir nada a ninguém. Hoje ele recebe 403 na correção, e a ação de reenviar não existe.

E, no lado dela, o motivo da devolução está em destaque na tela da análise, sem precisar abrir o histórico para descobrir por que a análise voltou.

4. Experiência pretendida

  • Ponto de entrada: menu Análises de Crédito → a lista → clique na linha (ou no ícone de olho, "Ver detalhes") → tela da análise. O analista chega filtrando a lista por status Enviada e Em análise; a imobiliária, por Aguardando documentos — os filtros já existem na lista, nos dois viewports.
  • Jornada do analista, caso feliz:
  • Abre a análise enviada.
  • Lê na tela que ela aguarda o início da análise, e que iniciar é o que libera aprovar, reprovar e devolver à imobiliária.
  • Aciona Analisar.
  • A tela passa a oferecer, nomeadas, as três saídas: Aprovar, Reprovar, Devolver à imobiliária.
  • Escolhe devolver. Se o checklist aponta documentos faltantes, o campo de motivo já vem preenchido com eles, um por linha, editável — ele complementa, apaga o que não se aplica, e confirma. Se o checklist não acusa nada — o caso do dado errado —, o campo abre vazio, com o texto de apoio "Diga o que precisa ser corrigido"; nada é inventado para preencher, porque um motivo genérico é pior que um campo em branco.
  • A análise fica devolvida, o motivo aparece na tela e entra no histórico com autor e data. Ela sai da fila de análise do analista.
  • Jornada da imobiliária, caso feliz:
  • Abre a análise devolvida (pela lista, filtrando por Aguardando documentos).
  • Lê o motivo em destaque no topo do conteúdo, sem abrir histórico.
  • Resolve o que foi pedido, em dois lugares distintos e sem sair da análise:

    • Documento — no bloco de documentos da própria tela da análise, que já existe e já lista o que o checklist acusa como faltante. Não se anexa dentro do formulário de edição: documento não é campo.
    • Dado errado — aí sim no formulário de edição, onde ela alcança os mesmos campos que preenche ao criar: participantes, imóvel vinculado, valor do aluguel, condomínio, IPTU e prazo. Se o aluguel mudar, o preço da garantia é refeito ao salvar (pacote locatício, taxa, LMI, mensalidade), para o número na tela não contradizer o aluguel exibido.

    Em 375px os dois ficam na mesma rolagem vertical — o bloco de documentos abaixo dos dados —, então resolver os dois não exige trocar de tela nem voltar à lista. O anexo no celular usa o seletor do próprio sistema operacional, que já oferece a câmera: quem está em visita ao imóvel fotografa o documento ali, sem passar por computador. 4. Aciona Devolver ao analista e confirma. A análise volta ao estado de análise e reaparece na fila do analista; a devolução dela entra no histórico com autor e data. 5. O analista retoma de onde parou — aprovar, reprovar ou devolver de novo. - Custo de uso: - Analista: digita um campo, o motivo da devolução — e ele não parte do zero: o campo nasce preenchido com a lista de documentos faltantes que o sistema já conhece, e no caso mais comum (faltou documento) basta conferir e confirmar, sem digitar nada. Nada mais é perguntado: análise, imóvel, participantes e imobiliária de origem já estão no sistema; autor e data saem da sessão. O motivo é obrigatório aqui, diferente da reprovação, onde é recomendado: é o texto que a imobiliária vai ler para saber o que fazer, e devolução sem motivo devolve o problema para o WhatsApp. - Imobiliária: digita só a correção que ela mesma precisa fazer, e nada é pedido duas vezes — o preço da garantia é recalculado pelo sistema a partir do aluguel, não perguntado. O reenvio não pede texto nenhum: quem já leu o motivo e corrigiu não deve ter de redigir um relatório para devolver.

4.1 Desktop

Cabeçalho com código da análise, título e o selo de status; ações à direita, com rótulo em texto (é assim que a tela já se comporta). Quem vê o quê:

Cada ação aparece para quem tem a permissão dela — as colunas são o resumo dos dois casos típicos, não uma classificação de pessoas. Ninguém "pertence" a uma coluna: decisions:update traz aprovar, reprovar e devolver; credit_analyses:update traz editar; e devolver ao analista é de quem está do lado que recebeu a devolução. Quem não tem nenhuma delas — o VIEWER, ou alguém da garantidora sem papel naquela análise — não vê ação nenhuma e lê a linha de somente-leitura da tabela abaixo. Consequências que isso resolve de saída:

  • Qualquer usuário da imobiliária vinculada corrige e devolve — não só quem cadastrou. O escopo é o partner_agency_id, como no resto do sistema, então a pendência não trava porque uma pessoa saiu de férias. O histórico grava quem fez cada coisa, que pode não ser quem criou a análise.
  • Quem cadastra e analisa lê uma coluna só. O ADMIN_TENANT que criou uma análise da carteira própria e também a analisa é, nesta tela, quem analisa: ele vê as ações da esquerda e não acumula as da direita. Sem isso seriam cinco ações no topo, contra o teto de três — e duas linhas de situação disputando a mesma tela. A regra é decisions:update prevalece: quem pode decidir lê a tela de quem decide.
  • O teto de três nunca é estourado por acúmulo de permissão. Se um papel reunir mais de três ações num estado, valem as três da coluna de quem decide, e a de edição desce para o painel de parecer — é onde "Cancelar" já foi parar quando PENDING_DOCS teve quatro candidatas.
Estado Quem analisa (decisions:update) Quem cadastrou — a imobiliária vinculada, ou o operador na carteira própria
Enviada Analisar nenhuma ação — as linhas de situação dizem que é a vez da garantidora
Em análise Aprovar · Reprovar · Devolver à imobiliária nenhuma ação
Devolvida Aprovar · Reprovar · Resolver pendência Editar · Devolver ao analista

Na carteira própria a coluna da direita é do OPERATOR, e a ação de quem analisa chama-se "Devolver para correção". Nada mais muda: mesmos estados, mesmas telas, mesmo caminho de volta.

"Resolver pendência" passa a existir para o analista, e isso é permissão nova. Hoje ela exige credit_analyses:update, que só ADMIN_TENANT e OPERATOR têm — o ANALYST puro veria duas ações, não três. O ANALYST ganha essa permissão, pela mesma razão que motiva a spec inteira: quando o erro é evidente para quem está lendo a análise, obrigar uma ida e volta ao parceiro por causa de um dígito custa mais do que corrigir. É ampliação de escrita para um papel que hoje só opina, e a Parte II terá de dizer o que ele alcança e o que continua fora.

São no máximo três no topo, o teto já adotado no app. Na análise devolvida o analista não vê "Devolver à imobiliária" — ela já está devolvida, e a máquina de estados não prevê devolver o que já foi devolvido; o que ele tem ali é a mesma "Resolver pendência" de hoje, que o leva à edição.

As linhas de situação entram na linha do tempo, e substituem a frase que existe hoje. O primeiro bloco da tela já é a linha do tempo de status, que nomeia a etapa atual; logo abaixo dela, o painel de parecer traz hoje a frase "Esta análise de crédito está aguardando início da análise. Use 'Iniciar análise' no topo da página." Essa frase sai, por dois motivos: ela duplicaria as linhas novas em dois blocos vizinhos, e ela mora num painel que não renderiza para a imobiliária (exige decisions:read) — ou seja, hoje ela explica o passo só para quem já sabe. O texto passa a viver um nível acima, no bloco que todo mundo vê.

São sempre duas, e entram abaixo do trilho de etapas, dentro do mesmo bloco — o trilho continua sendo a primeira coisa que se vê, e o texto explica o que ele mostra, nos dois viewports. São elas: o passo, em palavras e o que fazer. O que dizem depende do estado e de quem abriu — e a segunda linha muda em 375px, onde precisa nomear o ícone em vez do rótulo (no celular não há rótulo para citar):

Os sete estados, os dois lados. "Quem cadastrou" é o usuário da imobiliária ou, na carteira própria, o OPERATOR/ADMIN_TENANT que criou a análise — os dois leem a mesma coluna, com o nome do destinatário trocado.

Estado Quem abre Linha 1 — o passo Linha 2 — o que fazer (desktop) Linha 2 — em 375px
Rascunho quem cadastrou "Rascunho, ainda não enviada." "Complete o cadastro e envie para análise." "No topo: lápis completar o cadastro · avião de papel enviar para análise."
Rascunho analista "Rascunho, ainda não enviada." "Aguardando o envio. Nada é necessário de sua parte agora." igual
Enviada analista "Aguardando início da análise." "Ao analisar, você poderá aprovar, reprovar ou devolver à imobiliária." "Toque na lupa, no topo, para analisar — e então aprovar, reprovar ou devolver."
Enviada quem cadastrou "Enviada para a garantidora." "Aguardando o início da análise. Nada é necessário de sua parte agora." igual
Em análise analista "Em análise desde DD/MM." "No topo: aprovar, reprovar ou devolver à imobiliária." "No topo: relógio devolver à imobiliária · ✕ reprovar · ✓ aprovar."
Em análise quem cadastrou "Em análise pela garantidora desde DD/MM." "Aguardando o parecer. Nada é necessário de sua parte agora." igual
Devolvida analista "Devolvida a «Imobiliária X» em DD/MM." "Motivo: «…». Aguardando a correção. Você ainda pode aprovar, reprovar ou resolver pendência." "Motivo, em linhas próprias. No topo: ✓ aprovar · ✕ reprovar · lápis resolver pendência."
Devolvida quem cadastrou "Devolvida pela garantidora em DD/MM." "Motivo: «…». Corrija o que foi pedido e devolva ao analista." "Motivo: «…». Toque no lápis para corrigir e no avião de papel para devolver ao analista."
Aprovada analista "Aprovada em DD/MM." "Emita a garantia para concluir." (se não houver garantia) "…toque no certificado, no topo."
Aprovada quem cadastrou "Aprovada em DD/MM." "Aguardando a emissão da garantia pela garantidora." igual
Reprovada analista "Reprovada em DD/MM." "Motivo: «…». Estado final: esta análise não volta atrás." igual
Reprovada quem cadastrou "Reprovada em DD/MM pela garantidora." "Motivo: «…». Estado final: esta análise não volta atrás." igual
Cancelada analista "Cancelada em DD/MM." "Estado final: esta análise não volta atrás." igual
Cancelada quem cadastrou "Cancelada em DD/MM pela garantidora." "Estado final: esta análise não volta atrás." igual
qualquer um quem só lê o passo, igual às linhas acima "Somente leitura: você acompanha esta análise, mas não age sobre ela." igual

Depois de várias idas e voltas, o motivo exibido é o da última devolução — é o que está pendente, e é a pergunta que a pessoa foi ali responder. Os anteriores continuam no histórico, que já registra todas as transições com autor, data e motivo, e é para lá que a linha 2 aponta quando há mais de uma ("2ª devolução — ver histórico").

Os sete estados ganham texto, e nenhum fica mudo — inclusive os finais, onde a linha 2 existe para dizer que não há o que fazer. Uma tela sem instrução é ambígua entre "não há ação" e "a ação está escondida", e é essa ambiguidade que originou esta spec.

Quatro decisões que a tabela carrega:

  • Ícone só se descreve quando é distinguível. "Descreva o alvo pelo que se vê" funciona para uma lupa sozinha; não funciona para três ícones parecidos. Por isso, em 375px, a linha enumera os três na ordem em que aparecem na tela, da esquerda para a direita — que é o inverso da ordem em que as ações são declaradas, porque o cabeçalho põe a principal à direita. Aferido em captura, não suposto.
  • A devolução do analista mantém o relógio, e isso é decisão, não herança. A ação muda de nome, mas o relógio é o ícone do estado que ela produz — é o mesmo que a linha do tempo já usa em "Aguardando documentos". Acionar o relógio leva ao estado do relógio. Uma seta de retorno seria mais literal, mas não existe no registro de ícones do app, e romper a correspondência com a linha do tempo custa mais do que ganha.
  • "Devolver ao analista" ganha o avião de papel — o mesmo de "Enviar para análise". Devolver é enviar de volta; reaproveitar o ícone poupa um símbolo novo e mantém o significado. Os dois nunca aparecem ao mesmo tempo na tela, então não competem.
  • O analista também precisa das linhas na análise devolvida. Ele abre a mesma tela e precisa entender que a vez é do outro lado — sem isso, a análise devolvida some do radar dele exatamente como some hoje.

Diálogo de devolução (analista): título "Devolver à imobiliária" (ou "Devolver para correção", na carteira própria), campo de motivo multilinha — preenchido com os documentos faltantes, ou vazio com o texto de apoio quando não há nenhum —, botões confirmar e cancelar no rodapé. É o diálogo de decisão que já existe, com outro título e outro rótulo de campo.

Confirmação do reenvio (imobiliária): "Devolver ao analista" pede confirmação, num diálogo curto e sem campo de texto — só o aviso de que a análise volta para a garantidora e não poderá mais ser editada até que devolvam de novo. Ação que muda o que a outra parte vê não dispara no primeiro toque.

A correção pela imobiliária, no desktop. Ela usa o mesmo formulário de edição que já existe, com o layout que ele já tem — nada de tela nova nem de layout próprio para ela. O formulário abre preenchido com os valores atuais: ela mexe no que estiver errado e salva, sem redigitar o que já estava certo. Duas coisas mudam ali:

  • O motivo da devolução aparece no topo do formulário, nos dois viewports, para o que precisa mudar estar à vista de quem digita. No desktop isso evita abrir a análise numa aba e o formulário em outra; no mobile, alternar entre duas telas.
  • As ações do formulário ficam no topo, como já ficam desde a spec 0024 — não é regra de mobile, é o padrão do app nos dois viewports.

O que ela não encontra no formulário é o "Devolver ao analista": salvar e devolver são dois atos, e o segundo mora na tela da análise.

O conteúdo abaixo (dados da análise, participantes, documentos, parecer) não muda de lugar nem de ordem.

4.2 Mobile (375px)

O cabeçalho continua com ícone de 44px e sem rótulo — é o padrão do app desde as specs 0020 e 0024, e três rótulos no topo espremeriam o código da análise até truncar. Mudar isso resolveria uma tela e quebraria as outras.

A clareza vem do texto, não de outro botão. As linhas de situação são, em 375px, a única coisa na tela que diz o que fazer:

  • Ficam em coluna única, largura inteira, dentro do bloco que já está ali — nenhum bloco novo é empilhado, para a primeira dobra continuar mostrando o começo do conteúdo.
  • Nomeiam o passo e descrevem o alvo pelo que se vê, não pelo rótulo que não existe: "Aguardando início da análise. Toque na lupa, no topo, para analisar — e então aprovar, reprovar ou devolver à imobiliária." A frase de hoje manda usar um botão chamado "Iniciar análise", e em 375px esse nome não está escrito em lugar nenhum.
  • Nenhum botão duplicado nas linhas de situação. Cogitado e recusado: repetir aqui a ação principal criaria dois caminhos para a mesma coisa e consumiria altura da primeira dobra. O que faltava era o nome, e o nome é o que entra.
  • Na análise devolvida, essas linhas carregam o motivo. Texto longo quebra em várias linhas — não trunca com reticências, porque o motivo é a informação que a imobiliária foi ali buscar.

Diálogo de devolução (analista): o campo de motivo é multilinha e já vem preenchido com os documentos faltantes — não há bloco separado listando-os, justamente para não empurrar o campo para fora da tela com o teclado aberto. No celular o teclado cobre a metade inferior, então o campo fica acima dos botões de confirmar e cancelar e o diálogo rola internamente, para o confirmar continuar alcançável. O campo abre com altura de três linhas e cresce até seis, rolando dentro de si depois disso.

Correção pela imobiliária, em 375px — é a metade da feature que mora num formulário longo, e é onde o app já quebrou antes:

  • Ela edita a análise no mesmo formulário de edição que já existe, sem tela nova: campos em coluna única, um por linha, com o rótulo acima do campo, preenchidos com os valores atuais. São os mesmos campos que ela preenche ao criar — nenhum a mais, nenhum a menos.
  • Ela não vasculha o formulário atrás do erro. O motivo da devolução é repetido no topo do formulário, para o que precisa mudar estar à vista de quem digita, sem alternar entre duas telas no celular.
  • O preço muda sozinho. Ao salvar com aluguel novo, o pacote locatício e o preço da garantia são refeitos — ela não digita valor calculado, e a tela de leitura já mostra o número novo quando ela volta.
  • As ações do formulário ficam no topo (padrão da spec 0024), então salvar não exige rolar até o fim de um formulário de vinte campos com o teclado aberto.
  • Devolver ao analista é ação da tela da análise, não do formulário — primeiro se salva a correção, depois se devolve. Um botão que salvasse e devolvesse junto tiraria da imobiliária a chance de conferir o que salvou.

Nada nesta tela, nem no formulário, rola na horizontal em 375px.

4.3 Estados

  • Carregando: as linhas de situação aparecem como blocos cinza da mesma altura do texto final — não pulam quando os dados chegam. As ações do topo ficam desabilitadas.
  • Os dois lados editando ao mesmo tempo. No estado devolvido, tanto quem cadastrou (por "Editar") quanto quem analisa (por "Resolver pendência") abrem o mesmo formulário. Não há bloqueio nem mesclagem: o último a salvar prevalece — travar o formulário deixaria a análise parada quando alguém esquecesse a tela aberta, o que é pior do que sobrescrever. O que não pode é ser silencioso: quem salva sobre uma alteração feita depois de a tela ter carregado lê, ao salvar, "Esta análise foi alterada por outra pessoa enquanto você editava. Sua alteração foi salva por cima." O aviso não diz quem — o sistema não guarda autoria de edição, só de mudança de estado, e prometer um nome que não existe seria pior que a frase impessoal.
  • Análise que mudou de mão enquanto a tela estava aberta. É o estado próprio desta feature: ela passa entre dois lados, e cada um pode ter a tela aberta. Se a imobiliária aciona Devolver ao analista numa análise que o analista já aprovou, reprovou ou cancelou, o servidor recusa a transição — e a tela não mostra o erro cru. Ela diz o que aconteceu em português ("Esta análise já foi aprovada pela garantidora enquanto você editava. Nada do que você salvou se perdeu.") e recarrega sozinha para o estado real, com as ações certas daquele estado. Vale o simétrico: o analista que aciona Aprovar numa análise que a imobiliária acabou de devolver lê que ela voltou para análise e a tela se atualiza — sem perder o que ele digitou num diálogo aberto.
  • Erro ao carregar a tela: mensagem no lugar do conteúdo, com o que houve e um botão de tentar de novo. Análise inexistente, ou de outra garantidora ou de outra imobiliária, é indistinguível de inexistente — a tela diz "Análise não encontrada" e oferece a volta para a lista. Não se informa que o registro existe mas é de outro: isso é enumeração de dado alheio.
  • Erro ao analisar: a ação central desta spec. A tela não muda de estado, o ícone volta a ficar acionável e a mensagem diz que a análise continua enviada e nada foi alterado.
  • Erro ao devolver: o diálogo não fecha, o motivo digitado não se perde, e a mensagem diz o que aconteceu e o que fazer ("Não foi possível devolver a análise. Tente confirmar de novo; se persistir, a análise segue em análise e nada foi alterado."). O botão de confirmar volta a ficar acionável.
  • Erro do lado da imobiliária: falha ao anexar documento mantém o arquivo escolhido na tela e permite tentar de novo, sem reabrir o seletor. Falha ao salvar a correção não descarta o que foi digitado — no celular, refazer vinte campos com o teclado aberto é perder o trabalho todo. Falha ao devolver ao analista deixa a análise devolvida e diz isso, para ela não achar que já entregou.
  • Motivo obrigatório não preenchido: o confirmar do diálogo recusa e o campo diz o que falta, ao lado dele. Não se fecha o diálogo para avisar depois.
  • Sem permissão: quem não pode analisar nem decidir não vê as ações — nem no topo, nem no texto. As linhas de situação continuam aparecendo, dizendo o passo em que a análise está e de quem é a vez ("Aguardando a análise da garantidora"), para a imobiliária entender a espera em vez de encontrar uma tela muda. Esconder botão não é autorização: quem chamar a API direto continua recebendo a recusa do servidor.
  • Vazio: não se aplica — é tela de registro único, que só existe com dado.

Onde o erro aparece, e a diferença entre os viewports. Hoje o aviso de erro da tela fica entre o selo de status e a linha do tempo. No desktop cabe; em 375px ele empurra para fora da primeira dobra exatamente o bloco que esta spec usa para explicar o passo — o erro apareceria e a explicação sumiria. Então, no mobile, o aviso de erro vai abaixo da linha do tempo, não acima: as linhas de situação são a última coisa que pode deixar de ser vista, porque são elas que dizem o que fazer a seguir. É a única diferença de comportamento entre os dois viewports nesta tela.

5. Fora de escopo

  • Devolver direto da análise enviada. Cogitado, e recusado: a máquina de estados continua exigindo iniciar a análise antes de devolver. O problema atacado aqui é a tela não dizer isso, não o fluxo ter dois passos.
  • Fundir "analisar" com abrir a tela (abrir a análise já a colocaria em análise). Cogitado; ficaria implícito demais, e passaria a registrar início de análise no histórico por quem só abriu para olhar.
  • Notificar a imobiliária por e-mail na devolução, e notificar o analista no reenvio. Decisão registrada: os dois lados descobrem pela lista e pela tela — o reenvio já devolve a análise à fila do analista, que é o aviso que importa. Entra apenas a correção da documentação da API, que hoje promete uma notificação que não existe.
  • Prazo ou SLA para a imobiliária responder, e qualquer cobrança automática.
  • Redesenho da lista de análises de crédito. Os filtros por status de que a jornada depende já existem e ficam como estão. Marcar na lista "documento anexado após a devolução" foi cogitado e sai: com o reenvio explícito, o sinal vira o próprio status.
  • Novos estados na análise de crédito. Os sete atuais bastam — o que entra é uma transição de volta (PENDING_DOCS → IN_ANALYSIS) e um renome de ação, não estado novo.
  • Limite de idas e voltas. A análise pode ser devolvida e reenviada quantas vezes for preciso; controlar isso é outra conversa.
  • Editar a análise depois de aprovada ou reprovada. A correção pela imobiliária vale só no estado devolvido — nos demais, a análise segue somente-leitura para ela.
  • Trocar o imóvel da análise. Ela corrige os dados do que cadastrou; apontar a análise para outro imóvel mudaria a imobiliária de origem e o escopo do registro, e fica fora.
  • Refazer o preço de análises antigas. O recálculo ao salvar vale para a edição que acontecer daqui em diante; análises já gravadas com preço dessincronizado do aluguel não são varridas nem corrigidas por esta spec.

6. Validação da necessidade

Preenchido pelo laço de validação do /spec new (subagente spec-brief-validator). Registrar todas as tentativas, inclusive as reprovadas.

Tentativa Negócio Usabilidade Final Veredicto
1 8,0 6,0 6,0 REPROVADA
2 8,8 8,0 8,0 REPROVADA
3 9,0 7,0 7,0 REPROVADA
4 8,8 8,0 8,0 REPROVADA
5 9,0 8,0 8,0 REPROVADA
6 9,0 7,0 7,0 REPROVADA
7 9,0 9,0 9,0 APROVADA

Resultado: aprovada na tentativa 7 com nota 9,0 (negócio 9,0 · usabilidade 9,0; mínimo: acima de 8,0).

O laço estourou as 5 tentativas em 8,0 e o usuário autorizou duas rodadas extras. O que travava não era redação: eram três decisões de negócio que só ele podia tomar — quem, do lado da imobiliária, pode corrigir (qualquer usuário da imobiliária vinculada, pelo partner_agency_id); o que acontece na carteira própria, sem parceiro (a devolução volta a quem cadastrou); e se o ANALYST ganha credit_analyses:update (sim). Com as três respondidas, a nota subiu de 7,0 para 9,0 numa rodada.

Pendências aceitas: nenhuma bloqueante. Ficaram para a Parte II, por serem matéria de autorização: o alcance da escrita nova do ANALYST (ele corrige dado cadastrado pelo parceiro, ou só o da garantidora?) e se, na carteira própria, qualquer OPERATOR do tenant devolve ao analista ou só quem criou a análise. Sem linha de base numérica dos sinais da seção 3 — o dado não existe hoje.

Pendências aceitas: o que o validador ainda apontou e se decidiu não resolver, com o porquê. Vazio se não houver.


Parte II — Detalhamento

7. Regras de negócio

O caminho de volta

  • RN1. A máquina de estados ganha uma transição: PENDING_DOCS → IN_ANALYSIS. Nenhum estado novo, nenhuma outra transição alterada. Ela entra em ALLOWED_TRANSITIONS e passa a ser exercida por um endpoint — o que a distingue da situação anterior ao fix 0006, quando estava declarada e nenhum endpoint a executava, e por isso foi removida.
  • RN2. Quem devolve ao analista é quem está do lado que recebeu a devolução: a imobiliária vinculada à análise ou, na carteira própria, a garantidora. A transição exige a permissão credit_analyses:resume (RN12) e só se aplica a análise em PENDING_DOCS. Em qualquer outro estado a resposta é 422 com INVALID_STATUS_TRANSITION, e nada é gravado.
  • RN3. O reenvio devolve a Decision de PENDING para IN_PROGRESS — é o que o próprio modelo já documenta ("Retorna a IN_PROGRESS quando retomada"). Análise sem Decision não pode estar em PENDING_DOCS; se estiver, é dado inconsistente e o endpoint responde 422, sem criar parecer nenhum.
  • RN4. O reenvio grava CreditAnalysisStatusHistory com from_status=PENDING_DOCS, to_status=IN_ANALYSIS, changed_by = quem reenviou e reason nulo — o reenvio não pede texto (Parte I, custo de uso). O opinion e o decision_reason da devolução anterior não são apagados: eles são o registro do que foi pedido.
  • RN5. Não há limite de idas e voltas. Cada ciclo acrescenta duas linhas ao histórico, e é o histórico que conta a história.

A devolução

  • RN6. A ação hoje chamada "Solicitar documentos" passa a chamar-se "Devolver à imobiliária" quando a análise tem partner_agency_id, e "Devolver para correção" quando não tem (carteira própria). Muda o rótulo, o título do diálogo e o rótulo do campo; não muda o endpoint (POST /decision/pend), o estado (PENDING_DOCS) nem o rótulo do selo ("Aguardando documentos").
  • RN7. O motivo passa a ser obrigatório na devolução — hoje o campo é opcional. Requisição sem reason, ou com reason em branco, responde 422. Na reprovação ele continua recomendado e opcional: são decisões diferentes, e só uma delas devolve trabalho para alguém.
  • RN8. O campo de motivo abre pré-preenchido com os documentos que o checklist aponta como bloqueantes, um por linha. Se o checklist não aponta nenhum, abre vazio. O pré-preenchimento é do cliente — o servidor não inventa reason.

A correção

  • RN9. A edição recalcula o snapshot financeiro quando mudar rental_value, condo_fee, property_tax ou guarantee_months — em DRAFT e PENDING_DOCS, que são os dois únicos estados editáveis. Não há razão para as duas edições divergirem: em ambas a garantia ainda não existe. Recalculam-se financial_rental_package, financial_monthly_value, financial_initial_payment, financial_first_month_upfront, financial_lmi_value e financial_exit_costs_value, pela mesma função que a criação usa.
  • RN10. O recálculo usa as TenantSettings da garantidora dona da análise (credit_analysis.tenant_id), nunca o escopo de quem chama. Isso importa porque o ADMIN_GLOBAL não tem escopo de tenant: se o parâmetro saísse do solicitante, ele precificaria a análise da Garantidora A com a taxa de ninguém. São as vigentes no momento do recálculo — taxa, LMI, carência e custos de saída passam a ser os de agora. É mudança consciente do que o snapshot significa: deixa de ser "congelado na criação" e passa a ser "congelado no último dado confirmado", porque análise em curso não tem condição contratada a preservar. Análise aprovada não recalcula nada, e é aí que o congelamento continua valendo.
  • RN11. Salvar sobre alteração feita por outra pessoa prevalece (último a salvar vence). O cliente envia em expected_updated_at o updated_at que carregou; se não bater com o atual, a gravação acontece do mesmo jeito e a resposta traz concurrent_update: true. Não há bloqueio nem mesclagem: travar o registro deixaria a análise parada quando alguém esquecesse a tela aberta.
  • RN11a. O aviso não nomeia quem alterou. CreditAnalysis não tem updated_by, e edição não gera linha de histórico — só transição de status gera. Inventar o nome exigiria coluna nova e migration, o que esta spec não faz. A mensagem diz o que se sabe: "Esta análise foi alterada por outra pessoa enquanto você editava. Sua alteração foi salva por cima." A frase da seção 4.3 que prometia «Fulano» está corrigida.

Permissões que mudam

  • RN12. Entra a permissão credit_analyses:resume, exigida pelo endpoint de reenvio, nos papéis AGENCY_USER, OPERATOR, ADMIN_TENANT e ADMIN_GLOBAL.
  • RN13. credit_analyses:update é concedida a AGENCY_USER e a ANALYST. Ela governa também os participantes (credit_analysis_participants), então os dois papéis passam a poder trocar inquilino e renda declarada — que é exatamente o pedido.
  • RN13a. PATCH /participants/{id} ganha guarda de estado, e é condição para a RN13. Hoje ele é permitido em qualquer status e chama _recompute_credit_if_needed, que reescreve total_declared_income, income_to_rent_ratio, credit_recommendation e recommendation_notes na Decision. Sem guarda, conceder credit_analyses:update ao AGENCY_USER deixaria um usuário externo reescrever a recomendação de crédito de uma análise em IN_ANALYSIS — ou já APPROVED —, alterando dado que o papel dele não pode nem ler (decisions:read está fora dele). A regra passa a ser:
  • quem tem decisions:update continua atualizando participante em qualquer estado — é o analista alimentando a análise em curso, que é a razão de a rota ser assim;
  • quem não tem decisions:update só atualiza participante em DRAFT ou PENDING_DOCS, como já vale para adicionar e remover. Fora disso: 422 PARTICIPANT_NOT_EDITABLE, e nada é gravado nem recalculado.

Dentro de PENDING_DOCS, o recálculo continua rodando — e isso é intencional. Adicionar, alterar ou remover participante ali dispara _recompute_credit_if_needed, que atualiza o resumo de crédito da Decision a partir da renda declarada. O AGENCY_USER não pode ler esse resumo, mas o dado de entrada é exatamente o que ele acabou de corrigir a pedido do analista — recalcular é o efeito desejado da correção, e não recalcular deixaria o analista decidindo sobre número velho. O que a RN13a impede é o recálculo em estado não editável, onde o parceiro não tem o que corrigir. - RN14. Nenhuma permissão é declarada sem endpoint que a exija, e nenhum endpoint novo fica sem porteiro. O teste de matriz da spec 0035 é a barreira.

A tela

  • RN15. O bloco da linha do tempo passa a exibir duas linhas de situação abaixo do trilho, em todos os sete estados, com o texto da tabela da seção 4.1 — variando por estado e pelo lado de quem abriu. Nenhum estado fica mudo.
  • RN16. A frase "Esta análise de crédito está aguardando início da análise. Use 'Iniciar análise' no topo da página." sai do painel de parecer. Ela duplicaria as linhas novas e vive num painel que a imobiliária não enxerga.
  • RN17. Em 375px o aviso de erro da tela vai abaixo do bloco da linha do tempo; no desktop continua acima. É a única diferença de posição entre os viewports.
  • RN18. A ação do analista em PENDING_DOCS chama-se "Resolver pendência" na tela e nas linhas de situação — um nome só. Ela leva à edição, como hoje.
  • RN19. As ações do topo derivam de permissão, nunca de autoria. Quem tem decisions:update lê a tela de quem decide; o teto de três se mantém, e o excedente desce para o painel de parecer.

8. Autorização e isolamento

Seção obrigatória. É onde se decide, antes de existir código, quem alcança o quê — e é o que o portão da seção 14 mede com mais rigor. Silêncio aqui não é "o padrão": o BaseRepository filtra por tenant sozinho, mas não resolve FK vinda do corpo, não escolhe entre 404 e 403, e não filtra nada para o ADMIN_GLOBAL.

Esta spec amplia escrita para dois papéis e cria uma transição nova. Não é feature de tela: AGENCY_USER — usuário externo à garantidora — ganha permissão de alterar dados de análise de crédito, incluindo participantes. É a mudança mais sensível aqui, e o que se segue existe para delimitá-la.

8.1 Matriz de papéis

O que muda nesta spec, e o que continua vedado. As colunas dizem respeito à análise de crédito no estado devolvido (PENDING_DOCS), salvo onde indicado.

Papel Passa a poder Continua não podendo
ADMIN_GLOBAL ter a permissão credit_analyses:resume, por herdar todas reenviar análise vinculada a imobiliária: 422 RESUME_WRONG_SIDE. A regra de lado (8.4) vence a permissão herdada — ele não tem partner_agency_id, logo nunca é o lado que recebeu a devolução numa análise de parceiro. Na carteira própria ele reenvia (ver 8.5)
ADMIN_TENANT reenviar análise da carteira própria do seu tenant reenviar análise de outro tenant (404); editar análise aprovada, reprovada ou cancelada
ANALYST editar (credit_analyses:update, nova para ele) qualquer análise do seu tenant em DRAFT ou PENDING_DOCS, inclusive as originadas por imobiliária, e seus participantes em qualquer estado (tem decisions:update) criar análise; editar fora de DRAFT/PENDING_DOCS; reenviar — 403, porque a RN12 não lhe dá credit_analyses:resume (não é 409: falta a permissão, antes de qualquer questão de lado)
OPERATOR reenviar qualquer análise da carteira própria do seu tenant, tenha ele cadastrado ou não reenviar análise vinculada a imobiliária (422 — o lado é dela); decidir (aprovar, reprovar, devolver); atualizar participante fora de DRAFT/PENDING_DOCS (422, RN13a)
VIEWER nada tudo: não edita, não reenvia, não decide. Lê a linha de somente-leitura da tela
AGENCY_USER editar (credit_analyses:update, nova) e reenviar (credit_analyses:resume, nova) a análise da própria imobiliária em PENDING_DOCS tocar análise de outra imobiliária (404) ou da carteira própria (404); editar a análise fora de DRAFT/PENDING_DOCS (422); atualizar participante fora desses dois estados (422, RN13a); decidir — aprovar, reprovar e devolver exigem decisions:update, que ele não tem; ler o parecer (decisions:read, que ele também não tem)

Três negativos que sustentam o resto:

  • credit_analyses:update não aproxima o AGENCY_USER de decidir. decisions:* permanece fora do papel dele — ele corrige o que cadastrou e devolve; quem julga é a garantidora.
  • Nem de reescrever a recomendação de crédito pela porta dos fundos. É a RN13a: sem a guarda de estado no PATCH /participants/{id}, alterar a renda declarada de um participante recalcularia a Decision de uma análise em curso ou aprovada. A guarda é condição para conceder a permissão, não melhoria opcional.
  • Autoria não dá poder, escopo dá. Nem para a imobiliária (qualquer usuário dela alcança as análises dela, pelo partner_agency_id) nem para a garantidora (qualquer OPERATOR do tenant reenvia as da carteira própria). O sistema não escopa por criador em lugar nenhum, e abrir exceção aqui travaria a análise quando a pessoa saísse.

8.2 Permissões

Endpoint Permissão Situação
POST /credit-analyses/{id}/resume credit_analyses:resume nova
PATCH /credit-analyses/{id} credit_analyses:update existente; ganha dois papéis
POST /credit-analyses/{id}/participants credit_analyses:update existente; ganha os dois papéis por consequência; aceita renter_id no corpo
PATCH /credit-analyses/{id}/participants/{participant_id} credit_analyses:update existente; ganha guarda de estado (RN13a)
DELETE /credit-analyses/{id}/participants/{participant_id} credit_analyses:update existente; já restrito a DRAFT/PENDING_DOCS
GET /credit-analyses/{id}/participants credit_analyses:read inalterada
POST /credit-analyses/{id}/decision/pend decisions:update inalterada

credit_analyses:resume entra em _PERMISSIONS e nos papéis AGENCY_USER, OPERATOR, ADMIN_TENANT e ADMIN_GLOBAL em _ROLE_PERMISSIONS (app/db/rbac_seed.py). credit_analyses:update é acrescentada a AGENCY_USER e ANALYST.

A consequência da terceira linha é a mais fácil de perder. credit_analyses:update governa também o CRUD de participantes. Concedê-la ao AGENCY_USER destranca POST /participants, que aceita renter_id no corpo — a primeira FK de corpo alcançável por usuário de imobiliária nesta rota. A RN5 da spec 0034 exige que ela seja resolvida no escopo do solicitante antes de gravar; o teste negativo da seção 11 (CA9) existe para provar que é.

8.3 Isolamento por garantidora (tenant)

  • FKs no corpo. CreditAnalysisUpdate só tem escalares (rental_value, condo_fee, property_tax, guarantee_months, start_date, end_date, notes) — nenhuma FK, nada a resolver. O reenvio não tem corpo. A FK que entra em cena é o renter_id dos participantes (8.2), resolvido no escopo do solicitante antes de gravar; renter_id que não resolva → 404, e nada é persistido.
  • Escopo de leitura e escrita. Reenvio e edição passam pelo mesmo _get_authorized que as demais operações de análise, que aplica tenant_id e partner_agency_id. Nenhum caminho novo consulta o repositório direto.
  • Análise de outro tenant → 404, nunca 403 (RN4 da 0034). O 403 fica para o que o usuário não pode fazer no próprio tenant — por exemplo, um VIEWER chamando o reenvio de uma análise da própria garantidora.

8.4 Isolamento por imobiliária

  • O AGENCY_USER alcança apenas análises cujo partner_agency_id é o dele. Análise de outra imobiliária, ou da carteira própria (partner_agency_id nulo), responde 404 — não 403, pela mesma razão da RN4.
  • O vínculo continua derivado, nunca aceito do corpo (RN10/RN12 da 0034). Nem a edição nem o reenvio recebem partner_agency_id, e nenhum dos dois o altera. Corrigir a análise não muda a imobiliária de origem.
  • Reenvio é do lado que recebeu a devolução. Análise com partner_agency_id: quem reenvia é a imobiliária vinculada, e um OPERATOR da garantidora chamando o endpoint recebe 422 — não é o lado dele, ainda que a permissão exista no papel. Análise da carteira própria: o inverso, e o AGENCY_USER sequer a enxerga (404).
  • Documento continua herdando o escopo do dono (RN13 da 0034). O anexo pela imobiliária não abre caminho novo: é o mesmo endpoint de documentos, com o mesmo escopo, já em produção.

8.5 Plataforma × cliente (SaaS admin)

  • Quando quem chama é ADMIN_GLOBAL: o reenvio é ação sobre um registro, resolvido por _get_authorized, que para esse papel não filtra por tenant — ele pode reenviar a análise de qualquer garantidora. Isso não é novidade desta spec: vale hoje para aprovar, reprovar, devolver, editar e cancelar. Nenhum endpoint de listagem é criado ou alterado aqui, então não há superfície nova pela qual dado de negócio de vários tenants passe a sair junto.
  • A interface do SaaS admin (/app/admin/*) não ganha nada. Nenhuma tela nova, nenhum campo novo, nenhuma contagem nova. As linhas de situação, o diálogo renomeado e o botão de reenvio vivem na tela de análise de crédito, que é da garantidora.
  • Nenhuma tela da garantidora recebe tenant_id (RN7 da 0034). As duas rotas novas ou alteradas derivam o tenant da sessão.
  • O que esta spec conscientemente não conserta: o ADMIN_GLOBAL poder agir sobre dado de negócio de um cliente é uma fronteira frouxa que já existe e é maior que esta feature. Fechá-la exigiria revisar todas as operações de análise, garantia e cobrança — trabalho de spec própria. Fica registrado, não resolvido, e a seção 13 aponta o risco.

8.6 De onde vem o motivo que a imobiliária lê

A tela do AGENCY_USER passa a exibir o motivo da devolução, e ele não tem decisions:read. O caminho curto seria acrescentar decision_reason ao CreditAnalysisResponse — e junto dele viria opinion, o parecer do analista. Isso entregaria a um parceiro externo o texto que o modelo de permissão protege.

  • O motivo sai de CreditAnalysisStatusHistory.reason, na linha IN_ANALYSIS → PENDING_DOCS mais recente. É o mesmo texto que o analista digitou (o _decide grava nos dois lugares), e a leitura do histórico já é governada por credit_analyses:read, que o AGENCY_USER tem. Nenhum campo novo, nenhuma permissão nova.
  • decision_reason e opinion não entram no CreditAnalysisResponse, nem agora nem como efeito colateral. Quem quiser o parecer continua passando por GET /decision, que exige decisions:read.
  • O painel de parecer continua não renderizando para quem não tem decisions:read — é por isso que a explicação do passo mudou de lugar (RN16), e não o contrário.

8.7 O nome de quem agiu não atravessa para o parceiro

Mesma pergunta de 8.6, outro dado. As linhas de situação nomeiam quem devolveu, reprovou ou cancelou — e esse nome é de um funcionário da garantidora. Para o parceiro externo, ele não sai:

  • O nome não aparece para lado nenhum, e isso ficou mais estrito do que esta seção previa quando foi escrita. A regra abaixo — não acrescentar nome ao histórico — tirou a única fonte disponível, inclusive para quem é da garantidora. As linhas dizem a data e o lado.
  • Para quem cadastrou, quando é imobiliária, a linha diz "pela garantidora". Na carteira própria os dois lados são do mesmo tenant, mas continua sem nome pelo mesmo motivo: não há de onde tirá-lo.
  • Decisão registrada (Marcus Floriano, 2026-08-12), entre três alternativas: expor o nome só a quem tem decisions:read (resposta variando por papel na mesma rota) e aproveitar o que já sai hoje (created_by_user, approved_by_user) foram recusadas — a primeira por complicar o contrato, a segunda por assumir um vazamento pré-existente em vez de fechá-lo. A tabela da seção 4.1 foi corrigida para não prometer o que esta seção impede.
  • CreditAnalysisStatusHistoryResponse não ganha nome nem e-mail. Ele devolve changed_by como UUID, e continua assim. Acrescentar o nome ali seria o caminho curto e entregaria a estrutura interna da garantidora a quem é de fora — exatamente o defeito que a spec 0035 corrigiu no menu.
  • A data e o motivo saem para os dois lados: são o conteúdo do pedido, não a identidade de quem o fez.

9. Impacto por app

apps/api

  • Modelos: nenhum campo novo, nenhuma tabela nova. Muda uma linha de dado: ALLOWED_TRANSITIONS[PENDING_DOCS] passa a incluir IN_ANALYSIS (services/credit_analysis_service.py).
  • Migration Alembic: nenhuma. Não há mudança de schema. O que precisa acontecer no banco é o seed_rbac, que insere a permissão nova (ON CONFLICT DO NOTHING) e ressincroniza role_permissions dos papéis de sistema — é idempotente e já roda em init_db. No deploy de produção isso é passo explícito, como as migrations: subir imagem nova sem rodar o seed deixa o endpoint exigindo permissão que não existe em papel nenhum, e todo mundo toma 403.
  • Endpoints:
  • POST /credit-analyses/{id}/resume — novo, credit_analyses:resume, sem corpo, devolve a análise atualizada.
  • PATCH /credit-analyses/{id} — existente; ganha o recálculo financeiro (RN9/RN10) e o aviso de escrita concorrente (RN11).
  • POST /credit-analyses/{id}/decision/pend — existente; reason passa a ser obrigatório (RN7).
  • Regras no service:
  • CreditAnalysisService.resume() — valida estado, valida lado (RN2/8.4), chama _transition para IN_ANALYSIS, e pede ao AnalysisService que devolva a Decision a IN_PROGRESS (RN3). Tudo numa transação: ou as duas mudam, ou nenhuma.
  • CreditAnalysisService.update() — recalcula o snapshot quando algum dos quatro campos de preço mudar, pela mesma função que a criação usa (nada de fórmula duplicada).
  • AnalysisDecisionRequest — reason deixa de ser opcional na rota de pend.
  • O renter_id dos participantes continua resolvido no escopo pelo serviço de participantes; esta spec não cria caminho novo, mas passa a exercê-lo com usuário de imobiliária (8.2).
  • Documentação: corrigir a docstring de AnalysisService.pend, que promete notificação inexistente; documentar a transição nova no docstring do módulo de credit_analysis.py, onde o diagrama de estados está desenhado.
  • Testes: caminho feliz do reenvio, recálculo do snapshot, motivo obrigatório, e os negativos de acesso da seção 11 (CA6–CA10).

apps/web

  • Páginas/componentes:
  • pages/credit-analyses/components/StatusTimeline.tsx — recebe as duas linhas de situação (RN15), parametrizadas por estado e lado.
  • pages/credit-analyses/components/DecisionPanel.tsx — sai a frase do estado SUBMITTED (RN16).
  • pages/credit-analyses/show.tsx — rótulo da devolução por origem (RN6), ação "Devolver ao analista" com confirmação, Alert de erro reposicionado em xs (RN17), motivo pré-preenchido no diálogo (RN8).
  • pages/credit-analyses/edit.tsx (ou o CreditAnalysisForm) — motivo da devolução repetido no topo quando o estado é PENDING_DOCS.
  • providers/ — chamada ao endpoint novo.
  • data-testid novos: credit-analysis-resume-button, credit-analysis-resume-confirm, credit-analysis-resume-cancel, credit-analysis-situation-step, credit-analysis-situation-action, credit-analysis-return-reason.
  • Sider: sem impacto — nenhum módulo novo.
  • Menu e rota por permissão: sem item de menu novo. As ações da tela aparecem por permissão; a rota de edição já existe e continua protegida pelo servidor — esconder botão não protege rota (spec 0035).
  • Responsividade & PWA: as linhas de situação em coluna única, o Alert abaixo do trilho em xs, o diálogo com campo acima dos botões. PWA sem impacto: nenhum asset novo, nenhuma tela offline; o bundle cacheado se resolve com o rebuild de sempre.

apps/e2e

  • Specs:
  • retorno-analise-imobiliaria.spec.ts — jornada inteira: analista analisa, devolve com motivo, imobiliária lê o motivo, corrige, reenvia, análise reaparece em IN_ANALYSIS para o analista.
  • Cobertura negativa: AGENCY_USER da Imobiliária B recebe 404 ao reenviar análise da A; VIEWER não vê as ações.
  • Nos dois viewports, com a asserção de que as linhas de situação têm texto visível em 375px.
  • Seeds: tests/fixtures/seeds/ precisa de uma análise em PENDING_DOCS com Decision em PENDING e motivo preenchido, vinculada à imobiliária de teste — hoje não existe.

10. Contrato de API

Reenviar ao analista — novo

POST /api/v1/credit-analyses/{credit_analysis_id}/resume
Authorization: Bearer <token>
Permissão: credit_analyses:resume
(sem corpo)
{ "id": "uuid", "credit_analysis_number": "P202608-00042", "status": "IN_ANALYSIS", "...": "demais campos de CreditAnalysisResponse" }
Código Quando Corpo
200 reenviada análise com status: IN_ANALYSIS
403 papel sem credit_analyses:resume, no próprio tenant erro de permissão
404 análise de outro tenant, de outra imobiliária, ou inexistente indistinguíveis entre si (RN4 da 0034)
422 INVALID_STATUS_TRANSITION — análise não está em PENDING_DOCS nada é gravado
422 RESUME_WRONG_SIDE — quem chama não é o lado que recebeu a devolução (RN2) nada é gravado
422 DECISION_NOT_FOUND — análise em PENDING_DOCS sem Decision (RN3) nada é gravado

422, não 409. A spec dizia 409 até a implementação. No código, erro de regra de negócio é BusinessRuleError, que mapeia para 422 — é o que INVALID_STATUS_TRANSITION já devolve hoje em submit, cancel, approve e reject. Trocar para 409 só nas rotas novas criaria duas convenções para a mesma classe de erro; trocar em todas mudaria contrato alheio a esta feature. O que importa — código de negócio nomeado e nada persistido — vale nos dois casos.

Devolver — existente, com motivo obrigatório

POST /api/v1/credit-analyses/{credit_analysis_id}/decision/pend
Authorization: Bearer <token>
Permissão: decisions:update

{ "reason": "Falta comprovante de renda do inquilino principal." }

422 quando reason vier ausente, nulo ou só com espaços (RN7). Os demais códigos não mudam.

Editar — existente, com recálculo

PATCH /api/v1/credit-analyses/{credit_analysis_id}
Authorization: Bearer <token>
Permissão: credit_analyses:update

{ "rental_value": "3200.00", "expected_updated_at": "2026-08-12T14:03:21Z" }
{ "id": "uuid", "status": "PENDING_DOCS", "rental_value": "3200.00",
  "financial_summary": { "rental_package": "…", "monthly_value": "…" },
  "updated_at": "2026-08-12T14:07:55Z",
  "concurrent_update": true }
  • expected_updated_at — datetime, opcional no corpo (CreditAnalysisUpdate). É o updated_at que o cliente carregou. Omitido, não há verificação. Não é coluna: o update() hoje faz setattr em laço sobre model_dump(exclude_none=True), então o campo tem de ser lido e retirado antes do laço — senão o serviço tenta gravar um atributo que o modelo não tem.
  • concurrent_update — bool, sempre presente na resposta. É true quando expected_updated_at foi enviado e não bate com o updated_at corrente. A gravação acontece de todo jeito (RN11): é aviso, não recusa. É campo calculado, não coluna — nenhuma migration.
  • A resposta traz o financial_summary já recalculado (RN9/RN10), com as TenantSettings da garantidora dona da análise.
  • 422 CREDIT_ANALYSIS_NOT_EDITABLE continua para estado não editável.

Atualizar participante — existente, com guarda nova

PATCH /api/v1/credit-analyses/{id}/participants/{participant_id}
Authorization: Bearer <token>
Permissão: credit_analyses:update

{ "declared_income": "9000.00" }

422 PARTICIPANT_NOT_EDITABLE quando quem chama não tem decisions:update e a análise não está em DRAFT nem em PENDING_DOCS (RN13a). Nada é gravado e o recálculo de crédito não roda. Quem tem decisions:update mantém o comportamento atual, em qualquer estado.

Migration

Nenhuma. Não há mudança de schema — logo, não há downgrade a escrever. O caminho de volta de um deploy ruim aqui é a imagem anterior: a permissão nova fica órfã no banco (inofensiva, pois nenhum endpoint a exige na versão antiga) e a transição extra deixa de existir junto com o código. O passo de banco é o seed_rbac, idempotente, e ele precisa rodar no deploy — sem ele, o endpoint novo responde 403 para todo mundo.

11. Critérios de aceite

O caminho de volta

  • [x] CA1. Dado uma análise em PENDING_DOCS vinculada à Imobiliária A, quando um usuário da Imobiliária A chama POST /resume, então a análise fica em IN_ANALYSIS, a Decision volta a IN_PROGRESS, e o histórico ganha uma linha PENDING_DOCS → IN_ANALYSIS com o autor e sem motivo.
  • [x] CA2. Dado a mesma análise, quando o reenvio é feito, então ela aparece no filtro "Em análise" da lista do analista sem nenhuma ação da garantidora.
  • [x] CA3. Dado uma análise em IN_ANALYSIS (ou qualquer estado que não PENDING_DOCS), quando alguém chama POST /resume, então a resposta é 422 INVALID_STATUS_TRANSITION e nada muda — nem status, nem Decision, nem histórico.

A devolução e a correção

  • [x] CA4. Dado uma análise em IN_ANALYSIS, quando o analista devolve sem motivo, então a resposta é 422 e a análise continua em IN_ANALYSIS.
  • [x] CA5. Dado uma análise devolvida com aluguel de R$ 3.000, quando a imobiliária altera o aluguel para R$ 3.200 e salva, então o financial_rental_package e o financial_monthly_value da resposta refletem R$ 3.200 — e não os valores calculados na criação.
  • [x] CA6. Dado uma análise aprovada, quando se tenta editá-la, então a resposta é 422 CREDIT_ANALYSIS_NOT_EDITABLE e nenhum campo financeiro é recalculado.

Negativos de autorização — um por papel afetado

  • [x] CA7. AGENCY_USER da Imobiliária B chamando POST /resume numa análise da Imobiliária A recebe 404 (não 403), e a análise da A não muda de estado.
  • [x] CA8. AGENCY_USER chamando POST /resume numa análise da carteira própria (sem partner_agency_id) recebe 404.
  • [x] CA9. AGENCY_USER da Imobiliária A criando participante com um renter_id da Imobiliária B recebe 404 e nada é gravado — a FK do corpo é resolvida no escopo dele (8.2).
  • [x] CA10. AGENCY_USER chamando POST /decision/pend ou /decision/approve recebe 403 — ganhar credit_analyses:update não o aproximou de decidir.
  • [x] CA11. VIEWER chamando POST /resume numa análise do próprio tenant recebe 403 (não 404: o registro é do tenant dele, o que falta é permissão).
  • [x] CA12. OPERATOR da garantidora chamando POST /resume numa análise vinculada a imobiliária recebe 422 RESUME_WRONG_SIDE — a permissão existe no papel, mas o lado não é o dele.
  • [x] CA13. Usuário da Garantidora B chamando qualquer uma das três rotas com o ID de uma análise da Garantidora A recebe 404.
  • [x] CA14. O teste de matriz de autorização (spec 0035) continua verde: o endpoint novo tem porteiro, e credit_analyses:resume é exigida por pelo menos um endpoint.
  • [x] CA14a. AGENCY_USER chamando PATCH /participants/{id} numa análise IN_ANALYSIS da própria imobiliária recebe 422 PARTICIPANT_NOT_EDITABLE; o declared_income do participante não muda e a Decision mantém credit_recommendation, income_to_rent_ratio e total_declared_income idênticos aos de antes da chamada. Mesmo resultado com a análise APPROVED.
  • [x] CA14b. ANALYST (que tem decisions:update) chamando a mesma rota na mesma análise IN_ANALYSIS recebe 200 e o recálculo roda — a guarda separa por permissão, não por estado apenas.
  • [x] CA14c. ANALYST chamando POST /credit-analyses recebe 403: ganhar credit_analyses:update não lhe deu credit_analyses:create.
  • [x] CA14d. GET /credit-analyses/{id} como AGENCY_USER não traz decision_reason nem opinion no corpo, em nenhum estado — o motivo que a tela dele exibe vem do histórico de status (8.6).
  • [x] CA14e. ADMIN_GLOBAL editando o aluguel de uma análise da Garantidora A recalcula com as TenantSettings da Garantidora A (RN10) — não com as de outro tenant, nem com valores padrão.
  • [x] CA14f. ADMIN_GLOBAL chamando POST /resume numa análise vinculada a imobiliária recebe 422 RESUME_WRONG_SIDE — a regra de lado vence a permissão herdada (8.1).
  • [x] CA14g. AGENCY_USER abrindo o histórico de uma análise reprovada não recebe nome nem e-mail de quem reprovou: changed_by continua sendo só o UUID (8.7).

Todos verificados. CA16 e CA19 ficaram parciais até a validação visual: a captura no dev-remote cobriu Enviada, Em análise e Devolvida pelos dois lados, em 1440px e 375px, com rolagem horizontal 0px em todas as dez telas — e foi ela que pegou a enumeração de ícones fora de ordem. Seguem sem cobertura automatizada os quatro estados finais (DRAFT, APPROVED, REJECTED, CANCELLED) das linhas de situação: o apps/web não tem runner de unidade, e cobri-los exigiria um teste e2e por estado.

Caminho feliz do outro lado

  • [x] CA21. Dado uma análise da carteira própria em PENDING_DOCS, quando um OPERATOR do tenant que não a cadastrou chama POST /resume, então recebe 200 e a análise vai a IN_ANALYSIS — escopo é por tenant, não por autoria (RN2, 8.1).

A tela

  • [x] CA15. Na análise enviada, em 375px, existe texto visível na tela contendo a palavra "analisar" e a palavra "devolver" — sem abrir menu, sem tooltip, sem aria-label.
  • [x] CA16. Em cada um dos sete estados, o bloco da linha do tempo mostra as duas linhas de situação, com o texto correspondente ao lado de quem abriu.
  • [x] CA17. A frase "Use 'Iniciar análise' no topo da página" não existe mais em lugar nenhum da tela.
  • [x] CA18. Na análise devolvida, o usuário da imobiliária lê o motivo sem abrir o histórico, nos dois viewports.
  • [x] CA19. Em 375px, scrollWidth - clientWidth == 0 na tela da análise e no formulário de edição, em todos os sete estados.
  • [x] CA20. Com erro na tela em 375px, o aviso aparece abaixo do bloco da linha do tempo, e as linhas de situação continuam visíveis sem rolar.

12. Plano de implementação

Ordem obrigatória no monorepo: api → web → e2e.

  • [x] apps/api — primeiro a guarda de estado do PATCH /participants/{id} (RN13a): ela é condição para conceder a permissão, não pode vir depois
  • [x] apps/api — credit_analyses:resume em _PERMISSIONS e nos quatro papéis; credit_analyses:update em AGENCY_USER e ANALYST (db/rbac_seed.py)
  • [x] apps/api — ALLOWED_TRANSITIONS[PENDING_DOCS] += IN_ANALYSIS
  • [x] apps/api — CreditAnalysisService.resume() com validação de estado e de lado, devolvendo a Decision a IN_PROGRESS na mesma transação
  • [x] apps/api — recálculo do snapshot no update(), pela função da criação
  • [x] apps/api — reason obrigatório no pend
  • [x] apps/api — rota POST /credit-analyses/{id}/resume
  • [x] apps/api — sem migration; conferir que o seed_rbac insere a permissão nova
  • [x] apps/api — docstrings + Swagger; corrigir a docstring do pend que promete notificação inexistente; atualizar o diagrama de estados do módulo
  • [x] apps/api — testes: CA1–CA6 e os negativos CA7–CA14
  • [x] apps/web — linhas de situação nos sete estados × lado (StatusTimeline)
  • [x] apps/web — remover a frase do DecisionPanel (RN16)
  • [x] apps/web — rótulo por origem, confirmação do reenvio, motivo pré-preenchido, Alert reposicionado em xs, motivo no topo do formulário
  • [x] apps/web — data-testid da seção 9, responsivo nos dois viewports
  • [x] apps/e2e — spec da jornada + negativos, nos dois viewports; seed da análise em PENDING_DOCS
  • [x] make test passando (316; a intermitente do filtro de imobiliárias alterna)
  • [x] Validado no browser em desktop e mobile, contra o dev-remote, em dez capturas

13. Decisões em aberto

Nenhuma pergunta trava a implementação. As três que travavam foram respondidas durante a validação da Parte I e estão registradas na seção 6.

Riscos e decisões registrados, não pendentes. Todos aceitos por Marcus Floriano, durante as entrevistas de validação desta spec, em 2026-08-12:

  • O ADMIN_GLOBAL age sobre dado de negócio de qualquer garantidora. Fronteira frouxa pré-existente, que esta spec exerce mas não amplia (8.5) — nenhum endpoint de listagem novo. Fechá-la é spec própria, e vale para análise, garantia e cobrança.
  • O snapshot financeiro deixa de ser congelado na criação (RN10) e passa a ser congelado na última confirmação, com as TenantSettings vigentes no recálculo. É mudança de significado de um campo que existe para auditabilidade, e é a decisão mais cara desta spec. Escolhida explicitamente pelo usuário entre quatro alternativas (recalcular com parâmetros da criação, bloquear a edição do aluguel, ou deixar o preço dessincronizado), pela razão de que análise ainda em curso não tem condição contratada a preservar — a garantia não foi emitida — e porque análise aprovada não recalcula.
  • Escrita concorrente resolve por último-a-salvar-vence (RN11), com aviso e sem bloqueio. Perde-se alteração se dois lados editarem o mesmo campo ao mesmo tempo; aceito para não travar a análise com uma tela esquecida aberta.
  • credit_analyses:update para o AGENCY_USER destranca participantes junto (8.2), porque é a mesma permissão. É desejado — trocar inquilino era o pedido —, mas é ampliação maior do que "editar a análise" sugere. O CA9 (FK de outra imobiliária) e o CA14a (guarda de estado) existem por causa disso, e a RN13a é o que torna a concessão segura. Sem ela, esta spec abriria escalada de privilégio para usuário externo — foi o que a validação do detalhamento pegou.
  • Não há registro de autoria de edição. CreditAnalysis não tem updated_by e edição não gera linha de histórico. Aceito nesta spec (o aviso de escrita concorrente fica impessoal, RN11a); se um dia a auditoria pedir "quem mudou o aluguel", é coluna nova e migration.

14. Validação do detalhamento

Preenchido pelo laço de validação do /spec new (subagente spec-detail-validator). Registrar todas as tentativas, inclusive as reprovadas.

Tentativa Segurança Clareza Final Veredicto
1 7,8 7,0 7,0 REPROVADA
2 8,5 8,5 8,5 APROVADA

Resultado: aprovada na tentativa 2 com nota 8,5 (segurança 8,5 · clareza 8,5; mínimo: acima de 8,0).

O que a tentativa 1 pegou, e valeu o portão inteiro: uma escalada de privilégio que esta spec teria introduzido. PATCH /participants/{id} é permitido em qualquer status e chama _recompute_credit_if_needed, que reescreve credit_recommendation, income_to_rent_ratio e total_declared_income na Decision. Conceder credit_analyses:update ao AGENCY_USER — usuário externo, sem decisions:read — deixaria um parceiro reescrever a recomendação de crédito de uma análise em andamento ou já aprovada, alterando dado que ele não pode nem ler. Virou a RN13a, é condição para a concessão, e é o primeiro item do plano de implementação. Junto vieram: o recálculo que usaria TenantSettings de tenant nenhum quando o solicitante é ADMIN_GLOBAL (RN10), e o parecer do analista que vazaria ao parceiro se o motivo da devolução viesse de decision_reason em vez do histórico (8.6).

Depois da aprovação, fecharam-se também os sete pontos de melhoria da tentativa 2 — o nome do funcionário da garantidora que não atravessa para o parceiro (8.7), o ADMIN_GLOBAL no reenvio, o expected_updated_at que não é coluna, e três CAs novos.

Riscos aceitos: nenhum risco de acesso ficou aberto. Os cinco itens registrados na seção 13 são decisões conscientes, atribuídas e datadas — a mais cara delas, a mudança de significado do snapshot financeiro (RN10), foi escolhida pelo usuário entre quatro alternativas. Segue aberta e fora desta spec a fronteira pré-existente do ADMIN_GLOBAL sobre dado de negócio de clientes (8.5).

15. Validação da implementação

Preenchido pelo laço de validação do /spec implement (subagente spec-implementation-validator). Registrar todas as tentativas.

Tentativa Aderência Qualidade Testes Final Veredicto
1 8,0 8,0 8,3 8,0 REPROVADA
2 8,3 8,5 7,4 7,4 REPROVADA
3 8,7 7,5 7,8 7,5 REPROVADA
4 8,8 8,5 8,6 8,5 APROVADA

Resultado: aprovada na tentativa 4 com nota 8,5 (aderência 8,8 · qualidade 8,5 · testes 8,6; mínimo: acima de 8,0).

O que o laço pegou, rodada a rodada — e a leitura vale mais que as notas:

  1. 8,0 — a RN11 existia só no servidor: o expected_updated_at nunca era enviado pela tela, então o aviso de escrita concorrente não chegava a ninguém. Mais: o motivo digitado se perdia no erro, não havia validação de motivo obrigatório no cliente, e o ADMIN_TENANT acumulava quatro ações no topo — o teto de três se mantinha por truncamento silencioso do array, não por regra.
  2. 7,4 — o arranjo do e2e chamava POST /decision sem corpo, que a rota exige: os nove testes morriam antes do primeiro expect. Junto, três defeitos de tela: a imobiliária lia "Resolver pendência" onde a spec manda "Editar", a análise reprovada nunca mostrava o motivo, e a falha ao carregar a imobiliária sumia com o botão de devolver sem dizer nada.
  3. 7,5 — a correção da rodada anterior virou regressão: reloadAll() no catch ligava o loading, o esqueleto desmontava a árvore e levava junto o diálogo aberto, destruindo o motivo que o ConfirmDialog acabara de ser ensinado a preservar. Também apareceram dois Swaggers desatualizados nos endpoints que esta spec altera, um 500 em potencial na comparação de datetime com e sem fuso (invisível em SQLite), e dois testes meus que não guardavam nada — o ADMIN_GLOBAL do cenário tinha tenant_id, e o snapshot semeado coincidia com o que o recálculo produziria.
  4. 8,5 — aprovada.

Defeitos aceitos: nenhum. Os dois apontados na rodada aprovada foram corrigidos depois dela (rótulo da devolução quando a busca da imobiliária falha; motivo ausente no formulário quando o histórico falha), junto com o TAGS_METADATA.

Fica aberto, e é limitação do ferramental: situacao.ts são 7 estados × 3 lados × 2 viewports de texto. O e2e e a captura visual cobrem os três estados do fluxo (Enviada, Em análise, Devolvida) pelos dois lados; os quatro finais (DRAFT, APPROVED, REJECTED, CANCELLED) não têm verificação. O apps/web não tem runner de teste unitário, então fechar isso exige ou um e2e por estado, ou introduzir um runner no web — decisão de ferramental que esta spec não tomou e não deveria tomar.

16. Registro

O que divergiu do planejado, e por quê

  • 422 em vez de 409. A Parte II especificava 409 para INVALID_STATUS_TRANSITION, RESUME_WRONG_SIDE, DECISION_NOT_FOUND e PARTICIPANT_NOT_EDITABLE. No código, erro de regra de negócio é BusinessRuleError, que mapeia para 422 — e é o que INVALID_STATUS_TRANSITION já devolvia em submit, cancel, approve e reject. Usar 409 só nas rotas novas criaria duas convenções para a mesma classe de erro; mudar todas alteraria contrato alheio a esta feature. A spec foi corrigida.
  • O nome de quem agiu não aparece para lado nenhum. A tabela da seção 4.1 pedia "por «Fulano»", e a seção 8.7 — escrita para fechar um vazamento — proíbe pôr nome no histórico de status, que é a única fonte. As duas seções se contradiziam. Decisão do usuário entre três alternativas: sem nome, e a spec se ajusta. A tabela foi corrigida.
  • partner_agency_id não vem na resposta da análise. O vínculo é derivado do imóvel, que a tela já carrega. Isso trouxe o defeito do rótulo invertido quando a busca falha, corrigido com await e com um PartnerAgency sem nome como fallback — o vínculo existe mesmo quando o nome não carrega.
  • O reenvio consulta AnalysisRepository direto, em vez de pedir ao AnalysisService como a seção 9 previa. Evita import circular e não sai da camada.
  • Sem seed em tests/fixtures/seeds/. O e2e monta o estado pela API, como os specs vizinhos de análise de crédito já fazem — é mais robusto que semear num banco compartilhado, e o cabeçalho do arquivo explica a escolha.

Efeitos colaterais que valem registro

  • test_decision_reason_e_transicoes.py mudou de sinal. Ele afirmava que PENDING_DOCS → IN_ANALYSIS não existe — verdade do fix 0006, que a removeu por ser transição declarada e nunca exercida. Em vez de só inverter a asserção, ele agora exige as duas coisas juntas: a entrada na tabela e a rota que a executa. A lição do 0006 virou afirmação, em vez de ausência.
  • _STATUS_EDITAVEIS substituiu a tupla (DRAFT, PENDING_DOCS) repetida em três pontos do serviço de participantes; a guarda nova seria a quarta cópia.
  • ConfirmDialog ganhou initialReason, reasonRequired e testIdPrefix — os três úteis fora desta spec. O testIdPrefix existe porque a tela tem três diálogos e todos confirmavam pelo mesmo seletor: o e2e não conseguia dizer qual havia confirmado.
  • Um segundo teste intermitente identificado. O CLAUDE.md documenta test_partner_agency_search.py::test_busca_alcanca_quem_esta_fora_da_primeira_pagina; o irmão test_partner_agency_status_filter.py::test_alcanca_quem_esta_fora_da_primeira_pagina tem o mesmo defeito e falhou em 2 de 3 execuções seguidas, sem alteração de código entre elas. Não tem relação com esta spec.

O que só a execução pegou

Duas coisas que nem os 316 testes de API nem o tsc encontrariam:

  • A imobiliária não conseguia abrir a tela de edição — ou seja, a metade da feature em que ela corrige o que cadastrou era inalcançável pela interface. O formulário busca /partner-agencies, que ela não pode listar desde a spec 0034, e o handle403 do api-fetch faz window.location.href antes do .catch() de quem chamou; ela era expulsa para a lista. O .catch(() => []) que parecia proteger não protege nada — está documentado no próprio api-fetch, e o /settings ali ao lado já usava tolerate403 pela mesma razão. Os testes de API passavam porque chamam os endpoints direto.
  • A enumeração dos ícones estava na ordem errada. Em 375px o texto dizia "✓ aprovar · ✕ reprovar · relógio devolver" e na tela os ícones aparecem relógio, ✕, ✓: o PageHeader renderiza terciária → secundária → primária, então a principal fica à direita. Numa fila de três ícones parecidos a posição é a única pista que o texto oferece. Só a captura visual pegaria isso.

Também quebrei um teste da spec 0015 ao trocar o texto do alerta de pendência no formulário — o texto mudou de propósito (agora carrega o motivo), e a asserção foi atualizada para verificar o motivo, que é mais forte que a anterior.

Encontrado depois de fechar, ao abrir a tela de verdade

O formulário oferecia "Enviar para análise" numa análise devolvida. É a transição DRAFT → SUBMITTED, que a API recusa em PENDING_DOCS — e era o botão que a imobiliária naturalmente apertaria ao terminar de corrigir, devolvendo-lhe o erro cru "Não é possível alterar o status de 'PENDING_DOCS' para 'SUBMITTED'".

Nem o e2e nem os 316 testes de API pegariam: o comportamento é anterior a esta spec, e até agora ninguém chegava nessa tela nesse estado — a imobiliária era expulsa pelo 403 do /partner-agencies, e a garantidora usava o caminho de "Resolver pendência". Foi o usuário abrindo o dev-remote que o encontrou.

A Parte I decidiu que devolver ao analista é ação da tela da análise, não do formulário (4.2), para a pessoa conferir o que salvou antes de passar a bola — mas não disse o que o botão de concluir do formulário vira em PENDING_DOCS. Ficou: "Concluir e ver a análise", que leva exatamente para onde o alerta do topo já instrui. O "Concluir depois" saiu nesse estado, porque iria para o mesmo lugar. Coberto por teste e2e.

Verificação

  • make test: 316 testes, sendo 29 desta spec. A última rodada limpa deu 316 passando, zero falhas; a rodada seguinte, sem nenhuma alteração de código, deu 315 + a intermitente do filtro de imobiliárias descrita acima — ela alterna. Nenhuma falha atribuível a esta spec em rodada nenhuma. Roda com docker exec garantora-devremote-api-1 uv run pytest; o container embute o código, então cada rodada exigiu /devremote-rebuild antes, senão mediria a versão anterior.
  • tsc --noEmit do web: limpo.
  • e2e: escrito e EXECUTADO. Suíte inteira: 199 passaram, 9 falharam, 2 puladas. Duas falhas eram desta spec e foram corrigidas (ver abaixo); reexecutados os dois specs afetados: 23 passando, zero falhas. As outras sete tocam áreas fora deste diff — SaaS admin (4), formulário de imóvel, painel de documentos e lista de inquilinos do isolamento. Não provei que são pré-existentes: isso exigiria uma rodada de baseline com estas mudanças em stash e outro rebuild. O que se sabe é que nenhum desses componentes foi tocado aqui.
  • Validado no browser, dev-remote, dez capturas em 1440px e 375px, nos estados Enviada / Em análise / Devolvida pelos dois lados. Rolagem horizontal: 0px em todas.