API v1: add amount_cents + signed_amount_cents to transactions (#899)

* feat(api): add amount_cents + signed_amount_cents to transactions

* fix: use currency.minor_unit_conversion for amount_cents

- Replace hardcoded *100 with currency.minor_unit_conversion
- Handles JPY (0 decimals), KWD/BHD (3 decimals), etc. correctly
- Add assert_amount_cents_fields helper to validate sign/scale invariants
This commit is contained in:
David Gil
2026-02-04 22:55:50 +01:00
committed by GitHub
parent fb9468d80b
commit b08285a10f
2 changed files with 34 additions and 0 deletions

View File

@@ -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

View File

@@ -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