mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -18,7 +18,7 @@ class Security < ApplicationRecord
|
||||
end
|
||||
|
||||
def to_combobox_option
|
||||
SynthComboboxOption.new(
|
||||
ComboboxOption.new(
|
||||
symbol: ticker,
|
||||
name: name,
|
||||
logo_url: logo_url,
|
||||
|
||||
13
app/models/security/combobox_option.rb
Normal file
13
app/models/security/combobox_option.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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 ]
|
||||
|
||||
|
||||
@@ -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) %>
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user