# HR-Finance Fiscalisation — Phase Plan

> Plan for the integration of the HR Manager and Bursar (Finance) portals.
> Tracks all phases from proxy stabilisation through dashboard wiring.

**Status legend:** ✅ landed on `dev` · 🔄 in flight · ⬜ not started
**Branch model:** `dev` is the integration branch; feature branches off `dev`, merge back to `dev` after code-reviewer PASS.

**Phase 1 status:** ✅ §3a (Sprint 1a) merged to `dev` at `79c1482`. ✅ §3b (Sprint 1b) merged to `dev` at `91261cd`.
**Phase 2 status:** 🔄 §10a Sprint 2a in flight on `feature/finance-phase-2` (operational core). §10b (fee engine) and §10c (UI + portal wiring) scoped below.

---

## §0 — Conventions

- All work in a worktree branched from `dev`. No direct edits in main checkout.
- Frontend page template (5-file checklist, see `.harness/docs/conventions.md`):
  1. `client/src/store/<module>.ts` (Zustand slice wrapping the shared axios `api`)
  2. `client/src/pages/<area>/<Page>.tsx` (the page component)
  3. Route added in `client/src/App.tsx` `getRoutes()` with `<ProtectedRoute allowedRoles>`
  4. Nav entry added in `client/src/components/Nav.tsx` `NAV_CONFIG` (per role)
  5. Changelog entry in `.harness/changelogs/YYYY-MM-DD.md`
- Backend template (see `IMPLEMENTATION_SUMMARY.md` §1): inline `auth` middleware, FK pragma on, all writes set `sync_status='pending'`.
- Sync contract: every SQL table has `last_synced_at`, `sync_status DEFAULT 'pending'`, `is_deleted DEFAULT 0`, `uid TEXT UNIQUE`. Tables added to `tablesToSync` in `SyncEngine.js` (FK dependency order, parents first).
- Roles (active UI): `school_admin`, `systems_admin`, `principal`, `hr`, `bursar`, `teacher`, `student`, `parent`, `librarian`, `clubs_head`, **accountant** (now active in §10). Demo accounts in `server/src/database/init.js`.

---

## §3 — Phase 1: HR + Finance Portal UIs
*See above — fully landed.*

---

## §10 — Phase 2: Operational Finance + Fees Engine

**Goal:** Turn the Bursar/Accountant role from "looks at expense receipts" into a real back-office. Build:
- **Operational core** — formal invoices, suppliers/vendors, chart of accounts, bank accounts + reconciliation, school trips + trip payments.
- **Fees engine** — student groups, payment plans with installments, discounts (per-student or per-group), automatic fee-line generation.
- **Portal + views** — Accountant + Bursar back-office pages; Student + Parent portal pages that surface the same data (their own fees, installments, discounts, trip bills).

**Branch model:** one branch per sub-sprint, fast-forwarded into `dev` between sub-sprints.

| Sub-sprint | Branch | Goal |
|---|---|---|
| §10a | `feature/finance-phase-2` (2a slice) | Schema for everything + operational controllers (invoices, suppliers, chart of accounts, banking, trips, trip_payments) + tax-friendly CRUD UI for the operational core |
| §10b | `feature/finance-phase-2b` | Fee engine: student_groups + student_group_members; fee_plans + fee_plan_installments; discounts; auto-generate student_fees from a plan; apply plan/discount to a student or group |
| §10c | `feature/finance-phase-2c` | Accountant + Bursar portal pages; Student + Parent fees page rewrite to show installments + discounts + trip bills; route + nav wiring; SCREENS.md refresh |

Each sub-sprint gets its own 5-file-changeset; promote to `dev` after build-green.

### RBAC additions

| Endpoint family | Bursar | Accountant | Principal/Admin | Student/Parent |
|---|---|---|---|---|
| `/api/fees/groups`, `/api/fees/plans`, `/api/fees/discounts`, `/api/fees/groups/students/*` | CRUD | CRUD | CRUD | read-self |
| `/api/fees/students` (list of student_fees) | read-all | read-all | read-all | read-self / read-children |
| `/api/finance/invoices` | CRUD | CRUD | CRUD | read-self |
| `/api/finance/suppliers` | CRUD | CRUD | CRUD | — |
| `/api/finance/chart-of-accounts` | CRUD | CRUD | CRUD | — |
| `/api/finance/banking/accounts` + `/banking/reconciliations` | CRUD | CRUD | CRUD | — |
| `/api/finance/trips` + `/finance/trips/:id/students` | CRUD | CRUD | CRUD | read-self (own trips) |
| `/api/fees/invoices` per-student | yes | yes | yes | yes |

Accountant has the same operational write access as Bursar but **does not** own payroll — payroll stays HR-only (Phase 0 decision). Accountant focuses on fees/billing/invoices/reconciliation.

### New demo accounts

| Email | Password | Role | Phase 2 access |
|---|---|---|---|
| `accountant@school.com` | `accountant123` | `accountant` | All `/api/fees/*`, `/api/finance/{invoices,suppliers,chart-of-accounts,banking,trips}/*` |

### Out of scope here (deferred to §11)

- ZIMRA FDMS / fiscalisation (Phase 3 — gated on test creds).
- Double-entry book-keeping engine (we track nominal accounts only; full GL postings deferred).
- Invoice PDF generation (we render printable HTML — flag a tiny PDF lib as a stretch goal in §11).
- Mobile-money autopay integrations beyond Paynow (no plans in repo yet).
- Cross-portal reporting (`/reports` enrichment) — Phase 2 was already deferred in §9.
- Accountant / Librarian / Nurse dashboard rest-of-modules (Librarian already has books/issues/fines; this only adds the back-office half the user asked for).

### Acceptance tests (Phase 2)

- Logged in as `accountant@school.com / accountant123`, `/finance/invoices` lists every student invoice with status + total + paid.
- Logged in as `bursar@school.com`, `/finance/invoices/:uid` shows the printable view that can be sent to a parent.
- Logged in as `bursar@school.com`, `/finance/trips` shows trips with per-student cost and a "Mark student paid" button (POST /finance/trips/:id/students/:studentId/pay).
- Logged in as `bursar@school.com`, `/finance/banking/reconciliations` shows this period's reconciliation status with unmatched-count tile.
- Logged in as `accountant@school.com`, `/finance/suppliers` lists suppliers with active-accounts count and bulk-payment-shortcut (links to invoice creation).
- Logged in as `bursar@school.com`, `/finance/plans/new` accepts "3 equal installments starting 2027-02-15" and POSTs `fee_plan_installments` rows; existing `student_fees` stays untouched (plans are templates — apply step in §10b).
- Logged in as `parent@school.com`, `/parent/fees` shows the child's fee lines grouped by `fee_group` with paid/outstanding totals and a Payment button.
- Logged in as `student@school.com`, `/student/fees` lists the student's own fees; for teacher logins the route falls back to `/fees`.
- All new pages pass `npm run build`, no `alert()` / `window.prompt()` in this code path.
- Sync contract: every write goes through with `sync_status='pending'`. `tablesToSync` extended (see §10a).

### Tables to add — full Phase 2 list (all 12)

Schema source of truth: `server/src/database/init.js` (idempotent CREATE TABLE IF NOT EXISTS + matching idempotent ALTER in the `migrations` array).

| Group | Table | Notes |
|---|---|---|
| Operational | `suppliers` | vendors for ops expenses; uid, name, contact, email, address, tax_id, status (active/dormant) |
| Operational | `chart_of_accounts` | accounting categories; uid, code, name, type (asset/liability/equity/revenue/expense), parent_id self-FK nullable, color (for tree UI) |
| Operational | `bank_accounts` | uid, name, bank_name, account_number (last4 only), currency (USD/ZIG), balance REAL, is_active |
| Operational | `bank_reconciliations` | uid, bank_account_id FK, period_start, period_end, statement_balance, book_balance, status (draft/balanced/discrepancy), reconciled_at, reconciled_by FK users |
| Operational | `bank_reconciliation_lines` | uid, reconciliation_id FK, payment_id FK payments, statement_amount, match_status (matched/unmatched/ignored), notes |
| Operational | `invoices` | uid, invoice_number UNIQUE, student_id FK users, fee_group_id FK nullable, plan_id FK nullable, discount_id FK nullable, subtotal, discount_amount, total, paid_amount, currency, status (draft/issued/paid/overdue/cancelled), issued_at, due_date, notes |
| Operational | `trips` | uid, name, destination, departure_date, return_date, cost_per_student REAL, total_capacity, status (draft/published/closed/completed/cancelled), description |
| Operational | `trip_payments` | uid, trip_id FK, student_id FK users, invoiced_amount, paid_amount, status (pending/partial/paid/refunded); UNIQUE (trip_id, student_id) |
| Fees engine | `student_groups` | uid, name, description, type (class/trip/hostel/transport/club/custom), color, class_id nullable FK, capacity (nullable); class_id makes a group auto-track class enrollments |
| Fees engine | `student_group_members` | uid, group_id FK, student_id FK users, joined_at; UNIQUE (group_id, student_id) |
| Fees engine | `fee_plans` | uid, name, fee_group_id FK, total_amount, installments_count, start_date, end_date, academic_year, auto_apply_to_class_id nullable FK, notes |
| Fees engine | `fee_plan_installments` | uid, fee_plan_id FK, sequence INT, label, amount, due_date |
| Fees engine | `discounts` | uid, name, kind ('percent'|'fixed'), value REAL, scope ('student'|'group'|'fee_group'|'all'), scope_id nullable, reason, valid_from, valid_to, academic_year, sync fields |

Reuse-not-rebuild: `student_fees`, `fee_groups`, `payments`, `hostels`, `vehicles`, `routes`, `transport_allocations` already exist. We link extras (hostel, transport) by reusing `fee_groups.type` (`hostel` / `transport`) and `student_fees` rows.

### Sync engine additions

Append to `tablesToSync` in `server/src/services/SyncEngine.js` (in FK dependency order):

```
suppliers, chart_of_accounts, bank_accounts, bank_reconciliations, bank_reconciliation_lines,
invoices, trips, trip_payments,
student_groups, student_group_members,
fee_plans, fee_plan_installments,
discounts
```

12 new sync-aware tables, parent-first order respected.

---

### §10a — Sprint 2a: Operational core

**Goal:** Land schema for the full Phase 2 set + operational controllers + back-office UI for invoices/suppliers/chart-of-accounts/banking/trips.
**Branch:** `feature/finance-phase-2` (worktree at `.worktrees/finance-phase-2/`).
**Reins involved:** `backend-expert` (schema + controllers) → `frontend-expert` (operational pages) → `code-reviewer` (gate).

#### Backend (in this branch's first commit(s))

| # | Endpoint | File | Notes |
|---|---|---|---|
| C1 | `GET/POST/PATCH/DELETE /api/finance/suppliers` | new `finance-suppliers.controller.js` | CRUD; soft-delete. |
| C2 | `GET/POST/PATCH/DELETE /api/finance/chart-of-accounts` | new `finance-coa.controller.js` | Tree-aware: GET supports `?parent_id=` and a flat-list `?flat=true`. PATCH for reorder. |
| C3 | `GET/POST/PATCH /api/finance/banking/accounts` | new `finance-banking.controller.js` | Per-account balance updates via receipt. |
| C4 | `POST /api/finance/banking/accounts/:id/reconcile` | same | Body: statement lines (date, amount, ref). Server matches against unmatched `payments` rows by reference + amount. Returns matched/unmatched counts. |
| C5 | `GET /api/finance/banking/reconciliations` + `GET /:uid` + `POST /:uid/resolve` | same | Reconciliation session; resolve transitions a line to "ignored". |
| C6 | `GET/POST/PATCH /api/finance/invoices` | new `finance-invoices.controller.js` | List (paginated, status/student filters), detail (lines joined), POST creates from a fee_group_id or plan_id, PATCH updates notes/status (no direct paid-totals edit; those update via payment webhooks). |
| C7 | `POST /api/finance/invoices/:uid/send` | same | No-op mailer (logs and returns 200) — wire SMTP later. |
| C8 | `GET/POST/PATCH /api/finance/trips` | new `finance-trips.controller.js` | CRUD on trips. |
| C9 | `GET/POST /api/finance/trips/:id/students` | same | Assign students to a trip (bulk, by class or manual). |
| C10 | `POST /api/finance/trips/:id/students/:studentId/pay` | same | Records trip_payment with status `paid` (or `partial` if partial amount). |
| C11 | `GET/POST/PATCH/DELETE /api/fees/discounts` (pre-step for §10b, but schema-only here) | extend `fees.controller.js` | List/create/update/soft-delete; `scope` enforcement (server resolves `scope_id`). |
| C12 | `GET/POST/PATCH /api/fees/plans` + `/plans/:uid/installments` | extend `fees.controller.js` | CRUD on plans + nested installments. |

#### Frontend (in this branch's second commit)

| # | Page file | Route | Allowed roles | Backing API |
|---|---|---|---|---|
| 1 | `client/src/pages/finance/Invoices.tsx` | `/finance/invoices` | bursar/accountant/principal/admin | `GET /api/finance/invoices` |
| 2 | `client/src/pages/finance/InvoiceView.tsx` | `/finance/invoices/:uid` | bursar/accountant/principal/admin | `GET /api/finance/invoices/:uid` |
| 3 | `client/src/pages/finance/Suppliers.tsx` | `/finance/suppliers` | bursar/accountant/principal/admin | `GET /api/finance/suppliers` |
| 4 | `client/src/pages/finance/ChartOfAccounts.tsx` | `/finance/chart-of-accounts` | bursar/accountant/principal/admin | `GET /api/finance/chart-of-accounts` |
| 5 | `client/src/pages/finance/BankingAccounts.tsx` | `/finance/banking` | bursar/accountant/principal/admin | `GET /api/finance/banking/accounts` + `reconciliations` |
| 6 | `client/src/pages/finance/BankingReconcile.tsx` | `/finance/banking/reconciliations/:uid` | bursar/accountant/principal/admin | `GET /api/finance/banking/reconciliations/:uid` |
| 7 | `client/src/pages/finance/Trips.tsx` | `/finance/trips` | bursar/accountant/principal/admin/principal | `GET /api/finance/trips` |
| 8 | `client/src/pages/finance/TripDetail.tsx` | `/finance/trips/:id` | bursar/accountant/principal/admin | `GET /api/finance/trips/:id` + assign students |

#### Out of scope here

- Plan apply endpoint (`POST /api/fees/plans/:uid/apply`) — §10b.
- Group membership CRUD UI — §10b (we only seed groups + members here).
- Student/Parent view refresh — §10c.

---

### §10b — Sprint 2b: Fee engine

**Goal:** Student groups (with auto-class sync), fee plans, installments, discounts, plan-apply endpoint (auto-generates student_fees from a plan).
**Branch:** `feature/finance-phase-2b` (worktree at `.worktrees/finance-phase-2b/`).

#### Backend

| # | Endpoint | File | Notes |
|---|---|---|---|
| D1 | `GET/POST/PATCH/DELETE /api/fees/groups` | new `fees-groups.controller.js` OR append to `fees.controller.js` | CRUD. `type` enforces (tuition/transport/hostel/exam/library/activity/other). |
| D2 | `GET/POST/DELETE /api/fees/groups/:uid/members` | same | Bulk add (class_id shortcut → expands to all enrolled students). |
| D3 | `POST /api/fees/plans/:uid/apply` | extend `fees.controller.js` | Body: `{scope: 'student'|'group'|'class', id, academic_year, due_date_offset?}`. Generates N `student_fees` rows per recipient (one per installment) using plan.installments × recipient.student_fees assignment. Wrapped in `db.transaction()`. Each generated row has `due_date` = plan.installment.due_date + offset. Returns count. |
| D4 | `POST /api/fees/discounts/:uid/apply` | same | Body: `{scope, id, academic_year?}`. Walks recipients (`scope=student`: that student; `scope=group`: members; `scope=fee_group`: open student_fees for that fee_group in the academic year; `scope=all`: every active student). For each recipient, sets `student_fees.discount_amount = computed` (capped at fee amount; non-negative; idempotent per (student_fee, discount)). Discriminator stored in a `student_fee_discounts` join (NEW table) so a single fee can carry multiple discounts. |
| D5 | `DELETE /api/fees/discounts/:uid/revoke` | same | Removes join rows + reverts `student_fees.discount_amount`. |
| D6 | new `student_fee_discounts` join table | init.js | uid, student_fee_id FK, discount_id FK, amount_applied, applied_at, applied_by FK users; UNIQUE (student_fee_id, discount_id) |

#### Frontend

| # | Page file | Route | Allowed roles | Backing API |
|---|---|---|---|---|
| 9 | `client/src/pages/finance/StudentGroups.tsx` | `/finance/groups` | bursar/accountant/principal/admin | `GET /api/fees/groups` (a small note in plan explains naming collision — server route is `/api/fees/groups`) |
| 10 | `client/src/pages/finance/StudentGroupDetail.tsx` | `/finance/groups/:uid` | bursar/accountant/principal/admin | `GET /api/fees/groups/:uid` (extended to include members) |
| 11 | `client/src/pages/finance/FeePlans.tsx` | `/finance/plans` | bursar/accountant/principal/admin | `GET /api/fees/plans` |
| 12 | `client/src/pages/finance/FeePlanDetail.tsx` | `/finance/plans/:uid` | bursar/accountant/principal/admin | `GET /api/fees/plans/:uid` + `POST /plans/:uid/apply` |
| 13 | `client/src/pages/finance/Discounts.tsx` | `/finance/discounts` | bursar/accountant/principal/admin | `GET /api/fees/discounts` + apply/revoke |

#### Out of scope here

- Student-fee generation UX (the modal lives on the plan detail; new fee lines from a single click).
- Auto-apply to all new class joiners (nice-to-have; defer — `code-reviewer` would push back on this without a feature flag).

---

### §10c — Sprint 2c: Portal + Student/Parent view

**Goal:** Accountant dashboard + Bursar dashboard tiles for trips / banking / invoices / groups; Student + Parent view surfaces installments + discounts + trip payments; final route + nav wiring.

#### Backend additions

- `GET /api/finance/dashboard/summary` (already exists, §10c enriches the tiles to include pending reconciliation, overdue invoices, upcoming trips, plan install-due-soon count).
- `GET /api/students/:uid/fees` (helper) — joins `student_fees` + `fee_groups` + active discounts + active trip_payments for a single student. RBAC: admin/bursar/accountant/principal can fetch any; parent can fetch children; student can fetch self.

#### Frontend

| # | Page file | Route | Allowed roles | Backing API |
|---|---|---|---|---|
| 14 | rewrite `client/src/pages/dashboard/FinanceDashboard.tsx` | `/dashboard/finance` | bursar/accountant/principal/admin | enriched summary + new tiles |
| 15 | `client/src/pages/dashboard/AccountantDashboard.tsx` | `/dashboard/accountant` | accountant/bursar/principal/admin | lighter mirror of Finance dashboard with fee-engine focus |
| 16 | rewrite `client/src/pages/parent/Fees.tsx` | `/parent/fees` | parent | `/api/fees/students?student_id=...` + new `/students/:uid/fees` helper |
| 17 | `client/src/pages/student/Fees.tsx` | `/student/fees` | student | `/api/fees/students` + new helper |

#### Route + nav wiring

- Add `/dashboard/accountant`, `/finance/{invoices, suppliers, chart-of-accounts, banking, trips, groups, plans, discounts}` to `NAV_CONFIG` for `bursar` (most) and `accountant` (all).
- Add `/finance/invoices/:uid`, `/finance/banking/reconciliations/:uid`, `/finance/groups/:uid`, `/finance/plans/:uid`, `/finance/trips/:id` for `bursar`/`accountant`.
- Add `/student/fees` to the `student` nav block.

#### SCREENS.md

Refresh the Bursar + Accountant + Student + Parent tables with the new entries.

---

## §6 — Phase 0 (landed)

> `feature/stabilise-finance` from `dev` at `75b74e7` — landed on `dev` at `030288c`. Code-reviewer PASS.
> De-duplicated payroll routes (HR controller is now source of truth; `/api/finance/payroll/*` proxies to `/api/hr/payroll-runs` and `/api/hr/payslips`). Fixed `client/src/store/finance.ts` (HR-aligned, typed, no `/api/` prefix). Dropped the dead `expenses` slice. Allowed `bursar`/`accountant` on HR payroll routes. Seeded `bursar@school.com / bursar123`.

---

## §9 — Phase 2+ (deferred / covered in §10)

§10 absorbed all the formerly-§9 items the user asked for (cross-portal fee plumbing, paynow reconciliation scope, accountant dashboard skeleton). Remaining out-of-scope for Phase 2:

- Cross-portal **reporting joins** across expenses + payroll + fees (`/reports` enrichment) — pull into §11 once §10c lands and real data exists.
- Test framework + CI — `.harness/AGENTS.md` open follow-up — `tester` rein.
- Phase 3 fiscalisation (ZIMRA FDMS) — gated on test creds.
- Full double-entry GL (chart of accounts exists in §10a as a *nominal* lookup; journal entries and trial balance deferred).

## §11 — Stretch / post-Phase-2 (next sessions)

- PDF invoice rendering (lightweight; consider `pdfkit` or server-side print stylesheet).
- Paynow webhook extension to mark `trip_payments` paid.
- Burn-down reports for fee-collection period over period.
- Sibling discount + bursary tiers (sit on top of the `discounts` table; defer until §10b is in user hands).
- Bulk-import-from-Excel for student_groups + chart_of_accounts.
