diff --git a/app/controllers/securities_controller.rb b/app/controllers/securities_controller.rb
index 5be3cbd9..6cfe9fd0 100644
--- a/app/controllers/securities_controller.rb
+++ b/app/controllers/securities_controller.rb
@@ -3,7 +3,7 @@ class SecuritiesController < ApplicationController
query = params[:q]
return render json: [] if query.blank? || query.length < 2 || query.length > 100
- @securities = Security.search({
+ @securities = Security.search_provider({
search: query,
country: params[:country_code] == "US" ? "US" : nil
})
diff --git a/app/jobs/fetch_security_info_job.rb b/app/jobs/fetch_security_info_job.rb
index aa64c169..484a47e1 100644
--- a/app/jobs/fetch_security_info_job.rb
+++ b/app/jobs/fetch_security_info_job.rb
@@ -2,7 +2,7 @@ class FetchSecurityInfoJob < ApplicationJob
queue_as :latency_low
def perform(security_id)
- return unless Security.security_info_provider.present?
+ return unless Security.provider.present?
security = Security.find(security_id)
@@ -12,7 +12,7 @@ class FetchSecurityInfoJob < ApplicationJob
params[:mic_code] = security.exchange_mic if security.exchange_mic.present?
params[:operating_mic] = security.exchange_operating_mic if security.exchange_operating_mic.present?
- security_info_response = Security.security_info_provider.fetch_security_info(**params)
+ security_info_response = Security.provider.fetch_security_info(**params)
security.update(
name: security_info_response.info.dig("name")
diff --git a/app/models/account/data_enricher.rb b/app/models/account/data_enricher.rb
index 59979df0..8d07eff8 100644
--- a/app/models/account/data_enricher.rb
+++ b/app/models/account/data_enricher.rb
@@ -1,6 +1,4 @@
class Account::DataEnricher
- include Providable
-
attr_reader :account
def initialize(account)
@@ -37,7 +35,7 @@ class Account::DataEnricher
candidates.each do |entry|
begin
- info = self.class.synth_provider.enrich_transaction(entry.name).info
+ info = entry.fetch_enrichment_info
next unless info.present?
diff --git a/app/models/account/entry.rb b/app/models/account/entry.rb
index b53db19b..25065efd 100644
--- a/app/models/account/entry.rb
+++ b/app/models/account/entry.rb
@@ -1,5 +1,5 @@
class Account::Entry < ApplicationRecord
- include Monetizable
+ include Monetizable, Provided
monetize :amount
diff --git a/app/models/account/entry/provided.rb b/app/models/account/entry/provided.rb
new file mode 100644
index 00000000..c18654c9
--- /dev/null
+++ b/app/models/account/entry/provided.rb
@@ -0,0 +1,11 @@
+module Account::Entry::Provided
+ extend ActiveSupport::Concern
+
+ include Synthable
+
+ def fetch_enrichment_info
+ return nil unless synth_client.present?
+
+ synth_client.enrich_transaction(name).info
+ end
+end
diff --git a/app/models/concerns/providable.rb b/app/models/concerns/providable.rb
deleted file mode 100644
index 996efff8..00000000
--- a/app/models/concerns/providable.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# `Providable` serves as an extension point for integrating multiple providers.
-# For an example of a multi-provider, multi-concept implementation,
-# see: https://github.com/maybe-finance/maybe/pull/561
-
-module Providable
- extend ActiveSupport::Concern
-
- class_methods do
- def security_prices_provider
- synth_provider
- end
-
- def security_info_provider
- synth_provider
- end
-
- def exchange_rates_provider
- synth_provider
- end
-
- def git_repository_provider
- Provider::Github.new
- end
-
- def synth_provider
- api_key = self_hosted? ? Setting.synth_api_key : ENV["SYNTH_API_KEY"]
- api_key.present? ? Provider::Synth.new(api_key) : nil
- end
-
- private
- def self_hosted?
- Rails.application.config.app_mode.self_hosted?
- end
- end
-end
diff --git a/app/models/concerns/synthable.rb b/app/models/concerns/synthable.rb
new file mode 100644
index 00000000..51adcade
--- /dev/null
+++ b/app/models/concerns/synthable.rb
@@ -0,0 +1,37 @@
+module Synthable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def synth_usage
+ synth_client&.usage
+ end
+
+ def synth_overage?
+ synth_usage&.usage&.utilization.to_i >= 100
+ end
+
+ def synth_healthy?
+ synth_client&.healthy?
+ end
+
+ def synth_client
+ api_key = ENV.fetch("SYNTH_API_KEY", Setting.synth_api_key)
+
+ return nil unless api_key.present?
+
+ Provider::Synth.new(api_key)
+ end
+ end
+
+ def synth_client
+ self.class.synth_client
+ end
+
+ def synth_usage
+ self.class.synth_usage
+ end
+
+ def synth_overage?
+ self.class.synth_overage?
+ end
+end
diff --git a/app/models/exchange_rate/provided.rb b/app/models/exchange_rate/provided.rb
index d1e2aea2..d010ff98 100644
--- a/app/models/exchange_rate/provided.rb
+++ b/app/models/exchange_rate/provided.rb
@@ -1,19 +1,18 @@
module ExchangeRate::Provided
extend ActiveSupport::Concern
- include Providable
+ include Synthable
class_methods do
- def provider_healthy?
- exchange_rates_provider.present? && exchange_rates_provider.healthy?
+ def provider
+ synth_client
end
private
-
def fetch_rates_from_provider(from:, to:, start_date:, end_date: Date.current, cache: false)
- return [] unless exchange_rates_provider.present?
+ return [] unless provider.present?
- response = exchange_rates_provider.fetch_exchange_rates \
+ response = provider.fetch_exchange_rates \
from: from,
to: to,
start_date: start_date,
@@ -38,9 +37,9 @@ module ExchangeRate::Provided
end
def fetch_rate_from_provider(from:, to:, date:, cache: false)
- return nil unless exchange_rates_provider.present?
+ return nil unless provider.present?
- response = exchange_rates_provider.fetch_exchange_rate \
+ response = provider.fetch_exchange_rate \
from: from,
to: to,
date: date
diff --git a/app/models/family.rb b/app/models/family.rb
index ff2c8b07..ffb14a7d 100644
--- a/app/models/family.rb
+++ b/app/models/family.rb
@@ -1,5 +1,5 @@
class Family < ApplicationRecord
- include Providable, Plaidable, Syncable, AutoTransferMatchable
+ include Synthable, Plaidable, Syncable, AutoTransferMatchable
DATE_FORMATS = [
[ "MM-DD-YYYY", "%m-%d-%Y" ],
@@ -92,22 +92,25 @@ class Family < ApplicationRecord
).link_token
end
- def synth_usage
- self.class.synth_provider&.usage
- end
-
- def synth_overage?
- self.class.synth_provider&.usage&.utilization.to_i >= 100
- end
-
- def synth_valid?
- self.class.synth_provider&.healthy?
- end
-
def subscribed?
stripe_subscription_status == "active"
end
+ def requires_data_provider?
+ # If family has any trades, they need a provider for historical prices
+ return true if trades.any?
+
+ # If family has any accounts not denominated in the family's currency, they need a provider for historical exchange rates
+ return true if accounts.where.not(currency: self.currency).any?
+
+ # If family has any entries in different currencies, they need a provider for historical exchange rates
+ uniq_currencies = entries.pluck(:currency).uniq
+ return true if uniq_currencies.count > 1
+ return true if uniq_currencies.first != self.currency
+
+ false
+ end
+
def primary_user
users.order(:created_at).first
end
diff --git a/app/models/security.rb b/app/models/security.rb
index 991ac202..6d94c798 100644
--- a/app/models/security.rb
+++ b/app/models/security.rb
@@ -1,5 +1,5 @@
class Security < ApplicationRecord
- include Providable
+ include Provided
before_save :upcase_ticker
@@ -9,21 +9,6 @@ class Security < ApplicationRecord
validates :ticker, presence: true
validates :ticker, uniqueness: { scope: :exchange_operating_mic, case_sensitive: false }
- class << self
- def provider
- security_prices_provider
- end
-
- def search(query)
- security_prices_provider.search_securities(
- query: query[:search],
- dataset: "limited",
- country_code: query[:country],
- exchange_operating_mic: query[:exchange_operating_mic]
- ).securities.map { |attrs| new(**attrs) }
- end
- end
-
def current_price
@current_price ||= Security::Price.find_price(security: self, date: Date.current)
return nil if @current_price.nil?
diff --git a/app/models/security/price/provided.rb b/app/models/security/price/provided.rb
index e2a99774..aed56702 100644
--- a/app/models/security/price/provided.rb
+++ b/app/models/security/price/provided.rb
@@ -1,16 +1,19 @@
module Security::Price::Provided
extend ActiveSupport::Concern
- include Providable
+ include Synthable
class_methods do
- private
+ def provider
+ synth_client
+ end
+ private
def fetch_price_from_provider(security:, date:, cache: false)
- return nil unless security_prices_provider.present?
+ return nil unless provider.present?
return nil unless security.has_prices?
- response = security_prices_provider.fetch_security_prices \
+ response = provider.fetch_security_prices \
ticker: security.ticker,
mic_code: security.exchange_mic,
start_date: date,
@@ -31,11 +34,11 @@ module Security::Price::Provided
end
def fetch_prices_from_provider(security:, start_date:, end_date:, cache: false)
- return [] unless security_prices_provider.present?
+ return [] unless provider.present?
return [] unless security
return [] unless security.has_prices?
- response = security_prices_provider.fetch_security_prices \
+ response = provider.fetch_security_prices \
ticker: security.ticker,
mic_code: security.exchange_mic,
start_date: start_date,
diff --git a/app/models/security/provided.rb b/app/models/security/provided.rb
new file mode 100644
index 00000000..4a4fd6a5
--- /dev/null
+++ b/app/models/security/provided.rb
@@ -0,0 +1,26 @@
+module Security::Provided
+ extend ActiveSupport::Concern
+
+ include Synthable
+
+ class_methods do
+ def provider
+ synth_client
+ end
+
+ def search_provider(query)
+ response = provider.search_securities(
+ query: query[:search],
+ dataset: "limited",
+ country_code: query[:country],
+ exchange_operating_mic: query[:exchange_operating_mic]
+ )
+
+ if response.success?
+ response.securities.map { |attrs| new(**attrs) }
+ else
+ []
+ end
+ end
+ end
+end
diff --git a/app/models/trade_import.rb b/app/models/trade_import.rb
index ddaad904..549c9093 100644
--- a/app/models/trade_import.rb
+++ b/app/models/trade_import.rb
@@ -75,10 +75,7 @@ class TradeImport < Import
return internal_security if internal_security.present?
# If security prices provider isn't properly configured or available, create with nil exchange_operating_mic
- provider = Security.security_prices_provider
- unless provider.present? && provider.respond_to?(:search_securities)
- return Security.find_or_create_by!(ticker: ticker, exchange_operating_mic: nil)
- end
+ return Security.find_or_create_by!(ticker: ticker, exchange_operating_mic: nil) unless Security.provider.present?
# Cache provider responses so that when we're looping through rows and importing,
# we only hit our provider for the unique combinations of ticker / exchange_operating_mic
@@ -86,18 +83,10 @@ class TradeImport < Import
@provider_securities_cache ||= {}
provider_security = @provider_securities_cache[cache_key] ||= begin
- response = provider.search_securities(
+ Security.search_provider(
query: ticker,
exchange_operating_mic: exchange_operating_mic
- )
-
- if !response || !response.success? || !response.securities || response.securities.empty?
- nil
- else
- response.securities.first
- end
- rescue => e
- nil
+ ).first
end
return Security.find_or_create_by!(ticker: ticker, exchange_operating_mic: nil) if provider_security.nil?
diff --git a/app/models/upgrader/provided.rb b/app/models/upgrader/provided.rb
index 4b518e51..fc1e65b7 100644
--- a/app/models/upgrader/provided.rb
+++ b/app/models/upgrader/provided.rb
@@ -1,11 +1,14 @@
module Upgrader::Provided
extend ActiveSupport::Concern
- include Providable
class_methods do
private
def fetch_latest_upgrade_candidates_from_provider
git_repository_provider.fetch_latest_upgrade_candidates
end
+
+ def git_repository_provider
+ Provider::Github.new
+ end
end
end
diff --git a/app/views/accounts/_account_sidebar_tabs.html.erb b/app/views/accounts/_account_sidebar_tabs.html.erb
index d42fab86..bb409492 100644
--- a/app/views/accounts/_account_sidebar_tabs.html.erb
+++ b/app/views/accounts/_account_sidebar_tabs.html.erb
@@ -1,5 +1,25 @@
<%# locals: (family:) %>
+<% if family.requires_data_provider? && family.synth_client.nil? %>
+ Missing historical data Maybe uses Synth API to fetch historical exchange rates, security prices, and more. This data is required to calculate accurate historical account balances.
+ <%= link_to "Add your Synth API key here.", settings_hosting_path, class: "text-yellow-600 underline" %>
+
+
+
- Note: The Synth provider is not configured. Exchange validation is disabled. - Securities will be created without exchange validation, and price history will not be available. + Note: The security prices provider is not configured. Your trade imports will work, but Maybe will not backfill price history. Please go to your settings to configure this.
<%= t(".description") %>
+ <% if ENV["SYNTH_API_KEY"].present? %> +You have successfully configured your Synth API key through the SYNTH_API_KEY environment variable.
+ <% else %> +<%= t(".description") %>
+ <% end %>