Oracle3
Open-source trading engine and MCP server for prediction markets. Fee-aware no-arbitrage trading across related contracts, live on Kalshi, Polymarket and Solana or on paper.
School fee management for a Ghanaian primary school: fees, installments, cash and card payments, with three enforced roles.
FinancePython · TypeScriptGitHub @Rancho-1001OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
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.
Taken from the repository README
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.
Totals are computed without the fan-out. Joining students to assignments to payments produces one row per payment, so a 250.00 bill paid in three installments counts as 750.00. The naive join agrees with a hand-check for everyone who paid in one go and is wrong only for installments — the feature the product exists for. Payments are collapsed to one row per assignment first. Verified by substituting the naive join: a class of three read 600.00 instead of 300.00.
The login rate limiter counts rows in audit_log. No new infrastructure, and the count is shared across processes — an in-memory counter hands an attacker one full allowance per worker.
There are no DELETE endpoints. Classes archive, students go inactive, fee types cannot be removed. A public demo cannot be wiped, structurally rather than carefully.
CORS refuses to start on a wildcard. allow_credentials is on, and browsers reject wildcard-with-credentials anyway — so a * would make every cross-origin request fail in a way that looks like a frontend bug. Failing at startup names the cause.
| Capability | Admin | Bursar | Parent |
|---|---|---|---|
| Manage students and classes | ✅ | ❌ | ❌ |
| Define fee types; charge fees | ✅ | ❌ | ❌ |
| Record cash payments | ✅ | ✅ | ❌ |
| View all payments and reports | ✅ | ✅ | ❌ |
| View own children's fees and history | — | — | ✅ |
| Pay online | — | — | ✅ |
Every deny case has a test that fails if you delete the guard.
571 backend tests, 91 frontend. Coverage across the money and permission code — the balance rules, the payment service, the ledger queries, both gateways, the webhook, the role guards — is 99% (467 statements, 6 missed); 89% overall.
cd backend && pytest # everything, against real Postgres
cd backend && pytest tests/unit -q # the money rules alone, no database
cd frontend && npm test
Three conventions carry the suite:
NUMERIC semantics nor SELECT … FOR UPDATE — the two things the financial tests exist to check.The webhook tests sign their payloads with the real HMAC schemes — SHA-256 with a timestamp for Stripe, SHA-512 without one for Paystack — rather than patching verification out, so the tampered-body and stale-timestamp cases are actually exercised.
Prerequisites: Python 3.14, Node 20+, PostgreSQL 16+. uv installs Python 3.14 without touching the system one.
cd backend
uv venv --python 3.14 && source .venv/bin/activate
uv pip install -r requirements-dev.txt
cp .env.example .env # DATABASE_URL, JWT_SECRET, Stripe test keys
alembic upgrade head && python -m app.db.seed
uvicorn app.main:app --reload
cd frontend
npm install && cp .env.example .env
npm run dev
API docs at http://localhost:8000/docs. For a real card payment locally, stripe listen --forward-to localhost:8000/webhooks/stripe and put the secret it prints in .env.
Deploying it — Vercel, Render, Supabase, and the two things that went wrong the first time — is in docs/deploy.md.
backend/app/
├── api/routes/ # HTTP endpoints, thin; every guard is a dependency
├── api/deps.py # get_current_user, require_role, get_own_student
├── core/ # settings (CORS refuses wildcards), JWT, bcrypt
├── db/ # session, migrations, the seed
├── models/ # seven tables; money is NUMERIC(12,2)
├── schemas/ # Pydantic in and out; Money serialises as a string
└── services/
├── balances.py # the money rules, pure, no database
├── payments.py # the locked write path
├── ledger.py # aggregations without the fan-out
├── gateways/ # one interface; stripe.py and paystack.py behind it
└── rate_limit.py # failed logins, counted from the audit log
frontend/src/
├── lib/money.ts # the one door for formatting an amount
├── lib/api.ts # token attachment; the two kinds of 401
├── auth/ # context and guards — what is shown, never what is allowed
└── pages/ # admin, staff, parent
Payment gateway. Both markets are wired: PAYMENT_GATEWAY=stripe for the US and Canada, PAYMENT_GATEWAY=paystack for Ghana, where it offers mobile money — MTN MoMo, Telecel Cash — which is how most school fees there are actually paid. Going live on either needs a registered business; the code does not care which. The demo runs on Stripe because Stripe's test mode needs no paperwork.
Currency. Defaults to USD on Stripe and GHS on Paystack; PAYMENT_CURRENCY overrides (a Canadian school sets cad). The frontend's VITE_CURRENCY formats amounts and has to agree with it — the one setting a gateway switch touches on the frontend.
Hosting. Free Render instances sleep, and free Render Postgres expires after 30 days — which is why the database is Supabase. Both are in the deploy notes.
The commit history is written to be read; each message explains the decision, not just the change. Phases 0–8 are checked off with their judgement calls in docs/roadmap.md, which also lays out Part II — the phases between a demo and a product a school could run on.
MIT — see LICENSE.