# Provisão 4966 — Esperada / Incorrida / Adicional na tela Carteiras 4966

- **Data:** 2026-07-09
- **Autor:** Bruno + Claude
- **Status:** Aprovado (design) — aguardando revisão da spec
- **Tela alvo:** `/dashboard/credit/risk-4966-levels` (`RiskLevels4966View.vue`, "Carteiras 4966")
- **Regra:** Res. CMN 4.966/2021 (PPEARC / perdas esperadas)

> **Correção durante a implementação (2026-07-09):** a função vigente é a da migration
> **`20260526200000`** (renomeou `ledger_account`→`cosif_account` em `risk_level` e
> `risk_classification_result`), **não** a `20260522100000` citada em §2/§5.1. A migration
> final e o corpo de `fn_run_risk_classification` foram baseados na `20260526200000` (usa
> `cosif_account`). Basear na versão errada quebra a função com
> `column ledger_account does not exist`. Validado ao vivo: backfill exato (mismatches=0),
> zero-regressão (R$653,93→R$653,93, 0 diffs) e delta do feature (Esperada ×2 → provisão ×2).

## 1. Problema

A tela "Carteiras 4966" hoje apresenta uma única alíquota de perda por faixa
(`loss_rate`) mais a `additional_rate`. O negócio trabalha com **três** provisões
distintas — **Esperada**, **Incorrida** e **Adicional** — e cada uma precisa ser
apresentada e ter o percentual ajustável na tela.

## 2. Estado atual (como funciona hoje)

O encanamento contábil das 3 provisões **já existe**; o que falta é alíquota
própria para Esperada vs Incorrida + a superfície na tela.

- **`risk_level`** (parâmetros por faixa): tem só `loss_rate` (perda) e
  `additional_rate` (adicional). As faixas já são separadas por situação:
  adimplente (`is_problematic_asset = false`, dias 0–90) e problemática
  (`is_problematic_asset = true`, dias ≥ 90).
- **`fn_run_risk_classification`** (motor, definido por último na migration
  `20260522100000`, versão com lógica de garantia): para cada contrato calcula
  `days_overdue`, define `is_problematic_asset = days_overdue >= 90`, casa a
  faixa da `risk_level` e grava em `risk_classification_result` o `loss_rate`
  aplicado, o `additional_rate` e
  `provision_amount = saldo × (loss_rate + additional_rate) / 100`.
- **`ProvisionJournal`** (contabilização COSIF): **já divide** a provisão de cada
  contrato em até 3 lançamentos, lendo `rcr.loss_rate`, `rcr.additional_rate` e
  o flag `rcr.is_problematic_asset`:
  - **Incorrida** quando `is_problematic_asset = true` → base `saldo × loss_rate/100`
  - **Esperada** quando `is_problematic_asset = false` → base `saldo × loss_rate/100`
  - **Adicional** → `saldo × additional_rate/100`
  - As contas COSIF de cada balde vêm de `loan_products_historic`
    (`provision_esperada` / `provision_incorrida` / `provision_adicional`, por produto).
- **Consequência:** Esperada e Incorrida compartilham a **mesma** coluna
  (`loss_rate`) roteada pelo flag ≥90d — nunca têm percentuais independentes, e a
  tela mostra só "Perda" + "Adicional".

## 3. Decisões de desenho (confirmadas com o Bruno)

1. **Como as 3 incidem num mesmo contrato** → **Esperada XOR Incorrida + Adicional**:
   - Adimplente (<90d): `provisão = saldo × (Esperada% + Adicional%)`
   - Problemático (≥90d): `provisão = saldo × (Incorrida% + Adicional%)`
   - Esperada e Incorrida **nunca** incidem juntas (roteadas pela régua ≥90d);
     Adicional entra por cima nas duas. Casa com o estágio da 4966 e com o journal atual.
2. **Como guardar as alíquotas** → **colunas explícitas** `expected_rate` e
   `incurred_rate` (mais `additional_rate`, que já existe). Dado auto-explicativo.

## 4. Modelo alvo

Como as faixas já separam adimplente (0–90) de problemática (≥90), a **curva de
Esperada são as faixas 0–90** e a **curva de Incorrida são as faixas ≥90** —
convivem numa tabela só, cada faixa naturalmente numa das duas. A Adicional é
editável em todas.

## 5. Mudanças por camada

### 5.1 Migration (nova) — `20260709120000_add_expected_incurred_rates_to_risk_level.exs`

Transacional (sem `@disable_ddl_transaction`). Faz três coisas:

**a) Schema `risk_level`:**
```sql
ALTER TABLE risk_level ADD COLUMN IF NOT EXISTS expected_rate NUMERIC(5,2) NOT NULL DEFAULT 0.00;
ALTER TABLE risk_level ADD COLUMN IF NOT EXISTS incurred_rate NUMERIC(5,2) NOT NULL DEFAULT 0.00;
```

**b) Backfill a partir do `loss_rate` (zero regressão):**
```sql
UPDATE risk_level SET
  expected_rate = CASE WHEN is_problematic_asset THEN 0 ELSE loss_rate END,
  incurred_rate = CASE WHEN is_problematic_asset THEN loss_rate ELSE 0 END;
```
(As faixas "placeholder" Problemático-Adimplido caem em `incurred_rate` por serem
`is_problematic_asset = true`; são inertes de qualquer forma.)

**c) `CREATE OR REPLACE FUNCTION fn_run_risk_classification`** partindo **literalmente
da versão atual** (a de `20260522100000` up, com garantia), mudando **uma única
linha** dentro do CTE `with_level`:
```sql
-- antes:
n.loss_rate AS loss_rate,
-- depois:
CASE WHEN cl.is_problematic_asset THEN n.incurred_rate ELSE n.expected_rate END AS loss_rate,
```
Todo o resto (garantia, `provision_amount = ROUND(saldo × (loss_rate + additional_rate)/100, 2)`,
INSERT em `risk_classification_result`) fica **idêntico**. O `wl.loss_rate` agora já
é a Esperada ou a Incorrida correta.

**`down`:** dropa as 2 colunas e faz `CREATE OR REPLACE` da função de volta para a
versão de `20260522100000` (voltando a `n.loss_rate AS loss_rate`).

> **Cuidado de implementação:** copiar o corpo da função da migration
> `20260522100000` (o bloco `def up`), não da `20260509130000` (essa é anterior à
> lógica de garantia). Timestamp da nova migration precisa ser posterior a
> `20260527200000`.

### 5.2 `risk_classification_result` e `ProvisionJournal` — **sem mudança**

O motor continua gravando em `rcr.loss_rate` a alíquota aplicada (agora Esperada ou
Incorrida) e em `rcr.additional_rate` a adicional. O journal já roteia
Esperada/Incorrida pelo flag `is_problematic_asset` e usa `rcr.loss_rate` como base
— logo produz Esperada usando `expected_rate` e Incorrida usando `incurred_rate`,
sem tocar em nenhuma linha do `provision_journal.ex`. Soma dos baldes = `provision_amount`.

### 5.3 Schema Ecto — `Monetarie.Schemas.Credit.RiskLevel`

- Adicionar `field :expected_rate, :decimal` e `field :incurred_rate, :decimal`.
- `@editable`: remover `:loss_rate`, adicionar `:expected_rate`, `:incurred_rate`
  (fica: `start_day end_day expected_rate incurred_rate additional_rate cosif_account cosif_account_cp`).
- `validate_required`: trocar `:loss_rate` por `:expected_rate, :incurred_rate`
  (fica: `start_day end_day expected_rate incurred_rate additional_rate`).
- `validate_number` (0–100) para `:expected_rate` e `:incurred_rate` (como o `loss_rate` hoje).
- `field :loss_rate` **permanece no schema** (legado congelado, não mais editável).

### 5.4 Controller — `RiskClassification4966Controller`

- `serialize_level/1`: expor `expected_rate` e `incurred_rate`; manter
  `additional_rate`; remover `loss_rate` do payload.
- `update_level/2`: `Map.take` passa a permitir `"expected_rate"`, `"incurred_rate"`
  no lugar de `"loss_rate"` (mantém `additional_rate`, `start_day`, `end_day`,
  `cosif_account`, `cosif_account_cp`).

### 5.5 Frontend — `RiskLevels4966View.vue` (único arquivo de front)

- `type RiskLevel`: trocar `loss_rate` por `expected_rate` + `incurred_rate`.
- **Tabela:** substituir a coluna "Perda (%)" por duas — **"Esperada (%)"** e
  **"Incorrida (%)"** — e manter **"Adicional (%)"**. Regra de exibição por linha:
  - `is_problematic_asset = false` (Normal): Esperada = `expected_rate`; Incorrida = `–`.
  - `is_problematic_asset = true` (Problemático): Incorrida = `incurred_rate`; Esperada = `–`.
  - Adicional = `additional_rate` sempre.
- **Diálogo de edição (final, ajustado no aceite do Bruno 2026-07-09):** mostra **os 3
  campos sempre editáveis** — Taxa Esperada, Taxa Incorrida, Taxa Adicional — + uma dica
  de qual alíquota entra na situação da faixa. No `save`, envia `expected_rate` +
  `incurred_rate` + `additional_rate` + dias + contas COSIF. Semântica XOR preservada no
  motor: a alíquota de perda não-ativa fica salva, mas não incide naquela faixa (a tabela
  mostra o valor não-ativo em cinza).

## 6. Fora de escopo

- **Telas de resultado (`risk-4966`) e relatórios (PPEARC):** seguem lendo
  `result.loss_rate` (= alíquota aplicada) e funcionam sem alteração.
- **Contas COSIF das 3 provisões:** vivem em `loan_products_historic` por **produto**
  — não são editadas nesta tela de faixas. As colunas COSIF que a tela já tem são
  as de *classificação por carteira* (3.3.2 / 9.3.2) e ficam como estão.
- **Faixas mortas** (placeholder "Problemático Adimplido" + inadimplidas 0–30/31–60):
  limpeza separada, fora deste escopo.
- **Ampliar o critério de "ativo problemático"** além dos 90 dias: fora deste escopo.

## 7. Zero-regressão

Como o backfill copia `loss_rate` para a coluna certa por situação, a **primeira
pré-classificação após o deploy produz exatamente os mesmos números**. A divergência
só aparece quando o operador editar Esperada e/ou Incorrida para valores diferentes.
Linhas históricas de `risk_classification_result` permanecem válidas (não há backfill nelas).

## 8. Plano de validação (stack local, localhost:5175)

1. Rodar a pré-classificação numa data-base e conferir `provision_amount` **inalterado**
   vs. antes da mudança (deve bater ao centavo).
2. Editar uma faixa Esperada (ex.: C1 0–14) para um valor novo, re-rodar, e conferir
   o **delta** apenas nos contratos adimplentes daquela carteira/faixa.
3. Editar uma faixa Incorrida e conferir o delta apenas nos problemáticos.
4. Conferir os baldes do `ProvisionJournal` (lançamentos Esperada vs Incorrida vs
   Adicional) e que a soma = `provision_amount`.
5. Tela: as 3 colunas aparecem; o diálogo salva na coluna certa por situação.

## 9. Arquivos tocados

| # | Arquivo | Mudança |
|---|---------|---------|
| 1 | `priv/repo/migrations/20260709120000_add_expected_incurred_rates_to_risk_level.exs` | **novo** — colunas + backfill + `CREATE OR REPLACE` da função |
| 2 | `lib/monetarie/schemas/credit/risk_level.ex` | fields + changeset (expected/incurred) |
| 3 | `lib/monetarie_web/controllers/credit/risk_classification_4966_controller.ex` | `serialize_level` + `update_level` |
| 4 | `apps/admin/src/views/credit/RiskLevels4966View.vue` | colunas, diálogo, type, payload |

Sem mudança: `provision_journal.ex`, `risk.ex`, `risk_levels.ex` (use case),
`risk_classification_4966.ex`, seed `035_risk_level_cosif_accounts.exs`,
`risk_classification_result` (schema/migração).
