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:
- 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.
- 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.
- 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.
- 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.
- 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 oADMIN_TENANTque 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 oAGENCY_USERnã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_USERcorrige 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_TENANTque 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:updateprevalece: 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_DOCSteve 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 emALLOWED_TRANSITIONSe 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 emPENDING_DOCS. Em qualquer outro estado a resposta é422comINVALID_STATUS_TRANSITION, e nada é gravado. - RN3. O reenvio devolve a
DecisiondePENDINGparaIN_PROGRESS— é o que o próprio modelo já documenta ("Retorna aIN_PROGRESSquando retomada"). Análise semDecisionnão pode estar emPENDING_DOCS; se estiver, é dado inconsistente e o endpoint responde422, sem criar parecer nenhum. - RN4. O reenvio grava
CreditAnalysisStatusHistorycomfrom_status=PENDING_DOCS,to_status=IN_ANALYSIS,changed_by= quem reenviou ereasonnulo — o reenvio não pede texto (Parte I, custo de uso). Oopinione odecision_reasonda 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 comreasonem branco, responde422. 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_taxouguarantee_months— emDRAFTePENDING_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-sefinancial_rental_package,financial_monthly_value,financial_initial_payment,financial_first_month_upfront,financial_lmi_valueefinancial_exit_costs_value, pela mesma função que a criação usa. - RN10. O recálculo usa as
TenantSettingsda garantidora dona da análise (credit_analysis.tenant_id), nunca o escopo de quem chama. Isso importa porque oADMIN_GLOBALnã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_atoupdated_atque carregou; se não bater com o atual, a gravação acontece do mesmo jeito e a resposta trazconcurrent_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.
CreditAnalysisnão temupdated_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éisAGENCY_USER,OPERATOR,ADMIN_TENANTeADMIN_GLOBAL. - RN13.
credit_analyses:updateé concedida aAGENCY_USERe aANALYST. 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 reescrevetotal_declared_income,income_to_rent_ratio,credit_recommendationerecommendation_notesnaDecision. Sem guarda, concedercredit_analyses:updateaoAGENCY_USERdeixaria um usuário externo reescrever a recomendação de crédito de uma análise emIN_ANALYSIS— ou jáAPPROVED—, alterando dado que o papel dele não pode nem ler (decisions:readestá fora dele). A regra passa a ser: - quem tem
decisions:updatecontinua 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:updatesó atualiza participante emDRAFTouPENDING_DOCS, como já vale para adicionar e remover. Fora disso: 422PARTICIPANT_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_DOCSchama-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:updatelê 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:updatenão aproxima oAGENCY_USERde 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 aDecisionde 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 (qualquerOPERATORdo 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:updategoverna também o CRUD de participantes. Concedê-la aoAGENCY_USERdestrancaPOST /participants, que aceitarenter_idno 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.
CreditAnalysisUpdatesó 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 é orenter_iddos participantes (8.2), resolvido no escopo do solicitante antes de gravar;renter_idque não resolva → 404, e nada é persistido. - Escopo de leitura e escrita. Reenvio e edição passam pelo mesmo
_get_authorizedque as demais operações de análise, que aplicatenant_idepartner_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
VIEWERchamando o reenvio de uma análise da própria garantidora.
8.4 Isolamento por imobiliária¶
- O
AGENCY_USERalcança apenas análises cujopartner_agency_idé o dele. Análise de outra imobiliária, ou da carteira própria (partner_agency_idnulo), 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 umOPERATORda 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 oAGENCY_USERsequer 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_GLOBALpoder 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 linhaIN_ANALYSIS → PENDING_DOCSmais recente. É o mesmo texto que o analista digitou (o_decidegrava nos dois lugares), e a leitura do histórico já é governada porcredit_analyses:read, que oAGENCY_USERtem. Nenhum campo novo, nenhuma permissão nova. decision_reasoneopinionnão entram noCreditAnalysisResponse, nem agora nem como efeito colateral. Quem quiser o parecer continua passando porGET /decision, que exigedecisions: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. CreditAnalysisStatusHistoryResponsenão ganha nome nem e-mail. Ele devolvechanged_bycomo 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 incluirIN_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 ressincronizarole_permissionsdos papéis de sistema — é idempotente e já roda eminit_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;reasonpassa a ser obrigatório (RN7).- Regras no service:
CreditAnalysisService.resume()— valida estado, valida lado (RN2/8.4), chama_transitionparaIN_ANALYSIS, e pede aoAnalysisServiceque devolva aDecisionaIN_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—reasondeixa de ser opcional na rota depend.- O
renter_iddos 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 decredit_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 estadoSUBMITTED(RN16).pages/credit-analyses/show.tsx— rótulo da devolução por origem (RN6), ação "Devolver ao analista" com confirmação,Alertde erro reposicionado emxs(RN17), motivo pré-preenchido no diálogo (RN8).pages/credit-analyses/edit.tsx(ou oCreditAnalysisForm) — motivo da devolução repetido no topo quando o estado éPENDING_DOCS.providers/— chamada ao endpoint novo.data-testidnovos: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
Alertabaixo do trilho emxs, 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 emIN_ANALYSISpara o analista.- Cobertura negativa:
AGENCY_USERda Imobiliária B recebe 404 ao reenviar análise da A;VIEWERnã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 emPENDING_DOCScomDecisionemPENDINGe 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 queINVALID_STATUS_TRANSITIONjá devolve hoje emsubmit,cancel,approveereject. 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). É oupdated_atque o cliente carregou. Omitido, não há verificação. Não é coluna: oupdate()hoje fazsetattrem laço sobremodel_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. Étruequandoexpected_updated_atfoi enviado e não bate com oupdated_atcorrente. A gravação acontece de todo jeito (RN11): é aviso, não recusa. É campo calculado, não coluna — nenhuma migration.- A resposta traz o
financial_summaryjá recalculado (RN9/RN10), com asTenantSettingsda garantidora dona da análise. 422 CREDIT_ANALYSIS_NOT_EDITABLEcontinua 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_DOCSvinculada à Imobiliária A, quando um usuário da Imobiliária A chamaPOST /resume, então a análise fica emIN_ANALYSIS, aDecisionvolta aIN_PROGRESS, e o histórico ganha uma linhaPENDING_DOCS → IN_ANALYSIScom 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ãoPENDING_DOCS), quando alguém chamaPOST /resume, então a resposta é422 INVALID_STATUS_TRANSITIONe nada muda — nem status, nemDecision, 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 é422e a análise continua emIN_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_packagee ofinancial_monthly_valueda 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_EDITABLEe nenhum campo financeiro é recalculado.
Negativos de autorização — um por papel afetado¶
- [x] CA7.
AGENCY_USERda Imobiliária B chamandoPOST /resumenuma 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_USERchamandoPOST /resumenuma análise da carteira própria (sempartner_agency_id) recebe 404. - [x] CA9.
AGENCY_USERda Imobiliária A criando participante com umrenter_idda Imobiliária B recebe 404 e nada é gravado — a FK do corpo é resolvida no escopo dele (8.2). - [x] CA10.
AGENCY_USERchamandoPOST /decision/pendou/decision/approverecebe 403 — ganharcredit_analyses:updatenão o aproximou de decidir. - [x] CA11.
VIEWERchamandoPOST /resumenuma análise do próprio tenant recebe 403 (não 404: o registro é do tenant dele, o que falta é permissão). - [x] CA12.
OPERATORda garantidora chamandoPOST /resumenuma análise vinculada a imobiliária recebe 422RESUME_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_USERchamandoPATCH /participants/{id}numa análiseIN_ANALYSISda própria imobiliária recebe 422PARTICIPANT_NOT_EDITABLE; odeclared_incomedo participante não muda e aDecisionmantémcredit_recommendation,income_to_rent_ratioetotal_declared_incomeidênticos aos de antes da chamada. Mesmo resultado com a análiseAPPROVED. - [x] CA14b.
ANALYST(que temdecisions:update) chamando a mesma rota na mesma análiseIN_ANALYSISrecebe 200 e o recálculo roda — a guarda separa por permissão, não por estado apenas. - [x] CA14c.
ANALYSTchamandoPOST /credit-analysesrecebe 403: ganharcredit_analyses:updatenão lhe deucredit_analyses:create. - [x] CA14d.
GET /credit-analyses/{id}comoAGENCY_USERnão trazdecision_reasonnemopinionno corpo, em nenhum estado — o motivo que a tela dele exibe vem do histórico de status (8.6). - [x] CA14e.
ADMIN_GLOBALeditando o aluguel de uma análise da Garantidora A recalcula com asTenantSettingsda Garantidora A (RN10) — não com as de outro tenant, nem com valores padrão. - [x] CA14f.
ADMIN_GLOBALchamandoPOST /resumenuma análise vinculada a imobiliária recebe 422RESUME_WRONG_SIDE— a regra de lado vence a permissão herdada (8.1). - [x] CA14g.
AGENCY_USERabrindo o histórico de uma análise reprovada não recebe nome nem e-mail de quem reprovou:changed_bycontinua 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: oapps/webnã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 umOPERATORdo tenant que não a cadastrou chamaPOST /resume, então recebe 200 e a análise vai aIN_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, semaria-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 == 0na 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 doPATCH /participants/{id}(RN13a): ela é condição para conceder a permissão, não pode vir depois - [x]
apps/api—credit_analyses:resumeem_PERMISSIONSe nos quatro papéis;credit_analyses:updateemAGENCY_USEReANALYST(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 aDecisionaIN_PROGRESSna mesma transação - [x]
apps/api— recálculo do snapshot noupdate(), pela função da criação - [x]
apps/api—reasonobrigatório nopend - [x]
apps/api— rotaPOST /credit-analyses/{id}/resume - [x]
apps/api— sem migration; conferir que oseed_rbacinsere a permissão nova - [x]
apps/api— docstrings + Swagger; corrigir a docstring dopendque 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 doDecisionPanel(RN16) - [x]
apps/web— rótulo por origem, confirmação do reenvio, motivo pré-preenchido,Alertreposicionado emxs, motivo no topo do formulário - [x]
apps/web—data-testidda seção 9, responsivo nos dois viewports - [x]
apps/e2e— spec da jornada + negativos, nos dois viewports; seed da análise emPENDING_DOCS - [x]
make testpassando (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_GLOBALage 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
TenantSettingsvigentes 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:updatepara oAGENCY_USERdestranca 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.
CreditAnalysisnão temupdated_bye 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:
- 8,0 — a RN11 existia só no servidor: o
expected_updated_atnunca 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 oADMIN_TENANTacumulava quatro ações no topo — o teto de três se mantinha por truncamento silencioso do array, não por regra. - 7,4 — o arranjo do e2e chamava
POST /decisionsem corpo, que a rota exige: os nove testes morriam antes do primeiroexpect. 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. - 7,5 — a correção da rodada anterior virou regressão:
reloadAll()nocatchligava oloading, o esqueleto desmontava a árvore e levava junto o diálogo aberto, destruindo o motivo que oConfirmDialogacabara de ser ensinado a preservar. Também apareceram dois Swaggers desatualizados nos endpoints que esta spec altera, um 500 em potencial na comparação dedatetimecom e sem fuso (invisível em SQLite), e dois testes meus que não guardavam nada — oADMIN_GLOBALdo cenário tinhatenant_id, e o snapshot semeado coincidia com o que o recálculo produziria. - 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_FOUNDePARTICIPANT_NOT_EDITABLE. No código, erro de regra de negócio éBusinessRuleError, que mapeia para 422 — e é o queINVALID_STATUS_TRANSITIONjá devolvia emsubmit,cancel,approveereject. 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_idnã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 comawaite com umPartnerAgencysem nome como fallback — o vínculo existe mesmo quando o nome não carrega.- O reenvio consulta
AnalysisRepositorydireto, em vez de pedir aoAnalysisServicecomo 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.pymudou de sinal. Ele afirmava quePENDING_DOCS → IN_ANALYSISnã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_EDITAVEISsubstituiu a tupla(DRAFT, PENDING_DOCS)repetida em três pontos do serviço de participantes; a guarda nova seria a quarta cópia.ConfirmDialogganhouinitialReason,reasonRequiredetestIdPrefix— os três úteis fora desta spec. OtestIdPrefixexiste 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.mddocumentatest_partner_agency_search.py::test_busca_alcanca_quem_esta_fora_da_primeira_pagina; o irmãotest_partner_agency_status_filter.py::test_alcanca_quem_esta_fora_da_primeira_paginatem 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 ohandle403doapi-fetchfazwindow.location.hrefantes do.catch()de quem chamou; ela era expulsa para a lista. O.catch(() => [])que parecia proteger não protege nada — está documentado no próprioapi-fetch, e o/settingsali ao lado já usavatolerate403pela 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
PageHeaderrenderiza 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 comdocker exec garantora-devremote-api-1 uv run pytest; o container embute o código, então cada rodada exigiu/devremote-rebuildantes, senão mediria a versão anterior.tsc --noEmitdo 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
stashe 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.