SHA-256 dedup hash from sync_fio_to_sheets.py generate_sync_id.
Key subtlety: Python str(float) emits "500.0" for whole-valued floats
and switches to scientific notation at |f|>=1e16 or |f|<1e-4 —
replicated via formatAmount using 'f'/'e' format selection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Port scripts/infer_payments.py parse_czk_amount to Go as
internal/domain/money.ParseCZK. Preserves the Czech-locale heuristic
(comma = decimal sep; 2+ dots = thousand seps; single dot = decimal)
and returns (float64, error) so callers can opt into Python's
silent-zero contract via v, _ := money.ParseCZK(s).
All expected values verified against live Python on 2026-05-06.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ports calculate_fee and calculate_junior_fee from scripts/attendance.py
into a new go/internal/domain/fees package. Introduces the Expected type
(Value int, Unknown bool) for the junior "?" sentinel, keeping the Go
API strictly typed instead of mirroring Python's str|int return.
All 20 table-driven tests pass with -race; golangci-lint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ADULT_FEE_DEFAULT is 700 CZK, not 750. The 750 appears in
ADULT_FEE_MONTHLY_RATE for most current months but is not the fallback.
Rephrase the member-tiers bullet to point at the dict rather than a
number that drifts each season; update the fee-calc bullet to match
the junior line's style (default 700 vs default 500).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three-pass regex parser matching python/czech_utils.py parse_month_references:
1. Numeric slash notation — "11+12/2025", "01/26"; 2-digit year → +2000
2. Dot notation — "12.2025" (4-digit year only)
3. Czech month names — range walk (listopad-leden wrap logic) then
standalone with m≥10 → defaultYear-1 heuristic; longest-match
alternation (sorted desc by name length) handles cervenec vs cerven
35 table-driven tests, all expected outputs verified against live Python
on 2026-05-05 before locking. Plan at
docs/plans/2026-05-05-2337-go-rewrite-m2-2-parse-month-references.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the "do not use tea/gh/Gitea API" rule with explicit guidance to
run `tea pr create` and print the resulting PR URL. tea is already
authenticated on this machine. Merging stays a manual user action in
Gitea — neither tea nor git CLI may merge or delete branches.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds internal/domain/czech.Normalize, the first pure-domain function in
the Go rewrite (M2 milestone). Matches Python czech_utils.normalize byte-
for-byte: NFKD decompose via golang.org/x/text/unicode/norm, drop Mn-
category combining marks (unicode.Mn, not IsMark, to match Python's
unicodedata.combining() semantics), then strings.ToLower.
Includes 13-case table-driven test; all inputs spot-checked against the
Python implementation before locking. Adds golang.org/x/text v0.36.0 as
first external dependency.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Feature work now goes on feat/<slug> branches; Claude pushes and prints
the Gitea compare URL for the user to open the MR. Exceptions documented
for small fixes and typo tweaks.
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>
The previous calculation derived balance from total_balance (which includes
current/future-month activity and out-of-window credits) plus a one-sided
debt-only adjustment. Current-month surplus leaked through, making the balance
appear less negative than actual past-month debt (e.g. Mauric Daniel -1250 vs
correct -1750). Pay-All is now max(0, -balance) so the two values share a
single source and cannot disagree.
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>
Hide Pay/Pay All buttons for months still in progress, exclude
current month debt from balance column, and show in-progress
month debt in a muted red color.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Compare against current YYYY-MM to exclude future months from the
from/to selectors, default selection, and table column display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add from/to combobox selectors and Apply/All buttons to filter
which month columns are displayed. Defaults to last 5 months on load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deleted /fees, /fees-juniors, /reconcile, /reconcile-juniors routes and
their templates. Payment Ledger (/payments) is retained. Nav updated
across all remaining templates.
Co-Authored-By: Claude Sonnet 4.6 <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>
Adds a /flush-cache web page with a button to clear all cached Google
Sheets data and reset refresh timers. Link added to Tools nav across
all templates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Format is now "Name: MM/YYYY+MM/YYYY" instead of "Name / MM/YYYY+MM/YYYY"
for clearer readability when multiple months are included.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a new GET /sync-bank route that runs sync_to_sheets (2026) + infer_payments + flush_cache,
capturing all output and displaying it on a styled results page. Adds "Tools: [Sync Bank Data]"
nav link to all templates.
Co-Authored-By: Claude Opus 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>
Store git tag, commit hash, and build date as OCI-standard labels and a
build_meta.json file inside the Docker image. The Flask app reads this at
startup and displays version info in all page footers. Adds /version JSON
endpoint for programmatic access. Falls back to dev@local outside Docker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a junior attended only once in a month (fee = "?"), the dashboard
cells now display the attendance details (e.g., "? (1:1J)") instead of
a bare "?". Applied to both /juniors and /reconcile-juniors routes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-fetches all 4 cached datasets (attendance, juniors, payments,
exceptions) at module load time so the first request doesn't block
on Google API calls.
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>
Add optional serialize/deserialize hooks to get_cached_data() so it
can handle the exceptions dict (tuple keys → JSON-safe lists) without
needing a separate function.
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