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`),
+8 -4
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)
connections.value.unshift(conn) for (const conn of newConnections) {
syncIntervals[conn.id] = conn.syncIntervalMinutes connections.value.unshift(conn)
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")]
+35 -22
View File
@@ -180,41 +180,54 @@ 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 connection = new SyncConnection var groupedByInstitution = accounts.GroupBy(a => a.Institution ?? "SimpleFIN Connection");
var connectionIds = new List<Guid>();
foreach (var group in groupedByInstitution)
{ {
Id = Guid.NewGuid(), var connection = new SyncConnection
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
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
SyncConnectionId = connection.Id, UserId = userId,
ExternalAccountId = account.ExternalId, Provider = SyncProvider.SimpleFIN,
ExternalAccountName = account.Name, InstitutionName = group.Key,
ExternalAccountType = account.AccountType, EncryptedAccessToken = encryptedAccessUrl
LastKnownBalance = account.Balance };
});
_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(); 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);