reconcile() previously split a multi-month payment evenly across months, which falsely flagged months as underpaid when their expected fees differed (e.g. 1250 CZK for 02+03+04 2026 with rates 750/350/150 was shown as 416/month with two months red). The allocation now runs per matched member: greedy when the share covers the total expected (each month gets its expected fee, surplus -> credit), proportional by expected fee otherwise. Out-of-window months keep the previous even-split-to-credit behavior. 6 new test cases. Also adds CHANGELOG.md and a changelog convention in CLAUDE.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5.3 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Status
Flask-based financial management system for FUJ (Frisbee Ultimate Jablonec). Handles attendance-based fee calculation, Fio bank transaction sync, payment reconciliation, and a web dashboard.
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
scripts/config.py. Domain-specific constants (fees, merged months) stay in their respective modules.
Development Setup
This project uses uv for dependency management.
uv venv && uv sync
source .venv/bin/activate
Set PYTHONPATH=scripts:. when running scripts directly (the Makefile does this automatically).
Commands
make web # Start dashboard at http://localhost:5001
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 Docker image
Run a single test:
PYTHONPATH=scripts:. python -m unittest tests.test_app.TestWebApp.test_adults_route
Architecture
Data flow
Google Sheets (attendance) ──► attendance.py ──► reconcile() ──► Flask routes ──► templates/
Google Sheets (payments) ──► match_payments.py ──┘
Fio Bank API ──► sync_fio_to_sheets.py ──► Google Sheets (payments)
Key modules
app.py— Flask app; routes for/adults,/juniors,/payments,/sync-bank,/qr,/flush-cachescripts/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.scripts/match_payments.py—reconcile()matches transactions to members/months.fetch_sheet_data()reads the payments sheet.fetch_exceptions()reads theexceptionstab.scripts/cache_utils.py— Invalidation via Google Drive APImodifiedTime; falls back to 5-minute TTL buckets when Drive API is unavailable. Cache files live intmp/.scripts/sync_fio_to_sheets.py— Pulls Fio bank transactions and appends them to the payments Google Sheet.scripts/infer_payments.py— Fills in Person/Purpose/Inferred Amount columns using name-matching heuristics.scripts/config.py— All external IDs, paths, and tunable TTLs. Override via env vars (CREDENTIALS_PATH,BANK_ACCOUNT,CACHE_TTL_SECONDS).
Member tiers
Tiers are set in column B of the attendance sheet:
A— Adult, pays fees (750 CZK/month for 2+ sessions, 200 CZK for exactly 1)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, 2+ sessions → monthly rate (default 750 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
ADULT_MERGED_MONTHS / JUNIOR_MERGED_MONTHS in 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
get_cached_data() in app.py checks the Drive API modifiedTime before each request and serves a JSON file from tmp/ when the sheet hasn't changed. Cache is warmed up at startup (warmup_cache()). Flush via /flush-cache (POST) or flush_cache().
Payments sheet columns
Date | Amount | manual fix | Person | Purpose | Inferred Amount | Sender | VS | Message | Bank ID | Sync ID
Person and Purpose are written by 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).
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.