# Design — Empréstimo: opção "Valor Líquido em Conta" (gross-up)

> Data: **2026-07-13**. Branch: `feat/loan-product-required-cosif-accounts`.
> Status: **aprovado no brainstorming**; pendente plano de implementação (writing-plans).
> Área: crédito (`SimulationDialog.vue` + `loans.ex`/`loan_engine.ex`/`loan_simulations.ex`).

## Problema

Hoje o "Valor" informado na simulação é o **principal financiado** (bruto). Os encargos
(IOF, TAC, Prestamista, custos adicionais) são debitados na liberação (D0), então o cliente
recebe **menos** que o informado: `líquido = valor − encargos`. Num contrato de R$ 50.000 com
R$ 2.631 de custos, o cliente recebe R$ 47.369 líquidos na conta.

Falta uma opção para o operador informar o **líquido desejado em conta**: o cliente quer receber
exatamente R$ 50.000, e o sistema deve **grossar** o principal do contrato para que
`principal − encargos = R$ 50.000` — deixando o contrato (e as parcelas) maiores.

## Objetivo

Adicionar um **modo de valor** na simulação de empréstimo:
- **Financiado** (default, = hoje): o valor informado é o principal.
- **Líquido em conta** (novo): o valor informado é o líquido a creditar; o sistema calcula o
  principal grossado tal que o líquido creditado no D0 seja **exatamente** o informado, cobrindo
  **todos** os encargos deduzidos (IOF + TAC + Prestamista + custos adicionais que debitam na conta).

## Modelo de dados

`requested_amount` **sempre** armazena o principal **bruto** (o valor financiado = principal do
contrato). Isso mantém TODO o downstream (cronograma, juros, IOF, desembolso, parcelas, contrato)
**inalterado** — ele já opera sobre `requested_amount = bruto`.

Novos campos em `metadata`:
- `amount_mode`: `"gross"` (default) | `"net"`.
- `requested_net_cents`: presente só no modo `"net"` — o líquido que o cliente pediu (para
  exibição/auditoria; o `requested_amount` guarda o bruto grossado).

**Retrocompat:** sem `amount_mode` (ou `"gross"`) → comportamento **idêntico** ao atual.

## Cálculo do gross-up

Problema circular: os custos dependem do principal, e o principal do líquido desejado.
Solução: **ponto-fixo reusando `Loans.calculate_fees`** (a MESMA função de custos — DRY, sem
duplicar coeficientes; à prova de mudanças futuras de custo).

```
gross_up_principal(liquido_desejado, product, term, opts):
  principal = liquido_desejado
  para i em 1..MAX_ITER (~20):
    fees = calculate_fees(principal, product, term, opts)   # a MESMA função de hoje
    novo = liquido_desejado + fees
    se novo == principal: retorna principal    # convergiu → líquido exato ao centavo
    principal = novo
  # não convergiu (não deve): erro
```

- **Exato ao centavo** (o loop compensa os `floor` dos custos).
- Converge em poucas iterações (a taxa total dos custos é baixa, ~2–8%).
- **Guarda:** se a soma proporcional dos custos ≥ 100% (líquido impossível) ou não convergir em
  `MAX_ITER` → erro claro (ex.: `{:error, :net_funding_infeasible}`).

Local: `loan_engine.ex` (reusando `Loans.calculate_fees`). **Espelhado** no front
(`SimulationDialog.vue`) para o preview, igual `simulatedTotalFees` já espelha `calculate_fees`.

> **Nota de implementação:** `Loans.calculate_fees/1` hoje recebe um `%Loan{}`. O gross-up precisa
> avaliar os custos sobre um **principal candidato** a cada iteração. O plano decide entre expor um
> cálculo de custos sobre `(principal, product, term)` (refactor fino, preferível) ou construir um
> `%Loan{}` efêmero no loop. Regra: **uma fonte única** de cálculo de custos, reusada por
> `calculate_fees` e pelo gross-up — sem duplicar a lógica.

## Frontend (`SimulationDialog.vue`)

### Seletor (step 1, junto do campo "Valor")
`SelectButton` de 2 opções, default **"Financiado"**:
- **Financiado**: valor = principal (hoje). Rótulo do campo: "Valor".
- **Líquido em conta**: valor = líquido desejado. Rótulo/hint: "Líquido desejado em conta".

### Preview (step 2), modo "Líquido em conta"
- Linha nova em **destaque**: "Valor Financiado (principal)" = o bruto grossado.
- "Crédito Líquido na Conta" passa a bater **exato** com o informado (com ✓).
- Parcelas/juros/total automáticos sobre o bruto (o preview usa `amount_cents` = bruto grossado).
- Modo **Financiado**: preview **idêntico ao atual**.

### Validação (mirror do backend)
- O bruto grossado é validado contra `max_amount` do produto; se estourar, erro no campo +
  botão "Próximo" travado (mensagem: *"líquido de R$ X grossa pra R$ Y, acima do máximo R$ Z"*).
- `min_amount` também sobre o bruto.

### Submit
- `requested_amount = valor informado` (no modo net = o líquido) + `metadata.amount_mode`.
- O front **não** precisa mandar o bruto — o backend grossa (autoritativo).

## Backend

### `gross_up_principal/N` (novo, `loan_engine.ex`), reusando `Loans.calculate_fees`.

### `create_simulation` / `create_application`
Ordem:
1. `normalize_attrs`.
2. Se `metadata["amount_mode"] == "net"`:
   - `bruto = gross_up_principal(attrs["requested_amount"], product, term, opts)`.
   - `attrs["requested_amount"] := bruto`; `metadata["requested_net_cents"] := líquido`.
3. `validate_amount_bounds` (sobre o **bruto**) + demais validações.
4. Insere.

Backend é **autoritativo** (não confia num bruto vindo do front).

### `update_simulation`
Se `requested_amount` ou `amount_mode` mudarem, recomputa o gross-up. Incluir `metadata` no
`build_validation_attrs` (mesma correção já feita para a periodicidade) para o modo/valor
fluírem à validação.

## Edge cases

- **Renegociação**: modo "Líquido em conta" **DESABILITADO** (v1). Reneg não credita líquido ao
  cliente (recursos liquidam outros contratos; IOF incide sobre o excedente). O seletor não aparece
  / trava em produto de reneg; backend ignora/rejeita `amount_mode: "net"` em reneg.
- **Convergência**: `MAX_ITER ~20`; falha → erro.
- **Taxa ≥ 100%**: erro (líquido impossível).
- **IOF depende do prazo**: o gross-up itera o principal para o prazo já definido; ok.

## Testes (TDD, backend)

1. Gross-up: `principal − calculate_fees(principal) == líquido_informado` **exato** (várias taxas;
   com custo fixo; com/sem IOF; produto com/sem TAC/Prestamista).
2. Bruto grossado > `max_amount` → **rejeita**.
3. Modo `"gross"`/ausente → comportamento **idêntico** ao atual (retrocompat).
4. Convergência termina; `metadata.requested_net_cents` gravado.
5. Reneg + `amount_mode: "net"` → desabilitado/ignorado.

## Fora de escopo (YAGNI)

- Gross-up em renegociação (v1 desabilita).
- Toggle PF/PJ do IOF (segue como hoje — PF).
- Mudança em qualquer cálculo downstream (schedule/juros/desembolso) — todos reusam o bruto.

## Arquivos afetados (previsão)

| Arquivo | Mudança |
|---|---|
| `core/backend/lib/monetarie/use_cases/credit/loan_engine.ex` | `gross_up_principal/N` (ponto-fixo reusando `calculate_fees`) |
| `core/backend/lib/monetarie/use_cases/credit/loans.ex` | `create_application`: grossa antes de validar bounds (modo net) |
| `core/backend/lib/monetarie/use_cases/credit/loan_simulations.ex` | `create_simulation` + `update_simulation` (metadata no `build_validation_attrs`) |
| `core/apps/admin/src/views/credit/components/SimulationDialog.vue` | SelectButton do modo, mirror do gross-up, preview, validação, submit |
| `core/backend/test/monetarie/credit/*` | testes do gross-up + bounds + retrocompat |
