fix(enable_banking): clear stuck pending flag when ASPSP reuses same transaction_id (#1982)

* fix(enable_banking): clear stuck pending flag when ASPSP reuses same transaction_id for booked version

* fix: scope pending→booked bypass to user_modified entries only

* refactor: extract clear_pending_flags_from_extra helper to deduplicate pending-flag removal logic

* refactor: use clear_pending_flags_from_extra in user_modified bypass path

* fix(provider_import_adapter): add type check in clear_pending_flags_from_extra

Add a check to ensure that the value associated with a provider key in
the `extra` hash is a Hash before attempting to call `delete` on it.
This prevents a `NoMethodError` when encountering malformed data where
the provider key exists but does not map to a Hash.

* fix(provider_import_adapter): fix indentation and ensure proper return in clear_pending_flags_from_extra

* fix(provider_import_adapter): make clear_pending_flags_from_extra private

* fix: guard clear_pending_flags_from_extra against non-Hash extra values
This commit is contained in:
CrossDrain
2026-05-27 21:36:33 +00:00
committed by Juan José Mata
parent 77590d7cdf
commit 1bc227ea44
2 changed files with 125 additions and 17 deletions

View File

@@ -1445,4 +1445,77 @@ class Account::ProviderImportAdapterTest < ActiveSupport::TestCase
pending1.reload
assert_equal "plaid_pending_1", pending1.external_id
end
# =========================================================================
# Same-external-id pending → booked (e.g. Revolut Italy via Enable Banking)
# Some ASPSPs reuse the same transaction_id for pending and booked, so the
# entry is found by find_or_initialize_by (persisted), bypassing auto-claim.
# =========================================================================
test "clears pending flag when same external_id is reused for booked version (not user-modified)" do
pending_entry = @adapter.import_transaction(
external_id: "eb_same_id_123",
amount: 30.0,
currency: "EUR",
date: Date.today - 2.days,
name: "Piero Fiorista",
source: "enable_banking",
extra: { "enable_banking" => { "pending" => true } }
)
assert pending_entry.transaction.pending?, "entry should start as pending"
# Booked version arrives with the SAME external_id (Revolut Italy behaviour).
# extra is nil (no FX, no MCC) so the deep-merge block would normally be skipped.
assert_no_difference "@account.entries.count" do
booked_entry = @adapter.import_transaction(
external_id: "eb_same_id_123",
amount: 30.0,
currency: "EUR",
date: Date.today,
name: "Piero Fiorista",
source: "enable_banking",
extra: nil
)
assert_equal pending_entry.id, booked_entry.id, "should update the same entry"
assert_not booked_entry.transaction.pending?,
"pending flag should be cleared even when extra is nil"
end
end
test "clears pending flag when same external_id reused and entry is user-modified (Revolut Italy)" do
pending_entry = @adapter.import_transaction(
external_id: "eb_same_id_user_mod",
amount: 50.0,
currency: "EUR",
date: Date.today - 3.days,
name: "Clean Center",
source: "enable_banking",
extra: { "enable_banking" => { "pending" => true } }
)
assert pending_entry.transaction.pending?, "entry should start as pending"
# User categorises the pending entry — sets user_modified = true
pending_entry.mark_user_modified!
assert pending_entry.reload.user_modified?, "entry should be marked user-modified"
# Booked version with the same external_id arrives — protection check would normally
# return early and leave the pending badge intact.
assert_no_difference "@account.entries.count" do
booked_entry = @adapter.import_transaction(
external_id: "eb_same_id_user_mod",
amount: 50.0,
currency: "EUR",
date: Date.today,
name: "Clean Center",
source: "enable_banking",
extra: nil
)
assert_equal pending_entry.id, booked_entry.id, "should reference the same entry"
booked_entry.reload
assert_not booked_entry.transaction.pending?,
"pending flag must be cleared even for user-modified entries"
end
end
end