geocrop-platform./apps/nextgen/.harness/plans/hr-finance-fiscalisation.md

262 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.