mirror of
https://github.com/we-promise/sure.git
synced 2026-06-04 18:29:02 +00:00
* First pass of Akahu * fix up sync all * conflicts * fix db migration issue? - fix auto selection of akahu account type * Address Akahu PR feedback * Complete provider metadata * Fix PR 1921 CI tests * PR feedback * PR feedback * post merge --------- Co-authored-by: failing <failing@users.noreply.github.com> Co-authored-by: Juan José Mata <jjmata@jjmata.com> Co-authored-by: sure-admin <sure-admin@splashblot.com>
242 lines
7.4 KiB
Ruby
242 lines
7.4 KiB
Ruby
require "digest/md5"
|
|
|
|
class AkahuEntry::Processor
|
|
include CurrencyNormalizable
|
|
|
|
def self.canonical_external_id(akahu_transaction)
|
|
data = akahu_transaction.with_indifferent_access
|
|
id = data[:_id].presence || data[:id].presence
|
|
return "akahu_#{id}" if id.present?
|
|
|
|
"akahu_pending_#{content_hash_for(data)}"
|
|
end
|
|
|
|
def self.pending?(akahu_transaction)
|
|
data = akahu_transaction.with_indifferent_access
|
|
ActiveModel::Type::Boolean.new.cast(data[:_pending]) == true ||
|
|
ActiveModel::Type::Boolean.new.cast(data[:pending]) == true
|
|
end
|
|
|
|
def self.content_hash_for(data)
|
|
merchant = data[:merchant].is_a?(Hash) ? data[:merchant].with_indifferent_access : {}
|
|
attributes = [
|
|
data[:_account],
|
|
data[:account],
|
|
data[:date],
|
|
data[:amount],
|
|
data[:description],
|
|
merchant[:name].to_s.strip.presence,
|
|
data[:type]
|
|
].compact.join("|")
|
|
|
|
Digest::MD5.hexdigest(attributes)
|
|
end
|
|
|
|
def initialize(akahu_transaction, akahu_account:)
|
|
@akahu_transaction = akahu_transaction
|
|
@akahu_account = akahu_account
|
|
end
|
|
|
|
def process
|
|
unless account.present?
|
|
Rails.logger.warn "AkahuEntry::Processor - No linked account for akahu_account #{akahu_account.id}, skipping transaction #{external_id}"
|
|
return nil
|
|
end
|
|
|
|
import_adapter.import_transaction(
|
|
external_id: external_id,
|
|
amount: amount,
|
|
currency: currency,
|
|
date: date,
|
|
name: name,
|
|
source: "akahu",
|
|
merchant: merchant,
|
|
notes: notes,
|
|
extra: extra_metadata
|
|
)
|
|
rescue ArgumentError => e
|
|
Rails.logger.error "AkahuEntry::Processor - Validation error for transaction #{external_id}: #{e.message}"
|
|
raise
|
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
|
Rails.logger.error "AkahuEntry::Processor - Failed to save transaction #{external_id}: #{e.message}"
|
|
raise StandardError.new("Failed to import transaction: #{e.message}")
|
|
rescue => e
|
|
Rails.logger.error "AkahuEntry::Processor - Unexpected error processing transaction #{external_id}: #{e.class} - #{e.message}"
|
|
Rails.logger.error e.backtrace.join("\n")
|
|
raise StandardError.new("Unexpected error importing transaction: #{e.message}")
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :akahu_transaction, :akahu_account
|
|
|
|
def import_adapter
|
|
@import_adapter ||= Account::ProviderImportAdapter.new(account)
|
|
end
|
|
|
|
def account
|
|
@account ||= akahu_account.current_account
|
|
end
|
|
|
|
def data
|
|
@data ||= akahu_transaction.with_indifferent_access
|
|
end
|
|
|
|
def external_id
|
|
@external_id ||= begin
|
|
id = data[:_id].presence || data[:id].presence
|
|
if id.present?
|
|
"akahu_#{id}"
|
|
else
|
|
base_id = self.class.canonical_external_id(data)
|
|
if existing_pending_entry?(base_id)
|
|
base_id
|
|
else
|
|
final_id = base_id
|
|
counter = 1
|
|
while entry_exists_with_external_id?(final_id)
|
|
final_id = "#{base_id}_#{counter}"
|
|
counter += 1
|
|
end
|
|
|
|
final_id
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def existing_pending_entry?(external_id)
|
|
existing_entry = account&.entries&.find_by(external_id: external_id, source: "akahu")
|
|
existing_entry&.entryable.is_a?(Transaction) && existing_entry.entryable.pending?
|
|
end
|
|
|
|
def entry_exists_with_external_id?(external_id)
|
|
account.present? && account.entries.exists?(external_id: external_id, source: "akahu")
|
|
end
|
|
|
|
def name
|
|
merchant_name.presence || data[:description].presence || I18n.t("transactions.unknown_name")
|
|
end
|
|
|
|
def notes
|
|
meta = meta_data
|
|
parts = []
|
|
parts << data[:description] if data[:description].present? && data[:description] != name
|
|
parts << "#{t('akahu_entry.notes.reference')}: #{meta[:reference]}" if meta[:reference].present?
|
|
parts << "#{t('akahu_entry.notes.particulars')}: #{meta[:particulars]}" if meta[:particulars].present?
|
|
parts << "#{t('akahu_entry.notes.code')}: #{meta[:code]}" if meta[:code].present?
|
|
parts << "#{t('akahu_entry.notes.other_account')}: #{meta[:other_account]}" if meta[:other_account].present?
|
|
parts.presence&.join(" | ")
|
|
end
|
|
|
|
def merchant
|
|
return nil unless merchant_name.present?
|
|
|
|
provider_merchant_id = merchant_data[:_id].presence || merchant_data[:id].presence
|
|
provider_merchant_id ||= "akahu_merchant_#{Digest::MD5.hexdigest(merchant_name.downcase)}"
|
|
|
|
@merchant ||= import_adapter.find_or_create_merchant(
|
|
provider_merchant_id: provider_merchant_id,
|
|
name: merchant_name,
|
|
source: "akahu",
|
|
website_url: merchant_data[:website]
|
|
)
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
Rails.logger.error "AkahuEntry::Processor - Failed to create merchant '#{merchant_name}': #{e.message}"
|
|
nil
|
|
end
|
|
|
|
def amount
|
|
parsed_amount = case data[:amount]
|
|
when String
|
|
BigDecimal(data[:amount])
|
|
when Numeric
|
|
BigDecimal(data[:amount].to_s)
|
|
else
|
|
BigDecimal("0")
|
|
end
|
|
|
|
# Akahu uses banking convention: negative is money out, positive is money in.
|
|
# Sure stores expenses as positive and income as negative.
|
|
-parsed_amount
|
|
rescue ArgumentError => e
|
|
Rails.logger.error "Failed to parse Akahu transaction amount: #{e.class}"
|
|
raise ArgumentError, "Invalid transaction amount"
|
|
end
|
|
|
|
def currency
|
|
parse_currency(data[:currency]) || akahu_account.currency || account&.currency || "NZD"
|
|
end
|
|
|
|
def date
|
|
value = data[:date]
|
|
case value
|
|
when String
|
|
Date.parse(value)
|
|
when Integer, Float
|
|
Time.at(value).to_date
|
|
when Time, DateTime
|
|
value.to_date
|
|
when Date
|
|
value
|
|
else
|
|
Rails.logger.error("Akahu transaction has invalid date value")
|
|
raise ArgumentError, "Invalid date format"
|
|
end
|
|
rescue ArgumentError, TypeError => e
|
|
Rails.logger.error("Failed to parse Akahu transaction date: #{e.class}")
|
|
raise ArgumentError, "Unable to parse transaction date"
|
|
end
|
|
|
|
def extra_metadata
|
|
{
|
|
"akahu" => {
|
|
"pending" => pending?,
|
|
"type" => data[:type],
|
|
"category" => category_data[:name],
|
|
"category_id" => category_data[:_id].presence || category_data[:id],
|
|
"category_group" => category_group_name,
|
|
"reference" => meta_data[:reference],
|
|
"particulars" => meta_data[:particulars],
|
|
"code" => meta_data[:code],
|
|
"other_account" => meta_data[:other_account]
|
|
}.compact
|
|
}
|
|
end
|
|
|
|
def pending?
|
|
self.class.pending?(data)
|
|
end
|
|
|
|
def merchant_data
|
|
@merchant_data ||= data[:merchant].is_a?(Hash) ? data[:merchant].with_indifferent_access : {}
|
|
end
|
|
|
|
def merchant_name
|
|
merchant_data[:name].to_s.strip.presence
|
|
end
|
|
|
|
def category_data
|
|
@category_data ||= data[:category].is_a?(Hash) ? data[:category].with_indifferent_access : {}
|
|
end
|
|
|
|
def category_group_name
|
|
groups = category_data[:groups]
|
|
return nil unless groups.is_a?(Hash)
|
|
|
|
groups.with_indifferent_access.dig(:personal_finance, :name)
|
|
end
|
|
|
|
def meta_data
|
|
@meta_data ||= data[:meta].is_a?(Hash) ? data[:meta].with_indifferent_access : {}
|
|
end
|
|
|
|
def t(key, **options)
|
|
I18n.t(key, **options)
|
|
end
|
|
|
|
def log_invalid_currency(currency_value)
|
|
Rails.logger.warn("Invalid currency code '#{currency_value}' in Akahu transaction #{external_id}, falling back to account currency")
|
|
end
|
|
end
|