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>
This commit is contained in:
@@ -26,6 +26,15 @@ def canonical_member_key(name: str) -> str:
|
||||
return re.sub(r"\s+", " ", normalize(name)).strip()
|
||||
|
||||
|
||||
def _expected_amount(value) -> float:
|
||||
"""Numeric value of an expected fee; non-numeric markers like '?' → 0.
|
||||
|
||||
Juniors with exactly 1 session get expected='?' (manual-review marker).
|
||||
Treat those as 0 for arithmetic so payments become surplus/credit.
|
||||
"""
|
||||
return value if isinstance(value, (int, float)) else 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Name matching
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -466,7 +475,7 @@ def reconcile(
|
||||
if not in_window:
|
||||
continue
|
||||
|
||||
total_expected = sum(e for _, e in in_window)
|
||||
total_expected = sum(_expected_amount(e) for _, e in in_window)
|
||||
|
||||
if total_expected > 0:
|
||||
# Fill-first: iterate in_window in matched_months order (chronological by
|
||||
@@ -477,7 +486,7 @@ def reconcile(
|
||||
remaining = in_window_share
|
||||
for m, exp in in_window:
|
||||
paid_so_far = ledger[member_name][m]["paid"]
|
||||
deficit = max(0.0, float(exp) - paid_so_far)
|
||||
deficit = max(0.0, float(_expected_amount(exp)) - paid_so_far)
|
||||
alloc = min(remaining, deficit)
|
||||
if alloc <= 0:
|
||||
continue
|
||||
@@ -509,7 +518,7 @@ def reconcile(
|
||||
final_balances: dict[str, int] = {}
|
||||
for name in member_names:
|
||||
window_balance = sum(
|
||||
int(mdata["paid"]) - (mdata["expected"] if isinstance(mdata["expected"], int) else 0)
|
||||
int(mdata["paid"]) - _expected_amount(mdata["expected"])
|
||||
for mdata in ledger[name].values()
|
||||
)
|
||||
final_balances[name] = window_balance + credits.get(name, 0)
|
||||
|
||||
Reference in New Issue
Block a user