1
0
Fork 0
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:
Tony Vincent 2024-08-09 16:57:33 +02:00 committed by GitHub
parent f315370512
commit 6fa40e0fa2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 297 additions and 11 deletions

View file

@ -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

View file

@ -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)

View file

@ -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"

View file

@ -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

View file

@ -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)

File diff suppressed because one or more lines are too long