diff --git a/app/controllers/snaptrade_items_controller.rb b/app/controllers/snaptrade_items_controller.rb index 5341e2d83..bcc09e49a 100644 --- a/app/controllers/snaptrade_items_controller.rb +++ b/app/controllers/snaptrade_items_controller.rb @@ -215,10 +215,23 @@ class SnaptradeItemsController < ApplicationController # Trigger sync to process the newly linked accounts # Always queue the sync - if one is running, this will run after it finishes @snaptrade_item.sync_later - redirect_to accounts_path, notice: t(".success", count: linked_count, default: "Successfully linked #{linked_count} account(s).") + + if errors.any? + # Partial success - some linked, some failed + redirect_to accounts_path, notice: t(".partial_success", linked: linked_count, failed: errors.size, + default: "Linked #{linked_count} account(s). #{errors.size} failed to link.") + else + redirect_to accounts_path, notice: t(".success", count: linked_count, default: "Successfully linked #{linked_count} account(s).") + end else - redirect_to setup_accounts_snaptrade_item_path(@snaptrade_item), - alert: t(".no_accounts", default: "No accounts were selected for linking.") + if errors.any? + # All failed + redirect_to setup_accounts_snaptrade_item_path(@snaptrade_item), + alert: t(".link_failed", default: "Failed to link accounts: %{errors}", errors: errors.first) + else + redirect_to setup_accounts_snaptrade_item_path(@snaptrade_item), + alert: t(".no_accounts", default: "No accounts were selected for linking.") + end end end diff --git a/app/jobs/snaptrade_activities_fetch_job.rb b/app/jobs/snaptrade_activities_fetch_job.rb index 810459de7..18ecd3993 100644 --- a/app/jobs/snaptrade_activities_fetch_job.rb +++ b/app/jobs/snaptrade_activities_fetch_job.rb @@ -8,6 +8,8 @@ # SnaptradeActivitiesFetchJob.perform_later(snaptrade_account, start_date: 5.years.ago.to_date) # class SnaptradeActivitiesFetchJob < ApplicationJob + include SnaptradeAccount::DataHelpers + queue_as :default # Prevent concurrent jobs for the same account - only one fetch at a time @@ -115,20 +117,6 @@ class SnaptradeActivitiesFetchJob < ApplicationJob activities.map { |a| sdk_object_to_hash(a) } end - 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 - # Merge activities, deduplicating by ID # Fallback key includes symbol to distinguish activities with same date/type/amount def merge_activities(existing, new_activities) diff --git a/app/models/snaptrade_account.rb b/app/models/snaptrade_account.rb index 8beeac9ae..6e888cb1d 100644 --- a/app/models/snaptrade_account.rb +++ b/app/models/snaptrade_account.rb @@ -1,5 +1,6 @@ class SnaptradeAccount < ApplicationRecord include CurrencyNormalizable + include SnaptradeAccount::DataHelpers belongs_to :snaptrade_item @@ -60,7 +61,7 @@ class SnaptradeAccount < ApplicationRecord def upsert_from_snaptrade!(account_data) # Deep convert SDK objects to hashes - .to_h only does top level, # so we use JSON round-trip to get nested objects as hashes too - data = deep_convert_to_hash(account_data) + data = sdk_object_to_hash(account_data) data = data.with_indifferent_access # Extract meta data @@ -118,7 +119,7 @@ class SnaptradeAccount < ApplicationRecord # and is set by upsert_from_snaptrade! from the balance.total field. def upsert_balances!(balances_data) # Deep convert each balance entry to ensure we have hashes - data = Array(balances_data).map { |b| deep_convert_to_hash(b).with_indifferent_access } + data = Array(balances_data).map { |b| sdk_object_to_hash(b).with_indifferent_access } Rails.logger.info "SnaptradeAccount##{id} upsert_balances! - raw data: #{data.inspect}" @@ -161,23 +162,6 @@ class SnaptradeAccount < ApplicationRecord ) end - # Deep convert SDK objects to nested hashes - # The SnapTrade SDK returns objects that only convert the top level with .to_h - # We use JSON round-trip to ensure all nested objects become hashes - def deep_convert_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 log_invalid_currency(currency_value) Rails.logger.warn("Invalid currency code '#{currency_value}' for SnapTrade account #{id}, defaulting to USD") end diff --git a/app/models/snaptrade_account/data_helpers.rb b/app/models/snaptrade_account/data_helpers.rb index 17bb38dec..c0f1d4c23 100644 --- a/app/models/snaptrade_account/data_helpers.rb +++ b/app/models/snaptrade_account/data_helpers.rb @@ -3,6 +3,23 @@ module SnaptradeAccount::DataHelpers private + # Convert SnapTrade SDK objects to hashes + # SDK objects don't have proper to_h but do have to_json + # Uses JSON round-trip to ensure all nested objects become hashes + 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? diff --git a/app/models/snaptrade_item/importer.rb b/app/models/snaptrade_item/importer.rb index 7e072f8ee..478f6c3ac 100644 --- a/app/models/snaptrade_item/importer.rb +++ b/app/models/snaptrade_item/importer.rb @@ -1,5 +1,6 @@ class SnaptradeItem::Importer include SyncStats::Collector + include SnaptradeAccount::DataHelpers attr_reader :snaptrade_item, :snaptrade_provider, :sync @@ -53,22 +54,6 @@ class SnaptradeItem::Importer private - # Convert SnapTrade SDK objects to hashes - # SDK objects don't have to_h but do have to_json - 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 - # Extract activities array from API response # get_account_activities returns a paginated object with .data accessor # This handles both paginated responses and plain arrays diff --git a/config/locales/views/snaptrade_items/en.yml b/config/locales/views/snaptrade_items/en.yml index a5ee65d7f..ce9a773fc 100644 --- a/config/locales/views/snaptrade_items/en.yml +++ b/config/locales/views/snaptrade_items/en.yml @@ -16,6 +16,8 @@ en: success: one: "Successfully linked %{count} account." other: "Successfully linked %{count} accounts." + partial_success: "Linked %{linked} account(s). %{failed} failed to link." + link_failed: "Failed to link accounts: %{errors}" no_accounts: "No accounts were selected for linking." preload_accounts: not_configured: "SnapTrade is not configured."