# Plan: Full Go rewrite of the Python/Flask backend ## Context The current Flask app ([app.py](app.py) + [scripts/](scripts/), ~2400 LOC of Python) handles attendance-based fee calculation, Fio bank sync, payment reconciliation, and a server-rendered dashboard. The user wants a full rewrite in Go with two goals: 1. **Quality Go code** as the primary outcome — idiomatic stdlib-first design, strong typing, proper layering. The Python codebase grew organically and mixes domain logic, IO, and HTTP concerns. 2. **Feature-parity certainty** — no behavioural drift between the Python and Go versions on anything that touches money. Reconciliation is real money; silent divergence is unacceptable. **Switchable runtime**: both backends run on different TCP ports, started independently via Makefile targets (`make web-py` on :5001, `make web-go` on :8080). The user opens whichever they want in a browser. No reverse proxy, no traffic-splitting, no shared frontend constraint — just two services that read the same Google Sheets and the same `tmp/` cache. **Frontends are allowed to diverge.** The Go web layer is designed cleanly in its own right rather than as a byte-compatible Jinja port. Both backends expose a JSON API (`/api/...`) with an identical contract — that's what parity testing locks down. Rendered HTML and inline JS can be different. ## Versioning policy - **Go**: latest stable release at project start. Pin in `go.mod` via the `go` directive (e.g. `go 1.X`) and use the matching `golang:1.X` builder image. Bump on each new minor as it lands stable. - **Go libraries**: latest stable for every dependency in `go.mod`; run `go get -u ./... && go mod tidy` at the start and quarterly thereafter. - **Python deps** (during the parallel-run period): keep [pyproject.toml](pyproject.toml) on its current versions to avoid destabilizing the parity baseline; bump only after Python retires. - **Base images**: `golang:latest-stable` builder → `gcr.io/distroless/static:latest` runtime, both pinned by digest in CI for reproducibility. - **CI runners**: latest stable Linux image on Gitea Actions. The plan does not hardcode specific version numbers below — implementation picks current-stable at the time M1 starts. ## Approach summary - **Three-layer Go architecture**: pure domain (no IO) → IO clients (behind interfaces, easily faked) → HTTP/services (composition). - **Capture-then-port**: dump current Python outputs as JSON fixtures, port Go function-by-function, assert byte-equality with `cmp.Diff`. - **JSON contract is the spec, not the templates.** Each Python route gets an `/api/X` shadow that returns the dict already passed to the template. Go defines typed structs matching that shape; both sides validate against generated JSON Schema. - **Money is integer CZK**: existing fees are integer CZK (750/200/500); keep it that way to avoid float drift in reconcile allocation. Where Sheets returns floats, parse and round at the boundary. - **Frontend rewrite, not port**: Go uses `html/template` with cleanly organized templates and JS extracted into static files served via `embed.FS`. Same UX (filterable table, member-detail modal, QR launcher) but designed natively, no Jinja-port baggage. ## Go project layout `go/` lives at the repo root alongside `scripts/` and `templates/` so both backends share the same git history during migration. ``` go/ cmd/ fuj/main.go # single binary, subcommands: server | fees | sync | infer | reconcile parity/main.go # diff tool: hits both backends' /api/X, prints JSON diff internal/ domain/ # pure, no IO, no net/* czech/ # normalize, parse_month_references fees/ # calculate_fee, calculate_junior_fee, "?" sentinel type money/ # parse_czk_amount, format helpers reconcile/ # reconcile() + Ledger, MemberResult types matching/ # _build_name_variants, match_members, infer_transaction_details synch/ # generate_sync_id (pure hash) io/ # IO behind interfaces, all impls have an in-memory fake sheets/ # SheetsClient + Google impl + fake drive/ # DriveClient for modifiedTime fio/ # FioClient: API JSON impl + transparent-page HTML scraper cache/ # FileCache with modifiedTime gating + two TTL knobs services/ # composition layer; pure + IO, no HTTP attendance/ # GetMembersWithFees, GetJuniorMembersWithFees payments/ # FetchTransactions, FetchExceptions, BuildView banksync/ # SyncToSheets, InferPayments (write ops) web/ handlers/ # one file per route family view/ # HTML view-model structs (per route) api/ # JSON view-model structs (the parity-locked contract) templates/ # *.tmpl, embed.FS — designed natively, not a Jinja port static/ # js/*.js, css/*.css served via embed.FS middleware/ # request timer, recovery, slog config/ # mirrors scripts/config.py (env loading) qr/ # SPD string builder + PNG via go-qrcode tests/ fixtures/ # JSON fixtures captured from Python (PII-scrubbed) parity/ # Go-side characterization tests (replay fixtures) build/Dockerfile # multi-stage: latest-stable golang builder → distroless static go.mod ``` ## Library choices All on latest stable as per the versioning policy above. | Concern | Pick | Rationale | |---|---|---| | HTTP routing | `net/http` ServeMux | 8 static routes; no need for chi/gin given modern stdlib pattern matching | | Templates | `html/template` | Auto-escaping; native Go feel | | Static assets | `embed.FS` | Single binary, no loose files | | Sheets/Drive | `google.golang.org/api/{sheets/v4,drive/v3}` + `option` | Official client; service-account auth via `option.WithCredentialsFile` | | OAuth | `golang.org/x/oauth2/google` (token only; drop installed-app flow + pickle) | Production already uses service accounts | | QR PNG | `github.com/skip2/go-qrcode` | Mature, byte-stable PNG output | | NFKD | `golang.org/x/text/unicode/norm` + `unicode.IsMark` | Direct equivalent of `unicodedata.normalize("NFKD", ...)` | | HTML scrape | `golang.org/x/net/html` token visitor | Counts `