Files
finance-duck/internal/app/consent.go
T
Lars Nolden c33e8d5573 Authorize consents for the account-holder type the bank supports
Kontist authorized but shared no accounts: psu_type was hardcoded to
personal, and Enable Banking documents that a psu_type mismatch can
yield a consent without the expected accounts. The bank listing now
reports each institution's supported psu_types, the connect form offers
only those, the chosen type reaches POST /auth, and an unsupported
combination is refused before the user is sent to a bank. The choice is
stored per consent so reconnecting reuses it; consents predating the
choice stay personal.

Also repairs the frontend derivation, which the Montserrat dependency
broke: npmDepsHash was stale and web/public was missing from the
fileset, so the traced duck icon never reached the built assets.
2026-09-11 13:39:45 +02:00

106 lines
3.0 KiB
Go

package app
import (
"slices"
"time"
"finance-duck/internal/banking"
"finance-duck/internal/domain"
)
const defaultHistoryMonths = 12
type authorization struct {
Expires time.Time
Institution string
Country string
PSUType string
HistoryMonths int
}
type Consent struct {
Institution string `json:"institution"`
Country string `json:"country"`
// PSUType records the account-holder kind this consent was authorized for
// so reconnecting reuses it: a business account authorized as personal
// shares no accounts.
PSUType string `json:"psu_type,omitempty"`
HistoryMonths int `json:"history_months"`
Error string `json:"error,omitempty"`
NeedsReconnect bool `json:"needs_reconnect"`
}
type Connection struct {
AccountID string `json:"account_id"`
Institution string `json:"institution"`
Country string `json:"country"`
PSUType string `json:"psu_type"`
HistoryMonths int `json:"history_months"`
Status string `json:"status"`
ValidUntil string `json:"valid_until"`
Error string `json:"error"`
}
// psuType keeps legacy consents, which predate the choice, on the personal
// flow they were originally authorized with.
func (c Consent) psuType() string {
if !banking.ValidPSUType(c.PSUType) {
return banking.PSUPersonal
}
return c.PSUType
}
func (c Consent) historyMonths() int {
if c.HistoryMonths == 0 {
return defaultHistoryMonths
}
return c.HistoryMonths
}
func (a *App) connections(d domain.Dataset) []Connection {
out := make([]Connection, 0, len(d.Accounts))
for _, account := range d.Accounts {
c := Connection{AccountID: account.ID, Institution: account.Institution, Country: "DE", PSUType: banking.PSUPersonal, HistoryMonths: defaultHistoryMonths, Status: "local"}
if account.ExternalAccountID != "" {
c.Status = "reconnect_required"
c.Error = "No saved bank consent; reconnect this account"
}
for _, session := range a.ops.Sessions {
for _, linked := range session.Accounts {
if linked.ID != account.ID {
continue
}
meta := a.ops.Consents[session.ID]
c.HistoryMonths = meta.historyMonths()
c.PSUType = meta.psuType()
if meta.Institution != "" {
c.Institution = meta.Institution
}
if meta.Country != "" {
c.Country = meta.Country
}
c.ValidUntil = session.ValidUntil
c.Error = meta.Error
c.Status = "connected"
expiry, err := time.Parse(time.RFC3339, session.ValidUntil)
if meta.NeedsReconnect || err != nil || !expiry.After(time.Now()) {
c.Status = "reconnect_required"
if c.Error == "" {
c.Error = "Bank consent expired; reconnect to resume automatic imports"
}
} else if meta.Error != "" {
c.Status = "error"
}
}
}
out = append(out, c)
}
return out
}
func copySessions(sessions []banking.Session) []banking.Session {
out := append([]banking.Session{}, sessions...)
for i := range out {
out[i].Accounts = slices.Clone(out[i].Accounts)
}
return out
}