Files
fuj-management/docs/plans/2026-05-06-1626-infer-payments-junior-roster.md
T
kacerrandClaude Opus 5.5 e53b57f976 fix(pii): remove real member names tied to payments
- 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>
2026-10-09 00:22:15 +02:00

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 name Já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

Files to read for confidence (no edits)

  • scripts/attendance.py:208-289 — get_junior_members_with_fees returns (name, tier, …) tuples just like the adults version, so m[0] works for both.
  • scripts/match_payments.py:65-137 — match_members already handles the precedence the user wants (exact full-name short-circuit), so once Dvořák is in member_names, the case will be auto-matched with no [?].

Verification

  1. Manual sanity — re-run inference on the offending row:

    • Clear Person/Purpose for the Dvořák row in the payments sheet.
    • make infer.
    • Expect Person = Jáchym Dvořák, Purpose = 2026-01, 2026-03, 2026-04, no [?].
  2. Unit test — extend tests/test_match_members.py (or add a small tests/test_infer_payments.py) to assert that, given a roster that includes Jáchym Novotný (G) and Jáchym Dvořák, the message Jáchym Dvořák: 01/2026+03/2026+04/2026 resolves 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 that infer_payments now feeds in juniors.

  3. Run the suite: make test.

  4. 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.

  5. 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.