Skip to content

Finish PFMS: pre-filled forms, settlement, audit bug-fixes, seeded office, subscriber PWA - #18

Merged
Atishyy27 merged 91 commits into
mainfrom
feat/finish-pfms-phase1
Jul 17, 2026
Merged

Atishyy27 merged 91 commits into
mainfrom
feat/finish-pfms-phase1

Conversation

@Atishyy27

Copy link
Copy Markdown
Owner

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)

  • CSV double-post — a salary sheet listing an employee twice no longer double-credits them.
  • Part-final compliance falsification — the form no longer hardcodes "certificate submitted = yes"
    and no longer drops enclosures; the proof is stapled to the application.
  • Retirement never settled — approval now performs a real ledger settlement (recover the loan → pay
    out the fund → close the account), not just an active = false flip.
  • Rate back-dating — the loan-interest rate is locked onto a loan at disbursal, so a later
    back-dated rate row can't re-price a schedule already running.
  • Loan EMI submit — the loan form now sends the required instalment count (every submit was being
    rejected before).
  • Annual part-final cap — the seeded-but-unread MAX_PER_YEAR policy is now enforced (three per
    rolling year, confirmed).
  • Stale CSV help — the auditor upload screen shows the real header.

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 history
sanitisation 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).

Atishyy27 and others added 30 commits March 15, 2025 18:59
- 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
Atishyy27 added 25 commits July 17, 2026 14:55
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

gitguardian Bot commented Jul 17, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 3 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
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
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. 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


🦉 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.
@Atishyy27
Atishyy27 merged commit e7041f1 into main Jul 17, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants