Fix Broken Account Re-linking Feature (#469)

* Update SimpleFIN relinking flow and enhance duplicate account handling

- Updated logic to allow relinking of SimpleFIN accounts while preserving legacy mappings.
- Introduced clean-up logic to hide orphaned duplicate accounts after relinking.
- Enhanced UI to display current mappings for linked accounts.
- Improved test coverage for relinking scenarios and SimpleFIN account visibility.

* Localize SimpleFIN account selection messages and remove hardcoded text

- Added translations for user-facing messages in `select_existing_account` flow (`pt-BR` and `en` locales).
- Replaced hardcoded strings in the view with localized keys.

* Localize Enable Banking and SimpleFIN account linking messages; add support for investment accounts.

- Added translations for Enable Banking and SimpleFIN account linking flows.
- Updated views and controllers to replace hardcoded strings with localized keys.
- Introduced support for investment accounts in `Provider::LunchflowAdapter`.
- Enhanced relinking logic for SimpleFIN accounts and improved test coverage for related scenarios.

---------

Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
This commit is contained in:
LPW
2025-12-20 15:18:55 -05:00
committed by GitHub
parent 5c293b532a
commit e9dbf5f4e7
11 changed files with 188 additions and 32 deletions

View File

@@ -323,12 +323,20 @@ class SimplefinItemsController < ApplicationController
def select_existing_account
@account = Current.family.accounts.find(params[:account_id])
# Filter out SimpleFIN accounts that are already linked to any account
# (either via account_provider or legacy account association)
# Allow explicit relinking by listing all available SimpleFIN accounts for the family.
# The UI will surface the current mapping (if any), and the action will move the link.
@available_simplefin_accounts = Current.family.simplefin_items
.includes(:simplefin_accounts)
.includes(simplefin_accounts: [ :account, { account_provider: :account } ])
.flat_map(&:simplefin_accounts)
.reject { |sfa| sfa.account_provider.present? || sfa.account.present? }
# After provider setup, SFAs may already have an AccountProvider (linked to the freshly
# created duplicate accounts). During relink, we need to show those SFAs until the legacy
# link (`Account.simplefin_account_id`) has been cleared.
#
# Eligibility rule:
# - Show SFAs that are still legacy-linked (`sfa.account.present?`) => candidates to move.
# - Show SFAs that are fully unlinked (no legacy account and no account_provider) => candidates to link.
# - Hide SFAs that are linked via AccountProvider but no longer legacy-linked => already relinked.
.select { |sfa| sfa.account.present? || sfa.account_provider.nil? }
.sort_by { |sfa| sfa.updated_at || sfa.created_at }
.reverse
@@ -342,7 +350,7 @@ class SimplefinItemsController < ApplicationController
# Guard: only manual accounts can be linked (no existing provider links or legacy IDs)
if @account.account_providers.any? || @account.plaid_account_id.present? || @account.simplefin_account_id.present?
flash[:alert] = "Only manual accounts can be linked"
flash[:alert] = t("simplefin_items.link_existing_account.errors.only_manual")
if turbo_frame_request?
return render turbo_stream: Array(flash_notification_stream_items)
else
@@ -352,7 +360,7 @@ class SimplefinItemsController < ApplicationController
# Verify the SimpleFIN account belongs to this family's SimpleFIN items
unless Current.family.simplefin_items.include?(simplefin_account.simplefin_item)
flash[:alert] = "Invalid SimpleFIN account selected"
flash[:alert] = t("simplefin_items.link_existing_account.errors.invalid_simplefin_account")
if turbo_frame_request?
render turbo_stream: Array(flash_notification_stream_items)
else
@@ -364,9 +372,10 @@ class SimplefinItemsController < ApplicationController
# Relink behavior: detach any legacy link and point provider link at the chosen account
Account.transaction do
simplefin_account.lock!
# Clear legacy association if present
if simplefin_account.account_id.present?
simplefin_account.update!(account_id: nil)
# Clear legacy association if present (Account.simplefin_account_id)
if (legacy_account = simplefin_account.account)
legacy_account.update!(simplefin_account_id: nil)
end
# Upsert the AccountProvider mapping deterministically
@@ -382,9 +391,13 @@ class SimplefinItemsController < ApplicationController
if previous_account && previous_account.id != @account.id && previous_account.family_id == @account.family_id
begin
previous_account.reload
# Only disable if the previous account is truly orphaned (no other provider links)
# Only hide if the previous account is truly orphaned (no other provider links)
if previous_account.account_providers.none?
previous_account.disable!
# Disabled accounts still appear (greyed-out) in the manual list; for relink
# consolidation we want the duplicate to disappear from the UI.
# Use the app's standard deletion path (async) so the "pending_deletion" state
# remains truthful in the UI.
previous_account.destroy_later if previous_account.may_mark_for_deletion?
else
Rails.logger.info("Skipped disabling account ##{previous_account.id} after relink because it still has active provider links")
end
@@ -410,7 +423,7 @@ class SimplefinItemsController < ApplicationController
@simplefin_items = Current.family.simplefin_items.ordered.includes(:syncs)
build_simplefin_maps_for(@simplefin_items)
flash[:notice] = "Account successfully linked to SimpleFIN"
flash[:notice] = t("simplefin_items.link_existing_account.success")
@account.reload
manual_accounts_stream = if @manual_accounts.any?
turbo_stream.update(
@@ -434,7 +447,7 @@ class SimplefinItemsController < ApplicationController
turbo_stream.replace("modal", view_context.turbo_frame_tag("modal"))
] + Array(flash_notification_stream_items)
else
redirect_to accounts_path(cache_bust: SecureRandom.hex(6)), notice: "Account successfully linked to SimpleFIN", status: :see_other
redirect_to accounts_path(cache_bust: SecureRandom.hex(6)), notice: t("simplefin_items.link_existing_account.success"), status: :see_other
end
end