Add per-account historical bank imports without resetting sync cursors

This commit is contained in:
Lars Nolden
2026-09-10 17:02:17 +02:00
parent f4c7d54575
commit 2259db3e85
7 changed files with 561 additions and 8 deletions
+70
View File
@@ -92,6 +92,76 @@ func (a *App) ImportCSV(ctx context.Context, rev, accountID string, r io.Reader)
}
return ImportResult{}, errors.New("unknown account")
}
func (a *App) Backfill(ctx context.Context, rev, accountID string, historyMonths int) (ImportResult, error) {
a.mu.Lock()
defer a.mu.Unlock()
if historyMonths < 1 || historyMonths > 120 {
return ImportResult{}, errors.New("history_months must be an integer between 1 and 120")
}
s, err := a.snapshot(ctx)
if err != nil {
return ImportResult{}, err
}
if rev != s.Revision {
return ImportResult{}, errors.New("revision conflict: reload before importing")
}
if a.bank == nil {
return ImportResult{}, errors.New("Enable Banking is not configured")
}
index := slices.IndexFunc(s.Data.Accounts, func(account domain.Account) bool { return account.ID == accountID })
if index < 0 {
return ImportResult{}, errors.New("unknown account")
}
account := s.Data.Accounts[index]
if account.ExternalAccountID == "" {
return ImportResult{}, errors.New("account is not connected")
}
var session *banking.Session
for i := len(a.ops.Sessions) - 1; i >= 0; i-- {
saved := &a.ops.Sessions[i]
if slices.ContainsFunc(saved.Accounts, func(linked domain.Account) bool {
return linked.ID == account.ID && linked.ExternalAccountID == account.ExternalAccountID
}) {
session = saved
break
}
}
if session == nil || session.ID == "" {
return ImportResult{}, errors.New("account is not connected")
}
expiry, err := time.Parse(time.RFC3339, session.ValidUntil)
if a.ops.Consents[session.ID].NeedsReconnect || err != nil || !expiry.After(time.Now()) {
return ImportResult{}, banking.ErrReconnect
}
current, err := a.bank.Status(ctx, session.ID)
if err != nil {
if errors.Is(err, banking.ErrReconnect) {
return ImportResult{}, banking.ErrReconnect
}
return ImportResult{}, errors.New("bank connection unavailable; retry importing history")
}
expiry, err = time.Parse(time.RFC3339, current.ValidUntil)
if err != nil || !expiry.After(time.Now()) {
return ImportResult{}, banking.ErrReconnect
}
if !slices.ContainsFunc(current.Accounts, func(linked domain.Account) bool {
return linked.ExternalAccountID == account.ExternalAccountID
}) {
return ImportResult{}, banking.ErrReconnect
}
now := time.Now().UTC()
facts, err := a.bank.Transactions(ctx, account, now.AddDate(0, -historyMonths, 0).Format("2006-01-02"), now.Format("2006-01-02"))
if err != nil {
if errors.Is(err, banking.ErrReconnect) {
return ImportResult{}, banking.ErrReconnect
}
return ImportResult{}, errors.New("transaction retrieval failed; retry importing history")
}
// Use normal import processing without changing sync cursors or saved consent
// settings, including when the requested range adds no transactions.
return a.importFacts(ctx, s, facts)
}
func (a *App) Authorize(ctx context.Context, institution, country string, historyMonths int) (string, error) {
a.mu.Lock()
defer a.mu.Unlock()