Pular para conteúdo

0038 — Emissão da garantia e a espera pela assinatura

Status CONCLUÍDA
Branch spec/0038-emissao-e-assinatura-da-garantia
Criada em 2026-08-15
Apps afetados api · web · e2e
Revisão do problema aprovada na rodada 6 (com decisão do usuário)
Revisão da experiência aprovada na rodada 5 (com decisão do usuário)
Revisão do detalhamento aprovada na rodada 5 (com decisão do usuário)
Revisão da implementação api: rodada 2 · web: rodada 8 · e2e: rodada 1 · branch: aprovada na rodada 6

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.

  1. 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.
  2. A Parte II diz como se usa o que vai existir. Revisada na seção 8 (jornada × viewports).
  3. 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.
  4. 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.

As três dores desta spec têm uma causa só: nada no sistema move o status de uma garantia. Ele nasce ACTIVE na emissão e fica ACTIVE — enquanto o contrato não voltou assinado, e ainda depois de a vigência ter terminado. Tudo o que segue decorre disso: a espera invisível, a cobrança sem guarda, o anexo sem efeito e a garantia vencida que continua se apresentando como vigente. A dor da tela pobre é a exceção, e entra junto por ser o mesmo registro — quem abre a garantia para saber em que pé ela está é a mesma pessoa que não encontra ali o que ela cobre.

Nota de recorte (o como é da Parte III; aqui só por que as dores vêm juntas): as três primeiras se aplicam no mesmo ponto e na mesma ordem — não se recusa cobrança sem haver estado que a recuse, e a permissão decide quem opera o que as outras criam. Separá-las produziria specs em que as primeiras não entregam nada sozinhas. A dor da tela é a mais solta: o que falta nela já está gravado, então ela entregaria valor sozinha — entra por ser o mesmo registro, e por ser onde o estado novo vai aparecer. A seção 14 planeja a ordem; esta nota registra por que a ordem não pode ser "uma spec cada".

1. Dor

A garantia existe no sistema como se já valesse, semanas antes de valer — e continua valendo depois de ter acabado.

Aprovada a análise de crédito, a garantidora emite a garantia e ela nasce ACTIVE. Não por escolha de quem emite: ACTIVE é o único estado que a emissão sabe produzir. Só que, pelo processo que a garantidora pretende operar — a seção 3 marca passo a passo o que é fato do sistema e o que é processo pretendido —, nesse momento o termo acabou de ser redigido e mandado para o inquilino assinar, e volta dias ou semanas depois. Nesse intervalo o sistema afirma duas coisas que não são verdade — que a cobertura está valendo e que o inquilino aceitou. E na outra ponta acontece o simétrico: passada a end_date, nada rebaixa a garantia. Ela segue ACTIVE indefinidamente.

O custo de conviver com isso tem quatro faces:

  • A espera é invisível. Não há como perguntar ao sistema "quais garantias estão aguardando assinatura, e desde quando?". Uma garantia parada há três semanas é indistinguível de uma assinada ontem: as duas aparecem ACTIVE, na mesma lista, com o mesmo selo. Quem cobra o retorno do inquilino cobra de memória, ou não cobra.
  • Nada impede cobrar fora da vigência. A cobrança é criada à mão, uma a uma, e a API não olha o estado da garantia em momento nenhum ao criá-la: aceita cobrar a que aguarda assinatura e aceita a que já venceu. A tela esconde o botão fora de ACTIVE, mas isso é aparência — e, como a vencida continua ACTIVE, nem a tela a esconde. Um boleto contra um contrato não assinado é cobrar por cobertura que ainda não se deve; contra uma garantia vencida, por cobertura que já acabou.
  • Anexar o contrato assinado não muda nada. Há onde guardar o PDF — o painel de documentos da garantia existe e funciona. Mas o arquivo entra como mais um anexo: a garantia já estava ACTIVE antes, e continua igual depois. O ato que deveria ser o marco da operação — voltou assinado, agora vale — não tem efeito nenhum no sistema.
  • A garantia vencida se apresenta como vigente. Terminada a vigência, ela continua na lista com o selo de ativa, continua faturável, e só um alerta em outra tela sabe que algo está errado. Quem lê a garantia lê que ela vale.

A segunda dor: quem toca a operação não pode concluí-la. O OPERATOR conduz o processo inteiro — cadastra inquilino e imóvel, monta a análise, acompanha até a aprovação — e para ali: emitir garantia exige guarantees:create, que ele não tem. A garantia fica esperando um repasse ao gestor, e o gestor vira gargalo de agenda num processo cuja decisão de crédito já foi tomada. Isso não quer dizer que a emissão seja carimbo: quem emite escolhe início e fim de vigência e o valor garantido, nenhum deles conferido contra a análise, e o intervalo entra no cálculo do pagamento inicial — a emissão fixa termos financeiros, e os congela. É justamente por decidir dinheiro que conceder essa permissão é decisão de autorização, não formalidade; a Parte III é que a toma. O que a Parte I registra é o custo de hoje: a espera não é do inquilino nem do processo, é de agenda.

A terceira dor é de leitura. A tela da garantia mostra três campos de dado — início da vigência, fim e valor garantido —, mais o selo de status, um campo de notas, um link para a análise, a lista de cobranças e o painel de documentos. A garantidora já guardou na emissão a taxa aplicada, o LMI, o pacote locatício, a mensalidade, a taxa de setup, a carência de sinistro e o prazo para acionar; e sabe quem é o tomador e qual é o imóvel. Nada disso aparece. Para responder "quanto essa garantia cobre?" ou "de quem é?", sai-se da garantia e entra-se na análise de crédito, no inquilino e no imóvel — três telas para montar de cabeça o que uma poderia mostrar. É o registro do objeto que a garantidora vende, e é o que menos diz sobre ele.

Vale uma distinção que a Parte III vai precisar: dos nove dados que faltam, oito já estão na própria garantia e apenas não são exibidos — os sete financeiros mais o tomador (primary_lessee_id). O imóvel é a única exceção: a garantia não guarda property_id, e chegar até ele exige passar pela análise. Não é o mesmo trabalho.

2. Quem sente

Quem emite — hoje, o ADMIN_TENANT. É dele a dor do estado: emite, redige o termo, manda assinar, espera, recebe de volta e anexa. É caminho obrigatório — nenhuma garantia chega a valer sem passar por aqui, e não há emissão de exceção. (O ADMIN_GLOBAL também alcança a rota, por receber todas as permissões; é papel de plataforma, não de operação, e a fronteira plataforma × cliente é assunto da seção 10.)

Quem conduz e não conclui — o OPERATOR. Sente a segunda dor: faz o trabalho todo até a aprovação e depende do gestor para o último passo. Caminho obrigatório também — toda garantia originada por ele passa pelo repasse.

Quem lê depois — ANALYST, VIEWER, OPERATOR e ADMIN_TENANT. A terceira dor é de quem abre uma garantia para responder uma pergunta vinda de fora: a imobiliária ligou perguntando até quando a cobertura vale; o inquilino contesta um valor; alguém confere o que cobrar antes de lançar a mensalidade; o analista quer ver como ficou a garantia que nasceu do parecer dele. Todos chegam pela garantia e precisam de dados que estão em outras três telas.

Quem não sente, e é decisão registrada: a imobiliária (AGENCY_USER). Pelo processo pretendido, todo o ciclo é conduzido pela garantidora — ela gera a documentação, envia para assinatura e anexa o retorno; a imobiliária não participa.

O sistema hoje só sustenta metade disso, e a diferença importa. É fato que o AGENCY_USER não tem nenhuma permissão guarantees:* — não emite. Mas anexar documento em garantia ele consegue: tem documents:create, guarantee é um entity_type válido de upload, e a guarda que fecha o dossiê da análise enquanto a bola está com a garantidora não cobre garantia (cai num else: return). O identificador da garantia chega até ele pela própria análise, que ele lê. Isso importaria muito se anexar o assinado fosse, por si, o que faz a garantia passar a valer — a imobiliária moveria o estado da garantia por uma rota que ninguém guardou, que é a classe que a spec 0035 pagou.

Por isso anexar e ativar são atos separados, e é decisão tomada: guardar o arquivo não move estado nenhum; ativar é um ato explícito, e só a garantidora o pratica. A porta do upload deixa de ser um risco de estado — continua sendo assunto da seção 10 decidir se ela se fecha por outros motivos, e esta spec não a fecha nem a alarga.

Quanto: não há volume a medir, e não há como estimar. A produção subiu na spec 0028 e ainda não tem cliente nenhum — o CLAUDE.md da raiz registra isso. Não há operação da qual extrair "quantas garantias por mês", "quantos dias de espera" ou "quantas consultas por garantia", e qualquer número aqui seria inventado. O que sustenta o alcance é o desenho: as duas primeiras dores estão em caminho obrigatório — toda garantia é emitida, espera e vence, e toda garantia originada pelo OPERATOR passa pelo repasse. A terceira é mista: conferir o que cobrar antes de lançar a mensalidade é recorrente, já que cada cobrança é criada uma a uma; os demais gatilhos (ligação da imobiliária, contestação do inquilino) são exceção, e é honesto dizer que ali o alcance é menor.

3. A jornada de hoje

Gatilho: a análise de crédito é aprovada.

Sobre a origem desta jornada: os passos 1, 3, 6 e 7 são conferíveis no código e descrevem o que o sistema faz. Os passos 2, 4 e 5 acontecem fora do sistema. Eles são o processo pretendido pela própria garantidora que constrói o produto — quem pediu esta spec —, e não o relato de uma operação em curso: não há cliente (seção 2), então ninguém executou isso ainda. São premissa de desenho, não evidência de campo, e estão marcados um a um para que ninguém os leia como medição. Se a operação real divergir deles, o que esta spec constrói precisa ser reavaliado — é a premissa mais frágil da Parte I, e é honesto dizê-lo.

  1. O OPERATOR chega ao fim do que pode fazer. Conduziu inquilino, imóvel e análise até APPROVED. Emitir exige guarantees:create, que ele não tem.
  2. (processo pretendido) O repasse ao gestor acontece fora do sistema. Não há aviso, fila nem caixa de entrada: nada no produto diz ao ADMIN_TENANT que existe uma análise aprovada esperando emissão. O OPERATOR avisa por mensagem ou conversa, ou o gestor descobre olhando a lista. É aqui que a espera de agenda acontece.
  3. O ADMIN_TENANT emite a garantia. Um diálogo na tela da análise, e a garantia nasce ACTIVE — sem que exista outro estado possível. Quem emite informa início e fim de vigência e o valor garantido; o sistema congela aí o snapshot de preço (taxa, LMI, mensalidade, setup) e numera a garantia. Não gera cobrança: cobrança é ato manual, depois.
  4. (processo pretendido) A garantidora redige a documentação — fora do sistema. O termo é produzido à parte. Existe um ContractTemplate no banco que não é usado para isso. É por isso que a emissão precede a assinatura, e não o contrário: o termo precisa do número da garantia e dos valores que só a emissão congela. Emitir é o que dá ao documento o que ele tem a dizer.
  5. (processo pretendido) A garantidora envia para o inquilino assinar — fora do sistema. Por e-mail, por assinatura eletrônica, ou pelo caminho que a operação usar. O sistema não participa e não fica sabendo.
  6. Espera-se o retorno — e é aqui que quebra. Este passo não existe no sistema. Não há estado, não há data de envio, não há lista. A garantia que espera é, para todos os efeitos da tela, idêntica a uma em pleno vigor. E, porque o sistema não distingue, ele também aceita que se crie cobrança contra ela.
  7. O assinado volta e é anexado. O painel de documentos aceita o PDF (Document.guarantee_id existe desde antes desta spec). O anexo é gravado, e nada mais acontece — a garantia estava ACTIVE antes e continua ACTIVE depois.

Depois, e sem gatilho de ninguém: passada a end_date, a garantia permanece ACTIVE. Nenhum caminho do sistema atribui EXPIRED — o estado existe no enum e nunca é usado. A garantia vencida continua na lista como ativa e continua aceitando cobrança.

Onde exatamente quebra: entre o passo 3 e o passo 7, o sistema trata como instantâneo um intervalo que dura dias ou semanas, e não representa nem a espera nem o desfecho dela. Antes disso, no passo 2, o processo já tinha parado uma vez por falta de permissão. E depois do 7 ele nunca mais se move.

O que já se tentou:

  • O armazenamento foi construído e não bastou. O Document aceita guarantee_id e a tela já tem o painel. Guardar o arquivo não é o mesmo que reconhecer que a garantia passou a valer — resolveu-se o armazenamento e não o estado.
  • Há um contorno construído para a garantia vencida — código, não prática de ninguém, já que não há operação —, e ele confirma o diagnóstico: o painel de operações levanta um alerta CRITICAL GUARANTEE_EXPIRED_ACTIVE — "garantia com data de fim no passado ainda marcada como ACTIVE". Ou seja, alguém já precisou construir um detector para um estado que o sistema deveria simplesmente ter. É compensação, não solução: o alerta avisa, mas a garantia continua vigente e faturável.
  • Sobrou estado morto no enum. Além de EXPIRED, o SUSPENDED existe e nenhum caminho do serviço chega nele. A Parte III precisa decidir se os novos estados convivem com esses ou os substituem.
  • Fora isso não há contorno construído: nem campo de nota marcando "aguardando", nem planilha paralela. E não há contorno humano a relatar, porque ninguém operou isto ainda — o que a garantidora prevê, se nada mudar, é controlar a espera por fora do sistema.

Na terceira dor, a jornada é curta e repetida. Gatilho: uma pergunta que chega de fora — ligação da imobiliária, contestação do inquilino, conferência antes de lançar a mensalidade. Abre-se a garantia, leem-se três campos, não se acha o que se procurava, clica-se no link da análise de crédito; de lá, se a pergunta era sobre a pessoa ou o imóvel, mais um salto. Volta-se para a garantia para ver as cobranças. Nada disso quebra — só custa três telas toda vez, e obriga a pessoa a lembrar de onde vem cada número enquanto alguém espera na linha.

4. Resultado esperado

  • A garantia só afirma o que é verdade. Enquanto o contrato não voltou assinado, ela se apresenta como esperando; passada a vigência, deixa de se apresentar como vigente. Verificável: emitir uma garantia e conferir que ela não consta como vigente; registrar o assinado e conferir que passou a constar; passar da data de fim e conferir que deixou de constar.
  • Não se cobra garantia fora da vigência. Verificável: tentar criar uma cobrança numa garantia que aguarda assinatura é recusado pela API, e o mesmo vale para a encerrada e a vencida; na vigente é aceito. Hoje as três primeiras tentativas também são aceitas. A cobrança legítima não some, muda de momento: o setup e a primeira parcela que o domínio prevê no primeiro vencimento passam a poder ser criados a partir da ativação — que, pelo processo, é quando o inquilino já aceitou. A guarda atrasa a cobrança, não a elimina.
  • A espera é uma pergunta que o sistema responde. É possível ver quais garantias estão aguardando assinatura e há quanto tempo cada uma espera, sem perguntar a ninguém. Verificável: a informação existe e distingue duas garantias emitidas em datas diferentes.
  • Registrar o contrato assinado tem consequência. Verificável: existe um antes e um depois observáveis na própria garantia — o que hoje não existe, já que anexar não muda nada.
  • O alerta GUARANTEE_EXPIRED_ACTIVE deixa de encontrar caso. Verificável: o contorno do painel de operações passa a apontar zero, porque não existe mais garantia com fim no passado se apresentando como vigente. É a prova de que a causa foi tratada, e não o sintoma. Como o vencimento passa a ser reconhecido — estado gravado, condição derivada da data, ou outra coisa — é decisão da Parte III; se for derivado, a consulta do alerta (end_date < hoje AND status == ACTIVE) precisa ser ajustada junto, senão ela continua contando as mesmas linhas e o critério mede a coisa errada.
  • Quem conduz a operação a conclui sem depender de agenda alheia. Verificável: some o repasse do passo 2 da jornada. A forma — conceder guarantees:create ao OPERATOR, ou outro desenho de autorização — é decisão da seção 10, não deste resultado.
  • As perguntas sobre a garantia se respondem na garantia. Verificável: "de quem é", "qual imóvel", "quanto cobre", "quanto custa", "até quando vale" e "está paga" deixam de exigir a análise de crédito, o inquilino e o imóvel.

5. Fora de escopo

  • Geração automática de cobrança. A cobrança continua sendo criada à mão, como hoje. O que muda é que ela passa a ser recusada fora da garantia vigente. Gerar setup e primeira parcela automaticamente ao assinar foi cogitado e ficou de fora por decisão do usuário: é funcionalidade de faturamento nova — exige decidir vencimento, parcelamento do setup e o que fazer quando os parâmetros do tenant mudaram entre a emissão e a assinatura — e o ganho desta spec não depende dela.
  • Integração com assinatura eletrônica (D4Sign, Clicksign, DocuSign e afins). A assinatura continua acontecendo fora do sistema, e o sistema recebe o PDF assinado. Cogitado e deixado de fora porque exige contrato com provedor, credencial, retorno assíncrono e tratamento de falha — é uma spec inteira, e a espera visível não depende dela.
  • Geração do termo a partir do ContractTemplate. O modelo existe no banco e não é usado na emissão. Redigir o termo continua sendo feito fora. Fica de fora porque compor e versionar texto de contrato é problema independente do problema de estado que esta spec resolve — juntá-los faria a espera invisível esperar por um editor de contrato.
  • Outros signatários. O sistema espera apenas a assinatura do inquilino para reconhecer a garantia como vigente. Que o termo seja bilateral entre garantidora e tomador não muda isso: o que a garantidora assina não é o que ela fica esperando. A imobiliária e o proprietário não assinam — o proprietário, aliás, não é parte conhecida pela garantidora (ver o domínio no CLAUDE.md).
  • Expiração automática da garantia não assinada. Uma garantia pode aguardar assinatura indefinidamente; ninguém a cancela por decurso de prazo. Cogitado — o mercado costuma ter validade de proposta — e deixado de fora por decisão do usuário: sem operação rodando, qualquer prazo aqui seria chutado. Não confundir com o vencimento da vigência, que entra nesta spec: aquele tem data conhecida desde a emissão.
  • Escrita da imobiliária sobre a garantia. O AGENCY_USER passa a ler a garantia da própria imobiliária — decisão tomada no portão da experiência, registrada na seção 6 —, e não ganha nenhuma escrita: não emite, não ativa, não encerra. A seção 10 decide o que dos números ela enxerga, já que a tela nova expõe taxa aplicada, LMI, setup e total de entrada.
  • Fechar a porta de upload da imobiliária. Esta spec não fecha a porta que ele já alcança hoje (seção 2). Ela deixa de importar para o estado porque anexar e ativar são atos separados — a imobiliária não ativa. Fechar o upload em si é mudança de acesso com efeito fora desta spec (o AGENCY_USER anexa documento em análise e imóvel pelo mesmo caminho), e fica para quem for tratar isso de propósito.
  • Mudança no fluxo da análise de crédito. A análise segue terminando em APPROVED do jeito que termina hoje. Esta spec começa depois disso.
  • Régua de vencimento, pagamento e sinistro. Como a cobrança é calculada, quando vence e como é baixada não muda; o que muda é apenas quando ela pode ser criada. Sinistro não é tocado.

6. Revisão do problema

Rubrica .claude/rubrics/problema.md, revisor de contexto limpo e novo a cada rodada.

Rodada Critical Important Minor Veredicto
1 1 2 2 reprovada
2 0 2 4 reprovada
3 0 2 4 reprovada
4 0 1 3 reprovada
5 0 1 5 reprovada
6 0 1 4 reprovada — achados corrigidos sem 7ª rodada (ver abaixo)

Resultado: aprovada com decisão registrada do usuário, após a rodada 6.

Por que seis rodadas, e não três. O limite da régua é 3. Ele foi estourado duas vezes, com autorização explícita do usuário nas duas, porque o laço estava convergindo e não preso: 1C/2I → 0C/2I → 0C/2I → 0C/1I → 0C/1I → 0C/1I. Nenhum achado se repetiu entre rodadas — cada uma encontrou coisa nova, e da rodada 4 em diante os Important foram defeitos introduzidos pela correção anterior, não pontos resistentes.

O que as rodadas acharam, e o que isso custou. Três Important eram afirmações minhas sobre o código que não eram verdade, e as três teriam se propagado para o desenho:

  1. "A emissão gera cobrança" — não gera; cobrança é ato manual. Isso mudou a fronteira da spec (geração automática saiu de escopo) e o critério de aceite.
  2. "A emissão não decide nada" — decide: quem emite escolhe vigência e valor garantido, sem conferência contra a análise, e o intervalo entra no cálculo do pagamento inicial. Isso transformou a ampliação ao OPERATOR de formalidade em decisão de autorização.
  3. "A imobiliária não anexa" — anexa: tem documents:create, guarantee é entity_type válido e a guarda de janela cai num else: return. Isso levou à decisão de separar upload de ativação, que é o desenho que fecha a porta sem mexer em permissão.

A rodada 6 apontou um Important de numeração — eu havia inserido um passo na jornada sem atualizar o preâmbulo que diz quais passos são fato de código. Corrigido sem nova rodada, por decisão do usuário de encerrar o laço: é erro factual de uma frase, sem conteúdo de decisão, e registrá-lo como pendência aceita deixaria texto errado no documento. A correção não foi revisada — é a única parte da Parte I nessa condição.

Decisões do usuário registradas neste portão (nenhuma foi inventada):

Decisão Escolha
Quando a cobrança pode existir Só em garantia vigente — a API recusa fora disso. Sem geração automática.
Quem assina Só o inquilino (tomador).
Como a assinatura chega Upload do PDF assinado; sem integração com assinatura eletrônica.
Garantia não assinada Espera indefinidamente; sem expiração automática.
Emissão pelo OPERATOR Entra — o gestor é gargalo de agenda hoje.
Garantia vencida ainda ACTIVE Entra na spec — vencer também precisa mover o estado.
Porta de upload da imobiliária Não se fecha nem se alarga: anexar e ativar viram atos separados, e só a garantidora ativa.

Pendências aceitas: nenhuma. Todos os achados das seis rodadas foram corrigidos.

Alteração posterior à aprovação. O portão da experiência (seção 8, rodada 4) achou uma inconsistência que só aparecia ao descrever a tela: a Parte II dizia que a imobiliária "vê a garantia e não a ativa", enquanto o AGENCY_USER não tem nem guarantees:read e receberia 404. O usuário decidiu que ela passa a ler a garantia da própria imobiliária, e a fronteira da seção 5 foi corrigida para refletir isso. A Parte I não foi rerrevisada depois desta alteração — é o único ponto dela nessa condição.

Riscos que a Parte I deixa anotados para as partes seguintes:

  • A premissa mais frágil é a jornada externa (passos 2, 4 e 5): é processo pretendido pela garantidora que constrói o produto, não operação observada — não há cliente. Se a operação real divergir, o que esta spec constrói precisa ser reavaliado.
  • A porta de upload do AGENCY_USER em garantia continua aberta. Deixa de mover estado pela decisão acima, mas a seção 10 deve olhá-la de propósito.
  • SUSPENDED e EXPIRED são estados mortos no enum. A Parte III decide se os novos estados convivem com eles ou os substituem.
  • O título da spec anuncia metade do que a Parte I abriu — vencimento e guarda de cobrança entraram depois. Vale renomear ao fechar a Parte III.

Parte II — A experiência

Não preencher antes da revisão da seção 6 estar aprovada.

7. Jornada pretendida

Os quatro estados que a garantia passa a ter. Hoje ela só sabe ser Ativo. Passa a saber dizer em que ponto do ciclo está:

Estado Como se chega Cobra?
Aguardando assinatura é o estado de nascimento, na emissão não
Vigente de Aguardando, quando alguém registra o contrato assinado sim
Vencida de Vigente, sozinha, quando passa o fim da vigência não
Encerrada de Vigente ou de Aguardando, por ato humano com motivo não

Vencer rebaixa só quem estava vigente. Uma garantia que nunca foi assinada não vira Vencida ao passar do fim da vigência proposta: ela continua Aguardando assinatura, e a faixa de status ganha um aviso — "A vigência proposta terminou em 14/08/2026. Encerre esta garantia." O motivo é que a Parte I decidiu que a espera não tem prazo; rebaixá-la para Vencida esconderia o botão de registrar assinatura e deixaria a garantia inalcançável sem ninguém ter decidido isso. Quem decide é a pessoa, e a saída é encerrar com motivo — registrar a assinatura passa a ser recusado nesse caso (RN8a), porque ativar uma garantia cujo período já acabou a deixaria vigente e sem como faturar.

"Emitir outra" não é saída, e não pode ser. Há uma garantia por análise de crédito, garantido por unicidade no banco — o botão "Emitir garantia" da análise só aparece enquanto ela não tem garantia, e não volta depois de encerrada. Cobrir o mesmo aluguel de novo exige uma análise nova, que é outro caminho e não faz parte desta spec.

O nome do estado é o que a pessoa lê no selo. "Aguardando assinatura" e não "Pendente": pendente já nomeia um estado de análise de crédito, e as duas coisas aparecem na mesma jornada — reusar a palavra faria alguém ler "pendente" em dois lugares querendo dizer coisas diferentes.

Ponto de entrada. Quatro, e nenhum novo no menu:

  1. Da análise aprovada — o botão "Emitir garantia" que já existe no topo da análise. É o caminho de origem, e continua o mesmo.
  2. Da lista de garantias — quem quer saber o que está parado entra por aqui e filtra por estado. É o caminho de acompanhamento, e hoje não responde nada porque tudo é Ativo.
  3. Da própria garantia — quem recebeu o assinado abre a garantia e registra ali.
  4. Da imobiliária, pelo menu — o item "Garantias" passa a aparecer para o AGENCY_USER, já que ele é condicionado a guarantees:list, que ela ganha. Ela chega na lista, filtrada à própria carteira, e abre a garantia dos seus contratos. Só lê: não emite, não registra assinatura, não corrige e não encerra — nenhuma ação de escrita aparece para ela.

Jornada, caso feliz. Sete passos, dois deles fora do sistema:

  1. Aprovada a análise, quem conduz (OPERATOR ou gestor) clica em Emitir garantia.
  2. O diálogo mostra o que vai ser congelado — vigência, valor garantido, taxa, mensalidade, setup e total de entrada — já preenchido, e pede confirmação. Quem emite pode ajustar vigência e valor garantido; o resto é consequência e se recalcula à vista.
  3. Confirmada a emissão, o sistema leva quem emitiu para o detalhe da garantia recém-criada — que é o que o diálogo já faz hoje. Ela nasce Aguardando assinatura, e a faixa de status diz o que falta, no texto que a tabela de 7.1 fixa: "Aguardando o contrato assinado — emitida há 12 dias" (no primeiro dia, "emitida hoje").
  4. (fora do sistema) A garantidora redige o termo com o número e os valores que a emissão acabou de congelar, e envia ao inquilino.
  5. (fora do sistema) O inquilino assina e devolve.
  6. Quem recebeu abre a garantia e clica em Registrar contrato assinado. O diálogo pede o arquivo e a data da assinatura, e avisa o que vai mudar: a garantia passa a valer e a cobrança é liberada.
  7. A garantia fica Vigente. Um toast confirma, e a partir dali o botão de nova cobrança deixa de recusar.

Anexar não é ativar, e a tela precisa deixar isso óbvio. O painel de documentos continua aceitando arquivo como sempre — inclusive da imobiliária, que já consegue hoje. O que faz a garantia valer é o passo 6, que é ato próprio, com confirmação e com dono. Um arquivo largado no painel não muda estado nenhum, e a tela não deve sugerir que muda.

Como o arquivo entra no passo 6. Dois caminhos, e os dois terminam no mesmo lugar:

  • Enviar agora — o seletor de arquivo do diálogo. É o caminho normal.
  • Escolher um já anexado — uma lista dos documentos desta garantia, mostrando nome e data de anexo, para quem já subiu o PDF antes de lembrar de registrar. Sem filtro por tipo: exigir que a pessoa tivesse classificado o documento antes transformaria um acerto em pré-requisito. No desktop os dois caminhos são um par de rádio no topo do diálogo — "Enviar arquivo" (marcado por padrão) e "Escolher um já anexado" —, e a lista aparece abaixo quando o segundo é escolhido. Se a garantia não tiver documento nenhum, o segundo rádio vem desabilitado com o motivo ao lado ("Nenhum documento anexado ainda"), em vez de abrir uma lista vazia.

Em qualquer dos dois, o documento escolhido passa a ser o contrato assinado da garantia — é essa marcação que a seção Documentos exibe depois.

Marcar o PDF errado é possível, e por isso tem conserto. A lista não filtra por tipo, e os anexos se parecem entre si. Com a garantia já Vigente, a ação do topo vira "Corrigir contrato assinado" — mesmo diálogo, com o documento e a data atuais preenchidos, trocando um ou outro. Corrigir não mexe no estado: a garantia continua vigente, porque o que estava errado era o papel apontado, não o aceite. Fica registrado quem corrigiu e quando. Em Vencida e Encerrada a ação não aparece — ali o registro está fechado.

A data da assinatura. Vem pré-preenchida com a data de hoje e é editável, porque o PDF costuma voltar dias depois de assinado. Ela não mexe na vigência: início e fim foram congelados na emissão e continuam sendo. Serve de registro e é o que encerra a contagem de espera. Não pode ser futura — se for, o campo recusa ali mesmo: "A assinatura não pode ser depois de hoje." Pode ser anterior à emissão (o inquilino pode ter assinado antes de o PDF chegar), e nesse caso nada mais muda.

As duas datas nunca se confundem na tela, porque dizem coisas diferentes. A faixa de status sempre nomeia as duas: "Vigente de 01/08/2026 a 31/07/2027 · assinada em 09/08". O intervalo é o congelado na emissão — é ele que define a cobertura. A assinatura é quando o aceite chegou. Isso resolve os dois casos em que a ativação cai fora do intervalo:

  • Assinou depois do início (o mais comum): a cobertura já corria; a faixa mostra o intervalo inteiro e a data de assinatura ao lado, sem alarde. A garantia não passa a valer "a partir de agora" — ela passa a constar como valendo, pelo período que sempre foi.
  • Registrou antes do início (vigência futura): o selo diz Vigente e a faixa acrescenta "a cobertura começa em 01/09". Não se inventa um quinto estado para isso: a garantia está aceita e é irreversível; o que falta é o calendário passar.

Custo de uso. O que o sistema já sabe e não pede de novo: vigência e valor garantido (da análise), taxa aprovada, LMI, pacote locatício, mensalidade, taxa de setup, carência e prazo de sinistro (dos parâmetros da garantidora e do parecer), quem é o tomador e qual é o imóvel. O que ele precisa perguntar: o arquivo do contrato assinado e a data em que foi assinado — dois campos, uma vez, no fim do ciclo, um deles pré-preenchido. Na emissão, nada de novo: os mesmos quatro campos de hoje (início, fim, valor garantido e notas), agora com o resumo do que será congelado ao lado. As notas ficam onde estão, por último e opcionais.

7.1 Desktop

A tela da garantia deixa de ser um índice e vira a resposta. Segue o padrão de detalhe do ui-patterns.md — Paper com faixa de status no topo e seções abaixo, Field em grade de três colunas. Ordem das seções, e o motivo de cada posição:

  1. Faixa de status — o selo do estado e, ao lado, a frase que muda com ele, uma por estado:
Estado A frase ao lado do selo
Aguardando assinatura "Aguardando o contrato assinado — emitida há 12 dias"
Vigente "Vigente de 01/08/2026 a 31/07/2027 · assinada em 09/08"
Vencida "Vencida em 31/07/2027"
Encerrada "Encerrada em 14/08/2026 por Ana Souza" — o motivo fica na seção própria

Três variantes, e são todas: Vigente com vigência ainda futura acrescenta "a cobertura começa em 01/09"; Aguardando com a vigência proposta já vencida acrescenta o aviso descrito em 7.3; e Vigente sem data de assinatura registrada (garantia anterior a esta spec, migrada) omite o "· assinada em" em vez de mostrar vazio.

O tempo de espera fica aqui, não numa seção lá embaixo: é a informação que a pessoa veio buscar quando o estado é o de espera. 2. Cobertura — valor garantido, LMI, pacote locatício, taxa aplicada e mensalidade. Primeiro porque responde "quanto essa garantia cobre?", a pergunta mais frequente da terceira dor. 3. Partes — o tomador (nome, documento e contato), o imóvel (endereço e tipo) e a imobiliária. Cada um com link para a própria tela: resolve o "quem" e o "qual" sem tirar a pessoa da garantia, sem esconder o caminho de quem quer o cadastro inteiro. 4. Vigência e entrada — início, fim, prazo em meses, taxa de setup, primeira parcela e total de entrada. 5. Sinistro — carência e prazo para acionar. Última das seções de dado porque é a menos consultada no dia a dia e a mais consultada no pior dia; abaixo dela só ficam o campo livre e os dois painéis que já existem.

Fora da ordem, e só num estado: a seção Encerramento — motivo, quem encerrou e quando — aparece entre a faixa de status e Cobertura, e só quando a garantia foi encerrada. É o que a spec exige ao tornar o motivo obrigatório: exigir sem exibir seria pedir um dado para não mostrá-lo a ninguém. Fica no topo porque, numa garantia encerrada, é a primeira pergunta. Nos outros três estados ela não existe, e a numeração abaixo segue como está. 6. Notas — o campo livre que a tela já tem, com edição inline, preservado. Editável em Aguardando e Vigente; só leitura em Vencida e Encerrada, onde o registro está fechado. Hoje a edição existe só em ACTIVE — passa a existir também na espera, que é justamente quando alguém quer anotar "termo enviado em 12/08, cobrado no dia 20". Em 375px a edição inline abre o multiline de três linhas no lugar do texto, com Salvar e Cancelar logo abaixo e dentro da área visível — não é diálogo, e por isso não ganha rodapé ancorado; o que a protege do teclado é a seção rolar para si ao entrar em edição. 7. Cobranças — a tabela que já existe (competência, vencimento, valor, valor pago, status, ações), com o botão de nova cobrança desabilitado e explicado enquanto o estado não é Vigente: "Disponível quando a garantia estiver vigente." Botão cinza sem motivo é a pessoa adivinhando o que a trava. 8. Documentos — o painel que já existe, com o contrato assinado marcado como tal, no topo da lista, e não perdido entre os outros anexos.

A imobiliária vê quase a mesma tela. Ela cobra a garantia junto com o aluguel e repassa, então precisa do valor e do prazo:

Seção O que o AGENCY_USER vê
Faixa de status inteira — estado, tempo de espera, vigência, data de assinatura
Cobertura inteira, inclusive taxa aplicada e pacote locatício
Partes inteira — é a carteira dela
Vigência e entrada inteira, inclusive setup, primeira parcela e total de entrada
Sinistro inteira — é o que ela vai acionar
Notas não vê: é o campo interno da garantidora
Encerramento vê que foi encerrada e quando; não vê o motivo, que é registro interno
Cobranças vê a lista e os valores; não cria, baixa nem marca em atraso
Documentos inteira, incluindo o contrato assinado marcado

Isto mudou no portão do detalhamento, e o motivo é do código. A versão anterior desta tabela escondia dela a precificação (taxa, pacote, composição da entrada). A seção 16 achou que ela já lê esses números hoje, pelo resumo da garantia embutido na resposta da análise de crédito — que a tela dela consome. Esconder aqui o que sai por lá entregaria uma fronteira que não existe. O que sobra de restrição são os dois campos que não vazam por rota nenhuma: as notas e o motivo do encerramento. Ver 10.6 e os riscos aceitos da seção 15.

A seção 10 aplica isso no contrato da API — esconder na tela não protege a rota, e esta é a lição que as specs 0034 e 0035 pagaram.

Ações no topo, no teto de três do PageHeader, e cada uma com o estado em que aparece:

Ação Forma Aparece quando
Registrar contrato assinado preenchida estado é Aguardando assinatura
Encerrar contornada estado é Aguardando ou Vigente — nunca em Vencida ou Encerrada, onde não há o que encerrar
Ver análise de crédito contornada sempre

Vigente a garantia, a primeira sai e o topo fica com duas. Encerrar exige motivo (obrigatório, não recomendado): é ato irreversível, e o registro de por que se encerrou é o que a auditoria vai procurar. A confirmação diz o que se perde — "A garantia deixa de valer e não gera novas cobranças. Não é possível reativá-la."

O número da garantia passa a ser o número da garantia. Hoje a tela mostra dois identificadores fabricados e diferentes entre si — a lista compõe #260815.1432 a partir do created_at e o detalhe mostra os últimos 8 dígitos do id —, enquanto a API já gera e guarda o guarantee_number no formato G202608-00042 desde a emissão. O tipo Guarantee do web sequer o declara. Os dois códigos fabricados saem, e o guarantee_number passa a ser o que aparece no cabeçalho do detalhe, na coluna da lista e no que a busca encontra. Não é higiene: é a jornada que exige — o termo enviado ao inquilino carrega esse número, e a pessoa que atende a ligação digita o que está no papel. Dois números para a mesma garantia é a ligação que não se resolve. Quem for implementar não deve preservar o guaranteeCode do index.tsx achando que é o identificador da casa.

O cabeçalho da página passa a ser Garantia G202608-00042, e o subtítulo, o nome do tomador — a vigência sai dali porque a faixa de status assumiu.

Em 375px o cabeçalho não cabe inteiro, e isso é decisão, não acidente: com três ações de 44px sobram ~169px para o título (medida do ui-patterns.md), e Garantia G202608-00042 não entra. O título vira G202608-00042 — sem a palavra "Garantia", que a tela toda já diz — e o subtítulo continua sendo o tomador. O número não pode truncar: é a única coisa da tela que a pessoa vai ditar ao telefone, e um número cortado é pior que nenhum.

Lista de garantias. Ganha o que responde à espera invisível:

  • Busca e filtro no FilterBar, o recipiente que o ui-patterns.md define para as listas: o SearchField primeiro e crescendo, buscando por número da garantia e nome do tomador — é o que se tem na mão quando a imobiliária liga —, e ao lado um FilterSelect de estado. São esses dois, e mais nenhum: a lista de hoje está fora do padrão (um Select solto acima da tabela) e entra junto, porque replicar o desvio é o que faz um padrão morrer.
  • Colunas: número, tomador, imóvel, situação, valor garantido, mensalidade, vigência — sete —, mais a de Ações, que já existe e fica. São oito, que é exatamente o teto medido no CLAUDE.md do web ("acima de oito colunas a de Ações sai da tela"). Está no limite de propósito, e é o que impede a nona entrar sem alguém decidir o que sai.
  • Paginação: ListPagination no rodapé, nos dois viewports, com ordenação e paginação no servidor — o padrão da spec 0022. A lista de hoje carrega 200 registros e filtra em memória; ordenar "as que aguardam há mais tempo primeiro" só na página corrente seria mentira, e é o defeito que o CLAUDE.md da API registra.
  • A coluna Situação funde o selo de estado com o tempo: Aguardando · 12 dias para quem espera, o selo sozinho para os demais. Ela substitui a coluna "Vence em" de hoje, que mostra um chip com os dias restantes (vermelho abaixo de 30, amarelo até 90) — e a troca é deliberada: o sinal de urgência passa a ser o da espera, que é o que esta spec resolve, e os dias até vencer seguem legíveis na coluna de vigência. Quem for implementar não deve apagar o chip achando que é resto.
  • A coluna "Análise de Crédito" sai, e é decisão: o caminho para a análise passa a ser a ação "Ver análise de crédito" do detalhe. Uma coluna de link numa lista de oito colunas gasta a largura que a Situação passou a precisar, e ninguém navega para a análise a partir da lista — chega-se nela pela garantia que se foi ver. Quem for implementar não deve preservá-la achando que é resto.
  • O filtro de estado oferece os quatro estados desta spec, e só eles. SUSPENDED não aparece: é estado morto no enum, nenhum caminho do sistema chega nele, e oferecer um filtro que nunca retorna nada é prometer um recorte que não existe. Se a Parte III decidir ressuscitá-lo, o filtro é o primeiro lugar que muda.
  • Clicar em qualquer ponto da linha abre a garantia. A coluna de Ações de hoje (ver e encerrar por linha) fica: ver vira redundante mas inofensivo, e encerrar por linha é o que evita abrir três telas para encerrar três garantias.
  • Ordenáveis: situação, valor garantido, mensalidade e vigência — as quatro que o servidor sabe ordenar. Número, tomador e imóvel não recebem seta: ordenar só a página corrente seria mentira, e é a regra que o CLAUDE.md da API já fixa.
  • Ordenação padrão: por situação primeiro — as que Aguardam no topo, entre si da mais antiga para a mais nova —, e o resto abaixo pela emissão mais recente. Não é ordenar tudo por data, o que jogaria garantias vigentes antigas para cima e afundaria justamente o que a lista passou a existir para mostrar.

Emissão. O diálogo de hoje ganha o resumo do que será congelado, à vista, antes de confirmar: taxa, mensalidade, setup e total de entrada, recalculados quando se mexe na vigência ou no valor. Hoje esses números só aparecem depois de emitida — e são justamente os que a emissão torna imutáveis. Confirmar o que se vai congelar sem ver o que congela é o que a Parte I chamou de "a emissão decide dinheiro".

7.2 Mobile (375px)

Quem usa a garantia no celular está lendo, não emitindo. O caso real é atender uma pergunta longe da mesa — a imobiliária ligou, o inquilino contesta. Por isso o mobile prioriza leitura; as duas escritas continuam disponíveis, e as duas estão descritas abaixo.

O detalhe:

  • Vira MobileDetailSection empilhado, uma seção por bloco, na mesma ordem do desktop. A faixa de status vem antes de tudo e não colapsa: estado e tempo de espera são a primeira coisa na tela, sem rolar.
  • Colapsam as sete: Cobertura, Partes, Vigência e entrada, Sinistro, Notas, Cobranças e Documentos — abertas por padrão só Cobertura e Partes. As duas últimas são as de altura ilimitada (n cobranças, até 20 documentos), e são justamente por isso que colapsam: abertas, empurram tudo o mais para fora de alcance. O cabeçalho de cada uma traz a contagem (Cobranças (14), Documentos (3)), para a pessoa saber se vale abrir. Encerramento nunca colapsa: quando existe, é a resposta que a pessoa veio buscar. Em 375px, sete seções abertas viram milhares de pixels de rolagem para achar a mensalidade. O MobileDetailSection de hoje não colapsa — é Paper + cabeçalho + linhas —, então isto é comportamento novo do componente compartilhado, não uso do que existe. É de propósito: os dois casos de colapso do app hoje (HistoryTimeline e settings/document-types) resolveram cada um o seu com Collapse local, e um terceiro local seria o começo de um padrão por acidente.
  • Cobranças em 375px vira RecordCard, não tabela: seis colunas não cabem, e a de hoje não tem tratamento de viewport nenhum. Cada cartão traz competência, vencimento, valor e o selo de status; valor pago aparece só quando difere do valor; as ações da linha viram ícone de 44px no cartão. É a mesma conversão que as specs 0022 e 0023 fizeram nas outras listas.
  • Documentos em 375px: o DocumentsPanel também não tem tratamento de viewport hoje. Cada documento vira linha de uma coluna — nome, tipo e data em cima, ações de 44px embaixo —, e o contrato assinado mantém a marcação e a posição de topo.
  • As ações do topo viram ícone de 44px, como manda o PageHeader (specs 0016 e 0020): a de registrar assinatura preenchida, as outras contornadas. Três alvos e o título cabem — é a medida que a spec 0020 fez.
  • A faixa de status quebra em linhas, não encurta. Ela carrega as frases mais longas da tela ("Vigente de 01/08/2026 a 31/07/2027 · assinada em 09/08"), e é a primeira coisa que se lê: em 375px o selo fica na primeira linha e a frase desce para a segunda e a terceira se precisar. Encurtar o texto no estreito daria duas verdades diferentes para a mesma garantia dependendo do aparelho.
  • A data da assinatura usa o seletor nativo do aparelho (type="date"), não digitação mascarada: é uma data recente e conhecida, e o calendário do sistema resolve em dois toques sem teclado — que é o que mais atrapalha em 375px.

A lista: vira RecordCard com número, tomador, situação e valor garantido — os quatro que decidem se é esta a garantia procurada. Eles caem nos slots code, title, status e amount do componente, sem consumir as três linhas de meta, que ficam livres.

Aqui, sim, coisa some — e é decisão: imóvel, mensalidade e vigência não aparecem no cartão, só no detalhe a um toque. Saem porque nenhum dos três decide qual garantia é esta: o imóvel é longo demais para caber legível em 375px, e mensalidade e vigência são resposta, não triagem. É a única coisa da spec que o celular não mostra, e está dita aqui para ninguém tentar caber sete campos num cartão de três linhas.

A situação no cartão vem inteira — Aguardando · 12 dias, não só o selo. O celular é a tela de triagem, e a espera é o que esta spec existe para tornar visível: cortar o tempo justamente ali seria devolver a lista ao problema.

As ações da linha sobrevivem, no slot de ações do cartão: "encerrar" como ícone de 44px, pelo mesmo motivo do desktop; "ver" some, porque tocar o cartão já abre. A busca e o filtro de estado colapsam em MobileFilterBar/FilterPill, e são os dois que existem — o MobileFilterBar exige a busca, e ela é a operação de quem atende uma ligação com o nome do inquilino na mão. O ListPagination acompanha, na forma compacta da spec 0022.

Os três diálogos em 375px — os três são tela cheia, e os três têm o mesmo problema: com o teclado aberto sobram ~180px (medida da spec 0037). O ConfirmDialog compartilhado é maxWidth="sm" e não é tela cheia hoje; passa a sê-lo em 375px, como o diálogo de devolução da spec 0037 já é — é o mesmo componente servindo o mesmo viewport, e duas respostas para isso seria o começo de mais uma divergência.

  • Registrar contrato assinado — dois campos, arquivo e data. O botão de confirmar fica ancorado ao rodapé, não rola com o conteúdo. O seletor de arquivo abre a câmera ou os arquivos do aparelho, que é como o PDF assinado chega a quem está fora do escritório. Os dois caminhos aparecem como dois botões de largura inteira, empilhados — "Enviar arquivo" e "Escolher um já anexado" —, e não como o par de rádio do desktop: em 375px dois alvos de 44px são mais fáceis de acertar que dois rádios de 20px. O segundo vem desabilitado com o motivo abaixo ("Nenhum documento anexado ainda") quando a garantia não tem anexo, e ao ser tocado abre a lista como tela cheia própria, com alvos de 44px — não como um Select espremido.
  • Emitir garantia — é o mais apertado, porque cresce nesta spec: quatro campos mais o resumo do que será congelado. Em 375px o resumo não fica ao lado, fica abaixo dos campos e acima do rodapé ancorado, colapsado por padrão num "Ver o que será congelado" que abre em quatro linhas. Quem emite pelo celular ainda vê os números antes de confirmar — só não os vê ao mesmo tempo que digita, porque não cabe. Exceção obrigatória: se o resumo não puder ser calculado, ele abre sozinho e não colapsa, porque é ali que está o motivo de o botão estar desabilitado — deixá-lo fechado é o "botão cinza sem motivo" que esta spec condena no desktop.
  • Encerrar garantia — um campo, e o motivo agora é obrigatório. Tela cheia, rodapé ancorado, e o Alert de erro acima do campo. É o menor dos três e mesmo assim precisa do tratamento: um multiline com teclado aberto num diálogo de 560px de altura empurra o confirmar para fora da tela.

O que não aparece no celular: no detalhe, nada — muda a ordem, o que vem aberto e a forma (tabela → cartão). Na lista, saem três colunas do cartão (imóvel, mensalidade, vigência), pelo motivo dito acima; elas continuam a um toque, no detalhe.

PWA: nenhuma tela nova entra no cacheado offline e nenhum asset muda. A garantia é dado que muda de estado — servir versão velha diria "aguardando" para uma garantia já vigente, que é pior que não abrir. Sem mudança no service worker.

7.3 Estados

Situação O que a tela mostra A saída
Lista vazia "Nenhuma garantia ainda. Elas nascem da análise de crédito aprovada." link para a lista de análises
Filtro sem resultado "Nenhuma garantia aguardando assinatura." — e é boa notícia, não erro botão para limpar o filtro
Carregando o detalhe Skeleton nas seções, mantendo a altura — a tela não salta quando os dados chegam —
Carregando a lista no desktop, linhas de Skeleton na tabela, como hoje; em 375px, três RecordCard de Skeleton com a mesma altura do cartão real. A barra de busca e o filtro ficam visíveis e utilizáveis durante o carregamento — travá-los faz quem já sabe o que procura esperar duas vezes —
A garantia ou a lista não carrega Alert de erro no lugar do conteúdo, com o que houve "Tentar de novo", que refaz a busca sem recarregar a página
Sem permissão para emitir o botão não aparece; nada de botão cinza que ninguém sabe por que está cinza —
Sem permissão para registrar a assinatura idem — a ação não aparece. É o caso da imobiliária, que passa a ler a garantia da própria carteira e não pratica nenhuma escrita sobre ela —
Sem permissão para encerrar idem —
Sem permissão para ver a garantia 404, nunca 403 — confirmar que o identificador existe já é informação (specs 0034/0035) volta para a lista
Erro ao emitir Alert dentro do diálogo, acima dos campos, e o diálogo não perde o que foi digitado. Não é modal de Erro: o ui-patterns.md reserva o modal para falha de operação já concluída, e aqui a pessoa ainda está no formulário e vai corrigir e reenviar corrigir e confirmar de novo
Análise sem vigência definida a análise guarda início e fim como opcionais. Sem eles, o diálogo abre com os dois campos vazios e obrigatórios, e o resumo do congelamento fica em branco até que sejam preenchidos — nunca com zero ou com uma data inventada, que é o que faria alguém congelar um valor sem perceber preencher a vigência
Resumo do congelamento indisponível se os números não puderem ser calculados ao abrir o diálogo (parâmetros da garantidora ausentes ou fora de faixa), o bloco do resumo mostra o motivo no lugar dos valores — "Não foi possível calcular com os parâmetros atuais" — e o botão de emitir fica desabilitado: congelar valor que ninguém viu é o que esta spec existe para não fazer link para os parâmetros da garantidora
Data da assinatura no futuro validação inline no campo, sem esperar o envio: "A assinatura não pode ser depois de hoje." corrigir a data
Erro ao encerrar Alert dentro do diálogo, acima do campo; o motivo digitado não se perde. Mesmo tratamento das outras duas escritas tentar de novo
Encerrar sem motivo erro no campo, com o botão ainda habilitado: "Diga por que esta garantia está sendo encerrada." O ConfirmDialog compartilhado tem essa mensagem cravada em uma frase só — "Diga o que precisa ser corrigido ou anexado.", escrita para a devolução de análise da spec 0036 —, e reusá-la aqui faria quem encerra ler uma instrução sem relação com o que fez. A mensagem passa a vir de quem chama escrever o motivo
Garantia encerrada ou vencida a seção Encerramento abre o detalhe (só na encerrada), com motivo, quem e quando. Somem as ações de estado — registrar assinatura, encerrar — e o botão de nova cobrança fica desabilitado com o motivo. As ações de cobrança por linha continuam valendo: registrar pagamento e marcar em atraso funcionam em qualquer estado, porque dívida emitida não some porque a garantia acabou. A guarda desta spec é sobre criar cobrança, nunca sobre baixá-la —
Erro ao registrar assinatura mesmo tratamento; o arquivo escolhido continua escolhido tentar de novo
Cobrança recusada por estado a tela não deixa chegar lá: o botão está desabilitado com o motivo à vista. Se a recusa vier da API assim mesmo, aparece no painel de cobranças, não numa faixa no topo da página —
Garantia sem número guarantee_number é anulável, e garantias anteriores à spec 0027 têm prefixo C. Sem número, o cabeçalho mostra Garantia sem número e a coluna da lista fica vazia — nunca um id truncado disfarçado de número, que é o defeito que esta spec veio tirar. A busca por número simplesmente não as encontra buscar pelo tomador
Upload recusado no diálogo de assinatura o seletor do diálogo herda os limites do painel de documentos — 10 MB, formatos da lista, e o teto de 20 documentos por registro —, com a mesma mensagem de cada caso. O diálogo não fecha e o resto do que foi preenchido fica escolher outro arquivo
Garantia sem imobiliária (carteira própria) a seção Partes mostra tomador e imóvel, e diz "Sem imobiliária — carteira própria" em vez de campo vazio —
Aguardando com vigência proposta vencida a faixa mantém Aguardando assinatura e acrescenta: "A vigência proposta terminou em 14/08/2026. Encerre esta garantia." A ação de registrar assinatura fica desabilitada com esse motivo encerrar — não há "emitir outra": a análise já tem garantia

Diferença entre viewports: uma só, e é de posição, não de componente. O Alert de erro dos diálogos aparece acima do primeiro campo nos dois; em 375px, onde o diálogo é tela cheia, isso significa colado ao topo da tela — senão fica atrás do teclado.

8. Revisão da experiência

Rubrica .claude/rubrics/experiencia.md, revisor de contexto limpo e novo a cada rodada.

Rodada Critical Important Minor Veredicto
1 0 6 6 reprovada
2 0 5 4 reprovada
3 0 5 5 reprovada
4 0 3 7 reprovada
5 0 3 5 reprovada — achados corrigidos sem 6ª rodada

Resultado: aprovada com decisão registrada do usuário, após a rodada 5.

Por que parou na 5ª. O limite da régua é 3, estourado com autorização do usuário. A contagem de Important caiu de 6 para 3 e estacionou — a régua manda parar quando a nota não sobe, e foi o que se fez. Os três Important da última rodada foram corrigidos sem nova revisão: dois eram consequência direta de decisões que o usuário tomou durante o próprio laço (a imobiliária passar a ler, e a correção do registro de assinatura), e o terceiro era um erro factual meu que o código resolve sozinho. Essas correções não foram revisadas.

O que este portão achou que a Parte I não tinha como achar. Descrever a tela é o que revela o que a regra esconde:

  1. A imobiliária não tem guarantees:read. Eu havia escrito que ela "vê a garantia e não a ativa" — ela receberia 404 e nunca chegaria à tela. Virou decisão de acesso: ela passa a ler a própria carteira, com uma tabela de o que enxerga de cada seção.
  2. A tela mostra dois identificadores fabricados (#260815.1432 do created_at e o id truncado) enquanto a API já tem guarantee_number. A jornada exige o número real — o termo enviado ao inquilino o carrega —, e os dois códigos saem.
  3. "Emitir outra" era impossível. Ofereci essa saída duas vezes; há uma garantia por análise, com unicidade no banco. As saídas reais são registrar assim mesmo ou encerrar.
  4. Registrar assinatura era irreversível num diálogo que aceita escolher entre 20 anexos sem filtro. Virou "Corrigir contrato assinado", sem mexer no estado.
  5. Três diálogos, não dois — encerrar ganhou campo obrigatório e não estava descrito em 375px; e o ConfirmDialog tem a mensagem de motivo obrigatório cravada num texto escrito para a devolução de análise da spec 0036.

Decisões do usuário registradas neste portão:

Decisão Escolha
Garantia não assinada cuja vigência acabou Continua Aguardando, com aviso — não vira Vencida
Baixar cobrança em garantia vencida/encerrada Continua valendo — a guarda é só sobre criar
Identidade da garantia O guarantee_number da API; os dois códigos fabricados saem
A imobiliária lê a garantia? Sim, escopada à própria carteira, sem nenhuma escrita
O que ela vê dos números Valor garantido, LMI, mensalidade, vigência e cobranças. Não vê taxa aplicada, composição da entrada, notas nem motivo de encerramento
Registro de assinatura errado Corrigível enquanto Vigente, sem mexer no estado

Pendências aceitas: nenhuma. Todos os achados das cinco rodadas foram corrigidos.

Decisão revertida depois, no portão do detalhamento. "O que ela vê dos números" foi revisto na seção 16: a imobiliária já lê taxa aplicada, mensalidade e total de entrada pelo resumo da garantia embutido em GET /credit-analyses/{id}. A restrição decidida aqui era inexequível sem mexer numa jornada que esta spec não toca, e o usuário optou por aceitar. Ficam fora só notes e termination_reason, que não vazam por outra rota. A tabela de 7.1 foi corrigida; a Parte II não foi rerrevisada depois dessa correção.

O que a Parte II deixa anotado para a Parte III:

  • A tabela de o que a imobiliária vê é decisão de tela; aplicá-la no contrato da API é da seção 10. Esconder na tela não protege a rota (specs 0034 e 0035).
  • SUSPENDED sai do filtro de estado por ser estado morto. Se a Parte III o ressuscitar, o filtro é o primeiro lugar que muda.
  • O colapso do MobileDetailSection é comportamento novo do componente compartilhado, não uso do que existe — decidido assim para não criar um terceiro Collapse local.
  • O ConfirmDialog precisa aceitar a mensagem de motivo obrigatório de quem chama.
  • A lista de garantias sai do limit: 200 com filtro em memória para paginação e ordenação no servidor.

Parte III — Detalhamento

Não preencher antes das revisões das seções 6 e 8 estarem aprovadas.

9. Regras de negócio

O ciclo de vida

  • RN1. GuaranteeStatus passa a ter três valores gravados: PENDING_SIGNATURE (Aguardando assinatura), ACTIVE (Vigente) e TERMINATED (Encerrada). EXPIRED deixa de ser valor gravável e vira estado derivado (RN4). SUSPENDED sai do enum — nenhum caminho do sistema chega nele desde que existe, e estado morto foi o que criou a confusão que esta spec conserta.
  • RN2. Toda garantia nasce em PENDING_SIGNATURE. A emissão não aceita status no corpo — quem emite não escolhe o estado.
  • RN3. Transições, e são as únicas: PENDING_SIGNATURE → ACTIVE (registrar contrato assinado), PENDING_SIGNATURE → TERMINATED (encerrar antes de assinar), ACTIVE → TERMINATED (encerrar). Não há volta de TERMINATED, e não há transição para EXPIRED porque ele não é gravado. Transição inválida → 422 INVALID_TRANSITION.
  • RN4. Vencida é derivada, não gravada. O estado efetivo de uma garantia é EXPIRED quando status == ACTIVE e end_date < hoje; nos demais casos é o próprio status. Uma função única — estado_efetivo(guarantee) — é a única fonte desse cálculo, e é ela que a resposta da API, o filtro da lista, a ordenação e a guarda de cobrança consultam.

Por que derivada e não gravada: não existe agendador neste sistema — nenhum job, nenhum cron, nada que rode sozinho. Um EXPIRED gravado precisaria de alguém para movê-lo, e é exatamente por não ter esse alguém que o EXPIRED de hoje nunca foi usado. Gravar um estado que nada move é reconstruir o defeito com nome novo.

Consequência que não pode ser esquecida: o alerta GUARANTEE_EXPIRED_ACTIVE do painel de operações consulta end_date < hoje AND status == ACTIVE — que com esta regra passa a ser a definição de "vencida", não de "erro". O alerta é removido (RN20).

  • RN4a. Na resposta da API, status é o estado efetivo — é ele que vale para quem consome, e é o que o filtro ?status= aceita. O valor gravado não é exposto: dois campos de status na mesma resposta seriam duas verdades, e quem consome escolheria a errada. Ao lado dele vai proposed_period_expired: bool (RN5), que só é true em PENDING_SIGNATURE com end_date passada.

  • RN4b. O cálculo existe em duas formas, e é um teste que as mantém iguais. RN4 pede Python (para serializar) e RN18/RN19 pedem SQL (para filtrar, ordenar e paginar no banco) — uma função Python não entra em WHERE. As duas moram no mesmo módulo, lado a lado: estado_efetivo(guarantee) e estado_efetivo_sql(), esta devolvendo a expressão CASE correspondente. Um teste percorre todas as combinações de status × end_date e exige que as duas concordem — sem ele, "fonte única" seria só uma frase, e a regra divergiria no primeiro ajuste.

  • RN4d. "Hoje" é a data em America/Sao_Paulo, fixo — não UTC, e não parâmetro do tenant: não há campo de fuso em Tenant nem em TenantSettings, e criar um para esta spec seria inventar configuração que ninguém pediu. Num container UTC, comparar sem fuso faria uma garantia com end_date de hoje virar EXPIRED às 21h de Brasília — e a RN12 recusaria a cobrança legítima do último dia de vigência.

Um helper único (hoje_local()) serve as duas formas do cálculo, e o estado_efetivo_sql() compara contra a mesma data, passada como parâmetro — não contra CURRENT_DATE do banco, que é UTC. A CA17f prova a concordância em status × end_date; falta-lhe a hora de virada, e é por isso que ela ganha um caso às 23h UTC de um dia em que em São Paulo ainda é o dia anterior. - RN4c. A ordem de classificação de estado, quando alguém ordena pela coluna Situação: PENDING_SIGNATURE < ACTIVE < EXPIRED < TERMINATED — do que espera ação ao que está encerrado. - RN5. Passada a end_date de uma garantia em PENDING_SIGNATURE, ela continua em PENDING_SIGNATURE — vencer rebaixa só quem estava vigente. A resposta da API traz proposed_period_expired: true para a tela mostrar o aviso. Não há transição automática, e não há prazo que a encerre.

O registro da assinatura

  • RN6. Registrar o contrato assinado exige um documento e uma data, e é o único ato que leva a garantia a ACTIVE. Anexar documento pelo painel — por qualquer papel que o alcance — não muda estado nenhum.
  • RN7. O documento informado precisa ser um Document desta garantia (guarantee_id == :id), resolvido no escopo do solicitante (seção 10.3, que aplica a RN5 da spec 0034). Documento de outra garantia, de outra entidade ou de outro tenant → 404, nunca 403.
  • RN8. signed_at não pode ser futura → 422 SIGNED_AT_IN_FUTURE. Pode ser anterior à emissão: o inquilino pode ter assinado antes de o PDF chegar, e nada mais muda com isso.
  • RN8a. Não se ativa fora do período. Registrar a assinatura é recusado quando a end_date já passou → 422 PROPOSED_PERIOD_EXPIRED. Sem isso a garantia nasceria ACTIVE e a RN4 a devolveria EXPIRED no mesmo instante — e a RN12 proibiria criar qualquer cobrança, deixando setup e primeira parcela impagáveis pelo sistema, contra a promessa da Parte I de que "a guarda atrasa a cobrança, não a elimina". Uma garantia cuja vigência acabou antes de começar não vira vigente: a saída é encerrar.

Consequência na Parte II: o aviso da faixa de status passa a oferecer uma saída, não duas — "A vigência proposta terminou em 14/08/2026. Encerre esta garantia." — e a ação de registrar assinatura fica desabilitada com esse motivo. - RN9. Registrar a assinatura não altera a vigência. start_date e end_date foram congelados na emissão e permanecem. Se signed_at cai fora do intervalo, a garantia fica ACTIVE do mesmo jeito — a tela explica (Parte II), a regra não muda. - RN10. Corrigir o registro troca o documento e/ou a data e não mexe no estado. Só é permitido com o estado efetivo ACTIVE — o que exclui, pela RN4, tanto a encerrada quanto a que já passou da end_date. Fora disso, 422 SIGNATURE_LOCKED. Cada correção é auditada com autor e horário. - RN11. Os campos do registro são novos na Guarantee: signed_document_id (FK → documents, nulo), signed_at (date, nulo) e signed_by (FK → users, nulo). Em PENDING_SIGNATURE os três são nulos; e o caminho novo só chega a ACTIVE preenchendo os três. Não é invariante de banco — sem CHECK e sem NOT NULL condicional: as garantias que já estão ACTIVE quando a migration roda ficam com os três nulos, e inventar um backfill seria gravar uma data de assinatura que ninguém tem. ACTIVE com assinatura nula significa "vigente de antes desta spec": a tela omite o "· assinada em" (Parte II) e a correção do registro (RN10) é o caminho de conserto — ela aceita preencher pela primeira vez. O mesmo vale para terminated_at: as garantias já encerradas quando a migration roda ficam sem data, e a faixa de status omite o "em 14/08/2026" em vez de inventá-la — a updated_at não serve, porque mente na primeira edição posterior. A FK não é a proteção do arquivo — quem protege é a RN13c, porque a exclusão de documento neste sistema é lógica e nunca chega ao banco.

A guarda de cobrança

  • RN12. Criar cobrança só é aceito com o estado efetivo ACTIVE. Nos demais — PENDING_SIGNATURE, EXPIRED (derivado) e TERMINATED — a API responde 422 GUARANTEE_NOT_ACTIVE, com o estado atual na mensagem.
  • RN13. Baixar cobrança já emitida — registrar pagamento, marcar em atraso — continua valendo em qualquer estado. Dívida emitida não some porque a garantia acabou, e a Parte I pôs a régua de pagamento fora de escopo. A guarda da RN12 é sobre criar, e só.
  • RN13a. Editar cobrança já emitida (PATCH .../billings/{id} — vencimento, valor) segue a RN13, não a RN12: continua valendo em qualquer estado. Corrigir o vencimento de uma parcela de garantia encerrada é conserto de dado, não cobrança nova. Os três verbos (PATCH, pagar, marcar em atraso) compartilham billings:update com o POST que a RN12 trava — a guarda é por operação, não por permissão, e é por isso que ela mora no serviço e não no porteiro.

Escopo de cobrança e de documento

  • RN13b. GET /guarantees/{gid}/billings/{bid} passa a verificar o escopo de imobiliária. Hoje ele resolve a cobrança por get_by_guarantee_and_id, que filtra só por tenant_id — sem within_agency, ao contrário do list. Conceder billings:read ao AGENCY_USER sem essa correção deixaria a Imobiliária B ler a cobrança da A com os dois UUIDs em mãos. Este vazamento seria criado por esta spec, e é ela que o fecha: fora do escopo → 404.
  • RN13c. O documento marcado como contrato assinado não pode ser excluído enquanto o estado efetivo for ACTIVE ou EXPIRED → 422 SIGNED_DOCUMENT_IN_USE. A vencida entra de propósito: o prazo de acionamento de sinistro corre depois do fim da vigência, e o contrato assinado é o primeiro documento que alguém procura num sinistro. Só em TERMINATED a exclusão é aceita. A exclusão é irreversível — DocumentService.delete zera o content além de marcar deleted_at.
  • RN13c-bis. A mesma guarda vale para substituir o conteúdo (PUT /documents/{id}/content, da spec 0037) do documento marcado como contrato assinado → 422 SIGNED_DOCUMENT_IN_USE. O PATCH de metadados continua permitido — classificar o tipo, corrigir o nome e anotar não mexem na prova, e travá-los deixaria sem tipo, por toda a vigência, justamente o documento que a Parte II decidiu não obrigar a classificar antes.

Sem isto, proteger só a exclusão não protege nada. replace_content troca o arquivo preservando o registro — mesmo id, mesmo vínculo, mesmo signed_document_id —, e o AGENCY_USER alcança essa rota na garantia da própria carteira: tem documents:update, e _owned_by_agency inclui Document.guarantee_id, então o documento está no escopo dele. A garantia continuaria ACTIVE e faturável com outra prova por baixo.

Isto não é a porta de upload que a seção 5 aceita deixar aberta: aquela se apoia em "anexar não move estado", verdadeiro para POST /documents. Aqui o estado já se moveu, e o que muda é a prova que o sustenta. O ondelete="RESTRICT" da FK não protege: DocumentService.delete é soft delete e ainda zera o content, então o banco nunca é acionado e a garantia ficaria vigente apontando para um arquivo vazio. A guarda é no serviço de documentos, não na FK.

Notas

  • RN13d. Editar notas passa a exigir a permissão nova guarantees:annotate, em vez de guarantees:update. Hoje update carrega notas e encerrar; separar é o que permite o OPERATOR anotar "termo enviado em 12/08" sem ganhar o poder de encerrar.
  • RN13e. Notas são editáveis com o estado efetivo PENDING_SIGNATURE ou ACTIVE. Em EXPIRED e TERMINATED → 422 GUARANTEE_CLOSED_FOR_NOTES. A regra é do servidor, não da tela — a Parte II a descreveu como comportamento de interface, e guarda só de interface é o que esta spec condena.

Encerramento

  • RN14. Encerrar exige motivo não vazio → 422 TERMINATION_REASON_REQUIRED. Hoje o campo é opcional; passa a ser obrigatório, porque é o que a auditoria procura.
  • RN15. Encerrar é permitido em qualquer estado gravado que não seja TERMINATED — o que inclui PENDING_SIGNATURE, ACTIVE e a vencida (cujo gravado ainda é ACTIVE). Em TERMINATED → 422 INVALID_TRANSITION.

A API permite mais do que a tela oferece, e é de propósito. A tabela de ações da Parte II não mostra "Encerrar" numa garantia Vencida, porque ali não há o que interromper. A API aceita porque encerrar é registro de decisão, não interrupção — e recusar obrigaria a garantidora a conviver com uma vencida que ela quis encerrar formalmente.

O resumo do que será congelado

  • RN15a. O resumo exibido no diálogo de emissão usa exatamente os valores que a emissão vai congelar: a taxa e a taxa de setup aprovadas na decisão da análise (approved_guarantee_rate_percent, approved_initial_fee_value), caindo nos padrões da garantidora só quando a decisão não os traz, e o pacote locatício da análise (financial_rental_package).
  • RN15b. POST /guarantees/simulate não serve para isso, e usá-lo seria o defeito. Ele calcula com settings.guarantee_rate_percent e com o initial_fee_* do tenant — os padrões —, enquanto create congela o que foi aprovado. Numa análise aprovada com taxa negociada de 8%, o diálogo mostraria 10% e o sistema gravaria 8%: a tela cumpriria a letra da Parte II e falharia justamente no que ela existe para impedir.
  • RN15c. O resumo vem de POST /guarantees/simulate estendido para aceitar credit_analysis_id: com ele, a simulação resolve a análise no escopo do solicitante e usa a decisão aprovada; sem ele, segue com os padrões do tenant, como hoje. É um campo opcional, não um endpoint novo — e um teste exige que a simulação com credit_analysis_id devolva os mesmos números que a emissão daquela análise congela.

Identidade

  • RN16. O guarantee_number já é exposto pela API (GuaranteeResponse); o que muda é o cliente — o tipo Guarantee do apps/web passa a declará-lo, e ele vira o identificador que a interface mostra. Os dois códigos fabricados pelo apps/web (guaranteeCode do created_at e id.slice(-8)) são removidos.
  • RN17. guarantee_number continua anulável (garantias anteriores à spec 0027 têm prefixo C, e o campo é nulo em registros antigos). Nulo → a interface mostra "Garantia sem número" e a busca por número não as alcança. Não se gera número retroativo.

Busca, ordenação e listagem

  • RN18. A lista aceita busca por guarantee_number e por nome do tomador, e filtro por estado efetivo. Busca e filtro são resolvidos no banco, não em memória.
  • RN18a. days_waiting conta de created_at — a data de emissão — até hoje, em dias inteiros. É nulo fora de PENDING_SIGNATURE: em ACTIVE, EXPIRED e TERMINATED não há espera a medir, e devolver zero seria confundir "não espera" com "espera há pouco".
  • RN19. Paginação e ordenação são do servidor. A ordenação padrão é: PENDING_SIGNATURE primeiro, entre si por created_at crescente (a que espera há mais tempo no topo); depois os demais por created_at decrescente. Ordenáveis: estado, valor garantido, mensalidade e vigência.

Limpeza que esta spec faz

  • RN19a. "Cobranças" e "Sinistros" não aparecem no menu do AGENCY_USER, apesar de as permissões novas os condicionarem. É regra de exposição, não de conveniência: são módulos que a garantidora vai operar, e anunciá-los ao canal é a classe que a spec 0035 pagou. A condição é no nav-groups.tsx, além da permissão, e vale também na barra inferior do mobile.
  • RN20. GUARANTEE_EXPIRED_ACTIVE é removido dos dois lugares em que existe: o alerta e o item da fila de trabalho (priority=2, "requer atualização de status"), ambos alimentados pela mesma query. Removê-lo só do alerta deixaria a fila mandando a pessoa atualizar um status que passou a se atualizar sozinho. A condição que ele detecta passa a ser a definição de "vencida" (RN4), e um alerta crítico para um estado normal é ruído que treina a pessoa a ignorar o painel.
  • RN21. Os contadores do painel de operações passam a usar o estado efetivo, e sem isso eles mentem: hoje total_active e expiring_soon contam status == ACTIVE e total_terminated conta [TERMINATED, EXPIRED]. Com EXPIRED deixando de ser gravado, as vencidas seriam contadas como ativas e sumiriam das encerradas. Passa a valer: total_active = efetivo ACTIVE; total_terminated = efetivo TERMINATED ou EXPIRED; expiring_soon = efetivo ACTIVE com fim próximo. E há um quarto, fácil de esquecer porque não está no mesmo bloco: in_guarantee, dos contadores de análise, também filtra status == ACTIVE — com a garantia nascendo PENDING_SIGNATURE, "análises em garantia" deixaria de contar as recém-emitidas, e passa a contar ACTIVE ou PENDING_SIGNATURE. Entra ainda um contador novo, total_pending_signature — é o número que a Parte I diz que ninguém consegue perguntar hoje.

10. Autorização e isolamento

10.1 Matriz de papéis

Papel Pode Não pode
ADMIN_GLOBAL nada de novo. Continua alcançando as rotas de garantia por receber todas as permissões — é dívida conhecida, tratada em 10.5 —
ADMIN_TENANT emitir, registrar assinatura, corrigir registro, encerrar, ler tudo, criar/baixar cobrança —
ANALYST ler garantia e cobranças da própria garantidora emitir · registrar assinatura · corrigir · encerrar · criar cobrança
OPERATOR emitir (novo), registrar assinatura (novo), corrigir registro (novo), editar notas (novo), ler tudo encerrar — permanece só do gestor: é irreversível · criar cobrança (não muda)
VIEWER ler garantia e cobranças da própria garantidora toda escrita, inclusive notas
AGENCY_USER ler (novo) garantia e cobranças da própria imobiliária, com os campos financeiros que já lhe são visíveis pela análise (10.6) emitir · registrar assinatura · corrigir · encerrar · criar cobrança · baixar cobrança · editar notas · ver notes, termination_reason e terminated_by

O negativo que vira teste: o ANALYST recebe 403 ao tentar emitir no próprio tenant; o OPERATOR recebe 403 ao tentar encerrar; o AGENCY_USER da Imobiliária B recebe 404 ao abrir a garantia da Imobiliária A e 404 ao abrir a cobrança dela pelo id; e o AGENCY_USER que abre a própria garantia recebe 200 sem notes e sem termination_reason.

10.2 Permissões

Endpoint Permissão Observação
GET /guarantees guarantees:list + AGENCY_USER
GET /guarantees/{id} guarantees:read + AGENCY_USER
POST /guarantees guarantees:create + OPERATOR
POST /guarantees/{id}/signature guarantees:sign (nova) ADMIN_TENANT, OPERATOR
PUT /guarantees/{id}/signature guarantees:sign (nova) idem
POST /guarantees/{id}/terminate guarantees:update inalterada — não vai para o OPERATOR
PATCH /guarantees/{id} (notas) guarantees:annotate (nova) ADMIN_TENANT, OPERATOR — sai do update (RN13d)
POST /guarantees/simulate sem credit_analysis_id guarantees:read + AGENCY_USER por consequência. Calcula com os padrões da garantidora, como hoje — nada que ela já não alcance
POST /guarantees/simulate com credit_analysis_id guarantees:create o parâmetro novo exige a permissão de emitir, não a de ler (ver abaixo)
POST /guarantees/{id}/billings billings:update inalterada; ganha a guarda da RN12
GET /guarantees/{id}/billings billings:list + AGENCY_USER
GET /guarantees/{id}/billings/{bid} billings:read + AGENCY_USER, e ganha escopo de imobiliária (RN13b)
DELETE /documents/{id} documents:delete inalterada; ganha a guarda da RN13c
GET /operations-center/* operations_center:read inalterada; a resposta muda (RN20/RN21) e ganha o campo total_pending_signature. O AGENCY_USER não tem essa permissão e nada disso a alcança
GET /credit-analyses/{id} credit_analyses:read inalterada; o status do resumo da garantia passa a poder ser PENDING_SIGNATURE (11)
POST /documents documents:create inalterada — usada pelo caminho "Enviar arquivo" do diálogo
GET /documents?entity_type=guarantee documents:list inalterada — usada pelo caminho "Escolher um já anexado"
GET /documents/{id}/content documents:read inalterada — download pelo painel que a tela nova reordena
PUT /documents/{id}/content documents:update inalterada; ganha a guarda da RN13c-bis
PATCH /documents/{id} documents:update inalterada e sem guarda nova — metadados não são a prova (RN13c-bis)
PATCH /guarantees/{id}/billings/{bid} · POST .../pay · POST .../mark-overdue billings:update inalteradas quanto ao porteiro; seguem a RN13a (a guarda de estado não se aplica) e ganham o escopo de imobiliária pela correção do _get_billing (RN13b)

Os dois de documento não abrem nada: ADMIN_TENANT e OPERATOR, os únicos com guarantees:sign, já têm as duas permissões. Estão aqui porque esta tabela é o inventário do que a feature toca, e um endpoint usado e não listado é o que faz a próxima revisão refazer o levantamento.

O credit_analysis_id do simulate exige guarantees:create, e o motivo é uma janela de tempo que quase passou batido. A justificativa fácil seria "ela já vê a precificação pelo resumo da garantia na análise" — e ela é falsa antes da emissão: aquele resumo é null enquanto não há garantia, e o que a análise mostra até lá é o snapshot dos padrões do tenant, não a taxa negociada. A taxa negociada vive na Decision, atrás de decisions:read, que a imobiliária não tem. Sem esta regra, o parâmetro novo entregaria a ela a taxa e o setup aprovados assim que a análise fosse aprovada — e esta spec ainda alarga essa janela, porque a garantia agora nasce aguardando assinatura e pode esperar semanas. Exigir guarantees:create fecha isso sem tocar em mais nada: quem simula com a análise é quem emite.

Permissões novas — duas, e cada uma é exigida por endpoint real (permissão declarada e não usada é pior que ausente, spec 0035):

  • ("guarantees", "sign", "Registrar contrato assinado") — exigida por POST e PUT /guarantees/{id}/signature. Vai para ADMIN_TENANT e OPERATOR.
  • ("guarantees", "annotate", "Editar notas da garantia") — exigida por PATCH /guarantees/{id}. Vai para ADMIN_TENANT e OPERATOR. Sai do update, que passa a servir só ao encerrar.

Por que sign e não update: guarantees:update hoje serve para editar notas e para encerrar. Pendurar a ativação nela daria ao OPERATOR, junto, o poder de encerrar — que a matriz nega de propósito.

Mudanças no rbac_seed.py:

Papel Ganha
OPERATOR guarantees:create, guarantees:sign, guarantees:annotate
ADMIN_TENANT guarantees:sign, guarantees:annotate
AGENCY_USER guarantees:list, guarantees:read, billings:list, billings:read

VIEWER e ANALYST não ganham guarantees:annotate — hoje eles também não têm guarantees:update, então a separação não muda nada para eles. É registro para o portão não concluir que a permissão nova alargou alguém por omissão.

10.3 Isolamento por garantidora (tenant)

  • credit_analysis_id (corpo de POST /guarantees) — resolvido só por tenant hoje (get_by_id(..., tenant_id=...), sem within_agency). Não é furo hoje — nenhum AGENCY_USER tem guarantees:create, e o OPERATOR que esta spec traz não tem imobiliária —, mas também não é ponto fechado, e quem implementar não deve copiá-lo como modelo: a FK nova do simulate, logo abaixo, é o caso em que o mesmo padrão vazaria.
  • document_id (corpo de POST/PUT /signature) — FK nova vinda do corpo, e é o risco novo desta spec. Resolvida por resolve_in_scope e verificada contra guarantee_id == :id antes de gravar. Documento de outro tenant, de outra garantia ou de outra entidade → 404. Sem essa dupla verificação, informar o id de um documento alheio responderia "existe" ou "não existe" — leitura por escrita, que é a RN5 da spec 0034.
  • credit_analysis_id (corpo de POST /guarantees/simulate, novo pela RN15c) — é a segunda FK nova desta spec, e a mais perigosa, porque chega junto com um chamador novo: o AGENCY_USER passa a alcançar o simulate por consequência do guarantees:read. Resolvida por resolve_in_scope — tenant e imobiliária, não só tenant —, respondendo 404 fora do escopo.

O precedente citado acima não serve aqui, e é o ponto: o credit_analysis_id do POST /guarantees é resolvido só por tenant hoje (get_by_id(..., tenant_id=...), sem within_agency), o que é inofensivo porque nenhum AGENCY_USER tem guarantees:create. Copiar esse padrão para o simulate deixaria a Imobiliária B informar o id de uma análise da A e receber a taxa e o setup negociados com o cliente da concorrente — dado que hoje ela não alcança, porque CreditAnalysisService._get_authorized aplica within_agency e responde 404. É a mesma classe da RN13b: conceder permissão sem olhar o que ela abre. - Garantia de outro tenant → 404, nunca 403, em todos os endpoints. 403 fica para o que o papel não pode fazer no próprio tenant (o ANALYST emitindo, o OPERATOR encerrando).

10.4 Isolamento por imobiliária

  • O AGENCY_USER alcança apenas garantias cujo partner_agency_id é o dele. Qualquer outra → 404. O vínculo vem do usuário, nunca do corpo nem da query.
  • GET /guarantees para ele é filtrado no banco pelo partner_agency_id, não filtrado depois — contagem e paginação também mentem quando o filtro é de aplicação.
  • Cobranças herdam o escopo da garantia — nas duas rotas. GET /guarantees/{id}/billings já responde 404 para garantia fora da imobiliária. GET .../billings/{bid} não: ele resolve por get_by_guarantee_and_id, que filtra só por tenant_id. Como esta spec dá billings:read ao AGENCY_USER, ela criaria o vazamento se não o fechasse — e fecha (RN13b). É o achado mais importante deste portão, e nasceu de conceder uma permissão sem olhar o que ela abre.
  • Documentos herdam o escopo do dono (RN13 da 0034): nem metadado, nem download, nem contagem para garantia alheia. O signed_document_id só é resolvível dentro do escopo.

A porta que fica aberta, e está declarada: o AGENCY_USER já consegue anexar documento em garantia da própria carteira (documents:create + guarantee como entity_type válido + a janela da imobiliária que cai num else: return). Esta spec não fecha essa porta — decisão registrada na seção 5 — e ela deixa de importar para o estado porque anexar não ativa (RN6). O que precisa ser garantido é que o registro da assinatura permaneça inalcançável para ela: guarantees:sign não entra no AGENCY_USER, e há CA negativo para isso.

10.5 Plataforma × cliente (SaaS admin)

  • Quando quem chama é ADMIN_GLOBAL, o que estes endpoints devolvem? Garantia é dado de negócio do cliente, não de plataforma — o SaaS admin não deveria alcançá-la. Hoje ele alcança, porque recebe todas as permissões ({f"{r}:{a}" for r, a, _ in _PERMISSIONS}) e não tem filtro de tenant. Esta spec não conserta isso, e declara: a dívida é anterior, vale para todos os endpoints de negócio (inquilino, imóvel, análise, cobrança), e resolvê-la só para garantia daria a falsa impressão de que a fronteira existe. O que esta spec garante é não piorar: a permissão nova guarantees:sign entra no ADMIN_GLOBAL pela mesma regra automática de hoje, e nenhum endpoint novo aceita tenant_id de fora (RN7 da 0034).
  • A tela do SaaS admin (/app/admin/*) não muda. Nenhuma contagem, coluna ou item novo.

10.6 O que a imobiliária recebe da resposta

A decisão da seção 8 foi revertida aqui, e o motivo é do código. Lá se decidiu que o AGENCY_USER não veria taxa aplicada nem a composição da entrada. Este portão mostrou que ela já lê esses números hoje, por outra rota: CreditAnalysisResponse embute CreditAnalysisGuaranteeSummary, que traz guarantee_rate_percent, monthly_value, total_upfront, lmi_value e guaranteed_value — e ela tem credit_analyses:read. Omitir na tela da garantia enquanto o mesmo dado sai pela análise entregaria uma fronteira que não existe, e faria o CA passar verde com o dado vazando.

O que vale: o AGENCY_USER recebe a garantia da própria carteira com os mesmos campos financeiros que qualquer papel da garantidora — não há schema reduzido de precificação, e POST /guarantees/simulate (que exige guarantees:read) deixa de ser um problema pela mesma razão.

Dois campos continuam fora, e não é precificação:

Campo Por quê
notes texto interno da garantidora, escrito para si — não vaza por nenhuma rota hoje, e abri-lo seria exposição nova, não reconhecimento de uma existente
termination_reason · terminated_by o motivo pelo qual a garantidora encerrou é registro interno e de auditoria; a imobiliária vê que foi encerrada e quando
Billing.notes o mesmo argumento, no objeto vizinho. A cobrança tem campo de notas escrito só por quem tem billings:update — ou seja, só a garantidora —, e ele guarda o que ela anota sobre a operação, às vezes sobre a própria imobiliária ("está segurando o repasse"). Conceder billings:read sem tirar esse campo entregaria isso ao canal

Esses dois são omitidos na serialização, não escondidos na tela — a lição das specs 0034 e 0035: campo que só some no apps/web continua no JSON, e o JSON é o que vaza.

E omitir é a chave ausente, não o valor nulo. O endpoint escolhe o response_model pelo papel — um schema próprio para o AGENCY_USER —, em vez de anular os campos num schema único: com "notes": null no JSON, o CA15 ("a resposta não contém") passaria verde com o campo presente, e a próxima pessoa a mexer no serializador não teria como saber que o null era proposital.

A dívida que fica declarada: o vazamento da precificação pela análise não é criado por esta spec e não é consertado por ela. Fechá-lo exigiria mexer no CreditAnalysisGuaranteeSummary, que a tela da análise da imobiliária já consome — é mudança de contrato numa jornada que esta spec não toca. Registrado em Riscos aceitos (seção 15) com a decisão do usuário.

11. Impacto por app

apps/api

Modelos e migration

  • Guarantee: signed_document_id (FK → documents, ondelete="RESTRICT", nulo), signed_at (Date, nulo), signed_by (FK → users, ondelete="RESTRICT", nulo) e terminated_at (DateTime(timezone=True), nulo).

terminated_at é coluna nova, e é fácil não perceber: o model tem terminated_by e termination_reason e não tem a data. A Parte II promete "Encerrada em 14/08/2026" na faixa e o "quando" na seção Encerramento, e a 10.6 promete à imobiliária que ela vê que foi encerrada e quando. Reusar updated_at mentiria na primeira edição posterior, e ler o AuditLog para pintar uma faixa de status é caro e frágil. São quatro colunas novas, não três. - GuaranteeStatus (o enum Python): remove SUSPENDED, acrescenta PENDING_SIGNATURE, e mantém EXPIRED — ele deixa de ser gravado mas continua sendo um valor válido do tipo, porque é o que a API devolve como estado efetivo (RN4a). - No PostgreSQL, o tipo guarantee_status fica com PENDING_SIGNATURE, ACTIVE, EXPIRED e TERMINATED: SUSPENDED é removido do tipo, EXPIRED permanece ainda que nenhuma linha o use — tirá-lo obrigaria a recriar o tipo de novo no downgrade, e o ganho de remover um rótulo não gravado é zero. - Migration xxxx_guarantee_signature_and_states: - upgrade: cria as quatro colunas; converte o enum (ALTER TYPE/recriação, conforme o dialeto); migra os dados — SUSPENDED → TERMINATED (não deve haver linhas; se houver, é o destino honesto) e EXPIRED → ACTIVE (passa a ser derivado, e a end_date o reconstrói). - downgrade: real, e exercitado — devolve o enum ao conjunto anterior mapeando PENDING_SIGNATURE → ACTIVE (perde a distinção, que é o preço de voltar) e derruba as quatro colunas. Desde a spec 0028 este é o caminho de volta de um deploy ruim.

Endpoints — ver a seção 12 para request/response.

Método Path Permissão
POST /guarantees/{id}/signature guarantees:sign
PUT /guarantees/{id}/signature guarantees:sign

Os demais são alterações nos existentes: POST /guarantees deixa de aceitar estado e nasce em PENDING_SIGNATURE; GET passam a devolver estado efetivo, guarantee_number e os campos de assinatura; POST /guarantees/{id}/billings ganha a guarda da RN12; POST /terminate passa a exigir motivo.

Regras no service

  • estado_efetivo(guarantee) em guarantee_service.py — fonte única da RN4, consumida pelo próprio serviço, pelo billing_service e pela query de listagem.
  • GuaranteeService.register_signature / update_signature — resolvem document_id no escopo e contra a garantia (10.3), validam signed_at (RN8), gravam os três campos e auditam.
  • BillingService.create — passa a consultar o estado efetivo antes de instanciar (RN12). É a única mudança no billing_service: pay e mark_overdue não são tocados (RN13).
  • operations_center_service — remove o alerta GUARANTEE_EXPIRED_ACTIVE e a query dele (RN20), e corrige os contadores para o estado efetivo (RN21): total_active, total_terminated, expiring_soon e o novo total_pending_signature. Sem isso as vencidas passariam a ser contadas como ativas.
  • document_service.delete — guarda da RN13c: recusa excluir o documento que é o contrato assinado de uma garantia ACTIVE.
  • billing_service._get_billing — passa a aplicar within_agency, como o _get_guarantee já faz (RN13b).
  • schemas/credit_analysis.py — CreditAnalysisGuaranteeSummary.status passa a receber o estado efetivo (RN4a), o mesmo da tela de garantia; a descrição do campo, que lista os status antigos, é atualizada. Sem isso a análise mostraria "Ativa" para uma garantia que a tela da garantia mostra "Vencida" — duas verdades para o mesmo objeto, em duas telas que a mesma pessoa abre em sequência. A tela da análise (credit-analyses/show.tsx) renderiza esse status com map={GUARANTEE_STATUS} e precisa do rótulo novo, senão mostra a chave crua.

Testes — o caminho feliz e os negativos que sustentam a seção 10:

  • transição: emitir nasce PENDING_SIGNATURE; registrar leva a ACTIVE; registrar duas vezes → 422; encerrar de TERMINATED → 422
  • estado_efetivo: ACTIVE com end_date passada devolve EXPIRED; PENDING_SIGNATURE com end_date passada continua PENDING_SIGNATURE com proposed_period_expired
  • guarda de cobrança: criar em cada um dos quatro estados; baixar em cada um dos quatro (todos aceitos — é o teste que protege a RN13 de ser "consertada" por engano)
  • acesso: ANALYST emitindo → 403; OPERATOR encerrando → 403; AGENCY_USER registrando assinatura → 403; AGENCY_USER da Imobiliária B lendo garantia da A → 404; AGENCY_USER lendo a própria → 200 sem os campos de 10.6
  • isolamento da FK nova: document_id de outra garantia → 404; de outro tenant → 404

apps/web

Páginas e componentes

  • pages/guarantees/show.tsx — reescrita para o padrão de detalhe do ui-patterns.md: faixa de status, Encerramento (condicional), Cobertura, Partes, Vigência e entrada, Sinistro, Notas, Cobranças, Documentos.
  • pages/guarantees/index.tsx — FilterBar + SearchField + FilterSelect, oito colunas, ListPagination, ordenação e busca no servidor. Remove o guaranteeCode (RN16).
  • pages/guarantees/components/SignatureDialog.tsx — novo: registrar e corrigir.
  • pages/guarantees/components/BillingList.tsx — botão de nova cobrança desabilitado com motivo fora de ACTIVE; vira RecordCard em 375px.
  • components/shared/DocumentsPanel.tsx — marca o contrato assinado e o põe no topo; linha de uma coluna em 375px.
  • components/shared/MobileDetailSection.tsx — ganha colapso com contagem no cabeçalho.
  • components/shared/ConfirmDialog.tsx — a mensagem de motivo obrigatório passa a vir de quem chama (hoje é uma frase cravada, escrita para a spec 0036); tela cheia em 375px.
  • pages/credit-analyses/components/GuaranteeCreateDialog.tsx — resumo do que será congelado, colapsado em 375px e aberto quando indisponível.
  • pages/guarantees/api.ts — o tipo Guarantee passa a declarar guarantee_number, o estado efetivo, proposed_period_expired, os campos de assinatura e os objetos aninhados; e a lista passa a ler o envelope paginado em vez de uma lista nua.
  • pages/credit-analyses/api.ts — há uma segunda união de GuaranteeStatus aqui, com SUSPENDED e sem PENDING_SIGNATURE, na tela que consome o resumo da garantia. Fácil de esquecer, e o tsc só acusa se alguém comparar com o valor novo.
  • components/shared/StatusBadge.tsx — GUARANTEE_STATUS ganha PENDING_SIGNATURE ("Aguardando assinatura", warning) e perde SUSPENDED. O mapa é compartilhado: a tela da análise de crédito também o usa para o resumo da garantia, e sem o rótulo novo ela mostraria a chave crua.

data-testid novos

guarantee-signature-button · guarantee-signature-correct-button · guarantee-signature-dialog · guarantee-signature-file-input · guarantee-signature-existing-choice · guarantee-signature-date-input · guarantee-signature-confirm · guarantee-signature-cancel · guarantee-status-banner · guarantee-section-{cobertura,partes,vigencia,sinistro,encerramento} · guarantees-search · guarantees-status-filter · guarantees-pagination · guarantee-billing-create-disabled · guarantee-terminate-reason-input

Sider — e aqui o sider muda, ao contrário do que parecia. "Garantias" já existe em nav-groups.tsx condicionado a guarantees:list, e passa a aparecer para o AGENCY_USER sem código novo, o que é o desejado. Mas as permissões novas acendem mais dois itens que ninguém pediu:

Item Condição hoje O que acontece
Cobranças billings:list, comingSoon passaria a aparecer para ela
Sinistros guarantees:list, comingSoon passaria a aparecer para ela

Os dois ficam escondidos para o AGENCY_USER — condição extra no nav-groups.tsx, além da permissão. É a classe da spec 0035: o menu não vaza dado pela API e ainda assim anuncia ao canal os módulos que a garantidora vai operar. Ela vê as cobranças dentro da garantia, que é onde elas lhe dizem respeito. buildNavTabs (a barra inferior do mobile) segue a mesma regra. App.tsx não muda.

Menu e rota por permissão: quem digitar a URL de uma garantia fora do escopo recebe 404 da API e a tela mostra o estado "sem permissão para ver" — esconder o menu não protege a rota (spec 0035).

Porteiros de tela — nomeados, porque hoje não existe nenhum. Não há um só can( em pages/guarantees/show.tsx nem em index.tsx; o único do fluxo é can("guarantees:create") na tela da análise. Com o AGENCY_USER passando a abrir essas telas, cada ação passa a ser condicionada: registrar e corrigir → guarantees:sign; editar notas → guarantees:annotate; encerrar → guarantees:update; nova cobrança → billings:update. São conveniência, não proteção — a proteção é a da seção 10.

Responsividade e PWA: conforme 7.2 — colapso das sete seções, RecordCard para lista e cobranças, três diálogos em tela cheia com rodapé ancorado, ícones de 44px. PWA sem mudança: nenhuma tela nova entra no cache offline e nenhum asset muda.

apps/e2e

Specs (tests/e2e/emissao-e-assinatura-da-garantia.spec.ts):

  • emitir → nasce Aguardando assinatura, sem cobrança possível
  • registrar contrato assinado → vira Vigente, cobrança liberada
  • corrigir o registro → troca o documento, estado inalterado
  • encerrar sem motivo → erro no campo; com motivo → Encerrada
  • lista: filtro por estado, busca por número e por tomador, ordenação da espera
  • AGENCY_USER: lê a própria garantia, não vê notas nem motivo de encerramento, não tem nenhuma ação de escrita, e não vê "Cobranças" nem "Sinistros" no menu
  • 375px: detalhe não rola na horizontal, faixa de status visível sem rolar, diálogo com rodapé ancorado

Seeds: uma garantia em cada estado no tenant de e2e, criadas pelo caminho real (API) e removidas no afterEach — o tenant é compartilhado, e a lição da spec 0025 vale.

12. Contrato de API

Registrar o contrato assinado

POST /api/v1/guarantees/{id}/signature
Authorization: Bearer <token>          Permissão: guarantees:sign

{ "document_id": "uuid", "signed_at": "2026-08-09" }

Responde o GuaranteeResponse inteiro, com os aninhados — a tela acabou de mudar o estado e precisa repintar a faixa, as ações e o botão de cobrança; devolver um objeto reduzido obrigaria a um segundo GET logo em seguida. signed_by vem no mesmo formato dos demais:

{
  "id": "uuid",
  "guarantee_number": "G202608-00042",
  "status": "ACTIVE",
  "signed_at": "2026-08-09",
  "signed_document_id": "uuid",
  "signed_by": { "id": "uuid", "name": "Ana Souza" }
}
Erro Código Quando
403 — papel sem guarantees:sign no próprio tenant
404 — garantia de outro tenant ou de outra imobiliária; ou document_id que não é desta garantia
422 INVALID_TRANSITION garantia não está em PENDING_SIGNATURE
422 SIGNED_AT_IN_FUTURE signed_at > hoje
422 PROPOSED_PERIOD_EXPIRED a end_date já passou — não se ativa fora do período (RN8a)

Corrigir o registro

PUT /api/v1/guarantees/{id}/signature
{ "document_id": "uuid", "signed_at": "2026-08-10" }

Os dois campos são opcionais e ao menos um é obrigatório — manda-se o que muda. Corpo vazio ou com os dois nulos → 422 NOTHING_TO_CORRECT.

Exceção, e é a que conserta o legado: numa garantia ACTIVE sem assinatura registrada (as anteriores à migration, RN11), os dois campos são obrigatórios — não se preenche pela metade um registro que nunca existiu, e permitir gravar signed_at sem documento produziria exatamente o estado que a RN11 diz ser impossível.

Erros: 403, 404 e SIGNED_AT_IN_FUTURE como no POST; INVALID_TRANSITION não se aplica (a pré-condição aqui é o oposto), e no lugar dele vale 422 SIGNATURE_LOCKED quando o estado efetivo não é ACTIVE (RN10).

Criar cobrança — a guarda nova

POST /api/v1/guarantees/{id}/billings
Erro Código Quando
422 GUARANTEE_NOT_ACTIVE estado efetivo é PENDING_SIGNATURE, EXPIRED ou TERMINATED

Encerrar

POST /api/v1/guarantees/{id}/terminate
{ "reason": "texto obrigatório" }
Erro Código Quando
422 TERMINATION_REASON_REQUIRED motivo vazio ou só espaço
422 INVALID_TRANSITION já encerrada

Listar

GET /api/v1/guarantees?status=PENDING_SIGNATURE&search=Maria&skip=0&limit=25&sort=status

skip/limit, como todo o resto do repositório — não page/per_page. Chaves aceitas em sort, com - para descendente: status (na ordem da RN4c), guaranteed_value, monthly_value, end_date. Qualquer outra → 422.

GuaranteeListItem — o que cada item traz, e nada além: id, guarantee_number, status (efetivo), days_waiting (nulo fora de PENDING_SIGNATURE), guaranteed_value, monthly_value, start_date, end_date, lessee_name e property_summary. Os dois últimos vêm de join com a análise — o tomador por primary_lessee_id, o imóvel por credit_analysis.property_id —, resolvidos na mesma query, não em N+1.

Resposta paginada. O item de lista não traz notes, termination_reason nem terminated_by para papel nenhum — não cabem numa linha de tabela, e por isso a omissão da 10.6 não muda nada aqui: para o AGENCY_USER o item de lista é idêntico. A redução dela vale no detalhe.

Simular — estendido, e é o endpoint mais sensível desta spec

POST /api/v1/guarantees/simulate           Permissão: guarantees:read

{ "credit_analysis_id": "uuid",
  "start_date": "2026-08-01", "end_date": "2027-07-31",   ← opcionais
  "guaranteed_value": "3000.00" }                          ← opcional

Com credit_analysis_id, as condições (taxa e setup aprovados) e a base (financial_rental_package) vêm da análise.

Corrigido na implementação, por decisão do usuário. Esta seção dizia que guaranteed_value sobrepõe o que a pessoa digita — e isso contradiz a emissão, que desde a spec 0006 usa o Pacote Locatício da análise e ignora o valor informado. As duas não cabiam juntas, e a revisão da fase apps/api levou a escolha ao usuário: o Pacote manda, nos dois. Consequência para a Parte II: o campo "valor garantido" do diálogo não recalcula o resumo, e a tela precisa dizer isso — ou o campo sai. Só a vigência sobrepõe, e ela sobrepõe mesmo, porque o prazo entra no cálculo do setup

A vigência é o campo que sobrepõe, e existe porque a Parte II promete o resumo "recalculado quando se mexe na vigência" — e o prazo entra no cálculo de verdade: com initial_fee_base = TOTAL_GUARANTEE, o setup depende dos meses. Sem a sobreposição, alguém que muda a vigência de 12 para 24 meses veria o setup de 12 e o sistema congelaria o de 24 — o mesmo defeito da RN15b, por outra porta.

A simulação espelha o create, campo a campo: as mesmas datas produzem os mesmos meses, e o mesmo guaranteed_value cai no mesmo financial_rental_package or guaranteed_value. É o que a CA17i mede.

A resposta ganha guarantee_rate_percent — a taxa efetiva (a aprovada, ou o padrão do tenant quando a decisão não a traz). Hoje GuaranteeSimulateResponse não a devolve, e a taxa só é legível pelo parecer, que exige decisions:read — que o OPERATOR, o novo emissor, não tem. Sem esse campo, o resumo que a Parte II exige não teria fonte para o único papel que esta spec traz para a emissão; a alternativa seria conceder decisions:read a ele, que é mudança de acesso feita para pintar um número.

Sem credit_analysis_id, o contrato de hoje segue igual (guaranteed_value e guarantee_months obrigatórios, padrões do tenant).

Erro Código Quando
404 — análise de outro tenant ou de outra imobiliária
422 ANALYSIS_NOT_APPROVED a análise não está APPROVED — não há decisão aprovada de onde tirar taxa e setup
422 — guarantee_months junto com start_date/end_date: duas formas de dizer o prazo

Ler uma garantia — o contrato que a tela nova exige

GET /api/v1/guarantees/{id}                Permissão: guarantees:read
{
  "id": "uuid",
  "guarantee_number": "G202608-00042",
  "status": "PENDING_SIGNATURE",
  "proposed_period_expired": false,
  "start_date": "2026-08-01", "end_date": "2027-07-31",
  "guaranteed_value": "3000.00", "lmi_value": "36000.00",
  "rental_package": "3000.00", "guarantee_rate_percent": "10.00",
  "monthly_value": "300.00",
  "initial_fee": "3000.00", "first_month_upfront": "300.00", "total_upfront": "3300.00",
  "claim_grace_period_days": 30, "claim_trigger_days": 60,
  "signed_at": null, "signed_document_id": null, "signed_by": null,
  "notes": "texto interno",
  "terminated_at": null, "termination_reason": null, "terminated_by": null,
  "created_at": "2026-08-03T14:32:00Z",
  "days_waiting": 12,
  "credit_analysis_id": "uuid",
  "lessee": { "id": "uuid", "name": "Maria Silva", "document": "…", "email": "…", "phone": "…" },
  "property": { "id": "uuid", "address": "…", "city": "…", "state": "…", "property_type": "RESIDENTIAL" },
  "partner_agency": { "id": "uuid", "name": "Imobiliária Alfa" }
}

Quatro decisões de contrato que este bloco toma, e que não estavam em lugar nenhum:

  1. A resposta passa a aninhar lessee, property e partner_agency. Hoje ela devolve só ids, e a garantia não guarda property_id — o imóvel é alcançado pela análise. Sem aninhar, a tela da Parte II faria três chamadas a cada abertura para montar a seção Partes, e o mobile pagaria isso em rede. O serviço resolve o imóvel via credit_analysis.property_id.
  2. rental_package entra na resposta. Está gravado na garantia desde a spec 0006 e nunca foi exposto; a seção Cobertura da Parte II o mostra.
  3. status é o efetivo (RN4a), e proposed_period_expired vem ao lado. O gravado não aparece.
  4. signed_by e terminated_by vêm com o nome, não só o UUID — {"id": "…", "name": "Ana Souza"}, como os demais aninhados. É obrigatório e não é conveniência: a Parte II promete "Encerrada em 14/08/2026 por Ana Souza", e nem o OPERATOR nem o AGENCY_USER têm users:read — os dois papéis que esta spec traz para dentro desta tela não conseguiriam resolver o nome. A alternativa seria conceder users:read a eles, que é mudança de acesso que ninguém pediu para pintar um rótulo.

Para o AGENCY_USER, a mesma resposta sem notes, termination_reason e terminated_by (10.6) — e com todo o resto, inclusive os financeiros.

Os objetos aninhados não abrem nada novo: todo papel com guarantees:read já tem renters:read e properties:read no seed, então lessee e property não expõem a ninguém o que ele já não alcançasse por duas chamadas.

Listar devolve PaginatedResponse[GuaranteeListItem] — o envelope {items, total, skip, limit} de schemas/common.py, com skip/limit, que é o padrão do repositório inteiro. Isto é quebra de contrato: GET /guarantees devolve hoje uma lista nua, e o apps/web a consome assim. A troca está no plano da seção 14, e é barata porque o único cliente é o apps/web deste monorepo.

Migration: xxxx_guarantee_signature_and_states — descrita em 11.

13. Critérios de aceite

Marcados no fechamento do /spec implement. A evidência de cada um é: a suíta da API (426 verdes), o spec e2e emissao-e-assinatura-da-garantia.spec.ts (12 verdes contra o dev-remote reconstruído), e a conferência de cobertura da revisão da branch inteira, que leu teste por teste procurando RN sem prova.

A rodada 3 da revisão de branch encontrou CA4, CA7 e CA20 marcados sem teste que os segurasse — os três ganharam um, com red-green. A marcação abaixo só vale porque isso foi conferido teste a teste, e não por leitura do código.

Um fica aberto, e está declarado: o CA17k só é exercitado pelo lado do admin — falta o AGENCY_USER dono da carteira tentando substituir o contrato assinado. A guarda é no serviço e não olha papel, então o risco é baixo; a pendência está na seção 17.

O ciclo

  • [x] CA1. Emitida uma garantia, ela consta como Aguardando assinatura e nenhuma cobrança existe para ela.
  • [x] CA2. Numa garantia Aguardando assinatura, POST /billings responde 422 GUARANTEE_NOT_ACTIVE.
  • [x] CA3. Registrado o contrato assinado (documento + data), a garantia passa a Vigente e o mesmo POST /billings é aceito.
  • [x] CA4. Registrar a assinatura não altera start_date nem end_date.
  • [x] CA5. signed_at no futuro → 422 SIGNED_AT_IN_FUTURE.
  • [x] CA6. Registrar assinatura numa garantia já Vigente → 422 INVALID_TRANSITION; corrigir a mesma → aceito, e o estado continua Vigente.
  • [x] CA7. Uma garantia Vigente com end_date no passado é devolvida pela API com estado efetivo EXPIRED, sem que nada tenha rodado.
  • [x] CA8. Uma garantia Aguardando com end_date no passado continua Aguardando e traz proposed_period_expired: true.
  • [x] CA9. Em garantia Vencida e Encerrada, registrar pagamento de cobrança já emitida é aceito — só a criação é recusada.
  • [x] CA10. Encerrar sem motivo → 422 TERMINATION_REASON_REQUIRED.

Autorização — um negativo por papel afetado

  • [x] CA11. OPERATOR emite garantia com sucesso (novo) e recebe 403 ao tentar encerrar.
  • [x] CA12. ANALYST recebe 403 ao emitir e ao registrar assinatura.
  • [x] CA13. VIEWER recebe 403 em toda escrita e 200 na leitura.
  • [x] CA14. AGENCY_USER da Imobiliária B recebe 404 ao abrir a garantia da A.
  • [x] CA15. AGENCY_USER abre a própria garantia com 200, e a resposta não contém notes, termination_reason nem terminated_by — e contém os campos financeiros, que ela já lê pela análise (10.6).
  • [x] CA16. AGENCY_USER recebe 403 ao tentar registrar assinatura na própria garantia.
  • [x] CA17. document_id de outra garantia (mesmo tenant) → 404; de outro tenant → 404.
  • [x] CA17a. AGENCY_USER da Imobiliária B, com os dois UUIDs em mãos, recebe 404 em GET /guarantees/{id-da-A}/billings/{billing-da-A}. Este é o teste que prova a RN13b — sem ele, a permissão nova abriria a carteira alheia.
  • [x] CA17b. O item "Cobranças" e o item "Sinistros" não aparecem no menu do AGENCY_USER, nem na barra inferior em 375px, apesar de ele ter as permissões que os condicionam.
  • [x] CA17c. OPERATOR edita as notas com sucesso (guarantees:annotate) e recebe 403 ao tentar encerrar (guarantees:update). VIEWER recebe 403 nas duas.
  • [x] CA17d. Editar notas de garantia encerrada → 422 GUARANTEE_CLOSED_FOR_NOTES — pela API, não só pela tela desabilitada.
  • [x] CA17e. Excluir o documento marcado como contrato assinado de uma garantia ACTIVE → 422 SIGNED_DOCUMENT_IN_USE. Depois de encerrada, a exclusão é aceita.
  • [x] CA17g. AGENCY_USER da Imobiliária B chama POST /guarantees/simulate com credit_analysis_id de uma análise da Imobiliária A → 404. É o teste que impede esta spec de vazar a taxa negociada da concorrente.
  • [x] CA17h. Registrar assinatura numa garantia cuja end_date já passou → 422 PROPOSED_PERIOD_EXPIRED, e a garantia continua Aguardando assinatura.
  • [x] CA17i. A simulação com credit_analysis_id devolve exatamente os números que a emissão daquela análise congela — taxa e setup aprovados, não os padrões do tenant.
  • [x] CA17l. AGENCY_USER abre GET /guarantees/{id}/billings da própria garantia e a resposta não contém notes da cobrança.
  • [x] CA17m. AGENCY_USER chama POST /guarantees/simulate com credit_analysis_id da própria análise aprovada → 403 (exige guarantees:create). Sem o parâmetro, 200 com os padrões do tenant.
  • [x] CA17n. Garantia de outro tenant → 404 em todos os endpoints novos e alterados (o CA14 cobre só entre imobiliárias do mesmo tenant).
  • [x] CA17j. AGENCY_USER recebe 403 ao criar cobrança e ao registrar pagamento na própria garantia — a leitura não carrega escrita junto.
  • [ ] CA17k. Substituir o conteúdo (PUT /documents/{id}/content) do documento marcado como contrato assinado de uma garantia ACTIVE → 422 SIGNED_DOCUMENT_IN_USE, inclusive quando quem chama é o AGENCY_USER dono da carteira.
  • [x] CA17f. estado_efetivo em Python e em SQL concordam em todas as combinações de status × end_date (passada, hoje, futura) — é o teste que sustenta a "fonte única" da RN4b.

A tela

  • [x] CA18. A tela da garantia responde, sem sair dela: de quem é, qual imóvel, quanto cobre, quanto custa, até quando vale e está paga.
  • [x] CA19. A lista mostra G202608-00042, e não #260815.1432 nem id truncado.
  • [x] CA20. Filtrando por Aguardando assinatura, a lista mostra há quantos dias cada uma espera, e a mais antiga vem primeiro.
  • [x] CA21. O botão de nova cobrança está desabilitado com o motivo à vista fora de Vigente.
  • [x] CA22. Em 375px, o detalhe não rola na horizontal, a faixa de status é visível sem rolar, e o confirmar dos três diálogos fica na viewport com o teclado aberto.
  • [x] CA23. Em 375px, o cartão da lista mostra número, tomador, Aguardando · 12 dias e valor garantido.
  • [x] CA24. O alerta GUARANTEE_EXPIRED_ACTIVE não existe mais no painel de operações.
  • [x] CA25. No painel de operações, uma garantia com fim no passado conta em encerradas, não em ativas; e uma aguardando assinatura conta no novo total_pending_signature e não em ativas.
  • [x] CA26. Na tela da análise de crédito, o resumo da garantia recém-emitida mostra "Aguardando assinatura", e não a chave crua PENDING_SIGNATURE.

14. Plano de implementação

  • [ ] apps/api — enum e três colunas no model; estado_efetivo no service
  • [ ] apps/api — migration com downgrade exercitado (up, down, up)
  • [ ] apps/api — register_signature / update_signature + resolução da FK no escopo
  • [ ] apps/api — guarda da RN12 no billing_service (e não tocar pay/mark_overdue)
  • [ ] apps/api — motivo obrigatório no terminate; remover o alerta do painel de operações
  • [ ] apps/api — rbac_seed.py: guarantees:sign e guarantees:annotate novas + as concessões de 10.2
  • [ ] apps/api — within_agency no _get_billing (RN13b) — é a correção de segurança que a permissão nova exige, e vai antes de conceder billings:read
  • [ ] apps/api — guarda da RN13c/RN13c-bis em document_service.delete, replace_content e no PATCH de metadados
  • [ ] apps/api — contadores do painel de operações pelo estado efetivo (RN21)
  • [ ] apps/api — resposta de leitura com lessee, property e partner_agency aninhados, e a lista no envelope paginado (quebra de contrato registrada em 12)
  • [ ] apps/api — omissão de notes/termination_reason/terminated_by da garantia e de notes da cobrança para AGENCY_USER (10.6), por response_model próprio — chave ausente, não valor nulo
  • [ ] apps/api — busca, filtro, ordenação e paginação no banco
  • [ ] apps/api — docstrings + Swagger (summary, description, response_description) e os quatro textos que cravam a lista de status antiga: TAGS_METADATA em main.py ("Status possíveis: ACTIVE · SUSPENDED · …"), o description do GET /guarantees, a descrição de GuaranteeResponse.status e a docstring de classe do GuaranteeService. O CLAUDE.md da API torna o TAGS_METADATA obrigatório quando o comportamento do grupo muda
  • [ ] apps/api — testes, o teste da RN antes do código (test-driven-development)
  • [ ] apps/web — api.ts, detalhe, lista, SignatureDialog, painéis e diálogos compartilhados; data-testid em tudo; desktop e 375px no mesmo passo
  • [ ] apps/e2e — spec e seeds
  • [ ] make test e make lint sem apontamento novo nos arquivos tocados
  • [ ] Validado no dev-remote em desktop e 375px

15. Decisões em aberto

Nenhuma. As sete decisões do portão do problema e as seis do portão da experiência estão registradas nas seções 6 e 8, e a Parte III não abriu nenhuma nova.

Riscos aceitos, com o porquê:

  • O ADMIN_GLOBAL continua alcançando dado de negócio do cliente (10.5). Dívida anterior a esta spec, que vale para todos os endpoints de negócio; consertá-la só aqui daria falsa impressão de fronteira. Esta spec garante não piorar. — aceito pelo usuário que pediu a spec, durante a redação da Parte III; registrado na seção 16 ao fechar o portão.
  • A porta de upload do AGENCY_USER em garantia continua aberta (10.4). Deixa de importar para o estado porque anexar não ativa; fechá-la tem efeito fora desta spec. — aceito pelo usuário no portão do problema, ao decidir separar upload de ativação.
  • downgrade perde a distinção PENDING_SIGNATURE × ACTIVE (11). É o preço de voltar um deploy: sem o estado novo, não há onde guardar a diferença.
  • A imobiliária lê a precificação da garantia (taxa aplicada, mensalidade, total de entrada). Não é vazamento criado aqui — ela já lê pelo resumo embutido em GET /credit-analyses/{id}, que a tela dela consome. Decisão do usuário: aceitar, em vez de esconder na tela da garantia uma coisa que sai pela análise. Fechar exigiria mexer no CreditAnalysisGuaranteeSummary, numa jornada que esta spec não toca.
  • notes e termination_reason ficam fora da resposta dela, e essa é a única redução — são os dois campos que não vazam hoje por rota nenhuma, e abri-los seria exposição nova em vez de reconhecimento de uma existente.

16. Revisão do detalhamento

Rubrica .claude/rubrics/detalhamento.md, revisor de contexto limpo e novo a cada rodada.

Rodada Critical Important Minor Veredicto
1 2 7 5 reprovada
2 0 6 9 reprovada
3 1 3 7 reprovada
4 0 3 5 reprovada
5 0 2 8 reprovada — achados corrigidos sem 6ª rodada

Resultado: aprovada com decisão registrada do usuário, após a rodada 5.

Por que cinco rodadas. O limite da régua é 3, estourado com autorização do usuário. Este é o portão que a régua manda medir com mais rigor, e foi o que mais rendeu: três Critical ao todo, e os três eram vazamento de dado entre imobiliárias. Os Important caíram de 7 para 2 e os dois últimos foram fechados por decisão do usuário.

Os três Critical, e o que cada um teria custado:

  1. A promessa de esconder a precificação da imobiliária era ficção (rodada 1). Ela já lê taxa, mensalidade e total de entrada pelo resumo da garantia embutido na resposta da análise. O CA passaria verde com o dado vazando. Levou à reversão da decisão da seção 8.
  2. GET /guarantees/{id}/billings/{bid} não tem escopo de imobiliária (rodada 1) — ele resolve a cobrança só por tenant. Conceder billings:read ao AGENCY_USER, como esta spec faz, abriria a carteira da concorrente para quem tivesse os dois UUIDs. Este vazamento seria criado por esta spec, e é ela que o fecha (RN13b), com a correção antes da concessão no plano.
  3. O simulate estendido entregaria a taxa negociada da concorrente (rodada 3) — eu havia acrescentado credit_analysis_id ao corpo e, na mesma spec, dado o endpoint à imobiliária. Mesma classe do anterior, um passo além: permissão nova e parâmetro novo chegando juntos.

E um quarto, que só a rodada 5 pegou: a justificativa "ela já vê a precificação mesmo" tem uma janela de tempo. O resumo da garantia na análise é null antes da emissão; até lá a taxa negociada vive na Decision, atrás de decisions:read, que ela não tem. E esta spec alarga essa janela, porque a garantia passa a esperar assinatura por semanas. Por isso o credit_analysis_id no simulate exige guarantees:create.

Decisões do usuário registradas neste portão:

Decisão Escolha
O que a imobiliária vê da precificação Tudo — a restrição da seção 8 era inexequível; só notes e termination_reason ficam fora
Notas da garantia para o OPERATOR Permissão própria guarantees:annotate — anota sem poder encerrar
Ativação depois do fim da vigência Recusada (422) — evita a garantia vigente e impagável
Menu da imobiliária Só "Garantias"; "Cobranças" e "Sinistros" ficam escondidos
Notas da cobrança Também ficam fora da resposta dela
simulate com credit_analysis_id Exige guarantees:create — quem simula com a análise é quem emite

Pendências aceitas: nenhuma. Os achados das cinco rodadas foram corrigidos; os da rodada 5 sem nova revisão, por decisão de encerrar o laço. Essas correções não foram revisadas.

Riscos aceitos estão na seção 15, com o decisor nomeado nos três de acesso.


17. Revisão da implementação

Rubrica .claude/rubrics/implementacao.md, revisor de contexto limpo e novo a cada rodada. Uma revisão por fase, e uma sobre a branch inteira no fim.

Fase apps/api

Rodada Critical Important Minor Veredicto
1 1 5 6 reprovada
2 0 0 3 aprovada

O Critical da rodada 1 era GET /guarantees devolvendo 500 em toda chamada — self.db nunca atribuído no __init__ e PaginatedResponse montado com skip/limit em vez de page/page_size/pages. Nenhum teste exercitava o endpoint, que é como ele chegou ao portão. Dois defeitos apareceram fora do revisor, ao exercitar de verdade: o CASE de estado_efetivo_sql() misturando o enum com um bind varchar (invisível no SQLite, erro no PostgreSQL) e o Billing.notes que vazaria para a imobiliária na mesma spec que lhe concede billings:read.

Fase apps/web

Rodada Critical Important Minor Veredicto
1 1 11 10 reprovada
2 0 6 12 reprovada
3 0 3 7 reprovada
4 0 1 7 reprovada
5 1 0 3 reprovada
6 0 1 4 reprovada
7 0 1 5 reprovada
8 0 0 7 aprovada

Oito rodadas, e o limite da régua é três. Elas não repetiram achado: cada rodada trouxe coisa nova e a contagem caiu monotonicamente até a 4ª. O que justificou seguir foi isso — a régua manda parar quando a rodada nova traz os mesmos achados, e não foi o caso.

O Critical da rodada 5 foi criado pela correção da rodada 4. Um useRef acrescentado para rolar a seção de notas ao entrar em edição ficou abaixo dos dois return antecipados de show.tsx: na primeira renderização o hook não era chamado, na segunda era, e o React derruba a tela com "Rendered more hooks than during the previous render". O tsc não pega, o preflight não pega, e a tela era a central da spec. É o argumento mais forte a favor de revisar de novo depois de corrigir — e o motivo de a rodada 6 ter existido.

O Critical da rodada 1 era o resumo do congelamento simulando com o prazo da análise enquanto a emissão congela o do formulário: a tela mostrava o setup de 12 meses e o sistema gravava o de 24. Mesma classe do defeito que a §12 descreve, entrando pela porta da tela.

O Important da rodada 7 era a correção da rodada 6 aplicada no lugar errado. O erro de carregamento devia ocupar o lugar do conteúdo; a guarda entrou na barra de filtros, que tem a mesma abertura {isMobile ? (. Efeito: a lista continuava dizendo "Nenhuma garantia ainda" sob o aviso de erro, e quem filtrou perdeu o filtro. Duas rodadas seguidas em que o defeito novo nasceu da correção anterior — é o custo de corrigir em lote sem reler o ponto de aplicação, e o argumento de por que estas rodadas extras se pagaram.

Padrão que se repetiu, e vale registrar: três dos seis Important da rodada 2 eram afordâncias que contrariavam a Parte II já escrita — "Encerrar" aparecendo em garantia Vencida, a ação de registrar sumindo em vez de ficar desabilitada, a busca sem debounce. A Parte II estava certa; o código é que não tinha sido medido contra ela.

Pendências aceitas da fase apps/web:

  • A lista usa <Table> do MUI, não o DataGrid do padrão da casa. O apps/web/CLAUDE.md fixa @mui/x-data-grid + garantoraDataGridProps como o padrão e diz que "as demais listas devem convergir para cá". Esta lista foi reescrita mantendo <Table>, como as outras quatro. Converter é trabalho de outra spec, e fazê-lo aqui misturaria a migração de padrão com a feature.
  • Os sete Minor da rodada 8 foram corrigidos sem uma 9ª revisão. São eles: os chips de filtro aplicado na lista (sem eles o onClearAll era prop morta), a classificação do erro de cálculo pelo ApiError.code em vez do texto da mensagem, o fallback para mensagem vazia — que travava o resumo em "Calculando…" com o botão cinza e sem motivo —, o cabeçalho do resumo deixando de ser botão quando não há o que colapsar, e três docblocks colados na declaração errada.

Fase apps/e2e

Rodada Critical Important Minor Veredicto
1 0 5 7 reprovada — achados corrigidos, sem 2ª rodada

O spec novo (tests/e2e/emissao-e-assinatura-da-garantia.spec.ts) rodou antes do portão, como manda o Passo 4c: 10 testes, 10 verdes, 39s. Rodar cedo não pega o que o portão pega — dos cinco Important, dois eram testes verdes que não protegiam a regra que nomeavam:

  1. O teste da RN10 afirmava "corrigir troca o documento" e só conferia que o estado seguia ACTIVE. Tirar o onClick que escolhe o anexo deixaria o teste verde: o escolhido já vinha pré-preenchido com o documento errado, a API responderia 200 e a garantia continuaria vigente. Agora ele confere o signed_document_id e a etiqueta na tela.
  2. O teste do filtro por estado limpava a busca antes de filtrar. Assim que o tenant acumulasse mais de uma página de garantias aguardando, a linha não apareceria de qualquer jeito e o toHaveCount(0) passaria com o filtro quebrado — mediria paginação. A busca agora fica preenchida, isolando a linha, e o teste também confere que ela volta com o filtro certo: sem isso, um filtro que zera tudo passaria.

O terceiro que valeu a rodada: o helper hoje() calculava em UTC. Das 21h de Brasília em diante ele preencheria o campo com amanhã, a tela recusaria inline pela RN4d e o caminho feliz ficaria vermelho três horas por dia — a mesma armadilha que o SignatureDialog já tinha evitado, repetida no teste.

Pendências aceitas da fase apps/e2e:

  • O spec não limpa o que cria, e não há como. A seção 11 prometeu remoção no afterEach. Não existe DELETE /guarantees nem DELETE /credit-analyses na API, e apagar inquilino, imóvel ou imobiliária é recusado enquanto houver vínculo (RENTER_HAS_ACTIVE_LINKS e equivalentes) — que é exatamente o caso depois de emitir. O que dava para fazer foi feito: a imobiliária do spec passou a ser uma só, reusada entre execuções, em vez de uma por teste. O cenário concreto que o revisor levantou — o inquilino novo empurrar Locatário da Imobiliária E2E para fora da primeira página em isolamento-imobiliaria.spec.ts — não se sustenta, e isso foi medido, não suposto: os inquilinos criados aqui têm partner_agency_id nulo (SELECT no banco do dev-remote: 9 de 9), então não entram na carteira que aquele teste lê. Nenhum outro spec afirma sobre a primeira página de /guarantees.
  • Os achados desta rodada foram corrigidos sem uma 2ª revisão, e o spec foi reexecutado depois delas: 11 verdes em 35,6s, contra o dev-remote reconstruído.

Revisão da branch inteira

Rodada Critical Important Minor Veredicto
1 1 1 6 reprovada
2 0 3 4 reprovada
3 0 2 6 reprovada
4 0 1 6 reprovada
5 0 1 3 reprovada
6 0 0 4 aprovada

O Critical valeu a revisão sozinho, e nenhuma das três fases o veria. O simulate empurrava o setup arbitrado pelo analista para dentro do motor de preço (initial_fee_value=), enquanto o create o trata como valor final em reais — que é o que o Decision documenta. Em app/services/pricing.py esse parâmetro só significa reais quando initial_fee_type == FIXED; em PERCENTAGE ele é lido como percentual.

Num tenant PERCENTAGE com Pacote de R$ 3.000 e setup arbitrado em R$ 500, o diálogo mostrava R$ 15.000 de taxa de setup e a emissão congelava R$ 500. Trinta vezes, no número que esta spec existe para mostrar antes de congelar — a RN15b entrando por uma terceira porta, depois das duas que as fases já haviam fechado.

Ele sobreviveu às oito rodadas do apps/web e às duas do apps/api porque cada fase olha um lado: a API estava certa isolada, a tela estava certa isolada, e o defeito vive na costura. E o teste que declarava provar a CA17i não o alcançava — a fixture _settings_factory fixava initial_fee_type=FIXED, o único tipo em que os dois tratamentos coincidem por acidente. A CA17i estava marcada como coberta e não estava.

O red-green foi feito: com a correção revertida, o teste novo falha com assert '15000.00' == '500.00'; restaurada, passa. A fixture ganhou **sobrescritas para que um teste possa sair do FIXED.

O Important: LesseeRef.phone e PropertyRef.zip_code eram campos mortos. O Renter tem phone_landline, phone_mobile e phone_whatsapp — nenhum chamado phone; o Property tem cep, não zip_code. Com from_attributes e default declarado, o pydantic não levanta: os campos saíam null sempre. A seção Partes imprimia CPF · email e o telefone do tomador não aparecia em viewport nenhum, com os três preenchidos no cadastro — o oposto exato da terceira dor da Parte I, que é não ter o contato à mão. Resolvido com um model_validator que escolhe celular → WhatsApp → fixo, e um AliasChoices para o CEP.

Os Minor corrigidos: o terminate com corpo {} devolvia o 422 genérico do pydantic em vez de TERMINATION_REASON_REQUIRED (o reason ganhou default vazio, e os três casos passaram a responder igual); _count_guarantees sem chamador foi removido; e guarantee_status saiu de app/services/ para app/domain/ — o credit_analysis_repo importava de serviço, invertendo a seta do Router → Service → Repository → Model. O módulo é regra pura, sem sessão e sem I/O, e serviço e repositório precisam da mesma verdade sobre o estado efetivo.

Ao remover o código morto eu cortei junto dois métodos vivos que ficavam entre ele e o próximo — _count_por_estado_efetivo e _count_encerradas_ou_vencidas —, e a suíte devolveu três AttributeError. Restaurado e refeito cirurgicamente. Fica registrado porque é a terceira vez nesta spec que uma correção em lote nasce com defeito próprio.

A rodada 2 confirmou as correções da 1 — conferiu que o model_validator novo não perde campo, que o AliasChoices resolve por atributo do ORM e por dict, e que o simulate espelha o create nos quatro caminhos (taxa, base, meses, setup). Nenhum Critical, e nenhum defeito nascido da correção — o que quebrou a série de três.

Os três Important dela eram de cobertura e de contrato, não de comportamento:

  1. O Swagger de quatro endpoints alterados não acompanhou o código. O painel ainda anunciava o alerta GUARANTEE_EXPIRED_ACTIVE e o item de fila de prioridade 2 que a RN20 removeu; o POST /billings não mencionava o 422 GUARANTEE_NOT_ACTIVE, que é a guarda central desta spec; e o PUT /content e o DELETE de documentos listavam os erros um a um e omitiam justamente o SIGNED_DOCUMENT_IN_USE. O CLAUDE.md da API trata isso como regra, e o preflight só cobre endpoint novo.
  2. A FK nova do simulate não tinha negativo de escopo. O código estava certo — o que faltava era o teste. O irmão que existia provava o 403 do AGENCY_USER, que dispara no router antes de qualquer resolução de escopo, e por isso não alcançava o que a CA17g diz proteger. O teste novo alcança, e o red-green mostrou o vazamento inteiro: trocando o resolve_in_scope pelo get_by_id(..., tenant_id=...) que o create ainda usa, a resposta vira 200 com "guarantee_rate_percent":"13.00" — a taxa negociada da garantidora vizinha atravessando a fronteira.
  3. A correção de phone/zip_code da rodada 1 não tinha teste que a segurasse, e o modo de falha é silencioso: com from_attributes e default declarado o pydantic entrega null sem levantar nada. Coberto agora; revertida a correção, o teste falha com assert None == '(11) 98765-4321'.

Os Minor corrigidos: responses={200: ...} em endpoints que devolvem 201 — o schema ficava sob um código que nunca é retornado, e o 201 aparecia sem corpo no OpenAPI (conferido no app.openapi() depois); e o docstring de Guarantee.end_date, que ainda dizia "após esta data a garantia vai para EXPIRED", como se houvesse transição, enquanto a docstring da classe logo acima já dizia o contrário. O wiki é gerado daí.

A rodada 3 achou a quarta correção com defeito próprio, e ela era minha. Ao consertar o Swagger da rodada 2, renumerei a tabela de prioridades da fila de trabalho tirando a linha do GUARANTEE_EXPIRED_ACTIVE — mas o priority é campo devolvido ao cliente e impresso na tela pelo WorkQueueItem. O código seguia em 1, 3, 4, 5, 6 e o Swagger passou a dizer 1, 2, 3, 4, 6: documentação de endpoint alterado que não descreve o código, que é exatamente a classe que a rodada 2 tinha ido corrigir. O 2 voltou a ficar vago, com o motivo escrito — renumerar trocaria o significado de um campo público para economizar um buraco.

O outro Important: três regras centrais sem teste que as segurasse, duas delas com o critério de aceite já marcado como atendido. Todas agora têm, e o red-green foi feito nas três:

  • RN4a/CA7 — apagando dados["status"] = efetivo do _to_response, uma garantia vigente com end_date de ontem voltava a sair ACTIVE, e os 426 testes seguiam verdes. Nenhum teste de rota exercitava ACTIVE + vigência passada: o de estado efetivo media a função isolada. O teste novo passa pelo detalhe e pela lista, porque divergirem entre si é o pior sintoma possível.
  • RN19/CA20 — a ordem padrão. Trocando-a por created_at desc, a garantia que espera há três semanas afundava sob as emitidas hoje — literalmente a dor da Parte I — e nada ficava vermelho.
  • RN4c — a ordem de classificação por Situação, que só era exercitada de raspão.

O teste da ordem nasceu instável, e eu escrevi o defeito que este repositório documenta. As três garantias eram criadas na mesma chamada: empatavam no created_at (o SQLite guarda segundo) e o desempate é id desc sobre UUID aleatório. Falhava em 3 de 5 execuções. Só apareceu porque o CLAUDE.md — corrigido nesta mesma spec — passou a mandar rodar cinco vezes em vez de uma. Corrigido empurrando o created_at de uma delas 21 dias para trás, o que também é o cenário real: a espera se mede em dias, não em ordem de inserção. 5 de 5.

Os Minor corrigidos: o aviso da RN20 estava dentro da tabela de alertas, quebrando a renderização da última linha no Swagger; o CA4/RN9 (assinar não mexe na vigência) estava marcado sem asserção nenhuma e ganhou teste; o rótulo CA23 aparecia em dois testes e2e, um deles apontando para o critério errado; o approve do segundo tenant não era conferido, e a falha dele apareceria trinta linhas adiante como 404 != 200; a concordância Python×SQL do estado efetivo só era medida com o relógio no default — agora também às 02h UTC, que é a hora em que um fuso errado se manifesta; e o painel de operações comparava datas em UTC enquanto os contadores de estado usam São Paulo, então entre 21h e meia-noite uma garantia que termina hoje contava como ativa e sumia da janela de "vence em 30 dias".

A rodada 4 procurou a quinta correção-com-defeito e não achou. Ela conferiu as cinco correções da rodada 3 uma a uma — a numeração da fila contra os priority= do serviço, a troca UTC → hoje_local() ponto a ponto (todos os de calendário convertidos, todos os de duração deixados em UTC, porque 48h e 72h são intervalo e não data), e se o teste que altera o modelo no banco depois de chamar a API poderia passar por engano pelo cache de sessão. Não podia: o client compartilha a sessão do teste e a factory é expire_on_commit=False, então o objeto mutado é o que o repositório devolve.

O Important foi o mesmo padrão da rodada 3, num lugar novo: a RN15 sem teste. As onze chamadas a /terminate da suíte encerravam sempre uma garantia aguardando ou vigente, uma vez só — nenhuma encerrava uma vencida, e nenhuma encerrava duas vezes. Removida a guarda de TERMINATED, o segundo encerramento responde 200 e sobrescreve terminated_by, terminated_at e termination_reason: o registro que a RN14 tornou obrigatório "porque é o que a auditoria procura" some sem rastro, num ato que a spec chama de irreversível. O red-green mostrou o apagamento por extenso — "termination_reason":"outro motivo, de outra pessoa" no lugar do original.

Os Minor corrigidos: o GET /guarantees/{id}/billings declarava um objeto onde devolve um array (o list[...] se perdeu quando o response_model virou responses para não recoagir); signed_by/terminated_by ficaram de fora do lote de _carregar_relacionados e faziam um SELECT por item — numa página de 200 encerradas eram 4 + 400 consultas, e a docstring prometia quatro; o rótulo CA25 aparecia em dois testes e2e, os dois apontando para o critério errado (é a CA22 — o mesmo defeito que a rodada 3 corrigiu no CA23, sobrevivendo noutra linha); ANALYSIS_NOT_APPROVED, NO_CALCULATION_BASE e SIGNATURE_INCOMPLETE ganharam teste; e o comentário do caso das 02h UTC passou a dizer o que o bloco de fato prova (que o lado SQL honra o parâmetro em vez de cair no relógio do banco), e não o que ele parecia prometer sobre o fuso.

O NO_CALCULATION_BASE só é alcançável pela análise antiga, sem o snapshot do Pacote: corpo vazio para no schema e devolve VALIDATION_ERROR. O teste encena o caso real zerando a coluna, e confere também que o guaranteed_value de fallback volta a fazer a conta — que é o que torna a emissão dessas análises alcançável pela tela.

A rodada 5 procurou a quinta correção-com-defeito nas mudanças da rodada 4 e também não achou. A única que mexeu em código de produção — os dois usuários entrando no lote de _carregar_relacionados — foi conferida nas três hipóteses que interessavam: o cache não pode existir sem a chave (há um construtor só, e um chamador só), nenhum outro lugar monta o dict, e usuário apagado ou de outro tenant se comporta igual ao caminho antigo, porque nem o get_by_id nem a consulta em lote filtram deleted_at/tenant_id.

O Important foi o mesmo padrão pela terceira rodada seguida — e desta vez sobre a razão de ser da spec. O teste chamado test_operador_emite_e_registra_mas_nao_encerra nunca emitia: a garantia vinha do _emitir, que usa o ADMIN_TENANT, e o OPERATOR só era criado depois, para exercitar assinatura, nota e encerramento. Apagar guarantees:create do OPERATOR no seed deixava as 434 verdes e o e2e também — porque os doze testes de lá logam como gestor. Em uso, o papel que esta spec existe para habilitar tomava 403 na emissão, e o botão sumia da tela da análise. Corrigido com uma emissão de verdade, com o token dele, e red-green isolado.

O primeiro red-green saiu errado e teria mentido. Cortei guarantees:create procurando a linha do OPERATOR de trás para frente e acertei a do ADMIN_TENANT: os dois testes ficaram vermelhos pelo mesmo motivo, e um deles não era o que eu queria provar. Refeito um de cada vez — o do lote falha com TypeError, o da permissão com 403. Red-green que não isola qual regra caiu prova menos do que parece.

Os Minor corrigidos: a consulta em lote de usuários foi reescrita inline quando user_repo.get_many já existia desde a spec 0037, criada exatamente contra isso — seria a terceira cópia da mesma select(User); o ramo de cache usava .get("usuarios", {}) enquanto os três vizinhos acessam por chave, e a assimetria devolveria nomes nulos em toda a lista sem levantar nada; esse ramo agora tem teste, que confere também que lista e detalhe entregam o mesmo objeto; e o OPERATOR_PERMS do test_rbac_security foi atualizado. Não o chame de espelho: a rodada 6 mostrou que ele não é comparado com o rbac_seed em teste nenhum — só alimenta um papel OPERATOR_TEST montado à mão, e já diverge do seed noutro ponto (credit_analyses:resume). Quem protege o seed do OPERATOR é test_operador_emite_e_registra_mas_nao_encerra, que usa o papel real. A constante foi atualizada para não induzir quem a lê a concluir a política errada, e não porque ela guardasse alguma coisa.

A rodada 6 aprovou a branch inteira — 0 Critical, 0 Important, 4 Minor. É a primeira aprovação em seis rodadas, e o revisor disse por extenso que não via motivo para uma sétima: o código está correto e a costura entre os apps também.

O saldo das seis rodadas, e vale registrar como método: elas acharam zero defeitos de comportamento em código de produção. O que acharam, seis vezes, foi a mesma coisa — regra correta sem teste que a segurasse (RN4a, RN19, RN4c, RN15, a permissão de emitir do OPERATOR, e agora a quarta cláusula da RN21). Três delas estavam com o critério de aceite marcado como atendido. A revisão de branch inteira não é redundante com as de fase: as de fase olham um app de cada vez e não conseguem perguntar "se esta regra sumisse, alguma coisa ficaria vermelha?", porque a resposta mora em outro app ou em outro arquivo de teste.

Os quatro Minor da rodada 6 foram corrigidos, os três primeiros com red-green isolado: a quarta cláusula da RN21 (in_guarantee contando também quem aguarda assinatura — revertida, o painel diria 2 onde há 3, e uma garantidora que emitiu esta semana veria zero); a descrição Swagger desse mesmo campo, que continuava dizendo "garantias ativas"; e o estado efetivo no resumo da garantia dentro da análise, que é a terceira porta para o mesmo objeto — revertido, a tela da garantia diz "Vencida" e a da análise diz "Vigente", em duas telas que a mesma pessoa abre em sequência. O quarto era a frase deste registro sobre o OPERATOR_PERMS, corrigida acima.

Pendências aceitas da revisão da branch:

  • A §12 define um GuaranteeListItem que o código não implementa — a lista devolve o GuaranteeResponse inteiro (ou o GuaranteeAgencyResponse, para a imobiliária). Não é vazamento: os papéis da garantidora leem os mesmos campos no detalhe, e o recorte da imobiliária tem teste. É payload a mais e uma promessa de contrato não cumprida.
  • O ADMIN_GLOBAL deixou de emitir pelo diálogo. O isValid passou a exigir o resumo, e o simulate resolve parâmetros por tenant_id — nulo para ele, logo SETTINGS_NOT_FOUND. A direção está de acordo com a 10.5 (o SaaS admin não opera dado de negócio do cliente), mas aconteceu em silêncio, sem ser decisão declarada.
  • A fórmula de meses existe em três lugares — _months_between na API, mesesEntre no diálogo e prazoEmMeses no detalhe. As três conferem hoje (ceil(dias/30), mínimo 1) e nenhum teste as amarra.
  • O GuaranteeSimulateRequest não aceita start_date/end_date, que a §12 descreve no corpo — a tela converte para guarantee_months antes de chamar. O 422 do conflito entre os dois, documentado na §12, é portanto inalcançável. É desvio de contrato, não defeito de comportamento, e mudá-lo agora seria mexer no que está coberto e verde.
  • A CA17k só é exercitada pelo lado do admin — falta o AGENCY_USER dono da carteira. A guarda é no serviço e não olha papel, então o risco é baixo.
  • Os achados da rodada 6 foram corrigidos sem uma 7ª revisão, e são de teste e de texto: três asserções em testes que já montavam a fixture, mais uma descrição de campo.

Registro

Entregue nos três apps, na ordem do monorepo. O que a feature faz: a garantia deixa de nascer vigente e passa a esperar o contrato assinado, que é o ato que a ativa e libera a cobrança; ganha tela própria, com tomador, imóvel, imobiliária, cobertura, vigência e sinistro na mesma página; e a lista passa a buscar, filtrar, ordenar e paginar no servidor, dizendo há quanto tempo cada uma espera.

As suítes, no fechamento:

O quê Resultado
pytest da API 426 verdes, 1 vermelho — o intermitente conhecido de test_partner_agency_search, que passou na rodada anterior
emissao-e-assinatura-da-garantia.spec.ts 12 verdes, 38,9s, contra o dev-remote reconstruído
Suíte e2e completa 239 verdes, 10 vermelhos, 2 pulados — ver abaixo
tsc do web limpo
make preflight limpo

As 10 falhas da suíte e2e, uma a uma. Duas são desta branch por decisão: menu-por-permissao para a imobiliária, cujo teste codificava a política anterior — a spec dá guarantees:read ao canal e põe "Garantias" no menu dele. Atualizado, com "Cobranças" e "Sinistros" virando proibidos nomeados.

As outras oito não são regressão, e a evidência é de três tipos:

  1. Registro de spec anterior. credit-analysis-documents:365, isolamento-imobiliaria:76 e properties:156 constam como confirmadas pré-existentes com baseline rodado na main no Registro da spec 0037; o admin-interface está lá registrado como instável.
  2. Medição no banco. As outras duas do isolamento-imobiliaria caem porque a carteira da Imobiliária E2E tem 15 inquilinos e o do seed sai da primeira página. Dos 15, 14 foram criados entre 12 e 15 de agosto — nenhum é desta spec, e os que este spec cria têm partner_agency_id nulo (9 de 9), logo não entram naquela carteira.
  3. Medição no contrato. No properties:156, o selectAgency procura E2E e a busca da API casa name ILIKE '%E2E%' ou o CNPJ contendo qualquer dígito do termo — o "2" de "E2E". São 61 resultados para um teto de 50, e a "Imobiliária E2E" está na posição 61. Remover a única imobiliária que este spec cria a deixaria em 60.

O que mudou fora do escopo da spec, e por quê: o CLAUDE.md da raiz dizia que a assinatura dos dois testes intermitentes é "falha na suíte e passa sozinho". É meia verdade, e ela custou uma investigação aqui: como o desempate é sorteado, eles falham sozinhos também. Medido — 3 verdes e 2 vermelhos em 5 execuções isoladas. O texto agora diz que o que assina é o cara-ou-coroa, e manda rodar cinco vezes.

Nota de deploy. guarantees:sign e guarantees:annotate só chegam ao banco quando o RBAC seed roda. No dev-remote isso já aconteceu — o entrypoint chama init_db a cada recriação do container. Em produção é o /ship que precisa fazer, e sem isso o ADMIN_TENANT toma 403 ao registrar assinatura ou editar notas.

O que ficou aberto está nas Pendências aceitas da seção 17: o CA17k sem o lado da imobiliária, o GuaranteeListItem que a §12 define e o código não implementa, o start_date/end_date ausentes do simulate, a fórmula de meses em três lugares, o ADMIN_GLOBAL que deixou de emitir pelo diálogo, a lista em <Table> em vez de DataGrid, e o spec e2e que não limpa o que cria — porque não há DELETE para garantia nem para análise, e apagar inquilino, imóvel ou imobiliária é recusado enquanto houver vínculo.