* fix(enable-banking): reliable initial backfill, no more silent ~30-day windows (#443) PSD2 first-sync was a compound bug: the cron runs once daily so users got no data for up to 24h after activation; the "Sync now" button defaulted to 30 days and set last_synced_at, permanently locking the cron into 7-day incremental mode and discarding the 90-day backfill window. ASPSPs also truncate history below requested ranges, but the discrepancy was only logged. This change: - Runs the initial backfill inline when the user finishes account selection (PATCH /accounts), so data is available the moment they finish onboarding. - Tracks initial_sync_completed_at separately from last_synced_at; the cron now gates first-sync 90-day window on that, so manual syncs no longer clobber the backfill path. - Surfaces the actual returned date range to the UI ("Initial historik: X → Y (begärde Z)") with a warning when the bank truncated history. - Defaults manual /sync to 90 days (was 30) — matches user intent. - AccountPickerDialog uses SpeedLedger's SIE-anchor pattern when an SIE import covers prior periods (auto-defaults lookback to "day after last SIE entry"), with Bokio-style PSD2 disclosure on the standard path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(enable-banking): address review feedback on PR #486 Two fixes from review: 1. Memoise the browser Supabase client in AccountPickerDialog. createClient() in the component body returned a new reference every render, and `supabase` was in the SIE-fetch effect's dep array — every checkbox tick or parent re-render re-fired the SIE-imports query. 2. Drop `accounts_data` from the second supabase update inside the activation backfill. The first update already wrote it; including it here races with any concurrent writer (e.g. cron firing in the sub-60s window) and would silently overwrite. Only initial_sync_* metadata + last_synced_at need to be persisted in the second update. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(enable-banking): check metadata-update error after inline backfill The second supabase.update() inside the activation backfill block didn't check its error return. Supabase client methods don't throw on DB errors; they return { data, error }. If the metadata write failed (network blip, RLS quirk, etc.), the handler still populated initialSyncSummary and returned success — UI saw "imported N transactions" while the DB had initial_sync_completed_at = NULL, causing the cron to schedule another full 90-day backfill the next morning. Capture { error } from the metadata update. On failure, surface as initial_sync_error with a metadata_update_failed: prefix and skip the initialSyncSummary population. The cron's gate (initial_sync_completed_at IS NULL) still self-heals on the next run; this just keeps the UI honest about which path got us there. New test stub: SupabaseStub.updateErrorByCall lets a test succeed the first update and fail the second. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5eee06a56d
commit
04f902fe8f
@@ -131,7 +131,10 @@ export const GET = withCronContext('cron.bank_sync', async (_request, ctx) => {
|
||||
|
||||
const toDate = new Date().toISOString().split('T')[0]
|
||||
// First sync: 90-day lookback (PSD2 max). Subsequent: 7-day window.
|
||||
const isFirstSync = !connection.last_synced_at
|
||||
// Gate on initial_sync_completed_at, not last_synced_at — manual "Sync now"
|
||||
// sets last_synced_at without doing the deep backfill, and we want the cron
|
||||
// to still fall back to 90 days if the inline activation backfill failed.
|
||||
const isFirstSync = !connection.initial_sync_completed_at
|
||||
const lookbackDays = isFirstSync ? 90 : 7
|
||||
if (isFirstSync) {
|
||||
ctx.log.info('first sync for connection — using 90-day lookback', {
|
||||
@@ -218,11 +221,27 @@ export const GET = withCronContext('cron.bank_sync', async (_request, ctx) => {
|
||||
|
||||
// Successful sync: update connection and clear any previous error state.
|
||||
// Write allAccounts (not accounts) so disabled accounts stay in the row.
|
||||
const completedAt = new Date().toISOString()
|
||||
let initialSyncFields: Record<string, unknown> = {}
|
||||
if (isFirstSync) {
|
||||
// Aggregate returned booking dates across enabled accounts so the UI
|
||||
// can show "we requested X but the bank returned Y to Z".
|
||||
const minDates = syncResults.map(r => r.returnedMinBookingDate).filter((d): d is string => !!d)
|
||||
const maxDates = syncResults.map(r => r.returnedMaxBookingDate).filter((d): d is string => !!d)
|
||||
initialSyncFields = {
|
||||
initial_sync_completed_at: completedAt,
|
||||
initial_sync_requested_from: fromDate,
|
||||
initial_sync_returned_min_date: minDates.length > 0 ? minDates.reduce((a, b) => (a < b ? a : b)) : null,
|
||||
initial_sync_returned_max_date: maxDates.length > 0 ? maxDates.reduce((a, b) => (a > b ? a : b)) : null,
|
||||
initial_sync_lookback_days: lookbackDays,
|
||||
}
|
||||
}
|
||||
await supabase
|
||||
.from('bank_connections')
|
||||
.update({
|
||||
accounts_data: allAccounts,
|
||||
last_synced_at: new Date().toISOString(),
|
||||
last_synced_at: completedAt,
|
||||
...initialSyncFields,
|
||||
...(connection.error_message ? { error_message: null } : {}),
|
||||
})
|
||||
.eq('id', connection.id)
|
||||
|
||||
Reference in New Issue
Block a user