diff --git a/app/views/api/v1/transactions/_transaction.json.jbuilder b/app/views/api/v1/transactions/_transaction.json.jbuilder index 2a1c9779e..617f47505 100644 --- a/app/views/api/v1/transactions/_transaction.json.jbuilder +++ b/app/views/api/v1/transactions/_transaction.json.jbuilder @@ -3,6 +3,17 @@ json.id transaction.id json.date transaction.entry.date json.amount transaction.entry.amount_money.format + +# Agent/automation-friendly numeric fields (avoid localized parsing and clarify sign) +# `amount` in v1 is a localized string and may follow an accounting sign convention. +# Expose minor units (cents) as integers to make the API agent-friendly. +# Uses currency.minor_unit_conversion (e.g. 100 for USD/EUR, 1 for JPY, 1000 for KWD). +amount_money = transaction.entry.amount_money +conversion_factor = amount_money.currency.minor_unit_conversion +amount_cents = (amount_money.amount * conversion_factor).round(0).to_i.abs +json.amount_cents amount_cents +json.signed_amount_cents(transaction.entry.classification == "income" ? amount_cents : -amount_cents) + json.currency transaction.entry.currency json.name transaction.entry.name json.notes transaction.entry.notes diff --git a/test/controllers/api/v1/transactions_controller_test.rb b/test/controllers/api/v1/transactions_controller_test.rb index b028562a6..9b681d4de 100644 --- a/test/controllers/api/v1/transactions_controller_test.rb +++ b/test/controllers/api/v1/transactions_controller_test.rb @@ -41,6 +41,10 @@ class Api::V1::TransactionsControllerTest < ActionDispatch::IntegrationTest response_data = JSON.parse(response.body) assert response_data.key?("transactions") assert response_data.key?("pagination") + + # Agent-friendly numeric fields (validate type + sign invariants) + first = response_data["transactions"].first + assert_amount_cents_fields(first) assert response_data["pagination"].key?("page") assert response_data["pagination"].key?("per_page") assert response_data["pagination"].key?("total_count") @@ -130,6 +134,7 @@ class Api::V1::TransactionsControllerTest < ActionDispatch::IntegrationTest assert_equal @transaction.id, response_data["id"] assert response_data.key?("name") assert response_data.key?("amount") + assert_amount_cents_fields(response_data) assert response_data.key?("date") assert response_data.key?("account") end @@ -358,4 +363,22 @@ end def api_headers(api_key) { "X-Api-Key" => api_key.display_key } end + + # Validates agent-friendly numeric fields: type, sign invariants + def assert_amount_cents_fields(txn_json) + assert txn_json.key?("amount_cents"), "Expected amount_cents field" + assert txn_json.key?("signed_amount_cents"), "Expected signed_amount_cents field" + assert_kind_of Integer, txn_json["amount_cents"] + assert_kind_of Integer, txn_json["signed_amount_cents"] + assert_operator txn_json["amount_cents"], :>=, 0, "amount_cents must be non-negative" + assert_equal txn_json["amount_cents"].abs, txn_json["signed_amount_cents"].abs, + "Absolute values of amount_cents and signed_amount_cents must match" + if txn_json["classification"] == "income" + assert_operator txn_json["signed_amount_cents"], :>=, 0, + "income transactions should have non-negative signed_amount_cents" + else + assert_operator txn_json["signed_amount_cents"], :<=, 0, + "non-income transactions should have non-positive signed_amount_cents" + end + end end