Add account balance anchors
This commit is contained in:
@@ -416,3 +416,71 @@ func TestWealthCountsHandValuedAssets(t *testing.T) {
|
||||
t.Errorf("assets not echoed sorted by name with their dates: %+v", report.Assets)
|
||||
}
|
||||
}
|
||||
|
||||
// A bank's date-windowed history starts mid-life, so an anchored account
|
||||
// derives its start balance: the bank's booked figure on the anchor day less
|
||||
// everything booked through it. The derived line keeps the flows summing to
|
||||
// the balance, and the pre-anchor window is never judged as an overdraft —
|
||||
// the history there is incomplete by definition.
|
||||
func TestAnchoredAccountDerivesStartBalance(t *testing.T) {
|
||||
data := domain.NewDataset()
|
||||
data.Accounts = []domain.Account{
|
||||
{ID: "acc_anchored", DisplayName: "Checking", Currency: "EUR", Active: true, ExternalAccountID: "uid_one", AnchorBalance: "2450.00", AnchorDate: "2026-09-10"},
|
||||
{ID: "acc_plain", DisplayName: "Connected", Currency: "EUR", Active: true, ExternalAccountID: "uid_two"},
|
||||
}
|
||||
row := func(id, account, date string, amount domain.Money) domain.Transaction {
|
||||
f := domain.Facts{ID: id, Source: "enablebanking", AccountID: account, BookingDate: date, Amount: amount, Currency: "EUR", RawDescription: id, Fingerprint: "fp_" + id}
|
||||
return domain.Transaction{Facts: f, Enrichment: domain.Fallback(f)}
|
||||
}
|
||||
data.Transactions = []domain.Transaction{
|
||||
// The recorded window alone would dip to −900 before the anchor day.
|
||||
row("tx_pre", "acc_anchored", "2026-09-01", "-900.00"),
|
||||
row("tx_on", "acc_anchored", "2026-09-10", "50.00"),
|
||||
row("tx_post", "acc_anchored", "2026-09-12", "-100.00"),
|
||||
row("tx_other", "acc_plain", "2026-09-12", "10.00"),
|
||||
}
|
||||
if err := domain.Validate(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
report := WealthOf(data)
|
||||
anchored := report.Accounts[0]
|
||||
// 2450.00 on 2026-09-10 less the −850.00 booked through that day puts
|
||||
// 3300.00 before the window; the balance is 2450.00 − 100.00 booked after.
|
||||
if anchored.Cash != "2350.00" || anchored.Wealth != "2350.00" {
|
||||
t.Errorf("anchored cash %s wealth %s, want 2350.00", anchored.Cash, anchored.Wealth)
|
||||
}
|
||||
if len(anchored.Flows) == 0 || anchored.Flows[0].Event != "anchor" || anchored.Flows[0].Cash != "3300.00" {
|
||||
t.Errorf("start balance line missing or wrong: %+v", anchored.Flows)
|
||||
}
|
||||
total := int64(0)
|
||||
for _, flow := range anchored.Flows {
|
||||
cash, err := flow.Cash.Minor()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
total += cash
|
||||
}
|
||||
if domain.FormatMoney(total) != anchored.Cash {
|
||||
t.Errorf("flows sum to %s, balance is %s", domain.FormatMoney(total), anchored.Cash)
|
||||
}
|
||||
checks := map[string]WealthCheck{}
|
||||
for _, check := range anchored.Checks {
|
||||
checks[check.Name] = check
|
||||
}
|
||||
if _, ok := checks["Balance anchored"]; !ok {
|
||||
t.Errorf("no anchor note: %+v", anchored.Checks)
|
||||
}
|
||||
if check := checks["Cash never negative"]; check.Failed {
|
||||
t.Errorf("pre-anchor window judged as an overdraft: %s", check.Detail)
|
||||
}
|
||||
note := false
|
||||
for _, check := range report.Accounts[1].Checks {
|
||||
note = note || check.Name == "Balance not anchored"
|
||||
}
|
||||
if !note {
|
||||
t.Errorf("connected account without an anchor carries no note: %+v", report.Accounts[1].Checks)
|
||||
}
|
||||
if report.Totals[0].Cash != "2360.00" {
|
||||
t.Errorf("total cash %s, want 2360.00", report.Totals[0].Cash)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user