Address remaining CodeRabbit comments from PR #267 #351 (#451)

* Address remaining CodeRabbit comments from PR #267

This commit addresses the remaining unresolved code review comments:

1. Fix down migration in drop_was_merged_from_transactions.rb
   - Add null: false, default: false constraints to match original column
   - Ensures proper rollback compatibility

2. Fix bare rescue in maps_helper.rb compute_duplicate_only_flag
   - Replace bare rescue with rescue StandardError => e
   - Add proper logging for debugging
   - Follows Ruby best practices by being explicit about exception handling

These changes improve code quality and follow Rails/Ruby best practices.

* Refactor `SimplefinItemsController` and add tests for balances sync and account relinking behavior

- Replaced direct sync execution with `SyncJob` for asynchronous handling of balances sync.
- Updated account relinking logic to prevent disabling accounts with other active provider links.
- Removed unused `compute_relink_candidates` method.
- Added tests to verify `balances` action enqueues `SyncJob` and relinking respects account-provider relationships.

* Refactor balances sync to use runtime-only `balances_only` flag

- Replaced persistent `sync_stats` usage with runtime `balances_only?` predicate via `define_singleton_method`.
- Updated `SimplefinItemsController` `balances` action to pass `balances_only` flag to `SyncJob`.
- Enhanced `SyncJob` to attach transient `balances_only?` flag for execution.
- Adjusted `SimplefinItem::Syncer` logic to rely on the runtime `balances_only?` method.
- Updated controller tests to validate runtime flag usage in `SyncJob`.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
This commit is contained in:
LPW
2025-12-15 03:47:16 -05:00
committed by GitHub
parent 92cf98551b
commit 4d3d9d10df
6 changed files with 110 additions and 119 deletions

View File

@@ -87,7 +87,8 @@ module SimplefinItems
end
end
errs.present? && errs.all? { |m| m.to_s.downcase.include?("duplicate upstream account detected") }
rescue
rescue StandardError => e
Rails.logger.warn("SimpleFin maps: compute_duplicate_only_flag failed: #{e.class} - #{e.message}")
false
end
end

View File

@@ -146,8 +146,9 @@ class SimplefinItemsController < ApplicationController
# Starts a balances-only sync for this SimpleFin item
def balances
sync = @simplefin_item.syncs.create!(status: :pending, sync_stats: { "balances_only" => true })
SimplefinItem::Syncer.new(@simplefin_item).perform_sync(sync)
# Create a Sync and enqueue it to run asynchronously with a runtime-only flag
sync = @simplefin_item.syncs.create!(status: :pending)
SyncJob.perform_later(sync, balances_only: true)
respond_to do |format|
format.html { redirect_back_or_to accounts_path }
@@ -379,7 +380,17 @@ class SimplefinItemsController < ApplicationController
# visible manual list. This mirrors the unified flow expectation that the provider
# follows the chosen account.
if previous_account && previous_account.id != @account.id && previous_account.family_id == @account.family_id
previous_account.disable! rescue nil
begin
previous_account.reload
# Only disable if the previous account is truly orphaned (no other provider links)
if previous_account.account_providers.none?
previous_account.disable!
else
Rails.logger.info("Skipped disabling account ##{previous_account.id} after relink because it still has active provider links")
end
rescue => e
Rails.logger.warn("Failed disabling-orphan check for account ##{previous_account&.id}: #{e.class} - #{e.message}")
end
end
end
@@ -430,111 +441,6 @@ class SimplefinItemsController < ApplicationController
private
NAME_NORM_RE = /\s+/.freeze
def normalize_name(str)
s = str.to_s.downcase.strip
return s if s.empty?
s.gsub(NAME_NORM_RE, " ")
end
def compute_relink_candidates
# Best-effort dedup before building candidates
@simplefin_item.dedup_simplefin_accounts! rescue nil
family = @simplefin_item.family
manuals = Account.visible_manual.where(family_id: family.id).to_a
# Evaluate only one SimpleFin account per upstream account_id (prefer linked, else newest)
grouped = @simplefin_item.simplefin_accounts.group_by(&:account_id)
sfas = grouped.values.map { |list| list.find { |s| s.current_account.present? } || list.max_by(&:updated_at) }
Rails.logger.info("SimpleFin compute_relink_candidates: manuals=#{manuals.size} sfas=#{sfas.size} (item_id=#{@simplefin_item.id})")
used_manual_ids = Set.new
pairs = []
sfas.each do |sfa|
next if sfa.name.blank?
# Heuristics (with ambiguity guards): last4 > balance ±0.01 > name
raw = (sfa.raw_payload || {}).with_indifferent_access
sfa_last4 = raw[:mask] || raw[:last4] || raw[:"last-4"] || raw[:"account_number_last4"]
sfa_last4 = sfa_last4.to_s.strip.presence
sfa_balance = (sfa.current_balance || sfa.available_balance).to_d rescue 0.to_d
chosen = nil
reason = nil
# 1) last4 match: compute all candidates not yet used
if sfa_last4.present?
last4_matches = manuals.reject { |a| used_manual_ids.include?(a.id) }.select do |a|
a_last4 = nil
%i[mask last4 number_last4 account_number_last4].each do |k|
if a.respond_to?(k)
val = a.public_send(k)
a_last4 = val.to_s.strip.presence if val.present?
break if a_last4
end
end
a_last4.present? && a_last4 == sfa_last4
end
# Ambiguity guard: skip if multiple matches
if last4_matches.size == 1
cand = last4_matches.first
# Conflict guard: if both have balances and differ wildly, skip
begin
ab = (cand.balance || cand.cash_balance || 0).to_d
if sfa_balance.nonzero? && ab.nonzero? && (ab - sfa_balance).abs > BigDecimal("1.00")
cand = nil
end
rescue
# ignore balance parsing errors
end
if cand
chosen = cand
reason = "last4"
end
end
end
# 2) balance proximity
if chosen.nil? && sfa_balance.nonzero?
balance_matches = manuals.reject { |a| used_manual_ids.include?(a.id) }.select do |a|
begin
ab = (a.balance || a.cash_balance || 0).to_d
(ab - sfa_balance).abs <= BigDecimal("0.01")
rescue
false
end
end
if balance_matches.size == 1
chosen = balance_matches.first
reason = "balance"
end
end
# 3) exact normalized name
if chosen.nil?
name_matches = manuals.reject { |a| used_manual_ids.include?(a.id) }.select { |a| normalize_name(a.name) == normalize_name(sfa.name) }
if name_matches.size == 1
chosen = name_matches.first
reason = "name"
end
end
if chosen
used_manual_ids << chosen.id
pairs << { sfa_id: sfa.id, sfa_name: sfa.name, manual_id: chosen.id, manual_name: chosen.name, reason: reason }
end
end
Rails.logger.info("SimpleFin compute_relink_candidates: built #{pairs.size} pairs (item_id=#{@simplefin_item.id})")
# Return without the reason field to the view
pairs.map { |p| p.slice(:sfa_id, :sfa_name, :manual_id, :manual_name) }
end
def set_simplefin_item
@simplefin_item = Current.family.simplefin_items.find(params[:id])
end