Files
sure/app/models/recurring_transaction/cleaner.rb
Guillem Arias Fauste c274c5d8bb fix(recurring): match transfer pairs so Cleaner stops mis-retiring transfers (#2110)
Closes #1590. Implements Option A (the proper fix), replacing the interim skip.

A recurring transfer's name is seeded as "Transfer to {dest}", but future
occurrences carry arbitrary names (user free-text, importer wording, the
auto-matcher), so the name-based matching_transactions returned [] and the
Cleaner retired still-active transfers at the 6-month threshold. main worked
around this by skipping transfer rows entirely (Option B) — which also meant a
genuinely-stopped transfer never got retired.

matching_transactions now detects the Transfer *pair* for transfer rows: an
outflow on the source account paired with an inflow on the destination account,
within the usual amount/cadence window. The Cleaner no longer skips transfers:

- a transfer whose pair still occurs keeps surfacing recent matches → stays active
- a transfer whose pair has stopped → correctly retired

The amount / day-of-month scopes are extracted and shared between the name-based
and pair-based paths. The Identifier's separate transfer skip (auto-identifying
pairs from history) is intentionally untouched — that's the out-of-scope feature
the issue defers.
2026-06-03 00:04:32 +02:00

52 lines
1.7 KiB
Ruby

class RecurringTransaction
class Cleaner
attr_reader :family
def initialize(family)
@family = family
end
# Mark recurring transactions as inactive if they haven't occurred recently
# Uses 2 months for automatic recurring, 6 months for manual recurring.
#
# Transfer rows (destination_account_id present) are included: as of issue
# #1590, `matching_transactions` detects the Transfer pair, so a still-active
# transfer keeps surfacing recent matches and stays active, while one whose
# pair has genuinely stopped is correctly retired.
def cleanup_stale_transactions
stale_count = 0
family.recurring_transactions
.active
.find_each do |recurring_transaction|
next unless recurring_transaction.should_be_inactive?
# Determine threshold based on manual flag
threshold = recurring_transaction.manual? ? 6.months.ago.to_date : 2.months.ago.to_date
# Double-check if there are any recent matching transactions
recent_matches = recurring_transaction.matching_transactions.select { |entry| entry.date >= threshold }
if recent_matches.empty?
recurring_transaction.mark_inactive!
stale_count += 1
end
end
stale_count
end
# Remove inactive recurring transactions that have been inactive for 6+ months
# Manual recurring transactions are never automatically deleted
def remove_old_inactive_transactions
six_months_ago = 6.months.ago
family.recurring_transactions
.inactive
.where(manual: false)
.where("updated_at < ?", six_months_ago)
.destroy_all
end
end
end