Files
sure/test/controllers/pending_duplicate_merges_controller_test.rb
AdamWHY2K 3a869c760e feat: add pending transaction manual merging tool (#1088)
* refactor: use a map of providers that support pending transactions

* feat: add pending transaction manual merging tool

* fix(coderabbit): validate posted_entry_id against eligible posted candidates server-side

* fix(coderabbit): validate offset for negative numbers

* fix(coderabbit): check if pending_duplicate_candidates has_more in one transaction

* refactor: use list of radio buttons for better pagination

* chore: show current transaction range in paginated view

* chore: whitespace

chore: whitespace
2026-03-14 20:32:13 +01:00

206 lines
7.7 KiB
Ruby

require "test_helper"
class PendingDuplicateMergesControllerTest < ActionDispatch::IntegrationTest
include EntriesTestHelper
setup do
sign_in @user = users(:family_admin)
@account = accounts(:depository)
end
test "new displays pending transaction and candidate posted transactions" do
pending_transaction = create_pending_transaction(amount: -50, account: @account)
posted_transaction = create_transaction(amount: -50, account: @account)
get new_transaction_pending_duplicate_merges_path(pending_transaction)
assert_response :success
end
test "new redirects if transaction is not pending" do
posted_transaction = create_transaction(amount: -50, account: @account)
get new_transaction_pending_duplicate_merges_path(posted_transaction)
assert_redirected_to transactions_path
assert_equal "This feature is only available for pending transactions", flash[:alert]
end
test "create merges pending transaction with selected posted transaction" do
pending_transaction = create_pending_transaction(amount: -50, account: @account)
posted_transaction = create_transaction(amount: -50, account: @account)
assert_difference "Entry.count", -1 do
post transaction_pending_duplicate_merges_path(pending_transaction), params: {
pending_duplicate_merges: {
posted_entry_id: posted_transaction.id
}
}
end
assert_redirected_to transactions_path
assert_equal "Pending transaction merged with posted transaction", flash[:notice]
assert_nil Entry.find_by(id: pending_transaction.id), "Pending entry should be deleted after merge"
end
test "create redirects back to referer after successful merge" do
pending_transaction = create_pending_transaction(amount: -50, account: @account)
posted_transaction = create_transaction(amount: -50, account: @account)
assert_difference "Entry.count", -1 do
post transaction_pending_duplicate_merges_path(pending_transaction),
params: {
pending_duplicate_merges: {
posted_entry_id: posted_transaction.id
}
},
headers: { "HTTP_REFERER" => account_path(@account) }
end
assert_redirected_to account_path(@account)
assert_equal "Pending transaction merged with posted transaction", flash[:notice]
end
test "create redirects with error if no posted entry selected" do
pending_transaction = create_pending_transaction(amount: -50, account: @account)
post transaction_pending_duplicate_merges_path(pending_transaction), params: {
pending_duplicate_merges: {
posted_entry_id: ""
}
}
assert_redirected_to transactions_path
assert_equal "Please select a posted transaction to merge with", flash[:alert]
end
test "create stores potential_posted_match metadata before merging" do
pending_transaction = create_pending_transaction(amount: -50, account: @account)
posted_transaction = create_transaction(amount: -50, account: @account)
# Stub merge to prevent deletion so we can check metadata
Transaction.any_instance.stubs(:merge_with_duplicate!).returns(true)
post transaction_pending_duplicate_merges_path(pending_transaction), params: {
pending_duplicate_merges: {
posted_entry_id: posted_transaction.id
}
}
pending_transaction.reload
metadata = pending_transaction.entryable.extra["potential_posted_match"]
assert_not_nil metadata
assert_equal posted_transaction.id, metadata["entry_id"]
assert_equal "manual_match", metadata["reason"]
assert_equal posted_transaction.amount.to_s, metadata["posted_amount"]
assert_equal "high", metadata["confidence"]
assert_equal Date.current.to_s, metadata["detected_at"]
end
test "pending_duplicate_candidates excludes pending transactions" do
pending_transaction = create_pending_transaction(amount: -50, account: @account)
posted_transaction = create_transaction(amount: -50, account: @account)
another_pending = create_pending_transaction(amount: -40, account: @account)
candidates = pending_transaction.entryable.pending_duplicate_candidates
assert_includes candidates.map(&:id), posted_transaction.id
assert_not_includes candidates.map(&:id), another_pending.id
end
test "pending_duplicate_candidates only shows same account and currency" do
pending_transaction = create_pending_transaction(amount: -50, account: @account, currency: "USD")
same_account_transaction = create_transaction(amount: -50, account: @account, currency: "USD")
different_account_transaction = create_transaction(amount: -50, account: accounts(:investment), currency: "USD")
different_currency_transaction = create_transaction(amount: -50, account: @account, currency: "EUR")
candidates = pending_transaction.entryable.pending_duplicate_candidates
assert_includes candidates.map(&:id), same_account_transaction.id
assert_not_includes candidates.map(&:id), different_account_transaction.id
assert_not_includes candidates.map(&:id), different_currency_transaction.id
end
test "create rejects merge with pending transaction" do
pending_transaction = create_pending_transaction(amount: -50, account: @account)
another_pending = create_pending_transaction(amount: -50, account: @account)
assert_no_difference "Entry.count" do
post transaction_pending_duplicate_merges_path(pending_transaction), params: {
pending_duplicate_merges: {
posted_entry_id: another_pending.id
}
}
end
assert_redirected_to transactions_path
assert_equal "Invalid transaction selected for merge", flash[:alert]
end
test "create rejects merge with transaction from different account" do
pending_transaction = create_pending_transaction(amount: -50, account: @account)
different_account_transaction = create_transaction(amount: -50, account: accounts(:investment))
assert_no_difference "Entry.count" do
post transaction_pending_duplicate_merges_path(pending_transaction), params: {
pending_duplicate_merges: {
posted_entry_id: different_account_transaction.id
}
}
end
assert_redirected_to transactions_path
assert_equal "Invalid transaction selected for merge", flash[:alert]
end
test "create rejects merge with transaction in different currency" do
pending_transaction = create_pending_transaction(amount: -50, account: @account, currency: "USD")
different_currency_transaction = create_transaction(amount: -50, account: @account, currency: "EUR")
assert_no_difference "Entry.count" do
post transaction_pending_duplicate_merges_path(pending_transaction), params: {
pending_duplicate_merges: {
posted_entry_id: different_currency_transaction.id
}
}
end
assert_redirected_to transactions_path
assert_equal "Invalid transaction selected for merge", flash[:alert]
end
test "create rejects merge with invalid entry id" do
pending_transaction = create_pending_transaction(amount: -50, account: @account)
assert_no_difference "Entry.count" do
post transaction_pending_duplicate_merges_path(pending_transaction), params: {
pending_duplicate_merges: {
posted_entry_id: 999999
}
}
end
assert_redirected_to transactions_path
assert_equal "Invalid transaction selected for merge", flash[:alert]
end
private
def create_pending_transaction(attributes = {})
# Create a transaction with pending metadata
transaction = create_transaction(attributes)
# Mark it as pending by adding extra metadata
transaction.entryable.update!(
extra: {
"simplefin" => {
"pending" => true
}
}
)
transaction
end
end