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¶
- Entrar como usuário da garantidora (
ADMIN_TENANT,ANALYSTouOPERATOR— qualquer um sempartner_agency_id). - Abrir Análise de Crédito → Nova análise.
- 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). - Em Imóvel, escolher "Cadastrar novo" e preencher os dados — ele nasce associado à imobiliária escolhida no passo 3.
- Em Inquilino, escolher "Cadastrar novo" e preencher os dados de uma pessoa que ainda não existe na base.
- Salvar a etapa 1 (ou seguir até concluir a análise).
- Entrar como
AGENCY_USERda 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_idigual 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_idnulo (carteira própria da garantidora), independentemente da imobiliária escolhida.
4. Causa raiz¶
-
Onde:
apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx, funçãoresolveEntities. A chamadarentersApi.create({...}), na linha 724, monta o corpo da requisição sem o campopartner_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, emsearchProperties). 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 propagaragencyIdcomopartner_agency_id, e para o inquilino, não. A API não é o problema:RenterCreatejá aceitapartner_agency_idopcionalmente, eRenterService.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 quePropertyService.createusa — 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/webque 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.idnoonSubmit(linhas 149-150) quando há seleção. É a mesma decisão que falta no bloco do inquilino doCreditAnalysisForm.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 doAGENCY_USER— vem deRenterRepository.search_with_countfiltrar porpartner_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 deresolveEntities, no blocoif (renterMode === "new") { ... }. - O quê: incluir
...(agencyId ? { partner_agency_id: agencyId } : {})no corpo derentersApi.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_idigual ao da imobiliária escolhida, e aparece na lista de Inquilinos doAGENCY_USERdaquela imobiliária. Verificado pelo teste unitário (seção 6): o corpo dePOST /api/v1/renterscarrega 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
agencyIdestá 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 sempartner_agency_idexplícito, como hoje. Coberto pelo segundo caso do teste unitário (usuárioAGENCY_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/propertiescontinua carregandopartner_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 stashsó 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 preflightemake test-websem apontamento novo no arquivo tocado —make preflightsaiu 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.tsxusado 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):
- 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.
- 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ê.
- Metadado com status defasado (
docs/fixes/index.md,docs/metadata/catalog.yamlainda diziamdiagnosticado) — corrigido: sincronizados paracorrigidoao fechar este registro.
12. Registro¶
- Causa raiz: confirmada durante o diagnóstico (seção 4) —
resolveEntities, emCreditAnalysisForm.tsx, montava o corpo derentersApi.createsem propagaragencyIdcomopartner_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 preflightlimpo (Swagger, migrations,CommitRoute,data-testid, meta-testes e a suíte unitária doapps/web, que não é afetada pela API — este fix não tocaapps/api). Red-green feito duas vezes, de forma independente (por quem implementou e pelo revisor da seção 11). apps/apieapps/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.