mirror of
https://github.com/we-promise/sure.git
synced 2026-04-20 04:24:06 +00:00
Simplefin liabilities recording fix (#410)
* Add tests and logic for Simplefin account balance normalization - Introduced `SimplefinAccountProcessorTest` to verify balance normalization logic. - Updated `SimplefinAccount::Processor` to invert negative balances for liability accounts (credit cards and loans) while keeping asset balances unchanged. - Added comments to clarify balance conventions and sign normalization rules. * Refactor balances-only sync logic and improve tests for edge cases - Updated `SimplefinItem::Importer` and `SimplefinItem::Syncer` to ensure `last_synced_at` remains nil during balances-only runs, preserving chunked-history behavior for full syncs. - Introduced additional comments to clarify balances-only implications and syncing logic. - Added test case in `SimplefinAccountProcessorTest` to verify correct handling of overpayment for credit card liabilities. - Refined balance normalization in `SimplefinAccount::Processor` to always invert liability balances for consistency. --------- Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
This commit is contained in:
@@ -16,8 +16,14 @@ class SimplefinItem::Importer
|
||||
Rails.logger.info "SimplefinItem::Importer - sync_start_date: #{simplefin_item.sync_start_date.inspect}"
|
||||
|
||||
begin
|
||||
if simplefin_item.last_synced_at.nil?
|
||||
# First sync - use chunked approach to get full history
|
||||
# Defensive guard: If last_synced_at is set but there are linked accounts
|
||||
# with no transactions captured yet (typical after a balances-only run),
|
||||
# force the first full run to use chunked history to backfill.
|
||||
linked_accounts = simplefin_item.simplefin_accounts.joins(:account)
|
||||
no_txns_yet = linked_accounts.any? && linked_accounts.all? { |sfa| sfa.raw_transactions_payload.blank? }
|
||||
|
||||
if simplefin_item.last_synced_at.nil? || no_txns_yet
|
||||
# First sync (or balances-only pre-run) — use chunked approach to get full history
|
||||
Rails.logger.info "SimplefinItem::Importer - Using chunked history import"
|
||||
import_with_chunked_history
|
||||
else
|
||||
@@ -211,9 +217,15 @@ class SimplefinItem::Importer
|
||||
max_requests = 22
|
||||
current_end_date = Time.current
|
||||
|
||||
# Use user-selected sync_start_date if available, otherwise use default lookback
|
||||
# Decide how far back to walk:
|
||||
# - If the user set a custom sync_start_date, honor it
|
||||
# - Else, for first-time chunked history, walk back up to the provider-safe
|
||||
# limit implied by chunking so we actually import meaningful history.
|
||||
# We do NOT use the small initial lookback (7 days) here, because that
|
||||
# would clip the very first chunk to ~1 week and prevent further history.
|
||||
user_start_date = simplefin_item.sync_start_date
|
||||
default_start_date = initial_sync_lookback_period.days.ago
|
||||
implied_max_lookback_days = chunk_size_days * max_requests
|
||||
default_start_date = implied_max_lookback_days.days.ago
|
||||
target_start_date = user_start_date ? user_start_date.beginning_of_day : default_start_date
|
||||
|
||||
# Enforce maximum 3-year lookback to respect SimpleFin's actual 60-day limit per request
|
||||
@@ -698,7 +710,9 @@ class SimplefinItem::Importer
|
||||
end
|
||||
|
||||
def initial_sync_lookback_period
|
||||
# Default to 7 days for initial sync to avoid API limits
|
||||
# Default to 7 days for initial sync. Providers that support deeper
|
||||
# history will supply it via chunked fetches, and users can optionally
|
||||
# set a custom `sync_start_date` to go further back.
|
||||
7
|
||||
end
|
||||
|
||||
|
||||
@@ -6,16 +6,38 @@ class SimplefinItem::Syncer
|
||||
end
|
||||
|
||||
def perform_sync(sync)
|
||||
# If no accounts are linked yet, run a balances-only discovery pass so the user
|
||||
# can review and manually link accounts first. This mirrors the historical flow
|
||||
# users expect: initial 7-day balances snapshot, then full chunked history after linking.
|
||||
begin
|
||||
if simplefin_item.simplefin_accounts.joins(:account).count == 0
|
||||
sync.update!(status_text: "Discovering accounts (balances only)...") if sync.respond_to?(:status_text)
|
||||
# Pre-mark the sync as balances_only so downstream completion code does not
|
||||
# bump last_synced_at. The importer also sets this flag, but setting it here
|
||||
# guarantees the guard is present even if the importer exits early.
|
||||
if sync.respond_to?(:sync_stats)
|
||||
existing = (sync.sync_stats || {})
|
||||
sync.update_columns(sync_stats: existing.merge("balances_only" => true))
|
||||
end
|
||||
SimplefinItem::Importer.new(simplefin_item, simplefin_provider: simplefin_item.simplefin_provider, sync: sync).import_balances_only
|
||||
finalize_setup_counts(sync)
|
||||
mark_completed(sync)
|
||||
return
|
||||
end
|
||||
rescue => e
|
||||
# If discovery-only path errors, fall back to regular logic below so we don't block syncs entirely
|
||||
Rails.logger.warn("SimplefinItem::Syncer auto balances-only path failed: #{e.class} - #{e.message}")
|
||||
end
|
||||
|
||||
# Balances-only fast path
|
||||
if sync.respond_to?(:sync_stats) && (sync.sync_stats || {})["balances_only"]
|
||||
sync.update!(status_text: "Refreshing balances only...") if sync.respond_to?(:status_text)
|
||||
begin
|
||||
# Use the Importer to run balances-only path
|
||||
SimplefinItem::Importer.new(simplefin_item, simplefin_provider: simplefin_item.simplefin_provider, sync: sync).import_balances_only
|
||||
# Update last_synced_at for UI freshness if the column exists
|
||||
if simplefin_item.has_attribute?(:last_synced_at)
|
||||
simplefin_item.update!(last_synced_at: Time.current)
|
||||
end
|
||||
# IMPORTANT: Do NOT update last_synced_at during balances-only runs.
|
||||
# Leaving last_synced_at nil ensures the next full sync uses the
|
||||
# chunked-history path to fetch full historical transactions.
|
||||
finalize_setup_counts(sync)
|
||||
mark_completed(sync)
|
||||
rescue => e
|
||||
|
||||
Reference in New Issue
Block a user