Pular para conteúdo

0011 — A imobiliária troca e edita documento com a análise na garantidora

Status CORRIGIDO
Branch fix/0011-troca-de-arquivo-fura-janela-da-imobiliaria
Aberto em 2026-08-18
Severidade ALTA
Apps afetados api · web
Revisão do diagnóstico 3 rodadas; a 4ª foi dispensada pelo usuário
Revisão da correção aprovada na rodada 3

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

O fix atravessa dois portões, cada um revisado por um subagente de contexto limpo, em até 3 rodadas. Máquina do plugin superpowers, régua em .claude/rubrics/. Qualquer Critical reprova; Important em aberto reprova salvo decisão registrada do usuário.

  1. O diagnóstico (seções 1 a 4) é revisado 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) é revisada 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

São duas regras furadas pela mesma causa, e a segunda é mais larga que a primeira.

  1. A janela da análise. A imobiliária não mexe no dossiê enquanto a análise está com a garantidora — regra do commit c60ba72, com 403 ANALYSIS_NOT_EDITABLE_BY_AGENCY. Anexar é barrado; trocar o arquivo de um documento já anexado e editar as notas dele, não.
  2. A garantia, onde a porta ficou aberta de propósito — e o usuário decidiu fechar. A spec 0038 deu à imobiliária leitura e nenhuma escrita sobre garantia, mas deixou o upload de documento fora de escopo, por escrito: "não se fecha nem se alarga" (spec/0038…:308 e :368). Ela anexa, troca e edita documento de garantia da carteira dela, em qualquer estado — medido. Isto não é regra esquecida: é decisão registrada que este fix reverte, a pedido do usuário em 2026-08-18.

Na prática: o analista abre o comprovante de renda que está avaliando e lê um arquivo diferente do que foi enviado, trocado depois do envio.

O quanto isso é visível depende de onde o documento está anexado, e vale registrar porque muda o tamanho do sintoma. Documento da análise aparece no histórico da tela: replace_content emite ChangeOperation.REPLACED (document_service.py:324-332, RN23 da spec 0037) e a linha "Documento substituído" fica à vista de quem analisa. Documento do inquilino ou do imóvel — por onde passa a conferência por pessoa da spec 0008 — não entra nesse histórico: o registrar_item é condicionado a doc.credit_analysis_id, que ali é None.

Rastro existe nos dois casos no log de auditoria (DOCUMENT_REPLACED, :335-347), mas ele não serve de aviso: audit:list é do ADMIN_TENANT (apps/api/app/db/rbac_seed.py:166), e o analista, que é quem está avaliando, não o enxerga.

Não foi relatado por usuário — apareceu na revisão da correção do fix 0010, quando o botão "Anexar outra via" do modal passou a oferecer a troca também a partir da falha de exibição.

2. Reprodução

Contra o dev-remote, com imob@e2e.test (AGENCY_USER da Imobiliária E2E):

  1. Criar inquilino, imóvel e análise pela própria imobiliária.
  2. Anexar um PDF na análise em DRAFT — permitido, é a janela dela.
  3. POST /credit-analyses/{id}/submit → a análise vai para SUBMITTED, com a garantidora.
  4. Tentar as quatro escritas sobre documento, com o token dela.

Medido com curl contra https://dev.garantora.com.br/api/v1, autenticado como imob@e2e.test, nesta ordem:

Ação Rota Resultado
anexar outro POST /documents 403 ANALYSIS_NOT_EDITABLE_BY_AGENCY ✔
trocar o arquivo PUT /documents/{id}/content 200 ✗ — content_updated_at atualizado
editar notas/nome PATCH /documents/{id} 200 ✗
excluir DELETE /documents/{id} 403 — mas por permissão (routers/documents.py:270 × apps/api/app/db/rbac_seed.py:276), não pela janela

2b. Documento de garantia — nenhuma guarda, em nenhuma rota

Mesmo usuário, sem precisar de estado nenhum:

  1. GET /guarantees?limit=1 com o token dela — devolve garantia da própria carteira.
  2. POST /documents com entity_type=guarantee → 201.
  3. PUT /documents/{id}/content → 200.
  4. PATCH /documents/{id} → 200.

Medido na garantia bbf32930-… (PENDING_SIGNATURE), com o documento removido depois pela garantidora. Aqui nem o anexar é barrado — ao contrário do dossiê da análise.

  • Ambiente: dev-remote; o defeito é de serviço e vale igual em qualquer ambiente.
  • Superfície: API. Pela tela, o caminho é o botão de substituir do painel de documentos — e, desde o fix 0010, também o "Anexar outra via" do modal de falha.
  • Dados/pré-condições: usuário com partner_agency_id, análise da carteira dela fora de DRAFT/PENDING, e um documento anexado antes do envio.

3. Esperado × Atual

Dono do documento Esperado Atual
análise / inquilino / imóvel, fora da janela (DRAFT+PENDING) as quatro escritas recusadas com 403 ANALYSIS_NOT_EDITABLE_BY_AGENCY só o anexar é recusado; trocar e editar passam; excluir cai por falta de permissão, não pela regra
garantia, em qualquer estado nenhuma escrita da imobiliária (spec 0038) anexar, trocar e editar passam; excluir cai por falta de permissão

A proteção do excluir é por acidente: vem da matriz de permissões, não da regra.

4. Causa raiz

  • Onde: apps/api/app/services/document_service.py:128 é a única chamada de _garantir_janela_da_imobiliaria (definida em :193). Faltam em replace_content (:249), em update (:350) e em delete (:457). Introduzido em c60ba72 (2026-08-15), que criou a guarda junto com o upload e não voltou às outras escritas.

  • O mecanismo: a guarda é uma pergunta sobre a entidade — "existe análise desta imobiliária, fora da janela, ligada a este inquilino/imóvel/análise?" — e recebe (entity_type, entity_id). No upload esses dois vêm do corpo da requisição, então chamá-la ali é natural: os argumentos estão à mão. Nas outras três escritas não há corpo com entidade: elas recebem um doc_id, carregam o documento e mexem nele. A entidade existe (doc.entity_type / doc.entity_id), mas é preciso ir buscá-la no objeto carregado — e foi aí que a chamada não aconteceu.

Ou seja: a regra foi implementada como propriedade da ação de anexar, quando ela é uma propriedade do dossiê. É o que explica o recorte exato do defeito — barrado onde a entidade chega pronta, livre onde ela precisa ser derivada do documento.

E explica por que ninguém percebeu: test_documento_janela_imobiliaria.py tem quatro testes, os quatro sobre POST /documents — a suíte cobre a ação que foi corrigida, não a regra.

  • O alcance, e ele é maior do que "as outras escritas". Para documento de análise, inquilino e imóvel, o furo é nas três rotas além do upload; para documento de garantia, é nas quatro, porque a guarda nem chega a olhar esse tipo. As rotas: POST /documents (apps/api/app/api/v1/routers/documents.py:16, permissão em :54), PUT /documents/{id}/content (:200, permissão em :244), PATCH /documents/{id} (:175, permissão em :194) e DELETE /documents/{id} (:250, permissão em :270).

Duas estão abertas hoje; a terceira depende de uma permissão para continuar fechada. O DELETE exige documents:delete, que o AGENCY_USER não tem no seed — mas documents:update, que governa as duas abertas, ele tem. Um perfil customizado do tenant com documents:delete (o ADMIN_TENANT pode criar) reabre a exclusão sem nenhum aviso: a proteção de hoje é da matriz de permissões, não da regra de negócio, e as duas podem divergir a qualquer momento.

E o quarto tipo de dono, que a guarda nem chega a olhar: a garantia. O Document tem quatro FKs mutuamente exclusivas, e a guarda trata credit_analysis, renter e property — para o resto ela retorna cedo (document_service.py:238-239). Só que a imobiliária alcança documento de garantia da própria carteira: _owned_by_agency inclui Document.guarantee_id das garantias com o partner_agency_id dela (apps/api/app/repositories/document_repo.py:39-41). Com documents:update, ela troca e edita ali — e a spec 0038 deu a ela nenhuma escrita sobre garantia exceto por documento, que ela deixou explicitamente de fora ("Fechar a porta de upload da imobiliária… fica para quem for tratar isso de propósito"). O único freio existente é _garantir_que_nao_e_contrato_assinado (:425), que protege só o documento apontado como contrato, só em ACTIVE/EXPIRED, e não vale para o PATCH (:437-439).

Medido, não deduzido: com o token dela, POST devolveu 201, PUT /content 200 e PATCH 200 numa garantia PENDING_SIGNATURE da carteira dela (seção 2b). O upload chega até lá porque _VALID_ENTITY_TYPES inclui guarantee (apps/api/app/services/document_service.py:71-78) e _validate_entity (:505) o resolve pelo GuaranteeRepository, que casa o partner_agency_id — e aí a guarda cai no else.

Decisão do usuário (2026-08-18), e ela reverte a da spec 0038: documento de garantia passa a seguir a mesma linha do resto da garantia — a imobiliária não escreve, em estado nenhum. Não é "janela": é ausência de escrita. A 0038 tinha adiado isso de propósito, por ser "mudança de acesso com efeito fora daquela spec"; o efeito, agora, é este fix.

O que ficou descartado

  • Não é escopo de tenant nem de imobiliária. O documento é alcançado por get_with_content(..., agency_scope(current_user)) (:283 no replace_content, :458 no delete) e por get_metadata(..., agency_scope(current_user)) (:351 no update) — ela só mexe no que é da própria carteira. O defeito é sobre quando, não sobre de quem.
  • Não é a tela. O painel oferece o botão de substituir sem condicional, e o modal do fix 0010 oferece "Anexar outra via" — mas o 403 do POST prova que a tela não é o que trava nada; a regra vive no serviço, e é lá que ela está incompleta.
  • Não é regressão do fix 0010. A lacuna nasceu com a guarda, em c60ba72, três dias antes; o 0010 só ampliou a superfície que leva até ela.

5. Revisão do diagnóstico

Preenchido pelo laço do /fix new, rubrica .claude/rubrics/diagnostico.md. Registrar todas as rodadas, inclusive as reprovadas.

Rodada Critical Important Minor Veredicto
1 2 1 2 reprovado — dois arquivo:linha deslocados pelo fix 0010, e uma afirmação falsa sobre o histórico
2 0 2 2 reprovado — o alcance não cobria entity_type = guarantee
3 1 1 2 reprovado — o POST também está aberto para documento de garantia
4 — — — —

Resultado: aprovado na rodada N.

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

6. Correção

apps/api

  • Onde: app/services/document_service.py — replace_content (:249), update (:350) e delete (:457).
  • O quê: chamar a guarda depois de carregar o documento e antes de qualquer escrita, nas três — e não só nas duas abertas: fechar o DELETE pela regra tira a dependência de uma permissão que pode mudar.
  • E a guarda precisa cobrir o quarto tipo. Hoje ela retorna cedo para guarantee (:238-239). Pela decisão do usuário, documento de garantia é escrita nenhuma para quem tem partner_agency_id — regra mais simples que a janela, e que não depende de estado. Vale para as quatro rotas: ali o upload também está aberto hoje. O nome do método deixa de descrever o que ele faz; vale renomear para algo como _garantir_que_a_imobiliaria_pode_escrever, com os dois ramos dentro.
  • Cuidado: entity_type/entity_id são @property sobre as quatro FKs (models/document.py:65-79); documento sem nenhuma delas não pode estourar.

apps/web

  • Não era necessário para a regra valer, e virou parte da entrega mesmo assim — porque a correção cria três 403 novos, e a tela oferecia os três botões. O blockedReason, que já existia e escondia só o "Adicionar", passa a esconder também substituir e excluir; e a tela da garantia passa a recebê-lo para quem tem partner_agency_id.
  • Onde: src/components/shared/DocumentsPanel.tsx (ações de linha e o botão "Anexar outra via" do modal de falha do fix 0010) e src/pages/guarantees/show.tsx (os dois painéis).

apps/e2e

  • Sem impacto, e não é omissão: a prova é de serviço e cabe no pytest, que exercita as quatro rotas com o papel certo. O e2e só alcançaria o PUT /content da imobiliária encenando a tela inteira, para medir o mesmo 403.

7. Risco / impacto colateral

  • A guarda passa a rodar em três caminhos novos, e ela faz uma consulta a credit_analyses + credit_analysis_participants. É uma query indexada por tenant e imobiliária, executada uma vez por escrita — custo desprezível ao lado do read() do arquivo.
  • Efeito já aceito, agora mais largo: a decisão de 2026-08-15 registra que, com uma análise dela em avaliação, o cadastro daquele inquilino fica congelado para a imobiliária também nas outras locações. Isso passa a valer para trocar e editar, não só para anexar. É o mesmo princípio, e a alternativa — janela por ação — é o que produziu este defeito.
  • A garantidora não é afetada: a guarda retorna cedo para quem não tem partner_agency_id.
  • Não toca dinheiro nem cálculo.

8. Critérios de aceite

  • [x] CA1. Seguindo os passos da seção 2, PUT /documents/{id}/content e PATCH /documents/{id} respondem 403 ANALYSIS_NOT_EDITABLE_BY_AGENCY.
  • [x] CA2. DELETE /documents/{id} responde 403 pela regra, e não só por falta de permissão. O teste concede documents:delete a um perfil customizado mantendo o partner_agency_id do usuário — é esse par que reproduz o cenário em que a proteção de hoje evapora, e sem ele o CA passaria pelo motivo errado.
  • [x] CA3. Na janela dela (DRAFT e PENDING), as três escritas continuam funcionando.
  • [x] CA4. A garantidora continua trocando, editando e excluindo em qualquer estado.
  • [x] CA5. Análise de outra imobiliária não barra a troca — o recorte por carteira continua valendo.
  • [x] CA6. Documento anexado a uma garantia da carteira dela: a imobiliária recebe 403 nas quatro escritas — inclusive POST /documents, que hoje devolve 201 —, em qualquer estado da garantia. É a decisão da spec 0038, não a janela da análise. A garantidora continua escrevendo.

9. Plano

  • [x] Escrever o teste que reproduz o bug e vê-lo falhar (apps/api/tests/ e/ou apps/e2e/) — antes de qualquer correção
  • [x] Corrigir a causa raiz — na ordem api → web → e2e se atravessar camadas
  • [x] Red-green verificado: teste passa → reverter a correção → rodar → falha → restaurar → passa. Se o fix é só visual, a prova é o script de screenshot nos dois viewports, com o que se conferiu
  • [x] make test e make lint sem apontamento novo nos arquivos tocados
  • [ ] Se mexeu no web, validar no browser de verdade em desktop e mobile (e, se for build/PWA, reabrir a tela instalada para furar o cache)

10. Decisões em aberto

Nenhuma. A regra da análise já existe e está escrita (c60ba72); o que falta é aplicá-la nas outras três escritas. E a política para documento de garantia foi decidida pelo usuário em 2026-08-18, registrada na seção 4: nenhuma escrita pela imobiliária.

11. Revisão da correção

Preenchido pelo laço do /fix implement, rubrica .claude/rubrics/correcao.md. Registrar todas as rodadas.

Rodada Critical Important Minor Veredicto
1 0 1 5 reprovada — Swagger das rotas que mudaram de comportamento
2 0 2 5 reprovada — o PATCH de observação seguia oferecido, e a 0038 tinha decidido deixar a porta aberta
3 0 0 7 aprovada

O teste sobreviveria à reversão? Sim — duas medições, com comando, sobre o arquivo final.

  • A correção: revertendo as três chamadas novas da guarda e o ramo da garantia → 15 failed, 21 passed. Restaurando → 36 passed.
  • A ordem das guardas: pondo _garantir_que_nao_e_contrato_assinado de volta na frente → os dois casos de test_no_contrato_assinado_a_imobiliaria_ouve_a_regra_dela_primeiro ficam vermelhos (a imobiliária ouvia 422 SIGNED_DOCUMENT_IN_USE no lugar do 403 dela). Restaurando → verdes. A decisão de ordem virou prova, não comentário.

Fechados ainda na rodada 3, dos Minor apontados: o recorte por carteira ganhou teste nas rotas novas, o DELETE na janela passou a cobrir PENDING além de DRAFT, e saíram a constante morta e a duplicação do helper de perfil customizado.

Defeitos aceitos:

  • Os painéis de inquilino e imóvel não recebem blockedReason. Fechá-los exige o status da análise, que essas telas não carregam. A recusa da API aparece no alerta com a mensagem certa — degrada, não quebra.
  • A tela não foi exercitada por e2e nesta entrega. O apps/web do dev-remote não foi reconstruído; o que mudou ali é condicional de exibição, e a regra que importa está provada no pytest.

12. Registro

O que era, e o que virou

  • A guarda era chamada em um ponto (upload) e passa a ser chamada nos quatro, com a entidade derivada do documento carregado nos três novos.
  • Ela ganhou o ramo da garantia: 403 GUARANTEE_NOT_EDITABLE_BY_AGENCY, com código e mensagem próprios. Reusar o código da janela seria dizer "a análise já foi enviada" sobre um documento de garantia, o que é falso — ali não há estado que reabra a escrita.
  • O método mudou de nome — _garantir_janela_da_imobiliaria → _garantir_que_a_imobiliaria_pode_escrever —, porque passou a responder duas regras. O nome antigo mentiria sobre o ramo da garantia.

O que divergiu do plano

  • O apps/web entrou, e a seção 6 dizia "sem impacto necessário". A correção cria três 403 onde a tela oferecia botão; esconder o que vai ser negado é o princípio que o menu já segue.
  • A ordem das guardas mudou em replace_content e delete: "esta pessoa pode escrever aqui?" passou a vir antes de "este documento está em uso?". Invertida, a imobiliária que tentasse trocar o contrato assinado recebia 422 SIGNED_DOCUMENT_IN_USE no lugar do 403 da regra dela — bloqueada pelo motivo errado. Achado do revisor.
  • O Swagger das quatro rotas foi atualizado, incluindo o POST, que carregava a mesma dívida desde c60ba72 — o commit que criou a guarda não tocou o router.
  • Um docstring da spec 0038 virou mentira e foi corrigido. guarantee_service.register_signature dizia que a separação entre anexar e ativar "faz a porta de upload da imobiliária deixar de importar para o estado". A porta fechou; a frase agora aponta para este fix.
  • O lápis de observação ganhou data-testid (documents-edit-notes-{id}), que faltava — o preflight acusou assim que ele passou a ser condicional.
  • Dois painéis ficam sem blockedReason, e é pendência aceita: os de inquilino e imóvel (renters/show.tsx, properties/show.tsx). Fechá-los exige o status da análise, que essas telas não carregam — a recusa da API aparece no alerta com a mensagem certa, então degrada em vez de quebrar.