# frozen_string_literal: true module IndexaCapitalAccount::DataHelpers extend ActiveSupport::Concern private # Convert SDK objects to hashes via JSON round-trip # Many SDKs return objects that don't have proper #to_h methods def sdk_object_to_hash(obj) return obj if obj.is_a?(Hash) if obj.respond_to?(:to_json) JSON.parse(obj.to_json) elsif obj.respond_to?(:to_h) obj.to_h else obj end rescue JSON::ParserError, TypeError obj.respond_to?(:to_h) ? obj.to_h : {} end def parse_decimal(value) return nil if value.nil? case value when BigDecimal value when String BigDecimal(value) when Numeric BigDecimal(value.to_s) else nil end rescue ArgumentError => e Rails.logger.error("IndexaCapitalAccount::DataHelpers - Failed to parse decimal value: #{value.inspect} - #{e.message}") nil end def parse_date(date_value) return nil if date_value.nil? case date_value when Date date_value when String # Use Time.zone.parse for external timestamps (Rails timezone guidelines) Time.zone.parse(date_value)&.to_date when Time, DateTime, ActiveSupport::TimeWithZone date_value.to_date else nil end rescue ArgumentError, TypeError => e Rails.logger.error("IndexaCapitalAccount::DataHelpers - Failed to parse date: #{date_value.inspect} - #{e.message}") nil end # Find or create security with race condition handling def resolve_security(symbol, symbol_data = {}) ticker = symbol.to_s.upcase.strip return nil if ticker.blank? security = Security.find_by(ticker: ticker) # If security exists but has a bad name (looks like a hash), update it if security && security.name&.start_with?("{") new_name = extract_security_name(symbol_data, ticker) Rails.logger.info "IndexaCapitalAccount::DataHelpers - Fixing security name: #{security.name.first(50)}... -> #{new_name}" security.update!(name: new_name) end return security if security # Create new security security_name = extract_security_name(symbol_data, ticker) Rails.logger.info "IndexaCapitalAccount::DataHelpers - Creating security: ticker=#{ticker}, name=#{security_name}" Security.create!( ticker: ticker, name: security_name, exchange_mic: extract_exchange(symbol_data), country_code: extract_country_code(symbol_data) ) rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e # Handle race condition - another process may have created it Rails.logger.error "IndexaCapitalAccount::DataHelpers - Failed to create security #{ticker}: #{e.message}" Security.find_by(ticker: ticker) end def extract_security_name(symbol_data, fallback_ticker) symbol_data = symbol_data.with_indifferent_access if symbol_data.respond_to?(:with_indifferent_access) # Try various paths where the name might be name = symbol_data[:name] || symbol_data[:description] # If description is missing or looks like a type description, use ticker if name.blank? || name.is_a?(Hash) || name =~ /^(COMMON STOCK|CRYPTOCURRENCY|ETF|MUTUAL FUND)$/i name = fallback_ticker end # Titleize for readability if it's all caps name = name.titleize if name == name.upcase && name.length > 4 name end def extract_exchange(symbol_data) symbol_data = symbol_data.with_indifferent_access if symbol_data.respond_to?(:with_indifferent_access) exchange = symbol_data[:exchange] return nil unless exchange.is_a?(Hash) exchange.with_indifferent_access[:mic_code] || exchange.with_indifferent_access[:id] end def extract_country_code(symbol_data) symbol_data = symbol_data.with_indifferent_access if symbol_data.respond_to?(:with_indifferent_access) # Try to extract country from currency or exchange currency = symbol_data[:currency] currency = currency.dig(:code) if currency.is_a?(Hash) case currency when "USD" "US" when "CAD" "CA" when "GBP", "GBX" "GB" when "EUR" nil # Could be many countries else nil end end # Handle currency as string or object (API inconsistency) def extract_currency(data, fallback: nil) data = data.with_indifferent_access if data.respond_to?(:with_indifferent_access) currency_data = data[:currency] return fallback if currency_data.blank? if currency_data.is_a?(Hash) currency_data.with_indifferent_access[:code] || fallback elsif currency_data.is_a?(String) currency_data.upcase else fallback end end end