0034 — Isolamento de tenant e de imobiliária, e login sem credencial pré-preenchida¶
| Status | CONCLUÍDA |
| Branch | spec/0034-isolamento-de-tenant-e-login |
| Criada em | 2026-08-10 |
| Apps afetados | api · web · e2e |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDA
1. Problema¶
Duas dores, ambas de confiança na plataforma.
A tela de login entrega a chave da casa. Quem abre /app/login encontra os campos já preenchidos com admin@garantora.com.br e Admin@1234! — a conta de administrador global do SaaS (apps/web/src/pages/login/index.tsx:34-35). Era conveniência de desenvolvimento que atravessou para o ambiente que está no ar. Basta clicar em Entrar.
O isolamento entre empresas não é uniforme. A promessa do produto é que cada garantidora só enxerga o que é dela, e que cada imobiliária parceira só enxerga os próprios negócios. A primeira parte funciona quase toda — o BaseRepository filtra tenant_id. A segunda tem buracos, e há uma classe de falha que fura até a primeira.
O que vaza hoje¶
Um usuário de imobiliária vê os inquilinos de todas as imobiliárias da garantidora. RenterService filtra só por tenant (renter_service.py:63,103). O usuário da Imobiliária A abre Inquilinos e lê nome, CPF, RG, data de nascimento, telefone e endereço dos clientes da Imobiliária B — sua concorrente na mesma praça.
E baixa os documentos deles. DocumentService também filtra só por tenant. Com o ID do documento, o usuário da A baixa o comprovante de renda, o contracheque ou o RG digitalizado de um inquilino da B.
E consulta o cadastro das outras imobiliárias. GET /api/v1/partner-agencies/{id} responde para qualquer agência do mesmo tenant (partner_agency_service.py:78).
Um administrador de garantidora mexe no perfil de acesso de usuário de outra garantidora. PermissionService.assign_role_to_user e revoke_role_from_user (permission_service.py:177,193) validam o perfil, mas nunca conferem o tenant do usuário alvo. Com o UUID de um usuário da Garantidora B, o admin da A concede ou revoga perfis nele.
Chave estrangeira de entrada não é validada — e isso transforma uma escrita numa leitura de outro tenant. É a falha mais séria, porque o filtro do repositório não a pega: o registro criado é legitimamente seu.
CreditAnalysisService.create(credit_analysis_service.py:160) grava oproperty_idrecebido sem nenhuma verificação. A docstring prometeNotFoundErrorse o imóvel não existir no tenant; não há consulta alguma. A FK garante que o imóvel existe — em qualquer tenant.- Os
renter_iddos participantes idem: a única consulta é a do corresponsável PF, e ela toleraNone(segue adiante se o inquilino não for do tenant). - Na leitura,
get_properties_bulk(credit_analysis_repo.py:157) juntaPropertypor id, sem filtro de tenant — corretamente, já que as análises já vieram escopadas.
Encadeando: um usuário da Garantidora A cria uma análise apontando para o imóvel e o inquilino da Garantidora B, e a tela da própria análise passa a exibir o endereço, o nome do proprietário, o documento do proprietário e o contato — mais o nome e o CPF do inquilino alheio. Não é preciso adivinhar rota nem escalar privilégio: só o UUID.
O que não vaza, mas está quebrado¶
credit_analyses.partner_agency_id está NULL em 100% dos 2769 registros do dev-remote. O campo só é preenchido quando quem cria é AGENCY_USER (credit_analysis_service.py:159), e o fluxo normal é a garantidora criar. O vínculo real vive no imóvel — properties.partner_agency_id preenchido em 3465/3465, porque é obrigatório no schema.
Efeito: o filtro por imobiliária nas análises (credit_analysis_repo.py:81) devolve zero para um usuário de imobiliária. E garantias e cobranças herdam esse campo (guarantee_service.py:161, billing_service.py:101), então o mesmo vazio se propaga. O escopo que existe no papel não segura nada — nem no sentido de vazar, nem no de funcionar.
E não há barreira¶
Nenhum teste hoje afirma isolamento de forma sistemática. O próximo endpoint que esquecer o escopo entra sem alarme, exatamente como estes entraram.
2. Objetivo¶
Que o isolamento seja verdade verificável e não convenção: nenhum usuário alcança dados de outra garantidora, nenhum usuário de imobiliária alcança dados de outra imobiliária, e uma suíte de regressão prova isso a cada make test. E que a tela de login peça credenciais em vez de oferecê-las.
3. Fora de escopo¶
- Os 10 papéis area-scoped de
docs/navigation-brief.md. Esta spec trabalha com os 6 papéis que existem no código hoje. Reescrever a matriz de papéis é feature própria, e misturá-la aqui esconderia o que é correção de isolamento. - Row-Level Security no PostgreSQL. Cogitada como rede abaixo da aplicação. Fica fora: mudaria o modelo de conexão (a sessão precisaria carregar o tenant corrente) e o ganho não se paga antes do primeiro cliente. A barreira desta spec é a suíte, na camada de serviço.
- Extinguir
partner_agency_iddeCreditAnalysiseGuaranteeem favor de join comproperties. Foi a alternativa avaliada para a RN10 e recusada: fonte única seria mais limpa, mas encarece toda listagem com um join a mais. Mantém-se o campo, derivado do imóvel em um só lugar (RN10), que é o que elimina o risco de dessincronizar. - Refatorar o escopo para uma dependência única e obrigatória em todos os services. Tocaria quase tudo e transformaria uma correção de segurança numa refatoração ampla, difícil de revisar. A suíte de regressão (RN15) cobre o mesmo risco muito mais barato.
- Auditoria de acesso a documento (quem baixou o quê). Desejável, é outra spec.
- 2FA, política de senha, rate limit no login. A tela muda só no que esta spec pede.
- Rotação da senha do admin global. Já executada pelo usuário em 2026-08-10, fora do código. Registrada aqui só para não parecer pendência.
4. Regras de negócio¶
Login¶
- RN1. Os campos de e-mail e senha nascem vazios. Nenhum valor padrão, nenhum
placeholdercom credencial real, nenhuma dica de usuário de teste — em nenhum ambiente. Não há variável que reative o pré-preenchimento: o comportamento é único. - RN2. O
autocompletedo navegador continua valendo (email,current-password). A regra é sobre o que a aplicação preenche, não sobre o gerenciador de senhas do usuário.
Isolamento por garantidora (tenant)¶
- RN3. Nenhum endpoint autenticado devolve, cria, altera ou remove registro de tenant diferente do
tenant_iddo usuário. Única exceção:ADMIN_GLOBAL. - RN4. Registro de outro tenant responde 404, nunca 403 — um 403 confirmaria que o ID existe, e isso é enumeração de recurso alheio. A exceção é a operação que o usuário não pode fazer no próprio tenant: aí é 403, porque não há informação vazando.
- RN5. Toda chave estrangeira recebida no corpo de uma requisição é resolvida dentro do escopo do solicitante antes de ser gravada. ID que não resolve → 404, e nada é persistido. Vale para
property_iderenter_idna criação e edição de análise de crédito e de participantes,document_type_id,credit_analysis_id,guarantee_id,partner_agency_iderole_id— e para qualquer FK que venha a existir. É esta regra que fecha a leitura-por-escrita descrita na seção 1. - RN6.
assign_role_to_usererevoke_role_from_usersó operam sobre usuário do mesmo tenant do solicitante. Usuário de outro tenant → 404. Vale também para oADMIN_TENANT, que hoje passa direto. - RN7. Só a interface do SaaS admin (
/app/admin/*) enxerga mais de um tenant, e só para quem temADMIN_GLOBAL. Nenhuma tela da interface da garantidora aceita escolher ou receber umtenant_iddiferente do usuário logado.
Isolamento por imobiliária (partner_agency_id)¶
- RN8. Usuário com
partner_agency_idpreenchido — hoje oAGENCY_USER— só alcança registros da própria imobiliária: inquilinos, imóveis, análises de crédito, garantias, cobranças, documentos e o cadastro da imobiliária. Registro de outra imobiliária → 404, pela mesma razão da RN4. - RN9. O inquilino passa a ter
partner_agency_id, preenchido na criação com a imobiliária de quem o cadastrou. Cadastro feito por usuário da garantidora gravaNULL— é a carteira própria: visível a toda a garantidora e a nenhuma imobiliária. - RN10. O
partner_agency_idda análise de crédito é derivado do imóvel, sempre, independente de quem cria. O imóvel é a fonte da verdade do vínculo — é onde o campo é obrigatório. A gravação acontece num só ponto doCreditAnalysisService, e o campo nunca é aceito do corpo da requisição. - RN11. Garantia e cobrança seguem herdando o vínculo da análise que as originou, como já fazem. Corrigida a origem (RN10), a herança passa a carregar valor.
- RN12. O
partner_agency_idenviado no corpo por umAGENCY_USERé ignorado — vale sempre o vínculo do perfil, como já ocorre emPropertyService. Ele não cadastra em nome de outra imobiliária. - RN13. O documento herda o escopo de quem ele pertence. As quatro FKs do
Documentsão mutuamente exclusivas, então há sempre um dono: se o usuário não pode ver o dono, não vê o documento — nem o metadado na listagem, nem o conteúdo no download, nem a contagem no resumo de armazenamento. - RN14. O usuário da garantidora (
ADMIN_TENANT,ANALYST,OPERATOR,VIEWER) continua vendo todas as imobiliárias parceiras, tudo que elas originaram e também a carteira própria. O isolamento por imobiliária restringe quem é de uma imobiliária, não quem é dono da garantidora.
Barreira de regressão¶
- RN15. A suíte da API passa a conter um teste de matriz que, para cada endpoint autenticado de recurso de negócio, tenta alcançar registro de outro tenant e de outra imobiliária e exige 404. O teste descobre as rotas a partir do
app.routesdo FastAPI, não de uma lista escrita à mão — é o que faz o endpoint novo entrar já coberto. - RN16. O mesmo teste exercita a RN5: para cada endpoint de escrita que aceita FK no corpo, envia um ID de outro tenant e exige 404 e ausência de escrita.
- RN17. Rota que não puder ser exercitada automaticamente entra numa lista de exceção nomeada e justificada dentro do teste. A lista falha o teste se citar rota que não existe mais, para não virar sepultura de dispensas esquecidas.
5. Impacto por app¶
apps/api¶
Modelos/migrations
Renterganhapartner_agency_id: UUID | None, FK parapartner_agencies, indexado e anulável.- Migration Alembic com backfill em três passos, medido no dev-remote antes de ser escrito:
| Passo | Origem | Alcance medido |
|---|---|---|
credit_analyses.partner_agency_id | properties.partner_agency_id da análise | 2769/2769 (100%) |
guarantees.partner_agency_id | análise de origem, já corrigida | 121/121 (100%) |
renters.partner_agency_id | imóvel da análise mais antiga em que participa | 2556/3634 (70%) |
Os 1078 inquilinos que restam NULL não têm análise nenhuma e caem na carteira própria (RN9). São aceitáveis: 3633 dos 3634 estão no tenant Garantidora E2E — massa de teste — e produção não tem nenhum inquilino. O downgrade derruba a coluna de renters e zera os dois campos backfillados, e precisa ser exercitado, não só escrito.
Serviços
| Arquivo | Mudança |
|---|---|
services/credit_analysis_service.py | Resolve property_id e cada renter_id no escopo antes de gravar (RN5); deriva partner_agency_id do imóvel (RN10) |
services/credit_analysis_participant_service.py | Mesma resolução de renter_id ao adicionar participante |
services/renter_service.py | Grava partner_agency_id na criação (RN9/RN12); list e _get_authorized filtram pela agência |
repositories/renter_repo.py | search/list_with_count aceitam partner_agency_id |
services/document_service.py | list, get, get_content, update, delete, storage_summary resolvem o dono e aplicam o escopo (RN13) |
repositories/document_repo.py | Filtro pelo escopo do dono nas listagens |
services/partner_agency_service.py | list e _get_authorized restringem à própria agência quando o usuário tem partner_agency_id |
services/permission_service.py | assign_role_to_user/revoke_role_from_user conferem o tenant do alvo (RN6) |
services/credit_analysis_document_service.py | Conferência por pessoa respeita o escopo da agência |
Endpoints — nenhum novo, nenhum removido. Muda o conjunto de resultados e o código de erro no acesso cruzado. Os description dos decorators afetados passam a declarar o escopo aplicado; a matriz de papéis em main.py idem. A docstring de CreditAnalysisService.create deixa de prometer validação que não existia e passa a descrever a que existe.
Testes — tests/test_tenant_isolation.py (novo, RN15–RN17) mais casos dirigidos em test_renters_search.py, test_documents.py, test_credit_analyses.py, test_rbac_security.py e test_partner_agency_search.py.
apps/web¶
pages/login/index.tsx—useState("")nos dois campos (RN1). Só isso; o layout não muda.pages/renters/— a lista já consome o que a API devolve, então o filtro vem de graça. Muda o formulário: paraAGENCY_USERnão existe seletor de imobiliária (RN12); para usuário da garantidora, o vínculo aparece como campo de leitura noshow, exibindo Carteira própria quandoNULL.pages/properties/index.tsx— o filtro por imobiliária (linha 288) e o carregamento da lista de agências (linha 144) não fazem sentido para quem é de uma imobiliária só, e hoje já falham por falta departner_agencies:list. Passam a ser condicionais ao papel, em vez de erro silencioso.data-testidnovos:renter-agency-field,renter-agency-value,properties-filter-agency(marcando o elemento existente, para poder afirmar ausência).- Sider: sem módulo novo —
App.tsxecomponents/custom-sider/index.tsxnão mudam. - Responsividade & PWA: nenhum layout novo, então não há trabalho responsivo próprio — mas a tela de login e o formulário de inquilino são conferidos nos dois viewports, porque o campo de imobiliária entra e sai conforme o papel. PWA: o bundle muda, então a tela instalada só reflete a mudança após rebuild e reabertura; sem impacto em offline ou em assets.
apps/e2e¶
- Specs:
login-sem-credencial.spec.ts— a tela abre com os dois campos vazios e o botão Entrar com campos vazios não autentica.isolamento-imobiliaria.spec.ts—imob@e2e.test(Imobiliária A) não vê na lista os inquilinos da Imobiliária B; navegar direto para a URL de um inquilino, de um documento e de uma imobiliária da B resulta em não encontrado; e as análises da própria imobiliária aparecem (regressão da RN10, que hoje devolve lista vazia).isolamento-tenant.spec.ts— usuário da Garantidora A abrindo URL de registro da Garantidora B resulta em não encontrado.- Seeds:
tests/fixtures/seeds/ganha uma segunda imobiliária com inquilino, imóvel, análise e documento próprios, e um segundo tenant com um usuário e um registro de cada tipo. O seed atual (app/db/seed_e2e.py) tem uma imobiliária só, o que torna o cruzamento inexprimível hoje.
6. Contrato de API¶
Nenhuma rota nova. As mudanças são de escopo e de código de erro.
Inquilino ganha o vínculo com a imobiliária:
{
"id": "uuid",
"name": "Maria Souza",
"renter_type": "PF",
"cpf": "12345678901",
"partner_agency_id": "uuid-da-imobiliaria | null",
"created_at": "2026-08-10T12:00:00Z"
}
Na criação é aceito de usuário da garantidora e ignorado de AGENCY_USER (RN12).
FK de outro tenant no corpo — o que passa a acontecer (RN5):
POST /api/v1/credit-analyses
{ "property_id": "uuid-de-imovel-de-outra-garantidora", "participants": [...] }
Nenhuma análise é criada, nenhum número de sequência é consumido.
Acesso cruzado — formato uniforme em todos os recursos:
| Situação | HTTP | code |
|---|---|---|
| Registro de outro tenant | 404 | NOT_FOUND |
| Registro de outra imobiliária | 404 | NOT_FOUND |
| FK de outro tenant/imobiliária no corpo | 404 | NOT_FOUND |
| Operação sem permissão no próprio tenant | 403 | FORBIDDEN |
assign_role sobre usuário de outro tenant | 404 | NOT_FOUND |
| Token ausente ou expirado | 401 | NOT_AUTHENTICATED / SESSION_EXPIRED |
7. Critérios de aceite¶
- [x] CA1. Dado um navegador limpo, quando abro
/app/login, então e-mail e senha estão vazios e ovaluede ambos os inputs é string vazia no DOM. - [x] CA2. Dado o login aberto, quando clico em Entrar sem digitar nada, então a autenticação não ocorre e nenhuma requisição a
/api/v1/auth/loginé disparada. - [x] CA3. Dado
imob@e2e.test(Imobiliária A) autenticado, quando abro a lista de inquilinos, então nenhum inquilino da Imobiliária B aparece e o total paginado bate com o número de inquilinos da A. - [x] CA4. Dado o token da Imobiliária A e o UUID de um inquilino da B, quando chamo
GET /api/v1/renters/{id}, então a resposta é 404 — idemPATCHeDELETE. - [x] CA5. Dado o token da Imobiliária A e o UUID de um documento de inquilino da B, quando chamo
GET /api/v1/documents/{id}e.../content, então ambas respondem 404 e nenhum byte do arquivo é devolvido. - [x] CA6. Dado o token da Imobiliária A, quando chamo
GET /api/v1/partner-agencies/{id-da-B}, então 404; com o próprio ID, 200. - [x] CA7. Dado um
ADMIN_TENANTda Garantidora A e o UUID de um usuário da B, quando chamoPOST /api/v1/roles/{role_id}/users/{user_id}, então 404 e nenhum vínculo de perfil é criado no banco. - [x] CA8. Dado um usuário da Garantidora A, quando crio uma análise de crédito com
property_idde um imóvel da Garantidora B, então a resposta é 404, nenhuma análise é gravada e nenhum dado do imóvel da B aparece em qualquer resposta. - [x] CA9. Dado um usuário da Garantidora A, quando crio uma análise com um
renter_idde participante pertencente à Garantidora B, então 404 e nada é gravado. - [x] CA10. Dado um
AGENCY_USERda Imobiliária A, quando abro a lista de análises de crédito, então vejo as análises cujos imóveis são da Imobiliária A — hoje a lista vem vazia — e nenhuma da B. - [x] CA11. Dada uma análise criada por usuário da garantidora sobre imóvel da Imobiliária A, quando consulto o registro, então
partner_agency_idé o da Imobiliária A, e a garantia emitida a partir dela herda o mesmo valor. - [x] CA12. Dado um
ADMIN_TENANTda Garantidora A, quando percorro todos os endpoints de recurso de negócio com IDs da Garantidora B, então todos respondem 404 — verificado pelo teste de matriz, não por amostragem. - [x] CA13. Dado um
OPERATORda garantidora, quando abro a lista de inquilinos, então vejo os de todas as imobiliárias e também os da carteira própria (RN14). - [x] CA14. Dado um
AGENCY_USERcriando inquilino e enviando no corpo opartner_agency_idde outra imobiliária, quando a criação é aceita, então o inquilino gravado tem opartner_agency_iddo perfil do usuário, não o enviado. - [x] CA15. Dada a migration aplicada no dev-remote, quando consulto o banco, então
credit_analyses.partner_agency_idestá preenchido em 2769/2769,guarantees.partner_agency_idem 121/121 erenters.partner_agency_idem 2556, com os 1078 restantes emNULL. - [x] CA16. Dado um endpoint de recurso de negócio adicionado sem escopo de tenant, ou aceitando FK sem resolvê-la, quando rodo
make test, então o teste de matriz falha nomeando a rota. - [x] CA17. Dado
alembic downgrade -1a partir desta migration, quando o comando termina, entãorenters.partner_agency_idnão existe e a API sobe.
8. Plano de implementação¶
Ordem obrigatória no monorepo: api → web → e2e.
- [x]
apps/api— resolução de FK no escopo antes de gravar (RN5), começando porcredit_analysis_serviceecredit_analysis_participant_service - [x]
apps/api—partner_agency_idda análise derivado do imóvel (RN10/RN11) - [x]
apps/api—Renter.partner_agency_id(model → schema → repository → service) - [x]
apps/api— migration Alembic com o backfill de três passos edowngradeexercitado - [x]
apps/api— escopo por imobiliária emrenter_service/renter_repo - [x]
apps/api— escopo herdado do dono emdocument_service/document_repo(RN13) - [x]
apps/api— escopo da própria agência empartner_agency_service - [x]
apps/api— conferência de tenant empermission_service(RN6) - [x]
apps/api— 404 uniforme no acesso cruzado (RN4), revisando os_get_authorized - [x]
apps/api—tests/test_tenant_isolation.py(matriz por introspecção de rotas) - [x]
apps/api— testes dirigidos nos pontos corrigidos - [x]
apps/api— docstrings + Swagger (summary,description,response_description) refletindo o escopo, e a matriz de papéis emmain.py - [x]
apps/web— login sem valor padrão (RN1) - [x]
apps/web— formulário eshowde inquilino com o vínculo de imobiliária, filtro de agência condicional ao papel,data-testid, responsivo (desktop e mobile) - [x]
apps/e2e— seed com segunda imobiliária e segundo tenant - [x]
apps/e2e— os três specs de isolamento e login - [x]
make testemake lintpassando (sem apontamento novo nos arquivos tocados) - [x] Validado no browser em desktop e mobile, reabrindo a tela instalada para furar o cache do PWA
9. Decisões em aberto¶
Nenhuma. As quatro que existiam foram fechadas em 2026-08-10:
- Senha do
admin@garantora.com.brem produção — já rotacionada pelo usuário. A credencial esteve visível na tela de login enquanto o ambiente estava no ar; tirar o pré-preenchimento não invalidaria quem já a tivesse lido, por isso a rotação era necessária e independente do código. - Vínculo quebrado das análises — corrigido nesta spec, derivando do imóvel (RN10). A alternativa de extinguir o campo e resolver por join ficou registrada em Fora de escopo com o motivo.
- Carteira própria invisível ao
AGENCY_USER— confirmado (RN9/RN14). Inquilino sem vínculo é da garantidora e não aparece para nenhuma imobiliária parceira. - Alcance do backfill — medido no dev-remote antes de escrever a migration, e os números estão na tabela da seção 5. Os 1078 inquilinos que ficam
NULLsão massa de teste; produção não tem inquilino nenhum.
10. Registro¶
O que apareceu durante a implementação e não estava na spec¶
Mais três furos, achados pela própria matriz de regressão. A RN15 nasceu como barreira contra o futuro e serviu primeiro contra o presente:
GET /users/{id},DELETE /users/{id}eGET /tenants/{id}respondiam 403 no acesso cruzado, não 404 — violação da RN4 que ninguém tinha notado. Corrigido emuser_service._get_authorizedetenant_service._get_authorized.PropertyService.createaceitavapartner_agency_idde outro tenant sem validação, mesma classe da RN5 que a spec descreveu só para a análise de crédito. Entrou no escopo por ser idêntico em natureza e trivial em custo.- O upload de documento (
_validate_entity) conferia só o tenant, então um usuário de imobiliária anexava arquivo ao locatário da concorrente — e, anexando, ganhava um vínculo legítimo com aquele registro. Coberto por teste próprio.
Um erro meu, achado por sabotagem deliberada. Depois de a suíte ficar verde, quebrei de propósito o within_agency do RenterService dentro do container para conferir que a matriz reagia. Ela não reagiu — e a razão era a fixture: cada imobiliária estava sendo criada com o próprio tenant, então o filtro de tenant fazia todo o trabalho e o recorte por imobiliária nunca era exercitado. O cenário foi reescrito para a1 e a2 compartilharem a garantidora; repetida a sabotagem, os testes certos falharam. Sem esse passo, a spec teria entregue uma barreira decorativa.
Também havia um falso positivo no teste de documento: eu usava /documents/{id}/content, rota que não existe — o 404 vinha do roteador, não do escopo. O caminho certo é /download, e o teste agora afirma também que o próprio documento baixa, para distinguir "bloqueado" de "quebrado".
Divergências em relação ao planejado¶
- Módulo novo
app/services/scoping.py, não previsto. A RN5 se repetia em sete serviços, e a alternativa era espalhar a mesma consulta por todos. Não é a "dependência única obrigatória" que a spec pôs fora de escopo — é um punhado de funções puras que cada serviço chama, sem mudar a assinatura de ninguém. partner_agency_identrou no/auth/me. Não estava previsto, mas o web precisa saber se o usuário é de imobiliária para omitir seletores, e derivar isso do nome do papel seria frágil (um papel customizado pode ser escopado sem se chamarAGENCY_USER). É dado do próprio usuário, não há leitura de terceiro.- A
PartnerAgencyexigiu um caso especial emvisible_to: ela não aponta para uma imobiliária, ela é uma. Sem tratar isso, o usuário de imobiliária deixava de conseguir cadastrar imóvel — a resolução da FK não achava a própria agência. - Edição do vínculo do locatário ficou de fora. O campo aparece desabilitado no formulário de edição: mover locatário entre imobiliárias é decisão de negócio que a spec não tratou, e improvisá-la aqui seria inventar regra.
Aferições¶
make test: 279 passaram, 0 falharam. Eram 262 antes da spec; os 17 novos são a suíte de isolamento. Rodada pordocker exec garantora-devremote-api-1 uv run pytest, já que não háuvno host.- Intermitentes: a
CLAUDE.mdda raiz documentatest_partner_agency_search.py::test_busca_alcanca_quem_esta_fora_da_primeira_paginacomo instável. Na prática são dois — o irmãotest_partner_agency_status_filter.py::test_alcanca_quem_esta_fora_da_primeira_paginatem o mesmo cenário e a mesma causa (empate decreated_atem SQLite, desempate por UUID aleatório), e falhou 1 de 3 execuções isoladas. Não tem relação com esta spec. ruff: os arquivos novos (scoping.py,document_repo.py,test_tenant_isolation.py) passam limpos. OsE501restantes nos serviços tocados são linhas de mensagem preexistentes, não introduzidas aqui.tscdo web: limpo. Atenção para uma armadilha do ferramental: rodarpnpmemapps/webfaz o pnpm escrever umpnpm-workspace.yamlcomallowBuildse sem o campopackages, e esse arquivo quebra opnpm builddentro doweb.Dockerfile(ERROR packages field missing or empty). Aconteceu aqui e derrubou o primeiro rebuild. Use./node_modules/.bin/tsc --noEmite apague o arquivo se ele aparecer como não-rastreado.- Migration: aplicada no dev-remote com os números exatos da CA15 (2769/2769, 121/121, 2556 + 1078). O
downgradefoi exercitado: derrubou a coluna e zerou os dois campos; oupgradeseguinte reproduziu estado idêntico.
Pendências¶
- A migration ainda não foi aplicada em produção (
srv01). É passo do/ship, que é pedido do usuário. - e2e escrito, não executado. Nada mais em aberto no código.
Validação no browser (dev-remote, após rebuild e recriação dos containers)¶
Script de captura nos dois viewports contra https://dev.garantora.com.br/app:
| Tela | Desktop (1440) | Mobile (375) |
|---|---|---|
| Login — campos vazios, sem menção à conta de admin | ✅ | ✅ |
| Formulário de inquilino — campo de imobiliária para o gestor | ✅ | ✅ |
| Detalhe de inquilino — vínculo exibido | ✅ | ✅ |
Sem rolagem horizontal (scrollWidth - clientWidth == 0) e sem erro de console em nenhuma delas.
Dois defeitos apareceram só aqui, e nenhum dos dois o tsc pegaria:
- A rota de criação é
/renters/**create**, não/renters/new— eu havia escritonewno script e em dois pontos do spec e2e. Ogotocaía em tela vazia e o "campo ausente" que o script reportou era, na verdade, a página errada. - O detalhe do inquilino tem dois branches de layout, e eu só havia tocado no de desktop. No mobile, montado por
MobileDetailSection, o vínculo não aparecia. A linha precisa de?? "Carteira própria"também porque o.filter((r) => r.value)de lá descartaria a linha sem valor — sumindo com a informação exatamente no caso em que ela é "não há vínculo".