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¶
ContractTemplatenã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 entreguaranteesecontract_templatesna 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/contractsdeixa de existir; não fica redirecionando. OCLAUDE.mdautoriza — o único cliente da API é oapps/web, versionado no mesmo commit. - Números de garantia já emitidos não são reescritos. Decisão do usuário: quem é
C202605-00007continua sendo (ver RN5). - Sem
ruff formatna árvore. Vale a regra doCLAUDE.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
Guaranteeno código e Garantia na interface.guaranteesna tabela, na rota e noresourcedo 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.
GuaranteeStatusmantém os quatro valores literaisACTIVE,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|updateviramguarantees: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 prefixoCpermanecem 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") viraGUARANTEE("guarantee"), e a migration fazUPDATE numbering_sequences SET numbering_type='guarantee' WHERE numbering_type='contract', preservandolast_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_typepolimórfico passa a"guarantee". Vale para a property derivada emDocumente para o valor gravado emaudit_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_idedocuments.contract_id→documents.guarantee_id, com os índices e constraints renomeados junto. As regras deondeletenão mudam (RESTRICTembillings,SET NULLemdocuments). -
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_TENANTeANALYSTcom escrita". Está errada sobre o estado anterior: sóADMIN_GLOBALeADMIN_TENANTtêmcreate/update;ANALYST,OPERATOReVIEWERtêm apenaslist/read— e assim era comcontracts:*antes da spec (linha 123 dorbac_seed.pynoHEADé o único bloco comcreate). 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/contractspassa 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:
op.rename_table("contracts", "guarantees")alter_columndecontract_number→guarantee_numberalter_columndas duas FKs (billings,documents) →guarantee_id- renomear índices e constraints (
ix_*,fk_*,uq_*) que carregamcontractno nome UPDATE permissions SET resource='guarantees' WHERE resource='contracts'(RN4)UPDATE numbering_sequences SET numbering_type='guarantee' WHERE numbering_type='contract'(RN6)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 contractsnogarantora-db-1resolve.
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 comguarantees:createaciona Emitir garantia, então a garantia é emitida, aparece em/guaranteese seu número começa comG. > Redação corrigida em 2026-08-08: dizia "o analista", masANALYSTnão tem >guarantees:create— nem tinhacontracts:createantes (ver RN10). - [x] CA3. Dado o banco antes da migration, quando ela roda, então
guaranteesexiste com a mesma contagem de linhas quecontractstinha, e nenhuma cobrança ou documento perde vínculo (billings.guarantee_idedocuments.guarantee_idsem nulo onde antes havia valor). - [x] CA4. Dado um
GET /api/v1/contractscom 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; umADMIN_TENANTemite 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 em00001(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 testpassa 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 doHEADera 224 — o fix 0006 somou testes > depois de o número ter sido anotado noCLAUDE.md. - [x] CA10.
tsclimpo, eruffsem apontamento novo nos arquivos tocados (comparar antes/depois, não o total). - [x] CA11. Dado o dev-remote reconstruído, quando se abre
/guaranteese o diálogo de emissão em 375px e em desktop, entãoscrollWidth - clientWidth == 0nos dois e o botão "Emitir garantia" não trunca. - [x] CA12. Dada uma URL antiga
/app/contractsdigitada à 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/srcretorna apenas ocorrências deContractTemplate/contract_templatese 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êsUPDATEde dados (RN4, RN6, RN7).downgradesimétrico e testado. - [x]
apps/api—models/contract.py→guarantee.py(Guarantee,GuaranteeStatus,guarantee_number),models/__init__.py, e os relacionamentos emcredit_analysis.py,billing.py,document.py - [x]
apps/api—NumberingType.GUARANTEEe prefixo"G"no service - [x]
apps/api—schemas/,repositories/,services/(com atenção aoperations_center_service.py, o mais afetado) - [x]
apps/api—routers/contracts.py→guarantees.py, registro emapi/v1/__init__.py - [x]
apps/api—rbac_seed.pycomguarantees:*e descrições em português - [x]
apps/api— docstrings + Swagger em tudo que foi renomeado, e a docstring de esclarecimento emcontract_template.py - [x]
apps/api— ajustar os quatro testes existentes; cobrir RN5, RN6 e RN7 - [x]
apps/api—make testverde 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) ecredit-analyses/show.tsx - [x]
apps/web— desambiguar os textos de RN9, um a um - [x]
apps/web—data-testidrenomeados, listados no Registro - [x]
apps/e2e—credit-analyses.spec.tsechrome-borders.spec.ts; conferir seeds - [x]
make testemake lint(comparar apontamentos antes/depois, não o total) - [x]
/devremote-rebuild+ screenshot nos dois viewports (CA11, CA12) - [x]
grepfinal 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:
Gpara 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/0006foi mesclado namainantes 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 limitaguarantee_months. É o prazo da garantia. Ficou "Garantias acima deste prazo serão bloqueadas".start_date/end_dateda 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 contratoacompanhapkg * 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:
- Identificadores (47 arquivos Python, 28 do web), com
ContractTemplate/contract_templateprotegidos por sentinela. - 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 noCLAUDE.mde 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 emtest_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 HEADcopiado 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 doRecordCard, e oCLAUDE.mddo 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¶
ANALYSTnão emite garantia. A RN10 dizia "ADMIN_TENANTeANALYSTcom escrita"; o teste no ambiente devolveu 403 paraANALYST. Conferido contra oHEAD:ANALYSTtinhacontracts:list,reade agora temguarantees: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/alembicaninhou 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 headrodou em silêncio e reportouo8q0s2v4x6y7 (head)como se não houvesse nada a fazer. Só a conferência deto_regclass('guarantees')pegou.- Chamei
_settings_factorydepois de emitir a garantia, onde não tem efeito nenhum: é dasTenantSettingsque saem taxa, LMI e carência gravadas no snapshot. Os testes passavam porque o service tolera settings ausentes. A fixture foi para oconftest.py(onde já devia estar — importá-la de outro arquivo de teste geravaF811a 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, emproviders/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:
- 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 semmake migratedeixa código novo contra schema velho — a API procurariaguaranteesnuma base que ainda temcontracts. É o cenário que oCLAUDE.mdda API já descreve como "erro confuso de diagnosticar". - O
shipfaz backup antes de migrar e pede confirmação humana, o que cobre o risco principal. Odowngradefoi exercitado de verdade, mas só contra o dev-remote. shipdeployaorigin/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.