Add account balance anchors
This commit is contained in:
@@ -17,8 +17,9 @@ import (
|
||||
)
|
||||
|
||||
type bankScenario struct {
|
||||
session banking.Session
|
||||
fail bool
|
||||
session banking.Session
|
||||
fail bool
|
||||
balances []banking.Balance
|
||||
}
|
||||
|
||||
func (b *bankScenario) Authorize(context.Context, string, string, string, string) (string, error) {
|
||||
@@ -41,6 +42,9 @@ func (b *bankScenario) Status(context.Context, string) (banking.SessionStatus, e
|
||||
return status, nil
|
||||
}
|
||||
func (b *bankScenario) Balances(context.Context, string) ([]banking.Balance, error) {
|
||||
if b.balances != nil {
|
||||
return b.balances, nil
|
||||
}
|
||||
return []banking.Balance{{Amount: "100.00", Currency: "EUR", Type: "CLBD"}}, nil
|
||||
}
|
||||
func (b *bankScenario) Transactions(_ context.Context, a domain.Account, from, to string, _ bool) ([]domain.Facts, error) {
|
||||
@@ -83,6 +87,53 @@ func TestSyncRestoresSavedConsentBindingsAndDoesNotDuplicateFacts(t *testing.T)
|
||||
t.Fatal("provider failure was not isolated from canonical data")
|
||||
}
|
||||
}
|
||||
|
||||
// The first successful sync fixes the start balance from the bank's booked
|
||||
// figure only: an available balance includes pending amounts with no booked
|
||||
// fact to subtract, and a later balance change must never move an anchor that
|
||||
// has been set — the anchor is the day a figure was true, not a mirror.
|
||||
func TestSyncAnchorsBalanceOnceFromBookedFigureOnly(t *testing.T) {
|
||||
a, s := testApp(t)
|
||||
account := s.Data.Accounts[0]
|
||||
account.ExternalAccountID = "provider_uid"
|
||||
provider := &bankScenario{
|
||||
session: banking.Session{ID: "session", ValidUntil: time.Now().Add(24 * time.Hour).Format(time.RFC3339), Accounts: []domain.Account{account}},
|
||||
balances: []banking.Balance{{Amount: "999.99", Currency: "EUR", Type: "ITAV"}},
|
||||
}
|
||||
a.bank = provider
|
||||
a.ops.Sessions = []banking.Session{provider.session}
|
||||
if err := a.saveOps(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unbooked, err := a.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := unbooked.Data.Accounts[0]; got.AnchorBalance != "" || got.AnchorDate != "" {
|
||||
t.Fatalf("available-only balance was anchored: %+v", got)
|
||||
}
|
||||
yesterday := time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02")
|
||||
older := time.Now().UTC().AddDate(0, 0, -2).Format("2006-01-02")
|
||||
provider.balances = append(provider.balances,
|
||||
banking.Balance{Amount: "240.00", Currency: "EUR", Type: "CLBD", ReferenceDate: older},
|
||||
banking.Balance{Amount: "250.00", Currency: "EUR", Type: "CLBD", ReferenceDate: yesterday},
|
||||
)
|
||||
anchored, err := a.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := anchored.Data.Accounts[0]; got.AnchorBalance != "250.00" || got.AnchorDate != yesterday {
|
||||
t.Fatalf("booked balance was not anchored at its reference day: %+v", got)
|
||||
}
|
||||
provider.balances = []banking.Balance{{Amount: "300.00", Currency: "EUR", Type: "CLBD", ReferenceDate: yesterday}}
|
||||
retained, err := a.Sync(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(anchored.Data, retained.Data) {
|
||||
t.Fatal("a later balance moved an existing anchor")
|
||||
}
|
||||
}
|
||||
func TestReconnectReplacesOldConsentWithoutDuplicatingLocalAccount(t *testing.T) {
|
||||
a, s := testApp(t)
|
||||
account := s.Data.Accounts[0]
|
||||
@@ -196,9 +247,16 @@ func TestSyncSessionRateLimitPreservesBindingsAndRecovers(t *testing.T) {
|
||||
failures: map[string]error{},
|
||||
}
|
||||
a.bank = b
|
||||
first, err := a.Sync(ctx)
|
||||
if err != nil || len(first.Data.Transactions) != 4 {
|
||||
t.Fatalf("initial sync: transactions=%d, error=%v, sync error=%s", len(first.Data.Transactions), err, first.Status.SyncError)
|
||||
}
|
||||
// The first successful sync also anchors each account's balance; a second
|
||||
// sync reaches the steady state where the session bindings have absorbed
|
||||
// the anchored accounts and nothing changes any more.
|
||||
before, err := a.Sync(ctx)
|
||||
if err != nil || len(before.Data.Transactions) != 4 {
|
||||
t.Fatalf("initial sync: transactions=%d, error=%v, sync error=%s", len(before.Data.Transactions), err, before.Status.SyncError)
|
||||
if err != nil || !reflect.DeepEqual(first.Data, before.Data) {
|
||||
t.Fatalf("steady-state sync changed canonical data: %v", err)
|
||||
}
|
||||
old := time.Now().Add(-48 * time.Hour).UTC().Format(time.RFC3339)
|
||||
a.ops.LastSync = old
|
||||
@@ -267,9 +325,19 @@ func TestSyncMissingMembershipStillRejectsAccount(t *testing.T) {
|
||||
if len(b.accounts) != 1 || b.accounts[0].ID != "other" || a.ops.AccountSync[s.Data.Accounts[0].ID] != last || a.ops.LastSync != last {
|
||||
t.Fatal("missing member was fetched or advanced its cursor, or valid member was skipped")
|
||||
}
|
||||
if !reflect.DeepEqual(before.Accounts, after.Data.Accounts) || len(after.Data.Transactions) != 1 || after.Data.Transactions[0].Facts.AccountID != "other" {
|
||||
if !reflect.DeepEqual(before.Accounts[0], after.Data.Accounts[0]) || len(after.Data.Transactions) != 1 || after.Data.Transactions[0].Facts.AccountID != "other" {
|
||||
t.Fatal("missing membership changed bindings or imported unauthorized facts")
|
||||
}
|
||||
// The authorized member's first successful sync anchors its balance from
|
||||
// the bank's booked figure; the rejected member must not gain one.
|
||||
anchored := after.Data.Accounts[1]
|
||||
if anchored.AnchorBalance != "100.00" || anchored.AnchorDate == "" {
|
||||
t.Fatalf("authorized member was not anchored: %+v", anchored)
|
||||
}
|
||||
anchored.AnchorBalance, anchored.AnchorDate = "", ""
|
||||
if !reflect.DeepEqual(before.Accounts[1], anchored) {
|
||||
t.Fatal("anchoring changed more than the anchor on the authorized member")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncTransactionFailuresPreserveProgressAndSafeErrors(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user