From dbcbf89f4b26fc48e494e06881cb99faaf5c431f Mon Sep 17 00:00:00 2001 From: Sholom Ber <46351743+Himmelschmidt@users.noreply.github.com> Date: Thu, 7 Aug 2025 13:23:28 -0400 Subject: [PATCH] Complete subtype selection for SimpleFin accounts - Add subtype database columns to all accountable models - Create Stimulus controller for dynamic subtype dropdown interaction - Add delegation from Account to accountable subtype for clean API access - Update SimpleFin account setup form with working subtype selection - Fix account display to show proper subtype labels instead of generic "Cash" Users can now select both account type and subtype during SimpleFin import, and the selected subtypes are properly saved and displayed in the UI. --- app/controllers/simplefin_items_controller.rb | 18 +++++- .../account_type_selector_controller.js | 43 +++++++++++++ app/models/account.rb | 25 ++++++++ .../simplefin_items/setup_accounts.html.erb | 61 ++++++++++++++++--- ...50807170943_add_subtype_to_accountables.rb | 13 ++++ db/schema.rb | 11 +++- 6 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 app/javascript/controllers/account_type_selector_controller.js create mode 100644 db/migrate/20250807170943_add_subtype_to_accountables.rb diff --git a/app/controllers/simplefin_items_controller.rb b/app/controllers/simplefin_items_controller.rb index 5a02eb723..9a3bf087e 100644 --- a/app/controllers/simplefin_items_controller.rb +++ b/app/controllers/simplefin_items_controller.rb @@ -42,7 +42,7 @@ class SimplefinItemsController < ApplicationController # Immediately sync to get account and institution data @simplefin_item.sync_later - redirect_back_or_to root_path, notice: "SimpleFin connection added successfully! Your accounts will appear shortly as they sync in the background." + redirect_to simplefin_items_path, notice: "SimpleFin connection added successfully! Your accounts will appear shortly as they sync in the background." else @error_message = @simplefin_item.errors.full_messages.join(", ") render :new, status: :unprocessable_entity @@ -87,16 +87,28 @@ class SimplefinItemsController < ApplicationController ['Loan or Mortgage', 'Loan'], ['Other Asset', 'OtherAsset'] ] + + # Subtype options for each account type + @depository_subtypes = Depository::SUBTYPES.map { |k, v| [v[:long], k] } + @credit_card_subtypes = CreditCard::SUBTYPES.map { |k, v| [v[:long], k] } + @investment_subtypes = Investment::SUBTYPES.map { |k, v| [v[:long], k] } + @loan_subtypes = Loan::SUBTYPES.map { |k, v| [v[:long], k] } end def complete_account_setup account_types = params[:account_types] || {} + account_subtypes = params[:account_subtypes] || {} account_types.each do |simplefin_account_id, selected_type| simplefin_account = @simplefin_item.simplefin_accounts.find(simplefin_account_id) + selected_subtype = account_subtypes[simplefin_account_id] - # Create account with user-selected type - account = Account.create_from_simplefin_account_with_type(simplefin_account, selected_type) + # Create account with user-selected type and subtype + account = Account.create_from_simplefin_account_with_type_and_subtype( + simplefin_account, + selected_type, + selected_subtype + ) simplefin_account.update!(account: account) end diff --git a/app/javascript/controllers/account_type_selector_controller.js b/app/javascript/controllers/account_type_selector_controller.js new file mode 100644 index 000000000..db4dc39ce --- /dev/null +++ b/app/javascript/controllers/account_type_selector_controller.js @@ -0,0 +1,43 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["subtypeContainer"] + static values = { accountId: String } + + connect() { + console.log('Account type selector connected for account:', this.accountIdValue) + // Show initial subtype dropdown based on current selection + this.updateSubtype() + } + + updateSubtype(event) { + const selectElement = this.element.querySelector('select[name^="account_types"]') + const selectedType = selectElement ? selectElement.value : '' + const container = this.subtypeContainerTarget + const accountId = this.accountIdValue + + console.log('Updating subtype for account:', accountId, 'Selected type:', selectedType) + + // Hide all subtype selects + const subtypeSelects = container.querySelectorAll('.subtype-select') + subtypeSelects.forEach(select => { + select.style.display = 'none' + // Clear the name attribute so it doesn't get submitted + const selectElement = select.querySelector('select') + if (selectElement) { + selectElement.removeAttribute('name') + } + }) + + // Show the relevant subtype select + const relevantSubtype = container.querySelector(`[data-type="${selectedType}"]`) + if (relevantSubtype) { + relevantSubtype.style.display = 'block' + // Re-add the name attribute so it gets submitted + const selectElement = relevantSubtype.querySelector('select') + if (selectElement) { + selectElement.setAttribute('name', `account_subtypes[${accountId}]`) + } + } + } +} \ No newline at end of file diff --git a/app/models/account.rb b/app/models/account.rb index e76371586..20bbafd5f 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -27,6 +27,7 @@ class Account < ApplicationRecord has_one_attached :logo delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy + delegate :subtype, to: :accountable, allow_nil: true accepts_nested_attributes_for :accountable, update_only: true @@ -102,6 +103,20 @@ class Account < ApplicationRecord create_and_sync(attributes) end + def create_from_simplefin_account_with_type_and_subtype(simplefin_account, account_type, subtype) + attributes = { + family: simplefin_account.simplefin_item.family, + name: simplefin_account.name, + balance: simplefin_account.current_balance || simplefin_account.available_balance || 0, + currency: simplefin_account.currency, + accountable_type: account_type, + accountable_attributes: build_accountable_attributes_with_subtype(simplefin_account, account_type, subtype), + simplefin_account_id: simplefin_account.id + } + + create_and_sync(attributes) + end + def map_simplefin_type_to_accountable_type(simplefin_type, account_name: nil) # First try to map by explicit type if provided case simplefin_type&.downcase @@ -146,6 +161,16 @@ class Account < ApplicationRecord {} end end + + def build_accountable_attributes_with_subtype(simplefin_account, account_type, subtype) + base_attributes = build_accountable_attributes(simplefin_account, account_type) + + if subtype.present? + base_attributes[:subtype] = subtype + end + + base_attributes + end end def institution_domain diff --git a/app/views/simplefin_items/setup_accounts.html.erb b/app/views/simplefin_items/setup_accounts.html.erb index 5213304ac..02ebb6d51 100644 --- a/app/views/simplefin_items/setup_accounts.html.erb +++ b/app/views/simplefin_items/setup_accounts.html.erb @@ -45,13 +45,60 @@ -
- <%= label_tag "account_types[#{simplefin_account.id}]", "Account Type:", - class: "block text-sm font-medium text-primary mb-2" %> - <%= select_tag "account_types[#{simplefin_account.id}]", - options_for_select(@account_type_options, - Account.map_simplefin_type_to_accountable_type(simplefin_account.account_type, account_name: simplefin_account.name)), - { class: "w-full px-3 py-2 border border-primary rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" } %> +
+
+ <%= label_tag "account_types[#{simplefin_account.id}]", "Account Type:", + class: "block text-sm font-medium text-primary mb-2" %> + <%= select_tag "account_types[#{simplefin_account.id}]", + options_for_select(@account_type_options, + Account.map_simplefin_type_to_accountable_type(simplefin_account.account_type, account_name: simplefin_account.name)), + { class: "w-full px-3 py-2 border border-primary rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500", + data: { + action: "change->account-type-selector#updateSubtype" + } } %> +
+ + +
+ + + + + + + + + + +
<% end %> diff --git a/db/migrate/20250807170943_add_subtype_to_accountables.rb b/db/migrate/20250807170943_add_subtype_to_accountables.rb new file mode 100644 index 000000000..854d5a79f --- /dev/null +++ b/db/migrate/20250807170943_add_subtype_to_accountables.rb @@ -0,0 +1,13 @@ +class AddSubtypeToAccountables < ActiveRecord::Migration[7.2] + def change + add_column :depositories, :subtype, :string + add_column :investments, :subtype, :string + add_column :loans, :subtype, :string + add_column :credit_cards, :subtype, :string + add_column :other_assets, :subtype, :string + add_column :other_liabilities, :subtype, :string + add_column :properties, :subtype, :string + add_column :vehicles, :subtype, :string + add_column :cryptos, :subtype, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 1969c4c20..b5cc8ae4e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_08_07_163541) do +ActiveRecord::Schema[7.2].define(version: 2025_08_07_170943) do create_schema "sure_dev_schema" # These are extensions that must be enabled in order to support this database @@ -195,12 +195,14 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_07_163541) do t.date "expiration_date" t.decimal "annual_fee", precision: 10, scale: 2 t.jsonb "locked_attributes", default: {} + t.string "subtype" end create_table "cryptos", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.jsonb "locked_attributes", default: {} + t.string "subtype" end create_table "data_enrichments", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -220,6 +222,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_07_163541) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.jsonb "locked_attributes", default: {} + t.string "subtype" end create_table "entries", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -391,6 +394,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_07_163541) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.jsonb "locked_attributes", default: {} + t.string "subtype" end create_table "invitations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -425,6 +429,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_07_163541) do t.integer "term_months" t.decimal "initial_balance", precision: 19, scale: 4 t.jsonb "locked_attributes", default: {} + t.string "subtype" end create_table "merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -523,12 +528,14 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_07_163541) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.jsonb "locked_attributes", default: {} + t.string "subtype" end create_table "other_liabilities", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.jsonb "locked_attributes", default: {} + t.string "subtype" end create_table "plaid_accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -580,6 +587,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_07_163541) do t.integer "area_value" t.string "area_unit" t.jsonb "locked_attributes", default: {} + t.string "subtype" end create_table "rejected_transfers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -863,6 +871,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_07_163541) do t.string "make" t.string "model" t.jsonb "locked_attributes", default: {} + t.string "subtype" end add_foreign_key "accounts", "families"