Add second Fio account (CZ0820100000002502035405 / 2502035405/2010).
Both accounts are fetched on every sync run and combined before dedup,
so the payments sheet accumulates transactions from either account.
QR codes now default to the new account.
Go:
- config.go: hardcoded Accounts/LoadedAccount slice replaces scalar
BankAccount + FioAPIToken; Config.BankAccount renamed QRAccount;
per-account tokens via FIO_API_TOKEN_NEW / FIO_API_TOKEN_OLD
- banksync.SyncToSheets: accepts []fio.Client, loops to combine txns
- cmd/fuj/main.go: buildFioClients helper; both sync call sites updated
- html_handler + build_adults/juniors: use Config.QRAccount
- New TestSyncToSheets_MultiAccount covers cross-account dedup
Python:
- config.py: ACCOUNTS list + LOADED_ACCOUNTS (tokens from env)
- fio_utils.py: fetch_transactions_for (per-account) +
fetch_transactions_all (loops all accounts)
- sync_fio_to_sheets.py: uses fetch_transactions_all
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace proportional split with a fill-first loop that allocates
min(remaining, deficit) to each matched month in user-supplied order,
where deficit = expected - already_paid. Prior transactions' contributions
are now properly accounted for, so a second payment on overlapping months
fills only what's still owed instead of splitting proportionally by total
expected. Surplus after all deficits are covered goes to the credit bucket.
Fixes: Matyáš Thér 200+550 showing 566/183 instead of 500/250.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fio's transparent page now serves recent rows as DD.MM.YY while older
rows stay DD.MM.YYYY. parse_czech_date only knew the 4-digit form so
every recent transfer was silently dropped — make sync-2026 reported
zero new transactions. Adds %d.%m.%y and %d/%m/%y to the format list,
mirroring the Go-side fix from 2026-05-07.
Also adds a Python analog of make go-sync-debug:
- --dry-run skips header write / append / sort and prints "would …" lines
- --print-fio-table prints aligned per-txn table with NEW/DUP status
- make sync-debug [DAYS=N] wrapper (default DAYS=30)
- always-on stderr diagnostics in fio_utils: which fetcher was chosen
(with FIO_API_TOKEN-unset lag warning) + raw-vs-filtered counts, so
this class of "scraper drops everything" bug surfaces immediately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two regressions made older periods invisible on the adults dashboard:
- 1257f0d (Mar 9) commented out ADULT_MERGED_MONTHS, removing the
Sep+Oct 2025 merged label. Restored only the 2025-09 → 2025-10
mapping (Dec and Jan are billed separately for adults; the
Dec → Jan mapping stays disabled per product decision). Mirrored
on the Go side. Test fixtures in sources_test.go now assert Sep
dates land in merged 2025-10 instead of 2025-09.
- 7774301 (Apr 9) added a JS onload default that set the From
selector to maxMonthIdx − 4 and immediately filtered the table,
hiding everything older than 5 months on first load. Dropped that
default in templates/adults.html and templates/juniors.html so
the From-selector starts at the oldest available month. Future
months are still removed from the dropdowns and hidden in the
table — only the past-month truncation is gone.
Note: the live adults attendance sheet had also been pruned to
start at 02.12.2025; restoring Sep/Oct/Nov 2025 columns from
Sheets version history is required to actually see those periods.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Go's build_juniors sets cellText = "?" + countStr whenever
md.IsUnknown is true, regardless of whether an exception overrides the
expected amount. Python was checking expected == "?" for this branch,
but reconcile replaces expected with the exception amount (e.g. 0)
before the view builder runs, so the "?" was silently dropped to "-".
Fix: derive is_unknown from original_expected == "?" (set before
exception substitution) instead of expected == "?". Also align the
tooltip guard: Go only shows Received/Expected tooltip for non-unknown
months (or when paid > 0), matching the same is_unknown flag.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two remaining make parity diffs vs Go:
- amount: non-numeric sheet values like "---" passed through as strings;
Go's parseFloat silently returns 0.0 for unparseable values. Add
get_float helper that matches that behaviour.
- message: numeric cell values (e.g. a bank reference in the message
column) passed through as float64; Go's getVal uses fmt.Sprint and
always emits a string. Apply get_str to the message field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Sheets API returns VS (variabilní symbol) cells as float64 when
the column is number-formatted, so Python was emitting vs: 0 (a JSON
number) while Go's getVal uses fmt.Sprint and always emits vs: "0"
(a string). Add get_str helper that converts whole-number floats via
int() first (matching Go's %g formatting), applied to the vs field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Python's fetch_sheet_data read 9 sheet columns but skipped VS and
Sync ID, causing make parity to report extra fields on every raw
payment row emitted by the Go backend. Both columns are already on
the sheet; add idx_vs / idx_sync_id lookups and the matching keys
to the tx dict so the Python /api/* wire shape matches Go's
RawTransaction.
Update /api/* test fixtures to include vs/sync_id keys for realism.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull 350+ lines of inline per-row computation out of adults_view,
juniors_view, and payments into three pure builder functions with no
Flask globals or IO dependencies. Route handlers now contain only
cache/IO calls and a single render_template. No behaviour change —
all 27 tests pass.
Also moves get_month_labels, group_payments_by_person, and
adapt_junior_members out of app.py. Prep for /api/* shadow endpoints
(M5 Go parity).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes M3.1–M3.6. Parity safety net proving Go output matches Python
for every ported pure-domain function (M2.1–M2.9) and reconcile (M2.10).
Capture pipeline:
- scripts/capture_fixtures.py: calls each Python function with seeded
inputs, emits JSON fixtures to stdout (never writes files directly).
- scripts/scrub_fixtures.py: deterministic PII scrubber — SHA-256
pseudonyms for member names, digit-preserving hashes for VS/account/
bank_id, name-sweep in message text. Idempotent; no salt.
- scripts/_fixture_seeds.py: handcrafted seeds for all 11 functions;
synthetic names throughout (no real roster members).
- scripts/capture_all_fixtures.sh: convenience wrapper for full corpus
regeneration outside of make.
Fixture corpus (98 files, all PII-free):
- go/tests/fixtures/pure/<func>/<case>.json — 10 function directories.
- go/tests/fixtures/reconcile/<NN>_<case>.json — 10 branch-coverage
cases: greedy, overpayment credit, proportional remainder, even-split,
out-of-window, exception override, other: purpose, junior ?, multi-
person+month fan-out, unmatched.
Go parity tests (//go:build parity):
- go/tests/parity/parityio.go: generic LoadDir/RunAll helpers + typed
In/Out struct pairs for all 10 pure functions; Envelope decoder for
int/float/none disambiguation.
- 10 pure-function test packages + bespoke reconcile test with per-cell
float tolerance (math.Abs <= 0.01 for `paid` values).
Makefile: go-parity, go-test-all, capture-fixtures targets.
go/tests/fixtures/README.md: refresh workflow + PII audit guide.
Gate: make go-test green, make go-parity green (11/11 packages),
make go-lint clean (parity tag), make go-build clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
infer_payments was building member_names from get_members_with_fees()
(adults sheet only). Junior-only members were invisible to the matcher,
so a payment message containing an exact junior name would produce a
fuzzy review match against a different adult sharing the same first name.
Fix: union the adult and junior rosters (deduped via canonical_member_key)
so all members are candidates. The existing exact-name short-circuit in
match_members then handles precedence correctly.
Two regression tests added for the Jáchym Kubík / Jáchym Hrušák case.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add canonical_member_key() in match_payments.py to normalize names via
NFKD + lowercase + whitespace-collapse before ledger lookup; resolves
payments attributed to e.g. "Maria Maco" to canonical "Mária Maco".
Emits logger.info when a non-canonical cell is rescued so sheet typos
are visible in logs without losing the payment allocation.
- Extend group_payments_by_person() in app.py to accept member_names and
re-key raw-payment groups under the canonical attendance-sheet name so
the modal's Raw Payments debug section also finds the row correctly.
- Add raw payments collapsible section to member detail modal in adults.html
and juniors.html for debugging payment attribution issues.
- Remove 4 obsolete tests targeting routes /fees, /fees-juniors, /reconcile,
/reconcile-juniors that no longer exist; add test_match_payments.py
covering canonical key equivalence and reconcile() tolerance end-to-end.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
match_members() now short-circuits on whole-word full-name hits and
uses word-boundary regex everywhere else, so a nickname that is a
substring of another member's surname (e.g. "tov" inside "ottova")
no longer produces false positives. Adds tests/test_match_members.py.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pin Sep 2025 – Feb 2026 at 750 CZK in ADULT_FEE_MONTHLY_RATE so
historical billing is unchanged; new default 700 CZK applies to
2026-04 onward. March 2026 stays at 350.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Adds docs/operation-manual.md describing how to add per-month fee
overrides for adults and juniors. Also adds the March 2026 junior
fee override (250 CZK) to JUNIOR_MONTHLY_RATE to match the existing
adult override.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirror the junior fee override mechanism (JUNIOR_MONTHLY_RATE) for adults
via ADULT_FEE_MONTHLY_RATE. Set 2026-03 override to 350 CZK. Rename
FEE_FULL/FEE_SINGLE to ADULT_FEE_DEFAULT/ADULT_FEE_SINGLE for consistency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove insecure SSL verification bypass in attendance.py
- Add gunicorn as production WSGI server (Dockerfile + entrypoint)
- Fix silent data loss in reconciliation (log + surface unmatched members)
- Add required column validation in payment sheet parsing
- Add input validation on /qr route (account format, amount bounds, SPD injection)
- Centralize configuration into scripts/config.py
- Extract credentials path to env-configurable constant
- Hide unmatched transactions from reconcile-juniors page
- Fix test mocks to bypass cache layer (all 8 tests now pass reliably)
- Add pytest + pytest-cov dev dependencies
- Fix typo "Inffering" in infer_payments.py
- Update CLAUDE.md to reflect current project state
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the file mtime check from the API debounce tier in
get_sheet_modified_time(). Previously, the debounce was defeated when
CACHE_TTL_SECONDS differed from CACHE_API_CHECK_TTL_SECONDS because
the file age check would fail even though the API was checked recently.
Also fix cache key mappings (attendance_juniors sheet ID,
payments_transactions rename) and add tmp/ to .gitignore.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add cache_utils.py with JSON caching for Google Sheets
- Authenticate and cache Drive/Sheets API services globally to reuse tokens
- Use CACHE_SHEET_MAP dict to resolve cache names securely to Sheet IDs
- Change app.py data fetching to skip downloads if modifiedTime matches cache
- Replace global socket timeout with httplib2 to fix Werkzeug timeouts
- Add VS Code attach debugpy configurations to launch.json and Makefile
- Add dual-sheet architecture to pull attendance from both adult and junior spreadsheets.
- Introduce parsing rules to isolate juniors (e.g. above '# Treneri', tier 'J').
- Add new endpoints `/fees-juniors` and `/reconcile-juniors` to track junior attendances and match bank payments.
- Display granular attendance components showing adult vs. junior practices.
- Add fee rule configuration supporting custom pricing exceptions for specific months (e.g. Sep 2025) and merging billing periods.
- Add `make sync-2025` target to the Makefile for convenience.
- Document junior fees implementation logic and rules in prompts/outcomes.
Co-authored-by: Antigravity <antigravity@google.com>
- Users can now navigate between members in the details popup using Up/Down arrows.
- Fixed 0 attendance count in member popup by preserving count in reconciliation.
- Updated uv.lock following dependency changes.
Co-authored-by: Antigravity <antigravity@google.com>
- Added a Makefile to easily run project scripts (fees, match, web, image)
- Modified attendance.py to dynamically handle a variable number of header rows from the Google Sheet
- Updated both attendance calculations and calculate_fees terminal output to show actual attendance counts (e.g., '750 CZK (3)')
- Created a Flask web dashboard (app.py and templates/fees.html) to view member fees in an attractive, condensed, terminal-like UI
- Bound the Flask server to port 5000 and added a routing alias from '/' to '/fees'
- Configured Python virtual environment (.venv) creation directly into the Makefile to resolve global pip install errors on macOS
Co-authored-by: Antigravity <antigravity@deepmind.com>