mirror of
https://github.com/we-promise/sure.git
synced 2026-07-28 04:32:12 +00:00
* fix(lunchflow): prune orphaned accounts deleted upstream (#1861) Accounts deleted in Lunch Flow lingered in Sure as unlinked LunchflowAccount records, permanently pinning the item to "Need setup". The importer created/updated accounts returned by the API but never removed records for accounts that disappeared upstream. Add prune_orphaned_lunchflow_accounts, mirroring SimpleFin's prune_orphaned_simplefin_accounts: after importing, delete LunchflowAccount records whose account_id is no longer returned upstream and that are not linked to an Account via AccountProvider. Linked accounts are kept so the prune never cascade-destroys a user's Account. The prune is guarded to a non-empty upstream list so a transient empty/failed response can't wipe out all accounts. Fixes #1861 * fix(lunchflow): prune NULL-id orphans + consistent import return shape (#1886) * fix(lunchflow): make orphan prune resilient to destroy failures Wrap the per-record destroy in begin/rescue so a single failed prune doesn't abort the whole import. Pruning runs before transaction fetch, so an unhandled error would have blocked transaction syncing entirely. Matches the importer's existing continue-on-error convention. * fix(lunchflow): only count pruned accounts when destroy succeeds destroy returns false (without raising) when a before_destroy callback halts deletion; the prior code incremented the pruned count regardless, which could inflate it. Increment only on success and log when halted.
This commit is contained in:
121
test/models/lunchflow_item/importer_orphan_prune_test.rb
Normal file
121
test/models/lunchflow_item/importer_orphan_prune_test.rb
Normal file
@@ -0,0 +1,121 @@
|
||||
require "test_helper"
|
||||
|
||||
class LunchflowItem::ImporterOrphanPruneTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@family = families(:dylan_family)
|
||||
@item = LunchflowItem.create!(
|
||||
family: @family,
|
||||
name: "Test Lunchflow",
|
||||
api_key: "test_key_123",
|
||||
status: :good
|
||||
)
|
||||
@importer = LunchflowItem::Importer.new(@item, lunchflow_provider: mock())
|
||||
end
|
||||
|
||||
test "prunes orphaned unlinked LunchflowAccount no longer returned upstream" do
|
||||
orphan = @item.lunchflow_accounts.create!(
|
||||
account_id: "acct-old",
|
||||
name: "Deleted Account",
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
pruned = @importer.send(:prune_orphaned_lunchflow_accounts, [ "acct-new" ])
|
||||
|
||||
assert_equal 1, pruned
|
||||
assert_nil LunchflowAccount.find_by(id: orphan.id), "orphaned unlinked account should be deleted"
|
||||
end
|
||||
|
||||
test "keeps unlinked LunchflowAccount that is still returned upstream" do
|
||||
still_present = @item.lunchflow_accounts.create!(
|
||||
account_id: "acct-keep",
|
||||
name: "Active Account",
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
pruned = @importer.send(:prune_orphaned_lunchflow_accounts, [ "acct-keep" ])
|
||||
|
||||
assert_equal 0, pruned
|
||||
assert_not_nil LunchflowAccount.find_by(id: still_present.id)
|
||||
end
|
||||
|
||||
test "does not prune a LunchflowAccount linked via AccountProvider" do
|
||||
linked = @item.lunchflow_accounts.create!(
|
||||
account_id: "acct-linked",
|
||||
name: "Linked Account",
|
||||
currency: "USD"
|
||||
)
|
||||
account = @family.accounts.create!(
|
||||
name: "Linked Checking",
|
||||
balance: 100,
|
||||
currency: "USD",
|
||||
accountable: Depository.new(subtype: "checking")
|
||||
)
|
||||
AccountProvider.create!(account: account, provider: linked)
|
||||
|
||||
# Even though it's gone upstream, a linked account must be kept (deleting it
|
||||
# would cascade-destroy the AccountProvider and orphan the user's Account).
|
||||
pruned = @importer.send(:prune_orphaned_lunchflow_accounts, [ "acct-other" ])
|
||||
|
||||
assert_equal 0, pruned
|
||||
assert_not_nil LunchflowAccount.find_by(id: linked.id), "linked account should not be deleted"
|
||||
end
|
||||
|
||||
test "blank upstream list is a no-op so transient failures cannot wipe accounts" do
|
||||
orphan = @item.lunchflow_accounts.create!(
|
||||
account_id: "acct-old",
|
||||
name: "Account",
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
assert_equal 0, @importer.send(:prune_orphaned_lunchflow_accounts, [])
|
||||
assert_not_nil LunchflowAccount.find_by(id: orphan.id), "must not prune when upstream list is empty"
|
||||
end
|
||||
|
||||
test "prunes multiple orphaned unlinked accounts" do
|
||||
orphan1 = @item.lunchflow_accounts.create!(account_id: "old-1", name: "One", currency: "USD")
|
||||
orphan2 = @item.lunchflow_accounts.create!(account_id: "old-2", name: "Two", currency: "USD")
|
||||
kept = @item.lunchflow_accounts.create!(account_id: "new-1", name: "Three", currency: "USD")
|
||||
|
||||
pruned = @importer.send(:prune_orphaned_lunchflow_accounts, [ "new-1" ])
|
||||
|
||||
assert_equal 2, pruned
|
||||
assert_nil LunchflowAccount.find_by(id: orphan1.id)
|
||||
assert_nil LunchflowAccount.find_by(id: orphan2.id)
|
||||
assert_not_nil LunchflowAccount.find_by(id: kept.id)
|
||||
end
|
||||
|
||||
test "prunes an unlinked LunchflowAccount with a nil account_id" do
|
||||
# account_id is nullable; a NULL id can never match upstream and would be
|
||||
# silently skipped by a plain `where.not(account_id: ...)` (SQL three-valued
|
||||
# logic). Such an unlinked record is a genuine orphan and must be pruned.
|
||||
orphan = @item.lunchflow_accounts.create!(
|
||||
account_id: nil,
|
||||
name: "Never received an upstream id",
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
pruned = @importer.send(:prune_orphaned_lunchflow_accounts, [ "acct-new" ])
|
||||
|
||||
assert_equal 1, pruned
|
||||
assert_nil LunchflowAccount.find_by(id: orphan.id), "nil account_id orphan should be pruned"
|
||||
end
|
||||
|
||||
test "import returns accounts_pruned in its result and prunes orphans end-to-end" do
|
||||
orphan = @item.lunchflow_accounts.create!(
|
||||
account_id: "acct-old",
|
||||
name: "Deleted Account",
|
||||
currency: "USD"
|
||||
)
|
||||
|
||||
provider = mock()
|
||||
provider.stubs(:get_accounts).returns(
|
||||
accounts: [ { id: "acct-new", name: "New Account", currency: "USD" } ]
|
||||
)
|
||||
|
||||
importer = LunchflowItem::Importer.new(@item, lunchflow_provider: provider)
|
||||
result = importer.import
|
||||
|
||||
assert_equal 1, result[:accounts_pruned]
|
||||
assert_nil LunchflowAccount.find_by(id: orphan.id), "orphan should be pruned through import"
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user