# Loans Module Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add a Loans module to the Internet Banking following the same pattern as the Investments module.

**Architecture:** Two new backend endpoints in `account_controller.ex` (portfolio + detail), two new Vue views (`LoansHubView` + `LoanDetailView`), router routes, nav item, i18n keys. Backend loan schemas/contexts already exist — we only wire them to the IB.

**Tech Stack:** Elixir/Phoenix (backend), Vue 3 + PrimeVue + Chart.js (frontend), existing `Monetarie.Credit.Loans` context.

---

### Task 1: Backend — Loans Portfolio Endpoint

**Files:**
- Modify: `core/backend/lib/monetarie_web/controllers/v2/account_controller.ex`
- Modify: `core/backend/lib/monetarie_web/router.ex`

**Step 1: Add routes in router.ex**

In the merchant-scoped IB routes (after investment routes around line 1567), add:

```elixir
get "/loans/portfolio", AccountController, :loans_portfolio
get "/loans/:id", AccountController, :loan_show
```

In the self-scoped routes (after investment self routes around line 1681), add:

```elixir
get "/loans/portfolio", AccountController, :loans_portfolio_self
get "/loans/:id", AccountController, :loan_show_self
```

**Step 2: Implement `loans_portfolio` in account_controller.ex**

Add after the `investment_show_self` function. Pattern mirrors `investments_portfolio`:

```elixir
def loans_portfolio(conn, %{"merchant_id" => merchant_id}) do
  user_id = ensure_integer(merchant_id)
  current_user = Guardian.Plug.current_resource(conn)

  unless authorized?(current_user, user_id) do
    conn |> put_status(:forbidden) |> json(%{error: "Forbidden"})
  else
    alias Monetarie.Cooperative.Member
    member = from(m in Member, where: m.user_id == ^user_id, limit: 1) |> Repo.one()

    if is_nil(member) do
      conn |> put_status(:ok) |> json(%{data: %{
        total_outstanding: 0, next_payment_amount: 0, next_payment_date: nil,
        overdue_count: 0, overdue_amount: 0, loans: []
      }})
    else
      loans = Monetarie.Credit.Loans.get_member_loans(member.id)

      active_loans = Enum.filter(loans, &(&1.status in [:active, :disbursed]))
      total_outstanding = active_loans |> Enum.map(& &1.outstanding_balance) |> Enum.sum()

      # Find next upcoming installment across all active loans
      alias Monetarie.Credit.LoanInstallment
      next_installment = from(i in LoanInstallment,
        join: l in assoc(i, :loan),
        where: l.member_id == ^member.id and l.status in [:active, :disbursed],
        where: i.status in [:pending, :overdue],
        order_by: [asc: i.due_date],
        limit: 1
      ) |> Repo.one()

      # Overdue installments
      overdue_installments = from(i in LoanInstallment,
        join: l in assoc(i, :loan),
        where: l.member_id == ^member.id and l.status in [:active, :disbursed],
        where: i.status == :overdue,
        select: %{count: count(i.id), total: sum(i.payment_amount)}
      ) |> Repo.one()

      loans_data = Enum.map(loans, fn loan ->
        paid_installments = if Ecto.assoc_loaded?(loan.installments),
          do: Enum.count(loan.installments, &(&1.status == :paid)),
          else: 0
        total_installments = loan.term_months || 0

        %{
          id: loan.id,
          contract_number: loan.contract_number,
          status: loan.status,
          disbursed_amount: loan.disbursed_amount || loan.approved_amount || loan.requested_amount,
          outstanding_balance: loan.outstanding_balance,
          total_paid: loan.total_paid,
          overdue_amount: loan.overdue_amount,
          overdue_days: loan.overdue_days,
          annual_rate_bps: loan.annual_rate_bps,
          term_months: loan.term_months,
          amortization_type: loan.amortization_type,
          application_date: loan.application_date || loan.inserted_at,
          maturity_date: loan.maturity_date,
          disbursement_date: loan.disbursement_date,
          paid_installments: paid_installments,
          total_installments: total_installments,
          product: if(Ecto.assoc_loaded?(loan.loan_product) && loan.loan_product,
            do: %{code: loan.loan_product.code, name: loan.loan_product.name}, else: nil)
        }
      end)

      conn |> put_status(:ok) |> json(%{data: %{
        total_outstanding: total_outstanding,
        next_payment_amount: if(next_installment, do: next_installment.payment_amount, else: 0),
        next_payment_date: if(next_installment, do: next_installment.due_date, else: nil),
        overdue_count: (overdue_installments && overdue_installments.count) || 0,
        overdue_amount: (overdue_installments && overdue_installments.total) || 0,
        loans: loans_data
      }})
    end
  end
rescue
  e ->
    Logger.error("[V2 AccountController] loans_portfolio error: #{inspect(e)}")
    conn |> put_status(:ok) |> json(%{data: %{
      total_outstanding: 0, next_payment_amount: 0, next_payment_date: nil,
      overdue_count: 0, overdue_amount: 0, loans: []
    }})
end
```

**Step 3: Implement `loan_show` in account_controller.ex**

```elixir
def loan_show(conn, %{"merchant_id" => merchant_id, "id" => id} = params) do
  user_id = ensure_integer(merchant_id)
  current_user = Guardian.Plug.current_resource(conn)

  unless authorized?(current_user, user_id) do
    conn |> put_status(:forbidden) |> json(%{error: "Forbidden"})
  else
    alias Monetarie.Cooperative.Member
    alias Monetarie.Credit.{LoanInstallment, LoanPayment}

    member = from(m in Member, where: m.user_id == ^user_id, limit: 1) |> Repo.one()
    loan = Monetarie.Credit.Loans.get_loan(id)

    if is_nil(member) or is_nil(loan) or loan.member_id != member.id do
      conn |> put_status(:not_found) |> json(%{error: "Not found"})
    else
      page = parse_integer(params["page"], 1)
      per_page = parse_integer(params["per_page"], 12)

      product = if Ecto.assoc_loaded?(loan.loan_product), do: loan.loan_product,
                else: Repo.get(Monetarie.Credit.LoanProduct, loan.loan_product_id)

      # Paginated installments
      installments_query = from(i in LoanInstallment,
        where: i.loan_id == ^loan.id,
        order_by: [asc: i.number]
      )
      total_installments = Repo.aggregate(installments_query, :count)
      total_pages = max(ceil(total_installments / per_page), 1)

      installments = installments_query
        |> limit(^per_page)
        |> offset(^((page - 1) * per_page))
        |> Repo.all()

      installments_data = Enum.map(installments, fn i ->
        %{
          number: i.number,
          due_date: i.due_date,
          payment_amount: i.payment_amount,
          principal_amount: i.principal_amount,
          interest_amount: i.interest_amount,
          balance_after: i.balance_after,
          status: i.status,
          paid_amount: i.paid_amount,
          paid_date: i.paid_date,
          penalty_amount: i.penalty_amount,
          overdue_days: i.overdue_days
        }
      end)

      # Payments history
      payments = from(p in LoanPayment,
        where: p.loan_id == ^loan.id and p.status == :confirmed,
        order_by: [desc: p.payment_date],
        limit: 50
      ) |> Repo.all()

      payments_data = Enum.map(payments, fn p ->
        %{
          id: p.id,
          payment_date: p.payment_date,
          amount: p.amount,
          principal_paid: p.principal_paid,
          interest_paid: p.interest_paid,
          penalty_paid: p.penalty_paid,
          payment_method: p.payment_method,
          reference: p.reference
        }
      end)

      conn |> put_status(:ok) |> json(%{data: %{
        id: loan.id,
        contract_number: loan.contract_number,
        status: loan.status,
        disbursed_amount: loan.disbursed_amount || loan.approved_amount,
        outstanding_balance: loan.outstanding_balance,
        total_paid: loan.total_paid,
        overdue_amount: loan.overdue_amount,
        overdue_days: loan.overdue_days,
        annual_rate_bps: loan.annual_rate_bps,
        term_months: loan.term_months,
        amortization_type: loan.amortization_type,
        application_date: loan.application_date || loan.inserted_at,
        maturity_date: loan.maturity_date,
        disbursement_date: loan.disbursement_date,
        product: if(product, do: %{code: product.code, name: product.name}, else: nil),
        installments: %{
          items: installments_data,
          page: page,
          per_page: per_page,
          total: total_installments,
          total_pages: total_pages
        },
        payments: payments_data
      }})
    end
  end
rescue
  e ->
    Logger.error("[V2 AccountController] loan_show error: #{inspect(e)}")
    conn |> put_status(:not_found) |> json(%{error: "Not found"})
end
```

**Step 4: Add self-scoped wrappers**

```elixir
def loans_portfolio_self(conn, params) do
  current_user = Guardian.Plug.current_resource(conn)
  loans_portfolio(conn, Map.put(params, "merchant_id", to_string(current_user.id)))
end

def loan_show_self(conn, params) do
  current_user = Guardian.Plug.current_resource(conn)
  loan_show(conn, Map.put(params, "merchant_id", to_string(current_user.id)))
end
```

**Step 5: Commit**

```bash
git add core/backend/lib/monetarie_web/controllers/v2/account_controller.ex core/backend/lib/monetarie_web/router.ex
git commit -m "feat: add loans portfolio and detail endpoints for IB"
```

---

### Task 2: Frontend — i18n Keys + Router + Navigation

**Files:**
- Modify: `core/apps/banking/src/i18n/pt-BR.ts`
- Modify: `core/apps/banking/src/router/index.ts`
- Modify: `core/apps/banking/src/layouts/AuthenticatedLayout.vue`

**Step 1: Add i18n keys in pt-BR.ts**

Add after the `investments` block:

```typescript
loans: {
  title: 'Empréstimos',
  outstandingBalance: 'Saldo Devedor',
  nextPayment: 'Próxima Parcela',
  overdueInstallments: 'Parcelas em Atraso',
  debtEvolution: 'Evolução da Dívida',
  myLoans: 'Meus Empréstimos',
  noLoans: 'Nenhum empréstimo encontrado',
  all: 'Todos',
  onTime: 'Em dia',
  overdue: 'Em atraso',
  settled: 'Liquidados',
  contractDate: 'Contratação',
  loanAmount: 'Valor Emprestado',
  currentBalance: 'Saldo Devedor',
  rate: 'Taxa',
  installment: 'Parcela',
  detailTitle: 'Detalhe do Empréstimo',
  detail: 'Detalhe',
  contract: 'Contrato',
  maturityDate: 'Vencimento',
  disbursementDate: 'Desembolso',
  amortization: 'Amortização',
  totalPaid: 'Total Pago',
  overdueAmount: 'Valor em Atraso',
  schedule: 'Cronograma de Parcelas',
  noInstallments: 'Sem parcelas registradas',
  paymentHistory: 'Histórico de Pagamentos',
  noPayments: 'Sem pagamentos registrados',
  notFound: 'Empréstimo não encontrado',
  outstandingLine: 'Saldo Devedor',
  paidLine: 'Total Pago',
},
```

**Step 2: Add routes in router/index.ts**

Add after the investments routes:

```typescript
// Loans
{
  path: 'loans',
  name: 'loans',
  component: () => import('@/views/loans/LoansHubView.vue'),
},
{
  path: 'loans/:id',
  name: 'loan-detail',
  component: () => import('@/views/loans/LoanDetailView.vue'),
},
```

**Step 3: Add nav item in AuthenticatedLayout.vue**

Import `HandCoins` from lucide-vue-next. Add nav item after investments:

```typescript
{ path: '/loans', label: t('nav.loans', 'Empréstimos'), icon: markRaw(HandCoins) },
```

**Step 4: Commit**

```bash
git add core/apps/banking/src/i18n/pt-BR.ts core/apps/banking/src/router/index.ts core/apps/banking/src/layouts/AuthenticatedLayout.vue
git commit -m "feat: add loans i18n keys, routes, and nav item"
```

---

### Task 3: Frontend — LoansHubView

**Files:**
- Create: `core/apps/banking/src/views/loans/LoansHubView.vue`

**Key Requirements:**
- Summary cards: Saldo Devedor (teal) | Próxima Parcela (valor + data) | Parcelas em Atraso (vermelho se > 0)
- Chart: Two lines per active loan — saldo devedor (descendente) + total pago (ascendente)
- Tabs filter: Todos | Em dia | Em atraso | Liquidados
- Loan cards: produto, contrato, valor emprestado, saldo devedor, parcela (N/M), taxa
- Status tags: em dia (success), em atraso (danger), liquidado (secondary)
- Click navigates to `/loans/:id`
- Follow exact same CSS pattern as InvestmentsHubView.vue

**Commit:**

```bash
git add core/apps/banking/src/views/loans/LoansHubView.vue
git commit -m "feat: add LoansHubView with chart, tabs filter, and loan cards"
```

---

### Task 4: Frontend — LoanDetailView

**Files:**
- Create: `core/apps/banking/src/views/loans/LoanDetailView.vue`

**Key Requirements:**
- Header card (gradient navy→teal): produto + status tag, contrato, data, vencimento, taxa, amortização
- Summary cards (4): Valor Emprestado | Saldo Devedor | Total Pago | Parcelas em Atraso
- Installments table (paginated): Nº | Vencimento | Valor | Principal | Juros | Status | Pago
- Installment status colors: pendente (info), paga (success), vencida (danger), parcial (warn)
- Pagination: primeira/anterior/próxima/última (same as InvestmentDetailView)
- Payments history section: data, valor, método, referência
- Follow exact same CSS pattern as InvestmentDetailView.vue

**Commit:**

```bash
git add core/apps/banking/src/views/loans/LoanDetailView.vue
git commit -m "feat: add LoanDetailView with installments schedule and payment history"
```

---

### Task 5: Verify & Final Commit

**Step 1:** Restart Phoenix server and verify endpoints return data
**Step 2:** Verify frontend renders correctly with real data
**Step 3:** Verify tab filtering works
**Step 4:** Verify detail view pagination works
**Step 5:** Final commit if any adjustments needed
