# Design — Contas COSIF obrigatórias no cadastro de produtos de empréstimo

- **Data:** 2026-07-06
- **Autor:** Claude Code (brainstorming com Bruno)
- **Status:** Aprovado (design)
- **Repo:** `monetarie` (core banking)
- **Branch de trabalho:** `feat/loan-product-required-cosif-accounts`

## 1. Contexto e problema

No cadastro de produtos de empréstimo (app admin `@monetarie/admin`,
`core/apps/admin/src/views/credit/LoanProductsView.vue`, rota Crédito › Produtos), os campos
das contas contábeis COSIF **já existem** (painel "Contas Contábeis": os 14 campos do
`loan_products_historic` + a Contrapartida `loan_products.cosif_account`). Porém **nada é
obrigatório**:

- **Front:** não há biblioteca de validação (nem `vee-validate`/`vuelidate`/`zod` usados neste
  form); a obrigatoriedade é apenas o asterisco visual. `saveProduct()` chama a mutation sem
  checar campos vazios.
- **Back:** `LoanProductsHistoric.changeset/2` só faz `cast` (zero `validate_required`);
  `LoanProduct.changeset` não exige `cosif_account`. E `upsert_historic!/2`
  (`loan_products.ex`) **pula o historic quando o bloco vem vazio** (`map_size == 0`) → o
  produto é salvo sem conta nenhuma.

Consequência: produtos podem existir sem as contas que o motor contábil precisa, gerando
lançamentos *fail-soft* silenciosos (`loan_accrual` loga warning e não posta o JE).

Evidência empírica (mon_core local, 7 produtos): 3 dos 4 `normal` não têm `historic` nenhum;
`fees_ap` está preenchido em **0/7**.

## 2. Objetivo

Tornar as contas contábeis **obrigatórias** no cadastro de produtos — exibidas com marcação
clara na tela e **bloqueando o salvamento** (front **e** back) quando faltarem — com o
conjunto exigido **variando por `model_type`**.

## 3. Decisões (brainstorming 2026-07-06)

| # | Decisão | Escolha |
|---|---|---|
| Obrigatoriedade | universal vs por tipo | **Por `model_type`** |
| Conjunto base (tipos que provisionam) | quais contas | Lista do dono **+ Multa** |
| Multa (`fine`) | incluir como obrigatória | **Sim** |
| Encargos/Descontos (+ variantes `*_ap`) | incluir | **Não** (opcionais) |
| Contrapartida (`cosif_account`) | obrigatória onde | **Só no `limite`** |
| `garantidor` | conjunto | **Reduzido** (operacionais, sem provisão, sem `fees_ap`) |
| Juros (A Apropriar) `fees_ap` | obrigatório apesar de 0/7 e inerte no motor | **Sim** (decisão do dono) |
| Legado | quando validar | **Criação + edição** (estado efetivo) |
| Arquitetura | onde valida | **Back fonte única + espelho no front** |

## 4. Matriz de obrigatoriedade por `model_type`

Campos do `loan_products_historic` (campo ↔ label na tela):
`liberation` Liberação · `fees` Juros · `liquidation` Liquidação · `fees_delay` Juros de
Atraso · `fine` Multa · `fees_ap` Juros (A Apropriar) · `provision_incorrida` Provisão
Incorrida · `provision_adicional` Provisão Adicional · `provision_esperada` Perda Esperada ·
`despesa_provisao` Despesa de Provisão. Top-level: `cosif_account` Contrapartida.

| `model_type` | Campos obrigatórios |
|---|---|
| `normal`, `renegociacao`, `adiantamento` | `liberation, fees, liquidation, fees_delay, fine, fees_ap, provision_incorrida, provision_adicional, provision_esperada, despesa_provisao` (10) |
| `garantidor` | `liberation, fees, liquidation, fine, fees_delay` (5 — sem provisão, sem `fees_ap`) |
| `limite` | `liberation` (historic) + `cosif_account` (top-level) |

**Sempre opcionais:** `we_add`, `discount`, `we_add_ap`, `discount_ap`. **Contrapartida**
(`cosif_account`) obrigatória **só** no `limite`; nos demais permanece opcional e oculta
(comportamento atual da tela).

## 5. Arquitetura

### 5.1 Fonte única — módulo `ProductAccounts` (back)

Novo `Monetarie.UseCases.Credit.ProductAccounts` (espelha o padrão de
`MemberAccount.deposit_kinds/0`):

- `@required_by_model`: mapa `model_type` → lista de átomos (a matriz da §4).
- `required_historic_fields(model_type) :: [atom]`
- `requires_cosif_account?(model_type) :: boolean` (`true` só para `"limite"`)

É a fonte única da política, consumida pela validação e pelos testes.

### 5.2 Validação no use case (`loan_products.ex`)

`validate_required_accounts(model_type, product_attrs, historic_effective)`:

- **Estado efetivo:** no update, mescla o `historic` existente (`Repo.get_by`) com o
  `historic_attrs` do payload — valida o resultado **final**, não só o payload. Assim, editar
  um produto **já completo** (ex.: só o `name`) **não** trava; editar um **legado incompleto**
  trava até completar.
- Cada campo obrigatório (por tipo) deve estar preenchido (não `nil`/`""`).
- `cosif_account` exigido quando `requires_cosif_account?/1`.
- Retorna `:ok | {:error, {:missing_required_accounts, [campo]}}`.
- Chamada em `create_product/1` e no caminho **completo** de `update_product/3`, dentro da
  `Repo.transaction` (rollback em erro).
- **Não** aplicar no caminho *status-only* (`toggleActiveProduct`, que usa `status_changeset`)
  — ativar/inativar um legado não deve exigir contas.
- Ajustar `upsert_historic!/2`: deixa de ser um escape para tipos que exigem historic (a
  validação barra antes; o upsert persiste o bloco já validado).

### 5.3 Erro estruturado (`loan_product_controller.ex`)

Mapear `{:error, {:missing_required_accounts, campos}}` → **HTTP 422** com
`{errors: %{<campo> => "obrigatório"}}`, formato que o front traduz por
`FIELD_LABELS`/`extractSaveError`.

### 5.4 Frontend (`LoanProductsView.vue`)

- `REQUIRED_ACCOUNTS_BY_MODEL` (mapa espelho da §4).
- **Asterisco** vermelho (padrão existente) nos labels das contas obrigatórias, condicional ao
  `model_type` selecionado.
- `saveProduct()`: validação client-side por tipo antes da mutation — bloqueia o submit,
  destaca os campos faltantes (borda/mensagem) + `toast` (padrão inline do app, ex.
  `FeeConfigDialog.validate()`; sem lib nova).
- Fallback: se o back retornar 422, destacar os campos apontados.
- Exibição: `limite` já mostra Liberação + Contrapartida; os demais tipos mostram o conjunto
  completo (garantidor incluso) — basta marcar os obrigatórios.

## 6. Legado e impacto operacional

- **Criação + edição** com estado efetivo: os 3 produtos `normal` sem `historic` ficam
  bloqueados para nova edição até serem completados; produtos completos seguem editáveis
  normalmente.
- `fees_ap` obrigatório (normal/reneg/adiantamento) e hoje **0/7**: ao editar os 2
  adiantamentos será preciso informar o "Juros (A Apropriar)". O CE-pai (`limite`) não o
  exige, então passa.
- **Submodelo "Uso de Limite"** (auto-criado do CE-pai): fora do escopo da validação de tela —
  é derivado, herda `cosif_account` do pai; a provisão vem do seed 036/binding.
- Sem backfill automático dos `normal` vazios (regularização manual na edição).

## 7. Testes

- **Back (ExUnit)** `test/monetarie/credit/product_accounts_test.exs` (+ casos em
  `loan_products`):
  - `ProductAccounts` retorna a matriz correta por tipo.
  - `create_product`: rejeita incompleto (por tipo); aceita completo.
  - `update_product`: completo editando só `name` passa; incompleto barra; toggle `active` de
    incompleto **não** barra.
  - `limite`: exige `liberation` + `cosif_account`, não exige provisão.
  - `garantidor`: exige os 5, não exige provisão/`fees_ap`.
- **Front:** app sem infra de teste unit — validação por `docker build` + HTTP + tela (padrão
  do projeto).

## 8. Fora de escopo (follow-ups)

- Fazer o motor consumir `fees_ap`/encargos/descontos (seguem cadastrais).
- Adicionar Contrapartida na tela dos tipos não-`limite`.
- Backfill automático dos produtos `normal` incompletos.

## 9. Arquivos afetados

- **Novo:** `core/backend/lib/monetarie/use_cases/credit/product_accounts.ex`
- **Editar:** `core/backend/lib/monetarie/use_cases/credit/loan_products.ex` (validação em
  create/update; ajuste do `upsert_historic!`)
- **Editar:** `core/backend/lib/monetarie_web/controllers/credit/loan_product_controller.ex`
  (erro 422)
- **Novo:** `core/backend/test/monetarie/credit/product_accounts_test.exs`
- **Editar:** `core/apps/admin/src/views/credit/LoanProductsView.vue` (mapa espelho,
  asteriscos, validação client)
- Schemas `loan_product.ex` / `loan_products_historic.ex`: sem mudança estrutural obrigatória
  (a política vive no use case; reforço no changeset é opcional).
