Files
fuj-management/docs/plans/2026-06-08-1110-junior-expected-fix.md
Jan Novak 20b618685f fix(reconcile): handle '?' expected fee in fill-first allocation
Juniors with exactly 1 session get expected='?' (manual-review marker
from attendance.py). The fill-first allocation block summed and cast
expected values numerically without guarding against this, causing a
TypeError: unsupported operand type(s) for +: 'int' and 'str' on the
/juniors route whenever any matched payment landed on such a month.

Add _expected_amount() helper that coerces non-numeric markers to 0
(same convention the final-balance calculation at line 512 already used)
and apply it in the two failing spots plus the existing isinstance check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:10:41 +02:00

3.9 KiB

Fix /juniors 500: int + str in reconcile allocation

Context

The /juniors route crashes with:

File ".../scripts/match_payments.py", line 469, in reconcile
    total_expected = sum(e for _, e in in_window)
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Why it happens: A junior with exactly 1 session in a month gets an expected fee of the string "?" (manual-review marker), set in attendance.py:107. That value flows unchanged into the ledger as ledger[name][m]["expected"] (match_payments.py:370).

When a bank payment is matched to such a junior+month, the new fill-first allocation block builds in_window from those expected values and then sums them (match_payments.py:469) and later does float(exp) (match_payments.py:480) — both blow up on the string "?". Adults never hit this because adults never get "?", which is why only /juniors 500s.

Note the final-balance code at match_payments.py:511-512 already handles this defensively (mdata["expected"] if isinstance(..., int) else 0). The allocation block added in the recent fill-first work simply missed the same guard. The intended convention is clear: a "?" (unknown) expected counts as 0 for arithmetic, so any payment landing on such a month becomes surplus / positive balance rather than filling a deficit.

Approach

Add one small helper and apply the existing "non-numeric expected → 0" convention consistently inside reconcile().

1. Helper (near the top of scripts/match_payments.py)

def _expected_amount(value):
    """Numeric value of an 'expected' fee; non-numeric markers like '?' → 0."""
    return value if isinstance(value, (int, float)) else 0

2. Apply it in the allocation block

With expected coerced to 0 for "?" months:

  • A "?"-only payment falls into the existing total_expected == 0 fallback (match_payments.py:495-499) and is recorded as paid (prepayment behaviour) → shows as positive balance.
  • Mixed with real months, the "?" month gets deficit 0 (skipped), real months fill first, surplus → credit. Consistent with line 512.

3. Reuse the helper at line 512 (optional consistency tidy)

Replace the inline isinstance(mdata["expected"], int) check at match_payments.py:512 with _expected_amount(mdata["paid"]/expected) form, i.e. use _expected_amount(...). This also closes a latent gap where a float exception amount would be treated as 0 (current check only accepts int).

print_report (line 552+) iterates adults only, so it's unaffected and needs no change.

Verification

  1. make web and open http://localhost:5001/juniors — page renders 200 (was 500). Confirm a junior with a single-session "?" month who also has a matched payment shows a sensible balance.
  2. /adults still renders unchanged (regression check).
  3. make test — existing reconcile tests still pass; if there is a reconcile test fixture, add a case where a junior month has expected == "?" plus a matched transaction and assert no exception + payment counted as surplus.

Notes

  • Single-file change: scripts/match_payments.py.
  • This is a bug fix; per CLAUDE.md a small fix may go straight to main, but confirm with the user whether to branch (fix/junior-expected-question-mark).
  • Add a CHANGELOG.md entry once confirmed working.
  • On execution, copy this plan to docs/plans/<ts>-junior-expected-fix.md per the repo plan convention.