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

20 KiB

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'

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