Files
sure/app/models/ibkr_item.rb
William Wei Ming f210d1ca4c Perf/accounts controller index optimization (#1926)
* fix Prism issue - assigned but unused variable plaid_item

* measurement to improve accounts_controller__index performance based on skylight

* fix FEEDBACK - Stop eager-loading full sync histories on accounts index

* fix FEEDBACK - Remove sticky memoization from SimplefinItem#accounts

* fix lint error in PR review

* fix FEEDBACK - Avoid returning map misses as authoritative results

* fix test error

* fix FEEDBACK - Avoid redundant query by using already-loaded @manual_accounts

* Address maintainer review on accounts index sync preloading
Memoize SimplefinItem#accounts, align syncing? with Sync#visible?,
and add fallback regression tests.

* Add composite index for sync DISTINCT ON queries
Support latest_by_syncable ordering with syncable_type, syncable_id,
created_at DESC, and id DESC.

* fix error in dockerfile.preview file

* Suppress Pipelock false positives on CI database fixtures.
Pipelock 2.7.0 full-repo audit flags ephemeral postgres:// URLs in
workflow env blocks. Re-apply inline suppressions lost in the main merge.

* Address review: guard partial Current sync maps and drop unrelated diff
Add key? checks so partially populated Current sync maps fall back to DB
queries. Revert Dockerfile.preview and pipelock.yml changes unrelated to
the accounts index N+1 optimization.

* fix(sync): fully populate Current sync maps for all preloaded syncables

* fix(ci): skip scheduled preview cleanup on forks
Only run the hourly Cloudflare preview cleanup on we-promise/sure,
where the required secrets exist.

* Remove schema.rb Postgres-version churn, keep only new syncs index

Reset db/schema.rb to upstream/main and re-add only the
index_syncs_on_syncable_and_created_at_and_id index. The prior diff
included ~30 lines of noise (check-constraint reformatting, virtual
column reformat, and column reorderings) caused by dumping under a
different PostgreSQL version, which obscured the single intended
schema change and invited merge conflicts.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-25 03:35:35 +02:00

125 lines
3.8 KiB
Ruby

class IbkrItem < ApplicationRecord
include Syncable, Provided, Unlinking, Encryptable
enum :status, { good: "good", requires_update: "requires_update" }, default: :good
if encryption_ready?
encrypts :query_id, deterministic: true
encrypts :token
encrypts :raw_payload
end
belongs_to :family
has_one_attached :logo, dependent: :purge_later
has_many :ibkr_accounts, dependent: :destroy
validates :name, presence: true
validates :query_id, presence: true, on: :create
validates :token, presence: true, on: :create
scope :active, -> { where(scheduled_for_deletion: false) }
scope :syncable, -> { active.where.not(query_id: [ nil, "" ]).where.not(token: nil) }
scope :ordered, -> { order(created_at: :desc) }
scope :needs_update, -> { where(status: :requires_update) }
def destroy_later
update!(scheduled_for_deletion: true)
DestroyJob.perform_later(self)
end
def credentials_configured?
query_id.present? && token.present?
end
def import_latest_ibkr_data
provider = ibkr_provider
raise StandardError, "IBKR provider is not configured" unless provider
IbkrItem::Importer.new(self, ibkr_provider: provider).import
rescue => e
Rails.logger.error("IbkrItem #{id} - Failed to import data: #{e.message}")
raise
end
def process_accounts
return [] if ibkr_accounts.empty?
linked_ibkr_accounts.includes(account_provider: :account).each_with_object([]) do |ibkr_account, results|
account = ibkr_account.current_account
next unless account
next if account.pending_deletion? || account.disabled?
begin
result = IbkrAccount::Processor.new(ibkr_account).process
results << { ibkr_account_id: ibkr_account.id, success: true, result: result }
rescue => e
Rails.logger.error("IbkrItem #{id} - Failed to process account #{ibkr_account.id}: #{e.message}")
results << { ibkr_account_id: ibkr_account.id, success: false, error: e.message }
end
end
end
def schedule_account_syncs(parent_sync: nil, window_start_date: nil, window_end_date: nil)
accounts.reject { |account| account.pending_deletion? || account.disabled? }.each_with_object([]) do |account, results|
begin
account.sync_later(
parent_sync: parent_sync,
window_start_date: window_start_date,
window_end_date: window_end_date
)
results << { account_id: account.id, success: true }
rescue => e
Rails.logger.error("IbkrItem #{id} - Failed to schedule sync for account #{account.id}: #{e.message}")
results << { account_id: account.id, success: false, error: e.message }
end
end
end
def upsert_ibkr_snapshot!(payload)
update!(raw_payload: payload, status: :good)
end
def accounts
@accounts ||= ibkr_accounts.includes(account_provider: :account).filter_map(&:current_account).uniq
end
def linked_ibkr_accounts
ibkr_accounts.joins(:account_provider)
end
def linked_accounts_count
ibkr_accounts.joins(:account_provider).count
end
def unlinked_accounts_count
ibkr_accounts.left_joins(:account_provider).where(account_providers: { id: nil }).count
end
def total_accounts_count
ibkr_accounts.count
end
def has_completed_initial_setup?
accounts.any?
end
def sync_status_summary
total_accounts = total_accounts_count
linked_count = linked_accounts_count
unlinked_count = unlinked_accounts_count
if total_accounts.zero?
I18n.t("ibkr_items.sync_status.no_accounts")
elsif unlinked_count.zero?
I18n.t("ibkr_items.sync_status.all_linked", count: linked_count)
else
I18n.t("ibkr_items.sync_status.partial", linked: linked_count, unlinked: unlinked_count)
end
end
def institution_display_name
I18n.t("ibkr_items.defaults.name")
end
end