{"record":{"id":"022f834ac8ff039f","repo":"we-promise/sure","slug":"invalid-transaction-amount","errorCode":null,"errorMessage":"Invalid transaction amount","messagePattern":"Invalid transaction amount","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"app/models/akahu_entry/processor.rb","lineNumber":164,"sourceCode":"      nil\n    end\n\n    def amount\n      parsed_amount = case data[:amount]\n      when String\n        BigDecimal(data[:amount])\n      when Numeric\n        BigDecimal(data[:amount].to_s)\n      else\n        BigDecimal(\"0\")\n      end\n\n      # Akahu uses banking convention: negative is money out, positive is money in.\n      # Sure stores expenses as positive and income as negative.\n      -parsed_amount\n    rescue ArgumentError => e\n      Rails.logger.error \"Failed to parse Akahu transaction amount: #{e.class}\"\n      raise ArgumentError, \"Invalid transaction amount\"\n    end\n\n    def currency\n      parse_currency(data[:currency]) || akahu_account.currency || account&.currency || \"NZD\"\n    end\n\n    def date\n      value = data[:date]\n      case value\n      when String\n        if value.include?(\"T\") || value.include?(\":\")\n          Time.parse(value).in_time_zone(account&.family&.timezone).to_date\n        else\n          Date.parse(value)\n        end\n      when Integer, Float\n        Time.at(value).in_time_zone(account&.family&.timezone).to_date\n      when Time, DateTime","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/akahu_entry/processor.rb#L146-L182","documentation":"AkahuEntry::Processor parses each Akahu transaction's amount into a BigDecimal (String via BigDecimal(str), Numeric via BigDecimal(to_s)) and then negates it (Akuhu's banking convention: negative = out) for Sure's storage convention. If the amount string is present but BigDecimal() cannot parse it, the ArgumentError is caught, the class is logged (\"Failed to parse Akahu transaction amount: ArgumentError\"), and it re-raises ArgumentError(\"Invalid transaction amount\"). Note the else branch maps unknown types (nil, hash) to BigDecimal(\"0\") — so this error specifically means: a String/Numeric that looks parseable but isn't.","triggerScenarios":"Akahu returns an amount as \"1,234.56\" (thousands separator), \"12.34.56\" (double dot), \"\" handled? no — empty string: BigDecimal(\"\") raises ArgumentError, so a blank-but-present amount string trips this; localized decimal comma \"12,50\"; amounts with currency symbols \"$45.00\"; scientific notation edge strings some proxy normalizes; an API schema change surfacing amount as a formatted display string rather than raw decimal.","commonSituations":"Upstream Akahu API behavior changes (raw numeric field becomes display-formatted); an intermediate layer (custom proxy, VCR cassette editing, JSON transform) reformatting numbers; sandbox data with placeholder strings like \"N/A\"; regional formatting injected by a serialization library.","solutions":["Inspect the exact payload: the log line records the failure class; enable raw payload debug (compare with other providers' *_DEBUG_RAW pattern) or pry into data[:amount] to see the malformed value","Normalize the string before it reaches the processor: strip currency symbols and commas (\"$1,234.56\" -> \"1234.56\")","If Akahu genuinely changed the field format, patch parse_amount's String branch (e.g. data[:amount].to_s.gsub(/[^0-9.\\-]/, \"\")) and add a regression test","Skip/quarantine the offending transaction rather than aborting the whole sync if one row is bad — decide policy explicitly"],"exampleFix":"# before\ndef parse_amount\n  parsed = case (a = data[:amount])\n           when String then BigDecimal(a)      # \"1,234.56\" -> ArgumentError\n           ...\n\n# after\ndef parse_amount\n  parsed = case (a = data[:amount])\n           when String then BigDecimal(a.to_s.delete(\",$\").strip) # \"1,234.56\" -> 1234.56\n           when Numeric then BigDecimal(a.to_s)\n           else BigDecimal(\"0\")\n           end\n  -parsed\nrescue ArgumentError => e\n  Rails.logger.error(\"Failed to parse Akahu transaction amount=#{data[:amount].inspect}\")\n  raise ArgumentError, \"Invalid transaction amount\"\nend","handlingStrategy":"validation","validationCode":"# Normalize before parse, in your layer feeding the processor\nraw = data[:amount]\nnormalized =\n  case raw\n  when Numeric then raw.to_s\n  when String then raw.to_s.gsub(/[^0-9.\\-]/, \"\") # strips $ , spaces\n  else \"0\"\n  end\nBigDecimal(normalized) rescue BigDecimal(\"0\")","typeGuard":"def parseable_amount?(value)\n  return true if value.is_a?(Numeric)\n  begin\n    BigDecimal(value.to_s.strip)\n    true\n  rescue ArgumentError\n    false\n  end\nend","tryCatchPattern":"rescue ArgumentError => e\n  if e.message == \"Invalid transaction amount\"\n    # log data[:amount].inspect, quarantine this transaction, continue the sync\n    Rails.logger.error(\"akahu amount=#{data[:amount].inspect}\")\n    next # or mark row skipped\n  else\n    raise\n  end\nend","preventionTips":["Never assume feed numeric fields are raw; strip separators/symbols at the boundary","Log the offending value, not just the error class, when wrapping parse failures","Add fixtures with \"1,234.56\", \"$10.00\", \"\" to import tests","Pin down API payloads in cassettes so upstream format changes surface as diff, not runtime crash"],"tags":["ruby","akahu","bigdecimal","data-import","transaction-parsing"],"backgroundTag":"decimal-parse-failure","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}