Add Re-sync button for SimpleFIN connections
Resets LastSyncAt on all linked accounts so the next sync
re-fetches the full 90-day transaction window. Duplicate
detection prevents re-importing existing transactions.
- POST /api/bank-sync/connections/{id}/reset-sync endpoint
- Re-sync button shown only on SimpleFIN connection cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,9 @@ export const bankSyncApi = {
|
|||||||
updateLinkedAccount: (linkedAccountId: string, data: { accountId: string | null; isEnabled: boolean }) =>
|
updateLinkedAccount: (linkedAccountId: string, data: { accountId: string | null; isEnabled: boolean }) =>
|
||||||
api.put<LinkedAccountInfo>(`/bank-sync/linked-accounts/${linkedAccountId}`, data),
|
api.put<LinkedAccountInfo>(`/bank-sync/linked-accounts/${linkedAccountId}`, data),
|
||||||
|
|
||||||
|
resetSync: (connectionId: string) =>
|
||||||
|
api.post(`/bank-sync/connections/${connectionId}/reset-sync`),
|
||||||
|
|
||||||
syncNow: (connectionId: string) =>
|
syncNow: (connectionId: string) =>
|
||||||
api.post(`/bank-sync/connections/${connectionId}/sync-now`),
|
api.post(`/bank-sync/connections/${connectionId}/sync-now`),
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,16 @@
|
|||||||
>
|
>
|
||||||
Sync Now
|
Sync Now
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-if="conn.provider === 'SimpleFIN'"
|
||||||
|
variant="tonal"
|
||||||
|
color="secondary"
|
||||||
|
prepend-icon="mdi-refresh"
|
||||||
|
:loading="resyncingConnections[conn.id]"
|
||||||
|
@click="resyncFromScratch(conn)"
|
||||||
|
>
|
||||||
|
Re-sync
|
||||||
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
:color="conn.isActive ? 'warning' : 'success'"
|
:color="conn.isActive ? 'warning' : 'success'"
|
||||||
@@ -530,6 +540,7 @@ const connections = ref<SyncConnection[]>([])
|
|||||||
const accounts = ref<Account[]>([])
|
const accounts = ref<Account[]>([])
|
||||||
const syncIntervals = reactive<Record<string, number>>({})
|
const syncIntervals = reactive<Record<string, number>>({})
|
||||||
const syncingConnections = reactive<Record<string, boolean>>({})
|
const syncingConnections = reactive<Record<string, boolean>>({})
|
||||||
|
const resyncingConnections = reactive<Record<string, boolean>>({})
|
||||||
|
|
||||||
// Plaid credentials
|
// Plaid credentials
|
||||||
const plaidCredentials = ref<PlaidCredentials>({ isConfigured: false, environment: 'sandbox' })
|
const plaidCredentials = ref<PlaidCredentials>({ isConfigured: false, environment: 'sandbox' })
|
||||||
@@ -814,6 +825,20 @@ async function syncNow(conn: SyncConnection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resyncFromScratch(conn: SyncConnection) {
|
||||||
|
resyncingConnections[conn.id] = true
|
||||||
|
try {
|
||||||
|
await bankSyncApi.resetSync(conn.id)
|
||||||
|
await bankSyncApi.syncNow(conn.id)
|
||||||
|
syncingConnections[conn.id] = true
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMessage.value = err.response?.data?.error || 'Re-sync failed'
|
||||||
|
showError.value = true
|
||||||
|
} finally {
|
||||||
|
resyncingConnections[conn.id] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle sync results via SignalR notification
|
// Handle sync results via SignalR notification
|
||||||
const { notifications } = useNotifications()
|
const { notifications } = useNotifications()
|
||||||
watch(notifications, (notifs) => {
|
watch(notifications, (notifs) => {
|
||||||
|
|||||||
@@ -74,6 +74,13 @@ public class BankSyncController : ControllerBase
|
|||||||
public async Task<ActionResult<LinkedAccountResponse>> UpdateLinkedAccount(Guid id, UpdateLinkedAccountRequest request)
|
public async Task<ActionResult<LinkedAccountResponse>> UpdateLinkedAccount(Guid id, UpdateLinkedAccountRequest request)
|
||||||
=> Ok(await _bankSyncService.UpdateLinkedAccountAsync(UserId, id, request));
|
=> Ok(await _bankSyncService.UpdateLinkedAccountAsync(UserId, id, request));
|
||||||
|
|
||||||
|
[HttpPost("connections/{id}/reset-sync")]
|
||||||
|
public async Task<IActionResult> ResetSync(Guid id)
|
||||||
|
{
|
||||||
|
await _bankSyncService.ResetSyncDatesAsync(UserId, id);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("connections/{id}/sync-now")]
|
[HttpPost("connections/{id}/sync-now")]
|
||||||
public IActionResult SyncNow(Guid id)
|
public IActionResult SyncNow(Guid id)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -311,6 +311,23 @@ public class BankSyncService : IBankSyncService
|
|||||||
|
|
||||||
// --- Sync ---
|
// --- Sync ---
|
||||||
|
|
||||||
|
public async Task ResetSyncDatesAsync(Guid userId, Guid connectionId)
|
||||||
|
{
|
||||||
|
var connection = await _db.SyncConnections
|
||||||
|
.Include(c => c.LinkedAccounts)
|
||||||
|
.FirstOrDefaultAsync(c => c.Id == connectionId && c.UserId == userId)
|
||||||
|
?? throw new KeyNotFoundException("Connection not found");
|
||||||
|
|
||||||
|
foreach (var la in connection.LinkedAccounts)
|
||||||
|
{
|
||||||
|
la.LastSyncAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.LastSyncAt = null;
|
||||||
|
connection.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<SyncResultResponse> SyncNowAsync(Guid userId, Guid connectionId)
|
public async Task<SyncResultResponse> SyncNowAsync(Guid userId, Guid connectionId)
|
||||||
{
|
{
|
||||||
var connection = await _db.SyncConnections
|
var connection = await _db.SyncConnections
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public interface IBankSyncService
|
|||||||
|
|
||||||
// Sync
|
// Sync
|
||||||
Task<SyncResultResponse> SyncNowAsync(Guid userId, Guid connectionId);
|
Task<SyncResultResponse> SyncNowAsync(Guid userId, Guid connectionId);
|
||||||
|
Task ResetSyncDatesAsync(Guid userId, Guid connectionId);
|
||||||
Task<SyncResultResponse> SyncAllAsync(Guid userId);
|
Task<SyncResultResponse> SyncAllAsync(Guid userId);
|
||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
|
|||||||
Reference in New Issue
Block a user