diff --git a/app/models/provider/twelve_data.rb b/app/models/provider/twelve_data.rb index ccb97ff64..5a7738bb4 100644 --- a/app/models/provider/twelve_data.rb +++ b/app/models/provider/twelve_data.rb @@ -62,7 +62,15 @@ class Provider::TwelveData < Provider req.params["interval"] = "1day" end - data = JSON.parse(response.body).dig("values") + parsed = JSON.parse(response.body) + data = parsed.dig("values") + + if data.nil? + error_message = parsed.dig("message") || "No data returned" + error_code = parsed.dig("code") || "unknown" + raise InvalidExchangeRateError, "API error (code: #{error_code}): #{error_message}" + end + data.map do |resp| rate = resp.dig("close") date = resp.dig("datetime") @@ -88,8 +96,15 @@ class Provider::TwelveData < Provider end parsed = JSON.parse(response.body) + data = parsed.dig("data") - parsed.dig("data").map do |security| + if data.nil? + error_message = parsed.dig("message") || "No data returned" + error_code = parsed.dig("code") || "unknown" + raise Error, "API error (code: #{error_code}): #{error_message}" + end + + data.map do |security| country = ISO3166::Country.find_country_by_any_name(security.dig("country")) Security.new( @@ -152,7 +167,15 @@ class Provider::TwelveData < Provider end parsed = JSON.parse(response.body) - parsed.dig("values").map do |resp| + values = parsed.dig("values") + + if values.nil? + error_message = parsed.dig("message") || "No data returned" + error_code = parsed.dig("code") || "unknown" + raise InvalidSecurityPriceError, "API error (code: #{error_code}): #{error_message}" + end + + values.map do |resp| price = resp.dig("close") date = resp.dig("datetime") if price.nil? diff --git a/test/models/account/market_data_importer_test.rb b/test/models/account/market_data_importer_test.rb index 005411655..b74401997 100644 --- a/test/models/account/market_data_importer_test.rb +++ b/test/models/account/market_data_importer_test.rb @@ -104,4 +104,95 @@ class Account::MarketDataImporterTest < ActiveSupport::TestCase assert_equal 1, Security::Price.where(security: security, date: trade_date).count end + + test "handles provider error response gracefully for security prices" do + family = Family.create!(name: "Smith", currency: "USD") + + account = family.accounts.create!( + name: "Brokerage", + currency: "USD", + balance: 0, + accountable: Investment.new + ) + + security = Security.create!(ticker: "INVALID", exchange_operating_mic: "XNAS") + + trade_date = 10.days.ago.to_date + trade = Trade.new(security: security, qty: 1, price: 100, currency: "USD") + + account.entries.create!( + name: "Buy INVALID", + date: trade_date, + amount: 100, + currency: "USD", + entryable: trade + ) + + expected_start_date = trade_date - PROVIDER_BUFFER + end_date = Date.current.in_time_zone("America/New_York").to_date + + # Simulate provider returning an error response + @provider.expects(:fetch_security_prices) + .with(symbol: security.ticker, + exchange_operating_mic: security.exchange_operating_mic, + start_date: expected_start_date, + end_date: end_date) + .returns(provider_error_response( + Provider::TwelveData::Error.new("Invalid symbol", details: { code: 400, message: "Invalid symbol" }) + )) + + @provider.stubs(:fetch_security_info) + .with(symbol: security.ticker, exchange_operating_mic: security.exchange_operating_mic) + .returns(provider_success_response(OpenStruct.new(name: "Invalid Co", logo_url: "logo"))) + + # Ignore exchange-rate calls for this test + @provider.stubs(:fetch_exchange_rates).returns(provider_success_response([])) + + # Should not raise an error, just log and continue + assert_nothing_raised do + Account::MarketDataImporter.new(account).import_all + end + + assert_equal 0, Security::Price.where(security: security, date: trade_date).count + end + + test "handles provider error response gracefully for exchange rates" do + family = Family.create!(name: "Smith", currency: "USD") + + account = family.accounts.create!( + name: "Chequing", + currency: "CAD", + balance: 100, + accountable: Depository.new + ) + + # Seed a rate for the first required day + existing_date = account.start_date + ExchangeRate.create!(from_currency: "CAD", to_currency: "USD", date: existing_date, rate: 2.0) + + expected_start_date = (existing_date + 1.day) - PROVIDER_BUFFER + end_date = Date.current.in_time_zone("America/New_York").to_date + + # Simulate provider returning an error response + @provider.expects(:fetch_exchange_rates) + .with(from: "CAD", + to: "USD", + start_date: expected_start_date, + end_date: end_date) + .returns(provider_error_response( + Provider::TwelveData::Error.new("Rate limit exceeded", details: { code: 429, message: "Rate limit exceeded" }) + )) + + before = ExchangeRate.count + + # Should not raise an error, just log and continue + assert_nothing_raised do + Account::MarketDataImporter.new(account).import_all + end + + after = ExchangeRate.count + + # No new rates should be added due to error + assert_equal before, after + end end