Add support to unlink lunch flow accounts (#318)

* Add support to unlink lunch flow accounts

* add support to link and unlink to any provider

* Fix tests and query

* Let's keep Amr happy about his brand

* Wrap unlink operations in a transaction and add error handling.

* Fix tests

---------

Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
soky srm
2025-11-14 10:42:31 +01:00
committed by GitHub
parent 972648b66d
commit 606e4b1554
20 changed files with 546 additions and 17 deletions

View File

@@ -1,5 +1,5 @@
class AccountsController < ApplicationController
before_action :set_account, only: %i[sync sparkline toggle_active show destroy]
before_action :set_account, only: %i[sync sparkline toggle_active show destroy unlink confirm_unlink select_provider]
include Periodable
def index
@@ -17,7 +17,7 @@ class AccountsController < ApplicationController
def sync_all
family.sync_later
redirect_to accounts_path, notice: "Syncing accounts..."
redirect_to accounts_path, notice: t("accounts.sync_all.syncing")
end
def show
@@ -71,10 +71,93 @@ class AccountsController < ApplicationController
def destroy
if @account.linked?
redirect_to account_path(@account), alert: "Cannot delete a linked account"
redirect_to account_path(@account), alert: t("accounts.destroy.cannot_delete_linked")
else
@account.destroy_later
redirect_to accounts_path, notice: "Account scheduled for deletion"
redirect_to accounts_path, notice: t("accounts.destroy.success", type: @account.accountable_type)
end
end
def confirm_unlink
unless @account.linked?
redirect_to account_path(@account), alert: t("accounts.unlink.not_linked")
end
end
def unlink
unless @account.linked?
redirect_to account_path(@account), alert: t("accounts.unlink.not_linked")
return
end
begin
Account.transaction do
# Remove new system links (account_providers join table)
@account.account_providers.destroy_all
# Remove legacy system links (foreign keys)
@account.update!(plaid_account_id: nil, simplefin_account_id: nil)
end
redirect_to accounts_path, notice: t("accounts.unlink.success")
rescue ActiveRecord::RecordInvalid => e
redirect_to account_path(@account), alert: t("accounts.unlink.error", error: e.message)
rescue StandardError => e
Rails.logger.error "Failed to unlink account #{@account.id}: #{e.message}"
redirect_to account_path(@account), alert: t("accounts.unlink.error", error: t("accounts.unlink.generic_error"))
end
end
def select_provider
if @account.linked?
redirect_to account_path(@account), alert: t("accounts.select_provider.already_linked")
return
end
@available_providers = []
# Check SimpleFIN
if family.can_connect_simplefin?
@available_providers << {
name: "SimpleFIN",
key: "simplefin",
description: "Connect to your bank via SimpleFIN",
path: select_existing_account_simplefin_items_path(account_id: @account.id)
}
end
# Check Plaid US
if family.can_connect_plaid_us?
@available_providers << {
name: "Plaid",
key: "plaid_us",
description: "Connect to your US bank via Plaid",
path: select_existing_account_plaid_items_path(account_id: @account.id, region: "us")
}
end
# Check Plaid EU
if family.can_connect_plaid_eu?
@available_providers << {
name: "Plaid (EU)",
key: "plaid_eu",
description: "Connect to your EU bank via Plaid",
path: select_existing_account_plaid_items_path(account_id: @account.id, region: "eu")
}
end
# Check Lunch Flow
if family.can_connect_lunchflow?
@available_providers << {
name: "Lunch Flow",
key: "lunchflow",
description: "Connect to your bank via Lunch Flow",
path: select_existing_account_lunchflow_items_path(account_id: @account.id)
}
end
if @available_providers.empty?
redirect_to account_path(@account), alert: t("accounts.select_provider.no_providers")
end
end

View File

@@ -98,7 +98,7 @@ class LunchflowItemsController < ApplicationController
# Create or find lunchflow_item for this family
lunchflow_item = Current.family.lunchflow_items.first_or_create!(
name: "Lunchflow Connection"
name: "Lunch Flow Connection"
)
# Fetch account details from API
@@ -279,7 +279,7 @@ class LunchflowItemsController < ApplicationController
# Create or find lunchflow_item for this family
lunchflow_item = Current.family.lunchflow_items.first_or_create!(
name: "Lunchflow Connection"
name: "Lunch Flow Connection"
)
# Fetch account details from API
@@ -338,7 +338,7 @@ class LunchflowItemsController < ApplicationController
def create
@lunchflow_item = Current.family.lunchflow_items.build(lunchflow_params)
@lunchflow_item.name = "Lunchflow Connection"
@lunchflow_item.name = "Lunch Flow Connection"
if @lunchflow_item.save
# Trigger initial sync to fetch accounts

View File

@@ -48,6 +48,48 @@ class PlaidItemsController < ApplicationController
end
end
def select_existing_account
@account = Current.family.accounts.find(params[:account_id])
@region = params[:region] || "us"
# Get all Plaid accounts from this family's Plaid items for the specified region
# that are not yet linked to any account
@available_plaid_accounts = Current.family.plaid_items
.where(plaid_region: @region)
.includes(:plaid_accounts)
.flat_map(&:plaid_accounts)
.select { |pa| pa.account_provider.nil? && pa.account.nil? } # Not linked via new or legacy system
if @available_plaid_accounts.empty?
redirect_to account_path(@account), alert: "No available Plaid accounts to link. Please connect a new Plaid account first."
end
end
def link_existing_account
@account = Current.family.accounts.find(params[:account_id])
plaid_account = PlaidAccount.find(params[:plaid_account_id])
# Verify the Plaid account belongs to this family's Plaid items
unless Current.family.plaid_items.include?(plaid_account.plaid_item)
redirect_to account_path(@account), alert: "Invalid Plaid account selected"
return
end
# Verify the Plaid account is not already linked
if plaid_account.account_provider.present? || plaid_account.account.present?
redirect_to account_path(@account), alert: "This Plaid account is already linked"
return
end
# Create the link via AccountProvider
AccountProvider.create!(
account: @account,
provider: plaid_account
)
redirect_to accounts_path, notice: "Account successfully linked to Plaid"
end
private
def set_plaid_item
@plaid_item = Current.family.plaid_items.find(params[:id])

View File

@@ -186,6 +186,46 @@ class SimplefinItemsController < ApplicationController
redirect_to accounts_path, notice: t(".success")
end
def select_existing_account
@account = Current.family.accounts.find(params[:account_id])
# Get all SimpleFIN accounts from this family's SimpleFIN items
# that are not yet linked to any account
@available_simplefin_accounts = Current.family.simplefin_items
.includes(:simplefin_accounts)
.flat_map(&:simplefin_accounts)
.select { |sa| sa.account_provider.nil? && sa.account.nil? } # Not linked via new or legacy system
if @available_simplefin_accounts.empty?
redirect_to account_path(@account), alert: "No available SimpleFIN accounts to link. Please connect a new SimpleFIN account first."
end
end
def link_existing_account
@account = Current.family.accounts.find(params[:account_id])
simplefin_account = SimplefinAccount.find(params[:simplefin_account_id])
# Verify the SimpleFIN account belongs to this family's SimpleFIN items
unless Current.family.simplefin_items.include?(simplefin_account.simplefin_item)
redirect_to account_path(@account), alert: "Invalid SimpleFIN account selected"
return
end
# Verify the SimpleFIN account is not already linked
if simplefin_account.account_provider.present? || simplefin_account.account.present?
redirect_to account_path(@account), alert: "This SimpleFIN account is already linked"
return
end
# Create the link via AccountProvider
AccountProvider.create!(
account: @account,
provider: simplefin_account
)
redirect_to accounts_path, notice: "Account successfully linked to SimpleFIN"
end
private
def set_simplefin_item