Files
sure/app/models/account/syncer.rb
Serge L 5a43f123c2 feat(balance): Incremental ForwardCalculator — only recalculate from changed date forward (#1151)
* feat(balance): incremental ForwardCalculator — only recalculate from changed date forward

When a Sync record carries a window_start_date, ForwardCalculator now
seeds its starting balances from the persisted DB balance for
window_start_date - 1, then iterates only from window_start_date to
calc_end_date.  This avoids recomputing every daily balance on a
long-lived account when a single transaction changes.

Key changes:
- Account::Syncer passes sync.window_start_date to Balance::Materializer
- Balance::Materializer accepts window_start_date and forwards it to
  ForwardCalculator; purge_stale_balances uses opening_anchor_date as the
  lower bound in incremental mode so pre-window balances are not deleted
- Balance::ForwardCalculator accepts window_start_date; resolve_starting_balances
  loads end_cash_balance/end_non_cash_balance from the prior DB record and
  falls back to full recalculation when no prior record exists
- Tests added for incremental correctness, fallback behaviour, and purge safety

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

# Conflicts:
#	app/models/balance/materializer.rb

* Enhance fallback logic on ForwardCalculator and Materializer

* fix(balance): address CodeRabbit review issues on incremental ForwardCalculator

- materializer.rb: handle empty sorted_balances in incremental mode by still
  purging stale tail balances beyond window_start_date - 1, preventing orphaned
  future rows when a transaction is deleted and the recalc window produces no rows

- materializer_test.rb: stub incremental? alongside calculate in the incremental
  sync test so the guard in ForwardCalculator#incremental? doesn't raise when
  @fell_back is nil (never set because calculate was stubbed out)

- materializer_test.rb: correct window_start_date in the fallback test from
  3.days.ago to 2.days.ago so window_start_date - 1 hits a date with no
  persisted balance, correctly triggering full recalculation instead of
  accidentally seeding from the stale wrong_pre_window balance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(balance): multi-currency fallback to full recalculation and add corresponding tests

* address coderabbit comment about test

* Make the foreign-currency precondition explicit in the test setup.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:29:01 +01:00

38 lines
1.3 KiB
Ruby

class Account::Syncer
attr_reader :account
def initialize(account)
@account = account
end
def perform_sync(sync)
Rails.logger.info("Processing balances (#{account.linked? ? 'reverse' : 'forward'})")
import_market_data
materialize_balances(window_start_date: sync.window_start_date)
end
def perform_post_sync
account.family.auto_match_transfers!
end
private
def materialize_balances(window_start_date: nil)
strategy = account.linked? ? :reverse : :forward
Balance::Materializer.new(account, strategy: strategy, window_start_date: window_start_date).materialize_balances
end
# Syncs all the exchange rates + security prices this account needs to display historical chart data
#
# This is a *supplemental* sync. The daily market data sync should have already populated
# a majority or all of this data, so this is often a no-op.
#
# We rescue errors here because if this operation fails, we don't want to fail the entire sync since
# we have reasonable fallbacks for missing market data.
def import_market_data
Account::MarketDataImporter.new(account).import_all
rescue => e
Rails.logger.error("Error syncing market data for account #{account.id}: #{e.message}")
Sentry.capture_exception(e)
end
end