Private
Public Access
1
0

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 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-08 23:26:49 -05:00
parent 54a15cf3cf
commit a2b12d9abd
5 changed files with 46 additions and 29 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ export const bankSyncApi = {
api.post<SyncConnection>('/bank-sync/plaid/complete-link', { publicToken }), api.post<SyncConnection>('/bank-sync/plaid/complete-link', { publicToken }),
connectSimpleFin: (setupToken: string) => connectSimpleFin: (setupToken: string) =>
api.post<SyncConnection>('/bank-sync/simplefin/connect', { setupToken }), api.post<SyncConnection[]>('/bank-sync/simplefin/connect', { setupToken }),
discoverAccounts: (connectionId: string) => discoverAccounts: (connectionId: string) =>
api.get<DiscoveredAccount[]>(`/bank-sync/connections/${connectionId}/discover-accounts`), api.get<DiscoveredAccount[]>(`/bank-sync/connections/${connectionId}/discover-accounts`),
+6 -2
View File
@@ -728,12 +728,16 @@ function loadPlaidScript(): Promise<void> {
async function connectSimpleFin() { async function connectSimpleFin() {
simpleFinLoading.value = true simpleFinLoading.value = true
try { try {
const { data: conn } = await bankSyncApi.connectSimpleFin(simpleFinToken.value) const { data: newConnections } = await bankSyncApi.connectSimpleFin(simpleFinToken.value)
for (const conn of newConnections) {
connections.value.unshift(conn) connections.value.unshift(conn)
syncIntervals[conn.id] = conn.syncIntervalMinutes syncIntervals[conn.id] = conn.syncIntervalMinutes
}
showSimpleFinDialog.value = false showSimpleFinDialog.value = false
simpleFinToken.value = '' simpleFinToken.value = ''
openMappingDialog(conn) if (newConnections.length > 0) {
openMappingDialog(newConnections[0])
}
} catch (err: any) { } catch (err: any) {
errorMessage.value = err.response?.data?.error || 'Failed to connect via SimpleFIN. Check your setup token.' errorMessage.value = err.response?.data?.error || 'Failed to connect via SimpleFIN. Check your setup token.'
showError.value = true showError.value = true
@@ -59,7 +59,7 @@ public class BankSyncController : ControllerBase
=> Ok(await _bankSyncService.CompletePlaidLinkAsync(UserId, request)); => Ok(await _bankSyncService.CompletePlaidLinkAsync(UserId, request));
[HttpPost("simplefin/connect")] [HttpPost("simplefin/connect")]
public async Task<ActionResult<SyncConnectionResponse>> ConnectSimpleFin(CreateSimpleFinConnectionRequest request) public async Task<ActionResult<List<SyncConnectionResponse>>> ConnectSimpleFin(CreateSimpleFinConnectionRequest request)
=> Ok(await _bankSyncService.CreateSimpleFinConnectionAsync(UserId, request)); => Ok(await _bankSyncService.CreateSimpleFinConnectionAsync(UserId, request));
[HttpGet("connections/{id}/discover-accounts")] [HttpGet("connections/{id}/discover-accounts")]
+19 -6
View File
@@ -180,26 +180,32 @@ public class BankSyncService : IBankSyncService
// --- SimpleFIN Flow --- // --- SimpleFIN Flow ---
public async Task<SyncConnectionResponse> CreateSimpleFinConnectionAsync(Guid userId, CreateSimpleFinConnectionRequest request) public async Task<List<SyncConnectionResponse>> CreateSimpleFinConnectionAsync(Guid userId, CreateSimpleFinConnectionRequest request)
{ {
var accessUrl = await _simpleFin.ExchangeSetupTokenAsync(request.SetupToken); var accessUrl = await _simpleFin.ExchangeSetupTokenAsync(request.SetupToken);
var config = new Dictionary<string, string> { ["AccessUrl"] = accessUrl }; var config = new Dictionary<string, string> { ["AccessUrl"] = accessUrl };
var accounts = await _simpleFin.GetAccountsAsync(config); var accounts = await _simpleFin.GetAccountsAsync(config);
var institutionName = accounts.FirstOrDefault()?.Institution ?? "SimpleFIN Connection"; var encryptedAccessUrl = _encryption.Encrypt(accessUrl);
var groupedByInstitution = accounts.GroupBy(a => a.Institution ?? "SimpleFIN Connection");
var connectionIds = new List<Guid>();
foreach (var group in groupedByInstitution)
{
var connection = new SyncConnection var connection = new SyncConnection
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
UserId = userId, UserId = userId,
Provider = SyncProvider.SimpleFIN, Provider = SyncProvider.SimpleFIN,
InstitutionName = institutionName, InstitutionName = group.Key,
EncryptedAccessToken = _encryption.Encrypt(accessUrl) EncryptedAccessToken = encryptedAccessUrl
}; };
_db.SyncConnections.Add(connection); _db.SyncConnections.Add(connection);
connectionIds.Add(connection.Id);
foreach (var account in accounts) foreach (var account in group)
{ {
_db.LinkedAccounts.Add(new LinkedAccount _db.LinkedAccounts.Add(new LinkedAccount
{ {
@@ -211,10 +217,17 @@ public class BankSyncService : IBankSyncService
LastKnownBalance = account.Balance LastKnownBalance = account.Balance
}); });
} }
}
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
return await GetConnectionAsync(userId, connection.Id); var results = new List<SyncConnectionResponse>();
foreach (var id in connectionIds)
{
results.Add(await GetConnectionAsync(userId, id));
}
return results;
} }
// --- Account Linking --- // --- Account Linking ---
@@ -19,7 +19,7 @@ public interface IBankSyncService
Task<SyncConnectionResponse> CompletePlaidLinkAsync(Guid userId, CreatePlaidConnectionRequest request); Task<SyncConnectionResponse> CompletePlaidLinkAsync(Guid userId, CreatePlaidConnectionRequest request);
// SimpleFIN flow // SimpleFIN flow
Task<SyncConnectionResponse> CreateSimpleFinConnectionAsync(Guid userId, CreateSimpleFinConnectionRequest request); Task<List<SyncConnectionResponse>> CreateSimpleFinConnectionAsync(Guid userId, CreateSimpleFinConnectionRequest request);
// Account linking // Account linking
Task<List<DiscoveredAccountResponse>> DiscoverAccountsAsync(Guid userId, Guid connectionId); Task<List<DiscoveredAccountResponse>> DiscoverAccountsAsync(Guid userId, Guid connectionId);