Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/UI/account/activity_date.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<span class="font-medium"><%= end_balance_money.format %></span>
<span class="font-medium privacy-sensitive"><%= end_balance_money.format %></span>
<%= render DS::Tooltip.new(text: "The end of day balance, after all transactions and adjustments", placement: "left", size: "sm") %>
</div>
<%= helpers.icon "chevron-down", class: "group-open:rotate-180" %>
Expand Down
4 changes: 3 additions & 1 deletion app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def index
@mercury_items = visible_provider_items(family.mercury_items.ordered.includes(:syncs, :mercury_accounts))
@coinbase_items = visible_provider_items(family.coinbase_items.ordered.includes(:coinbase_accounts, :accounts, :syncs))
@snaptrade_items = visible_provider_items(family.snaptrade_items.ordered.includes(:syncs, :snaptrade_accounts))
@ibkr_items = visible_provider_items(family.ibkr_items.ordered.includes(:syncs, :ibkr_accounts))
@indexa_capital_items = visible_provider_items(family.indexa_capital_items.ordered.includes(:syncs, :indexa_capital_accounts))
@sophtron_items = visible_provider_items(family.sophtron_items.ordered.includes(:syncs, :sophtron_accounts))

Expand Down Expand Up @@ -47,13 +48,14 @@ def show
@chart_view = params[:chart_view] || "balance"
@tab = params[:tab]
@q = params.fetch(:q, {}).permit(:search, status: [])
entries = @account.entries.where(excluded: false).search(@q).reverse_chronological
entries = @account.entries.where(excluded: false).search(@q).reverse_chronological.includes(:entryable)
Comment thread
gian-reto marked this conversation as resolved.

@pagy, @entries = pagy(
entries,
limit: safe_per_page,
params: request.query_parameters.except("tab").merge("tab" => "activity")
)
Transaction::ActivitySecurityPreloader.new(@entries).preload
Comment thread
gian-reto marked this conversation as resolved.

@activity_feed_data = Account::ActivityFeedData.new(@account, @entries)
end
Expand Down
236 changes: 236 additions & 0 deletions app/controllers/ibkr_items_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
6 changes: 6 additions & 0 deletions app/controllers/settings/providers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def reload_provider_configs(updated_fields)
{ key: "binance", title: "Binance", turbo_id: "binance", partial: "binance_panel" },
{ key: "kraken", title: "Kraken", turbo_id: "kraken", partial: "kraken_panel" },
{ key: "snaptrade", title: "SnapTrade", turbo_id: "snaptrade", partial: "snaptrade_panel", auto_open: "manage" },
{ key: "ibkr", title: "Interactive Brokers", turbo_id: "ibkr", partial: "ibkr_panel" },
{ key: "indexa_capital", title: "Indexa Capital", turbo_id: "indexa_capital", partial: "indexa_capital_panel" },
{ key: "sophtron", title: "Sophtron", turbo_id: "sophtron", partial: "sophtron_panel" }
].freeze
Expand All @@ -208,6 +209,7 @@ def reload_provider_configs(updated_fields)
"binance" => "BinanceItem",
"kraken" => "KrakenItem",
"snaptrade" => "SnaptradeItem",
"ibkr" => "IbkrItem",
"indexa_capital" => "IndexaCapitalItem",
"sophtron" => "SophtronItem"
}.freeze
Expand All @@ -232,6 +234,8 @@ def load_provider_items(provider_key)
@kraken_items = Current.family.kraken_items.active.ordered
when "snaptrade"
@snaptrade_items = Current.family.snaptrade_items.includes(:snaptrade_accounts).ordered
when "ibkr"
@ibkr_items = Current.family.ibkr_items.ordered
when "indexa_capital"
@indexa_capital_items = Current.family.indexa_capital_items.ordered
when "sophtron"
Expand All @@ -257,6 +261,7 @@ def prepare_show_context
@mercury_items = Current.family.mercury_items.active.ordered
@coinbase_items = Current.family.coinbase_items.ordered # Coinbase panel needs name and sync info for status display
@snaptrade_items = Current.family.snaptrade_items.ordered
@ibkr_items = Current.family.ibkr_items.ordered.select(:id)
@indexa_capital_items = Current.family.indexa_capital_items.ordered.select(:id)
@binance_items = Current.family.binance_items.active.ordered
@kraken_items = Current.family.kraken_items.active.ordered
Expand Down Expand Up @@ -286,6 +291,7 @@ def family_panel_items
"binance" => @binance_items,
"kraken" => @kraken_items,
"snaptrade" => @snaptrade_items,
"ibkr" => @ibkr_items,
"indexa_capital" => @indexa_capital_items,
"sophtron" => @sophtron_items
}
Expand Down
1 change: 1 addition & 0 deletions app/controllers/transactions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def index
)

@pagy, @transactions = pagy(base_scope, limit: safe_per_page)
Transaction::ActivitySecurityPreloader.new(@transactions).preload
Comment thread
gian-reto marked this conversation as resolved.

# Preload split parent data
entry_ids = @transactions.map { |t| t.entry.id }
Expand Down
3 changes: 3 additions & 0 deletions app/helpers/settings_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ def provider_summary(provider_key)
return { status: :warn, meta: t("settings.providers.meta.registration_needed") }
end
sync_based_summary(key)
when "ibkr"
return { status: :off } unless @ibkr_items&.any?
sync_based_summary(key)
when "indexa_capital"
return { status: :off } unless @indexa_capital_items&.any?
sync_based_summary(key)
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/controllers/time_series_chart_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export default class extends Controller {
.append("div")
.attr(
"class",
"bg-container text-sm font-sans absolute p-2 border border-secondary rounded-lg pointer-events-none opacity-0 top-0",
"bg-container text-sm font-sans absolute p-2 border border-secondary rounded-lg pointer-events-none opacity-0 top-0 privacy-sensitive",
);
}

Expand Down
Loading
Loading