Remove Synth Finance references (#47)

* Remove Synth Finance integration

* Linter noise

* Fix failing (old) test, use it for Twelve Data

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
Juan José Mata
2025-08-01 15:28:55 -07:00
committed by GitHub
parent 656f7e9495
commit 54bc37a651
38 changed files with 35 additions and 1283 deletions

View File

@@ -6,9 +6,6 @@ class Settings::HostingsController < ApplicationController
before_action :ensure_admin, only: :clear_cache
def show
synth_provider = Provider::Registry.get_provider(:synth)
@synth_usage = synth_provider&.usage
twelve_data_provider = Provider::Registry.get_provider(:twelve_data)
@twelve_data_usage = twelve_data_provider&.usage
end
@@ -22,10 +19,6 @@ class Settings::HostingsController < ApplicationController
Setting.require_email_confirmation = hosting_params[:require_email_confirmation]
end
if hosting_params.key?(:synth_api_key)
Setting.synth_api_key = hosting_params[:synth_api_key]
end
if hosting_params.key?(:twelve_data_api_key)
Setting.twelve_data_api_key = hosting_params[:twelve_data_api_key]
end
@@ -43,7 +36,7 @@ class Settings::HostingsController < ApplicationController
private
def hosting_params
params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :synth_api_key, :twelve_data_api_key)
params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :twelve_data_api_key)
end
def ensure_admin

View File

@@ -36,9 +36,7 @@ class Family::AutoMerchantDetector
source: "ai",
name: auto_detection.business_name,
website_url: auto_detection.business_url,
) do |pm|
pm.logo_url = "#{default_logo_provider_url}/#{auto_detection.business_url}"
end
)
end
merchant_id = merchant_id || ai_provider_merchant&.id
@@ -65,9 +63,6 @@ class Family::AutoMerchantDetector
Provider::Registry.get_provider(:openai)
end
def default_logo_provider_url
"https://logo.synthfinance.com"
end
def user_merchants_input
family.merchants.map do |merchant|

View File

@@ -32,14 +32,6 @@ class Provider::Registry
Provider::Stripe.new(secret_key:, webhook_secret:)
end
def synth
api_key = ENV.fetch("SYNTH_API_KEY", Setting.synth_api_key)
return nil unless api_key.present?
Provider::Synth.new(api_key)
end
def twelve_data
api_key = ENV.fetch("TWELVE_DATA_API_KEY", Setting.twelve_data_api_key)
@@ -100,13 +92,13 @@ class Provider::Registry
def available_providers
case concept
when :exchange_rates
%i[synth twelve_data]
%i[twelve_data]
when :securities
%i[synth twelve_data]
%i[twelve_data]
when :llm
%i[openai]
else
%i[synth plaid_us plaid_eu github openai]
%i[plaid_us plaid_eu github openai]
end
end
end

View File

@@ -1,250 +0,0 @@
class Provider::Synth < Provider
include ExchangeRateConcept, SecurityConcept
# Subclass so errors caught in this provider are raised as Provider::Synth::Error
Error = Class.new(Provider::Error)
InvalidExchangeRateError = Class.new(Error)
InvalidSecurityPriceError = Class.new(Error)
def initialize(api_key)
@api_key = api_key
end
def healthy?
with_provider_response do
response = client.get("#{base_url}/user")
JSON.parse(response.body).dig("id").present?
end
end
def usage
with_provider_response do
response = client.get("#{base_url}/user")
parsed = JSON.parse(response.body)
remaining = parsed.dig("api_calls_remaining")
limit = parsed.dig("api_limit")
used = limit - remaining
UsageData.new(
used: used,
limit: limit,
utilization: used.to_f / limit * 100,
plan: parsed.dig("plan"),
)
end
end
# ================================
# Exchange Rates
# ================================
def fetch_exchange_rate(from:, to:, date:)
with_provider_response do
response = client.get("#{base_url}/rates/historical") do |req|
req.params["date"] = date.to_s
req.params["from"] = from
req.params["to"] = to
end
rates = JSON.parse(response.body).dig("data", "rates")
Rate.new(date: date.to_date, from:, to:, rate: rates.dig(to))
end
end
def fetch_exchange_rates(from:, to:, start_date:, end_date:)
with_provider_response do
data = paginate(
"#{base_url}/rates/historical-range",
from: from,
to: to,
date_start: start_date.to_s,
date_end: end_date.to_s
) do |body|
body.dig("data")
end
data.paginated.map do |rate|
date = rate.dig("date")
rate = rate.dig("rates", to)
if date.nil? || rate.nil?
Rails.logger.warn("#{self.class.name} returned invalid rate data for pair from: #{from} to: #{to} on: #{date}. Rate data: #{rate.inspect}")
Sentry.capture_exception(InvalidExchangeRateError.new("#{self.class.name} returned invalid rate data"), level: :warning) do |scope|
scope.set_context("rate", { from: from, to: to, date: date })
end
next
end
Rate.new(date: date.to_date, from:, to:, rate:)
end.compact
end
end
# ================================
# Securities
# ================================
def search_securities(symbol, country_code: nil, exchange_operating_mic: nil)
with_provider_response do
response = client.get("#{base_url}/tickers/search") do |req|
req.params["name"] = symbol
req.params["dataset"] = "limited"
req.params["country_code"] = country_code if country_code.present?
# Synth uses mic_code, which encompasses both exchange_mic AND exchange_operating_mic (union)
req.params["mic_code"] = exchange_operating_mic if exchange_operating_mic.present?
req.params["limit"] = 25
end
parsed = JSON.parse(response.body)
parsed.dig("data").map do |security|
Security.new(
symbol: security.dig("symbol"),
name: security.dig("name"),
logo_url: security.dig("logo_url"),
exchange_operating_mic: security.dig("exchange", "operating_mic_code"),
country_code: security.dig("exchange", "country_code")
)
end
end
end
def fetch_security_info(symbol:, exchange_operating_mic:)
with_provider_response do
response = client.get("#{base_url}/tickers/#{symbol}") do |req|
req.params["operating_mic"] = exchange_operating_mic
end
data = JSON.parse(response.body).dig("data")
SecurityInfo.new(
symbol: symbol,
name: data.dig("name"),
links: data.dig("links"),
logo_url: data.dig("logo_url"),
description: data.dig("description"),
kind: data.dig("kind"),
exchange_operating_mic: exchange_operating_mic
)
end
end
def fetch_security_price(symbol:, exchange_operating_mic: nil, date:)
with_provider_response do
historical_data = fetch_security_prices(symbol:, exchange_operating_mic:, start_date: date, end_date: date)
raise ProviderError, "No prices found for security #{symbol} on date #{date}" if historical_data.data.empty?
historical_data.data.first
end
end
def fetch_security_prices(symbol:, exchange_operating_mic: nil, start_date:, end_date:)
with_provider_response do
params = {
start_date: start_date,
end_date: end_date,
operating_mic_code: exchange_operating_mic
}.compact
data = paginate(
"#{base_url}/tickers/#{symbol}/open-close",
params
) do |body|
body.dig("prices")
end
currency = data.first_page.dig("currency")
exchange_operating_mic = data.first_page.dig("exchange", "operating_mic_code")
data.paginated.map do |price|
date = price.dig("date")
price = price.dig("close") || price.dig("open")
if date.nil? || price.nil?
Rails.logger.warn("#{self.class.name} returned invalid price data for security #{symbol} on: #{date}. Price data: #{price.inspect}")
Sentry.capture_exception(InvalidSecurityPriceError.new("#{self.class.name} returned invalid security price data"), level: :warning) do |scope|
scope.set_context("security", { symbol: symbol, date: date })
end
next
end
Price.new(
symbol: symbol,
date: date.to_date,
price: price,
currency: currency,
exchange_operating_mic: exchange_operating_mic
)
end.compact
end
end
private
attr_reader :api_key
def base_url
ENV["SYNTH_URL"] || "https://api.synthfinance.com"
end
def app_name
"maybe_app"
end
def app_type
Rails.application.config.app_mode
end
def client
@client ||= Faraday.new(url: base_url) do |faraday|
faraday.request(:retry, {
max: 2,
interval: 0.05,
interval_randomness: 0.5,
backoff_factor: 2
})
faraday.response :raise_error
faraday.headers["Authorization"] = "Bearer #{api_key}"
faraday.headers["X-Source"] = app_name
faraday.headers["X-Source-Type"] = app_type
end
end
def fetch_page(url, page, params = {})
client.get(url, params.merge(page: page))
end
def paginate(url, params = {})
results = []
page = 1
current_page = 0
total_pages = 1
first_page = nil
while current_page < total_pages
response = fetch_page(url, page, params)
body = JSON.parse(response.body)
first_page = body unless first_page
page_results = yield(body)
results.concat(page_results)
current_page = body.dig("paging", "current_page")
total_pages = body.dig("paging", "total_pages")
page += 1
end
PaginatedData.new(
paginated: results,
first_page: first_page,
total_pages: total_pages
)
end
end

View File

@@ -18,7 +18,7 @@ class Security < ApplicationRecord
end
def to_combobox_option
SynthComboboxOption.new(
ComboboxOption.new(
symbol: ticker,
name: name,
logo_url: logo_url,

View File

@@ -0,0 +1,13 @@
class Security::ComboboxOption
include ActiveModel::Model
attr_accessor :symbol, :name, :logo_url, :exchange_operating_mic, :country_code
def id
"#{symbol}|#{exchange_operating_mic}"
end
def to_combobox_display
"#{symbol} - #{name} (#{exchange_operating_mic})"
end
end

View File

@@ -1,13 +0,0 @@
class Security::SynthComboboxOption
include ActiveModel::Model
attr_accessor :symbol, :name, :logo_url, :exchange_operating_mic, :country_code
def id
"#{symbol}|#{exchange_operating_mic}" # submitted by combobox as value
end
def to_combobox_display
"#{symbol} - #{name} (#{exchange_operating_mic})" # shown in combobox input when selected
end
end

View File

@@ -2,7 +2,6 @@
class Setting < RailsSettings::Base
cache_prefix { "v1" }
field :synth_api_key, type: :string, default: ENV["SYNTH_API_KEY"]
field :twelve_data_api_key, type: :string, default: ENV["TWELVE_DATA_API_KEY"]
field :openai_access_token, type: :string, default: ENV["OPENAI_ACCESS_TOKEN"]

View File

@@ -18,7 +18,7 @@ class Trade::CreateForm
end
private
# Users can either look up a ticker from our provider (Synth) or enter a manual, "offline" ticker (that we won't fetch prices for)
# Users can either look up a ticker from a provider or enter a manual, "offline" ticker (that we won't fetch prices for)
def security
ticker_symbol, exchange_operating_mic = ticker.present? ? ticker.split("|") : [ manual_ticker, nil ]

View File

@@ -7,9 +7,7 @@
"full" => "w-full h-full"
} %>
<% if account.plaid_account_id? && account.institution_domain.present? %>
<%= image_tag "https://logo.synthfinance.com/#{account.institution_domain}", class: "shrink-0 rounded-full #{size_classes[size]}" %>
<% elsif account.logo.attached? %>
<% if account.logo.attached? %>
<%= image_tag account.logo, class: "shrink-0 rounded-full #{size_classes[size]}" %>
<% else %>
<%= render DS::FilledIcon.new(variant: :text, hex_color: color || account.accountable.color, text: account.name, size: size, rounded: true) %>

View File

@@ -3,7 +3,9 @@
<%= turbo_frame_tag dom_id(holding) do %>
<div class="grid grid-cols-12 items-center text-primary text-sm font-medium p-4">
<div class="col-span-4 flex items-center gap-4">
<%= image_tag "https://logo.synthfinance.com/ticker/#{holding.ticker}", class: "w-9 h-9 rounded-full", loading: "lazy" %>
<% if holding.security.logo_url.present? %>
<%= image_tag holding.security.logo_url, class: "w-9 h-9 rounded-full", loading: "lazy" %>
<% end %>
<div class="space-y-0.5">
<%= link_to holding.name, holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %>

View File

@@ -6,7 +6,9 @@
<%= tag.p @holding.ticker, class: "text-sm text-secondary" %>
</div>
<%= image_tag "https://logo.synthfinance.com/ticker/#{@holding.ticker}", loading: "lazy", class: "w-9 h-9 rounded-full" %>
<% if @holding.security.logo_url.present? %>
<%= image_tag @holding.security.logo_url, loading: "lazy", class: "w-9 h-9 rounded-full" %>
<% end %>
</div>
<% end %>

View File

@@ -1,49 +0,0 @@
<div class="space-y-4">
<div>
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
<% if ENV["SYNTH_API_KEY"].present? %>
<p class="text-sm text-secondary">You have successfully configured your Synth API key through the SYNTH_API_KEY environment variable.</p>
<% else %>
<p class="text-secondary text-sm mb-4"><%= t(".description") %></p>
<% end %>
</div>
<%= styled_form_with model: Setting.new,
url: settings_hosting_path,
method: :patch,
data: {
controller: "auto-submit-form",
"auto-submit-form-trigger-event-value": "blur"
} do |form| %>
<%= form.text_field :synth_api_key,
label: t(".label"),
type: "password",
placeholder: t(".placeholder"),
value: ENV.fetch("SYNTH_API_KEY", Setting.synth_api_key),
disabled: ENV["SYNTH_API_KEY"].present?,
container_class: @synth_usage.present? && !@synth_usage.success? ? "border-red-500" : "",
data: { "auto-submit-form-target": "auto" } %>
<% end %>
<% if @synth_usage.present? && @synth_usage.success? %>
<div class="space-y-4">
<div class="space-y-2">
<p class="text-sm text-secondary">
<%= t(".api_calls_used",
used: number_with_delimiter(@synth_usage.data.used),
limit: number_with_delimiter(@synth_usage.data.limit),
percentage: number_to_percentage(@synth_usage.data.utilization, precision: 1)) %>
</p>
<div class="w-52 h-1.5 bg-gray-100 rounded-2xl">
<div class="h-full bg-green-500 rounded-2xl"
style="width: <%= [@synth_usage.data.utilization, 2].max %>%;"></div>
</div>
</div>
<div class="bg-gray-100 rounded-md px-1.5 py-0.5 w-fit">
<p class="text-xs font-medium text-secondary uppercase">
<%= t(".plan", plan: @synth_usage.data.plan) %>
</p>
</div>
</div>
<% end %>
</div>

View File

@@ -1,9 +1,6 @@
<%= content_for :page_title, t(".title") %>
<%= settings_section title: t(".general") do %>
<div class="space-y-6">
<%= render "settings/hostings/synth_settings" %>
</div>
<div class="space-y-6">
<%= render "settings/hostings/twelve_data_settings" %>
</div>