fix: Distribute multi-month payments by per-month expected fee
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>
This commit is contained in:
97
CLAUDE.md
97
CLAUDE.md
@@ -16,22 +16,95 @@ Flask-based financial management system for FUJ (Frisbee Ultimate Jablonec). Han
|
||||
This project uses `uv` for dependency management.
|
||||
|
||||
```bash
|
||||
uv venv # Create virtual environment
|
||||
uv sync # Install dependencies from pyproject.toml
|
||||
uv venv && uv sync
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Alternatively, use the Makefile:
|
||||
- `make sync` - Sync bank transactions to Google Sheets
|
||||
- `make infer` - Automatically infer Person/Purpose/Amount in the sheet
|
||||
- `make reconcile` - Generate balance report from Google Sheets data
|
||||
- `make fees` - Calculate expected fees from attendance
|
||||
- `make match` - (Legacy) Match bank data directly
|
||||
- `make web` - Start dashboard
|
||||
- `make image` - Build Docker image
|
||||
Set `PYTHONPATH=scripts:.` when running scripts directly (the Makefile does this automatically).
|
||||
|
||||
Requires `.secret/fuj-management-bot-credentials.json` for Google Sheets API access (configurable via `CREDENTIALS_PATH` env var).
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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-cache`
|
||||
- `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.
|
||||
- `scripts/match_payments.py` — `reconcile()` matches transactions to members/months. `fetch_sheet_data()` reads the payments sheet. `fetch_exceptions()` reads the `exceptions` tab.
|
||||
- `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 `tmp/`.
|
||||
- `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 sheet
|
||||
- `X` — 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 `exceptions` tab 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
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user