diff --git a/app/controllers/concerns/accountable_resource.rb b/app/controllers/concerns/accountable_resource.rb index bc1d636f4..2d994d45c 100644 --- a/app/controllers/concerns/accountable_resource.rb +++ b/app/controllers/concerns/accountable_resource.rb @@ -86,6 +86,7 @@ module AccountableResource def account_params params.require(:account).permit( :name, :balance, :subtype, :currency, :accountable_type, :return_to, + :institution_name, :institution_domain, :notes, accountable_attributes: self.class.permitted_accountable_attributes ) end diff --git a/app/controllers/properties_controller.rb b/app/controllers/properties_controller.rb index f1df28d3d..c222d5a74 100644 --- a/app/controllers/properties_controller.rb +++ b/app/controllers/properties_controller.rb @@ -89,7 +89,14 @@ class PropertiesController < ApplicationController def property_params params.require(:account) - .permit(:name, :accountable_type, accountable_attributes: [ :id, :subtype, :year_built, :area_unit, :area_value ]) + .permit( + :name, + :accountable_type, + :institution_name, + :institution_domain, + :notes, + accountable_attributes: [ :id, :subtype, :year_built, :area_unit, :area_value ] + ) end def set_property diff --git a/app/models/account.rb b/app/models/account.rb index 2f1508494..4f96782b4 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -188,8 +188,12 @@ class Account < ApplicationRecord end end + def institution_name + read_attribute(:institution_name).presence || provider&.institution_name + end + def institution_domain - provider&.institution_domain + read_attribute(:institution_domain).presence || provider&.institution_domain end def destroy_later diff --git a/app/views/accounts/_account.html.erb b/app/views/accounts/_account.html.erb index b4651c64f..cd8dade0e 100644 --- a/app/views/accounts/_account.html.erb +++ b/app/views/accounts/_account.html.erb @@ -18,8 +18,8 @@ <% else %>
<%= link_to account.name, account, class: [(account.active? ? "text-primary" : "text-subdued"), "text-sm font-medium hover:underline"], data: { turbo_frame: "_top" } %> - <% if account.simplefin_account&.org_data&.dig('name') %> - • <%= account.simplefin_account.org_data["name"] %> + <% if account.institution_name %> + • <%= account.institution_name %> <% end %>
<% if account.long_subtype_label %> diff --git a/app/views/accounts/_form.html.erb b/app/views/accounts/_form.html.erb index d2139b56a..7f3ede919 100644 --- a/app/views/accounts/_form.html.erb +++ b/app/views/accounts/_form.html.erb @@ -16,6 +16,28 @@ <% end %> <%= yield form %> + +
+ + <%= icon "chevron-right", size: "sm", class: "group-open:rotate-90 transition-transform" %> + <%= t(".additional_details") %> + + +
+ <%= form.text_field :institution_name, + label: t(".institution_name_label"), + placeholder: account.provider&.institution_name || t(".institution_name_placeholder") %> + + <%= form.text_field :institution_domain, + label: t(".institution_domain_label"), + placeholder: account.provider&.institution_domain || t(".institution_domain_placeholder") %> + + <%= form.text_area :notes, + label: t(".notes_label"), + placeholder: t(".notes_placeholder"), + rows: 4 %> +
+
<%= form.submit %> diff --git a/app/views/accounts/_logo.html.erb b/app/views/accounts/_logo.html.erb index 364e7ccb5..52be912f8 100644 --- a/app/views/accounts/_logo.html.erb +++ b/app/views/accounts/_logo.html.erb @@ -7,7 +7,7 @@ "full" => "w-full h-full" } %> -<% if account.linked? && account.institution_domain.present? && Setting.brand_fetch_client_id.present? %> +<% if 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]}" %> diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml index e3911b1ab..517ff6825 100644 --- a/config/locales/views/accounts/en.yml +++ b/config/locales/views/accounts/en.yml @@ -21,6 +21,13 @@ en: balance: Current balance name_label: Account name name_placeholder: Example account name + additional_details: Additional details + institution_name_label: Institution name + institution_name_placeholder: e.g., Chase Bank + institution_domain_label: Institution domain + institution_domain_placeholder: e.g., chase.com + notes_label: Notes + notes_placeholder: Store additional information like account numbers, sort codes, IBAN, routing numbers, etc. index: accounts: Accounts manual_accounts: diff --git a/db/migrate/20251116010421_add_institution_fields_to_accounts.rb b/db/migrate/20251116010421_add_institution_fields_to_accounts.rb new file mode 100644 index 000000000..b98763923 --- /dev/null +++ b/db/migrate/20251116010421_add_institution_fields_to_accounts.rb @@ -0,0 +1,12 @@ +class AddInstitutionFieldsToAccounts < ActiveRecord::Migration[7.2] + def change + add_column :accounts, :institution_name, :string + add_column :accounts, :institution_domain, :string + add_column :accounts, :notes, :text + + # Touch all accounts to invalidate cached queries that depend on accounts.maximum(:updated_at) + # Without this, the following error would occur post-update and prevent page loads: + # "undefined method 'institution_domain' for an instance of BalanceSheet::AccountTotals::AccountRow" + Account.in_batches.update_all(updated_at: Time.current) + end +end diff --git a/db/schema.rb b/db/schema.rb index 06bc242de..27adfde1e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -46,6 +46,9 @@ ActiveRecord::Schema[7.2].define(version: 2025_12_15_100443) do t.jsonb "locked_attributes", default: {} t.string "status", default: "active" t.uuid "simplefin_account_id" + t.string "institution_name" + t.string "institution_domain" + t.text "notes" t.index ["accountable_id", "accountable_type"], name: "index_accounts_on_accountable_id_and_accountable_type" t.index ["accountable_type"], name: "index_accounts_on_accountable_type" t.index ["currency"], name: "index_accounts_on_currency" diff --git a/test/controllers/credit_cards_controller_test.rb b/test/controllers/credit_cards_controller_test.rb index d19db6512..1b11ce558 100644 --- a/test/controllers/credit_cards_controller_test.rb +++ b/test/controllers/credit_cards_controller_test.rb @@ -18,6 +18,9 @@ class CreditCardsControllerTest < ActionDispatch::IntegrationTest name: "New Credit Card", balance: 1000, currency: "USD", + institution_name: "Amex", + institution_domain: "americanexpress.com", + notes: "Primary card", accountable_type: "CreditCard", accountable_attributes: { available_credit: 5000, @@ -35,6 +38,9 @@ class CreditCardsControllerTest < ActionDispatch::IntegrationTest assert_equal "New Credit Card", created_account.name assert_equal 1000, created_account.balance assert_equal "USD", created_account.currency + assert_equal "Amex", created_account[:institution_name] + assert_equal "americanexpress.com", created_account[:institution_domain] + assert_equal "Primary card", created_account[:notes] assert_equal 5000, created_account.accountable.available_credit assert_equal 25.51, created_account.accountable.minimum_payment assert_equal 15.99, created_account.accountable.apr @@ -53,6 +59,9 @@ class CreditCardsControllerTest < ActionDispatch::IntegrationTest name: "Updated Credit Card", balance: 2000, currency: "USD", + institution_name: "Chase", + institution_domain: "chase.com", + notes: "Updated notes", accountable_type: "CreditCard", accountable_attributes: { id: @account.accountable_id, @@ -70,6 +79,9 @@ class CreditCardsControllerTest < ActionDispatch::IntegrationTest assert_equal "Updated Credit Card", @account.name assert_equal 2000, @account.balance + assert_equal "Chase", @account[:institution_name] + assert_equal "chase.com", @account[:institution_domain] + assert_equal "Updated notes", @account[:notes] assert_equal 6000, @account.accountable.available_credit assert_equal 50, @account.accountable.minimum_payment assert_equal 14.99, @account.accountable.apr diff --git a/test/controllers/loans_controller_test.rb b/test/controllers/loans_controller_test.rb index 47809400c..094933642 100644 --- a/test/controllers/loans_controller_test.rb +++ b/test/controllers/loans_controller_test.rb @@ -18,6 +18,9 @@ class LoansControllerTest < ActionDispatch::IntegrationTest name: "New Loan", balance: 50000, currency: "USD", + institution_name: "Local Bank", + institution_domain: "localbank.example", + notes: "Mortgage notes", accountable_type: "Loan", accountable_attributes: { interest_rate: 5.5, @@ -34,6 +37,9 @@ class LoansControllerTest < ActionDispatch::IntegrationTest assert_equal "New Loan", created_account.name assert_equal 50000, created_account.balance assert_equal "USD", created_account.currency + assert_equal "Local Bank", created_account[:institution_name] + assert_equal "localbank.example", created_account[:institution_domain] + assert_equal "Mortgage notes", created_account[:notes] assert_equal 5.5, created_account.accountable.interest_rate assert_equal 60, created_account.accountable.term_months assert_equal "fixed", created_account.accountable.rate_type @@ -51,6 +57,9 @@ class LoansControllerTest < ActionDispatch::IntegrationTest name: "Updated Loan", balance: 45000, currency: "USD", + institution_name: "Updated Bank", + institution_domain: "updatedbank.example", + notes: "Updated loan notes", accountable_type: "Loan", accountable_attributes: { id: @account.accountable_id, @@ -67,6 +76,9 @@ class LoansControllerTest < ActionDispatch::IntegrationTest assert_equal "Updated Loan", @account.name assert_equal 45000, @account.balance + assert_equal "Updated Bank", @account[:institution_name] + assert_equal "updatedbank.example", @account[:institution_domain] + assert_equal "Updated loan notes", @account[:notes] assert_equal 4.5, @account.accountable.interest_rate assert_equal 48, @account.accountable.term_months assert_equal "fixed", @account.accountable.rate_type diff --git a/test/controllers/properties_controller_test.rb b/test/controllers/properties_controller_test.rb index 872579b13..890f8b17e 100644 --- a/test/controllers/properties_controller_test.rb +++ b/test/controllers/properties_controller_test.rb @@ -14,6 +14,9 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest account: { name: "New Property", subtype: "house", + institution_name: "Property Lender", + institution_domain: "propertylender.example", + notes: "Property notes", accountable_type: "Property", accountable_attributes: { year_built: 1990, @@ -28,6 +31,9 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest assert created_account.accountable.is_a?(Property) assert_equal "draft", created_account.status assert_equal 0, created_account.balance + assert_equal "Property Lender", created_account[:institution_name] + assert_equal "propertylender.example", created_account[:institution_domain] + assert_equal "Property notes", created_account[:notes] assert_equal 1990, created_account.accountable.year_built assert_equal 1200, created_account.accountable.area_value assert_equal "sqft", created_account.accountable.area_unit @@ -39,6 +45,9 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest patch property_path(@account), params: { account: { name: "Updated Property", + institution_name: "Updated Lender", + institution_domain: "updatedlender.example", + notes: "Updated property notes", accountable_attributes: { id: @account.accountable.id, subtype: "condominium" @@ -50,6 +59,9 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest @account.reload assert_equal "Updated Property", @account.name assert_equal "condominium", @account.subtype + assert_equal "Updated Lender", @account[:institution_name] + assert_equal "updatedlender.example", @account[:institution_domain] + assert_equal "Updated property notes", @account[:notes] # If account is active, it renders edit view; otherwise redirects to balances if @account.active? diff --git a/test/controllers/vehicles_controller_test.rb b/test/controllers/vehicles_controller_test.rb index 55aa89cff..d4896230e 100644 --- a/test/controllers/vehicles_controller_test.rb +++ b/test/controllers/vehicles_controller_test.rb @@ -18,6 +18,9 @@ class VehiclesControllerTest < ActionDispatch::IntegrationTest name: "Vehicle", balance: 30000, currency: "USD", + institution_name: "Auto Lender", + institution_domain: "autolender.example", + notes: "Lease notes", accountable_type: "Vehicle", accountable_attributes: { make: "Toyota", @@ -32,6 +35,12 @@ class VehiclesControllerTest < ActionDispatch::IntegrationTest created_account = Account.order(:created_at).last + assert_equal "Vehicle", created_account.name + assert_equal 30000, created_account.balance + assert_equal "USD", created_account.currency + assert_equal "Auto Lender", created_account[:institution_name] + assert_equal "autolender.example", created_account[:institution_domain] + assert_equal "Lease notes", created_account[:notes] assert_equal "Toyota", created_account.accountable.make assert_equal "Camry", created_account.accountable.model assert_equal 2020, created_account.accountable.year @@ -50,6 +59,9 @@ class VehiclesControllerTest < ActionDispatch::IntegrationTest name: "Updated Vehicle", balance: 28000, currency: "USD", + institution_name: "Updated Lender", + institution_domain: "updatedlender.example", + notes: "Updated lease notes", accountable_type: "Vehicle", accountable_attributes: { id: @account.accountable_id, @@ -64,6 +76,13 @@ class VehiclesControllerTest < ActionDispatch::IntegrationTest } end + @account.reload + assert_equal "Updated Vehicle", @account.name + assert_equal 28000, @account.balance + assert_equal "Updated Lender", @account[:institution_name] + assert_equal "updatedlender.example", @account[:institution_domain] + assert_equal "Updated lease notes", @account[:notes] + assert_redirected_to account_path(@account) assert_equal "Vehicle account updated", flash[:notice] assert_enqueued_with(job: SyncJob) diff --git a/test/system/accounts_test.rb b/test/system/accounts_test.rb index da1b59384..555959190 100644 --- a/test/system/accounts_test.rb +++ b/test/system/accounts_test.rb @@ -109,9 +109,16 @@ class AccountsTest < ApplicationSystemTestCase click_link "Enter account balance" if accountable_type.in?(%w[Depository Investment Crypto Loan CreditCard]) account_name = "[system test] #{accountable_type} Account" + institution_name = "[system test] Institution" + institution_domain = "example.com" + notes = "Test notes for #{accountable_type}" fill_in "Account name*", with: account_name fill_in "account[balance]", with: 100.99 + find("summary", text: "Additional details").click + fill_in "Institution name", with: institution_name + fill_in "Institution domain", with: institution_domain + fill_in "Notes", with: notes yield if block_given? @@ -127,6 +134,9 @@ class AccountsTest < ApplicationSystemTestCase assert_text account_name created_account = Account.order(:created_at).last + assert_equal institution_name, created_account[:institution_name] + assert_equal institution_domain, created_account[:institution_domain] + assert_equal notes, created_account[:notes] visit account_url(created_account) @@ -135,9 +145,22 @@ class AccountsTest < ApplicationSystemTestCase click_on "Edit" end + updated_institution_name = "[system test] Updated Institution" + updated_institution_domain = "updated.example.com" + updated_notes = "Updated notes for #{accountable_type}" + fill_in "Account name", with: "Updated account name" + find("summary", text: "Additional details").click + fill_in "Institution name", with: updated_institution_name + fill_in "Institution domain", with: updated_institution_domain + fill_in "Notes", with: updated_notes click_button "Update Account" assert_selector "h2", text: "Updated account name" + + created_account.reload + assert_equal updated_institution_name, created_account[:institution_name] + assert_equal updated_institution_domain, created_account[:institution_domain] + assert_equal updated_notes, created_account[:notes] end def humanized_accountable(accountable_type)