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.
- 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.
- 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.
- 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 403ANALYSIS_NOT_EDITABLE_BY_AGENCY. Anexar é barrado; trocar o arquivo de um documento já anexado e editar as notas dele, não. - 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…:308e: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):
- Criar inquilino, imóvel e análise pela própria imobiliária.
- Anexar um PDF na análise em
DRAFT— permitido, é a janela dela. POST /credit-analyses/{id}/submit→ a análise vai paraSUBMITTED, com a garantidora.- 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:
GET /guarantees?limit=1com o token dela — devolve garantia da própria carteira.POST /documentscomentity_type=guarantee→ 201.PUT /documents/{id}/content→ 200.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 deDRAFT/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 emreplace_content(:249), emupdate(:350) e emdelete(:457). Introduzido emc60ba72(2026-08-15), que criou a guarda junto com ouploade 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). Nouploadesses 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 umdoc_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) eDELETE /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))(:283noreplace_content,:458nodelete) e porget_metadata(..., agency_scope(current_user))(:351noupdate) — 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
POSTprova 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) edelete(: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
DELETEpela 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 tempartner_agency_id— regra mais simples que a janela, e que não depende de estado. Vale para as quatro rotas: ali ouploadtambé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_idsão@propertysobre 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 tempartner_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) esrc/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 oPUT /contentda 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 doread()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}/contentePATCH /documents/{id}respondem 403ANALYSIS_NOT_EDITABLE_BY_AGENCY. - [x] CA2.
DELETE /documents/{id}responde 403 pela regra, e não só por falta de permissão. O teste concededocuments:deletea um perfil customizado mantendo opartner_agency_iddo 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 (
DRAFTePENDING), 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/ouapps/e2e/) — antes de qualquer correção - [x] Corrigir a causa raiz — na ordem
api → web → e2ese 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 testemake lintsem 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_assinadode volta na frente → os dois casos detest_no_contrato_assinado_a_imobiliaria_ouve_a_regra_dela_primeiroficam vermelhos (a imobiliária ouvia422 SIGNED_DOCUMENT_IN_USEno 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/webdo dev-remote não foi reconstruído; o que mudou ali é condicional de exibição, e a regra que importa está provada nopytest.
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/webentrou, 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_contentedelete: "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 recebia422 SIGNED_DOCUMENT_IN_USEno 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 desdec60ba72— o commit que criou a guarda não tocou o router. - Um docstring da spec 0038 virou mentira e foi corrigido.
guarantee_service.register_signaturedizia 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 — opreflightacusou 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.