Pular para conteúdo

0027 — Contrato vira Garantia

Status CONCLUÍDA
Branch spec/0027-contrato-vira-garantia
Criada em 2026-08-08
Apps afetados api · web · e2e

Status: RASCUNHO → APROVADA → EM IMPLEMENTAÇÃO → CONCLUÍDA

1. Problema

No menu, Contratos fica logo abaixo de Análise de Crédito. Quem abre a tela encontra uma lista de coberturas de garantia — LMI, carência, mensalidade, status ACTIVE/SUSPENDED/TERMINATED/EXPIRED. Mas "contrato", no vocabulário de quem trabalha numa garantidora, já é três coisas antes de ser essa:

  • Contrato de locação — inquilino ↔ proprietário. A garantidora não assina, não guarda, não gerencia. É da imobiliária.
  • Contrato de parceria — garantidora ↔ imobiliária. É o que autoriza o canal a vender a garantia.
  • Modelo de contrato (ContractTemplate) — o documento que se emite.

A ambiguidade já vazou para dentro do produto. Em apps/web/src/pages/settings/index.tsx a palavra "contrato" aparece em três sentidos diferentes dentro da mesma tela: "dias iniciais do contrato" (garantia), "Total do contrato" como base de cálculo (locação: aluguel × meses) e "Contratos acima deste valor serão bloqueados" (prazo da locação). Quem lê precisa desambiguar de cabeça, item a item.

O código conta a mesma história pelo avesso: a classe se chama Contract, mas seus campos já desistiram do nome — guaranteed_value, guarantee_rate_percent, claim_grace_period_days. A entidade é a garantia; só o rótulo ficou para trás.

E há o custo que ainda não chegou: Cobranças e Sinistros estão no menu como comingSoon. Sinistro é o evento que aciona a garantia e envolve os três contratos ao mesmo tempo — o de locação que foi descumprido, o de parceria pelo qual a imobiliária comunica, e a garantia que paga. Entrar nesse módulo com "contrato" significando a garantia é começar a próxima feature já devendo a correção.

2. Objetivo

A cobertura vendida ao inquilino passa a se chamar Garantia em todo lugar — menu, telas, rotas, código, banco e número do documento — para que "contrato" volte a significar apenas o contrato de locação ou o de parceria, sem desambiguação mental.

3. Fora de escopo

  • ContractTemplate não é renomeado. Decisão do usuário em 2026-08-08. O modelo é de documento contratual genérico e o nome atual continua correto. Entra só uma docstring dizendo o que ele modela e o que ele não é, para que a convivência entre guarantees e contract_templates na mesma árvore não pareça descuido.
  • Contrato de locação e contrato de parceria mantêm o nome. São contratos de verdade; a spec não os toca — exceto onde o texto atual usa "contrato" querendo dizer garantia (ver RN9).
  • Nenhuma mudança de comportamento. Nada de campo novo, regra nova, permissão nova ou tela nova. Se um valor calculado mudar, é defeito da spec, não entrega dela.
  • Cobranças e Sinistros seguem comingSoon. A spec só garante que, quando forem implementados, encontrem o vocabulário já corrigido.
  • Não há alias nem retrocompatibilidade de rota. /api/v1/contracts deixa de existir; não fica redirecionando. O CLAUDE.md autoriza — o único cliente da API é o apps/web, versionado no mesmo commit.
  • Números de garantia já emitidos não são reescritos. Decisão do usuário: quem é C202605-00007 continua sendo (ver RN5).
  • Sem ruff format na árvore. Vale a regra do CLAUDE.md; o diff desta spec já é grande o bastante sem reformatação carona.

4. Regras de negócio

  • RN1. Nome canônico. A entidade passa a ser Guarantee no código e Garantia na interface. guarantees na tabela, na rota e no resource do Refine. O plural em português é Garantias.

Por que Guarantee e não Warranty ou Policy: o negócio diz "garantia locatícia"; guarantee traduz 1:1 e já é o radical dos campos existentes (guaranteed_value, guarantee_rate_percent). Policy (apólice) traria vocabulário de seguro, que o produto não usa — e a garantia locatícia não é seguro.

  • RN2. Renomeação sem mudança de comportamento. Nenhum cálculo, validação, transição de status ou permissão efetiva muda. A suíte de testes existente, ajustada apenas nos nomes, precisa continuar passando com os mesmos números.

  • RN3. Status preservados. GuaranteeStatus mantém os quatro valores literais ACTIVE, SUSPENDED, TERMINATED, EXPIRED, com a mesma semântica. Só o nome do enum muda. Os valores estão gravados no banco — mexer neles seria migração de dados sem ganho.

  • RN4. Permissões renomeadas em bloco. contracts:list|read|create|update viram guarantees:list|read|create|update, com as mesmas descrições traduzidas ("Listar garantias", "Visualizar garantia", "Emitir garantia", "Atualizar garantia") e atribuídas aos mesmos papéis.

A migration faz UPDATE permissions SET resource='guarantees' WHERE resource='contracts' — não insere linhas novas. O rbac_seed usa ON CONFLICT DO NOTHING e nunca remove permissão: criar as novas pelo seed deixaria as quatro antigas órfãs no banco, ainda ligadas aos papéis por role_permissions. Atualizar a linha existente preserva o vínculo e não deixa resíduo.

  • RN5. Prefixo do número muda; o passado não. Garantias emitidas a partir desta spec recebem G{YYYY}{MM}-{NNNNN} (ex.: G202608-00042). As já emitidas com prefixo C permanecem como estão — o número é identificador usado em comunicação, e não há ganho em reescrevê-lo.

  • RN6. O contador de sequência continua de onde parou. NumberingType.CONTRACT ("contract") vira GUARANTEE ("guarantee"), e a migration faz UPDATE numbering_sequences SET numbering_type='guarantee' WHERE numbering_type='contract', preservando last_seq.

Sem esse update, next_seq criaria um contador novo para o mesmo tenant/ano começando do zero, e G202608-00001 conviveria com C202608-00001 no mesmo mês — dois números que só se distinguem por uma letra, numa busca por "202608-00001". Preservar o contador custa uma linha de SQL e elimina o caso.

  • RN7. entity_type polimórfico passa a "guarantee". Vale para a property derivada em Document e para o valor gravado em audit_logs.entity_type. A migration atualiza o histórico: UPDATE audit_logs SET entity_type='guarantee' WHERE entity_type='contract'.

Aqui o dado antigo é reescrito, ao contrário de RN5 — entity_type é chave de filtro interna, não identificador externo. Deixar os dois valores conviverem quebraria o filtro da tela de auditoria para os registros antigos.

  • RN8. FKs renomeadas. billings.contract_id → billings.guarantee_id e documents.contract_id → documents.guarantee_id, com os índices e constraints renomeados junto. As regras de ondelete não mudam (RESTRICT em billings, SET NULL em documents).

  • RN9. Desambiguação de texto: cada ocorrência decidida uma a uma. Onde o texto atual diz "contrato" querendo dizer a cobertura, vira "garantia". Onde diz querendo dizer a locação, vira "locação" ou "contrato de locação" — explicitamente, não simplesmente mantido. Casos já mapeados em settings/index.tsx:

Texto atual Sentido Vira
"Dias iniciais do contrato em que sinistros…" garantia "…da garantia…"
"Total do contrato" (base do initialFeeBase) locação "Total da locação (aluguel × meses)"
"Contratos acima deste valor serão bloqueados" ~~prazo da locação~~ → prazo da garantia "Garantias acima deste prazo serão bloqueadas"
"taxas aplicadas ao contrato de garantia" garantia "…aplicadas à garantia"
"todos os contratos ativos da plataforma" garantia "todas as garantias ativas"
"a garantia também cobre danos… ao encerrar o contrato" locação "…ao encerrar a locação"

A tabela não é exaustiva: a implementação percorre as ocorrências restantes de "contrato" em settings/index.tsx, no Painel e no Centro de Operações e classifica cada uma. Nenhuma substituição cega — trocar tudo por "garantia" produziria frases falsas (o prazo máximo é da locação, não da garantia).

  • RN10. Papéis e escopos inalterados. Quem via contrato vê garantia, com exatamente o mesmo alcance. Nada de reabrir o modelo de acesso — se algum papel mudar de alcance, é defeito desta spec.

Correção de 2026-08-08, durante a implementação. A redação original desta RN dizia "ADMIN_TENANT e ANALYST com escrita". Está errada sobre o estado anterior: só ADMIN_GLOBAL e ADMIN_TENANT têm create/update; ANALYST, OPERATOR e VIEWER têm apenas list/read — e assim era com contracts:* antes da spec (linha 123 do rbac_seed.py no HEAD é o único bloco com create). O código está correto e preservado; o texto é que descrevia o alcance errado.

  • RN11. Erro no caminho. Nenhum código de erro de negócio novo. /api/v1/contracts passa a responder 404 — comportamento padrão do FastAPI para rota inexistente, sem handler especial. Se o web ficar chamando a rota velha, é bug de implementação a corrigir, não caso a tratar.

5. Impacto por app

apps/api

Modelos/migrations

Antes Depois
app/models/contract.py app/models/guarantee.py
class Contract class Guarantee
class ContractStatus class GuaranteeStatus
__tablename__ = "contracts" "guarantees"
contract_number guarantee_number
billings.contract_id billings.guarantee_id
documents.contract_id documents.guarantee_id
NumberingType.CONTRACT NumberingType.GUARANTEE
CreditAnalysis.contract (rel.) CreditAnalysis.guarantee
Billing.contract (rel.) Billing.guarantee

Migration única, com downgrade simétrico:

  1. op.rename_table("contracts", "guarantees")
  2. alter_column de contract_number → guarantee_number
  3. alter_column das duas FKs (billings, documents) → guarantee_id
  4. renomear índices e constraints (ix_*, fk_*, uq_*) que carregam contract no nome
  5. UPDATE permissions SET resource='guarantees' WHERE resource='contracts' (RN4)
  6. UPDATE numbering_sequences SET numbering_type='guarantee' WHERE numbering_type='contract' (RN6)
  7. UPDATE audit_logs SET entity_type='guarantee' WHERE entity_type='contract' (RN7)

Conferir os nomes reais de índice e constraint no banco antes de escrever a migration — parte foi criada pelo Alembic com nome automático e parte explicitamente. \d contracts no garantora-db-1 resolve.

Endpoints — o router é app/api/v1/routers/guarantees.py, prefixo /guarantees, tag Garantias. Paths e permissões (RN10):

Método Antes Depois Permissão
POST /contracts/simulate /guarantees/simulate guarantees:create
POST /contracts /guarantees guarantees:create
GET /contracts /guarantees guarantees:list
GET /contracts/{contract_id} /guarantees/{guarantee_id} guarantees:read
PATCH /contracts/{contract_id} /guarantees/{guarantee_id} guarantees:update
POST /contracts/{contract_id}/terminate /guarantees/{guarantee_id}/terminate guarantees:update

Regras no service — contract_service.py → guarantee_service.py. A troca de prefixo de RN5 é uma linha: format_document_number("C", …) → format_document_number("G", …).

Arquivos com maior concentração (ocorrências de contract, medidas em 2026-08-08 — serve para dimensionar, não como checklist fechado):

operations_center_service.py  74     contract_service.py           55
routers/contracts.py          50     billing_service.py            24
routers/billing.py            19     tenant_purge_service.py       17
schemas/contract.py           11     models/contract.py            11
db/rbac_seed.py               10     repositories/credit_analysis_repo.py  8

44 arquivos Python no total, ~295 ocorrências.

Atenção no operations_center_service.py — é o arquivo mais afetado e o mais fácil de errar, porque mistura os dois sentidos: in_contract (analises que viraram garantia), ContractCounters, e o alerta CONTRACT_EXPIRING_15D. Viram in_guarantee, GuaranteeCounters e GUARANTEE_EXPIRING_15D. O type do alerta é string de contrato de API — o web casa por igualdade e precisa mudar junto.

Testes — quatro arquivos referenciam contrato: test_operations_center.py, test_rbac_security.py, test_upfront_payment.py, test_analise_completa.py. Não há arquivo de teste dedicado a contrato hoje. A spec não cria suíte nova além do mínimo de RN5/RN6/RN7 (ver CA7 e CA8) — o valor aqui é a suíte existente continuar verde com os nomes trocados.

Documentação — summary, description e response_description de todos os decorators do router novo, em português, falando de garantia. Docstrings de módulo, classe e método em guarantee.py, guarantee_service.py, guarantee_repo.py e schemas/guarantee.py. Docstring de esclarecimento em contract_template.py (fora de escopo renomear, mas não fora de escopo explicar).

apps/web

Páginas/componentes

Antes Depois
src/pages/contracts/ src/pages/guarantees/
ContractListPage / ContractDetailPage GuaranteeListPage / GuaranteeDetailPage
rota /contracts, /contracts/:id /guarantees, /guarantees/:id
resource: "contracts" (App.tsx) "guarantees"
ContractCreateDialog GuaranteeCreateDialog
CONTRACT_STATUS (StatusBadge.tsx) GUARANTEE_STATUS
overdueContracts (nav-badges) overdueGuarantees
icon/map.ts: contracts: "certificate" guarantees: "certificate"
label "Contratos" (nav-groups.tsx) "Garantias"
aba "contratos" (mobile-shell) "Garantias"

O ícone certificate permanece — serve a garantia tão bem quanto servia ao contrato.

Texto de ação: o botão que hoje diz "Criar contrato" na análise aprovada passa a "Emitir garantia". "Emitir" é o verbo do domínio para garantia; "criar" é do CRUD.

data-testid — os testids que carregam contract acompanham o rename (contract-create-dialog → guarantee-create-dialog, e afins). Como isso quebra o e2e existente, os specs mudam no mesmo commit — é justamente o que o monorepo permite. Levantar a lista completa por grep -rn 'data-testid.*contract' apps/web/src na implementação e listá-la aqui no Registro.

Arquivos — 24 no src, concentrados em pages/contracts/ (78 ocorrências somadas) e pages/credit-analyses/show.tsx (20).

Responsividade & PWA — nada de layout muda; nenhuma coluna, ordem ou breakpoint é tocado. Ainda assim há dois riscos reais de texto, verificáveis nos dois viewports:

  • "Garantias" é 1 caractere mais curto que "Contratos" no sider e na aba do mobile-shell — folga, não aperto. Confirmar mesmo assim.
  • "Emitir garantia" é 1 caractere mais longo que "Criar contrato", em botão de diálogo no viewport de 375px. É o único ponto onde o texto novo pode estourar.

PWA: sem asset novo e sem tela offline nova. Mas as rotas /contracts* deixam de existir — quem tiver a aplicação instalada com a lista de contratos aberta cai num 404 de client-side após atualizar. Aceitável em desenvolvimento; verificar que a rota morta cai no not-found do app e não em tela branca.

apps/e2e

Specs — dois arquivos referenciam contrato: credit-analyses.spec.ts (2) e chrome-borders.spec.ts (1). Ajustar seletores, rotas e testids. Não há spec dedicada a contrato hoje, e esta spec não cria uma — cobertura de garantia é assunto próprio, não carona de renomeação.

Seeds — sem dado novo. Conferir se algum seed em tests/fixtures/seeds/ cria contrato por nome de campo (contract_id, contract_number) e ajustar.

Regressão que importa — o fluxo "aprovar análise → emitir garantia → ver garantia na lista" já é coberto pelo credit-analyses.spec.ts. Se ele passa depois do rename, a troca não quebrou o caminho principal. É esse o critério, não spec nova.

6. Contrato de API

Sem campo novo, sem payload novo. O que muda é path, nome de campo e entity_type.

Emitir garantia

POST /api/v1/guarantees
Authorization: Bearer <token>

{ "credit_analysis_id": "uuid", "start_date": "2026-09-01", "notes": "…" }
{
  "id": "uuid",
  "guarantee_number": "G202608-00042",
  "status": "ACTIVE",
  "guaranteed_value": "2500.00",
  "guarantee_rate_percent": "8.50",
  "lmi_value": "30000.00"
}

Análise de crédito — o resumo embutido muda de chave e de tipo:

{
  "id": "uuid",
  "status": "APPROVED",
  "guarantee": { "id": "uuid", "guarantee_number": "G202608-00042", "status": "ACTIVE" }
}

CreditAnalysisContractSummary → CreditAnalysisGuaranteeSummary; o campo contract vira guarantee; contract_number vira guarantee_number.

Centro de Operações

{
  "guarantees": { "active": 12, "expiring_soon": 2 },
  "credit_analyses": { "in_guarantee": 12 },
  "alerts": [{ "type": "GUARANTEE_EXPIRING_15D", "entity_type": "guarantee" }]
}

Documentos — o filtro ?entity_type=contract passa a ?entity_type=guarantee; o corpo do upload troca contract_id por guarantee_id.

Auditoria — ?entity_type=guarantee, inclusive para registros gravados antes desta spec (RN7).

Erros previstos — os mesmos de hoje, com as mensagens falando de garantia. /api/v1/contracts responde 404 (RN11).

7. Critérios de aceite

  • [x] CA1. Dado um usuário autenticado, quando abre o menu, então lê Garantias — no sider do desktop e na navegação do mobile — e nenhum item chamado "Contratos".
  • [x] CA2. Dada uma análise de crédito APPROVED, quando um usuário com guarantees:create aciona Emitir garantia, então a garantia é emitida, aparece em /guarantees e seu número começa com G. > Redação corrigida em 2026-08-08: dizia "o analista", mas ANALYST não tem > guarantees:create — nem tinha contracts:create antes (ver RN10).
  • [x] CA3. Dado o banco antes da migration, quando ela roda, então guarantees existe com a mesma contagem de linhas que contracts tinha, e nenhuma cobrança ou documento perde vínculo (billings.guarantee_id e documents.guarantee_id sem nulo onde antes havia valor).
  • [x] CA4. Dado um GET /api/v1/contracts com token válido, então a resposta é 404 — e nenhuma tela do web dispara essa chamada (console limpo ao percorrer Painel, Análise de Crédito, Garantias e Centro de Operações).
  • [x] CA5. Dado um usuário sem guarantees:create (VIEWER, OPERATOR, ANALYST), quando abre /guarantees, então vê a lista e recebe 403 ao tentar emitir; um ADMIN_TENANT emite normalmente. (Mesmo alcance de antes — RN10.)
  • [x] CA6. Dada a tela de Configurações, quando se lê cada ocorrência de "contrato" ou "garantia", então cada uma está no sentido certo conforme a tabela de RN9 — em particular, o prazo máximo fala de locação, não de garantia.
  • [x] CA7. Dada uma garantia emitida antes da spec com número C…, quando se emite uma nova no mesmo mês e tenant, então a nova é G… com sequência seguinte, não reiniciada em 00001 (RN6).
  • [x] CA8. Dado um registro de auditoria gravado antes da spec para um contrato, quando se filtra a auditoria por entity_type=guarantee, então ele aparece (RN7).
  • [x] CA9. make test passa com todos os testes de antes da spec, mais os novos. Aferido: 224 no baseline → 230 passando, 0 falhas. Nenhum teste sumiu no caminho. > A spec dizia 223; a contagem real do HEAD era 224 — o fix 0006 somou testes > depois de o número ter sido anotado no CLAUDE.md.
  • [x] CA10. tsc limpo, e ruff sem apontamento novo nos arquivos tocados (comparar antes/depois, não o total).
  • [x] CA11. Dado o dev-remote reconstruído, quando se abre /guarantees e o diálogo de emissão em 375px e em desktop, então scrollWidth - clientWidth == 0 nos dois e o botão "Emitir garantia" não trunca.
  • [x] CA12. Dada uma URL antiga /app/contracts digitada à mão, então o app mostra a tela de não encontrado — não uma tela branca.
  • [x] CA13. grep -ri "contract" apps/api/app apps/web/src retorna apenas ocorrências de ContractTemplate / contract_templates e textos que falam legitimamente de contrato de locação ou de parceria. Nenhum resíduo da entidade renomeada.

8. Plano de implementação

Ordem obrigatória: api → web → e2e. Dentro da API, o banco primeiro — o resto desce dele.

  • [x] apps/api — migration Alembic: rename de tabela, colunas, FKs, índices e constraints, mais os três UPDATE de dados (RN4, RN6, RN7). downgrade simétrico e testado.
  • [x] apps/api — models/contract.py → guarantee.py (Guarantee, GuaranteeStatus, guarantee_number), models/__init__.py, e os relacionamentos em credit_analysis.py, billing.py, document.py
  • [x] apps/api — NumberingType.GUARANTEE e prefixo "G" no service
  • [x] apps/api — schemas/, repositories/, services/ (com atenção a operations_center_service.py, o mais afetado)
  • [x] apps/api — routers/contracts.py → guarantees.py, registro em api/v1/__init__.py
  • [x] apps/api — rbac_seed.py com guarantees:* e descrições em português
  • [x] apps/api — docstrings + Swagger em tudo que foi renomeado, e a docstring de esclarecimento em contract_template.py
  • [x] apps/api — ajustar os quatro testes existentes; cobrir RN5, RN6 e RN7
  • [x] apps/api — make test verde antes de encostar no web
  • [x] apps/web — pages/contracts/ → pages/guarantees/, App.tsx (import, resource, rotas), nav-groups.tsx, mobile-shell, nav-badges, icon/map.ts, StatusBadge.tsx
  • [x] apps/web — ContractCreateDialog → GuaranteeCreateDialog, botão "Emitir garantia"
  • [x] apps/web — Centro de Operações (contadores, alertas, fila) e credit-analyses/show.tsx
  • [x] apps/web — desambiguar os textos de RN9, um a um
  • [x] apps/web — data-testid renomeados, listados no Registro
  • [x] apps/e2e — credit-analyses.spec.ts e chrome-borders.spec.ts; conferir seeds
  • [x] make test e make lint (comparar apontamentos antes/depois, não o total)
  • [x] /devremote-rebuild + screenshot nos dois viewports (CA11, CA12)
  • [x] grep final de resíduo (CA13)

9. Decisões em aberto

Nenhuma. As quatro foram decididas pelo usuário em 2026-08-08:

  • [x] Profundidade: rename completo — banco, API, web e e2e. Descartado parar na interface (recriaria a divergência entre código e negócio que as specs 0007 e 0017 pagaram para eliminar) e parar antes do banco (deixaria ORM e schema divergentes).
  • [x] Prefixo: G para as novas, sem reescrever as emitidas (RN5).
  • [x] ContractTemplate: fica como está — modela documento contratual, não a garantia.
  • [x] Textos de Configurações e Painel: revisados um a um (RN9), não substituídos em massa.
  • [x] Sequência de branch: o fix/0006 foi mesclado na main antes desta spec nascer, para não haver duas branches abertas mexendo nas mesmas telas.

10. Registro

Implementada em 2026-08-08. A entidade é Garantia em todo lugar: banco, API, web, e2e e nos textos que o usuário lê.

O que a spec não previa

Achado O que era O que virou
contract_status é um ENUM do PostgreSQL A spec listava tabela, colunas e FKs, mas não o tipo. Sem ALTER TYPE, o banco ficaria com uma tabela guarantees cuja coluna status tem tipo contract_status. ALTER TYPE contract_status RENAME TO guarantee_status, com o downgrade simétrico. Os quatro valores literais não mudaram (RN3).
Nomes legados da spec 0007 O índice unique chamava-se ix_contracts_proposal_id e a FK contracts_proposal_id_fkey — mas a coluna é credit_analysis_id desde a 0007, que renomeou a coluna e esqueceu os objetos que a indexam. ix_guarantees_credit_analysis_id e guarantees_credit_analysis_id_fkey. Corrigido de passagem: era renomear os dois de qualquer forma.
Descrição das permissões não vinha pelo seed A RN4 previa UPDATE permissions SET resource. Mas o rbac_seed usa ON CONFLICT DO NOTHING e não atualiza linha existente — o banco seguiria dizendo "Listar contratos" com o resource já guarantees. A migration atualiza description junto, num CASE por action. Conferido no banco: as quatro descrições saíram corretas e os 14 vínculos em role_permissions foram preservados — que era o motivo de usar UPDATE em vez de INSERT.
GuaranteeCreateDialog não tinha data-testid nenhum Nem no diálogo, nem nos quatro campos, nem nos botões — lacuna anterior a esta spec, mas que o CLAUDE.md do web proíbe e que o CA2 precisa. Oito atributos adicionados (guarantee-create-dialog, -confirm, -cancel, -error e os quatro campos).

Onde a RN9 mudou de resposta

A regra mandava classificar cada "contrato" uma a uma, e duas das classificações que a própria spec havia adiantado estavam erradas. Ter conferido no código evitou publicar descrição falsa:

  • maxMonths / minMonths — a spec dizia "prazo da locação → Locações acima deste prazo". Mas o texto longo do mesmo campo diz "Maior prazo de garantia, em meses", e o campo limita guarantee_months. É o prazo da garantia. Ficou "Garantias acima deste prazo serão bloqueadas".
  • start_date / end_date da análise de crédito — não estavam na tabela da RN9 e seriam trocadas por inércia. O model é explícito: "Data de início prevista para a locação (informativo na análise de crédito)". Foram para "locação"; virariam mentira como "garantia".
  • FinancialSummaryPanel — o rótulo ${fee_pct}% do contrato acompanha pkg * months * fee_pct / 100, ou seja, incide sobre o total da locação. Ficou "% do total da locação".

O que ficou como estava, por ser contrato de verdade: ContractTemplate e a tabela contract_templates; os tipos de documento "Contrato de Locação" e "Contrato de Garantia"; a área de documentos "Contrato"; "Contrato Social" (documento de PJ, que uma troca cega transformaria em "Garantia Social"); "o contrato real" de docs/contratos/; e o "contrato" no sentido de contrato de software em test_transaction_boundary.py e test_pagination_order.py.

Método

O rename correu em duas passagens separadas, porque "contrato" não contém a substring "contract" — as duas não se atropelam:

  1. Identificadores (47 arquivos Python, 28 do web), com ContractTemplate / contract_template protegidos por sentinela.
  2. Texto humano, com regras de concordância — "contrato" é masculino e "garantia" é feminino, então trocar a palavra sozinha produzia "a garantia jurídico", "uma garantia gerado" e "visualizá-lo". Onze casos assim foram encontrados por varredura de concordância e corrigidos.

Arquivos movidos com git mv (preserva git blame): models/contract.py, schemas/contract.py, repositories/contract_repo.py, services/contract_service.py, routers/contracts.py e a pasta pages/contracts/ do web.

Migration validada contra o PostgreSQL

Os testes rodam em SQLite e não exercem a migration. Ela foi aplicada, revertida e reaplicada no banco do dev-remote, com contagem antes e depois:

Antes Depois do upgrade Depois do downgrade
linhas contracts = 115 guarantees = 115 contracts = 115
auditoria entity_type='contract' = 115 'guarantee' = 115, 'contract' = 0 'contract' = 115
permissões resource='contracts' = 4 'guarantees' = 4, com descrição nova 'contracts' = 4
role_permissions 14 14 14
contador contract, last_seq = 115 guarantee, last_seq = 115 contract = 115
enum contract_status guarantee_status contract_status

O last_seq preservado é o CA7: a próxima garantia sai G202608-00116, e não G202608-00001 colidindo visualmente com o C202608-00001 já emitido.

Verificação

  • API: 230 testes — 224 do baseline mais os 6 novos de test_guarantee_numbering.py. Duas execuções completas: uma com 230 passando e zero falhas, outra com 229 passando e 1 falha — test_partner_agency_status_filter.py::test_alcanca_quem_esta_fora_da_primeira_pagina, o flake já documentado no CLAUDE.md e reconfirmado pelo fix 0006 (reproduz ~2 em 3 execuções com o código parado, em arquivo que esta spec não toca).
  • Ruff: 249 contra 252 do baseline — 3 a menos. Os dois arquivos novos (teste e migration) passam limpos. Um apontamento novo foi introduzido e corrigido: o rename desordenou os imports em api/v1/__init__.py (I001). Outros três saíram ao limpar imports órfãos em test_analise_completa.py.

    O baseline correto é 252, não os 244 que eu havia medido no início: aqueles vinham do código embutido na imagem, anterior ao fix 0006. O número certo saiu de git archive HEAD copiado para o container.

  • tsc: limpo.
  • e2e: dois specs ajustados (credit-analyses.spec.ts, chrome-borders.spec.ts).

Validado contra o dev-remote, depois do rebuild

api e web reconstruídos e recriados. Bundle novo (assets/index-Bq02CXRC.js), /api/v1/guarantees responde 401 (existe, exige auth) e /api/v1/contracts responde 404 (CA4).

Emissão real pela API, no ambiente: G202608-00116, logo após o C202608-00115 que já existia — prefixo novo e sequência continuada, que é o CA7 de pé. Uma segunda, emitida por ADMIN_TENANT, saiu G202608-00117.

Telas percorridas em 1440px e 375px (/guarantees, /credit-analyses, /settings e a rota morta /contracts): scrollWidth - clientWidth == 0 nas oito, nenhum erro de console, nenhuma ocorrência de "Contrato" fora do que é contrato de verdade. O sider mostra Garantias, a aba do mobile também, e /app/contracts cai na tela de não encontrado — não em tela branca (CA12).

A lista de garantias no mobile ainda corta colunas à direita (rolagem interna da tabela, não da página). É dívida anterior a esta spec: a tela usa <Table> do MUI em vez do RecordCard, e o CLAUDE.md do web já registra que as listas fora da Análise de Crédito devem convergir para o padrão. Fora do escopo aqui.

Duas coisas que a spec afirmava e o ambiente desmentiu

  • ANALYST não emite garantia. A RN10 dizia "ADMIN_TENANT e ANALYST com escrita"; o teste no ambiente devolveu 403 para ANALYST. Conferido contra o HEAD: ANALYST tinha contracts:list,read e agora tem guarantees:list,read — ou seja, o alcance foi preservado, que é o que a RN10 de fato exige. Errado estava o texto. RN10 e CA2/CA5 corrigidos acima, com o registro do que dizia antes.
  • O baseline de testes era 224, não 223. O número da spec veio do CLAUDE.md, que não foi atualizado quando o fix 0006 somou testes.

Erros meus no caminho

  • docker cp alembic <container>:/app/alembic aninhou o diretório. O destino já existia, então a cópia virou /app/alembic/alembic/, e o Alembic não enxergou a revisão nova — upgrade head rodou em silêncio e reportou o8q0s2v4x6y7 (head) como se não houvesse nada a fazer. Só a conferência de to_regclass('guarantees') pegou.
  • Chamei _settings_factory depois de emitir a garantia, onde não tem efeito nenhum: é das TenantSettings que saem taxa, LMI e carência gravadas no snapshot. Os testes passavam porque o service tolera settings ausentes. A fixture foi para o conftest.py (onde já devia estar — importá-la de outro arquivo de teste gerava F811 a cada uso) e o helper passou a criá-las antes da emissão.
  • Chamei o helper de setup duas vezes no mesmo teste e tomei 409: ele cria tenant com CNPJ fixo, que é único por plataforma. Mesma armadilha que o fix 0006 registrou no spec e2e. O helper passou a aceitar um contexto existente — que o teste de sequência precisa de qualquer forma, já que dois números só são consecutivos dentro do mesmo tenant.
  • Meu script de screenshot validou a tela de login e reportou tudo verde. A chave que eu usei para injetar a sessão (garantora-auth) não é a do app (garantora-access-token, em providers/constants.ts). Errar a chave não dá erro: o app manda para o login — e a tela de login fala de "Gestão de garantias", então passou pela verificação que procurava a palavra. Oito ✓ falsos. Só apareceu porque abri o PNG em vez de confiar na saída do script. O script ganhou uma guarda explícita (CAIU NO LOGIN) para que a próxima falha desse tipo se denuncie.

11. Reavaliação: produção entrou no ar durante a implementação

Registrado em 2026-08-09, depois de mesclar origin/main.

Esta spec foi escrita e implementada sobre um main que já estava 10 commits desatualizado. Nesse intervalo entraram a spec 0028 (stack infra/prd na box srv01, app.garantora.com.br com TLS) e a spec 0030 (skill ship). A seção Fora de escopo apoiou duas decisões em "não existe nada em produção", frase que o CLAUDE.md ainda repete e que a própria 0028 já contradizia.

O que não muda, e por quê. O argumento das decisões nunca foi "não existe ambiente de produção" — foi não há base instalada a preservar. A 0028 é um provisionamento único: cria o ambiente e o sobe pela primeira vez, com seed apenas do usuário admin. Não há clientes, nem dados de negócio, nem integrador externo. O único cliente da API segue sendo o apps/web, versionado neste mesmo commit. Então continuam de pé:

  • sem alias para /api/v1/contracts (RN11) — não há consumidor a quebrar;
  • rename destrutivo em vez de coluna nova (RN1, RN8);
  • números C… preservados e contador continuado (RN5, RN6) — que, ironicamente, ficam mais certos com produção no ar do que sem ela.

O que passa a existir. Um destino a mais para a migration. Antes da 0028, aplicar a p9r1t3w5y7z8 significava o dev-remote e nada mais; agora há um banco em srv01 que também precisa dela. Três pontos de atenção para esse deploy:

  1. A migration não roda no boot. A RN7 da 0028 é explícita: migrations são passo do deploy, nunca do up. Subir a imagem nova sem make migrate deixa código novo contra schema velho — a API procuraria guarantees numa base que ainda tem contracts. É o cenário que o CLAUDE.md da API já descreve como "erro confuso de diagnosticar".
  2. O ship faz backup antes de migrar e pede confirmação humana, o que cobre o risco principal. O downgrade foi exercitado de verdade, mas só contra o dev-remote.
  3. ship deploya origin/main, nunca o local, e recusa se o local estiver à frente. Enquanto não houver push, nada disto chega a produção.

Pendência levantada aqui, resolvida a pedido do usuário. A seção Estágio do produto — nada em produção do CLAUDE.md ficou factualmente errada, e ela orienta o desenho de toda feature nova ("migration destrutiva é opção legítima", "quebrar contrato de API é barato"). O próprio texto avisava: "Quando entrar o primeiro cliente, esta seção precisa ser reescrita — não apagada e esquecida."

A intenção era deixar isso fora desta spec — mudar a régua que decide o desenho de todas as features não deveria entrar de carona num rename. O usuário pediu a correção, e ela foi feita em seguida, num commit próprio: a seção passou a "Estágio do produto — produção no ar, sem clientes ainda", e a estrutura no topo do arquivo deixou de chamar infra/prd/ de "esboço incompleto, não usar".

O eixo da reescrita é o mesmo desta reavaliação: a pergunta que decide o desenho não é "existe produção?", é "há base instalada a preservar?". Enquanto a resposta for não, as três permissões continuam de pé; o gatilho para revê-las passou a ser o primeiro cliente, não o primeiro servidor.