Pular para conteúdo

0006 — Pacote Locatício como base de cálculo + limpeza das configurações padrão

Status CONCLUÍDA
Branch spec/0006-pacote-locaticio-e-limpeza-de-settings
Criada em 2026-07-29
Apps afetados api · web · e2e

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

1. Problema

O contrato real (docs/contratos/: Termo + Quadro Resumo) calcula tudo sobre o Pacote Locatício = Aluguel + Condomínio + IPTU. O sistema só conhece o aluguel (Proposal.rental_value): não existe campo de condomínio nem de IPTU em nenhum model.

Consequências hoje:

  • Taxa, LMI e cobertura saem errados em relação ao Quadro: services/pricing.py aplica taxa e LMI sobre rent_value, não sobre o Pacote.
  • Custos de Saída não existem como conceito. O contrato define um sub-limite próprio (ex.: 5× o Pacote) para reparos/pintura/multa; o sistema só tem um teto fixo genérico de dano (covers_property_damage + max_property_damage_value), que é snapshotado mas nunca aplicado.

Em paralelo, as configurações padrão acumularam campos que não fazem nada:

  • payment_due_day — redundante: foi substituído por allowed_due_days, e o router mantém os dois em sincronia artificialmente.
  • min_rental_value / max_rental_value / min_guarantee_months / max_guarantee_months — gravados no onboarding e editáveis na UI, mas nenhuma regra valida nada contra eles. Configuração de enfeite, que sugere ao usuário um controle que não existe.

2. Objetivo

Fazer o sistema calcular sobre a base correta do contrato (Pacote Locatício), com Custos de Saída como sub-limite próprio, e deixar as configurações padrão contendo só o que de fato governa o comportamento.

3. Fora de escopo

  • Processo de sinistro (E3): aqui só se define a base e os limites (LMI e Custos de Saída). Nada de abertura/validação/pagamento de sinistro.
  • Cobrança/boleto e fechamento mensal (E2), assinatura (E4).
  • Reajuste anual do Pacote e renovação da garantia.
  • Recalcular propostas/contratos já existentes — snapshots antigos são imutáveis (ver RN8).

4. Regras de negócio

  • RN1. Pacote Locatício = rental_value + condo_fee + property_tax, todos da proposta. É a base de cálculo única.
  • RN2. Condomínio (condo_fee) e IPTU (property_tax) são opcionais, default 0. Ausentes ⇒ Pacote = aluguel (comportamento atual preservado).
  • RN3. Taxa de garantia passa a incidir sobre o Pacote: monthly_value = pacote × guarantee_rate_percent / 100 (hoje usa só o aluguel).
  • RN4. LMI = pacote × lmi_months_multiplier (hoje rental_value × multiplier).
  • RN5. Custos de Saída — novo sub-limite: pacote × exit_costs_multiplier, com exit_costs_multiplier nas configurações padrão (default sugerido 5; 0 = sem cobertura de custos de saída). Cobre reparos/pintura/manutenção, multa e taxas de desligamento (Cl. 7ª + Quadro).
  • RN6. covers_property_damage e max_property_damage_value são removidos das configurações padrão — substituídos por exit_costs_multiplier (um só conceito, alinhado ao contrato).
  • RN7. Campos removidos das configurações padrão (sem substituto, pois não governavam nada): payment_due_day, min_rental_value, max_rental_value, min_guarantee_months, max_guarantee_months.
  • O dia de vencimento passa a ser exclusivamente allowed_due_days.
  • RN8. Snapshots históricos são preservados. As colunas de snapshot já existentes em Proposal (financial_covers_property_damage, financial_max_property_damage_value, financial_payment_due_day) não são dropadas — guardam o que valeu em propostas antigas. Deixam de ser populadas com base nas settings removidas e ficam marcadas como legado nas docstrings.
  • RN9. Novos snapshots na proposta e no contrato: Pacote e Custos de Saída calculados na criação, para o valor não mudar se as settings mudarem depois.
  • RN10. Validações: condo_fee e property_tax ≥ 0; exit_costs_multiplier ≥ 0 (inteiro). Aluguel segue > 0.

Permissões: sem mudança. Editar configurações padrão continua com quem já podia (ADMIN_TENANT / ADMIN_GLOBAL); criar proposta, com quem já podia.

5. Impacto por app

apps/api

  • models/proposal.py — novos campos condo_fee, property_tax (Numeric(12,2), default 0) + snapshots financial_rental_package, financial_exit_costs_value. Docstrings de financial_covers_property_damage / financial_max_property_damage_value / financial_payment_due_day marcadas como legado (RN8).
  • models/tenant_settings.py — adiciona exit_costs_multiplier (Integer, default 5); remove payment_due_day, min_rental_value, max_rental_value, min_guarantee_months, max_guarantee_months, covers_property_damage, max_property_damage_value.
  • models/contract.py — snapshots rental_package, exit_costs_value (ao lado do lmi_value).
  • services/pricing.py — assinatura passa a receber o pacote (ou os três componentes) em vez de rent_value; calcula monthly_value, lmi_value e exit_costs_value sobre ele. É o coração da mudança — hoje o módulo tem ~20% de cobertura, então os testes vêm junto.
  • services/proposal_service.py — calcula o Pacote, popula os snapshots novos, para de ler as settings removidas.
  • services/onboarding_service.py (_default_settings) — remove os campos dropados, adiciona exit_costs_multiplier=5.
  • schemas/proposal.py, schemas/tenant_settings.py — campos novos/removidos, com description no Swagger; exemplos atualizados.
  • api/v1/routers/tenant_settings.py — remover a lógica de sincronia payment_due_day ↔ allowed_due_days e atualizar a documentação do endpoint.
  • Migration Alembic — add columns (proposal/contract/settings) + drop columns (settings). Backfill: financial_rental_package das propostas antigas = rental_value (Pacote = aluguel, coerente com RN2).
  • Testes — pricing com e sem condomínio/IPTU; LMI e Custos de Saída; proposta que omite os campos (Pacote = aluguel); settings sem os campos removidos.

apps/web

  • pages/proposals/ (create/edit/show) — campos Condomínio e IPTU ao lado do Aluguel, em BRL (padrão de campo monetário do ui-patterns.md), com o Pacote Locatício exibido como total calculado (leitura), e Taxa/LMI/Custos de Saída derivados dele no resumo financeiro.
  • pages/settings/index.tsx — remover os 7 campos dropados (hoje presentes nas interfaces do form e do payload, linhas ~43–77) e adicionar "Múltiplo de Custos de Saída"; o dia de vencimento fica só em allowed_due_days.
  • data-testid: proposal-condo-fee-input, proposal-property-tax-input, proposal-rental-package-value, proposal-exit-costs-value, settings-exit-costs-multiplier-input.
  • Responsividade & PWA: os campos novos entram na grade responsiva dos formulários — validar desktop e mobile (coluna única no mobile, spec 0004). Sem impacto de PWA além do cache normal de assets.

apps/e2e

  • Spec cobrindo: criar proposta com condomínio e IPTU → Pacote e valores derivados corretos na tela; criar sem eles → Pacote = aluguel.
  • Settings: os campos removidos não aparecem mais; o múltiplo de Custos de Saída salva e persiste.
  • Rodar em desktop e mobile.

6. Contrato de API

Proposta (campos novos, opcionais):

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

{
  "rental_value": "2500.00",
  "condo_fee": "450.00",
  "property_tax": "180.00"
}
{
  "id": "uuid",
  "rental_value": "2500.00",
  "condo_fee": "450.00",
  "property_tax": "180.00",
  "financial_rental_package": "3130.00",
  "financial_monthly_value": "344.30",
  "financial_lmi_value": "112680.00",
  "financial_exit_costs_value": "15650.00"
}

Configurações padrão — PATCH /api/v1/onboarding/settings: passa a aceitar exit_costs_multiplier; deixa de aceitar payment_due_day, min_rental_value, max_rental_value, min_guarantee_months, max_guarantee_months, covers_property_damage, max_property_damage_value.

Erros: 422 para condo_fee/property_tax negativos ou exit_costs_multiplier negativo. Campo removido enviado é ignorado (não quebra integração) — documentar.

7. Critérios de aceite

  • [x] CA1. Proposta com aluguel 2.500 + condomínio 450 + IPTU 180 mostra Pacote 3.130,00, e Taxa/LMI/Custos de Saída calculados sobre 3.130,00.
  • [x] CA2. Proposta sem condomínio e IPTU tem Pacote = aluguel e os mesmos valores de antes desta spec (nenhuma regressão).
  • [x] CA3. Com exit_costs_multiplier = 5, os Custos de Saída = 5× o Pacote, apresentados separados do LMI.
  • [x] CA4. As configurações padrão não exibem nem aceitam os 7 campos removidos; o dia de vencimento é configurado só por allowed_due_days.
  • [x] CA5. Propostas/contratos anteriores seguem consultáveis com seus valores originais (snapshots preservados) — nada recalculado retroativamente.
  • [x] CA6. Formulário de proposta com os campos novos funciona em desktop e mobile.

8. Plano de implementação

Ordem obrigatória: api → web → e2e.

  • [x] apps/api — models: condo_fee/property_tax + snapshots (proposal, contract); settings: +exit_costs_multiplier, −7 campos
  • [x] apps/api — pricing.py sobre o Pacote (taxa, LMI, Custos de Saída) + proposal_service populando os snapshots + _default_settings
  • [x] apps/api — schemas e routers (Swagger: summary/description/ response_description) + remover a sincronia payment_due_day↔allowed_due_days
  • [x] apps/api — migration Alembic (add + drop + backfill do Pacote = aluguel)
  • [x] apps/api — testes de pricing (com/sem cond+IPTU, LMI, Custos de Saída) e settings
  • [x] apps/web — proposta: Condomínio, IPTU, Pacote calculado e Custos de Saída no resumo; settings: −7 campos, +múltiplo de Custos de Saída; data-testid
  • [x] apps/e2e — cenários com/sem cond+IPTU e settings limpas, em desktop e mobile
  • [x] make test (173 passed) e tsc do web limpo — sem apontamento novo
  • [x] Validado no browser em desktop e mobile (e2e 6 cenários + screenshots)

9. Decisões em aberto

Resolvidas com o usuário (2026-07-29):

  • [x] Base de cálculo — Taxa, LMI e Custos de Saída todos sobre o Pacote (fiel ao Quadro). Assumido que o valor da taxa cobrada muda para propostas novas.
  • [x] Condomínio/IPTU — na proposta, opcionais com default 0.
  • [x] Dano ao imóvel — covers_property_damage/max_property_damage_value substituídos por exit_costs_multiplier nas settings.
  • [x] Campos mortos — remover todos os 5 (payment_due_day, min/max_rental_value, min/max_guarantee_months).

10. Registro

apps/api

  • Models: proposals.condo_fee / property_tax (default 0) + snapshots financial_rental_package / financial_exit_costs_value; contracts.rental_package / exit_costs_value; tenant_settings.exit_costs_multiplier (default 5) e remoção dos 7 campos. Snapshots legados da proposta preservados e marcados nas docstrings (RN8).
  • pricing.py: rent_value → package_value; novas funções puras rental_package(), calculate_lmi(), calculate_exit_costs().
  • proposal_service: compõe o Pacote e popula os snapshots novos; parou de ler as settings removidas. contract_service: base = proposal.financial_rental_package (cai em guaranteed_value para propostas antigas) e grava os snapshots.
  • Migration k4m6o8q0s2t3 com backfill (financial_rental_package = rental_value, contracts.rental_package = guaranteed_value) e downgrade recriando as colunas com os defaults antigos (valores originais não são recuperáveis — está documentado).
  • Testes: 11 novos (Pacote, taxa sobre o Pacote, LMI, Custos de Saída). Suíte 173 passed (era 137).

apps/web

  • Proposta: campos Condomínio e IPTU + Pacote Locatício exibido; o resumo financeiro passa a receber o Pacote como base (taxa, LMI e Custos de Saída).
  • Settings: painéis "Prazo de garantia", "Valor do aluguel" e "Cobertura de danos" removidos; entra Multiplicador de Custos de Saída.
  • Rótulos corrigidos para falar "pacote" em vez de "aluguel" (o painel exibia o Pacote rotulado como "Aluguel mensal").

apps/e2e

pacote-locaticio.spec.ts — 6 cenários (Pacote com/sem cond+IPTU, settings limpas, mobile). 6 passed.

Divergências e achados

  • A remoção era maior do que a spec supunha. A spec dizia que min/max_rental_value e min/max_guarantee_months "não validavam nada" — verdade na API, mas o web validava no cliente (bloqueava o envio fora da faixa e exibia "Faixa: R$ X – R$ Y" em create.tsx e edit.tsx). Levado ao usuário, que decidiu remover mesmo assim (a validação era contornável por API e dava falsa sensação de controle). Essa validação client-side foi removida junto.
  • Cópia local do painel financeiro: create.tsx tem a própria implementação do resumo, além do components/FinancialSummaryPanel.tsx usado por edit/show — os dois precisaram do mesmo ajuste. Duplicação preexistente, candidata a unificação futura.
  • Fixture de teste (test_analise_completa.py) construía TenantSettings com os campos removidos — ajustada.
  • Nit pendente: o painel de resumo ainda nomeia a prop de entrada rentalValue embora receba o Pacote (documentado por comentário). Renomear é cosmético.

Deploy e evidências

API + web rebuildados no dev-remote; migration aplicada e conferida no banco (exit_costs_multiplier presente, payment_due_day/min_rental_value ausentes). Evidências: proposta-pacote.png (Pacote 3.130 → taxa R$ 313/mês, LMI R$ 93.900, Custos de saída 5× = R$ 15.650) e settings-custos-saida.png.