Distinguish debit and credit postings sharing an upstream reference
This commit is contained in:
@@ -105,6 +105,104 @@ func TestUpstreamIdentityPreferredAndAccountScoped(t *testing.T) {
|
||||
t.Fatal("changed immutable upstream money accepted")
|
||||
}
|
||||
}
|
||||
func TestUpstreamReferenceSeparatesDebitAndCredit(t *testing.T) {
|
||||
d := fixtureDataset()
|
||||
debit := fixtureFacts()
|
||||
debit.Source = "enablebanking"
|
||||
debit.ExternalID = "shared-bank-reference"
|
||||
credit := debit
|
||||
credit.Amount = "12.30"
|
||||
rows, err := NormalizeAndDedupe(d, []domain.Facts{debit, credit, debit, credit})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
amounts := map[domain.Money]int{}
|
||||
for _, row := range rows {
|
||||
amounts[row.Facts.Amount]++
|
||||
}
|
||||
if !reflect.DeepEqual(amounts, map[domain.Money]int{"-12.30": 1, "12.30": 1}) {
|
||||
t.Fatalf("debit/credit postings lost or duplicated: %+v", rows)
|
||||
}
|
||||
d.Transactions = rows
|
||||
if err := domain.Validate(d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again, err := NormalizeAndDedupe(d, []domain.Facts{credit, debit})
|
||||
if err != nil || len(again) != 0 {
|
||||
t.Fatalf("repeated debit/credit pair was not idempotent: %+v %v", again, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamDirectionPreservesPreviouslyStoredIdentity(t *testing.T) {
|
||||
for _, amount := range []domain.Money{"-12.30", "12.30"} {
|
||||
t.Run(string(amount), func(t *testing.T) {
|
||||
d := fixtureDataset()
|
||||
stored := fixtureFacts()
|
||||
stored.Source = "enablebanking"
|
||||
stored.ExternalID = "shared-bank-reference"
|
||||
stored.Amount = amount
|
||||
// Reproduce the journal identity written before direction scoping.
|
||||
stored.ID = "tx_" + digest(stored.AccountID, stored.Source, stored.ExternalID)
|
||||
stored.Fingerprint = fingerprint(stored)
|
||||
d.Transactions = []domain.Transaction{{Facts: stored, Enrichment: domain.Fallback(stored)}}
|
||||
opposite := stored
|
||||
opposite.Amount = "12.30"
|
||||
if amount == "12.30" {
|
||||
opposite.Amount = "-12.30"
|
||||
}
|
||||
incoming := stored
|
||||
incoming.RawDescription = "Updated upstream display"
|
||||
added, err := NormalizeAndDedupe(d, []domain.Facts{opposite, incoming})
|
||||
if err != nil || len(added) != 1 || added[0].Facts.Amount != opposite.Amount {
|
||||
t.Fatalf("stored posting duplicated or opposite posting lost: %+v %v", added, err)
|
||||
}
|
||||
if !reflect.DeepEqual(d.Transactions[0].Facts, stored) {
|
||||
t.Fatal("previously stored bank facts were rewritten")
|
||||
}
|
||||
d.Transactions = append(d.Transactions, added...)
|
||||
if err := domain.Validate(d); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again, err := NormalizeAndDedupe(d, []domain.Facts{incoming, opposite})
|
||||
if err != nil || len(again) != 0 {
|
||||
t.Fatalf("upgraded journal was not idempotent: %+v %v", again, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamReferenceRejectsSameDirectionConflicts(t *testing.T) {
|
||||
original := fixtureFacts()
|
||||
original.Source = "enablebanking"
|
||||
original.ExternalID = "shared-bank-reference"
|
||||
original.Amount = "12.30"
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
change func(*domain.Facts)
|
||||
}{
|
||||
{"amount", func(f *domain.Facts) { f.Amount = "99.00" }},
|
||||
{"booking date", func(f *domain.Facts) { f.BookingDate = "2026-09-02" }},
|
||||
{"currency", func(f *domain.Facts) { f.Currency = "USD" }},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
d := fixtureDataset()
|
||||
changed := original
|
||||
tc.change(&changed)
|
||||
if added, err := NormalizeAndDedupe(d, []domain.Facts{original, changed}); err == nil || added != nil {
|
||||
t.Fatal("conflicting incoming postings were accepted")
|
||||
}
|
||||
var err error
|
||||
d.Transactions, err = NormalizeAndDedupe(d, []domain.Facts{original})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if added, err := NormalizeAndDedupe(d, []domain.Facts{changed}); err == nil || added != nil {
|
||||
t.Fatal("changed stored booking facts were accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDistinctUpstreamIDsPreserveEqualTransactions(t *testing.T) {
|
||||
d := fixtureDataset()
|
||||
a := fixtureFacts()
|
||||
|
||||
Reference in New Issue
Block a user