Show which legs a transfer cannot pair, and what would pair them

The transfers screen counted unpaired legs per definition but never said
which transactions they were; finding them meant retyping the definition
into the builder. A ⚠ row now opens onto its legs, newest first, each
naming the missing side and what that side would have had to be:

  revolut → ?  arriving leg  +250.00 in other matching *FROM REVOLUT*,
                             dated 2026-02-01 to 2026-02-11

That description comes from transfers.Engine.Want, next to
bestCounterpart, so it is stated in the terms the pairing actually uses:
the other half of the definition, the window, and the amount -- exact,
widened only by the definition's own tolerance, and "any amount" across
currencies, where amounts are not compared. When the other account has
nothing imported at all, the line says so, since that is the usual cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 19:43:13 +02:00
co-authored by Claude Opus 5.5
parent 17428a4f51
commit 7f5b4846ef
7 changed files with 240 additions and 14 deletions
+35
View File
@@ -214,6 +214,41 @@ func bestCounterpart(out model.Transaction, ins []model.Transaction, used map[in
return best
}
// Wanted describes the counterpart an unpaired leg is missing, in the terms
// bestCounterpart judges it by, so a person can go and look for it.
type Wanted struct {
Account string // where the other leg would be
Desc string // the glob it would have to match
From string // the window it would have to fall in, inclusive
To string
// Amount is the other leg's signed amount, exact unless TolerancePct
// widens it. AnyAmount is set across currencies, where the amount is not
// checked at all and the dates carry the pairing alone.
Amount int64
TolerancePct float64
AnyAmount bool
}
// Want says what would have paired l. currencyOf names an account's currency,
// or "" for one the index holds nothing for.
func (e *Engine) Want(l Leg, currencyOf func(slug string) string) Wanted {
def := e.defs[l.Def]
w := Wanted{Account: def.ToAccount, Desc: def.ToDesc, Amount: -l.Txn.AmountMinor}
if !l.Out {
w.Account, w.Desc = def.FromAccount, def.FromDesc
}
if c := currencyOf(w.Account); c != "" && c != l.Txn.Currency {
w.AnyAmount = true
} else {
w.TolerancePct = def.TolerancePct
}
if d, ok := day(l.Txn.Date); ok {
w.From = d.AddDate(0, 0, -WindowDays).Format("2006-01-02")
w.To = d.AddDate(0, 0, WindowDays).Format("2006-01-02")
}
return w
}
// allowance is how far the arriving leg may miss the leaving one, in minor
// units. It is a share of the amount that left, not of the difference, so the
// same percentage means the same thing on a large transfer as on a small one.