Skip to content

Instantly share code, notes, and snippets.

@herko
Created December 5, 2023 22:06
Show Gist options
  • Save herko/af4286240f470c96c8e5f2c3d9fd258f to your computer and use it in GitHub Desktop.
Save herko/af4286240f470c96c8e5f2c3d9fd258f to your computer and use it in GitHub Desktop.
# This is an ExchangeRate model class. It is responsible for keeping local exchange rates up to date.
# Cron job trigger `ExchangeRate.update` to periodically refresh rates. This method also verifies if `auto` update is enabled. This gives admin ability to temporary disable refreshing.
# The update method fetches refresh rates and creates either direct conversion between two currencies, or indirect (through 3rd currency) if its not available.
# This implementation works as is, but can be better. Please refactor this class to extract and separate responsibilities and overall improve code quality.
require 'money/bank/open_exchange_rates_bank'
class ExchangeRate < ActiveRecord::Base
CURRENCIES = %w[usd eur aed] # and more
class << self
def oxr_bank
oxr = Money::Bank::OpenExchangeRatesBank.new
oxr.ttl_in_seconds = 86400
oxr.app_id = ENV['EXCHANGE_RATES_API_KEY']
oxr
end
def update_rates
return unless auto?
oxr_response = oxr_bank.refresh_rates
oxr_rates = JSON.parse(oxr_response)
base_currency = oxr_rates['base']
rates = oxr_rates['rates']
CURRENCIES.combination(2).each do |currency_a, currency_b|
if currency_a == base_currency
add_direct_rate(
rates: rates,
base_currency: currency_a,
currency: currency_b
)
elsif currency_b == base_currency
add_direct_rate(
rates: rates,
base_currency: currency_b,
currency: currency_a
)
else
add_indirect_rate(
rates: rates,
currency_a: currency_a,
currency_b: currency_b
)
end
end
end
def add_direct_rate(rates:, base_currency:, currency:)
ExchangeRate.add_rate(base_currency, currency, rates[currency].to_f)
ExchangeRate.add_rate(currency, base_currency, 1/rates[currency].to_f)
end
def add_indirect_rate(rates:, currency_a:, currency_b:)
a_to_base = 1/rates[currency_a].to_f
base_to_b = rates[currency_b].to_f
rate = a_to_base * base_to_b
ExchangeRate.add_rate(currency_a, currency_b, rate)
ExchangeRate.add_rate(currency_b, currency_a, 1/rate)
end
end
validates :from, presence: true
validates :to, presence: true
validates :rate, presence: true
validates :rate, numericality: {greater_than: 0.0}
def self.auto?
Redis.current.exists('exchange_rates_auto') && Redis.current.get('exchange_rates_auto') == '1'
end
def self.auto=(value)
Redis.current.set('exchange_rates_auto', value)
end
def self.usd_rates
where(from: 'USD', to: CURRENCIES).order("created_at asc")
end
def self.get_rate(from_iso_code, to_iso_code)
@@rates_cache ||= {}
@@rates_cache["#{from_iso_code}-#{to_iso_code}"] ||= find_by(from: from_iso_code, to: to_iso_code).try(:rate)
end
def self.add_rate(from_iso_code, to_iso_code, rate)
ExchangeRateHistory.create(from: from_iso_code, to: to_iso_code, rate: rate)
currency = find_or_initialize_by(from: from_iso_code, to: to_iso_code)
currency.rate = rate
currency.save!
reset_cache
end
def self.reset_cache
@@rates_cache = {}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment