lib/exchange_rates/coin_market_cap.ex

defmodule SoftBank.ExchangeRates.CoinMarketCap do
  @moduledoc """
  Implements the `Money.ExchangeRates` for CoinMarketCap
  Rates service.

  ## Required configuration:

  The configuration key `:coin_market_cap_key` should be
  set to your `app_id`.  for example:

      config :soft_bank,
        coin_market_cap_key: "your_key"

  or configure it via environment variable:

      config :soft_bank,
        coin_market_cap_key: {:system, "coin_market_cap_key"}

  It is also possible to configure an alternative base url for this
  service in case it changes in the future. For example:

      config :soft_bank,
        coin_market_cap_key: "your_key"
        coin_market_cap_url: "https://pro-api.coinmarketcap.com"

  """
  require Logger
  alias SoftBank.ExchangeRates.CoinMarketCap.Retriever

  @behaviour Money.ExchangeRates

  @rate_url "https://pro-api.coinmarketcap.com/v1"

  @doc """
  Update the retriever configuration to include the requirements
  for  CoinMarketCap Rates.  This function is invoked when the
  exchange rate service starts up, just after the ets table
  :exchange_rates is created.

  * `default_config` is the configuration returned by `Money.ExchangeRates.default_config/0`

  Returns the configuration either unchanged or updated with
  additional configuration specific to this exchange
  rates retrieval module.
  """

  def init(default_config) do
    url = Application.get_env(:ex_money, :rate_url, @rate_url)
    api_key = Application.get_env(:ex_money, :exchange_rates_api_key, nil)
    Map.put(default_config, :retriever_options, %{url: url, api_key: api_key})
  end

  def decode_rates(body) do
    %{"data" => data} = Money.json_library().decode!(body)

    add_currencies_to_bank(data)

    rates = marshall_rates(data)

    rates
    |> Cldr.Map.atomize_keys()
    |> Enum.map(fn
      {k, v} when is_float(v) -> {k, Decimal.from_float(v)}
      {k, v} when is_integer(v) -> {k, Decimal.new(v)}
    end)
    |> Enum.into(%{})
  end

  defp marshall_rates(data) do
    Enum.map(data, fn x ->
      key = "X" <> String.slice(x["symbol"], 0..1)
      value = x["quote"]["USD"]["price"]

      {key, value}
    end)
  end

  defp add_currencies_to_bank(data) do
    Enum.each(data, fn x ->
      key = "X" <> String.slice(x["symbol"], 0..1)

      currency = %{
        name: x["name"],
        digits: 16,
        symbol: key,
        alt_code: x["slug"],
        code: x["symbol"]
      }

      SoftBank.Currencies.new(currency)
    end)
  end

  @doc """
  Retrieves the latest exchange rates from CoinMarketCap site.

  * `config` is the retrieval configuration. When invoked from the
  exchange rates services this will be the config returned from
  `Money.ExchangeRates.config/0`

  Returns:

  * `{:ok, rates}` if the rates can be retrieved

  * `{:error, reason}` if rates cannot be retrieved

  Typically this function is called by the exchange rates retrieval
  service although it can be called outside that context as
  required.

  """
  @spec get_latest_rates(Money.ExchangeRates.Config.t()) :: {:ok, map()} | {:error, String.t()}
  def get_latest_rates(config) do
    url = config.retriever_options.url
    api_key = config.retriever_options.api_key
    retrieve_latest_rates(url, api_key, config)
  end

  defp retrieve_latest_rates(_url, nil, _config) do
    {:error, api_key_not_configured()}
  end

  @latest_rates "/cryptocurrency/listings/latest"
  defp retrieve_latest_rates(url, api_key, config) do
    endpoint = url <> @latest_rates <> "?CMC_PRO_API_KEY=" <> api_key

    Retriever.retrieve_rates(endpoint, config)
  end

  @doc """
  Retrieves the historic exchange rates from CoinMarketCap.

  * `date` is a date returned by `Date.new/3` or any struct with the
    elements `:year`, `:month` and `:day`.

  * `config` is the retrieval configuration. When invoked from the
    exchange rates services this will be the config returned from
    `Money.ExchangeRates.config/0`

  Returns:

  * `{:ok, rates}` if the rates can be retrieved

  * `{:error, reason}` if rates cannot be retrieved

  Typically this function is called by the exchange rates retrieval
  service although it can be called outside that context as
  required.
  """
  def get_historic_rates(date, config) do
    url = config.retriever_options.url
    api_key = config.retriever_options.api_key
    retrieve_historic_rates(date, url, api_key, config)
  end

  defp retrieve_historic_rates(_date, _url, nil, _config) do
    {:error, api_key_not_configured()}
  end

  @historic_rates "/historical/"
  defp retrieve_historic_rates(%Date{calendar: Calendar.ISO} = _date, url, api_key, config) do
    Retriever.retrieve_rates(
      url <> @historic_rates <> "?CMC_PRO_API_KEY=" <> api_key,
      config
    )
  end

  defp retrieve_historic_rates(%{year: year, month: month, day: day}, url, api_key, config) do
    case Date.new(year, month, day) do
      {:ok, date} -> retrieve_historic_rates(date, url, api_key, config)
      error -> error
    end
  end

  defp api_key_not_configured do
    "exchange_rates_api_key is not configured.  Rates are not retrieved."
  end
end