Files
sure/app/models/snaptrade_item/syncer.rb
LPW a83f70425f Add SnapTrade brokerage integration with full trade history support (#737)
* Introduce SnapTrade integration with models, migrations, views, and activity processing logic.

* Refactor SnapTrade activities processing: improve activity fetching flow, handle pending states, and update UI elements for enhanced user feedback.

* Update Brakeman ignore file to include intentional redirect for SnapTrade OAuth portal.

* Refactor SnapTrade models, views, and processing logic: add currency extraction helper, improve pending state handling, optimize migration checks, and enhance user feedback in UI.

* Remove encryption for SnapTrade `snaptrade_user_id`, as it is an identifier, not a secret.

* Introduce `SnaptradeConnectionCleanupJob` to asynchronously handle SnapTrade connection cleanup and improve i18n for SnapTrade item status messages.

* Update SnapTrade encryption: make `snaptrade_user_secret` non-deterministic to enhance security.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-01-22 20:52:49 +01:00

87 lines
3.2 KiB
Ruby

class SnaptradeItem::Syncer
include SyncStats::Collector
attr_reader :snaptrade_item
def initialize(snaptrade_item)
@snaptrade_item = snaptrade_item
end
def perform_sync(sync)
Rails.logger.info "SnaptradeItem::Syncer - Starting sync for item #{snaptrade_item.id}"
# Verify user is registered
unless snaptrade_item.user_registered?
raise StandardError, "User not registered with SnapTrade"
end
# Phase 1: Import data from SnapTrade API
sync.update!(status_text: "Importing accounts from SnapTrade...") if sync.respond_to?(:status_text)
snaptrade_item.import_latest_snaptrade_data(sync: sync)
# Phase 2: Collect setup statistics
finalize_setup_counts(sync)
# Phase 3: Process holdings and activities for linked accounts
# Preload account_provider and account to avoid N+1 queries
linked_snaptrade_accounts = snaptrade_item.linked_snaptrade_accounts.includes(account_provider: :account)
if linked_snaptrade_accounts.any?
sync.update!(status_text: "Processing holdings and activities...") if sync.respond_to?(:status_text)
snaptrade_item.process_accounts
# Phase 4: Schedule balance calculations
sync.update!(status_text: "Calculating balances...") if sync.respond_to?(:status_text)
snaptrade_item.schedule_account_syncs(
parent_sync: sync,
window_start_date: sync.window_start_date,
window_end_date: sync.window_end_date
)
# Phase 5: Collect transaction, trades, and holdings statistics
account_ids = linked_snaptrade_accounts.filter_map { |sa| sa.current_account&.id }
collect_transaction_stats(sync, account_ids: account_ids, source: "snaptrade")
collect_trades_stats(sync, account_ids: account_ids, source: "snaptrade")
collect_holdings_stats(sync, holdings_count: count_holdings, label: "processed")
end
# Mark sync health
collect_health_stats(sync, errors: nil)
rescue Provider::Snaptrade::AuthenticationError => e
snaptrade_item.update!(status: :requires_update)
collect_health_stats(sync, errors: [ { message: e.message, category: "auth_error" } ])
raise
rescue => e
collect_health_stats(sync, errors: [ { message: e.message, category: "sync_error" } ])
raise
end
# Public: called by Sync after finalization
def perform_post_sync
# no-op
end
private
def count_holdings
snaptrade_item.snaptrade_accounts.sum { |sa| Array(sa.raw_holdings_payload).size }
end
def finalize_setup_counts(sync)
sync.update!(status_text: "Checking account configuration...") if sync.respond_to?(:status_text)
total_accounts = snaptrade_item.total_accounts_count
linked_count = snaptrade_item.linked_accounts_count
unlinked_count = snaptrade_item.unlinked_accounts_count
if unlinked_count > 0
snaptrade_item.update!(pending_account_setup: true)
sync.update!(status_text: "#{unlinked_count} accounts need setup...") if sync.respond_to?(:status_text)
else
snaptrade_item.update!(pending_account_setup: false)
end
# Collect setup stats
collect_setup_stats(sync, provider_accounts: snaptrade_item.snaptrade_accounts)
end
end