Packages

Money functions for operations on and localization of a money data type with support for ISO 4217 currencies and ISO 24165 digial tokens (crypto currencies).

Current section

Files

Jump to
ex_money lib money exchange_rates exchange_rates_retriever.ex
Raw

lib/money/exchange_rates/exchange_rates_retriever.ex

defmodule Money.ExchangeRates.Retriever do
@moduledoc """
Implements a `GenServer` to retrieve exchange rates from
a configured retrieveal module on a periodic basis. By default exchange
rates are retrieved from [Open Exchange Rates](http://openexchangerates.org).
Retrieved data is stored in an `:ets` table.
By default the period of execution is 5 minutes (300_000 microseconds). The
period of retrieval is configured in `config.exs` or the appropriate
environment configuration. For example:
config :ex_money,
open_exchange_rates_app_id: "app_id_string",
open_exchange_rates_retrieve_every: 300_000
"""
use GenServer
require Logger
alias Money.ExchangeRates
def start_link(name, config) do
GenServer.start_link(__MODULE__, config, name: name)
end
def init(config) do
log(config, :info, "Starting exchange rate retrieval service")
initialize_ets_table()
log(config, :info, log_init_message(config.retrieve_every))
schedule_work(0)
schedule_work(config.retrieve_every)
if config.preload_historic_rates do
log(config, :info, "Preloading historic rates for #{inspect(config.preload_historic_rates)}")
schedule_work(config.preload_historic_rates)
end
{:ok, config}
end
def handle_info(:latest_rates, config) do
retrieve_latest_rates(config)
schedule_work(config.retrieve_every)
{:noreply, config}
end
def handle_info({:historic_rates, %Date{calendar: Calendar.ISO} = date}, config) do
retrieve_historic_rates(date, config)
{:noreply, config}
end
def handle_info(message, config) do
Logger.error("Invalid message for ExchangeRates.Retriever: #{inspect(message)}")
{:noreply, config}
end
def retrieve_latest_rates(%{callback_module: callback_module} = config) do
case ExchangeRates.get_latest_rates(config) do
{:ok, rates} ->
retrieved_at = DateTime.utc_now()
store_latest_rates(rates, retrieved_at)
apply(callback_module, :latest_rates_retrieved, [rates, retrieved_at])
log(config, :success, "Retrieved latest exchange rates successfully")
{:ok, rates}
{:error, reason} ->
log(config, :failure, "Could not retrieve latest exchange rates: #{inspect(reason)}")
{:error, reason}
end
end
def retrieve_historic_rates(%Date{} = date, %{callback_module: callback_module} = config) do
case ExchangeRates.get_historic_rates(date, config) do
{:ok, rates} ->
store_historic_rates(rates, date)
apply(callback_module, :historic_rates_retrieved, [rates, date])
log(
config,
:success,
"Retrieved historic exchange rates for #{Date.to_string(date)} successfully"
)
{:ok, rates}
{:error, reason} ->
log(
config,
:failure,
"Could not retrieve historic exchange rates " <>
"for #{Date.to_string(date)}: #{inspect(reason)}"
)
{:error, reason}
end
end
defp schedule_work(delay_ms) when is_integer(delay_ms) do
Process.send_after(self(), :latest_rates, delay_ms)
end
defp schedule_work(%Date{calendar: Calendar.ISO} = date) do
Process.send(self(), {:historic_rates, date}, [])
end
defp schedule_work(%Date.Range{} = date_range) do
for date <- date_range do
Process.send(self(), {:historic_rates, date}, [])
end
end
defp schedule_work({%Date{} = from, %Date{} = to}) do
schedule_work(Date.range(from, to))
end
defp schedule_work(date_string) when is_binary(date_string) do
parts = String.split(date_string, "..")
case parts do
[date] -> schedule_work(Date.from_iso8601(date))
[from, to] -> schedule_work({Date.from_iso8601(from), Date.from_iso8601(to)})
end
end
defp initialize_ets_table do
:ets.new(:exchange_rates, [:named_table, read_concurrency: true])
end
defp store_latest_rates(rates, retrieved_at) do
:ets.insert(:exchange_rates, {:latest_rates, rates})
:ets.insert(:exchange_rates, {:last_updated, retrieved_at})
end
defp store_historic_rates(rates, date) do
:ets.insert(:exchange_rates, {date, rates})
end
def log(%{log_levels: log_levels}, key, message) do
case Map.get(log_levels, key) do
nil ->
nil
log_level ->
Logger.log(log_level, message)
end
end
defp log_init_message(every) do
{every, plural_every} = seconds(every)
"Rates will be retrieved now and then every #{every} #{plural_every}."
end
defp seconds(milliseconds) do
seconds = div(milliseconds, 1000)
plural = if seconds == 1, do: "second", else: "seconds"
{seconds, plural}
end
end