0003 — Análise completa: taxa setup pelo analista, cláusula obrigatória e corresponsável¶
| Status | CONCLUÍDA |
| Branch | spec/0003-analise-completa |
| Criada em | 2026-07-26 |
| Apps afetados | api · web · e2e |
Status:
RASCUNHO→APROVADA→EM IMPLEMENTAÇÃO→CONCLUÍDAPrimeira entrega (E1) do plano por jornada do
ROADMAP.md— etapa Análise / Contrato. Constrói a lógica de domínio da análise mais rica; a ativação real (assinatura Autentique + boleto) fica para o E4. Regras confirmadas pelo contrato real da TXC Garantia (docs/contratos/).
1. Problema¶
A análise de crédito hoje é rasa demais para refletir o que o negócio realmente decide:
- O analista não arbitra a taxa setup. O negócio diz que a taxa de adesão "vai do analista", mas na aprovação ele só ajusta a taxa de garantia (
approved_guarantee_rate_percent). A taxa setup vem fixa do cálculo do tenant, sem espaço para o analista arbitrar por caso. - Não há checkpoint da Cláusula Obrigatória. O contrato só é válido se a cláusula obrigatória estiver no contrato de locação (responsabilidade do parceiro). Hoje nada força essa verificação — dá para aprovar uma garantia que nasce inválida.
- Não existe corresponsável. O contrato prevê que a garantidora pode exigir um corresponsável (avalista PF, analisado, solidário). Hoje só há inquilino e proprietário — não há onde registrar o corresponsável nem somar a renda dele na análise.
2. Objetivo¶
Na aprovação, o analista arbitra a taxa setup e confirma a cláusula obrigatória (sem a qual não aprova). E a proposta passa a aceitar corresponsáveis, cuja renda soma no cálculo 3× — dando ao analista o quadro completo para decidir.
3. Fora de escopo¶
- Ativação real (geração do documento, assinatura Autentique/biometria, boleto inicial, gate de "só ativa após assinar + pagar") — é o E4, por último.
- Cobrança/Fechamento (E2) e Sinistro/Regresso (E3).
- Consulta a bureaus (Rede Busca / Caixa Aqui / LOFT) — decidido fora de escopo; a análise segue com a regra 3× (agora incluindo o corresponsável).
- Limites (min/max) para a taxa setup — decidido: valor livre, sem faixa.
- Portal da imobiliária e RBAC 10 papéis — trilha de base, specs próprias.
4. Regras de negócio¶
- RN1. Taxa setup arbitrada pelo analista. Na aprovação, o analista pode informar
approved_initial_fee_value(R$,≥ 0, livre, sem min/max). É persistido naAnalysise propaga para o snapshotContract.initial_feena criação do contrato (recompondototal_upfront). Se omitido, mantém o cálculo padrão do tenant (initial_feecomputado hoje). Permissão:analysis:approve. - RN2. Cláusula Obrigatória bloqueia a aprovação. Aprovar exige
mandatory_clause_confirmed = trueno payload. Sefalse/ausente, a API responde422 MANDATORY_CLAUSE_NOT_CONFIRMEDe nada muda (proposta segue em análise). A confirmação é registrada naAnalysis; o "quem/quando" é oanalyst_id+ o momento da decisão. Rejeição e pendência não exigem a confirmação. - RN3. Corresponsável é participante opcional (0..N). Novo papel
ParticipantRole.CO_RESPONSIBLE, sempre pessoa física, comdeclared_income. Zero ou mais por proposta. Adicionado/editado/removido pelos mesmos papéis que gerenciam participantes da proposta (OPERATOR,ADMIN_TENANT,AGENCY_USERno escopo da agência). Nuncais_primary. - RN4. Renda do corresponsável entra na regra 3×.
compute_credit_summarysoma adeclared_incomede participantesLESSEEeCO_RESPONSIBLE. Ototal_declared_income, oincome_to_rent_ratio, acredit_recommendatione as notas recalculam considerando os dois papéis. - RN5. Recálculo em análise ativa. Adicionar/remover/editar um corresponsável (ou sua renda) com uma análise ativa recomputa o resumo de crédito — mesmo comportamento que já existe ao mexer na renda do inquilino.
- RN6. Corresponsável PF. Se o cadastro de
Persondistinguir PF/PJ, o participanteCO_RESPONSIBLEdeve ser PF; senão,422. (SePersonnão tiver o tipo hoje, fica só como docstring/validação de UI — anotar no Registro.)
5. Impacto por app¶
apps/api¶
- Modelos/migrations:
models/proposal_participant.py— novo valorCO_RESPONSIBLEno enumParticipantRole(migration Alembic para o enumparticipant_role).models/analysis.py— novos camposapproved_initial_fee_value(Numeric(12,2), nullable) emandatory_clause_confirmed(Boolean, defaultfalse).- Schemas:
AnalysisDecisionRequest—approved_initial_fee_value: Decimal | None (ge=0)emandatory_clause_confirmed: bool = False.AnalysisResponse— expor os dois novos campos.- Schema de participante — aceitar
role=CO_RESPONSIBLE. - Regras no service:
analysis_service.approve()— validar RN2 (bloquear sem cláusula), gravarmandatory_clause_confirmedeapproved_initial_fee_valuena análise.- Criação do contrato — usar
approved_initial_fee_valuecomoContract.initial_feequando presente (senão o cálculo atual), recompondototal_upfront. services/credit.pycompute_credit_summary— passar a considerarCO_RESPONSIBLE(ajustar a assinatura/os callers que hoje filtram sóLESSEE).- Docstrings + Swagger —
approve(novos campos e o erroMANDATORY_CLAUSE_NOT_CONFIRMED), o modelAnalysis, o enumParticipantRole, e ocompute_credit_summary.
apps/web¶
- Página de decisão da análise (aprovação):
- Campo "Taxa setup (R$)" opcional (
data-testid="analysis-approve-setup-input"). - Checkbox "Cláusula obrigatória incluída no contrato de locação" — (
data-testid="analysis-approve-clause-checkbox"); o botão de aprovar fica desabilitado enquanto não marcado (e o backend reforça — RN2). - Mensagem de erro (
data-testid="analysis-approve-error"). - Formulário da proposta: seção de Corresponsável(is) (como a de inquilinos), com nome/pessoa + renda declarada, adicionar/remover.
data-testid:proposal-coresponsible-add,proposal-coresponsible-{i}-person-input,proposal-coresponsible-{i}-income-input,proposal-coresponsible-{i}-remove.- Resumo de crédito na tela da análise reflete a renda somada (inquilinos + corresponsáveis).
- Sider: sem impacto (não é módulo novo).
apps/e2e¶
- Specs:
- Aprovar sem marcar a cláusula → bloqueado (botão desabilitado; e API responde 422).
- Aprovar com taxa setup arbitrada → o contrato gerado tem
initial_fee= valor arbitrado. - Proposta com renda-limite (< 3×) que passa a ≥ 3× ao adicionar um corresponsável.
- Seeds: proposta com inquilino de renda ~2,5× o aluguel (borderline
MANUAL_ANALYSIS) para exercitar o efeito do corresponsável emtests/fixtures/seeds/.
6. Contrato de API¶
POST /api/v1/proposals/{proposal_id}/analysis/approve
Authorization: Bearer <token> (analysis:approve)
{
"reason": "Aprovado — renda adequada com corresponsável.",
"approved_guarantee_rate_percent": "12.50", // já existente, opcional
"approved_initial_fee_value": "300.00", // novo, opcional (R$, livre)
"mandatory_clause_confirmed": true // novo, obrigatório = true p/ aprovar
}
{
"id": "uuid",
"status": "APPROVED",
"approved_guarantee_rate_percent": "12.50",
"approved_initial_fee_value": "300.00",
"mandatory_clause_confirmed": true,
"total_declared_income": "6000.00",
"income_to_rent_ratio": "3.20",
"credit_recommendation": "APPROVE"
}
Adicionar corresponsável usa o endpoint de participantes da proposta com role=CO_RESPONSIBLE (mesmo contrato dos demais participantes; o path exato é confirmado na implementação).
Erros:
| HTTP | Código | Quando |
|---|---|---|
| 422 | MANDATORY_CLAUSE_NOT_CONFIRMED | Aprovar sem mandatory_clause_confirmed=true (RN2) |
| 422 | RATE_OUT_OF_RANGE | (já existente) taxa de garantia fora de min/max |
| 422 | (validação) | approved_initial_fee_value negativo, ou corresponsável PJ (RN6) |
7. Critérios de aceite¶
- [x] CA1. Dado uma proposta em análise, quando o analista aprova sem confirmar a cláusula obrigatória, então a API responde
422 MANDATORY_CLAUSE_NOT_CONFIRMEDe a proposta continua em análise (não viraAPPROVED). (API:test_approve_requires_mandatory_clause; e2e: botão desabilitado.) - [x] CA2. Dado a aprovação com
approved_initial_fee_value = 300,00, quando o contrato é criado, entãoContract.initial_fee = 300,00etotal_upfrontreflete esse valor. (API:test_approved_initial_fee_propagates_to_contract; e2e: setup arbitrado → contrato.) - [x] CA3. Dado a aprovação sem
approved_initial_fee_value, então a taxa setup do contrato é a calculada pelo tenant (comportamento atual, inalterado). (API:test_default_initial_fee_used_when_not_arbitrated.) - [x] CA4. Dado uma proposta com um inquilino de renda 2,5× o aluguel (
MANUAL_ANALYSIS), quando um corresponsável com renda suficiente é adicionado, entãoincome_to_rent_ratio ≥ 3×ecredit_recommendation = APPROVE. (API:test_co_responsible_income_counts_in_ratio; e2e: corresponsável pela UI.) - [x] CA5. Na tela de aprovação, o botão de aprovar fica desabilitado até a cláusula ser marcada; e o campo de taxa setup aceita valor em R$. (e2e:
aprovação exige a cláusula obrigatória.) - [x] CA6. Um corresponsável pode ser adicionado/removido na proposta; ao remover, o resumo de crédito recalcula (RN5). (API:
test_add_remove_co_responsible_recomputes_analysis.)
8. Plano de implementação¶
Ordem obrigatória no monorepo: api → web → e2e.
- [x]
apps/api— enumCO_RESPONSIBLE; camposapproved_initial_fee_valueemandatory_clause_confirmednaAnalysis; schemas - [x]
apps/api—credit.pysomaLESSEE+CO_RESPONSIBLE;approve()valida RN2 e grava setup; contrato usa o setup arbitrado - [x]
apps/api— migration Alembic (enum + colunas) - [x]
apps/api— docstrings + Swagger (summary,description,response_description) - [x]
apps/api— testes: bloqueio da cláusula, setup no contrato, corresponsável no ratio - [x]
apps/web— campos de setup + cláusula na aprovação; seção de corresponsável na proposta - [x]
apps/e2e— spec dos 3 fluxos + seed borderline - [x]
make testemake lintsem apontamento novo nos arquivos tocados
9. Decisões em aberto¶
Nenhuma — as quatro dúvidas foram resolvidas na entrevista de 2026-07-26:
- Taxa setup = valor R$ livre (omite → padrão do tenant).
- Cláusula obrigatória = confirmação que bloqueia a aprovação.
- Renda do corresponsável = soma no 3×.
- Vários corresponsáveis por proposta.
RN6 (PF do corresponsável) depende de o Person ter tipo PF/PJ — se não tiver, vira validação de UI/docstring; anotar no Registro na implementação.
10. Registro¶
Implementado em 2026-07-26 na branch spec/0003-analise-completa, na ordem api → web → e2e.
O que foi feito¶
apps/api - ParticipantRole.CO_RESPONSIBLE adicionado ao enum (models/proposal_participant.py). - Analysis.approved_initial_fee_value (Numeric(12,2), nullable) e Analysis.mandatory_clause_confirmed (Boolean, default False) (models/analysis.py). - Schemas: AnalysisDecisionRequest ganhou approved_initial_fee_value (≥ 0) e mandatory_clause_confirmed; AnalysisResponse expõe ambos; descrição de role no ParticipantInput cita CO_RESPONSIBLE. - credit.py: parâmetro renomeado para income_participants (soma LESSEE + CO_RESPONSIBLE); novo repo get_income_participants. Callers em analysis_service.create() e proposal_participant_service._recompute_credit_if_needed() passaram a usá-lo. - analysis_service.approve(): bloqueia sem mandatory_clause_confirmed (MANDATORY_CLAUSE_NOT_CONFIRMED, HTTP 422) antes de qualquer mutação; grava approved_initial_fee_value e marca a cláusula. - contract_service.create(): o setup arbitrado sobrepõe pricing.initial_fee e recompõe total_upfront. - RN3 (corresponsável nunca principal) aplicada em proposal_participant_service.add() e em proposal_service.create() (criação inline). RN5: recompute também em add()/remove(). - i18n analysis.mandatory_clause_not_confirmed (pt_BR/en_US). Swagger do approve e do add_participant atualizado. - RN6 (PF/PJ): Person.person_type (PF/PJ, default PF) + Person.cnpj; guarda CO_RESPONSIBLE_MUST_BE_PF em proposal_participant_service.add() e proposal_service.create(); PersonSummary/PersonCreate/PersonUpdate/PersonResponse expõem os campos. - Concorrência: numbering_sequence_repo.next_seq() tornou-se atômico (savepoint + retry no IntegrityError), corrigindo o HTTP 500 na criação concorrente de propostas (afetava contratos/billing também). - Migrations a1c3e5g7i9k2_analise_completa (enum CO_RESPONSIBLE + colunas da Analysis) e b2d4f6h8j0m3_person_type_pf_pj (enum person_type + cnpj) — ambas aplicadas e testadas em ida/volta no PostgreSQL real. - 7 testes novos em tests/test_analise_completa.py; suíte: 157 passando.
apps/web - AnalysisPanel: campo "Taxa setup (R$)" (analysis-approve-setup-input), checkbox da cláusula (analysis-approve-clause-checkbox) que desabilita o botão de aprovar (analysis-approve-button) até ser marcada, erro (analysis-approve-error), e leitura da taxa setup aprovada no estado terminal. - api.ts: CO_RESPONSIBLE no tipo, campos novos em Analysis, approveAnalysis estendido. - Formulário da proposta (create.tsx): seção Corresponsáveis com linhas adicionar/remover (proposal-coresponsible-add, -{i}-person-input, -{i}-income-input, -{i}-remove). edit.tsx: grupo de corresponsável no editor de participantes; show.tsx passou a renderizar o grupo. i18n pt_BR/en_US. - ConfirmDialog: adicionados confirm-dialog, confirm-dialog-confirm, confirm-dialog-cancel, confirm-dialog-reason-input (faltavam e o e2e precisava). - pnpm build (tsc + vite) limpo.
apps/e2e - tests/e2e/proposals-analysis.spec.ts — 3 fluxos (cláusula bloqueia; setup → contrato; corresponsável pela UI eleva a análise a ≥ 3×). Setup do mundo via API; UI exercida na tela de aprovação e na seção de corresponsável. 3 passando (chromium). - Seed tests/fixtures/seeds/proposals/borderline.json.
Divergências do plano e decisões¶
- RN6 (corresponsável PF) — implementada de fato (decisão do usuário em 2026-07-26): o
Personganhouperson_type(PF/PJ, defaultPF) ecnpj(migrationb2d4f6h8j0m3, aplicada e testada em ida/volta). O backend bloqueia adicionar um corresponsável PJ (CO_RESPONSIBLE_MUST_BE_PF, HTTP 422) tanto no endpoint de participantes quanto na criação inline da proposta. No web, o cadastro de pessoa tem seletor PF/PJ + campo CNPJ (com máscara/validação), e as listas de seleção de corresponsável filtram para PF. Testetest_co_responsible_must_be_pf. i18nproposal.co_responsible_must_be_pf. (Superou a §9, que admitia deixar só como docstring.) - CA6 — recompute na remoção: coberto por teste de API (
add/removerecomputam quando há análise ativa emIN_PROGRESS/PENDING). Pela UI, a edição de participantes é DRAFT-only (a tela de edição bloqueia outros status), então o e2e demonstra o corresponsável somando renda via a UI em DRAFT + análise aberta na sequência; o recompute dinâmico em análise ativa fica coberto pelo teste de API. Melhoria futura: permitir editar participantes emPENDING_DOCSpela UI para refletir o recompute ao vivo. - Bug de concorrência corrigido: criar propostas concorrentemente dava HTTP 500 — corrida no
next_seqda numeração (proposal_number): duas transações liamNonee inseriam a mesma linha. Corrigido emnumbering_sequence_repo.py— o insert roda num savepoint (begin_nested) e, noIntegrityErrorda constraint única, cai no caminho de incremento (a linha já existe). Corrige também contratos/billing (mesmo mecanismo). O e2e roda os três fluxos em paralelo (3 workers) sem 500, comprovando a correção. - Lint:
ruffnos arquivos tocados só acusaE501(estilo herdado, o projeto nunca rodouruff format) — nenhum apontamento novo de outra categoria; a migration ficou 100% limpa.tscdo web limpo.