Private
Public Access
1
0

Fix SimpleFIN re-sync timeout and 403 rate limiting

Cache SimpleFIN API responses for 60 seconds so multiple accounts in
the same sync cycle reuse a single HTTP call instead of hitting the
API per-account. Pre-load existing transactions by FitId in one batch
query to skip per-duplicate FindAsync calls during memo backfill.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-09 21:42:55 -05:00
parent f23bba53c0
commit 0562bb97b7
2 changed files with 65 additions and 14 deletions
+37 -12
View File
@@ -413,6 +413,17 @@ public class BankSyncService : IBankSyncService
var accountResults = new List<SyncAccountResult>();
int totalFetched = 0, totalImported = 0, totalSkipped = 0;
// For SimpleFIN, use the earliest start date across all accounts so the
// single API call (cached) returns transactions covering every account.
var endDate = DateTime.UtcNow;
DateTime? simpleFinStartDate = null;
if (connection.Provider == SyncProvider.SimpleFIN)
{
simpleFinStartDate = enabledAccounts
.Select(la => la.LastSyncAt ?? endDate.AddDays(-90))
.Min();
}
foreach (var linkedAccount in enabledAccounts)
{
var log = new SyncLog
@@ -427,8 +438,7 @@ public class BankSyncService : IBankSyncService
try
{
var endDate = DateTime.UtcNow;
var startDate = linkedAccount.LastSyncAt ?? endDate.AddDays(-90);
var startDate = simpleFinStartDate ?? linkedAccount.LastSyncAt ?? endDate.AddDays(-90);
var provider = connection.Provider == SyncProvider.Plaid
? (Purrse.Plugins.Abstractions.IAccountSyncProvider)_plaid
@@ -454,24 +464,39 @@ public class BankSyncService : IBankSyncService
int imported = 0;
int skipped = 0;
// Pre-load existing transactions for this account that have FitIds matching
// incoming data, so we can batch-update memos without per-row FindAsync calls
var incomingFitIds = parseResult.Transactions
.Where(t => !string.IsNullOrEmpty(t.FitId))
.Select(t => t.FitId!)
.ToHashSet();
var existingByFitId = incomingFitIds.Count > 0
? await _db.Transactions
.Where(t => t.AccountId == linkedAccount.AccountId!.Value
&& t.FitId != null && incomingFitIds.Contains(t.FitId))
.ToDictionaryAsync(t => t.FitId!, t => t)
: new Dictionary<string, Transaction>();
foreach (var parsed in parseResult.Transactions)
{
// Fast-path: if we already have a transaction with this FitId, skip
// and backfill memo if needed — no duplicate detection query required
if (!string.IsNullOrEmpty(parsed.FitId) && existingByFitId.TryGetValue(parsed.FitId, out var existingTx))
{
if (!string.IsNullOrEmpty(parsed.Memo) && string.IsNullOrEmpty(existingTx.Memo))
{
existingTx.Memo = parsed.Memo;
}
skipped++;
continue;
}
var duplicate = await _duplicateService.FindDuplicateAsync(
linkedAccount.AccountId!.Value, parsed.Date, parsed.Amount,
parsed.PayeeName, parsed.FitId, parsed.CheckNumber);
if (duplicate != null)
{
// Update memo if the existing transaction has no memo
if (!string.IsNullOrEmpty(parsed.Memo))
{
var existing = await _db.Transactions.FindAsync(duplicate.ExistingTransactionId);
if (existing != null && string.IsNullOrEmpty(existing.Memo))
{
existing.Memo = parsed.Memo;
}
}
skipped++;
continue;
}