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

116 lines
6.0 KiB
Markdown
Raw Permalink 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.

# Finance Phase 2b — Fee engine + student groups
> Plan: `.harness/plans/hr-finance-fiscalisation.md` §10b
> Branch: `feature/finance-phase-2b` (worktree `.worktrees/finance-phase-2b/`)
> Owners: backend-expert (engine + apply) → frontend-expert (groups / plans / discounts management) → code-reviewer.
## What landed
The fee engine that turns the §10a CRUD rows into actual parent-facing
billing events. This is the load-bearing part of Phase 2: a plan or
discount is just metadata until someone clicks "Apply".
### Backend — `server/src/controllers/finance-engine.controller.js`
Mounted at `/api/fees/engine`. Endpoints:
| Endpoint | Notes |
|---|---|
| `GET /api/fees/engine/groups` | list with `member_count` |
| `GET /api/fees/engine/groups/:uid` | detail incl. members array |
| `POST /api/fees/engine/groups` | create; respects `class_id + auto_track_class` to populate members automatically |
| `PATCH /api/fees/engine/groups/:uid` | edit |
| `DELETE /api/fees/engine/groups/:uid` | soft-delete the group + memberships |
| `POST /api/fees/engine/groups/:uid/members` | bulk add by ids / class / all; UNIQUE collision = skip |
| `DELETE /api/fees/engine/groups/:uid/members/:studentUid` | soft-delete a single membership |
| `POST /api/fees/engine/plans/:uid/apply` | materialise `student_fees` rows: 1 row per (recipient × installment). Body: `{scope:'student'|'group'|'class', scope_uid, academic_year?, due_date_offset_days?}`. Idempotent: matches `(student_id, plan_id, due_date)` so re-running is safe. Wrapped in `db.transaction()`. |
| `POST /api/fees/engine/plans/:uid/revoke` | soft-delete every `student_fees` row whose `plan_id` matches and `paid_amount = 0`. Paid lines are kept. |
| `POST /api/fees/engine/discounts/:uid/apply` | walk recipients per scope; insert / upsert `student_fee_discounts` rows; re-aggregate `student_fees.discount_amount` per affected fee line; defence-in-depth server cap defaults to 90% per line. Multiple discounts stack — caller orders. |
| `POST /api/fees/engine/discounts/:uid/revoke` | soft-delete every `student_fee_discounts` row tied to the discount; re-aggregate each affected fee line. |
#### Schema tweaks
Idempotent migration in `init.js`:
- `student_fees.plan_id INTEGER REFERENCES fee_plans(id)`
- `student_fees.applied_by INTEGER REFERENCES users(id)`
- `student_fees.academic_year TEXT`
These allow `revoke` to find the rows by `plan_id` without a join, and
let the operator see which academic year and which user materialised the
fee line.
#### Sync contract
- Every apply/revoke flips the touched rows' `sync_status='pending'`.
- `tablesToSync` (§10a commit) already includes `discounts`,
`fee_plans`, `fee_plan_installments`, `student_groups`,
`student_group_members`, `student_fee_discounts`.
### Frontend — 4 new pages + 1 new store slice
| # | Page file | Route | API used |
|---|---|---|---|
| 1 | `client/src/pages/finance/StudentGroups.tsx` | `/finance/groups` | `useFinanceEngineStore.{fetchStudentGroups, createStudentGroup, deleteStudentGroup, addStudentGroupMembers}` |
| 2 | `client/src/pages/finance/StudentGroupDetail.tsx` | `/finance/groups/:uid` | `fetchStudentGroup`, `removeStudentGroupMember` |
| 3 | `client/src/pages/finance/FeePlans.tsx` | `/finance/plans` | `useFinanceStore.{fetchPlans, fetchPlan, ...}` + `useFinanceEngineStore.{applyFeePlan, revokeFeePlan}`. Includes Apply-plan modal (class / group / student) and Revoke-confirm modal. |
| 4 | `client/src/pages/finance/Discounts.tsx` | `/finance/discounts` | `useFinanceStore.{fetchDiscounts, ...}` + `useFinanceEngineStore.{applyDiscount, revokeDiscount}`. Apply modal lets the operator override scope or `cap_percent`. |
A new dedicated store slice — `client/src/store/finance-engine.ts`
keeps the engine-surface state separate from the operational core.
This avoids further bloat of `finance.ts` and makes the engine actions
easy to mock for tests later.
### Wiring
- `client/src/App.tsx` — 4 routes registered across school_admin /
systems_admin / principal / bursar role guards; same allowedRoles
set as §10a (accountant inherits through these arrays).
- `client/src/components/Nav.tsx` — bursar block extended with
Payment Plans / Discounts / Student Groups; accountant block likewise.
- `SCREENS.md` — Bursar nav table widened; Accountant row extended.
### Smoke
```
POST /api/fees/plans (3-installment Term 1 plan auto-splits)
→ uid b896…
POST /api/fees/engine/plans/:uid/apply
body { scope:'student', scope_uid }
→ { scope:'student', recipients:1, installments_count:3, created:3, skipped:0 }
GET /api/fees/students?student_id=422
→ 3 fee rows, $300 each, due 2027-02-15 / 03-15 / 04-15
POST /api/fees/discounts (10% off, scope=all)
→ uid e69b…
POST /api/fees/engine/discounts/:uid/apply
→ { applied:1018, skipped:0, total_amount:11862.50, recipients:349 }
GET /api/fees/students?student_id=422
→ discount_amount:30 on every $300 row
POST /api/fees/engine/discounts/:uid/revoke
→ { removed:1018, re_aggregated:1018 }
→ discount_amount back to 0
```
### Deviations from the plan
1. **Group detail at `/finance/groups/:uid`** is a separate page from
`/finance/groups` (the plan listed membership under the detail page).
The listing page still has an "Add members" modal — clicking a row's
eye icon navigates to the detail for member removal.
2. **No `student_fees.pdf_blob` column** — invoice rendering stays HTML-
printable (the §10a invoices page). PDF generation flagged for §11.
3. **Discount scope='student' server requires scope_id** because the API
used the same hard guard for all non-"all" scopes (matches the §10a
CRUD). Future §11 polish: allow scope='student' to be applied
without scope_id, inferring the recipient from the apply body.
## Out of scope
- Auto-apply discount on new class enrolments (flagged in §10b risks;
deferred behind feature flag).
- Burn-down reports for fee-collection period-over-period (§11).
- PDF invoice generation (§11).
- Migrate trip-receipts to bank reconciliation (§11; recon match in
§10a currently scans `payments` table only).