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.pyaplica taxa e LMI sobrerent_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 porallowed_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, default0. 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(hojerental_value × multiplier). - RN5. Custos de Saída — novo sub-limite:
pacote × exit_costs_multiplier, comexit_costs_multipliernas configurações padrão (default sugerido5;0= sem cobertura de custos de saída). Cobre reparos/pintura/manutenção, multa e taxas de desligamento (Cl. 7ª + Quadro). - RN6.
covers_property_damageemax_property_damage_valuesão removidos das configurações padrão — substituídos porexit_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_feeeproperty_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 camposcondo_fee,property_tax(Numeric(12,2), default 0) + snapshotsfinancial_rental_package,financial_exit_costs_value. Docstrings definancial_covers_property_damage/financial_max_property_damage_value/financial_payment_due_daymarcadas como legado (RN8).models/tenant_settings.py— adicionaexit_costs_multiplier(Integer, default 5); removepayment_due_day,min_rental_value,max_rental_value,min_guarantee_months,max_guarantee_months,covers_property_damage,max_property_damage_value.models/contract.py— snapshotsrental_package,exit_costs_value(ao lado dolmi_value).services/pricing.py— assinatura passa a receber o pacote (ou os três componentes) em vez derent_value; calculamonthly_value,lmi_valueeexit_costs_valuesobre 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, adicionaexit_costs_multiplier=5.schemas/proposal.py,schemas/tenant_settings.py— campos novos/removidos, comdescriptionno Swagger; exemplos atualizados.api/v1/routers/tenant_settings.py— remover a lógica de sincroniapayment_due_day↔allowed_due_dayse atualizar a documentação do endpoint.- Migration Alembic — add columns (proposal/contract/settings) + drop columns (settings). Backfill:
financial_rental_packagedas propostas antigas =rental_value(Pacote = aluguel, coerente com RN2). - Testes —
pricingcom 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 doui-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ó emallowed_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.pysobre o Pacote (taxa, LMI, Custos de Saída) +proposal_servicepopulando os snapshots +_default_settings - [x]
apps/api— schemas e routers (Swagger:summary/description/response_description) + remover a sincroniapayment_due_day↔allowed_due_days - [x]
apps/api— migration Alembic (add + drop + backfill do Pacote = aluguel) - [x]
apps/api— testes depricing(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_valuesubstituídos porexit_costs_multipliernas 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) + snapshotsfinancial_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 purasrental_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 emguaranteed_valuepara propostas antigas) e grava os snapshots.- Migration
k4m6o8q0s2t3com backfill (financial_rental_package = rental_value,contracts.rental_package = guaranteed_value) edowngraderecriando 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_valueemin/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" emcreate.tsxeedit.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.tsxtem a própria implementação do resumo, além docomponents/FinancialSummaryPanel.tsxusado 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íaTenantSettingscom os campos removidos — ajustada. - Nit pendente: o painel de resumo ainda nomeia a prop de entrada
rentalValueembora 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.