Files
fuj-management/CHANGELOG.md
Jan Novak c5a8a4e7b1
Some checks failed
Deploy to K8s / deploy (push) Successful in 10s
Build and Push / build (push) Successful in 6s
Build and Push / build-go (push) Failing after 12m23s
fix: include juniors in payment-inference roster
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>
2026-05-06 16:38:21 +02:00

86 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## 2026-05-06 16:38 CEST — fix: include juniors in payment-inference roster
- `scripts/infer_payments.py`: union adults + junior rosters so junior-only members are visible to the matcher.
- Root cause: `get_members_with_fees()` reads only the adults sheet; junior-only kids like Jáchym Kubík were absent from `member_names`, causing the exact-match short-circuit to never fire and a different adult sharing the first name to win via fuzzy review.
- Two regression tests added to `tests/test_match_members.py`.
## 2026-05-06 13:18 CEST — feat(go/M2.7-2.9): port domain/matching package
- New `go/internal/domain/matching` package porting three helpers from `scripts/match_payments.py`.
- `BuildNameVariants` — extracts normalized ASCII search variants from a member name, including nickname (from parens) and separate first/last; filters variants shorter than 3 chars; `variants[0]` is always the full normalized base name.
- `MatchMembers` — finds members in free text with `"auto"` or `"review"` confidence; exact-name short-circuit prevents nickname substrings (e.g. `tov`) from matching inside surnames (e.g. `ottova`).
- `FormatDate` — normalizes Google Sheets date values: handles nil, empty, int/float64 serial-days since 1899-12-30 (supports fractional serials), pre-formatted `YYYY-MM-DD` strings, and garbage input — never errors.
- `InferTransactionDetails` — composes name + month matching over sender/message/user_id; falls back to sender-only member match and date-derived month when text gives no signal.
- 21 table-driven tests; all expected values verified against live Python on 2026-05-06.
## 2026-05-06 12:43 CEST — feat(go/M2.6): port domain/synch.GenerateSyncID
- New `go/internal/domain/synch` package with `GenerateSyncID(Transaction) string` ported from `scripts/sync_fio_to_sheets.py` `generate_sync_id`.
- Byte-stable SHA-256 hash over `date|amount|currency|sender|vs|message|bank_id` (lowercased, UTF-8); `Currency: ""` defaults to `"CZK"` matching the Python missing-key fallback.
- Key subtlety: Python's `str(float)` emits `"500.0"` for whole-valued floats and switches to scientific notation at `|f| >= 1e16` or `|f| < 1e-4` — replicated in `formatAmount` using `'f'`/`'e'` format selection.
- 6 table-driven hash tests + 9 `formatAmount` tests; all expected values verified against live Python on 2026-05-06.
## 2026-05-06 09:38 CEST — feat(go/M2.5): port domain/money.ParseCZK
- New `go/internal/domain/money` package with `ParseCZK(string) (float64, error)` ported from `scripts/infer_payments.py` `parse_czk_amount`.
- Preserves the Czech-locale heuristic: comma → decimal sep; 2+ dots → thousand seps; single dot → decimal (so `"1.500"``1.5`).
- Returns `(0, ErrInvalidAmount)` on parse failure; callers wanting Python's silent-zero contract use `v, _ := ParseCZK(s)`.
- 15 table-driven tests plus a silent-zero contract test; all expected values verified against live Python on 2026-05-06.
## 2026-05-06 09:24 CEST — feat(go/M2.3+M2.4): port domain/fees.CalculateFee and CalculateJuniorFee
- New `go/internal/domain/fees` package with adult and junior fee calculators ported from `scripts/attendance.py`.
- `CalculateFee(count, monthKey) int``0→0`, `1→200`, `2+→AdultFeeMonthlyRate[month]` (fallback 700 CZK).
- `CalculateJuniorFee(count, monthKey) Expected``0→{0}`, `1→{Unknown:true}` (the `"?"` sentinel, now strictly typed), `2+→JuniorFeeMonthlyRate[month]` (fallback 500 CZK).
- 20 table-driven tests, all verified against live Python; `-race` clean; `golangci-lint` clean.
## 2026-05-06 00:07 CEST — feat(go/M2.2): port czech.ParseMonthReferences
- `internal/domain/czech.ParseMonthReferences`: three-pass regex (numeric slash, dot, Czech month names) with range wrap-around and `m≥10 → previousYear` heuristic, byte-equivalent to Python.
- 35 table-driven tests; all expected outputs verified against live Python before locking (addresses risk #4 from the rewrite plan).
## 2026-05-05 23:33 CEST — feat(go/M2.1): port czech.Normalize
- First M2 pure-domain task: `internal/domain/czech.Normalize` (NFKD + Mn-strip + lowercase), byte-equivalent to Python `czech_utils.normalize`.
- Adds `golang.org/x/text v0.36.0` as first external Go dependency.
- 13-case table-driven test, all spot-checked against Python before locking.
## 2026-05-04 23:08 CEST — fix: payment inference exact-match short-circuit
- `match_members()` now short-circuits on whole-word full-name hits; nickname/partial checks only run when no full name is present.
- Replaced bare `in` substring checks with `_word_in()` word-boundary regex throughout, closing the class of bugs where a short nickname (e.g. `tov`) matches inside another member's surname (`ottova`).
- Added `tests/test_match_members.py` (6 cases). Affects `scripts/match_payments.py`.
## 2026-05-04 23:08 CEST — feat: lower adult monthly fee to 700 CZK from April 2026
- `ADULT_FEE_DEFAULT` reduced from 750 → 700 CZK.
- `ADULT_FEE_MONTHLY_RATE` now pins Sep 2025 Feb 2026 at 750 to preserve historical billing; Mar 2026 stays 350; AprMay 2026 at 700. Affects `scripts/attendance.py`.
## 2026-05-04 12:02 CEST — Go rewrite M1: skeleton + tooling
- Created `go/` tree with module `fuj-management/go` (Go 1.26).
- `cmd/fuj`: stdlib-flag subcommand dispatcher; `server` and `version` implemented, stubs for M2/M4 commands.
- `internal/config`: env loader mirroring `scripts/config.py` (same env var names and defaults).
- `internal/logging`: slog setup accepting log level from config.
- `internal/web`: `net/http` ServeMux on `:8080`; `middleware/timer.go` logs method/path/status/ms.
- `go/build/Dockerfile`: multi-stage (`golang:1.26``alpine:3`) producing a static binary image.
- Makefile: `web``web-py` alias; added `web-go`, `go-build`, `go-test`, `go-run`, `go-lint`.
- `.gitea/workflows/build.yaml`: parallel `build-go` job pushing `<tag>-go` image.
- Gate: `make go-build`, `make go-lint`, `make go-test`, `curl :8080` all pass.
## 2026-05-03 20:37 CEST — Fix Balance column to correctly reflect past-month debt
- Balance (and Pay-All) are now computed as `sum(paid expected)` over past months only, iterating directly over the ledger entries from `reconcile()`.
- Previously the balance used `total_balance` (which includes current/future-month activity and out-of-window credits) plus a one-sided current-month debt adjustment. Current-month *surplus* leaked through, making the balance appear less negative than the actual past-month debt.
- Pay-All is now `max(0, balance)` so the two values are derived from a single source and can never disagree.
- Affected: `adults_view()` and `juniors_view()` in `app.py`.
## 2026-05-03 19:26 CEST — Fee-aware allocation for multi-month payments
- `reconcile()` no longer splits a multi-month payment evenly. Allocation is now per-member with two phases: greedy (if amount ≥ total expected, each month gets exactly its expected fee and overflow → credit) and proportional (otherwise distribute by each month's expected). Fixes the case where e.g. 1250 CZK covering 3 months with mixed fees (750/350/150) marked two months red.
- Out-of-window months keep the previous even-split-to-credit behavior. Fallback to even split when all matched months have `expected = 0` (prepayment before attendance is recorded).
- Display layer only — no changes to how payments are stored in Google Sheets; `Inferred Amount` still holds the full bank amount.
- Files: [scripts/match_payments.py](scripts/match_payments.py), [tests/test_reconcile_exceptions.py](tests/test_reconcile_exceptions.py) (6 new test cases).