mirror of
https://github.com/we-promise/sure.git
synced 2026-06-04 10:19:03 +00:00
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.
This commit is contained in:
committed by
GitHub
parent
e232818e97
commit
c274c5d8bb
@@ -998,12 +998,9 @@ class RecurringTransactionTest < ActiveSupport::TestCase
|
||||
assert_not RecurringTransaction.exists?(rt.id)
|
||||
end
|
||||
|
||||
test "Cleaner skips recurring transfers so they aren't mistakenly marked inactive" do
|
||||
# `matching_transactions` is single-account name/amount-based and never
|
||||
# matches a Transfer pair, so without the skip the recurring transfer
|
||||
# would flip to inactive at the 6-month threshold even when the user
|
||||
# is still doing the transfer monthly. Issue #1590 tracks the proper
|
||||
# pair-detection fix.
|
||||
test "Cleaner keeps a recurring transfer active when its pair still occurs (issue #1590)" do
|
||||
# The seed name rarely matches future occurrences, so pair detection (not
|
||||
# name matching) is what keeps a live transfer active past the threshold.
|
||||
rt = @family.recurring_transactions.create!(
|
||||
account: @account, destination_account: accounts(:credit_card),
|
||||
name: "Transfer to CC", amount: 250, currency: "USD",
|
||||
@@ -1012,12 +1009,61 @@ class RecurringTransactionTest < ActiveSupport::TestCase
|
||||
next_expected_date: 5.days.from_now.to_date,
|
||||
manual: true
|
||||
)
|
||||
assert rt.should_be_inactive?, "guard sanity: row would be marked inactive without the skip"
|
||||
assert rt.should_be_inactive?, "guard sanity: stale last_occurrence_date"
|
||||
|
||||
# A fresh transfer pair this cycle, carrying a *different* free-text name.
|
||||
date = 1.month.ago.beginning_of_month + 4.days # day-of-month 5
|
||||
outflow = @account.entries.create!(
|
||||
date: date, amount: 250, currency: "USD", name: "rent transfer",
|
||||
entryable: Transaction.new(kind: "funds_movement")
|
||||
)
|
||||
inflow = accounts(:credit_card).entries.create!(
|
||||
date: date, amount: -250, currency: "USD", name: "rent transfer",
|
||||
entryable: Transaction.new(kind: "funds_movement")
|
||||
)
|
||||
Transfer.create!(outflow_transaction: outflow.entryable, inflow_transaction: inflow.entryable)
|
||||
|
||||
RecurringTransaction.cleanup_stale_for(@family)
|
||||
assert_equal "active", rt.reload.status
|
||||
end
|
||||
|
||||
test "Cleaner retires a recurring transfer whose pair has stopped" do
|
||||
# No matching Transfer pair → genuinely stale → should be retired. This is
|
||||
# the correctness the pair-detection (vs the old blanket skip) buys us.
|
||||
rt = @family.recurring_transactions.create!(
|
||||
account: @account, destination_account: accounts(:credit_card),
|
||||
name: "Transfer to CC", amount: 250, currency: "USD",
|
||||
expected_day_of_month: 5,
|
||||
last_occurrence_date: 7.months.ago.to_date,
|
||||
next_expected_date: 5.days.from_now.to_date,
|
||||
manual: true
|
||||
)
|
||||
|
||||
RecurringTransaction.cleanup_stale_for(@family)
|
||||
assert_equal "inactive", rt.reload.status
|
||||
end
|
||||
|
||||
test "matching_transactions finds the transfer pair regardless of occurrence name" do
|
||||
rt = @family.recurring_transactions.create!(
|
||||
account: @account, destination_account: accounts(:credit_card),
|
||||
name: "Transfer to CC", amount: 250, currency: "USD",
|
||||
expected_day_of_month: 5, last_occurrence_date: Date.current,
|
||||
next_expected_date: 1.month.from_now.to_date, manual: true
|
||||
)
|
||||
date = Date.current.beginning_of_month + 4.days # day-of-month 5
|
||||
outflow = @account.entries.create!(
|
||||
date: date, amount: 250, currency: "USD", name: "an importer's wording",
|
||||
entryable: Transaction.new(kind: "funds_movement")
|
||||
)
|
||||
inflow = accounts(:credit_card).entries.create!(
|
||||
date: date, amount: -250, currency: "USD", name: "an importer's wording",
|
||||
entryable: Transaction.new(kind: "funds_movement")
|
||||
)
|
||||
Transfer.create!(outflow_transaction: outflow.entryable, inflow_transaction: inflow.entryable)
|
||||
|
||||
assert_includes rt.matching_transactions.map(&:id), outflow.id
|
||||
end
|
||||
|
||||
test "Identifier#update_manual_recurring_transactions skips recurring transfers" do
|
||||
# Same reasoning as the Cleaner skip. Without the guard, the helper
|
||||
# would call find_matching_transaction_entries (single-account, by
|
||||
|
||||
Reference in New Issue
Block a user