fix(reconcile): fill earliest month deficit first in multi-month allocations #35

Merged
kacerr merged 1 commits from fix/fill-first-multi-month-allocation into main 2026-05-12 00:01:37 +02:00
Owner

What

Multi-month payment allocation was splitting each transaction proportionally across matched months based on total expected fees, ignoring what prior transactions had already paid into those months.

Surfaced by: Matyáš Thér paid 200 CZK (for 2026-02) then 550 CZK (for 2026-02 + 2026-03). The 550 was being split 366.67/183.33 by the proportional formula instead of filling 2026-02's remaining deficit (300) and spilling 250 into 2026-03. Result: 566/183 instead of the correct 500/250.

Fix

Replace the greedy + proportional branches in both Python and Go reconcilers with a single fill-first loop:

  • Iterate matched months in user-supplied order (chronological by convention).
  • For each month: alloc = min(remaining, max(0, expected − paid_so_far)).
  • Any surplus after all deficits are covered → credit bucket.

Files

  • scripts/match_payments.py — Python reconciler (canonical)
  • go/internal/domain/reconcile/reconcile.go — Go reconciler
  • Tests and two parity fixtures updated to match new behavior.
## What Multi-month payment allocation was splitting each transaction proportionally across matched months based on total expected fees, ignoring what prior transactions had already paid into those months. **Surfaced by**: Matyáš Thér paid 200 CZK (for 2026-02) then 550 CZK (for 2026-02 + 2026-03). The 550 was being split 366.67/183.33 by the proportional formula instead of filling 2026-02's remaining deficit (300) and spilling 250 into 2026-03. Result: 566/183 instead of the correct 500/250. ## Fix Replace the greedy + proportional branches in both Python and Go reconcilers with a single fill-first loop: - Iterate matched months in user-supplied order (chronological by convention). - For each month: `alloc = min(remaining, max(0, expected − paid_so_far))`. - Any surplus after all deficits are covered → credit bucket. ## Files - `scripts/match_payments.py` — Python reconciler (canonical) - `go/internal/domain/reconcile/reconcile.go` — Go reconciler - Tests and two parity fixtures updated to match new behavior.
kacerr added 1 commit 2026-05-12 00:00:08 +02:00
fix(reconcile): fill earliest month deficit first in multi-month allocations
All checks were successful
Deploy to K8s / deploy (push) Successful in 9s
8734089223
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>
kacerr merged commit 10e2e9dc04 into main 2026-05-12 00:01:37 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kacerr/fuj-management#35