# Design — Bloquear + avisar quando prazo x periodicidade estoura o produto (min/max)

> Data: **2026-07-14**. Branch: `feat/loan-product-required-cosif-accounts`.
> Status: **aprovado no brainstorming**. Area: credito (`SimulationDialog` + `loans.ex disburse_loan`).

## Problema

O prazo REAL do contrato = `term_months` (nº de parcelas) x `periodicidade` (meses por parcela). O limite do
produto (`min/max_term_months`) e o prazo TOTAL em meses. Da pra por o max de parcelas na 1a tela (ex.: 72) e
aumentar a periodicidade na tela de preview (ex.: 2) -> 144 meses, estourando o max (72). Hoje: o front
**reduz o nº de parcelas em silencio** (`watch(periodicity)`), e a mensagem so aparece na 1a tela; o
`disburse_loan` **estrutura as parcelas sem revalidar**.

## Decisao (dono)
- **Nao alterar em silencio** — mostrar **mensagem** ao operador ("nao e possivel seguir pelo prazo do
  produto"), tanto para **maximo** quanto para **minimo**.
- Mensagem **nas duas telas**: 1a (ao definir as parcelas) **e** na de calculo/preview.
- **Backend revalida ao estruturar** as parcelas (disburse), abortando com mensagem.

## Achados (exploracao)
- `termError` (SimulationDialog, computed) JA valida min E max (`contractMonths = term x per`) com mensagem;
  so aparece na etapa 1.
- `watch(periodicity)` (linha ~355) REDUZ `term_months` em silencio ao mudar a periodicidade.
- `validate_term_bounds` (loans.ex) valida `term x periodicidade` vs min/max e roda na criacao (simulacao +
  conversao + `create_application`), mas NAO no `disburse_loan`.
- `disburse_loan` tem `loan.loan_product` (preload) + `loan.term_months` + `loan.metadata` (periodicidade
  via `resolve_periodicity`) — tudo disponivel pra revalidar no ponto de estruturacao.

## Frontend (`SimulationDialog.vue`)
- **Remover** o `watch(periodicity)` que reduz `term_months` em silencio → passa a mostrar `termError` e
  bloquear (nao auto-corrige).
- **Exibir o `termError` na etapa de preview/calculo** tambem (hoje so na etapa 1, perto do nº parcelas).
- **Mensagem clara**, distinguindo max/min e acionavel, ex.:
  - Max: "Nao e possivel seguir: prazo de {Z} meses ({N} parcelas x periodicidade {P}) ultrapassa o maximo do
    produto ({max} meses). Reduza as parcelas ou a periodicidade."
  - Min: "Nao e possivel seguir: prazo de {Z} meses ... e menor que o minimo do produto ({min} meses)."
- `canProceed`/`canSubmit` ja bloqueiam com `termError` (mantido).

## Backend (`loans.ex disburse_loan`)
- Novo passo `Multi.run(:validate_term, ...)` ANTES do `:schedule`, revalidando via `validate_term_bounds`
  (montando attrs do loan: `term_months` + `metadata.periodicity`) contra `loan.loan_product`. Se estourar
  (min OU max), `{:error, mensagem}` → a Multi aborta, o emprestimo continua `approved`, nada estruturado
  nem creditado. Sem estouro → segue igual a hoje (zero regressao pro caminho valido).

## Testes
- **Backend** (`loan_lifecycle`): desembolsar loan com `term x periodicidade > max` → aborta com erro de
  prazo; `< min` → aborta; dentro do limite → estrutura normal (parcelas geradas).
- **Front:** type-check; `termError` aparece nas 2 telas; sem reducao silenciosa.

## Fora de escopo (YAGNI)
- Mudar `validate_term_bounds` em si (a regra ja esta certa) — so passa a rodar tambem no disburse.
- Alterar a criacao/simulacao (ja validam).

## Arquivos afetados
| Arquivo | Mudanca |
|---|---|
| `loans.ex` | `disburse_loan`: passo `:validate_term` (reusa `validate_term_bounds`) antes do `:schedule` |
| `SimulationDialog.vue` | remove `watch(periodicity)` clamp; `termError` no preview; mensagem clara min/max |
| `loan_lifecycle_test.exs` | desembolso aborta com prazo fora (min/max); dentro estrutura normal |
