mirror of
https://github.com/we-promise/sure.git
synced 2026-07-26 19:52:15 +00:00
* feat(exchange-rates): add Frankfurter as an exchange-rate provider Frankfurter (frankfurter.dev) is a free, keyless FX rates API backed by ECB daily reference rates, with no published rate limit and no auth flow to maintain (unlike Yahoo Finance's reverse-engineered cookie/ crumb auth or TwelveData's fast-exhausting free tier). Follows the Provider::MoexPublic template: Faraday client with retry middleware, SslConfigurable for self-hosted CA support, a light RateLimitable throttle, and a FRANKFURTER_URL env escape hatch for self-hosters. Registered as exchange-rates-only (no security/stock data) and added to the hosting settings dropdown. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * refactor(provider): switch Frankfurter to the v2 API v1 is explicitly marked "frozen" on Frankfurter's own root endpoint; v2 is "current" and covers 201 currencies across 84 central banks vs v1's ~30 ECB-only. Confirmed via the v2 OpenAPI spec and live requests: - Single-date lookups now use GET /rate/{base}/{quote}?date=..., which carries weekends/holidays forward server-side (a Saturday returns a real rate directly), so the provider no longer needs its own lookback-window logic. - Range lookups now use GET /rates?base=..."es=...&from=...&to=..., a flat array of { date, base, quote, rate } records (v2's shape) instead of v1's { "rates": { date => currencies } } hash. - Every calendar day in a range is present (v2 gapfills itself), rather than v1's omit-non-trading-days behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(provider): sanitize currency codes before URL path interpolation from/to were only upcased before being interpolated directly into the URL path in fetch_exchange_rate (GET /rate/{from}/{to}). Low risk since currency codes come from validated internal sources, but adds cheap defense-in-depth: strip anything that isn't A-Z, matching the ISO 4217 format real currency codes always take. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
81 lines
3.8 KiB
Plaintext
81 lines
3.8 KiB
Plaintext
<div class="space-y-6">
|
|
<%# Exchange Rate Provider - single dropdown %>
|
|
<div class="space-y-2">
|
|
<h3 class="font-medium text-sm"><%= t(".exchange_rate_title") %></h3>
|
|
<p class="text-secondary text-xs mb-2"><%= t(".exchange_rate_description") %></p>
|
|
|
|
<%= styled_form_with model: Setting.new,
|
|
url: settings_hosting_path,
|
|
method: :patch,
|
|
data: {
|
|
controller: "auto-submit-form",
|
|
"auto-submit-form-trigger-event-value": "change"
|
|
} do |form| %>
|
|
<%= form.select :exchange_rate_provider,
|
|
[
|
|
[t(".providers.twelve_data"), "twelve_data"],
|
|
[t(".providers.yahoo_finance"), "yahoo_finance"],
|
|
[t(".providers.moex_public"), "moex_public"],
|
|
[t(".providers.frankfurter"), "frankfurter"]
|
|
],
|
|
{ label: t(".exchange_rate_provider_label") },
|
|
{
|
|
value: ENV.fetch("EXCHANGE_RATE_PROVIDER", Setting.exchange_rate_provider),
|
|
disabled: ENV["EXCHANGE_RATE_PROVIDER"].present?,
|
|
data: { "auto-submit-form-target": "auto" }
|
|
} %>
|
|
<% end %>
|
|
</div>
|
|
|
|
<%# Securities Providers - multiple checkboxes %>
|
|
<div class="space-y-2">
|
|
<h3 class="font-medium text-sm"><%= t(".securities_title") %></h3>
|
|
<p class="text-secondary text-xs mb-2"><%= t(".securities_description") %></p>
|
|
|
|
<%= styled_form_with model: Setting.new,
|
|
url: settings_hosting_path,
|
|
method: :patch,
|
|
data: {
|
|
controller: "auto-submit-form"
|
|
} do |form| %>
|
|
<% disabled = ENV["SECURITIES_PROVIDERS"].present? || ENV["SECURITIES_PROVIDER"].present? %>
|
|
<% enabled_providers = Setting.enabled_securities_providers %>
|
|
|
|
<div class="space-y-2">
|
|
<%# Hidden field to ensure empty array is submitted when all unchecked %>
|
|
<input type="hidden" name="setting[securities_providers][]" value="">
|
|
|
|
<% [
|
|
["twelve_data", t(".providers.twelve_data"), t(".twelve_data_hint")],
|
|
["yahoo_finance", t(".providers.yahoo_finance"), t(".yahoo_finance_hint")],
|
|
["tiingo", t(".providers.tiingo"), t(".requires_api_key")],
|
|
["eodhd", t(".providers.eodhd"), t(".requires_api_key_eodhd")],
|
|
["alpha_vantage", t(".providers.alpha_vantage"), t(".requires_api_key_alpha_vantage")],
|
|
["mfapi", t(".providers.mfapi"), t(".mfapi_hint")],
|
|
["binance_public", t(".providers.binance_public"), t(".binance_public_hint")],
|
|
["moex_public", t(".providers.moex_public"), t(".moex_public_hint")],
|
|
["tinkoff_invest", t(".providers.tinkoff_invest"), t(".tinkoff_invest_hint")],
|
|
].each do |value, label, hint| %>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox"
|
|
name="setting[securities_providers][]"
|
|
value="<%= value %>"
|
|
class="checkbox checkbox--light"
|
|
<%= "checked" if enabled_providers.include?(value) %>
|
|
<%= "disabled" if disabled %>
|
|
data-auto-submit-form-target="auto">
|
|
<span class="text-sm"><%= label %></span>
|
|
<% if hint %>
|
|
<span class="text-xs text-secondary">(<%= hint %>)</span>
|
|
<% end %>
|
|
</label>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<% if ENV["EXCHANGE_RATE_PROVIDER"].present? || ENV["SECURITIES_PROVIDERS"].present? || ENV["SECURITIES_PROVIDER"].present? %>
|
|
<%= render DS::Alert.new(message: t(".env_configured_message"), variant: :warning) %>
|
|
<% end %>
|
|
</div>
|