One school ยท one tenant ยท built for the office, accounts team, and admins โ not parents, not the public.
๐ Live app: schoolfees-two.vercel.app ยท ๐ฆ Repo: github.com/veerpatta/schoolfees
A fast, append-only, audit-ready fee desk that replaces the office Excel workbook. Staff maintain Students and Fee Setup; everything else โ dues, dashboards, defaulters, receipts, exports โ derives automatically. Money records are immutable; corrections flow through adjustments. It's production-live for AY 2026-27 with real data. ๐ข
| ๐ฏ Audience | Office staff, accounts team, school admins (internal only) |
| ๐งพ Core job | Collect fees, print receipts, track dues, chase defaulters |
| ๐ Promise | Posted payments/receipts are never edited in place |
| ๐ Magic | Change a fee โ unpaid dues update everywhere, no manual sync |
| โ๏ธ Stack | Next.js 16 (App Router) ยท Supabase (Postgres + RLS, Mumbai) ยท Vercel |
- ๐๏ธ Architecture
- ๐ The one rule: Source of Truth
- ๐งฉ Modules
- ๐ฅ Roles & Access
- ๐ฐ Financial integrity (read this)
- ๐ Year-end: Transfer to Next Session
- ๐ค Automation (cron)
- ๐ค Exports (incl. the AI bundle)
- ๐งฎ Fee engine internals
- ๐ Quick start
- ๐ง Environment variables
- ๐๏ธ Database & migrations
- โ Quality gates
- ๐ก๏ธ Hard safety rules
- ๐ Docs map
flowchart TB
subgraph Browser["๐งโ๐ผ Office Browser (PWA)"]
UI["shadcn/ui ยท Tailwind ยท React 19"]
end
subgraph Vercel["โฒ Vercel (Next.js 16 App Router)"]
RSC["React Server Components"]
SA["Server Actions"]
API["Route Handlers /api"]
CRON["โฐ Vercel Cron"]
end
subgraph Supabase["๐ข Supabase ยท Mumbai (ap-south-1)"]
AUTH["Auth (JWT)"]
PG[("Postgres + RLS")]
RPC["SECURITY DEFINER RPCs"]
MV["Materialized views\n(financial projection)"]
PGCRON["pg_cron */2"]
end
UI -->|navigation / forms| RSC
UI -->|mutations| SA
UI -->|data fetch| API
RSC --> PG
SA --> RPC
API --> PG
RPC --> PG
PG --> MV
PGCRON --> MV
CRON -->|nightly backup ยท auto day-close| API
AUTH -. row-level security .-> PG
Why it's shaped like this
- ๐ง Server-first โ RSC + Server Actions keep logic and secrets on the server; the browser only renders.
- ๐ Two-layer auth โ app-layer guards (
requireStaffPermission) and Postgres RLS. Defense in depth. - โก Read/write split โ writes hit live RPCs; heavy dashboards read pre-computed materialized views kept fresh on every write (with a 2-min cron backstop, see Automation).
Students + Fee Setup are canonical. Everything else derives from them โ automatically.
flowchart LR
A["๐จโ๐ Students"] --> R{{"โป๏ธ Dues\nregeneration"}}
B["๐ Fee Setup\n(fees ยท fee heads ยท dates)"] --> R
R --> I["๐งพ Installments\n(unpaid rows only)"]
I --> MV["๐ Materialized views"]
MV --> D["๐ Dashboard"]
MV --> DF["๐ Defaulters"]
MV --> EX["๐ค Exports"]
MV --> TX["๐ Transactions"]
PD["๐ต Payment Desk"] -->|live snapshot| I
style R fill:#fde68a,stroke:#d97706,color:#000
style I fill:#bbf7d0,stroke:#16a34a,color:#000
- Edit a fee, fee head, or due date โ only unpaid installments are rebuilt (paid rows are frozen โ see safety).
- The Payment Desk always previews/posts against a live allocation snapshot, so collection is never stale.
- Dashboards/exports refresh on every write; worst-case catch-up is โค 2 minutes.
All staff modules live under app/protected/ with a parallel three-layer shape:
app/protected/<module> (routes) + components/<module> (UI) + lib/<module> (domain logic).
| Module | Route | What it does | |
|---|---|---|---|
| ๐ | Dashboard | /protected/dashboard |
Read-only analytics: collected, pending, %, class-wise, top defaulters |
| ๐จโ๐ | Students | /protected/students |
Student master + per-student fee exceptions & discounts |
| ๐ | Fee Setup | /protected/fee-setup |
Yearly policy: tuition, transport, due dates, late fee, fee heads |
| ๐ต | Payment Desk | /protected/payments |
The only place payments are posted; prints receipts |
| ๐ | Transactions | /protected/transactions |
Read-only receipts, dues, ledger, class register |
| ๐ | Defaulters | /protected/defaulters |
Daily call list with smart heat ranking & follow-up |
| ๐ค | Exports | /protected/exports |
XLSX download center + AI context bundle |
| ๐ ๏ธ | Admin Tools | /protected/admin-tools |
Year transfer, refunds, day-close, settings, staff, audit |
| ๐งพ | Receipts | /protected/receipts |
Lookup & reprint (A4) |
| ๐ณ | Finance Controls | /protected/finance-controls |
Auto day-close view, refunds, correction review |
| ๐๏ธ | Master Data | /protected/master-data |
Sessions, classes, routes, fee heads, payment modes |
| ๐ฎ | Staff | /protected/staff |
Accounts & RBAC |
| ๐ฅ | Imports | /protected/imports |
Staged student import (upload โ map โ dry-run โ commit) |
Five roles (lib/auth/roles.ts). Navigation visibility and landing routes are permission-driven.
| Role | ๐ Lands on | Can do |
|---|---|---|
๐ admin |
Dashboard | Everything |
๐งฎ accountant |
Payment Desk | Collect, refunds, finance controls, reports |
๐ teacher |
Students | Students, dues, defaulters (read payments) |
๐ fee_collector |
Defaulters | Follow-up + student reads |
๐ view_only |
Dashboard | Read-only |
Legacy aliases still resolve:
read_only_staff โ view_only,defaulter_followup โ fee_collector.
Money is append-only. payments and receipts are never updated or deleted โ Postgres triggers physically block it. Corrections happen as new payment_adjustments rows with an audit trail. Refunds post a reversal adjustment.
flowchart TD
F["Annual fee a student owes"]
F -->|"1๏ธโฃ tuition discount\n(RTE / staff child / 3rd child)"| F2["Lower base due"]
F2 -->|"collect cash"| P["Total paid ๐ต"]
F2 -->|"2๏ธโฃ discount close-out\n(write-off, mode='discount')"| W["Pending cleared\nโNOT counted as paid"]
LF["Late fee (โน1,000 flat)"] -->|"3๏ธโฃ late-fee waiver"| LF2["Reduced/zero late fee"]
style P fill:#bbf7d0,stroke:#16a34a,color:#000
style W fill:#fecaca,stroke:#dc2626,color:#000
| # | Reduction | Affects | Counted as paid? |
|---|---|---|---|
| 1๏ธโฃ | Tuition discount | lowers the base fee | โ no |
| 2๏ธโฃ | Discount close-out (write-off) | clears pending balance | โ no |
| 3๏ธโฃ | Late-fee waiver | waives late fee only | โ no |
The "paid" formula (per installment, in the projection view):
applied_amount = max( real_cash_payments + payment_adjustments , 0 )
total_paid = ฮฃ applied_amount # excludes discount-mode receipts & tuition discounts
So Total due โ Total paid is not always Outstanding when a write-off exists โ always trust the Outstanding column, which nets everything. โ
One click in Admin Tools โ Transfer to Next Session rolls the school forward.
sequenceDiagram
actor Admin
participant T as Transfer flow
participant DB as Supabase
Admin->>T: From 2026-27 โ To 2027-28
T->>DB: Create session + copy classes, fee policy,<br/>discount policies, fee settings
T->>DB: Build promotion preview (Class N โ N+1)
Admin->>T: Review & Apply โ
T->>DB: Promote students, carry credit forward
Note over Admin,DB: ๐งฏ Roll back anytime ยท ๐๏ธ delete a<br/>โค30-day, zero-payment session by mistake
- ๐ Copies classes, fee policy, conventional discount policies, and per-class fee settings โ all editable afterwards.
- ๐ Transport routes are global, shared across years automatically.
- ๐๏ธ A session created by mistake can be hard-deleted within 30 days only if it has zero posted payments/receipts (immutability-safe).
No manual buttons. The system keeps itself in sync and closed.
| โฐ Job | Schedule | What it does |
|---|---|---|
| ๐ Nightly backup | 0 18 * * * UTC (23:30 IST) |
CSV dump of core tables โ Supabase Storage |
| ๐ Auto day-close | 30 18 * * * UTC (00:00 IST) |
Snapshots yesterday's collections โ collection_closures (read-only, no approval) |
| โป๏ธ Mat-view refresh | */2 * * * * (pg_cron) |
Backstop that re-refreshes financial views if a write-time refresh was skipped |
Day close is fully automatic โ staff never tap a button. Finance Controls shows it read-only.
Every export streams all rows (no page caps) as XLSX. ๐งพ
- ๐จโ๐ All students ยท ๐ Class-wise dues ยท ๐ Defaulters ยท ๐งพ Receipt register ยท ๐ Conventional-discount students
- ๐ค AI context bundle โ a single workbook designed to feed an LLM:
_README(data dictionary) ยท Students (all statuses) ยท Installments ยท Payments ยท Adjustments ยท Refunds ยท Classes ยท Routes ยท Discounts ยท Defaulters ยท Recovery Follow-Up ยท Previous Year Dues ยท Left Student Recovery ยท Sessions. Every sheet joins on SR no.
Engine: workbook_v1 (lib/fees/, lib/workbook/).
| Object | Role |
|---|---|
v_workbook_student_financials |
Per-student projection (materialized) |
v_workbook_installment_balances |
Installment-level balances (materialized) |
v_student_financial_state |
Pending vs credit/refund projection |
preview_workbook_payment_allocation |
Date-aware live preview RPC |
post_student_payment |
Posting RPC (idempotency + advisory locks) |
process_refund_with_adjustment |
Posts reversal adjustments for a refund |
delete_academic_session_safe |
Guarded โค30-day, zero-payment session delete |
๐ Canonical fee rules: docs/product/school-rules.md + lib/config/fee-rules.ts.
git clone https://github.com/veerpatta/schoolfees.git
cd schoolfees
npm install
cp .env.example .env.local # fill in Supabase keys
npm run dev # โ http://localhost:3000npm run dev # ๐ฅ dev server
npm run check # โ
lint + typecheck
npm run test # ๐งช vitest (777 tests)
npm run build # ๐ฆ production build
npx vitest run tests/integration/payment-desk-workflow.test.ts # single fileCopy .env.example โ .env.local.
| Variable | Required | Notes |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
โ | Supabase project URL |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY |
โ | Anon / publishable key |
SUPABASE_SERVICE_ROLE_KEY |
โ | Server-only โ never in browser/NEXT_PUBLIC_* |
NEXT_PUBLIC_SITE_URL |
โ | Prod domain (http://localhost:3000 locally) |
NEXT_PUBLIC_SCHOOL_NAME |
โ | Shri Veer Patta Senior Secondary School |
NEXT_PUBLIC_APP_MODE |
โ | internal-admin |
CRON_SECRET |
โ (prod) | Authorizes Vercel cron routes |
BOOTSTRAP_*_PASSWORD |
โ๏ธ | One-time staff seeding only |
Project: vgqyilgstjvgohrsiwkb ยท Supabase Mumbai (ap-south-1).
supabase migration new <short_snake_case_name>
# edit supabase/migrations/<timestamp>_<name>.sql
supabase db push- Canonical schema:
supabase/schema.sqlยท ordered history:supabase/migrations/. โ ๏ธ Applying via the Supabase MCP renames the version from wall-clock time โ rename the local file to match (seeCLAUDE.md).
Read-only health checks (need Supabase env vars; see scripts/README.md for the full list):
node scripts/verify-phase1-migrations.mjs # phase-1 session-sync migration/readiness check
node scripts/verify-required-sessions.mjs # confirms required academic sessions exist
node scripts/verify-live-fee-health.mjs # production fee-health verification
node scripts/verify-live-sync-health.mjs # system sync verificationRun before every PR (AGENTS.md order): typecheck โ lint โ test โ build.
โ tsc --noEmit clean
โ eslint . 0 errors / 0 warnings
โ vitest run 777 / 777 passing
โ next build green
Test layout: tests/unit (pure/domain) ยท tests/integration (workflows) ยท tests/ui (routes/components).
- ๐ซ Never edit/delete posted
paymentsorreceiptsโ correct viapayment_adjustments. - ๐
SUPABASE_SERVICE_ROLE_KEYstays server-only. - ๐ Public signup stays disabled after bootstrap.
- ๐ต No payment-posting path outside the Payment Desk.
- ๐ฅ
2026-27is live production data โ useTEST-2026-27for all testing; never post test payments against real students. - ๐ Fee Setup publish must preview impact and protect paid/partial/adjusted rows.
| Path | What's inside |
|---|---|
docs/product/ |
Project context, MVP scope, school rules, roadmap |
docs/modules/ |
Per-module guides (import, payment-desk, exports, โฆ) |
docs/maps/ |
Folder map, database map, module map, danger zones |
docs/workflows/ |
Test-data setup, production operations |
CLAUDE.md / AGENTS.md |
Contributor + agent guide |
PRODUCTION_OPERATIONS_CHECKLIST.md ยท UAT_CHECKLIST.md |
Go-live & UAT |
Internal software for Shri Veer Patta Senior Secondary School. Not for public distribution.
Built with โค๏ธ for the fee office ยท maintained with append-only discipline ๐