fix(reconcile): fill earliest month deficit first in multi-month allocations
All checks were successful
Deploy to K8s / deploy (push) Successful in 9s
All checks were successful
Deploy to K8s / deploy (push) Successful in 9s
Replace proportional split with a fill-first loop that allocates min(remaining, deficit) to each matched month in user-supplied order, where deficit = expected - already_paid. Prior transactions' contributions are now properly accounted for, so a second payment on overlapping months fills only what's still owed instead of splitting proportionally by total expected. Surplus after all deficits are covered goes to the credit bucket. Fixes: Matyáš Thér 200+550 showing 566/183 instead of 500/250. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -468,26 +468,19 @@ def reconcile(
|
||||
|
||||
total_expected = sum(e for _, e in in_window)
|
||||
|
||||
if total_expected > 0 and in_window_share >= total_expected:
|
||||
# Greedy phase: payment covers all in-window fees; overflow → credit.
|
||||
credits[member_name] = credits.get(member_name, 0) + int(in_window_share - total_expected)
|
||||
for m, exp in in_window:
|
||||
alloc = float(exp)
|
||||
ledger[member_name][m]["paid"] += alloc
|
||||
ledger[member_name][m]["transactions"].append({
|
||||
"amount": alloc,
|
||||
"date": tx["date"],
|
||||
"sender": tx["sender"],
|
||||
"message": tx["message"],
|
||||
"confidence": confidence,
|
||||
})
|
||||
elif total_expected > 0:
|
||||
# Proportional phase: distribute in_window_share by each month's expected fee.
|
||||
# Last month absorbs any float remainder so the sum equals in_window_share exactly.
|
||||
if total_expected > 0:
|
||||
# Fill-first: iterate in_window in matched_months order (chronological by
|
||||
# convention from infer_payments.py), allocating min(remaining, deficit) to
|
||||
# each month. Deficit is net of what prior transactions already paid, so a
|
||||
# second payment on the same months correctly fills only what remains due.
|
||||
# Any surplus after all deficits are covered goes to the credit bucket.
|
||||
remaining = in_window_share
|
||||
for i, (m, exp) in enumerate(in_window):
|
||||
alloc = remaining if i == len(in_window) - 1 else in_window_share * exp / total_expected
|
||||
remaining -= alloc
|
||||
for m, exp in in_window:
|
||||
paid_so_far = ledger[member_name][m]["paid"]
|
||||
deficit = max(0.0, float(exp) - paid_so_far)
|
||||
alloc = min(remaining, deficit)
|
||||
if alloc <= 0:
|
||||
continue
|
||||
ledger[member_name][m]["paid"] += alloc
|
||||
ledger[member_name][m]["transactions"].append({
|
||||
"amount": alloc,
|
||||
@@ -496,6 +489,9 @@ def reconcile(
|
||||
"message": tx["message"],
|
||||
"confidence": confidence,
|
||||
})
|
||||
remaining -= alloc
|
||||
if remaining > 0:
|
||||
credits[member_name] = credits.get(member_name, 0) + int(remaining)
|
||||
else:
|
||||
# Fallback: no expected fees (prepayment before attendance recorded); even split.
|
||||
per_month = in_window_share / len(in_window)
|
||||
|
||||
Reference in New Issue
Block a user