Files
fuj-management/docs/plans/2026-05-06-1626-infer-payments-junior-roster.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

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

Files to read for confidence (no edits)

  • scripts/attendance.py:208-289get_junior_members_with_fees returns (name, tier, …) tuples just like the adults version, so m[0] works for both.
  • scripts/match_payments.py:65-137match_members already handles the precedence the user wants (exact full-name short-circuit), so once Kubí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 Kubík row in the payments sheet.
    • make infer.
    • Expect Person = Jáchym Kubí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 Hrušák (G) and Jáchym Kubík, the message Jáchym Kubík: 01/2026+03/2026+04/2026 resolves 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 that infer_payments now feeds in juniors.

  3. Run the suite: make test.

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

  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.