/sync-bank now uses one shared default window in both implementations: 1 November of the previous year to 31 December of the current year. Payments dated around New Year are picked up in January; duplicates are still filtered by Sync ID. Python used a hardcoded 2026 window and would have stopped syncing on 2027-01-01; Go used the current calendar year only. Adds make sync-year [YEAR=N] with the same window. The CLI default without flags (last 30 days) is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Status
Financial management system for FUJ (Frisbee Ultimate Jablonec). Handles attendance-based fee calculation, Fio bank transaction sync, payment reconciliation, and a web dashboard.
- Go is the primary implementation (
go/). It is the version that is built, deployed and developed first. - Python is legacy (
legacy/). The project was originally developed in Python (Flask); that version is kept runnable and in sync, but is considered legacy from now on.
Dual implementation rule
Until the Python version is retired, every new feature and every behaviour change must be implemented in both Go and Python:
- Implement and test in Go first (
go/). - Mirror the change in the legacy Python code (
legacy/), including its tests. - Keep the two in parity: shared constants (fee rates, merged months, bank accounts, sheet IDs) must match on both sides, and the JSON API contract (
/api/adults,/api/juniors,/api/payments) must stay identical. The HTML frontends are allowed to diverge. - If a pure function's behaviour changes, add or update seeds in
legacy/scripts/_fixture_seeds.py, runmake capture-fixtures, and make suremake go-test-allpasses. With both servers running,make paritydiffs the live JSON APIs.
Go-only or Python-only changes are fine for things that exist on one side only (build, tooling, HTML/CSS/JS, refactors with no behaviour change).
Key Constraints
- PII separation: Member data (names, emails, payment info) must never be committed to git. Enforce config/data separation from day one.
- Configuration: External service IDs, credentials, and tunable parameters are centralized in
go/internal/config/config.go(mirrored inlegacy/scripts/config.py). Domain-specific constants (fees, merged months) stay in their respective modules.
Development Setup
Go (primary): a recent Go toolchain (see go/go.mod) and golangci-lint.
make go-build # builds bin/fuj
Python (legacy) uses uv; the project file and virtualenv live in legacy/.
make venv # uv sync --project legacy → legacy/.venv
The Makefile sets PYTHONPATH (legacy/scripts:legacy) and CREDENTIALS_PATH (.secret/fuj-management-bot-credentials.json at the repo root) for the Python targets. Set both yourself when running Python scripts directly.
Commands
Go (primary):
make web-go # Build and start dashboard at http://localhost:8080
make go-build # Build bin/fuj
make go-test # Unit tests (go test -race ./...)
make go-parity # Parity tests against the captured Python fixtures
make go-test-all # Both of the above
make go-lint # golangci-lint
make go-run ARGS="fees" # Run a subcommand: server, fees, reconcile, sync, infer, version
make go-sync-debug [DAYS=N] # Dry-run bank sync with debug logs
Python (legacy):
make web-py # Start dashboard at http://localhost:5001 (make web is an alias)
make web-debug # Same with FLASK_DEBUG=1
make test # Run all tests (unittest discover)
make test-v # Tests with verbose output
make fees # Print fee report from attendance sheet
make sync-year # Sync Fio transactions for the current year plus Nov-Dec of the previous one (YEAR=N to override)
make infer # Auto-fill Person/Purpose/Amount columns in payments sheet
make reconcile # Print balance report from Google Sheets data
make image # Build the legacy Python Docker image
Cross-implementation:
make capture-fixtures # Regenerate go/tests/fixtures from the Python implementation
make parity # Diff /api/* between web-py (:5001) and web-go (:8080); both must be running
Run a single test:
cd go && go test ./internal/domain/reconcile -run TestName
cd legacy && PYTHONPATH=scripts:. .venv/bin/python -m unittest tests.test_app.TestWebApp.test_adults_route
Architecture
Data flow
Google Sheets (attendance) ──► attendance loader ──► reconcile ──► HTTP handlers ──► templates
Google Sheets (payments) ──► payments loader ──┘
Fio Bank API ──► bank sync ──► Google Sheets (payments)
Repository layout
go/— primary implementation (single binaryfuj, templates and static assets embedded).legacy/— legacy Python implementation (app.py,scripts/,templates/,tests/,pyproject.toml,build/Dockerfile).docs/— documentation;docs/plans/holds plans and task lists..gitea/workflows/— CI: image builds on tag push, gitops image-update PR.
Key modules — Go (primary)
go/cmd/fuj— CLI entry point; subcommandsserver,fees,reconcile,sync,infer,version.go/cmd/parity— diffs the JSON API of the Go and Python servers.go/internal/config— sheet IDs, bank accounts, env-driven settings (CREDENTIALS_PATH,CACHE_DIR,CACHE_TTL_SECONDS,CACHE_API_CHECK_TTL_SECONDS,MONTHS_TO_SHOW,SERVER_ADDR,LOG_LEVEL,FIO_API_TOKEN_NEW/FIO_API_TOKEN_OLD).go/internal/domain— pure logic:fees(rates,CalculateFee,CalculateJuniorFee),reconcile,matching(name matching, inference),czech(normalize, month parsing),money,synch(sync IDs).go/internal/io— external systems:attendance(CSV export),sheets,drive,fio(API + transparent-page scraper),cache(file cache). Each has a fake for tests.go/internal/services—membership(loading, merged months, fee and reconcile reports) andbanksync(Fio sync + inference).go/internal/web— routes for/adults,/juniors,/payments,/sync-bank,/flush-cache,/qr,/version,/api/*;templates/andstatic/are embedded.go/tests— parity tests and the scrubbed fixture corpus captured from Python.
Key modules — Python (legacy)
legacy/app.py— Flask app; routes for/adults,/juniors,/payments,/sync-bank,/qr,/flush-cache,/api/*legacy/scripts/attendance.py— Fetches attendance CSV from Google Sheets, computes per-member per-month fees. Contains fee rate constants (ADULT_FEE_DEFAULT,JUNIOR_FEE_DEFAULT) andADULT_MERGED_MONTHS/JUNIOR_MERGED_MONTHSdicts.legacy/scripts/match_payments.py—reconcile()matches transactions to members/months.fetch_sheet_data()reads the payments sheet.fetch_exceptions()reads theexceptionstab.legacy/scripts/cache_utils.py— Invalidation via Google Drive APImodifiedTime; falls back to 5-minute TTL buckets when Drive API is unavailable. Cache files live inlegacy/tmp/.legacy/scripts/sync_fio_to_sheets.py— Pulls Fio bank transactions and appends them to the payments Google Sheet.legacy/scripts/infer_payments.py— Fills in Person/Purpose/Inferred Amount columns using name-matching heuristics.legacy/scripts/views.py— View models shared by the HTML routes and the JSON API.legacy/scripts/config.py— All external IDs, paths, and tunable TTLs. Override via env vars (CREDENTIALS_PATH,CACHE_TTL_SECONDS,MONTHS_TO_SHOW).legacy/scripts/capture_fixtures.py,scrub_fixtures.py,_fixture_seeds.py— produce the scrubbed parity fixtures for Go.
Member tiers
Tiers are set in column B of the attendance sheet:
A— Adult, pays fees (per-month rate fromAdultFeeMonthlyRate/ADULT_FEE_MONTHLY_RATE, fallback 600 CZK for 2+ sessions; 200 CZK for exactly 1, capped at the month's rate)J— Junior attending adult practices; their attendance is merged with the junior sheetX— Excluded from junior fee calculation (coaches, etc.)
Fee calculation
- Adults: 0 sessions → 0, 1 session → 200 CZK (never more than the month's rate, so a 0-rate month is free), 2+ sessions → monthly rate (default 600 CZK)
- Juniors: 0 → 0, 1 →
"?"(manual review required), 2+ → monthly rate (default 500 CZK) - Per-member per-month overrides live in the
exceptionstab of the payments sheet (columns: Name, Period YYYY-MM, Amount, Note). Exceptions are keyed by(normalize(name), normalize(period)).
Merged months
AdultMergedMonths / JuniorMergedMonths in go/internal/services/membership/sources.go (ADULT_MERGED_MONTHS / JUNIOR_MERGED_MONTHS in legacy/scripts/attendance.py) map a source month to a target month (e.g., "2025-12": "2026-01" merges December into January billing). The target month accumulates attendance from both months.
Caching
Both implementations check the Drive API modifiedTime and serve a cached JSON file when the sheet hasn't changed, falling back to 5-minute TTL buckets when the Drive API is unavailable. Go: go/internal/io/cache, files in tmp/go/ (CACHE_DIR). Python: get_cached_data() in legacy/app.py, files in legacy/tmp/, warmed up at startup (warmup_cache()). Flush via /flush-cache (POST).
Payments sheet columns
Date | Amount | manual fix | Person | Purpose | Inferred Amount | Sender | VS | Message | Bank ID | Sync ID
Person and Purpose are written by the inference step (fuj infer / infer_payments.py) and can be manually corrected. manual fix column presence disables re-inference for that row. Multiple people or months are comma-separated in Person/Purpose.
QR codes
/qr?account=…&amount=…&message=… generates a Czech QR Platba PNG (SPD format).
Branching & merge requests
The remote is Gitea (gitea.home.hrajfrisbee.cz/kacerr/fuj-management).
For features, do not commit to main directly. Use a branch + merge
request flow:
-
Create a branch off
mainbefore starting work:feat/<slug>for features (e.g.feat/qr-code-overlay)fix/<slug>for bug-fix branches the user explicitly asks for<slug>is short kebab-case
-
Commit on the branch following the existing commit conventions (Co-Authored-By trailer, etc.).
-
Push the branch to
originwith-uso it tracks. -
Open the MR with
tearather than printing a compare URL:tea pr create \ --title "<short title>" \ --description "<body>" \ --base main \ --head <branch>teais already authenticated against the Gitea instance; just run it. Print the resulting PR URL for the user. Ifteais unavailable for some reason, fall back to printing the compare URL (https://gitea.home.hrajfrisbee.cz/kacerr/fuj-management/compare/main...<branch>) and let the user open the MR manually. -
Do not merge or delete the branch from the CLI — neither via
tea,gh, norgit push --delete. The user does that in Gitea.
Exceptions — when committing straight to main is fine:
- Small bug fixes / hotfixes the user describes as such.
- Typo / comment / formatting tweaks.
- Edits the user explicitly says to push to
main.
When uncertain whether something is a feature or a small fix, ask before committing.
Git Commits
When making git commits, always append yourself as co-author trailer to the end of the commit message to indicate AI assistance
Changelog
Maintain a running changelog in CHANGELOG.md at the repo root. After every significant change, fix, or update — once the user confirms it works — append a new entry at the top of the file in this format:
## YYYY-MM-DD HH:MM TZ — short title
- One-line summary of what changed and why.
- Key files touched (optional, only if useful for traceability).
Get the timestamp with date "+%Y-%m-%d %H:%M %Z". Skip trivial edits (typos, formatting, comment tweaks); only log changes a future reader would care about.
Plans
When Claude Code's plan mode is used, save the plan file inside the repo at
docs/plans/YYYY-MM-DD-HHMM-<slug>.md instead of the default ~/.claude/plans/
location. Get the timestamp with date "+%Y-%m-%d-%H%M" (matches the changelog
convention). The <slug> should be a short kebab-case summary of the plan's topic.
Create the docs/plans/ directory on first use. Plan files are committed to the
repo so other contributors can review historical decisions.