diff --git a/app/controllers/settings/hostings_controller.rb b/app/controllers/settings/hostings_controller.rb index beda30f75..5fb2869b5 100644 --- a/app/controllers/settings/hostings_controller.rb +++ b/app/controllers/settings/hostings_controller.rb @@ -19,6 +19,10 @@ class Settings::HostingsController < ApplicationController Setting.require_email_confirmation = hosting_params[:require_email_confirmation] end + if hosting_params.key?(:brand_fetch_client_id) + Setting.brand_fetch_client_id = hosting_params[:brand_fetch_client_id] + end + if hosting_params.key?(:twelve_data_api_key) Setting.twelve_data_api_key = hosting_params[:twelve_data_api_key] end @@ -36,7 +40,7 @@ class Settings::HostingsController < ApplicationController private def hosting_params - params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :twelve_data_api_key) + params.require(:setting).permit(:require_invite_for_signup, :require_email_confirmation, :brand_fetch_client_id, :twelve_data_api_key) end def ensure_admin diff --git a/app/models/family/auto_merchant_detector.rb b/app/models/family/auto_merchant_detector.rb index bea5dab32..423d1cdef 100644 --- a/app/models/family/auto_merchant_detector.rb +++ b/app/models/family/auto_merchant_detector.rb @@ -31,12 +31,14 @@ class Family::AutoMerchantDetector merchant_id = user_merchants_input.find { |m| m[:name] == auto_detection&.business_name }&.dig(:id) - if merchant_id.nil? && auto_detection&.business_url.present? && auto_detection&.business_name.present? + if merchant_id.nil? && auto_detection&.business_url.present? && auto_detection&.business_name.present? && Setting.brand_fetch_client_id.present? ai_provider_merchant = ProviderMerchant.find_or_create_by!( 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}/icon/fallback/lettermark/w/40/h/40?c=#{Setting.brand_fetch_client_id}" + end end merchant_id = merchant_id || ai_provider_merchant&.id @@ -63,6 +65,9 @@ class Family::AutoMerchantDetector Provider::Registry.get_provider(:openai) end + def default_logo_provider_url + "https://cdn.brandfetch.io" + end def user_merchants_input family.merchants.map do |merchant| diff --git a/app/models/setting.rb b/app/models/setting.rb index fa3dba1b4..1be168aa9 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -4,6 +4,7 @@ class Setting < RailsSettings::Base field :twelve_data_api_key, type: :string, default: ENV["TWELVE_DATA_API_KEY"] field :openai_access_token, type: :string, default: ENV["OPENAI_ACCESS_TOKEN"] + field :brand_fetch_client_id, type: :string, default: ENV["BRAND_FETCH_CLIENT_ID"] field :require_invite_for_signup, type: :boolean, default: false field :require_email_confirmation, type: :boolean, default: ENV.fetch("REQUIRE_EMAIL_CONFIRMATION", "true") == "true" diff --git a/app/views/accounts/_logo.html.erb b/app/views/accounts/_logo.html.erb index 40d9d4d0a..2893b1772 100644 --- a/app/views/accounts/_logo.html.erb +++ b/app/views/accounts/_logo.html.erb @@ -7,7 +7,9 @@ "full" => "w-full h-full" } %> -<% if account.logo.attached? %> +<% if account.plaid_account_id? && account.institution_domain.present? && Setting.brand_fetch_client_id.present? %> + <%= image_tag "https://cdn.brandfetch.io/#{account.institution_domain}/icon/fallback/lettermark/w/40/h/40?c=#{Setting.brand_fetch_client_id}", class: "shrink-0 rounded-full #{size_classes[size]}" %> +<% elsif 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) %> diff --git a/app/views/holdings/_holding.html.erb b/app/views/holdings/_holding.html.erb index 3b58fe284..7c03d86da 100644 --- a/app/views/holdings/_holding.html.erb +++ b/app/views/holdings/_holding.html.erb @@ -3,10 +3,13 @@ <%= turbo_frame_tag dom_id(holding) do %>
- <% if holding.security.logo_url.present? %> + <% if Setting.brand_fetch_client_id.present? %> + <%= image_tag "https://cdn.brandfetch.io/#{holding.ticker}/icon/fallback/lettermark/w/40/h/40?c=#{Setting.brand_fetch_client_id}", class: "w-9 h-9 rounded-full", loading: "lazy" %> + <% elsif holding.security.logo_url.present? %> <%= image_tag holding.security.logo_url, class: "w-9 h-9 rounded-full", loading: "lazy" %> + <% else %> + <%= render DS::FilledIcon.new(variant: :text, text: holding.name, size: "md", rounded: true) %> <% end %> -
<%= link_to holding.name, holding_path(holding), data: { turbo_frame: :drawer }, class: "hover:underline" %> diff --git a/app/views/holdings/show.html.erb b/app/views/holdings/show.html.erb index 1a612643d..4c1341ea6 100644 --- a/app/views/holdings/show.html.erb +++ b/app/views/holdings/show.html.erb @@ -6,8 +6,12 @@ <%= tag.p @holding.ticker, class: "text-sm text-secondary" %>
- <% if @holding.security.logo_url.present? %> + <% if Setting.brand_fetch_client_id.present? %> + <%= image_tag "https://cdn.brandfetch.io/#{@holding.ticker}/icon/fallback/lettermark/w/40/h/40?c=#{Setting.brand_fetch_client_id}", loading: "lazy", class: "w-9 h-9 rounded-full" %> + <% elsif @holding.security.logo_url.present? %> <%= image_tag @holding.security.logo_url, loading: "lazy", class: "w-9 h-9 rounded-full" %> + <% else %> + <%= render DS::FilledIcon.new(variant: :text, text: @holding.name, size: "md", rounded: true) %> <% end %>
<% end %> diff --git a/app/views/settings/hostings/_brand_fetch_settings.html.erb b/app/views/settings/hostings/_brand_fetch_settings.html.erb new file mode 100644 index 000000000..47c305766 --- /dev/null +++ b/app/views/settings/hostings/_brand_fetch_settings.html.erb @@ -0,0 +1,26 @@ +
+
+

<%= t(".title") %>

+ <% if ENV["BRAND_FETCH_CLIENT_ID"].present? %> +

You have successfully configured your Brand Fetch Client ID through the BRAND_FETCH_CLIENT_ID environment variable.

+ <% else %> +

<%= t(".description") %>

+ <% end %> +
+ + <%= 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 :brand_fetch_client_id, + label: t(".label"), + type: "password", + placeholder: t(".placeholder"), + value: ENV.fetch("BRAND_FETCH_CLIENT_ID", Setting.brand_fetch_client_id), + disabled: ENV["BRAND_FETCH_CLIENT_ID"].present?, + data: { "auto-submit-form-target": "auto" } %> + <% end %> +
diff --git a/app/views/settings/hostings/show.html.erb b/app/views/settings/hostings/show.html.erb index adea00382..0511c46b1 100644 --- a/app/views/settings/hostings/show.html.erb +++ b/app/views/settings/hostings/show.html.erb @@ -2,6 +2,7 @@ <%= settings_section title: t(".general") do %>
+ <%= render "settings/hostings/brand_fetch_settings" %> <%= render "settings/hostings/twelve_data_settings" %>
<% end %> diff --git a/config/locales/views/settings/hostings/en.yml b/config/locales/views/settings/hostings/en.yml index 4289abe65..b7564928d 100644 --- a/config/locales/views/settings/hostings/en.yml +++ b/config/locales/views/settings/hostings/en.yml @@ -21,6 +21,11 @@ en: confirm_clear_cache: title: Clear data cache? body: Are you sure you want to clear the data cache? This will remove all exchange rates, security prices, account balances, and other data. This action cannot be undone. + brand_fetch_settings: + description: Input the Client ID provided by Brand Fetch + label: Client ID + placeholder: Enter your Client ID here + title: Brand Fetch Settings twelve_data_settings: api_calls_used: "%{used} / %{limit} API daily calls used (%{percentage})" description: Input the API key provided by Twelve Data diff --git a/docs/hosting/logos-clientid.png b/docs/hosting/logos-clientid.png new file mode 100644 index 000000000..ab12f2979 Binary files /dev/null and b/docs/hosting/logos-clientid.png differ diff --git a/docs/hosting/logos.md b/docs/hosting/logos.md new file mode 100644 index 000000000..4a8aacfb2 --- /dev/null +++ b/docs/hosting/logos.md @@ -0,0 +1,20 @@ +# Account, Merchant and Security Logos + +Sure has integration with the [Brand Fetch Logo Link](https://brandfetch.com/developers/logo-api) service to provide logos for accounts, merchants and securities. +Logos are currently matched in the following ways: + +- For accounts, Plaid integration for the account is required and matched via FQDN (fully qualified domain name) from the Plaid integration +- For merchants, OpenAI integration is required and automatically matched to the merchant name and matched via FQDN +- For securities, logos are matched using the ticker symbol + +> [!NOTE] +> Currently ticker symbol matching cannot specify the exchange and since US exchanges are prioritized, securities from other exchanges might not have the right logo. + +## Enabling Brand Fetch Integration + +A Brand Fetch Client ID is required and to obtain a client ID, sign up for an account [here](https://brandfetch.com/developers/logo-api). + +Once you enter the Client ID into the Sure settings under the `Self hosting` section, logos from Brand Fetch integration will be enabled. +Alternatively, you can provide the client id using the `BRAND_FETCH_CLIENT_ID` environment variable to the web and worker services. + +![CLIENT_ID screenshot](logos-clientid.png) diff --git a/test/models/family/auto_merchant_detector_test.rb b/test/models/family/auto_merchant_detector_test.rb index 21f9f4e15..aa4a56be6 100644 --- a/test/models/family/auto_merchant_detector_test.rb +++ b/test/models/family/auto_merchant_detector_test.rb @@ -8,6 +8,7 @@ class Family::AutoMerchantDetectorTest < ActiveSupport::TestCase @account = @family.accounts.create!(name: "Rule test", balance: 100, currency: "USD", accountable: Depository.new) @llm_provider = mock Provider::Registry.stubs(:get_provider).with(:openai).returns(@llm_provider) + Setting.stubs(:brand_fetch_client_id).returns("123") end test "auto detects transaction merchants" do @@ -29,8 +30,8 @@ class Family::AutoMerchantDetectorTest < ActiveSupport::TestCase assert_equal "McDonalds", txn1.reload.merchant.name assert_equal "Chipotle", txn2.reload.merchant.name - assert_nil txn1.reload.merchant.logo_url - assert_nil txn2.reload.merchant.logo_url + assert_equal "https://cdn.brandfetch.io/mcdonalds.com/icon/fallback/lettermark/w/40/h/40?c=123", txn1.reload.merchant.logo_url + assert_equal "https://cdn.brandfetch.io/chipotle.com/icon/fallback/lettermark/w/40/h/40?c=123", txn2.reload.merchant.logo_url assert_nil txn3.reload.merchant # After auto-detection, all transactions are locked and no longer enrichable