Files
sure/app/models/enable_banking_account/transactions/processor.rb
CrossDrain 406e7217a1 fix(enable-banking): fix pending→posted auto-claim producing badge, duplicate, and wrong date (#1783)
* fix(enable-banking): clear pending flag and prevent stale re-import after auto-claim

When a booked transaction claims a pending entry via the amount/date heuristic
(find_pending_transaction), two bugs caused the entry to remain incorrectly pending
and the old pending transaction to reappear on subsequent syncs.

Bug 1: The extra["enable_banking"]["pending"] flag was never cleared on the claimed
entry. For simple booked transactions with nil extra the deep-merge path is skipped
entirely, so the pending badge persisted forever.

Bug 2: After the claim the old pending external_id (e.g. PDNG_123) stayed in the
stored raw_transactions_payload. The importer's C4 filter only removes pending
entries whose transaction_id matches a BOOK id — Enable Banking issues completely
different ids for pending vs booked transactions — so PDNG_123 was never pruned.
On the next sync find_or_initialize_by(PDNG_123) couldn't find the claimed entry
(now keyed as BOOK_456) and created a fresh pending duplicate with no category.

Fix: on claim, explicitly clear all providers' pending keys from extra in-memory,
and store the displaced pending external_id in extra["auto_claimed_pending_ids"].
The Processor now queries this field alongside manual_merge to build the excluded_ids
set, so the stale pending data is skipped on every future sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(enable-banking): preserve pending date when claiming transactions

When a pending transaction is claimed by a booked transaction, the
original pending date is now preserved instead of being overwritten
by the booked transaction's date. This ensures historical accuracy
for transactions that were originally recorded on a different date.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 14:03:37 +02:00

131 lines
6.2 KiB
Ruby

class EnableBankingAccount::Transactions::Processor
attr_reader :enable_banking_account
def initialize(enable_banking_account)
@enable_banking_account = enable_banking_account
end
def process
unless enable_banking_account.raw_transactions_payload.present?
Rails.logger.info "EnableBankingAccount::Transactions::Processor - No transactions in raw_transactions_payload for enable_banking_account #{enable_banking_account.id}"
return { success: true, total: 0, imported: 0, failed: 0, errors: [] }
end
total_count = enable_banking_account.raw_transactions_payload.count
Rails.logger.info "EnableBankingAccount::Transactions::Processor - Processing #{total_count} transactions for enable_banking_account #{enable_banking_account.id}"
imported_count = 0
skipped_count = 0
failed_count = 0
errors = []
shared_adapter = if enable_banking_account.current_account.present?
Account::ProviderImportAdapter.new(enable_banking_account.current_account)
end
# Pre-fetch external_ids that must not be re-imported.
# One query per category per sync; O(1) Set lookup per transaction — avoids N+1.
excluded_ids = if enable_banking_account.current_account
account_id = enable_banking_account.current_account.id
# 1. Manually merged: pending entries the user explicitly merged into a posted transaction.
# Uses a lateral join to extract merged_from_external_id from the manual_merge JSON
# (handles both Array current format and legacy Hash format via jsonb_typeof).
manually_merged_ids = Transaction.joins(:entry)
.where(entries: { account_id: account_id })
.where("transactions.extra ? 'manual_merge'")
.joins(
Arel.sql(<<~SQL.squish)
CROSS JOIN LATERAL jsonb_array_elements(
CASE jsonb_typeof(transactions.extra->'manual_merge')
WHEN 'array' THEN transactions.extra->'manual_merge'
WHEN 'object' THEN jsonb_build_array(transactions.extra->'manual_merge')
ELSE '[]'::jsonb
END
) AS merge_elem
SQL
)
.pluck(Arel.sql("merge_elem->>'merged_from_external_id'"))
.compact
.to_set
# 2. Auto-claimed: pending entries that were automatically matched to a booked transaction
# by the amount/date heuristic. Their old external_ids are stored in
# extra["auto_claimed_pending_ids"] so they are not re-imported as new pending entries
# on subsequent syncs (the stored raw payload still contains the old pending data).
auto_claimed_ids = Transaction.joins(:entry)
.where(entries: { account_id: account_id })
.where("transactions.extra ? 'auto_claimed_pending_ids'")
.joins(
Arel.sql(<<~SQL.squish)
CROSS JOIN LATERAL jsonb_array_elements_text(
transactions.extra->'auto_claimed_pending_ids'
) AS claimed_id
SQL
)
.pluck(Arel.sql("claimed_id"))
.compact
.to_set
manually_merged_ids | auto_claimed_ids
else
Set.new
end
enable_banking_account.raw_transactions_payload.each_with_index do |transaction_data, index|
begin
ext_id = EnableBankingEntry::Processor.compute_external_id(transaction_data)
if ext_id && excluded_ids.include?(ext_id)
Rails.logger.info("EnableBankingAccount::Transactions::Processor - Skipping re-import of manually merged pending transaction: #{ext_id}")
skipped_count += 1
next
end
result = EnableBankingEntry::Processor.new(
transaction_data,
enable_banking_account: enable_banking_account,
import_adapter: shared_adapter
).process
if result.nil?
failed_count += 1
errors << { index: index, transaction_id: transaction_data[:transaction_id], error: "No linked account" }
else
imported_count += 1
end
rescue ArgumentError => e
failed_count += 1
transaction_id = transaction_data.try(:[], :transaction_id) || transaction_data.try(:[], "transaction_id") || "unknown"
error_message = "Validation error: #{e.message}"
Rails.logger.error "EnableBankingAccount::Transactions::Processor - #{error_message} (transaction #{transaction_id})"
errors << { index: index, transaction_id: transaction_id, error: error_message }
rescue => e
failed_count += 1
transaction_id = transaction_data.try(:[], :transaction_id) || transaction_data.try(:[], "transaction_id") || "unknown"
error_message = "#{e.class}: #{e.message}"
Rails.logger.error "EnableBankingAccount::Transactions::Processor - Error processing transaction #{transaction_id}: #{error_message}"
Rails.logger.error e.backtrace.join("\n")
errors << { index: index, transaction_id: transaction_id, error: error_message }
end
end
result = {
success: failed_count == 0,
total: total_count,
imported: imported_count,
skipped: skipped_count,
failed: failed_count,
errors: errors
}
if failed_count > 0
Rails.logger.warn "EnableBankingAccount::Transactions::Processor - Completed with #{failed_count} failures out of #{total_count} transactions"
else
Rails.logger.info "EnableBankingAccount::Transactions::Processor - Successfully processed #{imported_count} transactions"
end
result
end
end