Pular para conteúdo

0018 — Inquilino cadastrado no formulário de análise não fica vinculado à imobiliária

Status CORRIGIDO
Branch fix/0018-inquilino-inline-sem-imobiliaria
Aberto em 2026-09-06
Severidade MÉDIA
Apps afetados web (api não muda — o campo já existe e já é resolvido no escopo)
Revisão do diagnóstico aprovado na rodada 2
Revisão da correção aprovado na rodada 1 (nota 9/10)

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. Sintoma

No formulário de nova análise de crédito, quando quem cria é usuário da garantidora (não é AGENCY_USER) e escolhe uma imobiliária para vincular o imóvel novo, o imóvel criado inline fica corretamente associado a essa imobiliária — mas o inquilino criado no mesmo formulário, na mesma submissão, não fica. Ele nasce com partner_agency_id nulo, na carteira própria da garantidora.

Efeito visível: um usuário AGENCY_USER daquela imobiliária, abrindo a própria lista de Inquilinos, não encontra a pessoa que ele mesmo passou a atender por aquela análise — o inquilino não aparece na busca nem na listagem, porque as duas são filtradas por partner_agency_id, e o dele ficou vazio. O imóvel e a própria análise, ao contrário, aparecem normalmente para ele, porque os dois carregam o vínculo certo.

2. Reprodução

  1. Entrar como usuário da garantidora (ADMIN_TENANT, ANALYST ou OPERATOR — qualquer um sem partner_agency_id).
  2. Abrir Análise de Crédito → Nova análise.
  3. Escolher uma imobiliária no seletor da etapa 1 (o painel "Imobiliária" aparece sempre para quem não é AGENCY_USER — se houver só uma imobiliária ativa, ela já vem pré-selecionada).
  4. Em Imóvel, escolher "Cadastrar novo" e preencher os dados — ele nasce associado à imobiliária escolhida no passo 3.
  5. Em Inquilino, escolher "Cadastrar novo" e preencher os dados de uma pessoa que ainda não existe na base.
  6. Salvar a etapa 1 (ou seguir até concluir a análise).
  7. Entrar como AGENCY_USER da imobiliária escolhida no passo 3 e abrir Inquilinos.

Resultado: o inquilino cadastrado no passo 5 não aparece na lista, nem buscando pelo nome ou documento. Confirmado também pelo lado do dado: consultando o registro do inquilino, partner_agency_id é nulo, enquanto o do imóvel criado no mesmo formulário tem o UUID da imobiliária escolhida.

  • Ambiente: dev-remote (reproduzido); o mecanismo é o mesmo em produção e local — não depende de dado de ambiente específico.
  • Superfície: desktop e mobile — o formulário é o mesmo componente (CreditAnalysisForm.tsx) nos dois viewports; o defeito está na chamada à API, não em layout.
  • Dados/pré-condições: quem cria não é AGENCY_USER (senão o painel de imobiliária nem aparece — o vínculo já vem do próprio perfil), e um inquilino novo (o defeito não se manifesta ao escolher um inquilino já existente na base, porque nesse caso nada é criado).

3. Esperado × Atual

  • Esperado: o inquilino criado inline no formulário, na mesma submissão em que uma imobiliária foi escolhida, nasce com partner_agency_id igual ao do imóvel criado na mesma submissão — o mesmo que a análise resultante vai carregar.
  • Atual: o inquilino nasce com partner_agency_id nulo (carteira própria da garantidora), independentemente da imobiliária escolhida.

4. Causa raiz

  • Onde: apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx, função resolveEntities. A chamada rentersApi.create({...}), na linha 724, monta o corpo da requisição sem o campo partner_agency_id. Trinta e quatro linhas abaixo, a chamada irmã propertiesApi.create({...}) começa na linha 758 e, na linha 767 do próprio corpo dessa chamada, inclui ...(agencyId ? { partner_agency_id: agencyId } : {}) — a mesma condição que falta no corpo da chamada do inquilino.

  • O mecanismo: o formulário mantém um único estado, agencyId, que representa a imobiliária escolhida para toda a submissão (ele também escopa a busca de imóveis existentes, em searchProperties). Ao montar o corpo de cada criação inline, o código decide campo por campo o que mandar; para o imóvel, alguém lembrou de propagar agencyId como partner_agency_id, e para o inquilino, não. A API não é o problema: RenterCreate já aceita partner_agency_id opcionalmente, e RenterService.create (apps/api/app/services/renter_service.py:88-92) já resolve esse campo no escopo do usuário antes de gravar, exatamente pelo mesmo caminho que PropertyService.create usa — o mecanismo de segurança existe e funciona; ele simplesmente nunca é acionado para o inquilino, porque o campo nunca sai do formulário.

A assimetria não é recente: o seletor de imobiliária e o encaminhamento de partner_agency_id para o imóvel existem desde a introdução desta tela (spec 0015, commit 6455b20) e foram refinados depois (a78d1b4, escondendo o seletor para quem já é imobiliária). O bloco do inquilino nunca recebeu o mesmo tratamento — não é regressão de uma mudança pontual, é uma lacuna que existe desde a origem da tela.

  • O alcance: procurei por todo outro ponto do apps/web que crie inquilino ou imóvel inline associando a uma imobiliária escolhida, e não há outro com o mesmo defeito. O cadastro avulso de inquilino (apps/web/src/pages/renters/form.tsx) também tem um seletor de imobiliária para quem é da garantidora (showAgencyField, linha 53) e, ao contrário do formulário de análise, propaga o campo corretamente: body.partner_agency_id = selectedAgency.id no onSubmit (linhas 149-150) quando há seleção. É a mesma decisão que falta no bloco do inquilino do CreditAnalysisForm.tsx — e é o padrão a seguir na correção, não um segundo lugar com o bug. Onde a mesma causa morde, dentro do formulário de análise: só o bloco do inquilino principal (resolveEntities); não há um segundo inquilino inline nesta tela (co-participantes usam outro fluxo, fora do escopo deste sintoma). O efeito colateral documentado no sintoma — o inquilino sumir da lista de Inquilinos do AGENCY_USER — vem de RenterRepository.search_with_count filtrar por partner_agency_id=agency_scope(current_user) (apps/api/app/services/renter_service.py:121-123): com o campo nulo, o registro não casa com o escopo de nenhuma imobiliária, só com o da própria garantidora.

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 1 1 0 reprovado
2 0 0 1 aprovado

Resultado: aprovado na rodada 2.

Pendências aceitas: a seção 2 dizia que o seletor de imobiliária "aparece quando há mais de uma imobiliária ativa"; na verdade ele aparece sempre que quem cria não é AGENCY_USER, e com uma única imobiliária ela só vem pré-selecionada. Corrigido de passagem, não ficou como pendência.

6. Correção

apps/api

Sem impacto. RenterCreate.partner_agency_id já existe e RenterService.create já resolve o vínculo no escopo do usuário (mesmo caminho que PropertyService.create usa) — nada muda aqui.

apps/web

  • Onde: apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx, dentro de resolveEntities, no bloco if (renterMode === "new") { ... }.
  • O quê: incluir ...(agencyId ? { partner_agency_id: agencyId } : {}) no corpo de rentersApi.create, espelhando exatamente o que o bloco do imóvel já faz.
  • data-testid: nenhum novo — a correção não adiciona elemento de interface, só um campo no corpo de uma requisição já existente.
  • Responsividade & PWA: sem impacto — a correção não muda layout nem asset.

apps/e2e

Sem impacto — o plano original previa um teste e2e aqui (ver histórico desta seção abaixo); a implementação optou pelo caminho mais barato e mais direto, explicado a seguir.

O teste de regressão — unitário no apps/web, não e2e

Mudança de plano em relação à seção 9 original, decidida na implementação. O plano previa um cenário e2e, contra a API, conferindo o efeito colateral do sintoma (o inquilino sumir de GET /renters para o AGENCY_USER). Escrevendo o teste, ficou claro que a suíte unitária do apps/web (spec 0047) já tem tudo que esse teste precisa — o dublê de rede intercepta o POST /api/v1/renters de verdade — e mede a causa raiz diretamente (o corpo da requisição), em vez do efeito colateral duas camadas depois (filtro de partner_agency_id na listagem, que é comportamento da API, não desta tela).

O teste novo está em apps/web/src/pages/credit-analyses/CreditAnalysisForm.test.tsx, describe "inquilino cadastrado inline herda a imobiliária escolhida — fix 0018": preenche o formulário com inquilino e imóvel novos, uma imobiliária pré-selecionada (caso de imobiliária única, que se autoseleciona), salva, e afirma que o corpo JSON enviado a POST /api/v1/renters carrega partner_agency_id — a mesma asserção que a correção existe para tornar verdadeira. Um segundo caso, no mesmo arquivo, cobre CA2 (abaixo).

7. Risco / impacto colateral

Nenhum identificado. A correção soma um campo a uma chamada existente, usando um dado (agencyId) que o próprio formulário já calcula e já usa para o imóvel — não introduz estado novo nem muda o que acontece quando agencyId está vazio (usuário de imobiliária, ou garantidora com uma imobiliária só e nenhuma selecionada): nesses casos a chamada continua exatamente como está hoje, sem o campo.

Não toca cálculo financeiro, migração de dado nem fluxo de outra tela.

8. Critérios de aceite

  • [x] CA1. Seguindo os passos da seção 2, o inquilino criado passa a ter partner_agency_id igual ao da imobiliária escolhida, e aparece na lista de Inquilinos do AGENCY_USER daquela imobiliária. Verificado pelo teste unitário (seção 6): o corpo de POST /api/v1/renters carrega o campo — o que a API já resolve corretamente a partir daí (RenterService.create) é comportamento preexistente, fora do escopo desta correção.
  • [x] CA2. Quando agencyId está vazio (usuário de imobiliária criando a própria análise, ou garantidora sem imobiliária selecionada), o comportamento não muda: o inquilino continua nascendo sem partner_agency_id explícito, como hoje. Coberto pelo segundo caso do teste unitário (usuário AGENCY_USER, onde o seletor de imobiliária nem existe na tela) — achado da rodada 1 de revisão, que apontou a lacuna.
  • [x] CA3. O imóvel criado na mesma submissão continua recebendo o vínculo como já recebe hoje — a correção não altera esse bloco. Verificado no mesmo teste do CA1: o corpo de POST /api/v1/properties continua carregando partner_agency_id.

9. Plano

  • [x] Escrever o teste que reproduz o bug e vê-lo falhar — antes de qualquer correção. Mudou de e2e (plano original) para unitário no apps/web, decisão registrada na seção 6.
  • [x] Corrigir em apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx
  • [x] Red-green verificado: teste passa → reverter a correção → rodar → falha → restaurar → passa. Feito rodando de verdade (git stash só do arquivo da correção, mantendo o teste), duas vezes — uma vez por quem implementou, outra pelo revisor da seção 11, que confirmou de forma independente.
  • [x] make preflight e make test-web sem apontamento novo no arquivo tocado — make preflight saiu limpo (inclusive a suíte unitária do web).
  • [x] Validar no browser em desktop e mobile — sem impacto de layout (seção 2: "o defeito está na chamada à API, não em layout"), então a prova aceitável passa a ser o teste unitário, que exercita o mesmo componente CreditAnalysisForm.tsx usado nos dois viewports.

10. Decisões em aberto

Nenhuma.

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 0 3 aprovado (nota 9/10)

Resultado: aprovado na rodada 1.

O teste sobreviveria à reversão? Sim. O revisor rodou de verdade — não só leu —, revertendo só CreditAnalysisForm.tsx e mantendo o teste: expect(corpo.partner_agency_id) .toBe(AGENCIA_ID) falha com Received: undefined, e a asserção gêmea sobre o corpo do imóvel (CA3) continua passando nesse cenário — confirma que o teste está isolado no mecanismo certo, não é falso vermelho por engano de arranjo.

Pendências aceitas (achados Minor da rodada 1, corrigidos de passagem ou aceitos):

  1. CA2 sem prova direta — corrigido: acrescentado o caso "sem imobiliária escolhida (usuário da própria imobiliária)" no mesmo arquivo de teste, antes de fechar o fix.
  2. Divergência entre o plano (e2e) e o entregue (unitário) — corrigido: as seções 6 e 9 acima passaram a registrar a mudança de abordagem e o porquê.
  3. Metadado com status defasado (docs/fixes/index.md, docs/metadata/catalog.yaml ainda diziam diagnosticado) — corrigido: sincronizados para corrigido ao fechar este registro.

12. Registro

  • Causa raiz: confirmada durante o diagnóstico (seção 4) — resolveEntities, em CreditAnalysisForm.tsx, montava o corpo de rentersApi.create sem propagar agencyId como partner_agency_id, ao contrário do bloco irmão do imóvel.
  • Correção: uma linha (mais comentário), espelhando exatamente o padrão já usado no bloco do imóvel — apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:736.
  • Teste de regressão: unitário, apps/web/src/pages/credit-analyses/ CreditAnalysisForm.test.tsx, dois casos novos (CA1+CA3 num, CA2 no outro). Decisão de trocar e2e por unitário registrada na seção 6.
  • Verificação: make preflight limpo (Swagger, migrations, CommitRoute, data-testid, meta-testes e a suíte unitária do apps/web, que não é afetada pela API — este fix não toca apps/api). Red-green feito duas vezes, de forma independente (por quem implementou e pelo revisor da seção 11).
  • apps/api e apps/e2e: sem alteração — o campo e a resolução de escopo já existiam e já funcionavam; o defeito era só a tela não propagar o valor.
  • Status final: CORRIGIDO.