# frozen_string_literal: true # This migration cleans up orphaned balance records that have a currency different # from their account's current currency. This can happen when linked accounts # (SimpleFIN, Lunchflow, Enable Banking, Plaid) were created with an initial sync # before the correct currency was known from the provider. # # The fix in Account.create_and_sync with skip_initial_sync: true prevents this # going forward, but existing data needs to be cleaned up. class CleanupOrphanedCurrencyBalances < ActiveRecord::Migration[7.2] def up # Skip in test environment with empty database (CI) return say "Skipping in test environment - no data to clean" if Rails.env.test? && account_count.zero? # First, identify affected accounts for logging affected_accounts = execute(<<~SQL).to_a SELECT DISTINCT a.id, a.name, a.currency as account_currency, b.currency as orphaned_currency, COUNT(b.id) as orphaned_balance_count FROM accounts a JOIN balances b ON a.id = b.account_id WHERE b.currency != a.currency AND ( a.simplefin_account_id IS NOT NULL OR a.plaid_account_id IS NOT NULL OR EXISTS (SELECT 1 FROM account_providers WHERE account_id = a.id) ) GROUP BY a.id, a.name, a.currency, b.currency ORDER BY a.name SQL if affected_accounts.any? say "Found #{affected_accounts.size} account-currency combinations with orphaned balances:" affected_accounts.each do |row| say " - #{row['name']}: #{row['orphaned_balance_count']} balances in #{row['orphaned_currency']} (account is #{row['account_currency']})" end # Delete orphaned balances where currency doesn't match account currency # Only for linked accounts (provider-connected accounts) execute(<<~SQL) DELETE FROM balances WHERE id IN ( SELECT b.id FROM balances b JOIN accounts a ON b.account_id = a.id WHERE b.currency != a.currency AND ( a.simplefin_account_id IS NOT NULL OR a.plaid_account_id IS NOT NULL OR EXISTS (SELECT 1 FROM account_providers WHERE account_id = a.id) ) ) SQL say "Deleted orphaned balances from linked accounts" # Get unique account IDs that need re-sync account_ids = affected_accounts.map { |row| row["id"] }.uniq # Schedule re-sync for affected accounts to regenerate correct balances # Only if Account model is available and responds to sync_later if defined?(Account) && Account.respond_to?(:where) say "Scheduling re-sync for #{account_ids.size} affected accounts..." Account.where(id: account_ids).find_each do |account| account.sync_later if account.respond_to?(:sync_later) end say "Scheduled re-sync for #{account_ids.size} affected accounts" else say "Skipping re-sync scheduling (Account model not available)" say "Please manually sync affected accounts: #{account_ids.join(', ')}" end else say "No orphaned currency balances found - database is clean" end end def down say "This migration cannot be fully reversed." say "The deleted balances will be regenerated by the scheduled syncs." say "If syncs haven't run yet, you may need to manually trigger them." end private def account_count execute("SELECT COUNT(*) FROM accounts").first["count"].to_i rescue 0 end end