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.
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.
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 main2026-05-12 00:01:37 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
alloc = min(remaining, max(0, expected − paid_so_far)).Files
scripts/match_payments.py— Python reconciler (canonical)go/internal/domain/reconcile/reconcile.go— Go reconciler