Files
fuj-management/CLAUDE.md
kacerrandClaude Opus 5.5 62c6dc2ca0 fix(sync): dashboard sync covers current year plus Nov-Dec of previous
/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>
2026-10-08 12:04:59 +02:00

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:

  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.

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 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:

    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:

## 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.