diff --git a/app/controllers/snaptrade_items_controller.rb b/app/controllers/snaptrade_items_controller.rb index dc01c21f3..e5d771b75 100644 --- a/app/controllers/snaptrade_items_controller.rb +++ b/app/controllers/snaptrade_items_controller.rb @@ -154,6 +154,14 @@ class SnaptradeItemsController < ApplicationController @snaptrade_item.sync_later end + # Existing unlinked, visible investment/crypto accounts that could be linked instead of creating duplicates + @linkable_accounts = Current.family.accounts + .visible + .where(accountable_type: %w[Investment Crypto]) + .left_joins(:account_providers) + .where(account_providers: { id: nil }) + .order(:name) + # Determine view state @syncing = @snaptrade_item.syncing? @waiting_for_sync = no_accounts && @syncing @@ -369,9 +377,10 @@ class SnaptradeItemsController < ApplicationController def link_existing_account account_id = params[:account_id] snaptrade_account_id = params[:snaptrade_account_id] + snaptrade_item_id = params[:snaptrade_item_id] account = Current.family.accounts.find_by(id: account_id) - snaptrade_item = Current.family.snaptrade_items.first + snaptrade_item = Current.family.snaptrade_items.find_by(id: snaptrade_item_id) snaptrade_account = snaptrade_item&.snaptrade_accounts&.find_by(id: snaptrade_account_id) if account && snaptrade_account diff --git a/app/views/snaptrade_items/setup_accounts.html.erb b/app/views/snaptrade_items/setup_accounts.html.erb index 9331c4854..4ef329562 100644 --- a/app/views/snaptrade_items/setup_accounts.html.erb +++ b/app/views/snaptrade_items/setup_accounts.html.erb @@ -138,6 +138,7 @@ <%= t("snaptrade_items.setup_accounts.sync_start_date_help", default: "Leave blank for all available history") %>

+ <% end %> @@ -160,40 +161,70 @@ <% end %> - <% if @linked_accounts.any? %> -
"> -

<%= t("snaptrade_items.setup_accounts.linked_accounts", default: "Already Linked") %>

- <% @linked_accounts.each do |snaptrade_account| %> -
-
-
- <%= icon "check-circle", class: "text-success" %> -
-

<%= snaptrade_account.name %>

-

- <%= t("snaptrade_items.setup_accounts.linked_to", default: "Linked to:") %> - <%= link_to snaptrade_account.current_account.name, account_path(snaptrade_account.current_account), class: "link" %> -

-
+ <% end %> + + <%# Link-to-existing forms rendered OUTSIDE the create form to avoid nested
%> + <% if @unlinked_accounts.any? && @linkable_accounts.any? %> +
+

+ <%= t("snaptrade_items.setup_accounts.or_link_existing", default: "Or link to an existing account instead of creating a new one:") %> +

+
+ <% @unlinked_accounts.each do |snaptrade_account| %> + <%= form_with url: link_existing_account_snaptrade_items_path, method: :post, data: { turbo_frame: "_top" } do |link_form| %> + <%= link_form.hidden_field :snaptrade_account_id, value: snaptrade_account.id %> + <%= link_form.hidden_field :snaptrade_item_id, value: @snaptrade_item.id %> +

<%= snaptrade_account.name %>

+
+ <%= link_form.select :account_id, + options_for_select(@linkable_accounts.map { |a| ["#{a.name} (#{number_to_currency(a.balance, unit: Money::Currency.new(a.currency || "USD").symbol)})", a.id] }), + { prompt: t("snaptrade_items.setup_accounts.select_account", default: "Select an account...") }, + class: "bg-container border border-primary rounded px-2 py-1 text-sm text-primary flex-1 min-w-0" %> + <%= render DS::Button.new( + text: t("snaptrade_items.setup_accounts.link_button", default: "Link"), + variant: "secondary", + size: "sm", + type: "submit" + ) %> +
+ <% end %> + <% end %> +
+
+ <% end %> + + <% if @linked_accounts.any? %> +
"> +

<%= t("snaptrade_items.setup_accounts.linked_accounts", default: "Already Linked") %>

+ <% @linked_accounts.each do |snaptrade_account| %> +
+
+
+ <%= icon "check-circle", class: "text-success" %> +
+

<%= snaptrade_account.name %>

+

+ <%= t("snaptrade_items.setup_accounts.linked_to", default: "Linked to:") %> + <%= link_to snaptrade_account.current_account.name, account_path(snaptrade_account.current_account), class: "link" %> +

- <% end %> -
- - <%# Show Done button when all accounts are linked (no unlinked) %> - <% if @unlinked_accounts.blank? %> -
- <%= render DS::Link.new( - text: t("snaptrade_items.setup_accounts.done_button", default: "Done"), - variant: "primary", - href: accounts_path, - frame: "_top" - ) %>
<% end %> - <% end %> +
+ <%# Show Done button when all accounts are linked (no unlinked) %> + <% if @unlinked_accounts.blank? %> +
+ <%= render DS::Link.new( + text: t("snaptrade_items.setup_accounts.done_button", default: "Done"), + variant: "primary", + href: accounts_path, + frame: "_top" + ) %> +
+ <% end %> <% end %> <% end %>
diff --git a/config/locales/views/snaptrade_items/en.yml b/config/locales/views/snaptrade_items/en.yml index f246a9418..63b70a28c 100644 --- a/config/locales/views/snaptrade_items/en.yml +++ b/config/locales/views/snaptrade_items/en.yml @@ -75,6 +75,9 @@ en: cancel_button: "Cancel" creating: "Creating Accounts..." done_button: "Done" + or_link_existing: "Or link to an existing account instead of creating a new one:" + select_account: "Select an account..." + link_button: "Link" linked_accounts: "Already Linked" linked_to: "Linked to:" snaptrade_item: diff --git a/test/controllers/snaptrade_items_controller_test.rb b/test/controllers/snaptrade_items_controller_test.rb index 9f6f78944..2011832cb 100644 --- a/test/controllers/snaptrade_items_controller_test.rb +++ b/test/controllers/snaptrade_items_controller_test.rb @@ -38,4 +38,69 @@ class SnaptradeItemsControllerTest < ActionDispatch::IntegrationTest assert_redirected_to portal_url end + + test "setup_accounts shows linkable investment and crypto accounts in dropdown" do + get setup_accounts_snaptrade_item_url(@snaptrade_item) + + assert_response :success + + # Investment and crypto accounts (no provider) should appear in the link dropdown + assert_match accounts(:investment).name, response.body + assert_match accounts(:crypto).name, response.body + + # Depository should NOT appear in the link dropdown (wrong type) + # The depository name may appear elsewhere on the page, so check the select options specifically + refute_match(/option.*#{accounts(:depository).name}/, response.body) + end + + test "setup_accounts excludes accounts that already have a provider from dropdown" do + # Link the investment account to a snaptrade_account + AccountProvider.create!( + account: accounts(:investment), + provider: snaptrade_accounts(:fidelity_401k) + ) + + get setup_accounts_snaptrade_item_url(@snaptrade_item) + + assert_response :success + + # Investment account is now linked → should NOT appear in link dropdown options + refute_match(/option.*#{accounts(:investment).name}/, response.body) + # Crypto still unlinked → should appear + assert_match accounts(:crypto).name, response.body + end + + test "link_existing_account links account to snaptrade_account" do + account = accounts(:investment) + snaptrade_account = snaptrade_accounts(:fidelity_401k) + + assert_difference "AccountProvider.count", 1 do + post link_existing_account_snaptrade_items_url, params: { + account_id: account.id, + snaptrade_account_id: snaptrade_account.id, + snaptrade_item_id: @snaptrade_item.id + } + end + + assert_redirected_to account_path(account) + assert_match(/Successfully linked/, flash[:notice]) + + snaptrade_account.reload + assert_equal account, snaptrade_account.current_account + end + + test "link_existing_account handles missing account gracefully" do + snaptrade_account = snaptrade_accounts(:fidelity_401k) + + assert_no_difference "AccountProvider.count" do + post link_existing_account_snaptrade_items_url, params: { + account_id: "nonexistent", + snaptrade_account_id: snaptrade_account.id, + snaptrade_item_id: @snaptrade_item.id + } + end + + assert_redirected_to settings_providers_path + assert_match(/not found/i, flash[:alert]) + end end