Open Net Worth
Open Net Worth is an open-source personal AI wealth assistant you run yourself: one ledger for your whole financial life, worked by an AI agent you control.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
School fee management for schools in Ghana — track tuition, feeding and uniform fees, take online payments in installments, and see who owes what. FastAPI + React + Postgres + Stripe.
翻訳待ち
Loading the full README. The saved excerpt is shown below.
Fees, installments, and payments for a primary school — with three roles the server actually enforces, and money that is never a float.
The API is on free hosting that sleeps after fifteen quiet minutes. A scheduled ping keeps it awake most of the time; if you do catch it cold, the first request takes about a minute, and the page says so. Card payments are Stripe test mode — use 4242 4242 4242 4242 with any future date. No real money moves.Read the full README ↗Fees, installments, and payments for a primary school — with three roles the server actually enforces, and money that is never a float.
Live demo: sukuu-pi.vercel.app
| Role | Password | |
|---|---|---|
| Administrator | [email protected] | sukuu-demo |
| Bursar | [email protected] | sukuu-demo |
| Parent | [email protected] | sukuu-demo |
The API is on free hosting that sleeps after fifteen quiet minutes. A scheduled ping keeps it awake most of the time; if you do catch it cold, the first request takes about a minute, and the page says so. Card payments are Stripe test mode — use 4242 4242 4242 4242 with any future date. No real money moves.
I taught in Ghana. Fee collection ran on a paper receipt book and a spreadsheet, and the bursar reconciled the two by hand at the end of each term. Parents paid in installments — a bit at the start of term, more when a harvest came in — and nobody could say with confidence who still owed what. Sukuu is "school" in Twi.
This is the system that office needed, built to demonstrate three things:
Admin sets up the school (classes, students, fee types)
→ charges a fee to one student, or a whole class at once
→ parents see itemised fees and what is still owed
→ payments arrive: cash at the office, or card via Stripe
→ the dashboard shows collected against outstanding, per class
flowchart LR
subgraph Browser
FE[React + Vite<br/>TanStack Query]
end
subgraph Render
API[FastAPI<br/>JWT · role guards · audit]
end
subgraph Supabase
DB[(Postgres<br/>NUMERIC money · row locks)]
end
Stripe[Stripe · US/Canada<br/>or Paystack · Ghana]
FE -- bearer token --> API
API --> DB
FE -- redirect --> Stripe
Stripe -- signed webhook --> API
Stripe -. success URL .-> FE
The dotted line is the one that matters: the browser's return from the processor is a hint, and the only thing that writes a payment is the signed webhook. Which processor is one environment variable on the API; the demo runs on Stripe.
These are the judgement calls. Each has a test that fails if the decision is undone, and most were verified by undoing them on purpose.
Money is never a float — anywhere. NUMERIC(12,2) in Postgres, Decimal in Python, and a string on the wire ("250.00"), because a JSON number becomes an IEEE 754 double the moment a browser parses it. In the frontend, nothing outside one module formats an amount, and comparisons go through integer cents. The one place a float exists is chart geometry, where it never reaches the screen.
Two payments cannot overpay a fee, even at the same instant. The payment service takes SELECT … FOR UPDATE on the fee assignment — the parent row, because the dangerous write is a new payment row, and a row that does not exist yet cannot be locked. Existing payments are read after the lock, never before. The test uses two independent database connections; through one session it passes whether or not the lock exists. I removed the lock and watched it fail.
The ledger is written by the webhook, never by the redirect. Anyone can type the success URL. So the success page does not say "paid" — it says "confirming", and watches the balance move past what it was when checkout began. Replays are no-ops via a unique index on stripe_event_id, proved in production by having Stripe redeliver the same event. An integrity error at that commit is only treated as a duplicate if the constraint is that index; anything else re-raises, because answering "already recorded" to a foreign-key failure would tell Stripe the money was handled and lose it behind a 200.
Money that arrives but cannot be applied is flagged, not refused. A bursar records cash while a parent is on the payment page; the card payment then overpays. A 409 would be a lie — the card is already charged and there is no smaller amount to retry. It is audited as payment.online_needs_refund for a human, and the invariant holds.
The processor is an interface, and the second one proved it. Stripe does not operate in Ghana and Paystack does not operate in Canada, so both sit behind one interface with two methods — open a checkout, verify a webhook — and PAYMENT_GATEWAY picks one. Everything after verification is shared: the idempotency key, the lock, the needs-refund path, the audit row. Adding Paystack touched no route and no test of the ledger; it added a module, a signing helper, and the tests for what differs — HMAC-SHA512 with no timestamp, a transaction reference instead of an event id, a mobile-money channel that the ledger records as mobile money rather than as the company that carried it. A gateway with no secret refuses every delivery with a 503, because an HMAC against an empty key is a signature anyone can produce.
404, not 403, for another family's child. A 403 confirms the record exists and lets a parent walk the IDs to learn the school roll. Role guards live in the API; the UI hiding a button is signposting, not security.
This is the beginning of the README. Read the rest at the original. Read the full README ↗