103 lines
2.8 KiB
Elixir
103 lines
2.8 KiB
Elixir
|
|
defmodule SimpleshopTheme.ExchangeRate do
|
||
|
|
@moduledoc """
|
||
|
|
Fetches and caches exchange rates for shipping cost conversion.
|
||
|
|
|
||
|
|
Uses the frankfurter.app API (ECB data, free, no API key).
|
||
|
|
Rates are fetched during product sync and cached in Settings so
|
||
|
|
they survive restarts without an API call.
|
||
|
|
"""
|
||
|
|
|
||
|
|
alias SimpleshopTheme.Settings
|
||
|
|
|
||
|
|
require Logger
|
||
|
|
|
||
|
|
@api_base "https://api.frankfurter.app"
|
||
|
|
@settings_prefix "exchange_rate_"
|
||
|
|
@default_rates %{"USD" => 0.80, "EUR" => 0.86}
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Fetches the latest exchange rates to GBP from the API and caches them.
|
||
|
|
|
||
|
|
Returns `{:ok, rates_map}` or `{:error, reason}`.
|
||
|
|
The rates map has currency codes as keys and GBP multipliers as values,
|
||
|
|
e.g. `%{"USD" => 0.7892, "EUR" => 0.8534}`.
|
||
|
|
"""
|
||
|
|
def fetch_and_cache do
|
||
|
|
case fetch_from_api() do
|
||
|
|
{:ok, rates} ->
|
||
|
|
cache_rates(rates)
|
||
|
|
{:ok, rates}
|
||
|
|
|
||
|
|
{:error, reason} ->
|
||
|
|
Logger.warning("Exchange rate fetch failed: #{inspect(reason)}, using cached rates")
|
||
|
|
{:ok, get_cached_rates()}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Returns cached exchange rates from Settings, falling back to defaults.
|
||
|
|
"""
|
||
|
|
def get_cached_rates do
|
||
|
|
Enum.reduce(@default_rates, %{}, fn {currency, default}, acc ->
|
||
|
|
key = @settings_prefix <> String.downcase(currency) <> "_to_gbp"
|
||
|
|
|
||
|
|
rate =
|
||
|
|
case Settings.get_setting(key) do
|
||
|
|
nil -> default
|
||
|
|
val when is_binary(val) -> String.to_float(val)
|
||
|
|
val when is_number(val) -> val / 1
|
||
|
|
end
|
||
|
|
|
||
|
|
Map.put(acc, currency, rate)
|
||
|
|
end)
|
||
|
|
end
|
||
|
|
|
||
|
|
@doc """
|
||
|
|
Returns the GBP rate for a given currency, using cached values.
|
||
|
|
"""
|
||
|
|
def rate_for(currency) when currency in ["GBP", "gbp"], do: 1.0
|
||
|
|
|
||
|
|
def rate_for(currency) do
|
||
|
|
rates = get_cached_rates()
|
||
|
|
|
||
|
|
Map.get(
|
||
|
|
rates,
|
||
|
|
String.upcase(currency),
|
||
|
|
Map.get(@default_rates, String.upcase(currency), 0.80)
|
||
|
|
)
|
||
|
|
end
|
||
|
|
|
||
|
|
# Fetch from frankfurter.app API
|
||
|
|
defp fetch_from_api do
|
||
|
|
url = "#{@api_base}/latest?from=USD&to=GBP,EUR"
|
||
|
|
|
||
|
|
case Req.get(url, receive_timeout: 10_000) do
|
||
|
|
{:ok, %{status: 200, body: %{"rates" => rates}}} ->
|
||
|
|
# API returns rates FROM USD, so "GBP" value is the USD→GBP multiplier
|
||
|
|
gbp_per_usd = rates["GBP"]
|
||
|
|
# Derive EUR→GBP: if 1 USD = X GBP and 1 USD = Y EUR, then 1 EUR = X/Y GBP
|
||
|
|
eur_rate = rates["EUR"]
|
||
|
|
|
||
|
|
eur_to_gbp =
|
||
|
|
if eur_rate && eur_rate > 0,
|
||
|
|
do: gbp_per_usd / eur_rate,
|
||
|
|
else: @default_rates["EUR"]
|
||
|
|
|
||
|
|
{:ok, %{"USD" => gbp_per_usd, "EUR" => eur_to_gbp}}
|
||
|
|
|
||
|
|
{:ok, %{status: status}} ->
|
||
|
|
{:error, {:http_status, status}}
|
||
|
|
|
||
|
|
{:error, reason} ->
|
||
|
|
{:error, reason}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp cache_rates(rates) do
|
||
|
|
Enum.each(rates, fn {currency, rate} ->
|
||
|
|
key = @settings_prefix <> String.downcase(currency) <> "_to_gbp"
|
||
|
|
Settings.put_setting(key, Float.to_string(rate))
|
||
|
|
end)
|
||
|
|
end
|
||
|
|
end
|