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>
5.4 KiB
Include junior members in payment inference roster
Context
A bank payment from sender JIŘÍ KUBÍK with the message
Jáchym Kubík: 01/2026+03/2026+04/2026 is being inferred as
[?] Jáchym Hrušák (G) instead of the obvious Jáchym Kubík, even though
the message contains his exact full name.
Root cause (confirmed with the user): Jáchym Kubí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 Kubík absent from member_names, the matcher in
scripts/match_payments.py:65 processes the
combined text jiri kubik jachym kubik: 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. - Hrušák
(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 Hrušák (G)", "review"), rendered as[?] Jáchym Hrušák (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 Kubí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 Kubí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 Kubík row in the payments sheet. make infer.- Expect
Person = Jáchym Kubí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 Hrušák (G)andJáchym Kubík, the messageJáchym Kubík: 01/2026+03/2026+04/2026resolves to[("Jáchym Kubí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 Kubí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.