# frozen_string_literal: true class BinanceItemsController < ApplicationController before_action :set_binance_item, only: [ :show, :edit, :update, :destroy, :sync, :setup_accounts, :complete_account_setup ] before_action :require_admin!, only: [ :new, :create, :select_accounts, :link_accounts, :select_existing_account, :link_existing_account, :edit, :update, :destroy, :sync, :setup_accounts, :complete_account_setup ] def index @binance_items = Current.family.binance_items.ordered end def show end def new @binance_item = Current.family.binance_items.build end def edit end def create @binance_item = Current.family.binance_items.build(binance_item_params) @binance_item.name ||= t(".default_name") if @binance_item.save @binance_item.set_binance_institution_defaults! @binance_item.sync_later if turbo_frame_request? flash.now[:notice] = t(".success") @binance_items = Current.family.binance_items.ordered render turbo_stream: [ turbo_stream.update( "binance-providers-panel", partial: "settings/providers/binance_panel", locals: { binance_items: @binance_items } ), *flash_notification_stream_items ] else redirect_to settings_providers_path, notice: t(".success"), status: :see_other end else @error_message = @binance_item.errors.full_messages.join(", ") if turbo_frame_request? render turbo_stream: turbo_stream.replace( "binance-providers-panel", partial: "settings/providers/binance_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 if @binance_item.update(binance_item_params) if turbo_frame_request? flash.now[:notice] = t(".success") @binance_items = Current.family.binance_items.ordered render turbo_stream: [ turbo_stream.update( "binance-providers-panel", partial: "settings/providers/binance_panel", locals: { binance_items: @binance_items } ), *flash_notification_stream_items ] else redirect_to settings_providers_path, notice: t(".success"), status: :see_other end else @error_message = @binance_item.errors.full_messages.join(", ") if turbo_frame_request? render turbo_stream: turbo_stream.replace( "binance-providers-panel", partial: "settings/providers/binance_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 @binance_item.destroy_later redirect_to settings_providers_path, notice: t(".success") end def sync unless @binance_item.syncing? @binance_item.sync_later end respond_to do |format| format.html { redirect_back_or_to accounts_path } format.json { head :ok } end end def select_accounts redirect_to settings_providers_path end def link_accounts redirect_to settings_providers_path end def select_existing_account @account = Current.family.accounts.find(params[:account_id]) @available_binance_accounts = Current.family.binance_items .includes(binance_accounts: [ :account, { account_provider: :account } ]) .flat_map(&:binance_accounts) .select { |ba| ba.account.present? || ba.account_provider.nil? } .sort_by { |ba| ba.updated_at || ba.created_at } .reverse render :select_existing_account, layout: false end def link_existing_account @account = Current.family.accounts.find(params[:account_id]) binance_account = BinanceAccount .joins(:binance_item) .where(id: params[:binance_account_id], binance_items: { family_id: Current.family.id }) .first unless binance_account alert_msg = t(".errors.invalid_binance_account") if turbo_frame_request? flash.now[:alert] = alert_msg render turbo_stream: Array(flash_notification_stream_items) else redirect_to account_path(@account), alert: alert_msg end return end if @account.account_providers.any? || @account.plaid_account_id.present? || @account.simplefin_account_id.present? alert_msg = t(".errors.only_manual") if turbo_frame_request? flash.now[:alert] = alert_msg return render turbo_stream: Array(flash_notification_stream_items) else return redirect_to account_path(@account), alert: alert_msg end end unless @account.crypto? alert_msg = t(".errors.only_manual") if turbo_frame_request? flash.now[:alert] = alert_msg return render turbo_stream: Array(flash_notification_stream_items) else return redirect_to account_path(@account), alert: alert_msg end end Account.transaction do binance_account.lock! ap = AccountProvider.find_or_initialize_by(provider: binance_account) previous_account = ap.account ap.account_id = @account.id ap.save! # Orphan cleanup (detaching the old account from this provider) is handled # by the background sync job; no immediate action is required here. if previous_account && previous_account.id != @account.id && previous_account.family_id == @account.family_id Rails.logger.info("Binance: re-linked BinanceAccount #{binance_account.id} from account ##{previous_account.id} to ##{@account.id}") end end if turbo_frame_request? item = binance_account.binance_item.reload @binance_items = Current.family.binance_items.ordered.includes(:syncs) @manual_accounts = Account.uncached { Current.family.accounts.visible_manual.order(:name).to_a } flash.now[:notice] = t(".success") @account.reload manual_accounts_stream = if @manual_accounts.any? turbo_stream.update("manual-accounts", partial: "accounts/index/manual_accounts", locals: { accounts: @manual_accounts }) else turbo_stream.replace("manual-accounts", view_context.tag.div(id: "manual-accounts")) end render turbo_stream: [ turbo_stream.replace( ActionView::RecordIdentifier.dom_id(item), partial: "binance_items/binance_item", locals: { binance_item: item } ), manual_accounts_stream, *Array(flash_notification_stream_items) ] else redirect_to accounts_path, notice: t(".success") end end def setup_accounts @binance_accounts = @binance_item.binance_accounts .left_joins(:account_provider) .where(account_providers: { id: nil }) .order(:name) end def complete_account_setup selected_accounts = Array(params[:selected_accounts]).reject(&:blank?) created_accounts = [] selected_accounts.each do |binance_account_id| ba = @binance_item.binance_accounts.find_by(id: binance_account_id) next unless ba begin ba.with_lock do next if ba.account.present? account = Account.create_from_binance_account(ba) provider_link = ba.ensure_account_provider!(account) if provider_link created_accounts << account else account.destroy! end end rescue StandardError => e Rails.logger.error("Failed to setup account for BinanceAccount #{ba.id}: #{e.message}") next end ba.reload begin BinanceAccount::HoldingsProcessor.new(ba).process rescue StandardError => e Rails.logger.error("Failed to process holdings for #{ba.id}: #{e.message}") end end unlinked_remaining = @binance_item.binance_accounts .left_joins(:account_provider) .where(account_providers: { id: nil }) .count @binance_item.update!(pending_account_setup: unlinked_remaining > 0) if created_accounts.any? flash.now[:notice] = t(".success", count: created_accounts.count) elsif selected_accounts.empty? flash.now[:notice] = t(".none_selected") else flash.now[:notice] = t(".no_accounts") end @binance_item.sync_later if created_accounts.any? if turbo_frame_request? @binance_items = Current.family.binance_items.ordered.includes(:syncs) render turbo_stream: [ turbo_stream.replace( ActionView::RecordIdentifier.dom_id(@binance_item), partial: "binance_items/binance_item", locals: { binance_item: @binance_item } ) ] + Array(flash_notification_stream_items) else redirect_to accounts_path, status: :see_other end end private def set_binance_item @binance_item = Current.family.binance_items.find(params[:id]) end def binance_item_params params.require(:binance_item).permit(:name, :sync_start_date, :api_key, :api_secret) end end