- Scrub three parity fixtures that still held raw bank messages - Use fictional names in the junior regression test, its changelog entry and its plan file - State the rule in CLAUDE.md: a real name as a bare test input is fine; a real name next to payment details, or any junior or parent name, is not - Tick T4 in the cleanup task list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.5 KiB
Include junior members in payment inference roster
Context
A bank payment from sender JIŘÍ DVOŘÁK with the message
Jáchym Dvořák: 01/2026+03/2026+04/2026 is being inferred as
[?] Jáchym Novotný (G) instead of the obvious Jáchym Dvořák, even though
the message contains his exact full name.
Root cause (confirmed with the user): Jáchym Dvořák is in the junior
attendance sheet only — he does not appear on the main/adults sheet. But
scripts/infer_payments.py:101-102
builds member_names by calling get_members_with_fees()
(scripts/attendance.py:170), which reads only
EXPORT_URL (the adults sheet). Junior-only members are therefore invisible
to the matcher.
With Dvořák absent from member_names, the matcher in
scripts/match_payments.py:65 processes the
combined text jiri dvorak jachym dvorak: 01/2026+03/2026+04/2026 against an
adults-only roster:
- The exact-full-name short-circuit (
match_payments.py:75-84) finds nothing — no adult's full name is in the text. - Novotný
(G)is the only adult with first nameJáchym. He fails the auto-rules (his surname isn't in the text) but hits the partial-first-name review rule (match_payments.py:123-125) → returned as("Jáchym Novotný (G)", "review"), rendered as[?] Jáchym Novotný (G).
The user's original framing — "exact match in message should win over everything" — is already implemented for any candidate that is in the roster (the May-04 short-circuit). The bug is upstream: the right candidate was never even considered.
Goal: make infer_payments consider junior members as candidates, so
junior-only names like Jáchym Dvořák get matched correctly.
Approach
Single-file change in scripts/infer_payments.py.
Replace the adults-only roster lookup with a union of the adult and junior
rosters. attendance.py already exposes both:
get_members_with_fees() for adults (and tier-J
juniors who train with adults) and
get_junior_members_with_fees() for everyone in
the junior sheet.
Edit at scripts/infer_payments.py:15
from attendance import get_members_with_fees, get_junior_members_with_fees
Edit at scripts/infer_payments.py:99-102
print("Fetching member list for matching...")
adult_members, _ = get_members_with_fees()
junior_members, _ = get_junior_members_with_fees()
# Union rosters, preserving first-seen order, deduping by canonical key
seen: set[str] = set()
member_names: list[str] = []
for m in adult_members + junior_members:
name = m[0]
key = canonical_member_key(name)
if key in seen:
continue
seen.add(key)
member_names.append(name)
canonical_member_key already lives in
scripts/match_payments.py:20 — import it
alongside infer_transaction_details. It normalizes diacritics/case/whitespace,
so "Maria Maco" and "Mária Maco" collapse to the same key.
Why downstream reconciliation still works
reconcile() is invoked twice per page — once with the adults roster
(app.py:200) and once with the juniors roster
(app.py:384). Each call resolves the Person cell against its
own roster; a junior name resolves cleanly in the juniors call and lands in
"unmatched" in the adults call. That's already the existing behavior for any
junior payment manually entered into the Person column, so no further
changes are needed.
Files to modify
- scripts/infer_payments.py — only the import + roster construction. ~10-line change.
Files to read for confidence (no edits)
- scripts/attendance.py:208-289 —
get_junior_members_with_feesreturns(name, tier, …)tuples just like the adults version, som[0]works for both. - scripts/match_payments.py:65-137 —
match_membersalready handles the precedence the user wants (exact full-name short-circuit), so once Dvořák is inmember_names, the case will be auto-matched with no[?].
Verification
-
Manual sanity — re-run inference on the offending row:
- Clear
Person/Purposefor the Dvořák row in the payments sheet. make infer.- Expect
Person = Jáchym Dvořák,Purpose = 2026-01, 2026-03, 2026-04, no[?].
- Clear
-
Unit test — extend tests/test_match_members.py (or add a small
tests/test_infer_payments.py) to assert that, given a roster that includesJáchym Novotný (G)andJáchym Dvořák, the messageJáchym Dvořák: 01/2026+03/2026+04/2026resolves to[("Jáchym Dvořák", "auto")]only. This is really a regression test for the May-04 short-circuit — the new behavior under test is just thatinfer_paymentsnow feeds in juniors. -
Run the suite:
make test. -
Dashboard smoke —
make web, open/payments, confirm the row now shows the correct member; open/juniors, confirm the payment is credited to Dvořák for the three months listed. -
Changelog — once the user confirms the fix, append an entry to CHANGELOG.md per CLAUDE.md:
## YYYY-MM-DD HH:MM TZ — fix: include juniors in payment-inference roster.