# Design — Trava de contas contábeis obrigatórias na liberação do contrato

- **Data:** 2026-07-06
- **Autor:** Claude Code (brainstorming com Bruno)
- **Status:** Aprovado (design)
- **Repo:** `monetarie` (core banking)
- **Branch:** `feat/loan-product-required-cosif-accounts` (continuação da feature de contas
  obrigatórias no cadastro — reusa `ProductAccounts`)

## 1. Contexto e problema

A feature anterior tornou as contas COSIF obrigatórias no **cadastro** de produtos
(`LoanProductController` + `ProductAccounts`). Mas produtos **legados** e **de sistema**
(PESSOAL/CONSIGNADO/GARANTIDO são criados pelo seed `loan_products.exs` **sem** contas; o CE
idem, vinculado depois pelo seed 036) podem existir incompletos. Ao **liberar** (desembolsar)
um contrato desses produtos, o fluxo é *fail-soft*: o passo `:account_entry` e o
`create_disbursement_journal_entry` (JE COSIF) apenas logam warning quando falta conta, e o
empréstimo vira `active` **sem** o lançamento contábil — buraco silencioso.

## 2. Objetivo

Na **liberação manual** (`PUT /api/v1/credit/loans/:id/disburse`), validar que o produto do
contrato tem **todas** as contas contábeis obrigatórias (a matriz por `model_type` de
`ProductAccounts`). Se faltar **qualquer** uma: **negar** a liberação (fail-closed — nada
persistido, empréstimo continua `approved`) e devolver **HTTP 422** com mensagem listando as
contas faltantes; a tela exibe o aviso.

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

| Decisão | Escolha |
|---|---|
| Escopo das contas | **Todas** as obrigatórias do produto (matriz `ProductAccounts` por `model_type`) |
| Aviso em tela | **Reativo**: backend nega (422) + a tela mostra a mensagem do servidor |
| Camada da trava | No **use case**, dentro da `Ecto.Multi` de `disburse_loan` (fail-closed por construção) |
| Escopo do fluxo | Só **liberação manual** (`disburse`, `approved → active`); adiantamento automático fora |

## 4. Fluxo atual (mapeado)

- `PUT /api/v1/credit/loans/:id/disburse` → `LoanController.disburse/2`
  (`loan_controller.ex:210`) → `Loans.disburse_loan/2` (`loans.ex:278`), uma `Ecto.Multi`.
- Passo `:loan` (`loans.ex:283`) preloada `[:loan_product, :member]` — **não** `:historic`.
- Passo `:validate` (`loans.ex:289`) checa `status == "approved"` (padrão de pré-condição).
- Passo `:account_entry` (`loans.ex:403`) é *fail-soft*; JE COSIF pós-transação
  (`create_disbursement_journal_entry`, `loans.ex:2072/2144`) também.
- `FallbackController` mapeia átomos → HTTP (ex.: `:product_inactive` → 422). Shape do corpo:
  `{"error": {"status", "message", "details"}}` (`error_json.ex`).
- Front: **duas** telas com botão de liberar — `LoanDetailView.vue` ("Desembolsar",
  handler `:187`) e `components/LoanDetail.vue` ("Liberar Crédito", handler `:109`), ambas via
  `disburseLoan.mutateAsync` (`useLoans.ts:152`). Hoje o `catch` mostra **toast genérico** e
  ignora a mensagem do servidor.

## 5. Arquitetura

### 5.1 `ProductAccounts` (2 adições)

- `missing_for_product(%LoanProduct{})` :: `[atom]` — contas obrigatórias faltantes olhando
  `product.historic` (campos) + `product.cosif_account`, via a matriz de `product.model_type`.
  Se `product.historic` não estiver carregado (`%NotLoaded{}`/`nil`), trata como vazio (tudo
  faltando) — o caller garante o preload.
- `account_label(atom)` :: `String.t()` — labels PT das contas, **fonte única**. O mapa
  `@account_labels` hoje duplicado em `LoanProductController` passa a vir daqui.

### 5.2 `Loans.disburse_loan/2` (fail-closed)

- Passo `:loan`: preload passa a `[:member, loan_product: :historic]` (carrega o `historic`
  aninhado no produto).
- Novo passo logo após `:loan`:
  ```elixir
  |> Multi.run(:validate_product_accounts, fn _repo, %{loan: loan} ->
    case ProductAccounts.missing_for_product(loan.loan_product) do
      [] -> {:ok, :valid}
      missing -> {:error, {:liberation_accounts_missing, missing}}
    end
  end)
  ```
  A `Multi` aborta em erro → **nada persistido**, empréstimo permanece `approved`.

### 5.3 `FallbackController`

- Cláusula `call(conn, {:error, {:liberation_accounts_missing, fields}})` → **422** com
  mensagem PT: *"Não é possível liberar o contrato: o produto não tem todas as contas
  contábeis obrigatórias configuradas. Faltam: &lt;labels&gt;. Configure-as no cadastro do
  produto."* (usa `ProductAccounts.account_label/1`).

### 5.4 Frontend (2 telas)

- `LoanDetailView.vue` e `components/LoanDetail.vue`: o `catch` do `handleDisburse` passa a
  exibir `err?.response?.data?.error?.message` (com fallback) — padrão já usado no resto de
  Crédito. Assim o motivo (contas faltantes) aparece no toast.

## 6. Testes

- **`ProductAccounts.missing_for_product`** (puro, structs): produto completo → `[]`;
  incompleto → campos certos; por tipo (garantidor sem provisão; limite liberation+cosif;
  `historic` não carregado → tudo faltando).
- **`disburse_loan`** (DataCase): produto **incompleto** → `{:error,
  {:liberation_accounts_missing, _}}` e o loan **permanece `approved`** (reload confirma —
  fail-closed provado); produto **completo** → `{:ok, loan}` com `status == "active"`.

## 7. Fora de escopo

- Fluxo de adiantamento automático (`disbursed`).
- Backfill de produtos legados incompletos.
- Bloqueio proativo do botão (decidido: reativo).

## 8. Arquivos afetados

- **Editar:** `core/backend/lib/monetarie/use_cases/credit/product_accounts.ex`
  (`missing_for_product/1` + `account_label/1`)
- **Editar:** `core/backend/lib/monetarie/use_cases/credit/loans.ex` (preload `:historic` +
  passo `:validate_product_accounts`)
- **Editar:** `core/backend/lib/monetarie_web/controllers/fallback_controller.ex` (cláusula 422)
- **Editar:** `core/backend/lib/monetarie_web/controllers/credit/loan_product_controller.ex`
  (usar `ProductAccounts.account_label/1` — DRY)
- **Editar:** `core/apps/admin/src/views/credit/LoanDetailView.vue` +
  `core/apps/admin/src/views/credit/components/LoanDetail.vue` (surface da mensagem)
- **Editar:** `core/backend/test/monetarie/credit/product_accounts_test.exs`
  (`missing_for_product`) + caso de disburse bloqueado (em `loans_test.exs` ou novo).
