mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-21 22:29:38 +02:00
Show UI warning to user when they need provider data but have not setup Synth yet (#1926)
* Simplify provider concerns * Update tests * Add UI warning for missing Synth key if family requires external data
This commit is contained in:
parent
624faa10d0
commit
fa0248056d
22 changed files with 184 additions and 136 deletions
|
@ -3,7 +3,7 @@ class SecuritiesController < ApplicationController
|
||||||
query = params[:q]
|
query = params[:q]
|
||||||
return render json: [] if query.blank? || query.length < 2 || query.length > 100
|
return render json: [] if query.blank? || query.length < 2 || query.length > 100
|
||||||
|
|
||||||
@securities = Security.search({
|
@securities = Security.search_provider({
|
||||||
search: query,
|
search: query,
|
||||||
country: params[:country_code] == "US" ? "US" : nil
|
country: params[:country_code] == "US" ? "US" : nil
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,7 @@ class FetchSecurityInfoJob < ApplicationJob
|
||||||
queue_as :latency_low
|
queue_as :latency_low
|
||||||
|
|
||||||
def perform(security_id)
|
def perform(security_id)
|
||||||
return unless Security.security_info_provider.present?
|
return unless Security.provider.present?
|
||||||
|
|
||||||
security = Security.find(security_id)
|
security = Security.find(security_id)
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ class FetchSecurityInfoJob < ApplicationJob
|
||||||
params[:mic_code] = security.exchange_mic if security.exchange_mic.present?
|
params[:mic_code] = security.exchange_mic if security.exchange_mic.present?
|
||||||
params[:operating_mic] = security.exchange_operating_mic if security.exchange_operating_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(
|
security.update(
|
||||||
name: security_info_response.info.dig("name")
|
name: security_info_response.info.dig("name")
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
class Account::DataEnricher
|
class Account::DataEnricher
|
||||||
include Providable
|
|
||||||
|
|
||||||
attr_reader :account
|
attr_reader :account
|
||||||
|
|
||||||
def initialize(account)
|
def initialize(account)
|
||||||
|
@ -37,7 +35,7 @@ class Account::DataEnricher
|
||||||
|
|
||||||
candidates.each do |entry|
|
candidates.each do |entry|
|
||||||
begin
|
begin
|
||||||
info = self.class.synth_provider.enrich_transaction(entry.name).info
|
info = entry.fetch_enrichment_info
|
||||||
|
|
||||||
next unless info.present?
|
next unless info.present?
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class Account::Entry < ApplicationRecord
|
class Account::Entry < ApplicationRecord
|
||||||
include Monetizable
|
include Monetizable, Provided
|
||||||
|
|
||||||
monetize :amount
|
monetize :amount
|
||||||
|
|
||||||
|
|
11
app/models/account/entry/provided.rb
Normal file
11
app/models/account/entry/provided.rb
Normal file
|
@ -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
|
|
@ -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
|
|
37
app/models/concerns/synthable.rb
Normal file
37
app/models/concerns/synthable.rb
Normal file
|
@ -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
|
|
@ -1,19 +1,18 @@
|
||||||
module ExchangeRate::Provided
|
module ExchangeRate::Provided
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include Providable
|
include Synthable
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def provider_healthy?
|
def provider
|
||||||
exchange_rates_provider.present? && exchange_rates_provider.healthy?
|
synth_client
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_rates_from_provider(from:, to:, start_date:, end_date: Date.current, cache: false)
|
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,
|
from: from,
|
||||||
to: to,
|
to: to,
|
||||||
start_date: start_date,
|
start_date: start_date,
|
||||||
|
@ -38,9 +37,9 @@ module ExchangeRate::Provided
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_rate_from_provider(from:, to:, date:, cache: false)
|
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,
|
from: from,
|
||||||
to: to,
|
to: to,
|
||||||
date: date
|
date: date
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class Family < ApplicationRecord
|
class Family < ApplicationRecord
|
||||||
include Providable, Plaidable, Syncable, AutoTransferMatchable
|
include Synthable, Plaidable, Syncable, AutoTransferMatchable
|
||||||
|
|
||||||
DATE_FORMATS = [
|
DATE_FORMATS = [
|
||||||
[ "MM-DD-YYYY", "%m-%d-%Y" ],
|
[ "MM-DD-YYYY", "%m-%d-%Y" ],
|
||||||
|
@ -92,22 +92,25 @@ class Family < ApplicationRecord
|
||||||
).link_token
|
).link_token
|
||||||
end
|
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?
|
def subscribed?
|
||||||
stripe_subscription_status == "active"
|
stripe_subscription_status == "active"
|
||||||
end
|
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
|
def primary_user
|
||||||
users.order(:created_at).first
|
users.order(:created_at).first
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
class Security < ApplicationRecord
|
class Security < ApplicationRecord
|
||||||
include Providable
|
include Provided
|
||||||
|
|
||||||
before_save :upcase_ticker
|
before_save :upcase_ticker
|
||||||
|
|
||||||
|
@ -9,21 +9,6 @@ class Security < ApplicationRecord
|
||||||
validates :ticker, presence: true
|
validates :ticker, presence: true
|
||||||
validates :ticker, uniqueness: { scope: :exchange_operating_mic, case_sensitive: false }
|
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
|
def current_price
|
||||||
@current_price ||= Security::Price.find_price(security: self, date: Date.current)
|
@current_price ||= Security::Price.find_price(security: self, date: Date.current)
|
||||||
return nil if @current_price.nil?
|
return nil if @current_price.nil?
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
module Security::Price::Provided
|
module Security::Price::Provided
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include Providable
|
include Synthable
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
private
|
def provider
|
||||||
|
synth_client
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
def fetch_price_from_provider(security:, date:, cache: false)
|
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?
|
return nil unless security.has_prices?
|
||||||
|
|
||||||
response = security_prices_provider.fetch_security_prices \
|
response = provider.fetch_security_prices \
|
||||||
ticker: security.ticker,
|
ticker: security.ticker,
|
||||||
mic_code: security.exchange_mic,
|
mic_code: security.exchange_mic,
|
||||||
start_date: date,
|
start_date: date,
|
||||||
|
@ -31,11 +34,11 @@ module Security::Price::Provided
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_prices_from_provider(security:, start_date:, end_date:, cache: false)
|
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
|
||||||
return [] unless security.has_prices?
|
return [] unless security.has_prices?
|
||||||
|
|
||||||
response = security_prices_provider.fetch_security_prices \
|
response = provider.fetch_security_prices \
|
||||||
ticker: security.ticker,
|
ticker: security.ticker,
|
||||||
mic_code: security.exchange_mic,
|
mic_code: security.exchange_mic,
|
||||||
start_date: start_date,
|
start_date: start_date,
|
||||||
|
|
26
app/models/security/provided.rb
Normal file
26
app/models/security/provided.rb
Normal file
|
@ -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
|
|
@ -75,10 +75,7 @@ class TradeImport < Import
|
||||||
return internal_security if internal_security.present?
|
return internal_security if internal_security.present?
|
||||||
|
|
||||||
# If security prices provider isn't properly configured or available, create with nil exchange_operating_mic
|
# If security prices provider isn't properly configured or available, create with nil exchange_operating_mic
|
||||||
provider = Security.security_prices_provider
|
return Security.find_or_create_by!(ticker: ticker, exchange_operating_mic: nil) unless Security.provider.present?
|
||||||
unless provider.present? && provider.respond_to?(:search_securities)
|
|
||||||
return Security.find_or_create_by!(ticker: ticker, exchange_operating_mic: nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Cache provider responses so that when we're looping through rows and importing,
|
# 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
|
# 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_securities_cache ||= {}
|
||||||
|
|
||||||
provider_security = @provider_securities_cache[cache_key] ||= begin
|
provider_security = @provider_securities_cache[cache_key] ||= begin
|
||||||
response = provider.search_securities(
|
Security.search_provider(
|
||||||
query: ticker,
|
query: ticker,
|
||||||
exchange_operating_mic: exchange_operating_mic
|
exchange_operating_mic: exchange_operating_mic
|
||||||
)
|
).first
|
||||||
|
|
||||||
if !response || !response.success? || !response.securities || response.securities.empty?
|
|
||||||
nil
|
|
||||||
else
|
|
||||||
response.securities.first
|
|
||||||
end
|
|
||||||
rescue => e
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return Security.find_or_create_by!(ticker: ticker, exchange_operating_mic: nil) if provider_security.nil?
|
return Security.find_or_create_by!(ticker: ticker, exchange_operating_mic: nil) if provider_security.nil?
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
module Upgrader::Provided
|
module Upgrader::Provided
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include Providable
|
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
private
|
private
|
||||||
def fetch_latest_upgrade_candidates_from_provider
|
def fetch_latest_upgrade_candidates_from_provider
|
||||||
git_repository_provider.fetch_latest_upgrade_candidates
|
git_repository_provider.fetch_latest_upgrade_candidates
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def git_repository_provider
|
||||||
|
Provider::Github.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,25 @@
|
||||||
<%# locals: (family:) %>
|
<%# locals: (family:) %>
|
||||||
|
|
||||||
|
<% if family.requires_data_provider? && family.synth_client.nil? %>
|
||||||
|
<details class="group bg-yellow-tint-10 rounded-lg p-2 text-yellow-600 mb-3 text-xs">
|
||||||
|
<summary class="flex items-center justify-between gap-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<%= icon "triangle-alert", size: "sm" %>
|
||||||
|
<p class="font-medium">Missing historical data</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= lucide_icon "chevron-down", class: "text-yellow-600 group-open:transform group-open:rotate-180 w-5" %>
|
||||||
|
</summary>
|
||||||
|
<div class="text-xs py-2 space-y-2">
|
||||||
|
<p>Maybe uses Synth API to fetch historical exchange rates, security prices, and more. This data is required to calculate accurate historical account balances.</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= link_to "Add your Synth API key here.", settings_hosting_path, class: "text-yellow-600 underline" %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="space-y-3"
|
class="space-y-3"
|
||||||
data-controller="tabs"
|
data-controller="tabs"
|
||||||
|
|
|
@ -23,11 +23,10 @@
|
||||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
||||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
||||||
|
|
||||||
<% if Security.security_prices_provider.nil? %>
|
<% unless Security.provider %>
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<p>
|
<p>
|
||||||
<strong>Note:</strong> The Synth provider is not configured. Exchange validation is disabled.
|
<strong>Note:</strong> 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.
|
||||||
Securities will be created without exchange validation, and price history will not be available.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
|
<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>
|
<p class="text-secondary text-sm mb-4"><%= t(".description") %></p>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= styled_form_with model: Setting.new,
|
<%= styled_form_with model: Setting.new,
|
||||||
|
@ -15,7 +19,8 @@
|
||||||
label: t(".label"),
|
label: t(".label"),
|
||||||
type: "password",
|
type: "password",
|
||||||
placeholder: t(".placeholder"),
|
placeholder: t(".placeholder"),
|
||||||
value: Setting.synth_api_key,
|
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" : "",
|
container_class: @synth_usage.present? && !@synth_usage.success? ? "border-red-500" : "",
|
||||||
data: { "auto-submit-form-target": "auto" } %>
|
data: { "auto-submit-form-target": "auto" } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -5,14 +5,16 @@ class ExchangeRateTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
@provider = mock
|
@provider = mock
|
||||||
|
|
||||||
ExchangeRate.stubs(:exchange_rates_provider).returns(@provider)
|
ExchangeRate.stubs(:provider).returns(@provider)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exchange rate provider nil if no api key configured" do
|
test "exchange rate provider nil if no api key configured" do
|
||||||
ExchangeRate.unstub(:exchange_rates_provider)
|
ExchangeRate.unstub(:provider)
|
||||||
|
|
||||||
|
Setting.stubs(:synth_api_key).returns(nil)
|
||||||
|
|
||||||
with_env_overrides SYNTH_API_KEY: nil do
|
with_env_overrides SYNTH_API_KEY: nil do
|
||||||
assert_not ExchangeRate.exchange_rates_provider
|
assert_not ExchangeRate.provider
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,7 +44,9 @@ class ExchangeRateTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "nil if rate is not found in DB and provider is disabled" do
|
test "nil if rate is not found in DB and provider is disabled" do
|
||||||
ExchangeRate.unstub(:exchange_rates_provider)
|
ExchangeRate.unstub(:provider)
|
||||||
|
|
||||||
|
Setting.stubs(:synth_api_key).returns(nil)
|
||||||
|
|
||||||
with_env_overrides SYNTH_API_KEY: nil do
|
with_env_overrides SYNTH_API_KEY: nil do
|
||||||
assert_not ExchangeRate.find_rate(from: "USD", to: "EUR", date: Date.current)
|
assert_not ExchangeRate.find_rate(from: "USD", to: "EUR", date: Date.current)
|
||||||
|
@ -102,7 +106,9 @@ class ExchangeRateTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns empty array if no rates found in DB or provider" do
|
test "returns empty array if no rates found in DB or provider" do
|
||||||
ExchangeRate.unstub(:exchange_rates_provider)
|
ExchangeRate.unstub(:provider)
|
||||||
|
|
||||||
|
Setting.stubs(:synth_api_key).returns(nil)
|
||||||
|
|
||||||
with_env_overrides SYNTH_API_KEY: nil do
|
with_env_overrides SYNTH_API_KEY: nil do
|
||||||
assert_equal [], ExchangeRate.find_rates(from: "USD", to: "JPY", start_date: 10.days.ago.to_date)
|
assert_equal [], ExchangeRate.find_rates(from: "USD", to: "JPY", start_date: 10.days.ago.to_date)
|
||||||
|
|
|
@ -5,14 +5,16 @@ class Security::PriceTest < ActiveSupport::TestCase
|
||||||
setup do
|
setup do
|
||||||
@provider = mock
|
@provider = mock
|
||||||
|
|
||||||
Security::Price.stubs(:security_prices_provider).returns(@provider)
|
Security::Price.stubs(:provider).returns(@provider)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "security price provider nil if no api key provided" do
|
test "security price provider nil if no api key provided" do
|
||||||
Security::Price.unstub(:security_prices_provider)
|
Security::Price.unstub(:provider)
|
||||||
|
|
||||||
|
Setting.stubs(:synth_api_key).returns(nil)
|
||||||
|
|
||||||
with_env_overrides SYNTH_API_KEY: nil do
|
with_env_overrides SYNTH_API_KEY: nil do
|
||||||
assert_not Security::Price.security_prices_provider
|
assert_not Security::Price.provider
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -60,7 +62,10 @@ class Security::PriceTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns nil if price not found in DB and provider disabled" do
|
test "returns nil if price not found in DB and provider disabled" do
|
||||||
Security::Price.unstub(:security_prices_provider)
|
Security::Price.unstub(:provider)
|
||||||
|
|
||||||
|
Setting.stubs(:synth_api_key).returns(nil)
|
||||||
|
|
||||||
security = Security.new(ticker: "NVDA")
|
security = Security.new(ticker: "NVDA")
|
||||||
|
|
||||||
with_env_overrides SYNTH_API_KEY: nil do
|
with_env_overrides SYNTH_API_KEY: nil do
|
||||||
|
@ -105,7 +110,9 @@ class Security::PriceTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns empty array if no prices found in DB or from provider" do
|
test "returns empty array if no prices found in DB or from provider" do
|
||||||
Security::Price.unstub(:security_prices_provider)
|
Security::Price.unstub(:provider)
|
||||||
|
|
||||||
|
Setting.stubs(:synth_api_key).returns(nil)
|
||||||
|
|
||||||
with_env_overrides SYNTH_API_KEY: nil do
|
with_env_overrides SYNTH_API_KEY: nil do
|
||||||
assert_equal [], Security::Price.find_prices(security: Security.new(ticker: "NVDA"), start_date: 10.days.ago.to_date, end_date: Date.current)
|
assert_equal [], Security::Price.find_prices(security: Security.new(ticker: "NVDA"), start_date: 10.days.ago.to_date, end_date: Date.current)
|
||||||
|
|
|
@ -12,30 +12,20 @@ class TradeImportTest < ActiveSupport::TestCase
|
||||||
# Create an existing AAPL security with no exchange_operating_mic
|
# Create an existing AAPL security with no exchange_operating_mic
|
||||||
aapl = Security.create!(ticker: "AAPL", exchange_operating_mic: nil)
|
aapl = Security.create!(ticker: "AAPL", exchange_operating_mic: nil)
|
||||||
|
|
||||||
provider = mock
|
|
||||||
|
|
||||||
# We should only hit the provider for GOOGL since AAPL already exists
|
# We should only hit the provider for GOOGL since AAPL already exists
|
||||||
provider.expects(:search_securities).with(
|
Security.expects(:search_provider).with(
|
||||||
query: "GOOGL",
|
query: "GOOGL",
|
||||||
exchange_operating_mic: "XNAS"
|
exchange_operating_mic: "XNAS"
|
||||||
).returns(
|
).returns([
|
||||||
OpenStruct.new(
|
Security.new(
|
||||||
securities: [
|
|
||||||
{
|
|
||||||
ticker: "GOOGL",
|
ticker: "GOOGL",
|
||||||
name: "Google Inc.",
|
name: "Google Inc.",
|
||||||
country_code: "US",
|
country_code: "US",
|
||||||
exchange_mic: "XNGS",
|
exchange_mic: "XNGS",
|
||||||
exchange_operating_mic: "XNAS",
|
exchange_operating_mic: "XNAS",
|
||||||
exchange_acronym: "NGS"
|
exchange_acronym: "NGS"
|
||||||
}
|
|
||||||
],
|
|
||||||
success?: true,
|
|
||||||
raw_response: nil
|
|
||||||
)
|
)
|
||||||
).once
|
]).once
|
||||||
|
|
||||||
Security.stubs(:security_prices_provider).returns(provider)
|
|
||||||
|
|
||||||
import = <<~CSV
|
import = <<~CSV
|
||||||
date,ticker,qty,price,currency,account,name,exchange_operating_mic
|
date,ticker,qty,price,currency,account,name,exchange_operating_mic
|
||||||
|
|
|
@ -52,6 +52,8 @@ class ImportsTest < ApplicationSystemTestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "trade import" do
|
test "trade import" do
|
||||||
|
Security.stubs(:search_provider).returns([])
|
||||||
|
|
||||||
visit new_import_path
|
visit new_import_path
|
||||||
|
|
||||||
click_on "Import investments"
|
click_on "Import investments"
|
||||||
|
|
|
@ -10,7 +10,7 @@ class TradesTest < ApplicationSystemTestCase
|
||||||
|
|
||||||
visit_account_portfolio
|
visit_account_portfolio
|
||||||
|
|
||||||
Security.stubs(:search).returns([
|
Security.stubs(:search_provider).returns([
|
||||||
Security.new(
|
Security.new(
|
||||||
ticker: "AAPL",
|
ticker: "AAPL",
|
||||||
name: "Apple Inc.",
|
name: "Apple Inc.",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue