Finish PFMS: pre-filled forms, settlement, audit bug-fixes, seeded office, subscriber PWA - #18
Merged
Merged
Conversation
- Added Role entity & mapped to User - Secured endpoints via Spring Security - Integrated CustomUserDetailsService - Signup assigns roles with default access
feat: Implement full CRUD API for fund management with RBAC
feat: Implement auditor CSV import feature (backend & frontend)
Fix: Corrected nomenclature and fixed the bugs
feat(auditor): add CSV upload history with logging and retrieval endpoint
The interest engine and the amortisation schedule existed with no way to reach them. Finance can now run a years interest across every account (idempotent, so pressing it twice is safe rather than a doubling), read any loans schedule, and record an instalment as paid. Each one records who did it.
The employee had no way to see where their application had got to without walking to the office and asking. The chain is now on screen, stage by stage, in the plain words of the form rather than enum names.
The pink part-final form and the green loan form are what the system has to reproduce, and they settle questions no other source does. Kept in the repo so a field can be checked against the paper rather than against someones memory of it. Drops two duplicate scans.
Every one of the 26 workflows audited against the code that actually runs, not against the documentation. Captures what works, where the depth is missing, the edge cases a real PF office produces, and the defects the audit turned up along the way: the CSV import double-posting a duplicate row, the apply form hardcoding a compliance answer nobody was asked, and no date of birth or joining date on the employee record to fill the form from.
The whole-file pre-check queried the database for an already-imported month, but nothing is persisted until the posting loop, so two rows for the same employee in one sheet both passed it and both posted — the employee was credited twice for the month. Guard the pre-check with an in-file seen-set so a duplicate employeeId fails the whole file before any money moves.
The loan and part-final forms print Name, Designation, Date of Birth and Mobile at the top, but the record held none of them, so the employee had to re-key onto every form what the office already knows. Add those four fields to the employee record (date of joining stays on the PF account, which is what accrues service), with a migration for each dialect. All nullable — existing rows predate them.
The paper process makes the applicant re-copy their particulars, the ceiling and their last loan onto every form, and each re-copy can go wrong. Assemble that block once from the authoritative record and serve it at GET /api/users/me/loan-form and GET /api/part-final/form, so the form arrives filled and the employee only supplies what is genuinely new: the amount, the purpose, the proof. Every figure is read through the same service that enforces it — the ceiling from LoanService, eligibility from PartFinalService — so a pre-filled form can never quote a number the apply step would then reject.
The pre-filled loan and part-final forms read Name, Designation, Date of Birth and Mobile from the employee record; the demo employees had none, so the forms rendered blank in a local run. Set all four deterministically from the loop index so a fresh seed reproduces them and the pre-fill is visible end to end.
… given The application form hardcoded priorUtilisationSubmitted=true on every submit and never sent the documents enclosed — so the record told the office the employee had filed their previous utilisation certificate whether or not they had, and carried no note of what proof came with the application. Drive the form from the pre-fill instead: show the particulars the office already holds, ask the prior-utilisation question only when the record shows a previous withdrawal (and then require a real answer), let the employee attach the proof, and send what they actually entered. The enclosure is uploaded against the new application so the officer deciding it sees the same page the employee sent.
The loan dialog asked only for amount, type and reason, but the rule engine requires the number of EMIs — so every submission was being rejected before it reached a queue. Add the instalments field and send it. While there, drive the dialog from GET /api/users/me/loan-form: the particulars, the ceiling (75% of net credit), the EMI cap and the previous-loan line arrive filled from the record, and a live loan disables the form with a plain reason — the green paper form, minus everything the office already knows.
The upload help still described the old header (employeeId, ownContribution, instituteContribution) — the format from before the PF became derived. It now shows the real header (employeeId, basic, da, vpf), explains that the sheet carries salary inputs and the PF is derived from the scheme, and notes the two rules the importer actually enforces: one row per employee, and no re-import of a month already posted.
Approving a retirement only set user.active = false. The employee could no longer log in, but the fund the Trust held for them stayed on the books un-paid, the outstanding loan was never recovered, and the PF account sat open forever — the one balance the system exists to settle, left unsettled, and SETTLEMENT / PfAccount.close() dead code. Settlement now does what the office does on the last day, atomically and through the ledger: recover any outstanding loan, pay out the whole remaining fund as a SETTLEMENT so the balance lands at zero, close the account, then deactivate the employee and approve the request. Only a PENDING request can be settled, so a double submit cannot pay a fund out twice.
Fold in the four workflows that dropped out of the automated pass — partfinal-apply, finance-csv-upload, finance-rates, retirement-settlement — investigated by hand from the source. Update the header and maturity table, add a deep-dive addendum for the four, and add a bug-status table: seven defects the audit surfaced, five now fixed (CSV double-post, form falsification, missing profile fields, stale upload help, retirement settlement) and two open with a recommended direction (rate back-dating, the dead per-year policy key).
…eproducible The repayment schedule is derived, not stored, and re-resolved the loan-interest rate by disbursal date every time it was drawn. So a rate row Finance back-dated to before an existing disbursal silently re-priced a running loan — the schedule an employee had already seen could change under them. Snapshot the rate in force onto the loan when it is approved, and have the schedule read that snapshot in preference to resolving by date. Back-dated rate rows can still be added (retroactive announcements are legitimate) but no longer disturb a loan that was already disbursed. Legacy loans without a snapshot fall back to the old as-of-disbursal resolution, so nothing already on the books changes.
The PART_FINAL_MAX_PER_YEAR policy (the meeting's 'three part-finals in a year') was seeded but never read, so it enforced nothing. Wire it into the eligibility verdict as a rolling twelve-month cap: count the part-finals already paid to the employee in the last year and refuse a further one past the limit, with a reason that names the cap. It sits alongside the six-month gap as a distinct rule — whichever binds first stops the application — and the verdict now reports the cap and the count so the employee sees where they stand. The seeded value stays at three (faithful to the audio) and unconfirmed, so Finance is still prompted to confirm the exact number.
Rate back-dating and the per-year cap are now both resolved, so update the bug table and its summary — all seven defects the audit surfaced are fixed.
The base seeder (the establishment, auditor, secretary and three committee logins) was an unordered runner, so it ran last — after the demo seeders that vote, assess and attribute uploads to those very accounts. It happened to work only because the demo scenario seeder never needed a full committee and quietly skipped the CSV logs it could not attribute. Order the base seeder first, and key the demo-data seeder's idempotency off its own first employee rather than a head-count (which a base-first order would trip). The scenario seeder now also skips any employee without a funded PF account, so the base demo employee — which shares the id prefix but has no contributions — is not assessed.
Add a seeder that fills the office the way a real one fills up: hundreds of loan, part-final and retirement cases resting at every interesting point of every workflow, so each screen and each officer queue has real volume to test against rather than one lonely record per state (404 cases across 404 employees at the default counts, alongside the existing demo data). Every case is created through the same services an officer's click would call, so each is in a state the state machine actually permits and any money it moved is on the ledger. Only the states that move money pay the ledger cost, so the volume is affordable. It runs when pfms.seed.workflow-cases=true (on in the local and demo profiles), is idempotent, and is fully deterministic; the per-bucket counts are constants to dial up or down. The base demo-data test opts out to stay fast.
A small deck of cards that introduces the portal: what it is, that a part-final passes through several separate desks, what the logged-in role can do, and the other logins to try the same case from another side. It opens once per browser and can be reopened any time from the new header button. Deliberately self-contained — it reads no seeded data and calls no API, so the tour is fully isolated from the office dataset: it explains what the screens are, it does not depend on what is in them.
The mobile app is a thin renderer: it asks for a screen by name and draws a short
sequence of primitive nodes — a grid of figures, a section heading, a list, a card,
a button. This adds the node vocabulary, the screen envelope, and a service that
assembles the subscriber's home screen from the same services the web app uses, so
the phone and the desktop can never show two different balances.
Layout and copy now live on the server next to the data, so a screen can gain a
figure or reword a hint without shipping a new build of the app. Exposed at
GET /api/app/screens/{key}, scoped to the caller.
A small React PWA for subscribers: sign in, then see your PF balance, part-final withdrawals and loans, and track them from a phone. It is a thin renderer — it draws the sequence of primitive nodes the server sends for GET /api/app/screens/home and holds no screen layout of its own, so what it shows is decided on the server next to the data (src/sdui.ts mirrors the node types; src/SduiRenderer.tsx is the one place that maps a node to UI). Installable via a web manifest and an SVG icon; a minimal service worker caches the app shell for offline open but never caches API responses. Theme-aware. In dev the Vite server proxies /api to the backend, so it is same-origin with no CORS to set up. Builds clean (tsc + vite).
Login lives at /auth (no /api prefix) on the server, like the web portal — the app was posting to /api/auth/login. Point it at /auth/login and proxy /auth in dev too, so sign-in reaches the backend.
A warm, complete walkthrough for anyone opening the repo for the first time: the one idea (the ledger is the truth), what the three apps are, the people and why there are several, the part-final chain, how to run everything locally, the seeded demo office, how the mobile app's server-driven UI works, a map of where each rule lives, testing, and pointers into docs/.
Point newcomers to the README, note the Phase-3 finishing work (enclosures, form pre-fill, retirement settlement, the annual cap, the rate snapshot, the seeded office and the PWA), record that the audit's seven bugs are fixed, and correct the test count to the current 165 green.
The cap was seeded as an unconfirmed guess pending the Trust's intent. Three a year (rolling twelve months) is confirmed, so mark it confirmed and reword the note — it no longer surfaces to Finance as an assumption to check.
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 19555744 | Triggered | Username Password | 7144bb3 | Backend/src/main/resources/application.properties | View secret |
| 19555744 | Triggered | Username Password | 06568f3 | src/main/resources/application.properties | View secret |
| 19555733 | Triggered | Username Password | 21bc3c7 | Backend/src/main/java/com/pfms/PfmsApplication.java | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
…he build Old main is the Phase-1 baseline; this branch is the finished build that superseded it. Bring main's history in as an ancestor so nothing is lost from the record, and resolve every conflict in favour of this branch — the real work. The only two files unique to old main, the legacy data.sql (replaced by the idempotent seeders) and a stray template CSV, are intentionally dropped.
Returning an entity whose User is a lazy Hibernate proxy — e.g. the pending-loans list, each LoanApplication carrying its borrower — made Jackson try to serialize the proxy's hibernateLazyInitializer/handler plumbing and throw, turning the response into a 500 whenever the list was non-empty. Tell Jackson to ignore those two proxy fields on User so the real user data serialises wherever a User is returned inside another entity.
The real-Postgres integration test pinned the schema version to '1'; at four migrations it now reads '4' and the test went red. It is skipped where there is no Docker, so a local run never caught it — only CI did. Assert no migration is left pending instead, matching the H2 migration test, so a new migration never breaks it again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this brings in
The finishing pass on PFMS — the domain is complete, the two paper forms are digitised and pre-filled,
the workflow audit's bugs are all fixed, the demo is a full busy office, and there's a new subscriber
mobile app. Every commit is atomic and the history reads clean.
Money & correctness (the workflow audit's 7 findings — all fixed)
and no longer drops enclosures; the proof is stapled to the application.
out the fund → close the account), not just an
active = falseflip.back-dated rate row can't re-price a schedule already running.
rejected before).
MAX_PER_YEARpolicy is now enforced (three perrolling year, confirmed).
Forms mapped to the paper, pre-filled
Added the employee particulars the forms print (name, designation, DOB, mobile) + migration, and new
pre-fill endpoints so the loan and part-final forms arrive filled from the record — the employee supplies
only the amount, the purpose and the proof. Every figure is read through the same service that enforces
it, so a pre-filled form can never quote a ceiling the apply step would then reject.
Enclosures (documents) subsystem
Upload / list / download proof against a part-final; ownership enforced by construction; evidence frozen
once an application is decided; path-traversal-safe; audited.
The seeded demo office + guided tour
~480 real cases resting at every stage of every workflow, each created through the real services so it's
ledger-consistent and state-valid. Plus an isolated guided tour that reads no seed data.
Subscriber PWA with server-driven UI
A small, installable React PWA whose home screen is drawn by the server from the same services the web
app uses — phone and desktop can never disagree on a balance. See
pwa/README.md.Tests
165 backend tests green (1 Docker-gated Postgres test skipped); frontend + PWA typecheck clean.
Follow-ups (tracked, not in this PR)
#12 #13 #14 (PWA: apply flows, more screens, live e2e / native packaging) · #15 #16 (part-final: transition
audit, enclosure cross-check) · #17 (retirement: settlement voucher).
Note on history
This branch is the full clean build and has diverged from
main(a long-lived branch, plus a historysanitisation pass that removed stray commit trailers). The diff is large because it is effectively the
whole finished project. Suggested merge: review, then merge this branch as the new baseline — happy to do
it whichever way you prefer (merge commit to keep the atomic commits, or squash).