mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Fetch exchange rates in bulk from synth (#1069)
* Fetch exchnage rates in bulk * Handle paginated response * Rename method and improve tests * Change argument names * Use standard date format
This commit is contained in:
parent
f315370512
commit
6fa40e0fa2
6 changed files with 297 additions and 11 deletions
|
@ -15,12 +15,11 @@ class ExchangeRate < ApplicationRecord
|
||||||
|
|
||||||
def find_rates(from:, to:, start_date:, end_date: Date.current, cache: true)
|
def find_rates(from:, to:, start_date:, end_date: Date.current, cache: true)
|
||||||
rates = self.where(from_currency: from, to_currency: to, date: start_date..end_date).to_a
|
rates = self.where(from_currency: from, to_currency: to, date: start_date..end_date).to_a
|
||||||
all_dates = (start_date..end_date).to_a.to_set
|
all_dates = (start_date..end_date).to_a
|
||||||
existing_dates = rates.map(&:date).to_set
|
existing_dates = rates.map(&:date)
|
||||||
missing_dates = all_dates - existing_dates
|
missing_dates = all_dates - existing_dates
|
||||||
|
|
||||||
if missing_dates.any?
|
if missing_dates.any?
|
||||||
rates += fetch_rates_from_provider(from:, to:, dates: missing_dates, cache:)
|
rates += fetch_rates_from_provider(from:, to:, start_date: missing_dates.first, end_date: missing_dates.last, cache:)
|
||||||
end
|
end
|
||||||
|
|
||||||
rates
|
rates
|
||||||
|
|
|
@ -6,12 +6,31 @@ module ExchangeRate::Provided
|
||||||
class_methods do
|
class_methods do
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_rates_from_provider(from:, to:, dates:, cache: false)
|
def fetch_rates_from_provider(from:, to:, start_date:, end_date: Date.current, cache: false)
|
||||||
return [] unless exchange_rates_provider.present?
|
return [] unless exchange_rates_provider.present?
|
||||||
|
|
||||||
dates.map do |date|
|
response = exchange_rates_provider.fetch_exchange_rates \
|
||||||
fetch_rate_from_provider from:, to:, date:, cache:
|
from: from,
|
||||||
end.compact
|
to: to,
|
||||||
|
start_date: start_date,
|
||||||
|
end_date: end_date
|
||||||
|
|
||||||
|
if response.success?
|
||||||
|
response.rates.map do |exchange_rate|
|
||||||
|
rate = ExchangeRate.new \
|
||||||
|
from_currency: from,
|
||||||
|
to_currency: to,
|
||||||
|
date: exchange_rate.dig(:date).to_date,
|
||||||
|
rate: exchange_rate.dig(:rate)
|
||||||
|
|
||||||
|
rate.save! if cache
|
||||||
|
rate
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
next
|
||||||
|
end
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_rate_from_provider(from:, to:, date:, cache: false)
|
def fetch_rate_from_provider(from:, to:, date:, cache: false)
|
||||||
|
|
|
@ -57,12 +57,40 @@ class Provider::Synth
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_exchange_rates(from:, to:, start_date:, end_date:)
|
||||||
|
exchange_rates = paginate(
|
||||||
|
"#{base_url}/rates/historical-range",
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
date_start: start_date.to_s,
|
||||||
|
date_end: end_date.to_s
|
||||||
|
) do |body|
|
||||||
|
body.dig("data").map do |exchange_rate|
|
||||||
|
{
|
||||||
|
date: exchange_rate.dig("date"),
|
||||||
|
rate: exchange_rate.dig("rates", to)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ExchangeRatesResponse.new \
|
||||||
|
rates: exchange_rates,
|
||||||
|
success?: true,
|
||||||
|
raw_response: exchange_rates.to_json
|
||||||
|
rescue StandardError => error
|
||||||
|
ExchangeRatesResponse.new \
|
||||||
|
success?: false,
|
||||||
|
error: error,
|
||||||
|
raw_response: error
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :api_key
|
attr_reader :api_key
|
||||||
|
|
||||||
ExchangeRateResponse = Struct.new :rate, :success?, :error, :raw_response, keyword_init: true
|
ExchangeRateResponse = Struct.new :rate, :success?, :error, :raw_response, keyword_init: true
|
||||||
SecurityPriceResponse = Struct.new :prices, :success?, :error, :raw_response, keyword_init: true
|
SecurityPriceResponse = Struct.new :prices, :success?, :error, :raw_response, keyword_init: true
|
||||||
|
ExchangeRatesResponse = Struct.new :rates, :success?, :error, :raw_response, keyword_init: true
|
||||||
|
|
||||||
def base_url
|
def base_url
|
||||||
"https://api.synthfinance.com"
|
"https://api.synthfinance.com"
|
||||||
|
|
|
@ -62,8 +62,16 @@ class ExchangeRateTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "finds multiple rates from provider and caches to DB" do
|
test "finds multiple rates from provider and caches to DB" do
|
||||||
@provider.expects(:fetch_exchange_rate).with(from: "EUR", to: "USD", date: 1.day.ago.to_date).returns(OpenStruct.new(success?: true, rate: 1.1)).once
|
@provider.expects(:fetch_exchange_rates).with(from: "EUR", to: "USD", start_date: 1.day.ago.to_date, end_date: Date.current)
|
||||||
@provider.expects(:fetch_exchange_rate).with(from: "EUR", to: "USD", date: Date.current).returns(OpenStruct.new(success?: true, rate: 1.2)).once
|
.returns(
|
||||||
|
OpenStruct.new(
|
||||||
|
rates: [
|
||||||
|
OpenStruct.new(date: 1.day.ago.to_date, rate: 1.1),
|
||||||
|
OpenStruct.new(date: Date.current, rate: 1.2)
|
||||||
|
],
|
||||||
|
success?: true
|
||||||
|
)
|
||||||
|
).once
|
||||||
|
|
||||||
fetched_rates = ExchangeRate.find_rates(from: "EUR", to: "USD", start_date: 1.day.ago.to_date, cache: true)
|
fetched_rates = ExchangeRate.find_rates(from: "EUR", to: "USD", start_date: 1.day.ago.to_date, cache: true)
|
||||||
refetched_rates = ExchangeRate.find_rates(from: "EUR", to: "USD", start_date: 1.day.ago.to_date)
|
refetched_rates = ExchangeRate.find_rates(from: "EUR", to: "USD", start_date: 1.day.ago.to_date)
|
||||||
|
@ -73,7 +81,15 @@ class ExchangeRateTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "finds missing db rates from provider and appends to results" do
|
test "finds missing db rates from provider and appends to results" do
|
||||||
@provider.expects(:fetch_exchange_rate).with(from: "EUR", to: "GBP", date: 2.days.ago.to_date).returns(OpenStruct.new(success?: true, rate: 1.1)).once
|
@provider.expects(:fetch_exchange_rates).with(from: "EUR", to: "GBP", start_date: 2.days.ago.to_date, end_date: 2.days.ago.to_date)
|
||||||
|
.returns(
|
||||||
|
OpenStruct.new(
|
||||||
|
rates: [
|
||||||
|
OpenStruct.new(date: 2.day.ago.to_date, rate: 1.1)
|
||||||
|
],
|
||||||
|
success?: true
|
||||||
|
)
|
||||||
|
).once
|
||||||
|
|
||||||
rate1 = exchange_rates(:one) # EUR -> GBP, today
|
rate1 = exchange_rates(:one) # EUR -> GBP, today
|
||||||
rate2 = exchange_rates(:two) # EUR -> GBP, yesterday
|
rate2 = exchange_rates(:two) # EUR -> GBP, yesterday
|
||||||
|
|
|
@ -16,6 +16,17 @@ class Provider::SynthTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "fetches paginated exchange_rate historical data" do
|
||||||
|
VCR.use_cassette("synth/exchange_rate_historical") do
|
||||||
|
response = @synth.fetch_exchange_rates(
|
||||||
|
from: "USD", to: "GBP", start_date: Date.parse("01.01.2024"), end_date: Date.parse("31.07.2024")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 213, response.rates.size # 213 days between 01.01.2024 and 31.07.2024
|
||||||
|
assert_equal [ :date, :rate ], response.rates.first.keys
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
test "retries then provides failed response" do
|
test "retries then provides failed response" do
|
||||||
@client = mock
|
@client = mock
|
||||||
Faraday.stubs(:new).returns(@client)
|
Faraday.stubs(:new).returns(@client)
|
||||||
|
|
213
test/vcr_cassettes/synth/exchange_rate_historical.yml
Normal file
213
test/vcr_cassettes/synth/exchange_rate_historical.yml
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue