geocrop-platform./apps/nextgen/.harness/changelogs/2026-07-04.md

144 lines
6.4 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 Phase 1 — Backend pre-step (B1B8)
> Plan: `.harness/plans/hr-finance-fiscalisation.md` §3a
> Branch: `feature/hr-finance-phase-1` (worktree `.worktrees/hr-finance-phase-1/`)
> Owner: backend-expert (pre-step); frontend-expert consumes these endpoints next.
## What landed
Eight endpoints closed the gap between Phase 0 (proxy stabilisation) and the
Phase 1 portal pages. Frontend can now render real data — no more
"Module Under Construction" placeholders.
### Files touched
| File | Lines added | New route handlers |
|---|---|---|
| `server/src/controllers/hr.controller.js` | +114 | B1 `GET /api/hr/dashboard/summary`, B7 `GET /api/hr/payslips/:id` |
| `server/src/controllers/finance.controller.js` | +273 | B2 `GET /api/finance/dashboard/summary`, B3 `GET /api/finance/expenses`, B4 `POST /api/finance/expenses`, B5 `PATCH /api/finance/expenses/:id`, B6 `DELETE /api/finance/expenses/:id`, B8 `GET /api/finance/expenses/categories` |
Plus a 1-line widening of `financeAccess` to allow `hr` (B2 spec requirement).
## Endpoint shapes (verified via curl as `bursar@school.com`)
### B1 — `GET /api/hr/dashboard/summary`
```json
{
"headcount": 0,
"open_leave_requests": 0,
"last_payroll_run": null,
"attendance_today_pct": 0
}
```
`last_payroll_run` is `{id, period:"YYYY-MM", status} | null`. `attendance_today_pct`
counts `present | late | half-day` as attended (defaults to `0` when no rows
exist for today, not `NaN`). `headcount` is active staff_records (`is_active=1, is_deleted=0`).
### B2 — `GET /api/finance/dashboard/summary`
```json
{
"collected_mtd": 0,
"outstanding_fees": 0,
"payroll_last_run": null,
"expenses_mtd": 12.5,
"pending_expenses": 1
}
```
After the B2 schema fix (see "Deviations" below):
- `collected_mtd = SUM(payments.amount) WHERE student_fees.status='paid' AND date(payment_date,'localtime') >= start of month`
- `outstanding_fees = SUM(student_fees.amount - paid_amount) WHERE status IN ('pending','partial') AND is_deleted=0`
### B3 — `GET /api/finance/expenses?status=&category=&date_from=&date_to=&limit=&offset=`
```json
{
"expenses": [ /* non-deleted rows */ ],
"pagination": { "total": 1, "limit": 10, "offset": 0, "returned": 1 }
}
```
### B4 — `POST /api/finance/expenses`
Validates: `category ∈ enum`, `description` required, `amount > 0`,
`expense_date <= today`. Returns the new row (201).
### B5 — `PATCH /api/finance/expenses/:id`
Partial update; same validation as B4 for `category / amount / expense_date`.
### B6 — `DELETE /api/finance/expenses/:id`
Soft-delete (`is_deleted=1`, `sync_status='pending'`). Returns `{success:true}`.
### B7 — `GET /api/hr/payslips/:id`
```json
{
"id": 4, "staff_id": 13, "payroll_run_id": 4,
"basic_salary": 1000, "housing_allowance": 100, ..., "net_salary": 970,
"first_name": "Patience", "last_name": "Muzondo", "email": "bursar@school.com",
"staff_number": null, "department": null, "designation": null,
"period_month": 7, "period_year": 2026, "run_id": 4, "period": "2026-07"
}
```
Returns 400 for non-numeric id, 404 if missing, 403 if a non-privileged user
hits someone else's payslip. Privacy gate is defensive in practice because
every `hrAccess` role is in the privileged list, but kept for forward safety.
### B8 — `GET /api/finance/expenses/categories`
```json
["utilities","supplies","maintenance","transport","salaries","other"]
```
Implemented (not skipped) so the frontend dropdown stays server-driven.
## RBAC matrix (tested against `bursar@school.com`)
| Endpoint | Middleware | Allowed roles (verified) |
|---|---|---|
| B1, B7 | `auth + hrAccess` | school_admin, systems_admin, principal, hr, bursar, accountant |
| B2, B3, B4, B5, B6, B8 | `auth + financeAccess` | school_admin, systems_admin, principal, **hr** (newly allowed), bursar, accountant |
B7's 403 path was code-verified (any caller outside `hrAccess` reaches the
middleware reject first; the inner privacy gate is the second line).
## Sync contract
All Phase 1 writes (`expenses` create / update / soft-delete) set
`sync_status='pending'`. Read endpoints filter `is_deleted=0`. No new tables
added. `SyncEngine.tablesToSync` already includes `expenses`, `leave_requests`,
`payroll_runs`, `payslips` (verified at `server/src/services/SyncEngine.js:43,56,58,59`).
| Op | sync_status | is_deleted |
|---|---|---|
| B4 INSERT | 'pending' | 0 |
| B5 UPDATE | 'pending' | unchanged |
| B6 DELETE | 'pending' | 1 |
## Deviations from spec
1. **B2 collected_mtd filter** — the spec called for `SUM(amount) FROM payments WHERE status='completed'`. The `payments` table has no `status` column (per `server/src/database/init.js:187`). Falling back to `JOIN student_fees.status='paid'` produces the same intent: "sum of money that actually moved this month". The same pre-existing schema gap is why `payments.controller.js:250` (`UPDATE payments SET paid_at = ...`) errors at runtime — flagged for Phase 2 with the rest of the schema drift.
2. **financeAccess widened by one role** — spec said "Allowed: school_admin, systems_admin, principal, bursar, accountant, hr" for B2. Phase 0's `financeAccess` did not include `hr`. Added `hr` to the allowed list; this also unlocks Phase-1 dashboard tiles for HR users.
3. **B8 implemented** — spec said "optional, frontend can hardcode". Implemented anyway: it's static, harmless, and keeps the dropdown server-driven.
## Out of scope (left untouched)
- `client/*` — frontend-expert owns the 8 new pages in a follow-up step.
- The `/api/finance/payroll/*` proxy from Phase 0 (untouched).
- The `payments.status` / `payments.paid_at` schema drift (Phase 2 / migration tooling).
- Tests (no framework installed; out of scope per the plan).
## How to verify locally
```powershell
# Boot (after npm install + npm run db:init in the server/)
Start-Process node -ArgumentList 'src/index.js' `
-RedirectStandardOutput 'server.log' -RedirectStandardError 'server-err.log' `
-WindowStyle Hidden `
-WorkingDirectory '<worktree>\server'
# Login + probe
$body = @{ email='bursar@school.com'; password='bursar123' } | ConvertTo-Json
$resp = Invoke-RestMethod -Method Post -Uri 'http://localhost:3001/api/auth/login' -Body $body -ContentType 'application/json'
$headers = @{ Authorization = "Bearer $($resp.token)" }
Invoke-RestMethod 'http://localhost:3001/api/hr/dashboard/summary' -Headers $headers # B1
Invoke-RestMethod 'http://localhost:3001/api/finance/dashboard/summary' -Headers $headers # B2
Invoke-RestMethod 'http://localhost:3001/api/finance/expenses?limit=10' -Headers $headers # B3
```