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
@@ -0,0 +1,73 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Purrse.Core.DTOs;
using Purrse.Core.Interfaces.Services;
using System.Security.Claims;
namespace Purrse.Api.Controllers;
[Authorize]
[ApiController]
[Route("api/bank-sync")]
public class BankSyncController : ControllerBase
{
private readonly IBankSyncService _bankSyncService;
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
public BankSyncController(IBankSyncService bankSyncService) => _bankSyncService = bankSyncService;
[HttpGet("connections")]
public async Task<ActionResult<List<SyncConnectionResponse>>> GetConnections()
=> Ok(await _bankSyncService.GetConnectionsAsync(UserId));
[HttpGet("connections/{id}")]
public async Task<ActionResult<SyncConnectionResponse>> GetConnection(Guid id)
=> Ok(await _bankSyncService.GetConnectionAsync(UserId, id));
[HttpPut("connections/{id}")]
public async Task<ActionResult<SyncConnectionResponse>> UpdateConnection(Guid id, UpdateConnectionRequest request)
=> Ok(await _bankSyncService.UpdateConnectionAsync(UserId, id, request));
[HttpDelete("connections/{id}")]
public async Task<IActionResult> DeleteConnection(Guid id)
{
await _bankSyncService.DeleteConnectionAsync(UserId, id);
return NoContent();
}
[HttpPost("plaid/create-link-token")]
public async Task<ActionResult<CreateLinkTokenResponse>> CreatePlaidLinkToken()
=> Ok(await _bankSyncService.CreatePlaidLinkTokenAsync(UserId));
[HttpPost("plaid/complete-link")]
public async Task<ActionResult<SyncConnectionResponse>> CompletePlaidLink(CreatePlaidConnectionRequest request)
=> Ok(await _bankSyncService.CompletePlaidLinkAsync(UserId, request));
[HttpPost("simplefin/connect")]
public async Task<ActionResult<SyncConnectionResponse>> ConnectSimpleFin(CreateSimpleFinConnectionRequest request)
=> Ok(await _bankSyncService.CreateSimpleFinConnectionAsync(UserId, request));
[HttpGet("connections/{id}/discover-accounts")]
public async Task<ActionResult<List<DiscoveredAccountResponse>>> DiscoverAccounts(Guid id)
=> Ok(await _bankSyncService.DiscoverAccountsAsync(UserId, id));
[HttpPost("connections/{id}/link-account")]
public async Task<ActionResult<LinkedAccountResponse>> LinkAccount(Guid id, LinkAccountRequest request)
=> Ok(await _bankSyncService.LinkAccountAsync(UserId, id, request));
[HttpPut("linked-accounts/{id}")]
public async Task<ActionResult<LinkedAccountResponse>> UpdateLinkedAccount(Guid id, UpdateLinkedAccountRequest request)
=> Ok(await _bankSyncService.UpdateLinkedAccountAsync(UserId, id, request));
[HttpPost("connections/{id}/sync-now")]
public async Task<ActionResult<SyncResultResponse>> SyncNow(Guid id)
=> Ok(await _bankSyncService.SyncNowAsync(UserId, id));
[HttpPost("sync-all")]
public async Task<ActionResult<SyncResultResponse>> SyncAll()
=> Ok(await _bankSyncService.SyncAllAsync(UserId));
[HttpGet("sync-logs")]
public async Task<ActionResult<List<SyncLogResponse>>> GetSyncLogs([FromQuery] Guid? connectionId, [FromQuery] int limit = 50)
=> Ok(await _bankSyncService.GetSyncLogsAsync(UserId, connectionId, limit));
}