Skip to content

feat: add incremental balance sync windows after provider imports#1880

Open
claytonlin1110 wants to merge 4 commits into
we-promise:mainfrom
claytonlin1110:feat/incremental-balance-sync-windows
Open

feat: add incremental balance sync windows after provider imports#1880
claytonlin1110 wants to merge 4 commits into
we-promise:mainfrom
claytonlin1110:feat/incremental-balance-sync-windows

Conversation

@claytonlin1110
Copy link
Copy Markdown
Contributor

@claytonlin1110 claytonlin1110 commented May 20, 2026

Summary

  • Add Account::BalanceSyncWindow to resolve an effective window_start_date per account after provider imports (explicit parent window, import window, entries touched during the sync, or last_synced_at - 7 days, floored at opening_anchor_date).
  • Add Account::SchedulesBalanceSyncs and wire all provider items through it so balance syncs receive the derived window instead of always passing nil.
  • Add incremental mode to Balance::ReverseCalculator and Balance::Materializer so linked accounts only recalculate balances from the window onward when a prior persisted balance exists.

Why

Daily provider syncs were recomputing full balance history on every run even when imports only changed a few days of transactions. This reduces CPU/DB work on self-hosted and managed instances without UI changes.

Test plan

  • bin/rails test test/models/account/balance_sync_window_test.rb
  • bin/rails test test/models/account/schedules_balance_syncs_test.rb
  • bin/rails test test/models/balance/reverse_calculator_test.rb
  • bin/rails test test/models/balance/materializer_test.rb
  • Run a linked-account provider sync locally and confirm account child syncs are created with a non-nil window_start_date when last_synced_at is set or entries were updated during the import
  • Confirm balances before the window are unchanged after sync (incremental path)

Summary by CodeRabbit

  • New Features

    • Incremental balance sync using derived per-account date windows
    • Unified per-account sync scheduling via a shared scheduling mechanism
  • Improvements

    • Safer window selection with fallbacks when historical data is missing
    • More targeted stale-balance purging and bounded reverse recalculation to reduce work
  • Tests

    • New tests covering window computation and incremental reverse recalculation behaviors

Review Change Stack

@superagent-security superagent-security Bot added contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis. labels May 20, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8dd85f0-096f-4451-8eae-56479397bf80

📥 Commits

Reviewing files that changed from the base of the PR and between fb05e91 and ef25a16.

📒 Files selected for processing (5)
  • app/models/balance/materializer.rb
  • app/models/coinstats_item.rb
  • app/models/plaid_item.rb
  • app/models/simplefin_item.rb
  • test/models/account/schedules_balance_syncs_test.rb
💤 Files with no reviewable changes (1)
  • app/models/coinstats_item.rb

📝 Walkthrough

Walkthrough

This PR centralizes per-account balance sync scheduling into a new concern, adds Account::BalanceSyncWindow to derive incremental windows (7‑day lookback + touched-entry signals), enables bounded incremental reverse recalculation with fallback detection, and adapts Materializer purge logic. Several provider item models adopt the new concern and small private hooks.

Changes

Incremental balance sync support

Layer / File(s) Summary
Balance sync window calculation
app/models/account/balance_sync_window.rb, test/models/account/balance_sync_window_test.rb
Account::BalanceSyncWindow.for_account computes an earliest incremental recalculation date from explicit parent/import windows, parent_sync-touched entries, and last_synced_at - LOOKBACK (7 days); clamps to account anchor/start and returns nil when no signals. Tests cover parent-window, lookback, touched-entry, anchor clamp, and nil cases.
Balance sync scheduling concern
app/models/account/schedules_balance_syncs.rb, test/models/account/schedules_balance_syncs_test.rb
Account::SchedulesBalanceSyncs centralizes scheduling via schedule_account_syncs/schedule_account_syncs_for; supports schedule_each and schedule_with_results, derives effective window with BalanceSyncWindow.for_account, and enqueues via account.sync_later. Tests assert derived window is passed to sync_later and failure handling returns per-account results when requested.
Incremental reverse calculator
app/models/balance/reverse_calculator.rb, test/models/balance/reverse_calculator_test.rb
Balance::ReverseCalculator accepts window_start_date:, resolves a bounded start via resolve_calc_start_date, falls back to full recalculation for multi-currency accounts or when no prior balance exists at window_start_date - 1, and exposes incremental? that reflects fallback and enforces call order. Tests cover both incremental and fallback flows.
Materializer incremental integration
app/models/balance/materializer.rb
Balance::Materializer now bases stale-tail purge and oldest_valid_date decisions on the calculator's incremental? capability and presence of @window_start_date; constructs Balance::ReverseCalculator with window_start_date.
Item model scheduling consolidation
app/models/binance_item.rb, app/models/brex_item.rb, app/models/coinbase_item.rb, app/models/coinstats_item.rb, app/models/enable_banking_item.rb, app/models/ibkr_item.rb, app/models/indexa_capital_item.rb, app/models/kraken_item.rb, app/models/lunchflow_item.rb, app/models/mercury_item.rb, app/models/plaid_item.rb, app/models/simplefin_item.rb, app/models/snaptrade_item.rb, app/models/sophtron_item.rb
Thirteen provider item models remove their inline schedule_account_syncs implementations and include Account::SchedulesBalanceSyncs, adding small private hooks (schedule_account_syncs_report_results?, balance_sync_accounts) as needed. SophtronItem signature updated to accept import_window_start_date and delegates scheduling to the concern.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • jjmata
  • sokie

Poem

🐰 I hop through windows, dates in a line,
Finding where balances start to realign.
A shared concern bundles each scheduled chore,
Reverse calc checks the past and then some more.
Incremental hops—precise, tidy, and fine.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add incremental balance sync windows after provider imports' directly and clearly summarizes the primary feature added: incremental balance sync windows that are computed and applied after provider imports, which is the main objective of this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfc51a48ee

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +95 to +98
if prior
Rails.logger.info("Incremental reverse sync from #{@window_start_date}, preserving balances before #{@window_start_date}")
@fell_back = false
return [ @window_start_date, account.opening_anchor_date ].max
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fall back for reverse sync unless boundary balance is enforced

When window_start_date is present, this branch enables incremental reverse calculation solely based on the existence of prior_balance, but the reverse algorithm never seeds from that prior balance or validates the boundary. In reverse mode, days before window_start_date depend on the recomputed balance at window_start_date, so any changed flow/valuation on or after the window can require updates to window_start_date - 1 and earlier. Because the materializer now preserves pre-window rows in incremental mode, linked accounts can keep stale historical balances after provider imports.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/models/coinstats_item.rb (1)

75-79: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove stale documentation from removed method.

These YARD comments (@param parent_sync, @param window_start_date, @param window_end_date, @return [Array<Hash>]) describe the removed schedule_account_syncs method, not the upsert_coinstats_snapshot! method below them.

🧹 Proposed fix to remove stale comments
-  # Queues balance sync jobs for all visible accounts.
-  # `@param` parent_sync [Sync, nil] Parent sync for tracking
-  # `@param` window_start_date [Date, nil] Start of sync window
-  # `@param` window_end_date [Date, nil] End of sync window
-  # `@return` [Array<Hash>] Results with success status per account
   # Persists raw API response for debugging and reprocessing.
   # `@param` accounts_snapshot [Hash] Raw API response data
   def upsert_coinstats_snapshot!(accounts_snapshot)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/coinstats_item.rb` around lines 75 - 79, Remove the stale YARD
param/return comments that refer to the deleted schedule_account_syncs method
and update the documentation above upsert_coinstats_snapshot! so it only
documents that method; specifically, delete the lines mentioning `@param`
parent_sync, `@param` window_start_date, `@param` window_end_date and `@return`
[Array<Hash>] and ensure any remaining comment text matches
upsert_coinstats_snapshot!'s purpose and parameters (if any).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/models/account/balance_sync_window.rb`:
- Line 16: The current early return of parent_window_start_date.to_date bypasses
applying the account-level opening-anchor floor and allows parent windows
earlier than allowed; change the logic so that when parent_window_start_date is
present you convert it to a date, then compare it with opening_anchor_floor and
return the later date (e.g., compute start = parent_window_start_date.to_date
and return [start, opening_anchor_floor].max) so the opening-anchor floor is
always enforced; modify the method containing parent_window_start_date and
opening_anchor_floor references accordingly.

---

Outside diff comments:
In `@app/models/coinstats_item.rb`:
- Around line 75-79: Remove the stale YARD param/return comments that refer to
the deleted schedule_account_syncs method and update the documentation above
upsert_coinstats_snapshot! so it only documents that method; specifically,
delete the lines mentioning `@param` parent_sync, `@param` window_start_date, `@param`
window_end_date and `@return` [Array<Hash>] and ensure any remaining comment text
matches upsert_coinstats_snapshot!'s purpose and parameters (if any).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 536c28ef-5d62-438b-83d8-2740806ce7b4

📥 Commits

Reviewing files that changed from the base of the PR and between 04ba4dd and dfc51a4.

📒 Files selected for processing (21)
  • app/models/account/balance_sync_window.rb
  • app/models/account/schedules_balance_syncs.rb
  • app/models/balance/materializer.rb
  • app/models/balance/reverse_calculator.rb
  • app/models/binance_item.rb
  • app/models/brex_item.rb
  • app/models/coinbase_item.rb
  • app/models/coinstats_item.rb
  • app/models/enable_banking_item.rb
  • app/models/ibkr_item.rb
  • app/models/indexa_capital_item.rb
  • app/models/kraken_item.rb
  • app/models/lunchflow_item.rb
  • app/models/mercury_item.rb
  • app/models/plaid_item.rb
  • app/models/simplefin_item.rb
  • app/models/snaptrade_item.rb
  • app/models/sophtron_item.rb
  • test/models/account/balance_sync_window_test.rb
  • test/models/account/schedules_balance_syncs_test.rb
  • test/models/balance/reverse_calculator_test.rb

# @param last_synced_at [Time, nil] Provider item freshness timestamp
# @return [Date, nil] nil means full balance recalculation
def for_account(account, parent_sync: nil, parent_window_start_date: nil, import_window_start_date: nil, last_synced_at: nil)
return parent_window_start_date.to_date if parent_window_start_date.present?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Apply opening-anchor floor even for explicit parent windows.

Returning early here bypasses the floor in Lines 26-27, so an explicit parent window can go earlier than the account’s valid floor. That breaks the incremental-window contract and can trigger unnecessary backfill recalculation.

Proposed fix
-      return parent_window_start_date.to_date if parent_window_start_date.present?
+      candidates = []
+      candidates << parent_window_start_date.to_date if parent_window_start_date.present?
 
-      candidates = []
       candidates << import_window_start_date.to_date if import_window_start_date.present?
       candidates << entries_touched_since(parent_sync, account) if parent_sync
       candidates << (last_synced_at.to_date - LOOKBACK) if last_synced_at.present?
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return parent_window_start_date.to_date if parent_window_start_date.present?
candidates = []
candidates << parent_window_start_date.to_date if parent_window_start_date.present?
candidates << import_window_start_date.to_date if import_window_start_date.present?
candidates << entries_touched_since(parent_sync, account) if parent_sync
candidates << (last_synced_at.to_date - LOOKBACK) if last_synced_at.present?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/account/balance_sync_window.rb` at line 16, The current early
return of parent_window_start_date.to_date bypasses applying the account-level
opening-anchor floor and allows parent windows earlier than allowed; change the
logic so that when parent_window_start_date is present you convert it to a date,
then compare it with opening_anchor_floor and return the later date (e.g.,
compute start = parent_window_start_date.to_date and return [start,
opening_anchor_floor].max) so the opening-anchor floor is always enforced;
modify the method containing parent_window_start_date and opening_anchor_floor
references accordingly.

@claytonlin1110
Copy link
Copy Markdown
Contributor Author

@jjmata what do you think about this improve?

Copy link
Copy Markdown
Collaborator

jjmata commented May 21, 2026

Large refactor with a good motivation. A few concerns worth addressing before merge:

1. Behavior change for PlaidItem and SimplefinItem. The previous schedule_account_syncs on both iterated accounts.each (no visibility filter). The new SchedulesBalanceSyncs concern calls balance_sync_accountsaccts.visible if available. If plaid_item.accounts responds to .visible, this silently changes behavior — disabled or pending-deletion Plaid/SimpleFIN accounts would no longer get balance syncs. Is that intentional? IbkrItem and SnaptradeItem explicitly override balance_sync_accounts to filter out disabled accounts, but PlaidItem and SimplefinItem do not. Please verify the intended account set is unchanged for those two.

2. Orphaned comment in CoinstatsItem. After removing schedule_account_syncs, there's a dangling Yard comment block left at the call site:

  # @param window_start_date [Date, nil] Start of sync window
  # @param window_end_date [Date, nil] End of sync window
  # @return [Array<Hash>] Results with success status per account
  # Persists raw API response...

That comment now documents nothing — it should be removed.

3. ReverseCalculator#incremental? call order assumption. The method raises if called before calculate (when @fell_back is still nil). The Materializer calls calculator.incremental? inside purge_stale_balances — confirm that purge_stale_balances is always invoked after calculate in all code paths. If purge_stale_balances can be called on an uninitialized calculator, this raises in production rather than degrading gracefully.

4. SchedulesBalanceSyncs test coverage for error path. The schedule_with_results method has a rescue => e branch that logs and continues. There's no test asserting that a per-account sync failure doesn't abort the remaining accounts. A test for this would add meaningful confidence.


Generated by Claude Code

@claytonlin1110
Copy link
Copy Markdown
Contributor Author

@jjmata updated addressed issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants