# Rotina diária: classificação de risco 4.966 + contabilização da provisão (delta)

- **Data:** 2026-07-10
- **Autor:** Bruno + Claude
- **Status:** Aprovado (design) — a implementar
- **Área:** Crédito / Provisionamento 4.966 / COSIF

## 1. Objetivo

Criar uma rotina **diária** que, **após a correção de juros dos empréstimos**, (a) reclassifica
o risco 4.966 dos contratos e (b) **contabiliza a variação da provisão** (modelo delta:
constituição/reversão do movimento do dia), mantendo o razão COSIF sempre igual à provisão
vigente sem inflar.

## 2. Contexto (o que já existe)

Provisionamento hoje está fragmentado em 3 caminhos:

- `Calculator` → grava `provision_entries` (tem `provision_amount`, `previous_provision`,
  `delta` por contrato/data) — infra de delta existente.
- `RiskClassification4966.run(data)` → `risk_classification_result` (**fonte da tela do Risco**);
  `ProvisionJournal.run(data)` contabiliza a partir dela, porém em **valor cheio**,
  **replace-by-date** (foi feito pra rodar 1x/mês).
- `PPEARC.calculate` → `ppearc_history` (worker `PPEARCBatchWorker`, mensal, **sem** contábil).

O balancete soma os JEs do mês (`SUM(débitos) − SUM(créditos)`, com carry-forward). Logo, rodar
o `ProvisionJournal` (valor cheio) todo dia **empilharia** a provisão. Por isso o modelo diário
é **delta**. `write_off` já reverte a PDD dele na baixa (D 1.6.9.xx) — não duplicar.

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

1. **Cadência:** diária.
2. **Ordem:** roda **depois** da correção de juros. Cron **`0 5 * * *`** (05:00 UTC), após
   `LoanAccrualWorker` (03:30) e `StopAccrualWorker` (04:00). Racional técnico: a provisão depende
   do `outstanding_balance` (atualizado pelos juros) e do `stop_accrual` (aplicado às 04:00).
3. **Modelo contábil:** **delta** (constituição quando sobe, reversão quando cai).
4. **Âncora:** `risk_classification_result` (mesma fonte da tela) — evita divergência tela×contábil.
5. **Coordenação com baixa/quitação:** contrato que **sai** da classificação é revertido **só se
   `status ≠ written_off`** (baixa o `write_off` já trata; quitação/liquidação o diário reverte).
6. **PPEARCBatchWorker mensal:** **intacto** (não mexer no caminho antigo).
7. **Paridade Oban:** registrar o worker em **config.exs E runtime.exs** (lição do fix de hoje).

## 4. Componentes

### 4.1 Worker
`Monetarie.Workers.Credit.RiskProvisioningDailyWorker`
- `use Oban.Worker, queue: :scheduled, max_attempts: 3`
- `perform/1`: `date = Date.utc_today()` → Passo 1 → Passo 2 → log (contratos, movimento total).
- Cron `{"0 5 * * *", ...}` em **ambos** os crontabs.
- **Sem catchup:** delta é sobre o estado atual; pular um dia → o próximo pega a variação
  acumulada (a provisão vigente é reconstruída pela reclassificação do dia).

### 4.2 Passo 1 — Classificar
`RiskClassification4966.run(date)` (reuso direto) → popula `risk_classification_result` do dia.
Obrigatório: se falhar, aborta o dia (retorna erro → Oban retry).

### 4.3 Passo 2 — Contabilizar o delta (núcleo novo)
Novo caminho de contabilização por **movimento**, ancorado em `risk_classification_result`.
Provável forma: nova função `ProvisionJournal.run_movement(date)` (ou módulo
`DailyProvisionMovement`), reusando os helpers de resolução de conta/tag do `ProvisionJournal`.

Algoritmo:
1. **Provisão de hoje** por contrato/bucket = `outstanding × loss_rate/100` (incorrida se
   problemático / esperada se performing) + `outstanding × additional_rate/100` (adicional),
   da `reference_date = date`.
2. **Baseline** por contrato/bucket = mesma fórmula na **reference_date anterior mais recente**
   em `risk_classification_result` (0 se contrato novo).
3. **Contratos que saíram** (estavam na anterior, não estão hoje): baseline com hoje = 0 →
   reversão cheia, **exceto** `status = written_off` (pular — write_off já reverteu).
4. **delta(contrato, bucket) = hoje − baseline.** Agrega os deltas por `(tag, débito, crédito)`.
5. Por bucket agregado:
   - `delta > 0` → **constituição**: D = despesa_provisao(tag) / C = retificadora(tag).
   - `delta < 0` → **reversão**: lançamento inverso (D = retificadora / C = despesa|receita).
   - `delta = 0` → sem JE.
   - Tags por bucket: `provision_{incorrida,esperada,adicional}_{debit,credit}` do
     `loan_products_historic`, com fallback legado (`despesa_provisao`/`provision_esperada`),
     como o `ProvisionJournal` já resolve.
6. **Trilha:** grava `provision_entries` (reusa `provision_amount`/`previous_provision`/`delta`).
7. **Balancete:** `SnapshotGenerator.generate_for_date(date)` ao final (best-effort).
8. `created_by_admin_id = nil` (sistema). `reference_type` próprio (ex.:
   `loan_provision_movement`) pra distinguir do `ProvisionJournal` valor-cheio.

### 4.4 Idempotência
Re-execução no mesmo dia **não pode duplicar** o movimento. Como o delta é vs a data anterior,
uma 2ª execução no mesmo dia calcularia delta 0 (hoje já é o baseline?) — **não**: o baseline é a
`reference_date` ANTERIOR, então re-rodar recalcularia o mesmo delta e duplicaria. Portanto o
Passo 2 deve ser **replace-by-date do próprio movimento**: apaga os JEs `loan_provision_movement`
de `date` antes de repostar (igual ao replace-by-date do ProvisionJournal, mas do movimento).
Assim re-rodar o dia é idempotente.

## 5. Ordem e erro

- Passo 1 obrigatório (falha → aborta o dia, Oban retry).
- Passo 2 fail-soft por bucket (conta COSIF ausente → log + segue; não trava o lote).
- Log final: nº de contratos, constituição total, reversão total, líquido.

## 6. Fora de escopo

- Não mexe no `PPEARCBatchWorker` mensal, no `PPEARC.calculate`/`ppearc_history`.
- Sem mudança de UI. Sem tocar no fluxo manual da tela (ver risco em §8).

## 7. Arquivos

| Arquivo | Mudança |
|---|---|
| `workers/credit/risk_provisioning_daily_worker.ex` (novo) | worker diário 05:00 UTC |
| `use_cases/credit/provisioning/provision_journal.ex` (ou módulo novo) | `run_movement/2` (delta) |
| `config/config.exs` + `config/runtime.exs` | cron nos DOIS crontabs (paridade) |
| (validação) | script de dry-run/checagem no ambiente local |

## 8. Riscos / pontos a validar

1. **Manual × diário:** a tela do Risco ainda dispara o `ProvisionJournal` valor-cheio
   (`loan_provision_ppearc`). O diário usa `loan_provision_movement` (delta). São reference_types
   distintos, mas **os dois somam no razão** — se ambos rodarem, dupla contagem. **Decisão a
   validar na implementação:** ou o diário passa a ser a única fonte (tela deixa de postar
   valor-cheio, ou também vira delta), ou documentar que não se usa os dois no mesmo período.
2. **Primeiro run / baseline:** a 1ª execução usa como baseline a última classificação anterior.
   Como o `ProvisionJournal` já postou valor-cheio nela, o razão fica coerente (cheio_anterior +
   delta = vigente) **se** não houver conflito com o item 1.
3. **Quitação sem sair da classificação:** contrato quitado com `outstanding = 0` → provisão hoje
   = 0 → reversão pelo delta (ok). Contrato quitado que **sai** da classificação → tratado pela
   regra do §4.3.3 (reverter se `status ≠ written_off`).
4. **Contas COSIF ausentes** em produtos não configurados → bucket sem JE (log), provisão fica
   sem contábil naquele contrato (mesma limitação já existente).

## 9. Validação (local, monetarie-local)

- Rebuild + registrar cron (paridade); confirmar `HAS_RISK_PROVISIONING=true` no Oban.
- Rodar 1 dia manualmente (dry-run em transação com rollback): conferir que o movimento = delta
  esperado por bucket e que **não acumula** (rodar 2×/mesmo dia = mesmo saldo).
- Conferir consistência tela (`risk_classification_result`) × razão (provisão vigente).
- Conferir que contrato `written_off` **não** é revertido em dobro.
