# 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/.ts` (Zustand slice wrapping the shared axios `api`) 2. `client/src/pages//.tsx` (the page component) 3. Route added in `client/src/App.tsx` `getRoutes()` with `` 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.