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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user