Report failed bank connections and stop resurrecting deleted accounts
Three defects made new connections silently vanish while removed accounts returned: - A single shared account the journal cannot represent (securities or card entries without IBAN, stable identification or currency) aborted the entire consent. Usable accounts are now linked and the rest counted and reported. - A consent that linked nothing was stored, redirected as success and later reaped by session recovery. It now fails with the reason. - Callback failures rendered a bare JSON error page and were never logged. They now log and redirect into the app with the reason shown. - Deleting an account left its session binding, so the next connect or sync recovered the binding and re-added the account. Account deletion now releases bindings, consents and cursors before committing.
This commit is contained in:
+25
-14
@@ -249,29 +249,40 @@ func connectAccounts(d *domain.Dataset, session *banking.Session, reconnect bool
|
||||
}
|
||||
}
|
||||
}
|
||||
func (a *App) Callback(ctx context.Context, code, state string) error {
|
||||
|
||||
// Callback completes a bank authorization and returns how many accounts the
|
||||
// bank shared that could not be linked to a journal account.
|
||||
func (a *App) Callback(ctx context.Context, code, state string) (int, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
auth, ok := a.authStates[state]
|
||||
delete(a.authStates, state)
|
||||
if !ok || time.Now().After(auth.Expires) {
|
||||
return errors.New("authorization state expired or invalid; reconnect again")
|
||||
return 0, errors.New("authorization state expired or invalid; reconnect again")
|
||||
}
|
||||
if a.bank == nil || code == "" {
|
||||
return errors.New("authorization did not provide a code")
|
||||
return 0, errors.New("authorization did not provide a code")
|
||||
}
|
||||
session, err := a.bank.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
// A consent without linkable accounts can never sync and its stored
|
||||
// session would be reaped silently. Fail visibly instead.
|
||||
if len(session.Accounts) == 0 {
|
||||
if session.Unlinkable > 0 {
|
||||
return 0, fmt.Errorf("the bank shared %d account(s), but none could be linked: they lack an IBAN or stable identification, or use an unsupported currency", session.Unlinkable)
|
||||
}
|
||||
return 0, errors.New("the bank authorized the connection but shared no accounts, so nothing was linked; accounts of another type (for example business) may need a separate consent")
|
||||
}
|
||||
a.ops.Sessions = append(a.ops.Sessions, session)
|
||||
a.ops.Consents[session.ID] = Consent{Institution: auth.Institution, Country: auth.Country, HistoryMonths: auth.HistoryMonths}
|
||||
if err = a.saveOps(); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
s, err := a.snapshot(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
connectAccounts(&s.Data, &session, true)
|
||||
// Remove superseded account bindings, not unrelated bank consents.
|
||||
@@ -295,16 +306,16 @@ func (a *App) Callback(ctx context.Context, code, state string) error {
|
||||
// Save once-only provider details before the canonical commit. Sync can recover
|
||||
// the account bindings if a crash or external edit interrupts that commit.
|
||||
if err = a.saveOps(); err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
_, err = a.commit(ctx, s.Revision, s.Data)
|
||||
if err == nil {
|
||||
select {
|
||||
case a.syncRequested <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
if _, err = a.commit(ctx, s.Revision, s.Data); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return err
|
||||
select {
|
||||
case a.syncRequested <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return session.Unlinkable, nil
|
||||
}
|
||||
func (a *App) Balances(ctx context.Context, id string) ([]banking.Balance, error) {
|
||||
a.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user