Distinguish debit and credit postings sharing an upstream reference

This commit is contained in:
Lars Nolden
2026-09-10 15:48:42 +02:00
parent 964b9dfc15
commit bec6d0b444
3 changed files with 119 additions and 8 deletions
+13 -4
View File
@@ -18,7 +18,15 @@ func digest(parts ...string) string {
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
func identity(f domain.Facts) string { return digest(f.AccountID, f.Source, f.ExternalID) }
func identity(f domain.Facts) string {
// Banks can reuse a reference for a debit and its credit counterpart.
// Facts are normalized before lookup; never rewrite stored journal IDs.
direction := "credit"
if strings.HasPrefix(string(f.Amount), "-") {
direction = "debit"
}
return digest(f.AccountID, f.Source, f.ExternalID, direction)
}
func fingerprint(f domain.Facts) string {
return digest(f.AccountID, f.BookingDate, f.ValueDate, f.Amount.String(), f.Currency, strings.Join(strings.Fields(f.RawDescription), " "), strings.ToLower(strings.Join(strings.Fields(f.Counterparty), " ")), f.CounterpartyIBAN)
}
@@ -30,9 +38,10 @@ func sameBookedMoney(a, b domain.Facts) bool {
}
// NormalizeAndDedupe returns new records without mutating the input. Stable bank
// entry references take precedence over text. CSV rows without references use
// occurrence counts, not a set: two identical rows remain two transactions and
// importing the same export again creates none. For overlapping partial exports,
// entry references, scoped by account, source and debit/credit direction, take
// precedence over text. Rows without references use occurrence counts, not a set:
// two identical rows remain two transactions; reimporting creates none.
// For overlapping partial exports,
// indistinguishable rows cannot prove an additional occurrence; import complete
// overlapping date windows to establish multiplicity.
//