Pular para conteúdo

0009 — A aprovação pede num modal o que a tela já mostra, e o setup é um campo vazio sem referência

Status CORRIGIDO
Branch fix/0009-aprovacao-em-modal-duplicado
Aberto em 2026-08-15
Severidade MÉDIA
Apps afetados api · web · e2e
Nota do diagnóstico 8,5 (tentativa 3 de 3)
Nota da correção 8,3 (tentativa 3 de 3)

Status: RASCUNHO → DIAGNOSTICADO → EM CORREÇÃO → CORRIGIDO

O fix atravessa dois portões, cada um com nota de 0 a 10 dada por um subagente verificador de contexto limpo, em até 3 tentativas, passando a partir de 7,0:

  1. O diagnóstico (seções 1 a 4) é validado na seção 5 (reprodução × causa raiz). É o portão barato: sem ele, o segundo mede o conserto contra um palpite.
  2. A correção (o diff e os testes) é validada na seção 11 (correção × regressão), com duas perguntas — atacou a causa ou o sintoma, e o que prova que não volta.

1. Sintoma

O analista da garantidora abre a análise, trabalha no painel de parecer — o quadro laranja "Parecer da análise", onde ele escreve o parecer e as notas internas e salva rascunho — e então clica em Aprovar. Abre-se um modal que pede outras quatro coisas, num formulário que ele não viu até aqui:

  • Taxa setup (R$) — campo vazio, com o auxílio "Opcional — se vazio, usa a taxa de adesão padrão da garantidora";
  • Justificativa da decisão (opcional) — texto livre;
  • Cláusula obrigatória incluída no contrato de locação — caixa de seleção;
  • Justificativa dos documentos, quando há obrigatório faltando e ele é gestor.

Três defeitos, e os dois últimos são o mesmo problema visto de dois lados:

  1. O modal é um segundo formulário, não uma confirmação. Aprovar é a ação mais irreversível do produto — emite garantia —, e o passo que deveria ser "confira e confirme" é onde a pessoa descobre que ainda falta preencher coisa. Quem revisa não pode estar digitando.
  2. O campo de setup não diz qual é o valor. Ele é opcional e, vazio, a garantia usa a taxa padrão — mas a tela não mostra qual é essa taxa no momento de decidir. O analista escolhe entre "deixar como está" e "arbitrar", sem ver o número de nenhum dos dois lados. O valor existe e já está calculado logo acima, no resumo financeiro, como "Pagamento inicial" — só não chega ao campo.
  3. O valor sugerido do setup não pode vir do snapshot da análise. Os campos financial_* são gravados na criação e só se refazem numa edição que toque valor (credit_analysis_service.py:466-467). Se a garantidora mudar a taxa de adesão depois, a análise parada em IN_ANALYSIS carrega o preço de antes — e sugerir esse número no campo faria o analista arbitrar, sem saber, um valor que a garantidora já trocou.

2. Reprodução

  1. Entrar como analista da garantidora — o botão do topo exige decisions:update (show.tsx:543). A análise precisa já ter parecer aberto: é o botão "Analisar" do cabeçalho (show.tsx:629), que leva de SUBMITTED a IN_ANALYSIS. Sem ele o painel mostra "Nenhum parecer registrado" e não há campo nenhum.
  2. Abrir uma análise em IN_ANALYSIS cujo tenant tenha taxa de adesão configurada (initial_fee_type diferente de NONE).
  3. Rolar até o painel laranja "Parecer da análise" e reparar no que ele oferece: parecer do analista, notas internas, salvar rascunho.
  4. Clicar em Aprovar, no topo.

Observado: abre o modal com Taxa setup vazia, Justificativa da decisão e a caixa da cláusula — nenhum dos três existia na tela até este clique. O resumo financeiro, poucos centímetros acima, exibe "Pagamento inicial" com o valor que o campo vazio usaria.

  • Ambiente: dev-remote (https://dev.garantora.com.br/app). Não depende de dado específico: reproduz em qualquer análise em IN_ANALYSIS.
  • Superfície: ambos. O modal é o mesmo nos dois viewports; em 375px ele pesa mais, porque o formulário inteiro disputa a tela com o teclado virtual. Não é agravado pelo PWA — é estrutura de componente, não asset cacheado.
  • Dados/pré-condições: o segundo defeito só é visível quando a garantidora tem taxa de adesão configurada. Com initial_fee_type = NONE o valor padrão é zero, e o campo vazio não esconde nada.

Para o terceiro defeito, dois passos a mais, como ADMIN_TENANT: alterar initial_fee_value em Parâmetros e reabrir a análise em IN_ANALYSIS. O resumo financeiro segue mostrando o valor anterior; editar e salvar a análise atualiza.

3. Esperado × Atual

  • Esperado (refinado pelo usuário em 2026-08-15, depois da aprovação do diagnóstico):

  • o setup sai do modal e passa a ser um terceiro campo do box amarelo, ao lado de "Parecer do analista" e "Notas internas", já preenchido e editável;

  • a "Justificativa da decisão" não muda de lugar — é removida. O box amarelo já tem Parecer do analista, e era ele que a justificativa duplicava. Um campo a menos, não um campo mudado de casa;
  • a cláusula obrigatória vai para o box amarelo também. O modal a mostra como item da revisão; não confirmada, o Aprovar do modal fica indisponível e o resumo diz o que falta — que é como o botão do topo deixa de "sempre abrir e não responder";
  • o modal deixa de ter entrada, com uma exceção declarada: a Justificativa dos documentos continua nele. Ela só aparece quando há obrigatório faltando e quem aprova é gestor (RN10 da spec 0008) — é declaração do instante da aprovação, não trabalho de análise, e tirá-la poria na página um campo que quase nunca existe. Tudo o mais some do modal, que passa a ser resumo do que foi decidido, com Aprovar e um Cancelar que apenas fecha.
  • Atual: o painel tem dois campos, o modal tem outros quatro, o setup não tem valor de referência, e a confirmação e o preenchimento acontecem no mesmo passo.

E, para o terceiro defeito:

  • Esperado: o setup é resolvido no momento da aprovação — é ali que ele se fecha. O campo sugere o valor calculado com os parâmetros vigentes agora, e é esse que vai para a garantia se o analista não arbitrar outro.
  • Atual: o único valor disponível na tela é o snapshot da criação (a.financial_summary.initial_payment), que pode estar vencido.

Não há recálculo em massa (decisão do usuário, 2026-08-15). Mudar os parâmetros não dispara nada sobre as análises existentes: elas seguem como estão, e o valor novo só aparece quando alguém for aprovar — que é o momento em que o setup deixa de ser estimativa e vira preço.

4. Causa raiz

Onde

  • apps/web/src/pages/credit-analyses/components/ApproveDialog.tsx:117-155 — os três campos da decisão (Taxa setup (R$), Justificativa da decisão, a caixa da cláusula) dentro do <DialogContent>.
  • apps/web/src/pages/credit-analyses/components/ApproveDialog.tsx:16-29 — o docstring diz por que eles estão lá, e é a peça central deste diagnóstico. Ver o mecanismo.
  • apps/web/src/pages/credit-analyses/components/DecisionPanel.tsx:286-311 — o painel laranja tem hoje só opinion e notes, mais o "Salvar rascunho".
  • apps/web/src/pages/credit-analyses/show.tsx:215 — const canReadSettings = isAdminTenant || isAdminGlobal. Para o analista, que é quem reproduz este bug, a tela nem chama /settings (show.tsx:366-369), então settings fica null.
  • apps/web/src/pages/credit-analyses/components/FinancialSummaryPanel.tsx:216 — o "Pagamento inicial" que o analista vê nesta tela vem de financialSummary.initial_payment, isto é, de a.financial_summary (show.tsx:928), calculado pela API. O cálculo do cliente (:110-132) existe, mas não executa aqui, porque depende de settings.
  • apps/api/app/services/tenant_settings_service.py:21-28 — update() grava as configurações e termina ali, sem efeito sobre as análises em curso. Isso está certo e não muda: registrado aqui para que a correção não seja tentada a mexer nele.
  • apps/api/app/services/credit_analysis_service.py:340 e :466-467 — _apply_financial_snapshot roda na criação e, na edição, só sob if muda_preco:. Editar o telefone do inquilino não refaz o snapshot; editar o aluguel refaz. O snapshot já se atualiza com parâmetros novos — mas apenas de carona numa edição de valor, nunca pelo gatilho que deveria disparar.
  • apps/api/app/services/guarantee_service.py:140-145 — decision.approved_initial_fee_value sobrepõe pricing.initial_fee (app/services/pricing.py). O padrão é conhecido pelo servidor; a tela é que não o oferece ao campo.

Não é acidente, é reversão de decisão. O git show 1bb952f^ mostra os campos decision-approve-setup-input e decision-approve-clause-checkbox dentro do DecisionPanel (linhas 325-344). O commit 1bb952f (spec 0025) os moveu para o ApproveDialog, que nasceu ali.

O mecanismo

O primeiro defeito é o preço de uma troca feita de propósito. A spec 0025 levou o botão "Aprovar" para o cabeçalho. Com o botão no topo e os campos que o habilitam no fim da página, o analista veria um botão cinza sem saber por quê — o próprio ApproveDialog registra isso em :19-24: "o botão do topo sempre abre, e quem valida é o confirmar daqui". Os campos foram junto para que a validação tivesse onde acontecer.

Ou seja: a duplicação que se vê hoje não é obra inacabada, é a consequência aceita de resolver o botão-cinza-sem-motivo. O que a 0025 não previu foi que, com o painel de parecer ganhando opinion e notes na mesma spec, a decisão passaria a ocupar duas superfícies — e que a segunda seria descoberta só no clique de aprovar.

Por isso a correção precisa responder o que a 0025 respondia, e não pode fingir que a pergunta não existe: com os campos de volta ao painel, o botão do topo continua sempre abrindo, e a cláusula obrigatória continua sendo condição. A resposta está na seção 10 — o modal deixa de validar entrada e passa a exibir o que foi decidido, então uma cláusula não confirmada aparece ali como pendência a resolver na página, e não como um botão que não responde.

O segundo defeito tem causa própria, e ela não é a que parece. ApproveDialog não recebe parâmetro de preço nenhum (show.tsx:1284-1291 passa só blockingDocuments e canOverrideDocuments), mas o motivo de o campo nascer vazio vai mais fundo: para o analista não existe fonte de preço no cliente. canReadSettings (show.tsx:215) é isAdminTenant || isAdminGlobal, então a requisição a /settings nem sai, settings é null, e o ramo de cálculo do FinancialSummaryPanel (:110-132) não roda.

O número que ele vê na tela vem por outro caminho: a.financial_summary, calculado pela API e renderizado em FinancialSummaryPanel.tsx:216. É a fonte que a correção tem em mãos, e ela já está em show.tsx, a poucas linhas do diálogo.

Mas não é o mesmo número que a garantia vai usar, e a diferença importa. financial_initial_payment é snapshot, gravado em credit_analysis_service.py:844 com as TenantSettings do momento em que a análise foi criada ou recalculada. A garantia recalcula na emissão, com as settings de então (guarantee_service.py:126-144). Mesma fórmula, momentos diferentes: se a garantidora mudar a taxa de adesão entre a criação e a aprovação, o campo sugeriria o valor antigo — e, como o campo preenchido passa a valer como arbitramento (seção 7), o analista aprovaria o número velho sem saber que o arbitrou.

O terceiro defeito é sobre de onde o número vem, e a resposta muda o desenho. O snapshot financial_* é gravado na criação e só se refaz numa edição que toque valor (:466-467) — ele é uma estimativa da época, útil para o resumo, imprópria como preço. Preenchê-lo no campo transformaria estimativa vencida em arbitramento, porque o campo preenchido é, por definição, o valor que o analista arbitrou (guarantee_service.py:140-145).

A decisão do usuário resolve isso pelo momento, não pela propagação: o setup se fecha na aprovação. Enquanto a análise está aberta, ninguém precisa que os números acompanhem os parâmetros — e é por isso que não há recálculo em massa. O que precisa estar atual é o número no instante em que ele deixa de ser estimativa, e esse instante é um só.

Consequência que a correção tem de encarar: o resumo financeiro logo acima continua exibindo o snapshot. Se os parâmetros mudaram, os dois números divergem na mesma tela — e uma tela que mostra "Pagamento inicial R$ 500" e sugere setup R$ 600 sem explicar é pior que qualquer um dos dois sozinho.

Detalhe que envelheceu nesta mesma branch: o comentário de show.tsx:213-214 diz que /settings é fechado por papel "e não por settings:read, que não existe". Desde o commit c3cd260, na main, a permissão existe (tenant_settings:read) e todo papel com tenant a tem. O cliente é que não foi atualizado. Isso não é a causa deste bug — usar financial_summary continua sendo o caminho certo, porque é a mesma conta que a garantia vai usar —, mas explica por que o campo nunca teve como se preencher.

Por isso o problema aparece aqui e não nas outras decisões: reprovar e devolver não têm valor a arbitrar nem condição a confirmar, então os diálogos deles (show.tsx:1203-1210 e o PendDialog) são confirmação de motivo — que é o que uma confirmação deve ser.

O alcance

  • Só a aprovação. ApproveDialog tem um consumidor (show.tsx:1284).
  • Dois specs de e2e quebram, e a lista veio de Grep pelos testids (decision-approve-setup-input, decision-approve-clause-checkbox, credit-analysis-approve-dialog, credit-analysis-approve-confirm): tests/e2e/credit-analyses.spec.ts:114-149 e tests/e2e/credit-analysis-detail-order.spec.ts:298-299, que clica decision-approve-button-panel e afirma o diálogo visível. O tests/e2e/credit-analysis-documents.spec.ts não entra: ele chama POST /decision/approve por fetch, sem passar pela interface.
  • A fonte do valor é a.financial_summary, não o cálculo do cliente. Reusar o ramo calcs seria apontar para código que não executa para o analista — e criaria uma terceira cópia da regra que já vive em pricing.py (autoridade) e no FinancialSummaryPanel (exibição, quando há settings).
  • Nenhuma análise existente é reescrita. Sem recálculo em massa, o PATCH /settings segue sem efeito colateral, e as análises aprovadas ficam intactas por construção — não por um filtro de status que alguém pode errar.
  • A justificativa de documentos fica no modal. Só existe quando há obrigatório faltando e o usuário é gestor (RN10 da spec 0008): é exceção do instante da aprovação, não trabalho contínuo.
  • O gate envelhecido é ocorrência única. Grep por canReadSettings devolve três linhas, todas em show.tsx (:215, :366, :374). O formulário de análise busca /settings sem gate nenhum (CreditAnalysisForm.tsx:234), então não há um segundo lugar carregando a suposição velha.

5. Validação do diagnóstico

Tentativa Reprodução Causa raiz Final Veredicto
1 9,0 4,0 4,0 REPROVADO
2 8,7 8,0 8,0 APROVADO
3 8,5 9,0 8,5 APROVADO

Resultado: aprovado na tentativa 3 com nota 8,5 (mínimo: 7,0).

A tentativa 3 existiu porque o usuário acrescentou o terceiro defeito depois da aprovação da 2: mudar a taxa de adesão passa a recalcular as análises abertas. Isso trouxe a API para dentro do fix, e um diagnóstico que não descreve a API não serve de régua para uma correção que mexe nela.

A tentativa 1 foi reprovada por hipótese escrita como conclusão — o achado mais grave deste portão. Eu havia escrito que "a tela foi construída em duas etapas e ninguém juntou as duas", com os campos tendo ficado no modal por falta de onde persistir. O git show 1bb952f^ mostra o contrário: os campos estavam no painel, e a spec 0025 os moveu de propósito, com a razão escrita no próprio ApproveDialog — o botão subiu para o cabeçalho e passou a sempre abrir, então a validação precisava de casa. Isso muda o que este fix é: não é juntar o que ninguém juntou, é reverter uma decisão de desenho, e a correção tem de dizer o que faz com a razão dela.

A mesma rodada mostrou que a fonte do valor que eu apontara (calcs, do cliente) não executa para o analista, e que a lista de e2e afetados estava errada nos dois itens.

Pendências aceitas: —

6. Correção

Preencher com /fix implement.

apps/api

  • Onde: só o caminho do valor sugerido. tenant_settings_service.py não muda.
  • O quê: o setup calculado com os parâmetros vigentes precisa chegar ao painel. Dois caminhos, e o implement escolhe com o custo à vista:
  • o cliente calcular, lendo /settings — possível desde c3cd260, que deu tenant_settings:read a todo papel com tenant; falta só corrigir canReadSettings (show.tsx:215), que ainda filtra por papel. Barato, mas cria a terceira cópia da regra de preço;
  • a API devolver o valor já calculado na resposta da análise. Não duplica a regra, custa campo novo no contrato.
  • O contrato de POST /decision/approve não muda: já aceita approved_initial_fee_value, e a sobreposição já está em guarantee_service.py.

apps/web

  • Onde: DecisionPanel.tsx (recebe os campos da decisão, incluindo o setup), ApproveDialog.tsx (vira resumo + Aprovar/Cancelar), show.tsx (fiação, e é dele que sai o valor: a.financial_summary.initial_payment).
  • data-testid: os decision-approve-* migram para o painel; o modal ganha os do resumo.
  • Responsividade & PWA: o painel cresce; conferir 375px.

apps/e2e

  • Teste de regressão: o fluxo novo — preencher no painel, confirmar no modal — mais a atualização de tests/e2e/credit-analyses.spec.ts e tests/e2e/credit-analysis-detail-order.spec.ts.

7. Risco / impacto colateral

Toca dinheiro. O setup vai para Guarantee.initial_fee e compõe o total à vista (guarantee_service.py:137-146). Um valor pré-preenchido que o analista não perceba é pior que um campo vazio: hoje vazio significa "use o padrão", e depois da correção o campo sempre terá número. Se o pré-preenchimento divergir do que a API calcularia, a garantia sai com valor arbitrado sem ninguém ter arbitrado.

Segundo risco: a aprovação deixa de ter formulário no modal, mas não pode deixar de ter confirmação — o modal precisa continuar sendo um passo, agora de revisão.

Terceiro, e é o que a spec 0025 tinha resolvido: o botão do cabeçalho sempre abre. Com a validação de volta ao painel, é preciso garantir que ele não vire um botão que abre um modal que só diz "não dá" — a pendência tem de ser visível na página, antes do clique.

8. Critérios de aceite

  • [x] CA1. Seguindo os passos da seção 2, o setup aparece no box amarelo junto de parecer e notas internas, e o modal de Aprovar não pede preenchimento nenhum.
  • [x] CA2. O campo de setup nasce com o valor dos parâmetros vigentes — que coincide com o "Pagamento inicial" do resumo enquanto ninguém mudar a taxa de adesão, e diverge dele depois (é o CA8/CA10). A redação anterior dizia "o mesmo que o resumo mostra", o que contradizia os dois.
  • [x] CA3. O analista pode alterar o valor sugerido, e o valor alterado é o que chega à API.
  • [x] CA4. O modal de Aprovar mostra um resumo que permite conferir a decisão e oferece Aprovar e um Cancelar que só fecha.
  • [x] CA7. O campo "Justificativa da decisão" não existe mais em lugar nenhum da interface — o parecer do analista é o que cumpre o papel.
  • [x] CA5. A justificativa de documentos continua no modal, e continua bloqueando quem não é gestor (RN10 da spec 0008 intacta).
  • [x] CA6. O painel não rola na horizontal em 375px.
  • [x] CA8. Alterada a taxa de adesão nos Parâmetros, o setup sugerido numa análise em IN_ANALYSIS passa a ser o valor novo, sem que ninguém edite a análise.
  • [x] CA9. Nenhuma análise existente é reescrita por mudar os parâmetros — os campos financial_* das análises abertas e aprovadas ficam como estavam.
  • [x] CA10. Se o setup sugerido divergir do "Pagamento inicial" do resumo financeiro, a tela não se contradiz em silêncio.

9. Plano

  • [x] Reproduzir o bug — pelo código e pela tela: os campos no modal, o setup vazio.
  • [x] Corrigir a causa raiz — só web. A API não mudou: o valor sugerido é calculado no cliente com /settings, que o analista já pode ler desde c3cd260.
  • [x] Teste de regressão que falharia se a correção fosse revertida
  • [x] make preflight limpo · make test 393 passaram, 0 falharam · tsc limpo
  • [x] Validado no dev-remote reconstruído, desktop e mobile, com a divergência forçada (initial_fee_value de 100% para 50%, restaurado no finally): painel e modal sem rolagem horizontal nos dois viewports, zero erro de console, e o campo dizendo "Sugerido pelos parâmetros de hoje (50% do pacote). O resumo financeiro acima mostra R$ 2.000,00, calculado quando a análise foi criada."
  • [x] e2e — 12 casos verdes em credit-analyses.spec.ts e credit-analysis-detail-order.spec.ts, incluindo o CA8.

10. Decisões em aberto

Respondidas pelo usuário em 2026-08-15, antes da abertura deste registro:

  • [x] O setup calculado é sugestão ou trava? Sugestão — o campo nasce preenchido e o analista pode alterar.
  • [x] A confirmação some? Não. O modal deixa de ter campos e passa a mostrar o resumo do que foi decidido, com Aprovar e Cancelar.
  • [x] O valor recalcula quando o pacote muda? Sim, sempre — inclusive por cima de uma edição manual.

    A consequência, registrada porque a correção tem de lidar com ela: o pacote não é editável na tela de decisão (muda em /edit, ou por outra pessoa), então o recálculo por cima da edição só acontece quando o analista sai e volta, ou quando alguém altera a análise em paralelo. Se a sugestão for calculada ao montar a tela e não a cada tecla, "sempre recalcula" e "não perde o que ele digitou" convivem sem contradição. Se a implementação encontrar um caso em que os dois se excluem, é [PERGUNTAR AO USUÁRIO], não escolha do implementador.

  • [x] Qual valor pré-preencher: o snapshot ou o recálculo? O recálculo, com os parâmetros de agora — decidido pelo usuário em 2026-08-15. É o mesmo número que a garantia usaria se ninguém arbitrasse, então o campo nunca sugere preço vencido.

  • [x] Mudar os parâmetros recalcula as análises existentes? Não — decisão revista pelo usuário em 2026-08-15, depois de considerada e descartada. O setup se fecha na aprovação: enquanto a análise está aberta nada precisa acompanhar os parâmetros, e o valor novo entra no momento em que alguém aprova. Sai da correção o recálculo em massa, e com ele o risco de reescrever a carteira inteira a cada PATCH /settings.

    Consequência que a implementação herda: o cliente não tem essa conta hoje para o analista — canReadSettings (show.tsx:215) impede a chamada a /settings. Há dois caminhos, e o implement escolhe com o custo à vista: usar a permissão tenant_settings:read, aberta no commit c3cd260 para todo papel com tenant (o gate do cliente é que ficou velho), ou expor o valor recalculado na resposta da análise. O primeiro é o mais barato e não inventa contrato; o segundo evita recalcular a regra no cliente, que já existe em duplicidade.

    Note que isso pode fazer o campo divergir do "Pagamento inicial" exibido no resumo financeiro, que é snapshot. Se divergirem, a tela precisa não se contradizer — é achado para o implement resolver, não para inventar regra.

  • [x] A cláusula obrigatória vai junto para o box amarelo. Decidido em 2026-08-15. O modal a exibe como item do resumo; sem ela marcada, Aprovar fica indisponível e o resumo aponta a pendência.

11. Validação da correção

Tentativa Correção Regressão Final Veredicto
1 7,0 6,0 6,0 REPROVADO
2 8,3 7,7 7,7 APROVADO
3 8,3 8,7 8,3 APROVADO

Resultado: aprovado na tentativa 3 com nota 8,3 (mínimo: 7,0).

A tentativa 1 reprovou por três coisas, e duas eram estrago meu: a lista de documentos obrigatórios faltantes virou contagem (perdendo quais faltam, no ponto em que se decide escalar), e o onClose perdeu a guarda de loading. A terceira foi cobertura: o CA8 — o coração do terceiro defeito — não tinha teste nenhum, e a asserção do valor sugerido era not.toHaveValue(''), que passaria igual com o snapshot vencido.

A tentativa 2 aprovou em 7,7 com um [REGISTRO INCOMPLETO] procedente e caro: eu mandava setupFee sempre, então a sugestão virava arbitramento em toda aprovação — o risco que a seção 7 deste registro havia escrito. Passou a ir só o que o analista tocou; sem isso, o arredondamento e o insumo de meses do navegador chegariam à garantia.

A tentativa 3 fechou o resto: um caso rodando como analista (todo o e2e roda como gestor, para quem o gate velho já permitia — a linha que mexe em permissão era revertível com a suíte verde), as asserções de ausência dos campos dentro do modal, e a serialização do arquivo que altera parametrização do tenant.

Defeitos aceitos:

  • O CA5 não tem prova pela interface. A lógica de bloqueio documental foi reescrita no diálogo e só a regra do lado da API está coberta. Falta o caso com obrigatório faltando, aberto como analista (confirmar desabilitado) e como gestor (justificativa presente, confirmar liberado só depois de preenchida).
  • A janela do PATCH /settings no tenant compartilhado. O CA8 altera initial_fee_value e restaura no finally; o mode: 'serial' ordena só este arquivo. Nada mais no repositório escreve em /settings, então o risco é baixo — mas a serialização não basta por si, e é isso que fica dito.
  • "Salvar rascunho" ignora o setup e a cláusula. DecisionPanel grava só opinion e notes; os dois campos novos vivem no estado da página e não são persistidos. Quem digita um setup, salva rascunho e recarrega perde o número — a sugestão volta por cima, sem aviso. É consequência de trazer os campos para o painel, e a decisão de persistir ou não é do usuário; enquanto não for tomada, o comportamento fica declarado aqui.
  • TOTAL_GUARANTEE usa insumo diferente do servidor. O cliente multiplica por credit_analysis.guarantee_months; guarantee_service.py:124 usa os meses entre início e fim da garantia. Com a base RENT — o padrão, e o que o seed usa — os dois coincidem, e a sugestão é exata. Com TOTAL_GUARANTEE podem divergir, e como o campo preenchido vira arbitramento, o valor gravado seria o do cliente. Alinhar exigiria calcular no servidor, que é o caminho que este fix não tomou.
  • Arredondamento. toFixed(2) no cliente contra ROUND_HALF_UP do pricing.py. Divergem no meio-centavo — e, pela mesma razão acima, só no que se lê, não no que se grava.
  • O gate de /settings alargado muda o resumo financeiro para mais gente. OPERATOR, VIEWER e AGENCY_USER passam a carregar as configurações nesta tela, então o ramo de cálculo do FinancialSummaryPanel fica disponível para eles. É o efeito pretendido pelo commit c3cd260, mas quem lê este fix precisa saber que ele acontece aqui.

13. Acrescentado depois: a cláusula obrigatória sai do produto

Pedido do usuário em 2026-08-15, depois de o diagnóstico e a correção já terem passado pelos portões. Entra aqui e não num fix próprio porque a cláusula era um dos dois campos que este fix acabara de mover para o painel — separá-los faria um commit acrescentar o campo e o seguinte apagá-lo, com teste e critério de aceite pelo caminho.

O que ela era. Uma caixa que o analista marcava atestando que o contrato de locação daquele inquilino contém a cláusula que vincula a garantia. Bloqueava a aprovação com 422 MANDATORY_CLAUSE_NOT_CONFIRMED (spec 0003, RN2) e guardava um booleano na decisão.

Por que sai. É regra do contrato da garantidora, não da ferramenta que faz a gestão. O sistema nunca teve como conferir a afirmação — guardar o "eu confirmo" dava aparência de controle sem controle nenhum, e a única coisa entre a aprovação e o clique era marcar uma caixa. Estava misturando a gestão com o contrato de quem usa a gestão.

O que muda. mandatory_clause_confirmed sai do schema de entrada e de saída, da validação, do modelo e do banco (migration x7z9b1e3h5i6, com downgrade recriando a coluna e exercitado contra PostgreSQL). No web saem a caixa, o estado e o item do resumo. A RN2 da spec 0003 fica revogada, e o teste que a afirmava foi reescrito para afirmar o oposto: aprovar sem campo nenhum de cláusula devolve 200, e o campo não volta na resposta.

Não passou por portão. As três tentativas do laço da correção foram gastas antes deste pedido. O que sustenta a mudança é o que dá para medir: suíte da API 393 verdes, e2e 13 verdes nos dois specs afetados, downgrade exercitado, e preflight limpo. Um quarto portão exigiria estourar o limite — se você quiser, rodo assim mesmo, ciente disso.

14. Acrescentado depois: o botão "Atualizar" do PWA que não fazia nada

Pedido do usuário em 2026-08-15, e não tem relação com este fix — entra aqui por decisão dele, com a objeção registrada: o fix.md não descreve nada de PWA, e o commit fica misto.

O sintoma. Em desenvolvimento, o botão "Atualizar" do banner de nova versão às vezes não faz nada, e o usuário notou que isso acontece logo depois de um hard reset de cache.

A causa. needRefresh é estado de React e sobrevive ao service worker que o originou. O hard reset remove o worker em waiting, mas o banner continua na tela; clicar chamava updateServiceWorker(true), que procura um waiting que não existe mais — nada acontece e a página não recarrega. No dev-remote isso é frequente porque o rebuild troca o SW várias vezes por sessão.

O conserto. Três degraus em pwa-update-banner/index.tsx: usa o waiting se houver; senão pergunta ao servidor uma vez (registration.update()); senão recarrega. Mais um temporizador de 5s para o caso de o controllerchange nunca chegar, que deixaria o botão preso em "Atualizando…".

Sem prova de regressão automatizada, e isto não é "só visual". O diff mexe em condição. Reproduzir o caminho exige um service worker em waiting (dois builds do dev-remote) e depois destruí-lo pelo navigator.serviceWorker — o spec existente (pwa-update-banner.spec.ts) só afirma a ausência do banner no estado normal, e continua verde. O que se mediu: tsc limpo, os 3 casos do spec verdes, e o banner segue ausente no estado normal nos dois viewports. O caminho consertado não foi exercitado.

12. Registro

O que era: dois campos no painel, quatro no modal, o setup vazio prometendo um valor que a tela não mostrava, e o preenchimento acontecendo no passo em que se deveria conferir.

O que virou: setup e cláusula no box amarelo, com o setup sugerido pelos parâmetros vigentes e editável; a "Justificativa da decisão" removida (o parecer já cumpria o papel); o modal como revisão, com resumo, Aprovar e Cancelar.

O que divergiu do plano:

  • A cláusula obrigatória foi removida no meio. Ver a seção 13 — chegou depois dos portões, e é a única parte deste fix que não passou por um.

  • A API não mudou. O plano admitia campo novo no contrato; não foi preciso. O gate canReadSettings estava velho desde c3cd260 — corrigi-lo destravou a fonte no cliente.

  • A correção quebrou a tela inteira antes de funcionar. O useEffect da sugestão entrou abaixo de um retorno antecipado, e React #310 apagou a página. Só a captura contra o dev-remote mostrou — tsc estava limpo, e o e2e não chegou a rodar. O hook subiu para antes do primeiro retorno, que é o do esqueleto.
  • setupTocado existia e não era consultado. O portão pegou: sugerir não é arbitrar.
  • O resumo do modal lia o parecer do servidor, então texto digitado e não salvo aparecia como "não preenchido" na conferência. Passou a ler o estado vivo do painel.