Files
sure/lib/generators/provider/family/templates/transactions_processor.rb.tt

148 lines
5.6 KiB
Plaintext

# frozen_string_literal: true
class <%= class_name %>Account::Transactions::Processor
include <%= class_name %>Account::DataHelpers
attr_reader :<%= file_name %>_account
def initialize(<%= file_name %>_account)
@<%= file_name %>_account = <%= file_name %>_account
end
def process
unless <%= file_name %>_account.raw_transactions_payload.present?
Rails.logger.info "<%= class_name %>Account::Transactions::Processor - No transactions in raw_transactions_payload for <%= file_name %>_account #{<%= file_name %>_account.id}"
return { success: true, total: 0, imported: 0, failed: 0, errors: [] }
end
total_count = <%= file_name %>_account.raw_transactions_payload.count
Rails.logger.info "<%= class_name %>Account::Transactions::Processor - Processing #{total_count} transactions for <%= file_name %>_account #{<%= file_name %>_account.id}"
imported_count = 0
failed_count = 0
errors = []
# Each entry is processed inside a transaction, but to avoid locking up the DB when
# there are hundreds or thousands of transactions, we process them individually.
<%= file_name %>_account.raw_transactions_payload.each_with_index do |transaction_data, index|
begin
result = process_transaction(transaction_data)
if result.nil?
# Transaction was skipped (e.g., no linked account or blank external_id)
failed_count += 1
transaction_id = transaction_data.try(:[], :id) || transaction_data.try(:[], "id") || "unknown"
errors << { index: index, transaction_id: transaction_id, error: "Skipped" }
else
imported_count += 1
end
rescue ArgumentError => e
# Validation error - log and continue
failed_count += 1
transaction_id = transaction_data.try(:[], :id) || transaction_data.try(:[], "id") || "unknown"
error_message = "Validation error: #{e.message}"
Rails.logger.error "<%= class_name %>Account::Transactions::Processor - #{error_message} (transaction #{transaction_id})"
errors << { index: index, transaction_id: transaction_id, error: error_message }
rescue => e
# Unexpected error - log with full context and continue
failed_count += 1
transaction_id = transaction_data.try(:[], :id) || transaction_data.try(:[], "id") || "unknown"
error_message = "#{e.class}: #{e.message}"
Rails.logger.error "<%= class_name %>Account::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,
failed: failed_count,
errors: errors
}
if failed_count > 0
Rails.logger.warn "<%= class_name %>Account::Transactions::Processor - Completed with #{failed_count} failures out of #{total_count} transactions"
else
Rails.logger.info "<%= class_name %>Account::Transactions::Processor - Successfully processed #{imported_count} transactions"
end
result
end
private
def account
@<%= file_name %>_account.current_account
end
def import_adapter
@import_adapter ||= Account::ProviderImportAdapter.new(account)
end
def process_transaction(transaction_data)
return nil unless account.present?
data = transaction_data.with_indifferent_access
# TODO: Customize based on your provider's transaction format
# Extract transaction fields from the provider's API response
external_id = (data[:id] || data[:transaction_id]).to_s
return nil if external_id.blank?
# Parse transaction attributes
amount = parse_transaction_amount(data)
return nil if amount.nil?
# TODO: Customize date field names based on your provider
date = parse_date(data[:date] || data[:transaction_date] || data[:posted_at])
return nil if date.nil?
name = data[:name] || data[:description] || data[:merchant_name] || "Transaction"
currency = extract_currency(data, fallback: account.currency)
# Build provider-specific metadata for transaction.extra
extra = build_extra_metadata(data)
Rails.logger.info "<%= class_name %>Account::Transactions::Processor - Importing transaction: id=#{external_id} amount=#{amount} date=#{date}"
# Use ProviderImportAdapter for proper deduplication via external_id + source
import_adapter.import_transaction(
external_id: external_id,
amount: amount,
currency: currency,
date: date,
name: name[0..254], # Limit to 255 chars
source: "<%= file_name %>",
extra: extra
)
end
def parse_transaction_amount(data)
amount = parse_decimal(data[:amount])
return nil if amount.nil?
# TODO: Adjust sign convention based on your provider
# Most banking APIs use positive amounts for debits (money out)
# and negative amounts for credits (money in)
# Sure convention: positive = money out, negative = money in
#
# If your provider uses the opposite convention, negate the amount:
# amount = -amount
amount
end
def build_extra_metadata(data)
# TODO: Customize which fields to store based on your provider
{
"<%= file_name %>" => {
"id" => data[:id] || data[:transaction_id],
"pending" => data[:pending] || data[:is_pending],
"merchant" => data[:merchant] || data[:merchant_name],
"category" => data[:category]
}.compact
}
end
end