From a2b12d9abddd7c9761839982c8681c1b76927784 Mon Sep 17 00:00:00 2001 From: Catherine Renelle <32781029+catrenelle@users.noreply.github.com> Date: Sun, 8 Feb 2026 23:26:49 -0500 Subject: [PATCH] Split SimpleFIN connections by institution Group discovered SimpleFIN accounts by institution and create a separate SyncConnection per institution so each bank appears as its own card in the UI. Co-Authored-By: Claude Opus 4.6 --- frontend/src/services/bankSync.ts | 2 +- frontend/src/views/settings/SettingsView.vue | 12 ++-- .../Controllers/BankSyncController.cs | 2 +- src/Purrse.Api/Services/BankSyncService.cs | 57 ++++++++++++------- .../Interfaces/Services/IBankSyncService.cs | 2 +- 5 files changed, 46 insertions(+), 29 deletions(-) diff --git a/frontend/src/services/bankSync.ts b/frontend/src/services/bankSync.ts index de179f1..62cc1de 100644 --- a/frontend/src/services/bankSync.ts +++ b/frontend/src/services/bankSync.ts @@ -35,7 +35,7 @@ export const bankSyncApi = { api.post('/bank-sync/plaid/complete-link', { publicToken }), connectSimpleFin: (setupToken: string) => - api.post('/bank-sync/simplefin/connect', { setupToken }), + api.post('/bank-sync/simplefin/connect', { setupToken }), discoverAccounts: (connectionId: string) => api.get(`/bank-sync/connections/${connectionId}/discover-accounts`), diff --git a/frontend/src/views/settings/SettingsView.vue b/frontend/src/views/settings/SettingsView.vue index 8cd6ffb..8911081 100644 --- a/frontend/src/views/settings/SettingsView.vue +++ b/frontend/src/views/settings/SettingsView.vue @@ -728,12 +728,16 @@ function loadPlaidScript(): Promise { async function connectSimpleFin() { simpleFinLoading.value = true try { - const { data: conn } = await bankSyncApi.connectSimpleFin(simpleFinToken.value) - connections.value.unshift(conn) - syncIntervals[conn.id] = conn.syncIntervalMinutes + const { data: newConnections } = await bankSyncApi.connectSimpleFin(simpleFinToken.value) + for (const conn of newConnections) { + connections.value.unshift(conn) + syncIntervals[conn.id] = conn.syncIntervalMinutes + } showSimpleFinDialog.value = false simpleFinToken.value = '' - openMappingDialog(conn) + if (newConnections.length > 0) { + openMappingDialog(newConnections[0]) + } } catch (err: any) { errorMessage.value = err.response?.data?.error || 'Failed to connect via SimpleFIN. Check your setup token.' showError.value = true diff --git a/src/Purrse.Api/Controllers/BankSyncController.cs b/src/Purrse.Api/Controllers/BankSyncController.cs index d624133..0f1e364 100644 --- a/src/Purrse.Api/Controllers/BankSyncController.cs +++ b/src/Purrse.Api/Controllers/BankSyncController.cs @@ -59,7 +59,7 @@ public class BankSyncController : ControllerBase => Ok(await _bankSyncService.CompletePlaidLinkAsync(UserId, request)); [HttpPost("simplefin/connect")] - public async Task> ConnectSimpleFin(CreateSimpleFinConnectionRequest request) + public async Task>> ConnectSimpleFin(CreateSimpleFinConnectionRequest request) => Ok(await _bankSyncService.CreateSimpleFinConnectionAsync(UserId, request)); [HttpGet("connections/{id}/discover-accounts")] diff --git a/src/Purrse.Api/Services/BankSyncService.cs b/src/Purrse.Api/Services/BankSyncService.cs index 285e1ba..16fbc7f 100644 --- a/src/Purrse.Api/Services/BankSyncService.cs +++ b/src/Purrse.Api/Services/BankSyncService.cs @@ -180,41 +180,54 @@ public class BankSyncService : IBankSyncService // --- SimpleFIN Flow --- - public async Task CreateSimpleFinConnectionAsync(Guid userId, CreateSimpleFinConnectionRequest request) + public async Task> CreateSimpleFinConnectionAsync(Guid userId, CreateSimpleFinConnectionRequest request) { var accessUrl = await _simpleFin.ExchangeSetupTokenAsync(request.SetupToken); var config = new Dictionary { ["AccessUrl"] = accessUrl }; var accounts = await _simpleFin.GetAccountsAsync(config); - var institutionName = accounts.FirstOrDefault()?.Institution ?? "SimpleFIN Connection"; + var encryptedAccessUrl = _encryption.Encrypt(accessUrl); - var connection = new SyncConnection + var groupedByInstitution = accounts.GroupBy(a => a.Institution ?? "SimpleFIN Connection"); + var connectionIds = new List(); + + foreach (var group in groupedByInstitution) { - Id = Guid.NewGuid(), - UserId = userId, - Provider = SyncProvider.SimpleFIN, - InstitutionName = institutionName, - EncryptedAccessToken = _encryption.Encrypt(accessUrl) - }; - - _db.SyncConnections.Add(connection); - - foreach (var account in accounts) - { - _db.LinkedAccounts.Add(new LinkedAccount + var connection = new SyncConnection { Id = Guid.NewGuid(), - SyncConnectionId = connection.Id, - ExternalAccountId = account.ExternalId, - ExternalAccountName = account.Name, - ExternalAccountType = account.AccountType, - LastKnownBalance = account.Balance - }); + UserId = userId, + Provider = SyncProvider.SimpleFIN, + InstitutionName = group.Key, + EncryptedAccessToken = encryptedAccessUrl + }; + + _db.SyncConnections.Add(connection); + connectionIds.Add(connection.Id); + + foreach (var account in group) + { + _db.LinkedAccounts.Add(new LinkedAccount + { + Id = Guid.NewGuid(), + SyncConnectionId = connection.Id, + ExternalAccountId = account.ExternalId, + ExternalAccountName = account.Name, + ExternalAccountType = account.AccountType, + LastKnownBalance = account.Balance + }); + } } await _db.SaveChangesAsync(); - return await GetConnectionAsync(userId, connection.Id); + var results = new List(); + foreach (var id in connectionIds) + { + results.Add(await GetConnectionAsync(userId, id)); + } + + return results; } // --- Account Linking --- diff --git a/src/Purrse.Core/Interfaces/Services/IBankSyncService.cs b/src/Purrse.Core/Interfaces/Services/IBankSyncService.cs index e18edcf..73a44cd 100644 --- a/src/Purrse.Core/Interfaces/Services/IBankSyncService.cs +++ b/src/Purrse.Core/Interfaces/Services/IBankSyncService.cs @@ -19,7 +19,7 @@ public interface IBankSyncService Task CompletePlaidLinkAsync(Guid userId, CreatePlaidConnectionRequest request); // SimpleFIN flow - Task CreateSimpleFinConnectionAsync(Guid userId, CreateSimpleFinConnectionRequest request); + Task> CreateSimpleFinConnectionAsync(Guid userId, CreateSimpleFinConnectionRequest request); // Account linking Task> DiscoverAccountsAsync(Guid userId, Guid connectionId);