# Finance & Billing — Phase 9 of the SIS

How students get billed, how payments land on their account, and how
the registrar / finance officer / student all see the same truth.

The same SSOT discipline that runs the SIS runs here: one ledger per
student, one canonical place to look for "what does this person owe".

---

## Design pillars

1. **Catalogue first, schedules second, invoices third.**
   Tenants define what fee items they charge (Tuition / Library /
   Medical / Hostel / …). They then assemble those items into
   schedules per (programme × level × year × applicant_category).
   The schedule is what gets materialised into an invoice on a
   specific student.

2. **Single student ledger.** Every charge, every payment, every
   adjustment, every refund is a row in `student_ledger_entries`.
   The student's balance is `SUM(debit) − SUM(credit)`. Statements
   are just ordered slices of the ledger. No reconciliation drift.

3. **Payments allocate to invoices.** The existing `payments` table
   stays generic; a new `payment_allocations` table links each
   payment to one or more invoices with explicit amounts. Same
   payment can cover voucher + acceptance fee + first-year tuition;
   each allocation is auditable.

4. **Minor units only.** All amounts in lowest currency unit
   (pesewas for GHS) as integers — no floats anywhere in finance.

5. **Multi-currency aware but not multi-currency yet.** Every row
   carries `currency` even though one tenant always uses one. Saves
   us a migration when a tenant goes international.

---

## Phase plan

| # | Name | Status | Scope |
|---|---|---|---|
| 9.1 | **Schema foundation** | **NOW** | `fee_items`, `fee_schedules` + lines, `invoices` + lines, `student_ledger_entries`, `payment_allocations` |
| 9.2 | **Fee catalogue + schedule editor UI** | Next | Finance / tenant admin pages to manage items + assemble schedules |
| 9.3 | **Invoice generation engine** | Pending | `GenerateInvoicesForStudent` action + bulk `finance:generate-invoices` command + auto-trigger on enrol |
| 9.4 | **Payment allocation + statement of account** | Pending | `AllocatePayment` service, statement PDF export, refund integration with `student_withdrawals` |
| 9.5 | **Student-facing fees view + pay-now** | Pending | Dashboard tile + Fees page reusing existing payment gateway |
| 9.6 | _Optional_ Instalment plans | Future | Split invoice into scheduled instalments with reminders |
| 9.7 | _Optional_ Late-fee accrual | Future | Cron-driven penalty on overdue invoices |
| 9.8 | _Optional_ Bank-statement reconciliation | Future | Import bank CSV and auto-match against payments |

---

## Sub-phase 9.1 — Schema (what's being built now)

### Tables

```
fee_items
  id, tenant_id, code (TUITION/LIBRARY/MEDICAL/…), name, description,
  default_amount_minor (nullable; fallback when schedule line omits),
  is_active, sort_order, timestamps, soft delete
  -- unique(tenant_id, code)

fee_schedules
  id, tenant_id, name (free-text, e.g. "BSc CS Year 1 — 2026/27"),
  academic_year_id,
  programme_id           (nullable; null = applies to any programme),
  level                  (nullable; null = applies to any year level),
  applicant_category     (nullable; "regular" / "fee_paying" / "international" / …),
  campus_qualifier       (nullable; obuasi-only schedules etc),
  currency (8 chars; defaults to tenant currency),
  effective_from, effective_until, is_active,
  timestamps, soft delete
  -- partial unique on the matching scope

fee_schedule_lines
  id, fee_schedule_id, fee_item_id, amount_minor,
  is_mandatory (bool), notes, sort_order, timestamps

invoices
  id, tenant_id, student_id, academic_year_id,
  invoice_number (tenant-unique, e.g. KNUST-INV-26-000001),
  fee_schedule_id (provenance — which schedule was materialised),
  issued_on, due_on,
  status (draft/issued/partially_paid/paid/overdue/void),
  currency, total_minor, paid_minor, balance_minor,
  notes, timestamps, soft delete

invoice_lines
  id, invoice_id, fee_item_id, description, amount_minor,
  is_mandatory, sort_order, timestamps

student_ledger_entries
  id, tenant_id, student_id,
  entry_type (charge/payment/adjustment/refund/write_off),
  reference_type, reference_id  (polymorphic: Invoice, Payment, StudentWithdrawal),
  description, debit_minor, credit_minor, currency,
  occurred_at, posted_by_user_id, notes, timestamp(created_at only)

payment_allocations
  id, tenant_id, payment_id, invoice_id,
  amount_minor, allocated_at, allocated_by_user_id,
  timestamps
  -- unique(payment_id, invoice_id)
```

### Why a separate ledger AND invoices?

The invoice is the *statement of intent* the school issues. The
ledger is the *running record* of what's happened. They serve
different questions:

- "How much was this student billed for 2026/27 tuition?" → invoice.
- "What's their current balance right now?" → ledger sum.
- "Did this payment cover invoice INV-26-000123?" → allocation.

Keeping them separate means we can: (a) void an invoice without
losing the payment history, (b) credit a partial refund without
breaking invoice totals, (c) post adjustments (waivers, bursaries)
that aren't tied to any invoice.

### Why per-tenant unique on invoice_number?

So tenants can pick their own format (KNUST-INV-26-000001 vs
AC/INV/2026/001) without ID collisions across the platform. The
generator lives in the Invoice model, same pattern as
`Student::generateNumber`.

### What's deliberately deferred to 9.6+

- Instalment plans (split invoice into scheduled instalments)
- Late-fee accrual (cron-driven penalties)
- Bank-statement CSV import + matching
- Multi-currency conversion (rates, settled-currency tracking)
- Per-student waivers and bursary integration

These are all additive on top of this schema — no breaking changes
to land them later.
