- Default adult monthly rate 700 -> 600 CZK - 2026-07 and 2026-08 set to 0; 2026-09 through 2026-12 added at 600 - Single-session fee is capped at the month's rate, so a 0-rate month is free for one session as well - Mirrored in Go and legacy Python; new calculate_fee parity fixtures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
222 lines
12 KiB
Markdown
222 lines
12 KiB
Markdown
# 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**:
|
|
|
|
1. Implement and test in Go first (`go/`).
|
|
2. Mirror the change in the legacy Python code (`legacy/`), including its tests.
|
|
3. 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.
|
|
4. If a pure function's behaviour changes, add or update seeds in `legacy/scripts/_fixture_seeds.py`, run `make capture-fixtures`, and make sure `make go-test-all` passes. With both servers running, `make parity` diffs 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 in `legacy/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`.
|
|
|
|
```bash
|
|
make go-build # builds bin/fuj
|
|
```
|
|
|
|
Python (legacy) uses `uv`; the project file and virtualenv live in `legacy/`.
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
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-2026 # Sync Fio bank transactions for 2026 to Google Sheets
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
```bash
|
|
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 binary `fuj`, 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; subcommands `server`, `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) and `banksync` (Fio sync + inference).
|
|
- `go/internal/web` — routes for `/adults`, `/juniors`, `/payments`, `/sync-bank`, `/flush-cache`, `/qr`, `/version`, `/api/*`; `templates/` and `static/` 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`) and `ADULT_MERGED_MONTHS` / `JUNIOR_MERGED_MONTHS` dicts.
|
|
- `legacy/scripts/match_payments.py` — `reconcile()` matches transactions to members/months. `fetch_sheet_data()` reads the payments sheet. `fetch_exceptions()` reads the `exceptions` tab.
|
|
- `legacy/scripts/cache_utils.py` — Invalidation via Google Drive API `modifiedTime`; falls back to 5-minute TTL buckets when Drive API is unavailable. Cache files live in `legacy/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 from `AdultFeeMonthlyRate` / `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 sheet
|
|
- `X` — 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 `exceptions` tab 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:
|
|
|
|
1. **Create a branch off `main`** before 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
|
|
2. **Commit on the branch** following the existing commit conventions
|
|
(Co-Authored-By trailer, etc.).
|
|
3. **Push the branch** to `origin` with `-u` so it tracks.
|
|
4. **Open the MR with `tea`** rather than printing a compare URL:
|
|
|
|
```bash
|
|
tea pr create \
|
|
--title "<short title>" \
|
|
--description "<body>" \
|
|
--base main \
|
|
--head <branch>
|
|
```
|
|
|
|
`tea` is already authenticated against the Gitea instance; just run it.
|
|
Print the resulting PR URL for the user. If `tea` is 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.
|
|
5. **Do not merge or delete the branch** from the CLI — neither via `tea`,
|
|
`gh`, nor `git 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:
|
|
|
|
```markdown
|
|
## 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.
|