Skip to content

Latest commit

 

History

History
151 lines (114 loc) · 6.55 KB

File metadata and controls

151 lines (114 loc) · 6.55 KB

TipSplit — Dev Guide

English · Português (PT-PT) · README

For whoever touches the code next (probably you, six months from now, at 1am).

Layout

File Responsibility
main.py FastAPI app: every route, the owner/staff PIN gate, the security headers, print pages
splitting.py all money math — hours, shares, largest-remainder rounding, the statement text
db.py SQLite access (no ORM), migrations runner, weeks/entries/staff/vales/settings/audit
auth.py PIN hashing (pbkdf2), the signed session cookie (role + staff id + expiry), brute-force brake
exporters.py payslips, cash sheet, week + annual xlsx/csv. Formats only — never decides money
static/index.html the whole UI: one file, vanilla JS, inline CSS, system fonts, PT-PT/EN dictionary (I18N + t(), data-t attributes in the markup)
migrations/*.sql schema steps, applied by PRAGMA user_version
ops/seed_demo.py the fictional venue (demos, screenshots)
tests/, e2e/smoke.mjs unit suite + browser journey

Rule of the house: money math lives in splitting.py and nowhere else. The browser renders what the server computed; exporters.py only formats it. If you ever need a number in two places, send it from the server.

Data model

weeks(id, start_date UNIQUE, status open|locked, closed_at, closed_by)
week_pools(week_id, pool_eur)
entries(week_id, staff_id, mon..sun)
staff(id, name, position, archived, pin_hash)          -- pin_hash '' = no PIN issued
vales(id, staff_id, date, week_id, amount, note)       -- week_id is always set
settings(key, value)                                   -- venue_name, lang, vale_max
audit_log(id, ts, actor, action, detail)

Migrations: add migrations/00N_name.sql, and bump LATEST in tests/test_migrations.py. The base schema is deliberately the old shape so 001 exercises on every fresh install.

Running it

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python -m uvicorn main:app --reload --port 8778

TIPSPLIT_DB overrides the database path (used by tests, e2e and the demo).

Tests

.venv/bin/python -m pytest tests/ -q      # 76 unit tests (~1 min)
node e2e/smoke.mjs                        # 60 browser assertions against a throwaway DB

The unit suite covers the split math (proportional shares, cent balancing, zero-hour weeks), vales (week required, ceiling, grouping, auto-listing), migrations from a legacy DB, auth (hashing, token tampering, expiry, per-staff isolation) and the printed statement's arithmetic.

The browser suite boots the real app and drives the whole ritual: first-run PIN → new week → hours → advance → ceiling → lock → payslips → exports → unlock with a motivo → staff login → the staff page → 403s on management endpoints → phone layouts.

e2e/smoke.mjs reuses playwright-core from the BarSpec checkout (the PW_CORE env var points at your own install — npm i -D playwright-core — otherwise a sibling ../barspec/node_modules) and a Chromium from ~/.cache/ms-playwright. On the author's machine no install was needed.

Never point tests at the live DB. They copy or override TIPSPLIT_DB.

Deploy (this network)

sudo systemctl restart tipsplit && sleep 15 && systemctl is-active tipsplit

Live on the venue's LAN address (http://<host>:8778), DB at <repo>/tipsplit.db, migrations run on startup. Before a restart that touches schema: copy the DB first.

Backup and restore

cp tipsplit.db "tipsplit-$(date +%F).db"     # backup: the whole app is this file

Restore = stop the service, put the file back, start it. There is no other state.

PIN recovery (forgotten owner PIN)

There is no email reset — no cloud, no accounts. On the machine:

sqlite3 tipsplit.db "DELETE FROM settings WHERE key='pin_hash'"

The gate then asks to define a PIN on the next visit. Staff PINs live in staff.pin_hash; clear one per person from Equipa → limpar, or in SQL.

Adding a feature without breaking trust

  1. Money change? Reproduce the arithmetic in a test first, off a real payslip page if you can. A rounding bug is invisible until payday.
  2. New endpoint? Decide who may reach it: owner, staff, or nobody without a cookie. Add it to the e2e 403 list if it's owner-only.
  3. New field on a week or a person? It probably belongs in the week payload from the server, not in client state.
  4. UI change? Run node e2e/smoke.mjs. Unit tests were green through five real UI bugs during the design port; only the browser caught them.
  5. Anything that writes money records (advances, locks) deserves an audit() call.

Regenerating the screenshots

rm -f /tmp/tipsplit-demo.db
TIPSPLIT_DB=/tmp/tipsplit-demo.db .venv/bin/python ops/seed_demo.py --weeks 8

Then drive the app with a small playwright script and save into docs/screenshots/. The seed is deterministic, so the numbers in the README stay true.

Language (PT-PT / English)

One setting (settings.lang) drives everything: the interface (LANG + applyLang(), applied in boot()), the fairness statement (splitting.statement(..., lang)), the printed payslips and cash sheet (exporters._L(lang)), and the user-facing API messages (main._msg). Defaults to pt everywhere, so a venue that never touches it sees no change.

  • Adding a user-visible string: add the key to both dictionaries and use t("key") (or data-t="key" in the markup). Never hardcode PT in a template — that is how the toggle leaks.
  • Euro formatting stays Portuguese in both languages (33,60 €); the toggle is about words, not locale.
  • The spreadsheet exports stay Portuguese: their row mapping is keyed on HEAD.
  • e2e/smoke.mjs covers the toggle (switching to English must leave no Portuguese in the sidebar or the week view, then switching back must restore it).

Known sharp edges

  • The interface is PT-PT only, by decision. English strings in the UI are bugs — they were found twice (Payslips/Cash sheet, Mon…Sun), so check for them when adding copy.
  • #gate must keep display:none !important when hidden; an id selector beat the class once and left the app dead behind an invisible overlay.
  • Any refresh that re-renders a <select> must preserve the user's choice, and concurrent refreshes are coalesced in loadTeam() — a stale render once re-pointed an advance at the wrong person.
  • /api/* and /print/* must stay Cache-Control: no-store: a cached 401 made the app look broken right after setup.