# Plano de Implementação — Contas COSIF obrigatórias no cadastro de produtos

- **Spec:** `docs/superpowers/specs/2026-07-06-loan-product-required-cosif-accounts-design.md`
- **Branch:** `feat/loan-product-required-cosif-accounts`
- **Data:** 2026-07-06

Entregável: as contas contábeis do produto de empréstimo passam a ser **obrigatórias** no
cadastro (front + back), com o conjunto exigido variando por `model_type` (matriz do spec §4).

---

## Fase 0 — Aterrissagem (concluída; descobertas que guiam o plano)

- **`create_product/1` → `do_create_with_retry` → `insert_product!(attrs, historic)` →
  `upsert_historic!`.** O `insert_product!/2` é **reusado pelo submodelo**:
  `create_uso_limite_submodelo!/1` (`loan_products.ex:307-334`) chama
  `insert_product!(submodelo_attrs, %{})` com `model_type => "normal"` e **historic vazio**.
  ⚠️ **A validação NÃO pode ficar em `insert_product!`/`do_create_with_retry`** — quebraria a
  criação do submodelo "Uso de Limite". Vai em **`create_product/1`** (valida só o produto do
  operador; o submodelo derivado escapa por não passar por lá).
- **`update_product/3`** (`:86-115`): `base_changeset/2` usa `status_changeset` quando
  `status_only_change?/1` (payload só com `"active"`). ⚠️ **Validar contas só no caminho
  NÃO status-only** — ativar/inativar um legado não deve exigir contas.
- **`upsert_historic!/2`** (`:405-420`) pula quando `map_size == 0`. Manter o skip (é o que
  deixa o submodelo com historic vazio); a validação já garante o historic dos tipos exigidos.
- **`model_type` é imutável na edição** → usar `product.model_type` no update.
- O **front sempre envia o historic completo** (todas as chaves, `null` p/ vazias) → validar o
  payload já cobre; o merge com o existente é rede de segurança no update.
- Campos do `loan_products_historic` (`loan_products_historic.ex:8-12`): `liberation`, `fees`,
  `liquidation`, `we_add`, `discount`, `fine`, `fees_delay`, `fees_ap`, `we_add_ap`,
  `discount_ap`, `provision_incorrida`, `provision_adicional`, `provision_esperada`,
  `despesa_provisao`. `cosif_account` é campo do `loan_products` (top-level).

## Fase 1 — Módulo `ProductAccounts` (fonte única)

Criar `core/backend/lib/monetarie/use_cases/credit/product_accounts.ex`:

```elixir
@completo [:liberation, :fees, :liquidation, :fees_delay, :fine, :fees_ap,
           :provision_incorrida, :provision_adicional, :provision_esperada, :despesa_provisao]
@required_by_model %{
  "normal" => @completo, "renegociacao" => @completo, "adiantamento" => @completo,
  "garantidor" => [:liberation, :fees, :liquidation, :fine, :fees_delay],
  "limite" => [:liberation]
}
def required_historic_fields(model_type), do: Map.get(@required_by_model, model_type, [])
def requires_cosif_account?(model_type), do: model_type == "limite"
```

**Aceite:** retornos corretos por tipo (10/10/10/5/1 + cosif só no limite).

## Fase 2 — Validação no use case (`loan_products.ex`)

`validate_required_accounts(model_type, existing_historic, historic_attrs, product_attrs, existing_cosif)`:
- `efetivo = Map.merge(existing_string_map, historic_attrs)` (no create `existing = nil` → só
  `historic_attrs`).
- `faltando_historic = required_historic_fields(model_type) |> Enum.reject(&filled?(efetivo, &1))`.
- se `requires_cosif_account?` e `cosif` efetivo em branco → acrescenta `:cosif_account`.
- `[] → :ok`; senão `{:error, {:missing_required_accounts, faltando}}`.

Integração:
- **`create_product/1`:** estender o `with` após `validate_adiantamento_limit` (existing=nil).
- **`update_product/3`:** no início da `Repo.transaction`, se `not status_only_change?(product_attrs)`,
  validar com `existing = Repo.get_by(LoanProductsHistoric, product_id: product.id)` e
  `existing_cosif = product.cosif_account`; `Repo.rollback(reason)` em erro.

**Aceite:** create/update rejeitam incompleto; completo (edição de só `name`) passa; toggle
`active` não barra; criar produto `limite` (com submodelo) funciona.

## Fase 3 — Controller (`loan_product_controller.ex`)

Nos `do_create/2` e `update/2`, tratar `{:error, {:missing_required_accounts, campos}}` →
**HTTP 422** `%{error: %{message: "...", fields: campos}}` (mensagem lista os labels).

**Aceite:** POST/PUT sem as contas → 422 com `fields`.

## Fase 4 — Frontend (`LoanProductsView.vue`)

- `REQUIRED_ACCOUNTS_BY_MODEL` (espelho da matriz) + `isRequiredAccount(field)` reativo ao
  `displayForm.model_type`.
- **Asterisco** vermelho (padrão existente) nos labels das contas obrigatórias.
- `saveProduct()`: validar antes da mutation; faltando → `toast` + destaque nos campos +
  abortar. Tratar o 422 do back destacando `fields`.

**Aceite:** tela bloqueia o submit e destaca faltantes; salva quando completo.

## Fase 5 — Testes

`core/backend/test/monetarie/credit/product_accounts_test.exs`:
- Matriz por tipo (`ProductAccounts`).
- `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; **submodelo criado ok**.
- `garantidor`: exige os 5, não exige provisão/`fees_ap`.

**Aceite:** `mix test test/monetarie/credit/product_accounts_test.exs` verde (roda em dev/CI;
o container local é release, sem `mix`).

## Fase 6 — Validação empírica

`docker build` exit 0 → recriar container → `/health` 200 → HTTP (criar sem contas = 422; com
contas = 201/200) → tela em `localhost:5175` (asteriscos, bloqueio, salvar completo).

## Riscos

| Risco | Mitigação |
|---|---|
| Validação em `insert_product!` quebra o submodelo "Uso de Limite" | Validar em `create_product/1` (Fase 0) |
| Travar edição válida (só `name`) de produto completo | Estado efetivo (merge) + teste dedicado |
| Travar toggle `active` de legado | Validar só no caminho não status-only |
| Drift entre matriz do back e do front | Comentário cruzado + teste da matriz; back é a autoridade (422) |
| `fees_ap` obrigatório trava edição dos 2 ADEP (0/7) | Esperado/documentado (decisão do dono; impacto operacional no spec §6) |
