0049 — Remoção lógica e recuperação: imobiliária, inquilino, imóvel, análise de crédito, garantia e cobrança¶
| Status | CONCLUÍDA |
| Branch | spec/0049-remocao-logica-de-registros |
| Criada em | 2026-09-01 |
| Apps afetados | api · web · e2e |
| Revisão do problema | ✅ aprovada (rodada 5) |
| Revisão da experiência | ⚠️ interrompida por decisão do usuário (rodada 10, correções não revalidadas) |
| Revisão do detalhamento | ✅ aprovada (rodada 5) |
| Revisão da implementação | — |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDA
A spec tem três partes, escritas em ordem, e quatro portões — cada um revisado por um subagente de contexto limpo, em até 3 rodadas. A máquina de revisão é do plugin superpowers; a régua é a rubrica correspondente em .claude/rubrics/. Achados vêm classificados: qualquer Critical reprova, Important em aberto reprova salvo decisão registrada do usuário, e Minor vira pendência aceita.
- A Parte I diz qual é o problema e como se convive com ele hoje. Revisada na seção 6 (necessidade × jornada de hoje). É o portão mais barato de todos, e o único que pergunta contra que problema isto está sendo construído.
- A Parte II diz como se usa o que vai existir. Revisada na seção 8 (jornada × viewports).
- A Parte III diz como se constrói. Revisada na seção 16 (segurança × clareza técnica) — autorização, isolamento, e se sobrou decisão para quem vai codar.
- A implementação é medida contra as três, na seção 17 (aderência × qualidade × testes) — uma revisão por fase, e uma da branch inteira antes de fechar.
Cada parte só se escreve depois que a anterior passou. Descrever tela sobre problema mal enquadrado, ou detalhar endpoint sobre experiência vaga, é o jeito mais caro de descobrir que era outra coisa.
Parte I — O problema¶
Aqui não entra solução: nem tela, nem endpoint, nem botão. Só o que dói, para quem, e o que se faz hoje. Se você já sabe qual é a tela, guarde para a Parte II.
1. Dor¶
Hoje o sistema trata "cadastro errado" de duas formas, e nenhuma das duas resolve.
Para três das seis entidades — imobiliária, inquilino, imóvel — já existe exclusão, mas ela é irreversível de propósito: o endpoint de imobiliária diz, no próprio texto, "a operação é irreversível via API"; os de inquilino e imóvel não repetem a frase, mas o efeito é o mesmo — nenhum dos três tem qualquer endpoint de restauração. Quando a exclusão foi um engano — apagou o registro certo por querer apagar outro, ou apagou sem perceber que ainda havia vínculo relevante —, não há como voltar atrás. A única saída é recriar o cadastro do zero, o que perde o histórico (documentos anexados, participações em análises anteriores, o vínculo com a imobiliária que originou) e não religa nada que apontava para o registro antigo.
Para as outras três — análise de crédito, garantia, cobrança — não existe exclusão nenhuma hoje, e a ferramenta disponível não é a mesma nas três. Análise de crédito tem CANCELLED e garantia tem TERMINATED — os dois resolvem o lado comercial (sinalizam que aquilo não vai seguir), mas não tiram o registro da lista: ele continua listado entre os ativos, e quem opera a tela precisa aprender a reconhecer e ignorar aquela linha específica. Cobrança não tem nem isso: o único caminho para BillingStatus.CANCELLED hoje é efeito colateral automático de encerrar a garantia, e só alcança cobrança futura ainda não vencida — uma cobrança lançada manualmente errada nunca chega a CANCELLED por nenhum caminho do sistema. Quando o estado de negócio não basta — porque o cadastro nunca devia ter existido, e não porque o negócio foi cancelado —, ou quando nem existe estado de negócio que sirva (o caso da cobrança), a saída relatada hoje é pedir para alguém com acesso direto ao banco de dados apagar a linha manualmente, fora de qualquer tela: sem RBAC, sem registro de quem fez, e sem as verificações de dependência que o sistema já aplica nas outras exclusões (por exemplo, sem saber se aquela cobrança já foi referenciada em algum relatório fechado antes de sumir).
O custo de conviver com isso: listas que acumulam lixo permanente (as três sem exclusão), uma exclusão que é uma aposta sem seguro (as três com exclusão irreversível), e para as análises, garantias e cobranças, a única saída de emergência de um cadastro indevido é uma intervenção manual no banco — arriscada, sem controle e sem trilha.
2. Quem sente¶
O RBAC de hoje já trata as seis entidades de forma desigual, e a dor segue essa mesma desigualdade:
- Imobiliária:
ADMIN_TENANTcria e exclui (partner_agencies:create/delete) —OPERATORsó lista e lê.ADMIN_GLOBALtambém tempartner_agencies:delete(o próprio endpoint documenta: "SomenteADMIN_TENANTeADMIN_GLOBALpodem remover imobiliárias"), mas ele é o administrador da plataforma, não do tenant — na prática éADMIN_TENANTquem sente a irreversibilidade de uma exclusão errada aqui. - Inquilino:
ADMIN_TENANT,OPERATORe a imobiliária (AGENCY_USER) criam, atualizam e excluem — os três sentem hoje a mesma irreversibilidade quando erram. - Imóvel:
ADMIN_TENANTeOPERATORcriam, atualizam e excluem; a imobiliária cria e atualiza, mas não exclui — decisão deliberada, para não deixar a imobiliária apagar um imóvel que afeta análise de outra imobiliária no mesmo tenant. A dor da irreversibilidade hoje é só deADMIN_TENANTeOPERATOR. - Análise de crédito:
ADMIN_TENANT,OPERATORe a imobiliária criam.ADMIN_TENANTtemcredit_analyses:cancel— e, como não há guarda de código bloqueandoADMIN_GLOBALaqui (ao contrário da emissão de garantia), ele também aciona esse cancelamento em qualquer tenant.OPERATORpode criar o erro e não tem como cancelar sozinho — depende de pedir a umADMIN_TENANT. A imobiliária tem uma saída própria, mais estreita:credit_analyses:withdraw(spec 0037) deixa que ela mesma desista de uma análise que ainda esteja em rascunho, submetida ou pendente de documento, sem precisar acionar ninguém — mas, comoCANCELLED, isso também não tira o registro da lista de ativos.ANALYSTnão cria, mas temcredit_analyses:updatedesde a spec 0036 — corrige um dado errado da análise (ex.: um dígito) sem devolver o trabalho, e tambémrenters:update_sensitive, para corrigir CPF/RG do inquilino. Sente a dor como ruído na lista, e às vezes como algo que ele mesmo pode ajustar direto, sem ferramenta de remoção nenhuma. - Garantia:
ADMIN_TENANT,OPERATOReANALYSTcriam;ADMIN_TENANTtem a permissão que sustenta o encerramento (guarantees:update) — e, pela mesma ausência de guarda,ADMIN_GLOBALtambém alcança e aciona o encerramento em qualquer tenant hoje. A imobiliária só lê a própria carteira — não cria nem encerra. - Cobrança: a maior parte nasce automática, gerada a partir dos dados da garantia (spec 0048) — não é uma pessoa "criando" e podendo digitar errado. O lançamento manual, quando existe, exige
billings:update, que sóADMIN_TENANTtem — é também só ele quem marca uma cobrança como paga ou vencida.OPERATOR,ANALYSTe a imobiliária só leem.
Em resumo: ADMIN_TENANT é quem tem alguma ferramenta de "tirar de circulação" em todas as seis entidades hoje — mesmo que imperfeita (irreversível em três, sem efeito na lista nas outras duas, e inexistente para uma cobrança lançada manualmente errada). ADMIN_GLOBAL soma a ele nas mesmas três — imobiliária, análise, garantia —, mas é o administrador da plataforma, não de um tenant específico, e na prática não é quem convive com esse cadastro no dia a dia. OPERATOR cria em quatro das seis (inquilino, imóvel, análise, garantia): tem a ferramenta corretiva — a exclusão — só nas duas primeiras; em análise e garantia ele pode cometer o erro, mas depende de ADMIN_TENANT (ou ADMIN_GLOBAL) para corrigir. Em imobiliária e cobrança ele só lê — não cria, não sente o erro como próprio. ANALYST cria garantia (spec 0039) e pode errar ali, sem nenhuma ferramenta corretiva disponível; em imóvel, imobiliária e cobrança só lê; em análise de crédito e no CPF/RG do inquilino, tem permissão de corrigir o dado direto (credit_analyses:update, renters:update_sensitive), mas nenhuma das duas é uma ferramenta de remoção. VIEWER não cria em entidade nenhuma. A imobiliária (AGENCY_USER) cria inquilino, imóvel e análise, mas só tem a ferramenta corretiva plena (exclusão) em inquilino; em imóvel ela pode errar e não tem como corrigir sozinha; em análise ela tem uma saída própria e mais estreita (credit_analyses:withdraw, só em rascunho/submetida/pendente de documento) que, como o cancelamento de ADMIN_TENANT, também não some da lista. Ela continua sem qualquer ação de remoção nas outras entidades, com ou sem esta spec (ver seção 5).
Não é um caminho obrigatório do fluxo — a maioria dos cadastros nasce certa. É uma exceção que, quando acontece, hoje tem exclusão irreversível para inquilino/imóvel/imobiliária, e nenhuma saída que tire o registro da lista para análise, garantia e cobrança. Não há operação real rodando ainda (produção sem clientes, só o ambiente) — não há volume a medir; o que sustenta o alcance é o desenho: os papéis e permissões que já existem hoje, entidade por entidade.
3. A jornada de hoje¶
Imobiliária, inquilino ou imóvel cadastrado errado. Quem tem a permissão de excluir cada um (ADMIN_TENANT só para imobiliária; ADMIN_TENANT e OPERATOR para imóvel; os dois mais AGENCY_USER para inquilino) abre o menu de ações da linha na listagem, clica em "Excluir", confirma no modal — e a linha some da lista para sempre. Se depois descobrirem que a exclusão foi um engano (era o registro certo, ou havia um vínculo que não apareceu na checagem), não existe botão de desfazer em lugar nenhum do sistema. A única saída é recriar o cadastro do zero — perdendo o histórico que apontava para o id antigo.
Análise de crédito ou garantia cadastrada errada. Não existe botão de exclusão em nenhuma das duas telas. OPERATOR pode ser quem cria o registro errado, mas não tem como corrigi-lo sozinho: cancelar uma análise (CANCELLED) exige credit_analyses:cancel e encerrar uma garantia (TERMINATED) exige guarantees:update — as duas de ADMIN_TENANT (e, sem guarda de código bloqueando, também de ADMIN_GLOBAL, ainda que ele não seja quem convive com esse cadastro no dia a dia). A imobiliária, quando é ela quem cria a análise errada, tem uma saída própria e mais estreita — credit_analyses:withdraw desiste da análise sem precisar acionar ADMIN_TENANT, mas só enquanto ela está em rascunho, submetida ou pendente de documento. Em qualquer um dos casos, a ferramenta usada resolve o lado do negócio mas deixa o registro na lista de ativos, misturado com o que é real.
Cobrança lançada errada. Nem exclusão nem estado de negócio ajudam aqui: uma cobrança lançada manualmente errada não tem caminho nenhum, dentro do produto, para deixar de aparecer como ativa — CANCELLED só acontece como efeito colateral automático de encerrar a garantia, e apenas para cobrança futura ainda não vencida.
Quando nada disso é suficiente — porque o cadastro nunca devia ter existido, e não porque o negócio foi cancelado, ou porque a entidade (cobrança) nem tem essa saída —, o relato é pedir a alguém com acesso ao banco que apague a linha manualmente, fora do sistema: sem tela, sem RBAC, sem trilha de quem fez, e sem as verificações de dependência que o resto do sistema já aplica em outras exclusões (por exemplo, não há garantia de que aquela cobrança não estava referenciada em algo que dependia dela).
Onde quebra: no momento em que corrigir o erro depende de tirar o registro da lista sem apagar o histórico. Isso hoje só existe, com irreversibilidade, para três das seis entidades; para as outras três, não existe de forma nenhuma dentro do produto.
O que já se tentou: usar CANCELLED/TERMINATED como um "apagar de fato" para análise e garantia — não resolve, porque a linha continua na lista dos ativos, e nem chega a existir para cobrança. Quando a exclusão das três antigas foi um engano, recriar o cadastro do zero — que perde vínculo e histórico em vez de restaurar o que existia. E, no próprio código, uma tentativa que ficou pela metade: os modelos de análise de crédito e garantia já carregam a mesma infraestrutura de remoção lógica usada por imobiliária/inquilino/imóvel — a coluna deleted_at e a propriedade is_deleted derivada dela —, mas nenhum service chega a gravar esse campo para as duas: a coluna existe no banco e nunca foi ligada a endpoint nenhum.
4. Resultado esperado¶
Passa a ser possível remover um cadastro incorreto das seis entidades sem perder o histórico, e desfazer essa remoção pela própria tela se foi um engano, dentro de uma janela de tempo. Sabe-se que resolveu quando:
- a lista deixa de mostrar o registro removido por padrão, sem que quem opera precise aprender a reconhecer e ignorar uma linha específica;
- quem removeu por engano consegue trazer o registro de volta pela tela, sem pedir intervenção direta no banco de dados;
- ninguém mais precisa pedir exclusão manual no banco para análise de crédito, garantia ou cobrança — o caminho que hoje é "fora do sistema" passa a ser "dentro do sistema", com RBAC e registro de quem removeu e de quem restaurou.
5. Fora de escopo¶
- Exclusão física. Não existe, e não vai existir por esta spec — a remoção é sempre lógica, em todas as seis entidades.
- Restaurar depois do prazo. A ação de remoção tem uma janela de 60 dias para ser desfeita pela tela — número decidido com o usuário na entrevista desta spec, não presunção de quem escreve; passado esse prazo, o botão de restaurar deixa de estar disponível, mesmo a linha continuando gravada no banco.
- Remoção em massa. Selecionar várias linhas de uma vez para remover ou restaurar juntas fica fora — cada remoção e cada restauração continua sendo uma ação sobre um registro por vez, como as telas que já excluem hoje fazem.
- A imobiliária (
AGENCY_USER) remover ou restaurar análise de crédito, garantia ou cobrança. Essa ação fica só com a garantidora, mesmo quando é a própria imobiliária quem originou o registro. A situação da imobiliária para essas três entidades não muda em relação a hoje — ela continua sem exclusão nelas. - Motivo obrigatório ao remover. A ação é confirmar num modal, sem campo de texto explicando o porquê — mesmo padrão já usado hoje nas exclusões existentes.
- Mudar o comportamento dos estados de negócio já existentes.
CANCELLEDde análise de crédito e cobrança, eTERMINATEDde garantia, continuam existindo exatamente como hoje. A remoção lógica é uma camada adicional e independente desses status — não os substitui, não impede remoção fora deles, e não é acionada por eles. - Entidades fora da lista das seis.
Document,Decision,CreditAnalysisParticipant,CreditAnalysisStatusHistory,CreditAnalysisChangeLogeUsernão entram nesta spec, mesmo referenciando ou sendo referenciadas por alguma das seis — nem ganham remoção própria, nem têm seu comportamento atual alterado.
6. Revisão do problema¶
Preenchido pelo laço do /spec new, rubrica .claude/rubrics/problema.md. Registrar todas as rodadas, inclusive as reprovadas.
| Rodada | Critical | Important | Minor | Veredicto |
|---|---|---|---|---|
| 1 | 2 | 1 | 0 | reprovada |
| 2 | 2 | 1 | 0 | reprovada |
| 3 | 0 | 1 | 2 | reprovada |
| 4 (extra, pedida pelo usuário após estourar o limite de 3) | 1 | 1 | 0 | reprovada |
| 5 (extra, pedida pelo usuário) | 1 | 0 | 0 | reprovada |
Resultado: aprovada na rodada 5, depois de correção. A rodada 5 auditou a seção 2 inteira contra o RBAC real e a tabela de Afirmações conferidas e não achou nenhum papel trocado — as quatro rodadas anteriores estavam preocupadas com essa classe de defeito, e ela está resolvida. O único achado foi de outra natureza: a linha sobre cobrança lançada errada dizia ser "o caso mais comum de duplicidade", uma afirmação de frequência sem lastro que contradizia a própria spec (seção 2 já diz "não há operação real rodando... não há volume a medir"). Corrigido removendo a hierarquização não sustentada — o argumento não dependia dela.
Pendências aceitas: nenhuma. porquê. Vazio se não houver.
Pendências aceitas: o que a revisão ainda apontou e se decidiu não resolver, com o porquê. Vazio se não houver.
Parte II — A experiência¶
Não preencher antes da revisão da seção 6 estar aprovada.
7. Jornada pretendida¶
Ponto de entrada: a mesma superfície onde já se exclui hoje — nada disso ganha tela nova. Nas listagens (imobiliária, inquilino, imóvel, análise de crédito, garantia, cobrança), o ícone de remover entra na coluna de ações da linha, no mesmo lugar e com o mesmo desenho do ícone trash já usado nas três telas que excluem hoje (imobiliária, inquilino, imóvel). Nas telas de detalhe com área de ações (PageHeader) — análise de crédito e garantia —, "Remover" entra no mesmo slot de ação destrutiva que hoje hospeda "Desistir"/"Cancelar" (análise) ou "Encerrar" (garantia).
Caso feliz — remover:
- Quem tem a permissão vê o ícone de remover na linha da lista, ou o botão "Remover" na área de ações do detalhe.
- Clica; abre um modal de confirmação. Qual desenho depende da tela, porque as seis já não usam um diálogo único hoje: imobiliária, inquilino e imóvel excluem hoje com um
Dialogpróprio por tela (selo com o íconetrash, dois botões full-width) — "Remover" nessas três reusa o mesmo desenho visual, com o texto atualizado: sai "esta ação não pode ser desfeita" (que hoje descreve uma exclusão de verdade irreversível) e entra a explicação de que o registro pode ser restaurado em até 60 dias — o componente é reaproveitado, o texto não, porque o comportamento que ele descreve mudou. Análise de crédito e garantia já usam oConfirmDialogcompartilhado para as próprias ações destrutivas (Cancelar, Desistir, Encerrar) — "Remover" nessas duas (e em cobrança, que também já usaConfirmDialogpara "marcar como inadimplente") entra como mais uma ação nesse mesmo componente, com o mesmo texto sobre os 60 dias, para não conviver na mesma tela com dois estilos de diálogo diferentes. Em ambos os casos o texto explica que o registro deixa de aparecer nas listas mas pode ser restaurado em até 60 dias. - Confirma; a chamada é feita. Sucesso: se veio da lista, o modal fecha e a linha some, exatamente como acontece hoje nas três exclusões existentes. Se veio da tela de detalhe (análise de crédito, garantia), confirmar não navega para outro lugar — "Remover" vira "Restaurar" no mesmo lugar onde estava (o slot do cabeçalho, ou o painel condicional abaixo dele, conforme 7.1), e o corpo da tela continua mostrando os dados do registro, agora removido, do mesmo jeito que "Cancelar"/"Desistir"/"Encerrar" hoje atualizam o status sem tirar ninguém da página. Chegando direto na tela de detalhe de um registro já removido (ex.: pelo link de uma linha listada com "Mostrar removidos" ativo), "Restaurar" já aparece no lugar de "Remover" — no cabeçalho quando o slot destrutivo estiver livre, no painel condicional quando não estiver, seguindo a mesma regra de 7.1.
- Se a regra de ordem de remoção bloquear (existe um dependente ainda não removido — ver Parte I, decisão registrada com o usuário), o modal continua aberto e mostra um
Alertde erro dentro do próprio diálogo, nomeando a dependência que bloqueia — por exemplo, "Não é possível remover esta garantia: existem cobranças vinculadas a ela que ainda não foram removidas." — o mesmo padrão já usado hoje para o vínculo ativo que bloqueia excluir um inquilino ou uma imobiliária.
Caso feliz — restaurar:
- Quem tem a permissão de remover/restaurar aquela entidade ativa o filtro "Mostrar removidos" na lista — ele só aparece para quem tem essa permissão, mais ninguém o vê.
- A lista passa a incluir os registros removidos, cada um com um segundo selo "Removido" ao lado do selo que a linha já mostra hoje — soma, não substitui. Em imobiliária, análise, garantia e cobrança esse selo é o status (ex.: uma garantia
ACTIVE), que continua visível porque ajuda a decidir se vale restaurar; em inquilino e imóvel, que não têm status, é o tipo (PF/PJ, Residencial/Comercial) — nesse caso o segundo selo soma do mesmo jeito, só que não carrega a mesma informação de estado no momento da remoção, porque essas duas telas não têm o que mostrar ali. - Na linha de um registro removido, o ícone de remover é substituído pelo de restaurar.
- Clica; modal de confirmação "Restaurar {entidade}", mesmo desenho, sem campo de motivo. Confirma; o registro volta a aparecer na lista padrão, sem precisar do filtro ativo.
- Passados 60 dias da remoção, o ícone de restaurar deixa de estar clicável — não é um erro que o usuário aciona e recebe de volta, é uma affordance que já nasce desabilitada, com um texto explicando que o prazo passou.
Custo de uso: nenhum dado novo a digitar em nenhuma das duas ações — são cliques com confirmação, sem texto livre. O sistema já sabe quem está logado e quando cada remoção aconteceu (para contar os 60 dias); nada disso é perguntado a quem opera.
7.1 Desktop¶
Na tabela, quando "Mostrar removidos" está ativo, cada linha de um registro removido ganha o segundo selo "Removido" e a coluna de ações troca o ícone de remover pelo de restaurar; passados os 60 dias, o ícone de restaurar aparece desabilitado, com tooltip explicando o motivo. O filtro em si não é um controle novo: não existe hoje, em components/shared/, nenhum toggle liga/desliga — o que existe é o seletor de valor único (FilterSelect no desktop, FilterPill no mobile) já usado pelo filtro de status. "Mostrar removidos" reusa esse mesmo widget, com duas opções — "Ativos" (padrão) e "Removidos" — em vez de um Switch que a base não tem. Ele entra na mesma barra de filtros (FilterBar) das seis listas, ao lado do filtro de status quando a tela já tiver um, condicionado à permissão do usuário como qualquer outro elemento condicional das telas hoje.
Nas telas de detalhe, "Remover" mora num lugar diferente em cada uma, porque as duas telas já são estruturadas de um jeito diferente hoje:
- Análise de crédito já tem um mecanismo de excedente — quando o topo atinge o teto de três ações, o resto vai para o painel abaixo (
DecisionPanel). O critério que decide onde "Remover" aparece é sempre o mesmo, em qualquer estado (decisão do usuário: cada ação destrutiva no lugar que já está livre para ela): se o cabeçalho tem um dos três slots livre, "Remover" ocupa esse slot; se os três já estão ocupados por outras ações, "Remover" vai para o painel, isolado — nunca depende de "existe Cancelar em algum lugar da tela". Na prática, paraADMIN_TENANT— o único papel com a permissão de remover análise (seção 10.1) —, os três slots do cabeçalho já ficam cheios emPENDINGantes de "Cancelar" entrar em cena: "Resolver pendência", "Aprovar" e "Reprovar" preenchem os três (Cancelarnem disputa esse mecanismo — vive isolado no painel por regra própria do estado, RN11 da spec 0036). Então, para quem pode remover, "Remover" cai no painel também emPENDING, do mesmo jeito que cai emDRAFT/SUBMITTED(onde o slot já está ocupado por "Desistir"/ "Cancelar"). Quando "Remover" cai no painel, não entra na fila genérica de ações excedentes que já convive ali com "Aprovar"/"Reprovar" — segue o padrão isolado que o painel já usa hoje para "Cancelar" emPENDING: um botão próprio, separado por uma borda, no rodapé do painel, ao lado de "Cancelar" quando os dois coexistirem. - Garantia não tem painel abaixo hoje — só a análise de crédito tem — e o "menu" que o
PageHeadercita para uma quarta ação é comentário de intenção da spec 0020, nunca construído em nenhuma tela do app; inventar o primeiro menu de overflow do produto dentro desta spec não é o caminho barato. A garantia ganha, em vez disso, um painel abaixo do cabeçalho — mas não oDecisionPanelda análise por dentro: aquele painel existe porque a análise já tem parecer e histórico de decisão para mostrar, conteúdo que a garantia não tem equivalente. O painel da garantia é mais simples e condicional: só existe quando os três slots do cabeçalho já estão ocupados sem "Remover" caber entre eles; quando existe, não tem título nem seção — é só o espaço que hospeda o botão da ação que não coube no cabeçalho, no mesmo estilo contornado das ações do topo. A garantia não tem uma variável de "ação destrutiva" separada disputando um slot fixo, como a análise — o terceiro slot é sempre "Ver análise de crédito" (fixo, incondicional), e o primeiro e o segundo são ocupados por "Registrar"/"Corrigir contrato assinado" e "Encerrar" quando disponíveis. "Remover" entra na mesma fila: ocupa o slot que sobrar, se algum sobrar; quando os três já estão preenchidos — o que hoje acontece exatamente quando "Encerrar" está disponível, porque aí "Registrar/Corrigir contrato", "Encerrar" e "Ver análise" (fixa) já fecham os três lugares —, "Remover" vai para o painel condicional — sem depender de um menu que não existe em lugar nenhum do código hoje.
Cobrança tem duas superfícies de lista hoje — a agregada (Cobranças no menu) e a embutida dentro do detalhe da garantia — e remover/restaurar aparece nas duas, com o mesmo ícone e o mesmo modal, replicando o padrão que a tela já segue hoje para "marcar como paga"/"marcar como vencida" (também duplicado nas duas superfícies).
7.2 Mobile (375px)¶
O ícone de remover/restaurar entra no bloco de ações do cartão, ao lado de "Ver"/"Editar" — sem crescer o cartão, porque já é uma linha de IconButtons pequenos, não um alvo de toque de 44px (esse tamanho é das ações do topo da página, não das ações por linha). As seis telas usam dois cartões diferentes hoje — imobiliária usa MobileListCard (ações numa faixa no rodapé do cartão), inquilino/imóvel/análise/garantia/cobrança usam RecordCard (ações ao lado do valor) —, e "Remover"/"Restaurar" entra em cada um na área de ações que ele já tem, sem mudar a estrutura de nenhum dos dois. O filtro "Mostrar removidos" vira uma pílula dentro da barra de filtros mobile, ao lado da pílula de status quando a tela já tiver uma — rola na horizontal com as demais, sem quebrar linha, do mesmo jeito que o filtro de status de imobiliária já se comporta hoje em 375px. O selo "Removido" no cartão usa o mesmo componente de selo que a tela já usa para status, somado ao selo existente. O modal de confirmação segue a mesma divisão descrita no desktop (diálogo próprio para imobiliária/inquilino/imóvel, ConfirmDialog para análise/garantia/cobrança) — não muda de desenho entre os dois viewports.
Em 375px, a mesma divisão do desktop vale: na análise de crédito, "Remover" segue o mesmo critério de slot-livre-ou-painel descrito em 7.1, que já existe no mobile igual ao desktop; na garantia, "Remover" segue a mesma fila de slots e o mesmo painel condicional descritos no desktop — o painel novo da garantia se comporta no mobile como o DecisionPanel da análise já se comporta ali, sem mecanismo diferente por viewport.
7.3 Estados¶
- Vazio: lista com "Mostrar removidos" ativo mas sem nenhum registro removido — mesma mensagem de lista vazia já usada nas seis telas, adaptada para "Nenhum registro removido".
- Carregando: o indicador que cada tela já usa hoje, sem trocar por um padrão único novo — e em duas das seis ele muda por viewport. No desktop:
DataGrid.loadingnativo em análise de crédito, inquilino e imóvel (as três já usamDataGrid);Skeletonem garantia e na lista de cobrança agregada; texto "Carregando…" em imobiliária e na lista de cobrança embutida na garantia. No mobile, imobiliária, inquilino e imóvel convergem para o mesmo texto "Carregando…" do cartão (RecordCardList/MobileListCard), independente do que usam no desktop. - Erro ao remover (dependência ainda não removida bloqueando):
Alertdentro do próprio modal, nomeando a dependência — o diálogo não fecha, mesmo padrão do vínculo ativo hoje. - Restaurar não tem estado de erro equivalente — a regra de ordem que bloqueia remover não se aplica ao restaurar (Parte I não registrou bloqueio nenhum nesse sentido): dá para restaurar um registro mesmo que o pai dele continue removido — por exemplo, restaurar uma cobrança cuja garantia ainda está removida. Nesse caso a cobrança volta a aparecer nas listas normais, solta do pai que segue removido, até alguém restaurar o pai também.
- Restaurar fora do prazo: não é um estado de erro acionável — nasce desabilitado depois de 60 dias, mas o mecanismo de aviso muda com o tamanho da ação. Na linha da lista (ação pequena), o ícone usa
Tooltipexplicando o motivo — o mesmo padrão dos demais ícones de linha das seis listas. No cabeçalho ou no painel condicional de análise/ garantia (ação de topo), "Restaurar" desabilitado usa a faixa de status acima do botão, o mesmo padrão que a garantia já usa hoje para uma ação de topo indisponível — nãoTooltip, que ali não é o mecanismo em uso. - Sem permissão: o ícone de remover/restaurar e o filtro "Mostrar removidos" simplesmente não aparecem — mesmo padrão condicional já usado hoje para o ícone de excluir. Não há 403 visível, porque a ação nunca chega a ser oferecida a quem não pode usá-la.
8. Revisão da experiência¶
Preenchido pelo laço do /spec new, rubrica .claude/rubrics/experiencia.md. Registrar todas as rodadas, inclusive as reprovadas.
| Rodada | Critical | Important | Minor | Veredicto |
|---|---|---|---|---|
| 1 | 0 | 3 | 3 | reprovada |
| 2 | 0 | 2 | 0 | reprovada |
| 3 | 0 | 2 | 1 | reprovada |
| 4 (extra, pedida pelo usuário) | 1 | 1 | 0 | reprovada |
| 5 (extra, pedida pelo usuário) | 1 | 0 | 1 | reprovada |
| 6 (extra, pedida pelo usuário) | 0 | 1 | 1 | reprovada |
| 7 (extra, pedida pelo usuário) | 0 | 2 | 0 | reprovada |
| 8 (extra, pedida pelo usuário) | 0 | 1 | 0 | reprovada |
| 9 (extra, pedida pelo usuário) | 0 | 1 | 0 | reprovada |
| 10 (extra, pedida pelo usuário) | 1 | 0 | 1 | reprovada |
Resultado: revisão interrompida por decisão do usuário após a rodada 10. A correção está aplicada e o spec-lint está verde, mas não houve uma 11ª rodada de revisor confirmando-a. A rodada 10 achou que a premissa sobre a qual a decisão do usuário (rodada 9) foi tomada estava errada: para ADMIN_TENANT — o único papel que remove análise —, em PENDING o cabeçalho não tem slot livre — "Resolver pendência", "Aprovar" e "Reprovar" já preenchem os três antes de "Cancelar" entrar em cena (que nem disputa esse mecanismo — vive isolado no painel por regra própria do estado). [Critical], porque a decisão do usuário foi aplicada em cima de um fato que não é verdade, e o efeito prático teria sido tentar abrir um quarto lugar onde não sobra nenhum. A boa notícia: a decisão do usuário continua de pé — "cada ação destrutiva no lugar que já está livre para ela" — só a descrição de qual lugar está livre em PENDING estava errada. Corrigido: para quem remove, PENDING se comporta como DRAFT/SUBMITTED — os três slots já estão cheios, então "Remover" vai para o painel, ao lado de "Cancelar". Um Minor irmão na garantia foi corrigido junto: a spec descrevia um "slot destrutivo compartilhado" com "Encerrar" que não existe — a garantia não tem uma variável de ação destrutiva separada como a análise, e a explicação foi reescrita para refletir a fila real de três slots (contrato, encerrar, "Ver análise" fixa).
A rodada 9 achou que a correção da rodada 8 não resolve o estado PENDING da análise: é o único estado em que "Cancelar" aparece no painel sem o slot do cabeçalho estar ocupado (regra hardcoded da RN11 da spec 0036, não "slot ocupado") — então "Remover" nesse estado específico podia ir para o slot livre do cabeçalho, ou acompanhar "Cancelar" no painel isolado mesmo com o cabeçalho livre, e a spec não decidia qual. Marcado [PERGUNTAR AO USUÁRIO], levado ao usuário: decidiu que "Remover" ocupa o slot livre do cabeçalho, convivendo com "Cancelar" isolado no painel — cada ação destrutiva no lugar que já está livre para ela, sem regra especial para PENDING. A seção 7.1 foi reescrita com o critério explícito por estado.
O usuário voltou depois da Parte III para fechar este portão, em vez de deixá-lo interrompido. O único achado da rodada 8: a spec dizia que, na análise de crédito, "Remover" excedente ia "para o painel abaixo junto com as demais ações excedentes — o mesmo mecanismo que a tela já usa", mas a fila genérica de ações do painel (acoesDoPainel) nunca recebeu uma ação destrutiva — o único precedente real ("Cancelar" em PENDING) é um botão isolado no rodapé, separado por borda, não a fila que já convive com "Aprovar"/"Reprovar". Corrigido para "Remover" seguir esse padrão isolado, não a fila genérica.
As duas últimas rodadas antes desta (6 e 7) já tinham vindo sem Critical, com achados cada vez menores — sinal de convergência, não de problema estrutural aberto. A rodada 7 (0 Critical) achou dois pontos que a correção da rodada 6 abriu ao generalizar demais: "o botão do cabeçalho vira Restaurar" nomeava só o cabeçalho, quando "Remover" também pode estar no painel condicional — corrigido para "no mesmo lugar onde estava, conforme 7.1"; e "Restaurar fora do prazo" descrevia só o Tooltip do ícone de linha, sem cobrir a instância de cabeçalho/painel de análise/garantia — corrigido para usar a faixa de status já confirmada como padrão de ação de topo desabilitada.
A rodada 6 não achou Critical — sinal de que o mecanismo de "onde Remover mora na garantia" (as rodadas 2-5 giraram em torno disso) estabilizou. Os dois achados foram de outra natureza: o caso feliz de remover não dizia o que a tela de detalhe mostra depois de confirmar (fica na página, "Remover" vira "Restaurar" no mesmo slot — corrigido, cobrindo também quem chega direto num registro já removido); e o estado de "restaurar" não tinha equivalente ao "erro ao remover" nem declarava a ausência de bloqueio — corrigido explicitando que restaurar não passa pela regra de ordem que bloqueia remover.
A rodada 5 achou que "análogo ao DecisionPanel" (correção da rodada 4) ainda não dizia o que o painel novo da garantia mostra — o DecisionPanel real tem parecer e histórico de decisão, conteúdo que a garantia não tem equivalente. Corrigido descrevendo o painel da garantia como mais simples e condicional: sem título nem seção, existe só quando há ação excedente, e contém apenas o botão que não coube no cabeçalho. O Minor da rodada 5 apontou que "o selo de status" não existe em inquilino/imóvel — lá o selo mostra tipo (PF/PJ, Residencial/Comercial), não status — corrigido para nomear as duas variações.
A rodada 4 confirmou que a correção da rodada 3 (o "menu de mais ações" da garantia) escorregava pela terceira vez: o menu que o PageHeader cita nunca foi construído em nenhuma tela do app, e "reusar" algo inexistente derrubava o próprio critério "7.1 ausente ou genérica" da lista Sempre Critical. Corrigido com o caminho mais barato, sugerido pelo próprio revisor: a garantia ganha um painel abaixo do cabeçalho análogo ao DecisionPanel da análise (mesmo padrão, instância própria), em vez de inventar o primeiro menu de overflow do produto. O Important da rodada 4 apontou uma contradição menor: "reusa exatamente esse mesmo diálogo" (imobiliária/inquilino/imóvel) ao lado de um texto novo sobre 60 dias — corrigido para "reusa o mesmo desenho visual, com o texto atualizado".
Os três Important da rodada 1 exigiam decisão de desenho que a Parte II não tinha tomado: qual diálogo de confirmação "Remover" usa em cada tela (a base tem dois hoje, sem padrão único), qual widget cobre "Mostrar removidos" (não existe toggle liga/desliga em components/shared/, só seletor de valor) e se o selo "Removido" substitui ou soma ao selo de status existente. Resolvidas: diálogo bespoke nas três telas que já o usam (imobiliária/inquilino/imóvel), ConfirmDialog nas que já o usam (análise/garantia/cobrança); "Mostrar removidos" reusa FilterSelect/ FilterPill como seletor de duas opções; o selo "Removido" soma ao de status, não substitui. Os três Minor (citação errada dos 44px, "esqueleto único" que não existe, e o cartão mobile tratado no singular quando há dois componentes) também foram corrigidos.
Rodada 2, 2 Important: a spec prometia que "Remover" na garantia usaria o mesmo painel de excedente que a análise de crédito tem — mas a tela de garantia não tem painel nenhum (DecisionPanel é exclusivo de análise), então a garantia ficaria sem lugar para a 4ª ação quando o topo já estivesse cheio; e o estado "Carregando" citava só dois padrões (texto/Skeleton) quando a base tem cinco, incluindo o loading nativo do DataGrid em análise e a divergência entre as duas superfícies de cobrança. Corrigido: a garantia ganha um ícone de "Remover" próprio no cabeçalho, fora dos slots principal/secundária/ terciária, em vez de competir por um painel que não existe; o estado "Carregando" agora lista as cinco variações reais, tela por tela.
Rodada 3, 2 Important + 1 Minor: o ícone próprio da rodada 2 reabria a própria RN7 do PageHeader — o componente documenta que a quarta ação vai para um menu, não para um quarto ícone solto, e no estado mais comum de garantia ativa os três slots já estão ocupados antes de "Remover" entrar. Corrigido para seguir a RN7 já existente: "Remover" entra na contagem de três, e o excedente vai para o menu que o componente já reserva para essa situação. O estado "Carregando", corrigido na rodada 2, ainda tratava inquilino e imóvel como só-texto quando isso só é verdade no mobile — no desktop as duas telas usam o DataGrid.loading nativo, igual à análise de crédito; corrigido para distinguir por viewport. O Minor (tooltip vs. faixa de status para ação desabilitada) foi esclarecido: "Restaurar fora do prazo" é ação de linha, não de topo, e o Tooltip já é o mecanismo que os ícones de linha das seis listas usam hoje.
Pendências aceitas: o que a revisão ainda apontou e se decidiu não resolver, com o porquê. Vazio se não houver.
Parte III — Detalhamento¶
Não preencher antes das revisões das seções 6 e 8 estarem aprovadas.
9. Regras de negócio¶
- RN1. A remoção lógica reaproveita o
SoftDeleteMixinjá usado porPartnerAgency,Renter,Property,CreditAnalysiseGuarantee(deleted_at,is_deleted,soft_delete()). Nenhum campo novo entra no mixin — restaurar édeleted_at = None, escrito pelo service novo, não um método do mixin. - RN2.
Billingganha oSoftDeleteMixinpor migration — hoje é a única das seis sem ele. A colunadeleted_atnasce nullable, sem dado a migrar (todo registro existente fica comdeleted_at = NULL, ou seja, ativo). - RN3. Toda remoção e toda restauração grava um
AuditLog(entity_type,entity_id,action="remover"/"restaurar",user_id,tenant_id) — reaproveita o mecanismo já usado no dossiê da garantia (spec 0040), em vez de acrescentar campo de auditoria aoSoftDeleteMixin. - RN4. Restaurar zera
deleted_at, grava oAuditLogde RN3, e não altera o status de negócio do registro (CreditAnalysisStatus,GuaranteeStatus,BillingStatusvoltam exatamente como estavam antes da remoção — remover/restaurar não participa dessas máquinas de estado). - RN5. A restauração tem uma janela de 60 dias corridos a partir de
deleted_at(decisão do usuário na entrevista desta spec, não presunção de quem escreve). Passado o prazo,POST /restoredevolve422com o códigoRESTORE_WINDOW_EXPIRED— a linha continua no banco, só a ação de restaurar deixa de estar disponível. - RN6. Remover é bloqueado enquanto existir um dependente não removido, qualquer que seja o status de negócio dele (RN8) — decisão do usuário: a ordem de remoção segue a árvore de dependência de baixo para cima:
| Remover… | …é bloqueado por | Verificação |
|---|---|---|
| Cobrança | nada — é folha da árvore | sempre libera |
| Garantia | Cobrança (guarantee_id) não removida | nova |
| Análise de crédito | Garantia (credit_analysis_id) não removida | nova |
| Imóvel | Análise de crédito (property_id) não removida | já existe (count_active_links) |
| Inquilino | Garantia (primary_lessee_id) ou Participante de análise (renter_id) não removidos | já existe (count_active_links) |
| Imobiliária | Inquilino, Imóvel, Análise de crédito ou Garantia (partner_agency_id) não removidos | estende o count_active_links existente (RN7) |
Cada verificação nova segue o padrão já usado por RenterRepository.count_active_links/ PartnerAgencyRepository.count_active_links: conta com deleted_at.is_(None) e levanta BusinessRuleError (código <ENTIDADE>_HAS_ACTIVE_LINKS, HTTP 422) quando a contagem é maior que zero. - RN7. A checagem de bloqueio de remoção da imobiliária é uma contagem própria, não o PartnerAgencyRepository.count_active_links existente — que hoje conta User, Property, CreditAnalysis e Guarantee, e não conta Renter (Renter.partner_agency_id existe no model, mas PartnerAgencyService.delete nunca consulta RenterRepository). A contagem nova troca User por Renter: passa a contar Renter, Property, CreditAnalysis e Guarantee — fechando a tabela da RN6, que já lista Inquilino como bloqueador — e exclui User, porque User está fora do escopo desta spec (Parte I, seção 5) e continua sem remoção própria; contar usuário vinculado tornaria impossível remover qualquer imobiliária com um usuário ativo, o que não é o que esta spec resolve. - RN8. A checagem de RN6 conta qualquer dependente não removido, independente do status de negócio dele — uma garantia TERMINATED com cobranças PAID e CANCELLED ainda bloqueia a remoção da garantia até cada cobrança ser removida (decisão do usuário: "qualquer filho não removido, seja qual for o status dele"). - RN9. Restaurar não passa pela checagem de RN6 — dá para restaurar um registro com o pai dele continuando removido (decisão do usuário, Parte II). - RN10. Nem remover nem restaurar exigem motivo — os dois endpoints não recebem corpo de requisição (decisão do usuário). - RN11. Não há remoção nem restauração em massa — os endpoints operam em um id por vez (decisão do usuário). - RN12. As seis listagens continuam ocultando removidos por padrão — comportamento herdado do BaseRepository (deleted_at.is_(None), automático desde que o model tenha o mixin). Um parâmetro novo, include_deleted: bool = false, passa a incluir os removidos quando true. Ele exige a permissão <recurso>:restore de quem chama (seção 10.2) — sem ela, a API devolve 403, não ignora o parâmetro em silêncio: é o mesmo padrão de porteiro explícito usado no resto do app, e evita a ambiguidade entre "não tem removido" e "não pode ver removido". - RN13. POST /{recurso}/{id}/restore não pode resolver o registro pelo _get_authorized que os seis services já usam hoje — ele filtra deleted_at explicitamente (if not x or x.deleted_at: raise NotFoundError, confirmado em guarantee_service.py, credit_analysis_service.py, renter_service.py, property_service.py, partner_agency_service.py) e devolveria 404 para exatamente o registro que se quer restaurar. restore() usa uma resolução nova, que filtra só tenant_id/imobiliária (10.3/10.4) e ignora deleted_at de propósito — no service, _get_authorized(id, current_user, include_deleted=True) (parâmetro novo no método existente) ou um método irmão dedicado; a escolha exata de nome é implementação, mas o comportamento fica descrito aqui para não ser descoberto tarde.
GET /{recurso}/{id} só usa essa mesma resolução (include_deleted=True) quando quem chama tem <recurso>:restore — a mesma permissão que já governa include_deleted na listagem (RN12). Sem essa permissão, GET /{id} continua chamando _get_authorized sem include_deleted (o comportamento de hoje) e devolve 404 para um registro removido, exatamente como devolveria antes desta spec — quem só tem <recurso>:read nunca soube que aquele id existia e continua não sabendo depois de removido. Isso fecha a mesma fronteira que já protege a listagem: ver o conteúdo de um registro removido é tão sensível quanto vê-lo listado, e as duas usam a mesma régua de permissão.
Sem essa resolução (para restore()) a restauração é inatingível: BaseRepository.get_by_id, que de fato ignora deleted_at hoje, é a peça de baixo nível que a nova resolução usa por dentro — ela não substitui _get_authorized diretamente nos services, porque _get_authorized também resolve o escopo por imobiliária (10.4), que get_by_id sozinho não faz. - RN14. Document, Decision, CreditAnalysisParticipant, CreditAnalysisStatusHistory, CreditAnalysisChangeLog e User não mudam de comportamento (Parte I, seção 5). Remover uma das seis entidades é uma atualização (UPDATE deleted_at), não uma exclusão física — nenhuma FK RESTRICT/SET NULL/ CASCADE dessas tabelas é acionada, e elas continuam apontando para o registro removido exatamente como apontavam antes.
10. Autorização e isolamento¶
10.1 Matriz de papéis¶
Restaurar usa o mesmo papel que remove (decisão do usuário) — a matriz abaixo lista as duas ações juntas por isso. Análise de crédito, garantia e cobrança são as três entidades novas: nenhum papel tem delete/restore nelas hoje, porque o endpoint não existe.
| Papel | Pode | Não pode |
|---|---|---|
ADMIN_GLOBAL | Remover/restaurar imobiliária, inquilino, imóvel (mesmo alcance que já tem hoje em partner_agencies:delete/renters:delete/properties:delete, sem guarda nova) | Remover/restaurar análise de crédito, garantia ou cobrança — guarda de código nova (RN15), mesmo padrão que já exclui guarantees:dossier_attach/dossier_amend/billings:update dele |
ADMIN_TENANT | Remover/restaurar as seis entidades do próprio tenant | Remover/restaurar registro de outro tenant (isolamento de tenant, seção 10.3) |
ANALYST | — | Remover/restaurar qualquer uma das seis — nunca teve a ação irreversível/sensível em nenhuma delas (cancelar análise, encerrar garantia e marcar cobrança já são só de ADMIN_TENANT hoje; remover segue o mesmo padrão) |
OPERATOR | Remover/restaurar inquilino e imóvel (já tem delete, ganha restore simétrico) | Remover/restaurar imobiliária, análise de crédito, garantia ou cobrança — nunca teve delete em nenhuma das quatro |
VIEWER | — | Remover/restaurar qualquer uma das seis — só leitura, sem mudança |
AGENCY_USER | Remover/restaurar inquilino (já tem delete, ganha restore simétrico), escopado à própria imobiliária | Remover/restaurar imóvel (decisão já existente: "é destrutivo e afeta análise de outra imobiliária"), imobiliária, análise de crédito, garantia ou cobrança — decisão desta spec (Parte I, seção 5): ficam só com a garantidora, mesmo quando é ela quem originou o registro |
10.2 Permissões¶
Nove permissões novas. Três pares completos, para as entidades sem delete hoje:
credit_analyses:delete,credit_analyses:restoreguarantees:delete,guarantees:restorebillings:delete,billings:restore
E três só de restore, para as entidades que já excluem hoje:
partner_agencies:restorerenters:restoreproperties:restore
Concessão por papel (rbac_seed.py, _ROLE_PERMISSIONS):
ADMIN_TENANTganha as nove.OPERATORganharenters:restoreeproperties:restore(ao lado dorenters:delete/properties:deleteque já tem).AGENCY_USERganharenters:restore(ao lado dorenters:deleteque já tem).ADMIN_GLOBAL: o conjunto por compreensão ({f"{r}:{a}" for r, a, _ in _PERMISSIONS}) já inclui as nove automaticamente — a subtração explícita (rbac_seed.py:167-168) ganha seis entradas novas:credit_analyses:delete,credit_analyses:restore,guarantees:delete,guarantees:restore,billings:delete,billings:restore. As três derestoredas entidades antigas (partner_agencies/renters/properties) não entram na subtração —ADMIN_GLOBALjá temdeletenelas sem guarda hoje, erestoresegue o mesmo alcance, sem mudar o que já existe.- RN15. A subtração da lista é reforço e sinal na tela de perfis (mesmo motivo do dossiê e do
billings:update, comentário emrbac_seed.py:20-24); quem de fato recusa é a guarda de código:CreditAnalysisService.delete/.restore,GuaranteeService.delete/.restoreeBillingService.delete/.restorelevantamPermissionDeniedErrorexplicitamente quandocurrent_user.is_admin_global, no mesmo padrão deguarantee_service.py:120-136(RN13, spec 0039) — porque a fixture de teste doADMIN_GLOBALconcede todas as permissões sem consultar_ROLE_PERMISSIONS.
Nenhuma permissão nova fica declarada e não exigida: as nove são checadas por require_permission("<recurso>", "delete"/"restore") nos endpoints da seção 12, e include_deleted=true (RN12) exige <recurso>:restore do próprio jeito.
10.3 Isolamento por garantidora (tenant)¶
DELETE/POST .../restore resolvem o id do path com BaseRepository.get_by_id(id, tenant_id=tenant_scope(current_user)) — o mesmo padrão que os três delete() existentes já usam. Registro de outro tenant → 404 (o id simplesmente não é encontrado dentro do escopo), nunca 403. Isso vale também para ADMIN_TENANT: ele não alcança id de outro tenant, porque tenant_scope devolve o tenant_id dele, não None.
ADMIN_GLOBAL continua sem filtro de tenant (tenant_scope devolve None) nas três entidades onde pode remover/restaurar — mesmo alcance cross-tenant que ele já tem hoje em partner_agencies:delete/renters:delete/properties:delete. Nada disso é exposição nova: é o mesmo escopo, com a ação de restaurar espelhando a de remover.
10.4 Isolamento por imobiliária¶
AGENCY_USER remove/restaura inquilino só dentro do escopo que RenterService.delete já aplica hoje — o partner_agency_id vem do perfil de quem chama, nunca do corpo ou do path, e um id de inquilino de outra imobiliária no mesmo tenant devolve 404. restore reusa a mesma resolução de escopo do delete existente; nenhuma checagem nova é necessária porque o id já chega escopado antes de a ação (remover ou restaurar) ser decidida.
AGENCY_USER não alcança remoção/restauração de imóvel, imobiliária, análise, garantia ou cobrança (10.1) — não há escopo por imobiliária a descrever para ações que ela não tem.
10.5 Plataforma × cliente (SaaS admin)¶
Quando quem chama é ADMIN_GLOBAL:
- Imobiliária, inquilino, imóvel: alcança remover/restaurar em qualquer tenant, porque já alcança
deleteassim hoje — este endpoint não abre superfície nova, só espelhadeleteemrestore. - Análise de crédito, garantia, cobrança: não alcança remover nem restaurar em nenhum tenant — RN15 bloqueia explicitamente. É dado de negócio do cliente (decisão de crédito, contrato de garantia, cobrança), na mesma categoria do dossiê da garantia e do
billings:updateque já ficam fora do alcance dele.
A tela do SaaS admin (/admin/*) não ganha nada novo — remover/restaurar continua sendo ação dentro das telas da garantidora (/app/*), não do painel de administração da plataforma. Nenhuma tela do SaaS admin passa a mostrar dado de negócio do cliente por causa desta spec.
11. Impacto por app¶
apps/api¶
- Modelos:
BillingganhaSoftDeleteMixin(RN2). - RBAC: nove permissões novas em
apps/api/app/db/rbac_seed.py(_PERMISSIONS) e a concessão por papel em_ROLE_PERMISSIONS, descritas na seção 10.2 — inclusive a subtração explícita deADMIN_GLOBALpara as três entidades novas (RN15). - Migration:
add_deleted_at_to_billings—add_column("billings", Column("deleted_at", DateTime(timezone=True), nullable=True)), comcreate_index(padrão der1t3v5y7b9c0_isolamento_por_imobiliaria.py).downgrade()remove o índice e a coluna, na ordem inversa. - Endpoints — nove novos, mais três alterados:
DELETE /credit-analyses/{id}(novo) — permissãocredit_analyses:deletePOST /credit-analyses/{id}/restore(novo) — permissãocredit_analyses:restoreDELETE /guarantees/{id}(novo) — permissãoguarantees:deletePOST /guarantees/{id}/restore(novo) — permissãoguarantees:restoreDELETE /billings/{id}(novo) — permissãobillings:deletePOST /billings/{id}/restore(novo) — permissãobillings:restorePOST /partner-agencies/{id}/restore(novo) — permissãopartner_agencies:restorePOST /renters/{id}/restore(novo) — permissãorenters:restorePOST /properties/{id}/restore(novo) — permissãoproperties:restoreGET /{recurso}× 6 (alterado) — ganha o parâmetroinclude_deleted(RN12)DELETE /partner-agencies/{id},/renters/{id},/properties/{id}(alterado) — a checagem de dependência estende para a árvore da RN6; o texto do Swagger deixa de dizer "irreversível via API" (deixou de ser verdade) e passa a citar a janela de 60 dias- Regras no service:
delete()/restore()novos emCreditAnalysisService(apps/api/app/services/credit_analysis_service.py),GuaranteeService(apps/api/app/services/guarantee_service.py),BillingService(apps/api/app/services/billing_service.py), seguindo o padrão deRenterService.delete(apps/api/app/services/renter_service.py) — resolve oidno escopo (10.3/10.4), aplica RN6/RN9 conforme a ação, gravaAuditLog(RN3), e para as três novas entidades aplica a guarda deADMIN_GLOBAL(RN15). Só odelete()existente dePartnerAgencyService(apps/api/app/services/partner_agency_service.py) ganha checagem nova (RN7, trocandoUserporRenter) — os deRenterServiceePropertyService(apps/api/app/services/property_service.py) já checam o que a RN6 pede (count_active_linksatual), sem mudança. Os três ganhamrestore()novo, sem checagem de dependência (RN9), e o_get_authorizedde cada um ganha o parâmetroinclude_deleted(RN13). - Repositórios:
count_active_linksnovo emCreditAnalysisRepository(Guarantee.credit_analysis_id) eGuaranteeRepository(Billing.guarantee_id), mesmo padrão deRenterRepository.count_active_links.PartnerAgencyRepository.count_active_linksé modificado no lugar, trocandoUserporRenter(RN7) — ele tem um único chamador hoje, o próprioPartnerAgencyService.delete, então não há segundo consumidor a preservar com um método irmão._get_authorizeddos cinco services existentes ganha o parâmetroinclude_deleted(RN13), usado só porrestore()e porGET /{id}quando o registro está removido. - Testes: caminho feliz de remover e restaurar nas seis; bloqueio por dependente não removido (RN6/RN8) — inclusive dependente em status terminal, para provar que status não isenta, e inclusive imobiliária bloqueada por inquilino ativo (RN7, CA17); restaurar com o pai ainda removido (RN9); prazo de 60 dias expirado (RN5);
GET /{id}de um registro removido devolvendo o registro, não 404 (RN13, CA18); negativo de autorização por papel (10.1) e por tenant/imobiliária (10.3/10.4); guarda deADMIN_GLOBALnas três novas (RN15).
apps/web¶
- Páginas/componentes: as seis listagens (
agencies,renters,properties,credit-analyses,guarantees,billings/index.tsxeguarantees/components/BillingList.tsx) ganham o ícone remover/restaurar por linha, o selo "Removido", e o filtroFilterSelect/FilterPillde duas opções condicionado à permissão derestore.credit-analyses/show.tsxeguarantees/show.tsxganham a ação "Remover"/"Restaurar" no slot destrutivo doPageHeader;guarantees/show.tsxganha o painel condicional novo descrito na Parte II, seção 7.1. data-testidnovos:<recurso>-remove-{id}/<recurso>-restore-{id}nas seis linhas,<recurso>-remove-dialog/<recurso>-restore-dialog(ou reuso dos existentes deConfirmDialog),<recurso>s-include-deleted-filternas seis listagens,<recurso>-removed-badgeno selo.- Sider: sem mudança — nenhum item de menu novo, a ação vive dentro das telas existentes.
- Menu e rota por permissão: o filtro e os ícones já nascem condicionados a
user.permissions.includes("<recurso>:restore")(mesmo padrão decanDeletehoje). Digitar a URL de detalhe sem nenhuma permissão de leitura continua dando o mesmo tratamento que qualquer outro 403/404 hoje — isso não muda. Mas para um registro removido, quem tem<recurso>:reade não tem<recurso>:restorerecebe404da API (RN13, seção 12) — é comportamento novo, proposital, não ausência de mudança: a tela de detalhe trata esse404como "não encontrado", igual trataria umidinexistente, e não precisa de lógica própria para diferenciar "não existe" de "existe mas está removido e você não pode ver" — a API já resolve isso por trás. - Responsividade & PWA: desktop e mobile descritos na Parte II, seções 7.1/7.2 — sem impacto de PWA (nenhum asset novo, nenhuma tela offline).
apps/e2e¶
- Specs: um fluxo completo (remover → filtro "mostrar removidos" → restaurar) numa das seis telas com jornada mais rica (garantia, pelo painel condicional novo), mais um spec por bloqueio de dependência (RN6) e um por negativo de autorização (
AGENCY_USERtentando remover análise/garantia/cobrança → 403). O prazo de 60 dias (RN5) não é praticável no e2e sem manipular tempo — cobrir por seed (registro já comdeleted_athá mais de 60 dias) e não por espera real. - Seeds: um registro já removido (dentro do prazo) e um já removido há mais de 60 dias, em pelo menos uma das seis entidades, para os dois specs acima.
12. Contrato de API¶
Erros previstos, os dois endpoints:
| Código | Situação |
|---|---|
404 | id não encontrado no escopo do tenant (ou da imobiliária, para renters) |
403 | Papel sem a permissão credit_analyses:delete/restore (10.1), ou ADMIN_GLOBAL tentando remover/restaurar uma das três novas entidades (RN15) |
422 CREDIT_ANALYSIS_HAS_ACTIVE_LINKS | Só em DELETE — existe uma Guarantee não removida vinculada (RN6) |
422 RESTORE_WINDOW_EXPIRED | Só em POST .../restore — passaram mais de 60 dias de deleted_at (RN5) |
O mesmo par de endpoints, com o código de negócio equivalente, para guarantees (GUARANTEE_HAS_ACTIVE_LINKS), billings (sem código de bloqueio — cobrança nunca bloqueia, RN6), partner-agencies (AGENCY_HAS_ACTIVE_LINKS, já existe — a checagem estende para a árvore da RN6/RN7), renters (RENTER_HAS_ACTIVE_LINKS, já existe) e properties (PROPERTY_HAS_ACTIVE_LINKS, já existe) — só o endpoint restore é novo para essas três; o DELETE já existe e ganha a checagem estendida.
{ "items": [ { "id": "uuid", "deleted_at": "2026-08-15T12:00:00Z", "...": "..." } ], "total": 1, "page": 1, "page_size": 50, "pages": 1 }
include_deleted=true sem a permissão <recurso>:restore → 403 (RN12). O mesmo parâmetro entra em GET /partner-agencies, /renters, /properties, /guarantees e /billings.
Sem parâmetro novo — o comportamento muda por permissão, não por flag (RN13). Se id aponta para um registro removido: quem tem credit_analyses:restore recebe 200 com o registro (deleted_at preenchido no corpo); quem só tem credit_analyses:read recebe 404, exatamente como receberia se o id não existisse — o mesmo endpoint, os mesmos dois desfechos possíveis hoje (200/404), só que agora a linha entre eles também depende de restore, não só de tenant/imobiliária. Mesma regra em GET /{recurso}/{id} das outras cinco entidades.
Migration: add_deleted_at_to_billings — upgrade() adiciona billings.deleted_at (nullable, indexado); downgrade() remove o índice e a coluna, restaurando o schema anterior sem perda de dado nas colunas que já existiam.
13. Critérios de aceite¶
- [ ] CA1. Dado um
ADMIN_TENANT, quando ele remove uma cobrança sem nenhum dependente, então ela some da listagem padrão e passa a aparecer só cominclude_deleted=true. - [ ] CA2. Dado um
ADMIN_TENANT, quando ele tenta remover uma garantia com uma cobrança ainda não removida, então recebe422 GUARANTEE_HAS_ACTIVE_LINKSe a garantia continua ativa. - [ ] CA3. Dado o mesmo cenário do CA2, quando a cobrança é removida primeiro e a garantia é removida em seguida, então a remoção da garantia é aceita.
- [ ] CA4. Dado um registro removido há menos de 60 dias, quando quem removeu (ou qualquer um com a mesma permissão) o restaura, então ele volta a aparecer na listagem padrão com o status de negócio que tinha antes da remoção.
- [ ] CA5. Dado um registro removido há mais de 60 dias, quando alguém tenta restaurá-lo, então recebe
422 RESTORE_WINDOW_EXPIREDe o registro continua removido. - [ ] CA6. Dado um
AGENCY_USER, quando ele tenta remover ou restaurar uma análise de crédito, uma garantia ou uma cobrança — inclusive uma que ele mesmo originou —, então recebe403nos dois casos. - [ ] CA7. Dado um
OPERATOR, quando ele tenta remover uma imobiliária, então recebe403(nunca teve essa permissão, e esta spec não muda isso). - [ ] CA8. Dado um
ADMIN_GLOBAL, quando ele tenta remover ou restaurar uma análise de crédito, uma garantia ou uma cobrança de qualquer tenant, então recebe403nos dois casos (RN15) — mesmo tendo alcance cross-tenant nas outras três entidades. - [ ] CA9. Dado um
VIEWER, quando ele acessa a listagem de qualquer uma das seis entidades, então não vê o ícone de remover/restaurar nem o filtro "Mostrar removidos". - [ ] CA10. Dado um registro removido com status de negócio
ACTIVE/APPROVEDantes da remoção, quando ele é restaurado, então o status de negócio volta a ser exatamente o que era, sem passar por nenhuma transição de estado (RN4). - [ ] CA11. Dado um inquilino removido cuja garantia-mãe segue removida, quando alguém restaura só o inquilino, então a restauração é aceita mesmo com o pai removido (RN9).
- [ ] CA12. Dado o filtro "Mostrar removidos" ativo numa listagem, no mobile (375px), quando a lista mostra um registro removido, então o cartão exibe o selo "Removido" ao lado do selo existente (status ou tipo, conforme a entidade) e a ação vira "Restaurar".
- [ ] CA13. Dado o cabeçalho de uma garantia
ACTIVEcom "Corrigir contrato assinado" e "Encerrar" ocupando os dois slots de ação, quando "Remover" é acionado, então ele aparece no painel condicional abaixo do cabeçalho, não no cabeçalho. - [ ] CA14. Dado qualquer uma das seis entidades, quando alguém com permissão remove ou restaura um registro, então a requisição não envia nem exige campo de motivo — só a confirmação no modal (RN10).
- [ ] CA15. Dada a listagem de qualquer uma das seis entidades, quando o usuário seleciona várias linhas, então não existe ação de remover/restaurar em lote — cada ação continua sendo uma chamada por
id(RN11). - [ ] CA16. Dado um documento anexado a uma análise de crédito, quando a análise é removida, então o documento continua acessível e vinculado a ela, sem nenhuma mudança de comportamento — mesmo vale para
Decision,CreditAnalysisParticipant,CreditAnalysisStatusHistory,CreditAnalysisChangeLogeUser(RN14). - [ ] CA17. Dado um
ADMIN_TENANT, quando ele tenta remover uma imobiliária com um inquilino ainda não removido vinculado a ela, então recebe422 AGENCY_HAS_ACTIVE_LINKSe a imobiliária continua ativa (RN6/RN7). - [ ] CA18. Dado um registro removido dentro do prazo de 60 dias, quando alguém com permissão de
restoreabre a tela de detalhe dele (GET /{recurso}/{id}), então a tela carrega normalmente, mostrando o registro removido — não um 404 (RN13). Parabillings, que não tem tela de detalhe própria (billings/redirect.tsxresolveGET /billings/{id}e redireciona para a garantia), o critério vale no nível do endpoint: a chamada que o redirect faz recebe200, não404. - [ ] CA19. Dado um
VIEWER(tem<recurso>:read, nunca tem<recurso>:restore), quando ele tenta abrirGET /{recurso}/{id}de um registro removido, então recebe404— o mesmo que receberia se oidnão existisse, sem indício de que o registro já existiu (RN13).
14. Plano de implementação¶
- [x]
apps/api— migrationadd_deleted_at_to_billings(Alembic,upgrade/downgrade) - [x]
apps/api—Billingmodel ganhaSoftDeleteMixin - [x]
apps/api— nove permissões novas emrbac_seed.py(_PERMISSIONSe_ROLE_PERMISSIONS, inclusive a subtração deADMIN_GLOBAL) - [x]
apps/api—count_active_linksnovo emCreditAnalysisRepositoryeGuaranteeRepository; variante emPartnerAgencyRepositorytrocandoUserporRenter - [x]
apps/api—delete()/restore()emCreditAnalysisService,GuaranteeService,BillingService, com guarda deADMIN_GLOBAL - [x]
apps/api—restore()novo emPartnerAgencyService/RenterService/PropertyService; só odelete()dePartnerAgencyServiceganha checagem nova (RN7) — os deRenterService/PropertyServicejá checam o que a RN6 pede - [x]
apps/api—include_deletednos seis endpointsGET /{recurso}, com o porteiro de permissão da RN12 - [x]
apps/api— nove endpoints novos (routers), Swagger completo (summary,description,response_description),CommitRoutenos routers que ainda não têm - [x]
apps/api— testes (caminho feliz, RN6/RN8/RN9, RN5, RN15, autorização) — TDD, o teste antes do código - [x]
apps/web— ícone remover/restaurar, selo "Removido" e filtro nas seis listagens,data-testidem cada elemento interativo, responsivo (desktop e mobile) - [x]
apps/web— ação "Remover"/"Restaurar" noPageHeaderde análise de crédito e garantia, mais o painel condicional novo da garantia - [x]
apps/web— teste unitário (*.test.tsx) das telas alteradas,make test-web - [x]
apps/e2e— spec do fluxo completo, do bloqueio de dependência, e do negativo de autorização; seeds de registro removido dentro e fora do prazo - [x]
make testpassando —apps/web37 arquivos/190 testes verdes;apps/api820 verdes e 1 vermelho (o intermitente já documentado noCLAUDE.mdda raiz,test_partner_agency_search.py::test_busca_alcanca_quem_esta_fora_da_primeira_pagina, arquivo não tocado por esta branch).make lintnão passa no repositório, como já documentado — as linhas que esta branch acrescenta que excedem 120 colunas replicam um padrão já existente no mesmo arquivo (partner_agency_service.py), não uma categoria nova de dívida. - [x] Validado no browser em desktop e mobile —
apps/web/src/pages/*/index.test.tsxe*/show.test.tsxcobrem os dois viewports;apps/e2e/tests/e2e/remocao-logica.spec.tsroda contra o dev-remote de verdade.
15. Decisões em aberto¶
Nenhuma — as decisões de negócio (ordem de remoção, escopo de recuperação, prazo, RBAC, motivo, remoção em massa, papel da imobiliária) foram tomadas com o usuário na Parte I e na Parte II, e o nome exato do parâmetro (include_deleted) e dos códigos de erro (RESTORE_WINDOW_EXPIRED, <ENTIDADE>_HAS_ACTIVE_LINKS) são decisão técnica desta parte, não pendência.
16. Revisão do detalhamento¶
Preenchido pelo laço do /spec new, rubrica .claude/rubrics/detalhamento.md. Registrar todas as rodadas, inclusive as reprovadas.
| Rodada | Critical | Important | Minor | Veredicto |
|---|---|---|---|---|
| 1 | 0 | 1 | 0 | reprovada |
| 2 | 0 | 2 | 0 | reprovada |
| 3 | 0 | 1 | 0 | reprovada |
| 4 (extra, pedida pelo usuário) | 0 | 1 | 0 | reprovada |
| 5 (extra, pedida pelo usuário) | 0 | 0 | 1 | aprovada |
Resultado: aprovada na rodada 5. Nenhum achado novo de segurança (S1-S5) ou de clareza maior (C1-C4) — o revisor conferiu de novo os nove endpoints, a matriz de papéis, o isolamento por tenant/imobiliária e a fronteira do ADMIN_GLOBAL, e tudo bateu. O único achado foi Minor: três frases descrevendo de formas ligeiramente diferentes a mesma mudança em PartnerAgencyRepository.count_active_links (RN6 "estende", RN7 "troca", seção 11 "ganha uma variante... se houver outro consumidor" — quando o método só tem um chamador). Corrigido, ainda que Minor não travasse a aprovação.
Rodada 4, 1 Important: a seção apps/web ainda dizia "remover/restaurar não altera quem lê o registro, só quem pode agir sobre ele" — frase escrita antes da correção da rodada 3, e que a própria RN13 (também da rodada 3) contradiz: para um registro removido, <recurso>:restore passa a decidir quem lê (200) e quem não lê (404). Corrigido, distinguindo "sem nenhuma permissão de leitura" (nada muda) de "com :read mas sem :restore sobre um removido" (o 404 novo e proposital da RN13). Duas imprecisões menores também corrigidas: CA18 não dizia que billings não tem tela de detalhe própria (o critério vale no endpoint que o redirect chama); e a seção 11/14 davam a entender que RenterService/PropertyService ganhavam checagem de dependência nova, quando só PartnerAgencyService ganha (RN7) — as outras duas já checam o que a RN6 pede.
Rodada 3, 1 Important: a correção da rodada 2 (include_deleted em _get_authorized) abriu um risco novo — GET /{recurso}/{id} passaria a exibir o conteúdo de um registro removido para qualquer papel com <recurso>:read, sem checar <recurso>:restore, contradizendo a régua que RN12 já aplica à listagem (visibilidade de removido é ação sensível, gated por restore). Corrigido: RN13 agora diz explicitamente que GET /{id} só usa include_deleted=True quando quem chama tem <recurso>:restore — sem essa permissão, o endpoint continua devolvendo 404 para um registro removido, exatamente como devolvia antes desta spec. Acrescentado CA19 (negativo: VIEWER recebe 404, não o conteúdo) e um bloco de contrato para GET /{id} na seção 12, que antes só cobria DELETE/restore/listagem.
Rodada 2, 2 Important, os dois riscos concretos de implementação: (a) _get_authorized, usado pelos cinco services existentes, já filtra deleted_at — reaproveitá-lo em restore() faria a própria restauração devolver 404 para o registro que se quer restaurar (quebraria CA4/CA5/CA10/CA11/CA13 como escritos). Corrigido com RN13 reescrita: restore() e GET /{id} de um removido usam uma resolução que ignora deleted_at de propósito. (b) A tabela da RN6 já listava Inquilino como bloqueador da remoção de imobiliária, mas a RN7 descrevia a checagem nova como "count_active_links existente menos User" — que nunca conta Renter hoje, deixando a própria tabela contradita pelo texto que a implementa. Corrigido: a checagem nova troca User por Renter explicitamente. Acrescentados CA17 (imobiliária bloqueada por inquilino) e CA18 (GET de removido não devolve 404).
O único achado da rodada 1 foi de precisão: a spec dizia PARTNER_AGENCY_HAS_ACTIVE_LINKS para o código de erro já existente da imobiliária, quando o código real (partner_agency_service.py:76) é AGENCY_HAS_ACTIVE_LINKS. Corrigido. Nenhum achado de segurança (S1-S5) — o revisor conferiu linha a linha RN1-RN15, a matriz de papéis, isolamento de tenant/imobiliária e a guarda de ADMIN_GLOBAL contra o código real, e tudo bateu.
Riscos aceitos: risco de acesso que a revisão apontou e se decidiu não fechar nesta spec, com quem decidiu e por quê. Vazio se não houver — e o esperado é que esteja vazio.
17. Revisão da implementação¶
Preenchido pelo /spec implement, rubrica .claude/rubrics/implementacao.md. As revisões de fase (api, web, e2e) vão nas primeiras linhas; a da branch inteira, na última.
| Rodada | Escopo | Critical | Important | Minor | Veredicto |
|---|---|---|---|---|---|
| 1 | apps/api | — | — | — | — |
| 2 | apps/web | — | — | — | — |
| 3 | branch | — | — | — | — |
Resultado: aprovada na rodada N da revisão de branch.
Defeitos aceitos: o que a revisão apontou e se decidiu não corrigir nesta spec, com quem decidiu e por quê. Vazio se não houver.
18. Registro¶
Anotações durante a implementação: o que mudou em relação ao planejado e por quê. Preencher com /spec implement.
Afirmações conferidas¶
Fatos sobre o repositório que uma rodada de portão já conferiu abrindo o arquivo. O recortar.py leva a seção junto em qualquer parte, e o prompt do revisor diz o que fazer com ela.
| Afirmação | Onde | Conferida em |
|---|---|---|
OPERATOR não tem partner_agencies:create/delete — só list/read (apps/api/app/db/rbac_seed.py, bloco OPERATOR) | Seção 2, bullet Imobiliária | rodada 1 |
partner_agencies.py:117 — "Somente ADMIN_TENANT e ADMIN_GLOBAL podem remover imobiliárias" | Seção 2, bullet Imobiliária | rodadas 1 e 4 |
ANALYST não tem guarantees:sign/annotate — só guarantees:create (apps/api/app/db/rbac_seed.py, bloco ANALYST, comentário "Ganha só create: assinar, encerrar e anotar continuam fora") | Seção 2, bullet Garantia | rodadas 1 e 2 |
BillingUpdate (apps/api/app/schemas/billing.py:39-40) só tem due_date/notes, sem campo de status | Seção 1, parágrafo de cobrança | rodada 2 |
_cancelar_cobrancas_futuras (apps/api/app/services/guarantee_service.py:592-616) só alcança origin=CALENDAR ainda não vencida — nunca origin=MANUAL | Seção 1 e 3, parágrafos de cobrança | rodadas 2 e 3 |
CreditAnalysis/Guarantee herdam SoftDeleteMixin (apps/api/app/db/base.py:31-39, credit_analysis.py:66, guarantee.py:77) — deleted_at e is_deleted (@property derivada) —, mas nenhum service grava deleted_at para elas | Seção 3, "O que já se tentou" | rodadas 2 e 3 |
ANALYST tem credit_analyses:update (RN13, spec 0036) e renters:update_sensitive (apps/api/app/db/rbac_seed.py:218,223) | Seção 2, bullet Análise de crédito | rodada 3 |
"a operação é irreversível via API" é texto verbatim só do endpoint de partner_agencies (apps/api/app/api/v1/routers/partner_agencies.py:113) — renters/properties não repetem a frase | Seção 1, parágrafo de imobiliária/inquilino/imóvel | rodada 3 |
AGENCY_USER tem credit_analyses:withdraw (spec 0037, apps/api/app/db/rbac_seed.py bloco AGENCY_USER; docstring em credit_analysis_service.py:578-609) — desiste da própria análise em DRAFT/SUBMITTED/PENDING_DOCS sem acionar ADMIN_TENANT | Seção 2 e 3, análise de crédito | rodada 4 |
ADMIN_GLOBAL não tem subtraído partner_agencies:delete, credit_analyses:cancel nem guarantees:update (apps/api/app/db/rbac_seed.py:167-168, conjunto subtraído) — só billings:update é de fato exclusivo de ADMIN_TENANT | Seção 2, bullets Imobiliária/Análise/Garantia e o resumo | rodada 4 |
cancel() (credit_analysis_service.py:559-575) e terminate() (guarantee_service.py:551-587) não têm guarda de código bloqueando ADMIN_GLOBAL, ao contrário da emissão de garantia (RN13, spec 0039, guarantee_service.py:120-136); scoping.py:40-45 devolve None para ADMIN_GLOBAL | Seção 2, bullets Análise/Garantia e o resumo | rodada 4 |
apps/web/src/components/shared/PageHeader.tsx:46-97 — teto real de 3 slots tipados (primaryAction/secondaryAction/tertiaryAction); nenhum "menu de mais ações" existe hoje em apps/web/src/pages/ nem apps/web/src/components/shared/ | Seção 7.1, bullet Garantia | rodada 5 |
apps/web/src/pages/credit-analyses/show.tsx:861-881 — o mecanismo de excedente da análise (TETO_DE_ACOES_NO_TOPO = 3, acoesExcedentes para o DecisionPanel) é real | Seção 7.1, bullet Análise de crédito | rodada 4 e 5 |
apps/web/src/pages/credit-analyses/components/DecisionPanel.tsx:19-38 — o DecisionPanel real mostra parecer escrito, histórico da decisão e campos de aprovação, não é só um container de botão excedente | Seção 7.1, bullet Garantia | rodada 5 |
apps/web/src/pages/guarantees/show.tsx:393-444 — hoje só 3 slots no cabeçalho da garantia, sem painel abaixo | Seção 7.1, bullet Garantia | rodada 4 e 5 |
Diálogo próprio (selo trash, dois botões fullWidth, texto "Esta ação não pode ser desfeita") em apps/web/src/pages/agencies/index.tsx:339-409, apps/web/src/pages/renters/index.tsx:387-455, apps/web/src/pages/properties/index.tsx:528-596; ConfirmDialog em apps/web/src/pages/credit-analyses/show.tsx, apps/web/src/pages/guarantees/show.tsx, apps/web/src/pages/billings/index.tsx, apps/web/src/pages/guarantees/components/BillingList.tsx | Seção 7, "Caso feliz — remover", passo 2 | rodadas 1, 4 e 5 |
Cartão mobile: MobileListCard só em apps/web/src/pages/agencies/index.tsx (ações em faixa no rodapé, apps/web/src/components/shared/MobileListCard.tsx:89-100); RecordCard nas outras cinco (ações ao lado do valor, apps/web/src/components/shared/RecordCard.tsx:187-207) | Seção 7.2 | rodada 1 e 5 |
FilterSelect/FilterPill em apps/web/src/components/shared/FilterBar.tsx e apps/web/src/components/shared/MobileFilterBar.tsx:109; nenhum Switch em apps/web/src/components/shared/ | Seção 7.1/7.2, "Mostrar removidos" | rodada 1 e 5 |
Carregamento: DataGrid.loading em credit-analyses/renters/properties (desktop); Skeleton em guarantees/billings (agregada); texto "Carregando…" em agencies/BillingList (embutida) e nos cartões mobile de agencies/renters/properties, todos em apps/web/src/pages/ | Seção 7.3, "Carregando" | rodada 3 e 5 |
Ação de topo desabilitada usa faixa de status, não Tooltip (apps/web/src/pages/guarantees/show.tsx:406-409); ícones de linha usam Tooltip (apps/web/src/pages/agencies/index.tsx, apps/web/src/pages/renters/index.tsx, apps/web/src/pages/properties/index.tsx) | Seção 7.3, "Restaurar fora do prazo" | rodada 3 e 5 |
Renter/Property não têm campo status — o selo que apps/web/src/pages/renters/index.tsx:218,326 e apps/web/src/pages/properties/index.tsx:288,420 mostram é renter_type/property_type | Seção 7, "Caso feliz — restaurar", passo 2 | rodada 5 |
O código de erro real do bloqueio de imobiliária é AGENCY_HAS_ACTIVE_LINKS (apps/api/app/services/partner_agency_service.py:76), não PARTNER_AGENCY_HAS_ACTIVE_LINKS; RENTER_HAS_ACTIVE_LINKS (renter_service.py:122) e PROPERTY_HAS_ACTIVE_LINKS (property_service.py:131) conferem | Seção 12, contrato | rodada 1 (detalhamento) |
_get_authorized dos cinco services existentes (guarantee_service.py, credit_analysis_service.py, renter_service.py:128-138, property_service.py:137-144, partner_agency_service.py:82-93) filtra deleted_at explicitamente (if not x or x.deleted_at: raise NotFoundError) — devolveria 404 para um registro removido se reaproveitado sem alteração | Seção 9, RN13 | rodada 2 (detalhamento) |
Renter.partner_agency_id existe no model (apps/api/app/models/renter.py), mas PartnerAgencyService.delete nunca consulta RenterRepository — PartnerAgencyRepository.count_active_links hoje conta User/Property/CreditAnalysis/Guarantee, nunca Renter | Seção 9, RN6/RN7 | rodada 2 (detalhamento) |
rbac_seed.py:167-168 — o conjunto subtraído de ADMIN_GLOBAL tinha, antes desta spec, só guarantees:dossier_attach, guarantees:dossier_amend e billings:update | Seção 10.2, RN15 | rodada 1-2 (detalhamento) |
VIEWER tem <recurso>:read nas seis entidades (rbac_seed.py:256-269) e nunca tem <recurso>:restore em nenhuma — confirma que GET /{id} de um removido não pode depender só de :read | Seção 9, RN13 | rodada 3 (detalhamento) |
A fila genérica de ações do DecisionPanel (acoesDoPainel, credit-analyses/show.tsx:862-879, renderizada em DecisionPanel.tsx:164,203-260,413-424) nunca recebe acaoDestrutiva — o único precedente de ação destrutiva no painel ("Cancelar" em PENDING) é uma prop dedicada (onCancel/showCancel), renderizada isolada no rodapé, separada por borderTop | Seção 7.1, bullet Análise de crédito | rodada 8 |
Duas regras, e a segunda é a que faz a primeira valer:
- Só entra o que um revisor conferiu — não o que você conferiu ao escrever.
- Mexeu na frase, a linha sai da tabela.
Feature sem afirmação sobre o repositório não precisa da seção — apague-a.