Private
Public Access
1
0

Implement Phase 6: automated bank sync (Plaid + SimpleFIN) & settings

Add automated transaction syncing via Plaid and SimpleFIN providers,
a Settings page for managing connections and account mappings,
background scheduled syncing, and sync history logging.

New backend: BankSync plugin project with PlaidSyncProvider and
SimpleFinSyncProvider, SyncConnection/LinkedAccount/SyncLog models,
AES-256 encryption service, BankSyncService orchestration,
BankSyncBackgroundService (15-min tick), and BankSyncController
(13 endpoints). EF migration creates sync_connections,
linked_accounts, and sync_logs tables.

New frontend: Settings view with 3 tabs (Profile, Bank Connections,
Sync History), bankSync API service, BankSyncComplete SignalR
notifications, and Settings nav item in sidebar + avatar dropdown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 15:08:23 -05:00
parent eb49147080
commit 3e0446c9be
34 changed files with 3956 additions and 8 deletions
+1
View File
@@ -25,4 +25,5 @@ public class Account
public ICollection<Transaction> Transactions { get; set; } = new List<Transaction>();
public ICollection<InvestmentHolding> InvestmentHoldings { get; set; } = new List<InvestmentHolding>();
public ICollection<Reconciliation> Reconciliations { get; set; } = new List<Reconciliation>();
public ICollection<LinkedAccount> LinkedAccounts { get; set; } = new List<LinkedAccount>();
}
+19
View File
@@ -0,0 +1,19 @@
namespace Purrse.Core.Models;
public class LinkedAccount
{
public Guid Id { get; set; }
public Guid SyncConnectionId { get; set; }
public Guid? AccountId { get; set; }
public string ExternalAccountId { get; set; } = string.Empty;
public string ExternalAccountName { get; set; } = string.Empty;
public string? ExternalAccountType { get; set; }
public string? ExternalAccountMask { get; set; }
public decimal? LastKnownBalance { get; set; }
public bool IsEnabled { get; set; } = true;
public DateTime? LastSyncAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public SyncConnection SyncConnection { get; set; } = null!;
public Account? Account { get; set; }
}
+24
View File
@@ -0,0 +1,24 @@
using Purrse.Core.Enums;
namespace Purrse.Core.Models;
public class SyncConnection
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public SyncProvider Provider { get; set; }
public string InstitutionName { get; set; } = string.Empty;
public string? InstitutionId { get; set; }
public string EncryptedAccessToken { get; set; } = string.Empty;
public string? PlaidItemId { get; set; }
public bool IsActive { get; set; } = true;
public int SyncIntervalMinutes { get; set; } = 360;
public DateTime? LastSyncAt { get; set; }
public string? LastSyncError { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public User User { get; set; } = null!;
public ICollection<LinkedAccount> LinkedAccounts { get; set; } = new List<LinkedAccount>();
public ICollection<SyncLog> SyncLogs { get; set; } = new List<SyncLog>();
}
+21
View File
@@ -0,0 +1,21 @@
using Purrse.Core.Enums;
namespace Purrse.Core.Models;
public class SyncLog
{
public Guid Id { get; set; }
public Guid SyncConnectionId { get; set; }
public Guid? LinkedAccountId { get; set; }
public SyncStatus Status { get; set; }
public int TransactionsFetched { get; set; }
public int TransactionsImported { get; set; }
public int TransactionsSkipped { get; set; }
public string? ErrorMessage { get; set; }
public DateTime StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public TimeSpan? Duration { get; set; }
public SyncConnection SyncConnection { get; set; } = null!;
public LinkedAccount? LinkedAccount { get; set; }
}
+1
View File
@@ -12,4 +12,5 @@ public class User
public DateTime? RefreshTokenExpiresAt { get; set; }
public ICollection<Account> Accounts { get; set; } = new List<Account>();
public ICollection<SyncConnection> SyncConnections { get; set; } = new List<SyncConnection>();
}