Files
sure/app/controllers/ibkr_items_controller.rb
Gian-Reto Tarnutzer ce5d7dd736 Add Interactive Brokers Provider (#1722)
* Display multi-currency holdings correctly

* Implement IBKR provider

* Fix: Use historical exchange rate for historical prices

* Add brokerage exchange rate for trades

* Sync historical balances from IBKR

* Add logos in activity history

* Fix privacy mode blur in account view

* Improve IBKR XML Flex report parser errors
2026-05-12 23:45:19 +02:00

237 lines
7.8 KiB
Ruby

class IbkrItemsController < ApplicationController
before_action :set_ibkr_item, only: [ :update, :destroy, :sync, :setup_accounts, :complete_account_setup ]
before_action :require_admin!, only: [ :create, :select_accounts, :select_existing_account, :link_existing_account, :update, :destroy, :sync, :setup_accounts, :complete_account_setup ]
def create
@ibkr_item = Current.family.ibkr_items.build(ibkr_item_params)
@ibkr_item.name ||= t("ibkr_items.defaults.name")
if @ibkr_item.save
@ibkr_item.sync_later
if turbo_frame_request?
flash.now[:notice] = t(".success")
render turbo_stream: [
turbo_stream.replace(
"ibkr-providers-panel",
partial: "settings/providers/ibkr_panel"
),
*flash_notification_stream_items
]
else
redirect_to accounts_path, notice: t(".success"), status: :see_other
end
else
@error_message = @ibkr_item.errors.full_messages.join(", ")
if turbo_frame_request?
render turbo_stream: turbo_stream.replace(
"ibkr-providers-panel",
partial: "settings/providers/ibkr_panel",
locals: { error_message: @error_message }
), status: :unprocessable_entity
else
redirect_to settings_providers_path, alert: @error_message, status: :see_other
end
end
end
def update
attrs = ibkr_item_params.to_h
attrs["query_id"] = @ibkr_item.query_id if attrs["query_id"].blank?
attrs["token"] = @ibkr_item.token if attrs["token"].blank?
if @ibkr_item.update(attrs.merge(status: :good))
@ibkr_item.sync_later unless @ibkr_item.syncing?
if turbo_frame_request?
flash.now[:notice] = t(".success")
render turbo_stream: [
turbo_stream.replace(
"ibkr-providers-panel",
partial: "settings/providers/ibkr_panel"
),
*flash_notification_stream_items
]
else
redirect_to accounts_path, notice: t(".success"), status: :see_other
end
else
@error_message = @ibkr_item.errors.full_messages.join(", ")
if turbo_frame_request?
render turbo_stream: turbo_stream.replace(
"ibkr-providers-panel",
partial: "settings/providers/ibkr_panel",
locals: { error_message: @error_message }
), status: :unprocessable_entity
else
redirect_to settings_providers_path, alert: @error_message, status: :see_other
end
end
end
def destroy
begin
@ibkr_item.unlink_all!(dry_run: false)
rescue => e
Rails.logger.warn("IBKR unlink during destroy failed: #{e.class} - #{e.message}")
end
@ibkr_item.destroy_later
redirect_to settings_providers_path, notice: t(".success"), status: :see_other
end
def sync
@ibkr_item.sync_later unless @ibkr_item.syncing?
respond_to do |format|
format.html { redirect_back_or_to accounts_path }
format.json { head :ok }
end
end
def select_accounts
ibkr_item = current_ibkr_item
unless ibkr_item
redirect_to settings_providers_path, alert: t(".not_configured")
return
end
redirect_to setup_accounts_ibkr_item_path(ibkr_item)
end
def select_existing_account
@account = Current.family.accounts.find(params[:account_id])
@available_ibkr_accounts = Current.family.ibkr_items
.includes(ibkr_accounts: { account_provider: :account })
.flat_map(&:ibkr_accounts)
.select { |ibkr_account| ibkr_account.account_provider.nil? }
.sort_by { |ibkr_account| ibkr_account.updated_at || ibkr_account.created_at }
.reverse
render :select_existing_account, layout: false
end
def link_existing_account
account = Current.family.accounts.find_by(id: params[:account_id])
ibkr_account = Current.family.ibkr_items
.joins(:ibkr_accounts)
.where(ibkr_accounts: { id: params[:ibkr_account_id] })
.first
&.ibkr_accounts
&.find_by(id: params[:ibkr_account_id])
if account.blank? || ibkr_account.blank?
redirect_to settings_providers_path, alert: t(".not_found")
return
end
if account.accountable_type != "Investment" || account.account_providers.any? || account.plaid_account_id.present? || account.simplefin_account_id.present?
redirect_to account_path(account), alert: t(".only_manual_investment")
return
end
provider = nil
ibkr_account.with_lock do
if ibkr_account.current_account.present?
redirect_to account_path(account), alert: t(".already_linked")
return
end
provider = ibkr_account.ensure_account_provider!(account)
end
raise "Failed to create AccountProvider link" unless provider
begin
IbkrAccount::Processor.new(ibkr_account.reload).process
rescue => e
Rails.logger.error("Failed to process linked IBKR account #{ibkr_account.id}: #{e.class} - #{e.message}")
end
ibkr_account.ibkr_item.sync_later unless ibkr_account.ibkr_item.syncing?
redirect_to account_path(account), notice: t(".success"), status: :see_other
rescue => e
Rails.logger.error("Failed to link existing IBKR account: #{e.class} - #{e.message}")
redirect_to settings_providers_path, alert: t(".failed"), status: :see_other
end
def setup_accounts
@ibkr_accounts = @ibkr_item.ibkr_accounts.includes(account_provider: :account)
@linked_accounts = @ibkr_accounts.select { |ibkr_account| ibkr_account.current_account.present? }
@unlinked_accounts = @ibkr_accounts.reject { |ibkr_account| ibkr_account.current_account.present? }
no_accounts = @linked_accounts.blank? && @unlinked_accounts.blank?
latest_sync = @ibkr_item.syncs.ordered.first
should_sync = latest_sync.nil? || !latest_sync.completed?
if no_accounts && !@ibkr_item.syncing? && should_sync
@ibkr_item.sync_later
end
@linkable_accounts = Current.family.accounts
.visible
.where(accountable_type: "Investment")
.left_joins(:account_providers)
.where(account_providers: { id: nil })
.order(:name)
@syncing = @ibkr_item.syncing?
@waiting_for_sync = no_accounts && @syncing
@no_accounts_found = no_accounts && !@syncing && @ibkr_item.last_synced_at.present?
end
def complete_account_setup
selected_accounts = Array(params[:account_ids]).reject(&:blank?)
created_accounts = []
selected_accounts.each do |ibkr_account_id|
ibkr_account = @ibkr_item.ibkr_accounts.find_by(id: ibkr_account_id)
next unless ibkr_account
ibkr_account.with_lock do
next if ibkr_account.current_account.present?
account = Account.create_from_ibkr_account(ibkr_account)
ibkr_account.ensure_account_provider!(account)
created_accounts << account
end
begin
IbkrAccount::Processor.new(ibkr_account.reload).process
rescue => e
Rails.logger.error("Failed to process IBKR account #{ibkr_account.id} after setup: #{e.class} - #{e.message}")
end
end
@ibkr_item.update!(pending_account_setup: @ibkr_item.unlinked_accounts_count.positive?)
@ibkr_item.sync_later if created_accounts.any?
if created_accounts.any?
redirect_to accounts_path, notice: t(".success", count: created_accounts.count), status: :see_other
elsif selected_accounts.empty?
redirect_to setup_accounts_ibkr_item_path(@ibkr_item), alert: t(".none_selected"), status: :see_other
else
redirect_to setup_accounts_ibkr_item_path(@ibkr_item), alert: t(".none_created"), status: :see_other
end
end
private
def set_ibkr_item
@ibkr_item = Current.family.ibkr_items.find(params[:id])
end
def current_ibkr_item
active_items = Current.family.ibkr_items.active
active_items.syncable.ordered.first || active_items.ordered.first
end
def ibkr_item_params
params.require(:ibkr_item).permit(:name, :query_id, :token)
end
end