mirror of
https://github.com/we-promise/sure.git
synced 2026-05-24 21:14:56 +00:00
* fix(api): include disabled-account transaction history * fix(api): hide pending deletion transaction history
808 lines
26 KiB
Ruby
808 lines
26 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
class Api::V1::TransactionsControllerTest < ActionDispatch::IntegrationTest
|
|
setup do
|
|
@user = users(:family_admin)
|
|
@family = @user.family
|
|
@account = @family.accounts.first
|
|
@transaction = @family.transactions.first
|
|
|
|
# Destroy existing active API keys to avoid validation errors
|
|
@user.api_keys.active.destroy_all
|
|
|
|
# Create fresh API keys instead of using fixtures to avoid parallel test conflicts (rate limiting in test)
|
|
@api_key = ApiKey.create!(
|
|
user: @user,
|
|
name: "Test Read-Write Key",
|
|
scopes: [ "read_write" ],
|
|
display_key: "test_rw_#{SecureRandom.hex(8)}"
|
|
)
|
|
|
|
@read_only_api_key = ApiKey.create!(
|
|
user: @user,
|
|
name: "Test Read-Only Key",
|
|
scopes: [ "read" ],
|
|
display_key: "test_ro_#{SecureRandom.hex(8)}",
|
|
source: "mobile" # Use different source to allow multiple keys
|
|
)
|
|
|
|
# Clear any existing rate limit data
|
|
Redis.new.del("api_rate_limit:#{@api_key.id}")
|
|
Redis.new.del("api_rate_limit:#{@read_only_api_key.id}")
|
|
end
|
|
|
|
# INDEX action tests
|
|
test "should get index with valid API key" do
|
|
get api_v1_transactions_url, headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
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")
|
|
assert response_data["pagination"].key?("total_pages")
|
|
end
|
|
|
|
test "should get index with read-only API key" do
|
|
get api_v1_transactions_url, headers: api_headers(@read_only_api_key)
|
|
assert_response :success
|
|
end
|
|
|
|
test "should filter transactions by account_id" do
|
|
get api_v1_transactions_url, params: { account_id: @account.id }, headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
response_data["transactions"].each do |transaction|
|
|
assert_equal @account.id, transaction["account"]["id"]
|
|
end
|
|
end
|
|
|
|
test "should include disabled account transactions in index history" do
|
|
disabled_transaction = create_disabled_account_transaction(name: "Closed Account Grocery")
|
|
|
|
get api_v1_transactions_url, headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
transaction_ids = response_data["transactions"].map { |transaction| transaction["id"] }
|
|
assert_includes transaction_ids, disabled_transaction.id
|
|
end
|
|
|
|
test "should exclude pending deletion account transactions from index history" do
|
|
pending_deletion_transaction = create_account_transaction(
|
|
status: "pending_deletion",
|
|
name: "Pending Delete Account Grocery"
|
|
)
|
|
|
|
get api_v1_transactions_url, headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
transaction_ids = response_data["transactions"].map { |transaction| transaction["id"] }
|
|
assert_not_includes transaction_ids, pending_deletion_transaction.id
|
|
end
|
|
|
|
test "should filter disabled account transactions by account_id" do
|
|
disabled_transaction = create_disabled_account_transaction(name: "Closed Account Filter")
|
|
disabled_account = disabled_transaction.entry.account
|
|
|
|
get api_v1_transactions_url,
|
|
params: { account_id: disabled_account.id },
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal [ disabled_transaction.id ], response_data["transactions"].map { |transaction| transaction["id"] }
|
|
end
|
|
|
|
test "should filter transactions by date range" do
|
|
start_date = 1.month.ago.to_date
|
|
end_date = Date.current
|
|
|
|
get api_v1_transactions_url,
|
|
params: { start_date: start_date, end_date: end_date },
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
response_data["transactions"].each do |transaction|
|
|
transaction_date = Date.parse(transaction["date"])
|
|
assert transaction_date >= start_date
|
|
assert transaction_date <= end_date
|
|
end
|
|
end
|
|
|
|
test "should filter disabled account transactions by date range" do
|
|
disabled_transaction = create_disabled_account_transaction(
|
|
name: "Closed Account Date Range",
|
|
date: Date.current - 3.days
|
|
)
|
|
|
|
get api_v1_transactions_url,
|
|
params: { start_date: Date.current - 4.days, end_date: Date.current - 2.days },
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
transaction_ids = response_data["transactions"].map { |transaction| transaction["id"] }
|
|
assert_includes transaction_ids, disabled_transaction.id
|
|
end
|
|
|
|
test "should search transactions" do
|
|
# Create a transaction with a specific name for testing
|
|
entry = @account.entries.create!(
|
|
name: "Test Coffee Purchase",
|
|
amount: 5.50,
|
|
currency: "USD",
|
|
date: Date.current,
|
|
entryable: Transaction.new
|
|
)
|
|
|
|
get api_v1_transactions_url,
|
|
params: { search: "Coffee" },
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
found_transaction = response_data["transactions"].find { |t| t["id"] == entry.transaction.id }
|
|
assert_not_nil found_transaction, "Should find the coffee transaction"
|
|
end
|
|
|
|
test "should search disabled account transactions" do
|
|
disabled_transaction = create_disabled_account_transaction(name: "Closed Account Coffee")
|
|
|
|
get api_v1_transactions_url,
|
|
params: { search: "Closed Account Coffee" },
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
found_transaction = response_data["transactions"].find { |transaction| transaction["id"] == disabled_transaction.id }
|
|
assert_not_nil found_transaction, "Should find disabled account transactions in global history search"
|
|
end
|
|
|
|
test "should paginate transactions" do
|
|
get api_v1_transactions_url,
|
|
params: { page: 1, per_page: 5 },
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert response_data["transactions"].size <= 5
|
|
assert_equal 1, response_data["pagination"]["page"]
|
|
assert_equal 5, response_data["pagination"]["per_page"]
|
|
end
|
|
|
|
test "should reject index request without API key" do
|
|
get api_v1_transactions_url
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
test "should reject index request with invalid API key" do
|
|
get api_v1_transactions_url, headers: { "X-Api-Key" => "invalid-key" }
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
# SHOW action tests
|
|
test "should show transaction with valid API key" do
|
|
get api_v1_transaction_url(@transaction), headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
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
|
|
|
|
test "should show transaction with read-only API key" do
|
|
get api_v1_transaction_url(@transaction), headers: api_headers(@read_only_api_key)
|
|
assert_response :success
|
|
end
|
|
|
|
test "should show disabled account transaction" do
|
|
disabled_transaction = create_disabled_account_transaction(name: "Closed Account Show")
|
|
|
|
get api_v1_transaction_url(disabled_transaction), headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal disabled_transaction.id, response_data["id"]
|
|
assert_equal disabled_transaction.entry.account_id, response_data["account"]["id"]
|
|
end
|
|
|
|
test "should return 404 for valid missing transaction id" do
|
|
get api_v1_transaction_url(SecureRandom.uuid), headers: api_headers(@api_key)
|
|
assert_response :not_found
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "not_found", response_data["error"]
|
|
assert_equal "Transaction not found", response_data["message"]
|
|
end
|
|
|
|
test "should return 404 for malformed id" do
|
|
get api_v1_transaction_url(999999), headers: api_headers(@api_key)
|
|
assert_response :not_found
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "not_found", response_data["error"]
|
|
assert_equal "Transaction not found", response_data["message"]
|
|
end
|
|
|
|
test "should reject show request without API key" do
|
|
get api_v1_transaction_url(@transaction)
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
# CREATE action tests
|
|
test "should create transaction with valid parameters" do
|
|
transaction_params = {
|
|
transaction: {
|
|
account_id: @account.id,
|
|
name: "Test Transaction",
|
|
amount: 25.00,
|
|
date: Date.current,
|
|
currency: "USD",
|
|
nature: "expense"
|
|
}
|
|
}
|
|
|
|
assert_difference("@account.entries.count", 1) do
|
|
post api_v1_transactions_url,
|
|
params: transaction_params,
|
|
headers: api_headers(@api_key)
|
|
end
|
|
|
|
assert_response :created
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "Test Transaction", response_data["name"]
|
|
assert_equal @account.id, response_data["account"]["id"]
|
|
end
|
|
|
|
test "should create transaction with external idempotency key" do
|
|
transaction_params = {
|
|
transaction: {
|
|
account_id: @account.id,
|
|
name: "Imported Transaction",
|
|
amount: 25.00,
|
|
date: Date.current,
|
|
currency: "USD",
|
|
nature: "expense",
|
|
external_id: "import-txn-1",
|
|
source: "external_import"
|
|
}
|
|
}
|
|
|
|
assert_difference("@account.entries.count", 1) do
|
|
post api_v1_transactions_url,
|
|
params: transaction_params,
|
|
headers: api_headers(@api_key)
|
|
end
|
|
|
|
assert_response :created
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "import-txn-1", response_data["external_id"]
|
|
assert_equal "external_import", response_data["source"]
|
|
|
|
entry = @account.entries.find_by!(external_id: "import-txn-1", source: "external_import")
|
|
assert_equal response_data["id"], entry.transaction.id
|
|
end
|
|
|
|
test "should use default source when external_id provided without source" do
|
|
transaction_params = {
|
|
transaction: {
|
|
account_id: @account.id,
|
|
name: "Imported Transaction",
|
|
amount: 25.00,
|
|
date: Date.current,
|
|
currency: "USD",
|
|
nature: "expense",
|
|
external_id: "default-source-test"
|
|
}
|
|
}
|
|
|
|
assert_difference("@account.entries.count", 1) do
|
|
post api_v1_transactions_url,
|
|
params: transaction_params,
|
|
headers: api_headers(@api_key)
|
|
end
|
|
|
|
assert_response :created
|
|
response_data = JSON.parse(response.body)
|
|
entry = @account.entries.find_by!(external_id: "default-source-test")
|
|
assert_equal "api", entry.source
|
|
assert_equal "api", response_data["source"]
|
|
|
|
assert_no_difference("@account.entries.count") do
|
|
post api_v1_transactions_url,
|
|
params: transaction_params.deep_merge(transaction: { name: "Changed Name" }),
|
|
headers: api_headers(@api_key)
|
|
end
|
|
|
|
assert_response :ok
|
|
end
|
|
|
|
test "should reject source without external idempotency key" do
|
|
transaction_params = {
|
|
transaction: {
|
|
account_id: @account.id,
|
|
name: "Imported Transaction",
|
|
amount: 25.00,
|
|
date: Date.current,
|
|
currency: "USD",
|
|
nature: "expense",
|
|
source: "external_import"
|
|
}
|
|
}
|
|
|
|
assert_no_difference("@account.entries.count") do
|
|
post api_v1_transactions_url,
|
|
params: transaction_params,
|
|
headers: api_headers(@api_key)
|
|
end
|
|
|
|
assert_response :unprocessable_entity
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "validation_failed", response_data["error"]
|
|
assert_equal "Source requires external_id", response_data["message"]
|
|
assert_equal [ "Source requires external_id" ], response_data["errors"]
|
|
end
|
|
|
|
test "should return existing transaction for duplicate external idempotency key" do
|
|
transaction_params = {
|
|
transaction: {
|
|
account_id: @account.id,
|
|
name: "Imported Transaction",
|
|
amount: 25.00,
|
|
date: Date.current,
|
|
currency: "USD",
|
|
nature: "expense",
|
|
external_id: "import-txn-2",
|
|
source: "external_import"
|
|
}
|
|
}
|
|
|
|
post api_v1_transactions_url,
|
|
params: transaction_params,
|
|
headers: api_headers(@api_key)
|
|
assert_response :created
|
|
created_data = JSON.parse(response.body)
|
|
|
|
assert_no_difference("@account.entries.count") do
|
|
post api_v1_transactions_url,
|
|
params: transaction_params.deep_merge(transaction: { name: "Changed Name" }),
|
|
headers: api_headers(@api_key)
|
|
end
|
|
|
|
assert_response :ok
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal created_data["id"], response_data["id"]
|
|
assert_equal "Imported Transaction", response_data["name"]
|
|
end
|
|
|
|
test "should scope external idempotency keys to account" do
|
|
other_account = @family.accounts.create!(
|
|
name: "Other API Account",
|
|
accountable: Depository.new,
|
|
balance: 0,
|
|
currency: "USD"
|
|
)
|
|
transaction_params = {
|
|
transaction: {
|
|
name: "Imported Transaction",
|
|
amount: 25.00,
|
|
date: Date.current,
|
|
currency: "USD",
|
|
nature: "expense",
|
|
external_id: "shared-import-txn",
|
|
source: "external_import"
|
|
}
|
|
}
|
|
|
|
assert_difference("Entry.count", 2) do
|
|
post api_v1_transactions_url,
|
|
params: transaction_params.deep_merge(transaction: { account_id: @account.id }),
|
|
headers: api_headers(@api_key)
|
|
assert_response :created
|
|
|
|
post api_v1_transactions_url,
|
|
params: transaction_params.deep_merge(transaction: { account_id: other_account.id }),
|
|
headers: api_headers(@api_key)
|
|
assert_response :created
|
|
end
|
|
end
|
|
|
|
test "should scope external idempotency keys to source" do
|
|
transaction_params = {
|
|
transaction: {
|
|
account_id: @account.id,
|
|
name: "Imported Transaction",
|
|
amount: 25.00,
|
|
date: Date.current,
|
|
currency: "USD",
|
|
nature: "expense",
|
|
external_id: "shared-source-txn",
|
|
source: "external_import"
|
|
}
|
|
}
|
|
|
|
assert_difference("Entry.count", 2) do
|
|
post api_v1_transactions_url,
|
|
params: transaction_params,
|
|
headers: api_headers(@api_key)
|
|
assert_response :created
|
|
|
|
post api_v1_transactions_url,
|
|
params: transaction_params.deep_merge(transaction: { source: "other_import" }),
|
|
headers: api_headers(@api_key)
|
|
assert_response :created
|
|
end
|
|
|
|
@account.entries.find_by!(external_id: "shared-source-txn", source: "external_import")
|
|
@account.entries.find_by!(external_id: "shared-source-txn", source: "other_import")
|
|
end
|
|
|
|
test "should reject external idempotency key collision with non-transaction entry" do
|
|
@account.entries.create!(
|
|
name: "Existing valuation",
|
|
amount: 100,
|
|
currency: "USD",
|
|
date: Date.current,
|
|
external_id: "import-non-transaction",
|
|
source: "external_import",
|
|
entryable: Valuation.new
|
|
)
|
|
|
|
post api_v1_transactions_url,
|
|
params: {
|
|
transaction: {
|
|
account_id: @account.id,
|
|
name: "Imported Transaction",
|
|
amount: 25.00,
|
|
date: Date.current - 1.day,
|
|
currency: "USD",
|
|
nature: "expense",
|
|
external_id: "import-non-transaction",
|
|
source: "external_import"
|
|
}
|
|
},
|
|
headers: api_headers(@api_key)
|
|
|
|
assert_response :unprocessable_entity
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "validation_failed", response_data["error"]
|
|
end
|
|
|
|
test "should reject create with read-only API key" do
|
|
transaction_params = {
|
|
transaction: {
|
|
account_id: @account.id,
|
|
name: "Test Transaction",
|
|
amount: 25.00,
|
|
date: Date.current
|
|
}
|
|
}
|
|
|
|
post api_v1_transactions_url,
|
|
params: transaction_params,
|
|
headers: api_headers(@read_only_api_key)
|
|
assert_response :forbidden
|
|
end
|
|
|
|
test "should reject create with invalid parameters" do
|
|
transaction_params = {
|
|
transaction: {
|
|
# Missing required fields
|
|
name: "Test Transaction"
|
|
}
|
|
}
|
|
|
|
post api_v1_transactions_url,
|
|
params: transaction_params,
|
|
headers: api_headers(@api_key)
|
|
assert_response :unprocessable_entity
|
|
end
|
|
|
|
test "should reject invalid date on create" do
|
|
transaction_params = {
|
|
transaction: {
|
|
account_id: @account.id,
|
|
name: "Invalid Date Transaction",
|
|
amount: 25.00,
|
|
date: "not-a-date",
|
|
currency: "USD",
|
|
nature: "expense"
|
|
}
|
|
}
|
|
|
|
assert_no_difference("@account.entries.count") do
|
|
post api_v1_transactions_url,
|
|
params: transaction_params,
|
|
headers: api_headers(@api_key)
|
|
end
|
|
|
|
assert_response :unprocessable_entity
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "validation_failed", response_data["error"]
|
|
assert_equal "Transaction could not be created", response_data["message"]
|
|
assert response_data["errors"].any? { |error| error.match?(/Date/) }
|
|
end
|
|
|
|
test "should reject create without API key" do
|
|
post api_v1_transactions_url, params: { transaction: { name: "Test" } }
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
# UPDATE action tests
|
|
test "should update transaction with valid parameters" do
|
|
update_params = {
|
|
transaction: {
|
|
name: "Updated Transaction Name",
|
|
amount: 30.00
|
|
}
|
|
}
|
|
|
|
put api_v1_transaction_url(@transaction),
|
|
params: update_params,
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
response_data = JSON.parse(response.body)
|
|
assert_equal "Updated Transaction Name", response_data["name"]
|
|
end
|
|
|
|
test "should reject update with read-only API key" do
|
|
update_params = {
|
|
transaction: {
|
|
name: "Updated Transaction Name"
|
|
}
|
|
}
|
|
|
|
put api_v1_transaction_url(@transaction),
|
|
params: update_params,
|
|
headers: api_headers(@read_only_api_key)
|
|
assert_response :forbidden
|
|
end
|
|
|
|
test "should reject update for non-existent transaction" do
|
|
put api_v1_transaction_url(999999),
|
|
params: { transaction: { name: "Test" } },
|
|
headers: api_headers(@api_key)
|
|
assert_response :not_found
|
|
end
|
|
|
|
test "should reject update without API key" do
|
|
put api_v1_transaction_url(@transaction), params: { transaction: { name: "Test" } }
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
test "should preserve tags when tag_ids not provided in update" do
|
|
# Set up transaction with existing tags
|
|
original_tags = [ Tag.first, Tag.second ]
|
|
@transaction.tags = original_tags
|
|
@transaction.save!
|
|
|
|
# Update only the name, without providing tag_ids
|
|
update_params = {
|
|
transaction: {
|
|
name: "Updated Name Only"
|
|
}
|
|
}
|
|
|
|
put api_v1_transaction_url(@transaction),
|
|
params: update_params,
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
@transaction.reload
|
|
assert_equal "Updated Name Only", @transaction.entry.name
|
|
# Tags should be preserved since tag_ids was not in the request
|
|
assert_equal original_tags.map(&:id).sort, @transaction.tag_ids.sort
|
|
end
|
|
|
|
test "should clear tags when empty tag_ids explicitly provided in update" do
|
|
# Set up transaction with existing tags
|
|
@transaction.tags = [ Tag.first, Tag.second ]
|
|
@transaction.save!
|
|
|
|
# Explicitly provide empty tag_ids to clear tags
|
|
update_params = {
|
|
transaction: {
|
|
name: "Updated Name",
|
|
tag_ids: []
|
|
}
|
|
}
|
|
|
|
put api_v1_transaction_url(@transaction),
|
|
params: update_params,
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
@transaction.reload
|
|
# Tags should be cleared since tag_ids was explicitly provided as empty
|
|
assert_empty @transaction.tags
|
|
end
|
|
|
|
test "should update tags when tag_ids explicitly provided in update" do
|
|
# Set up transaction with one tag
|
|
@transaction.tags = [ Tag.first ]
|
|
@transaction.save!
|
|
|
|
new_tags = [ Tag.second ]
|
|
|
|
update_params = {
|
|
transaction: {
|
|
tag_ids: new_tags.map(&:id)
|
|
}
|
|
}
|
|
|
|
put api_v1_transaction_url(@transaction),
|
|
params: update_params,
|
|
headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
@transaction.reload
|
|
assert_equal new_tags.map(&:id), @transaction.tag_ids
|
|
end
|
|
|
|
# DESTROY action tests
|
|
test "should destroy transaction" do
|
|
entry_to_delete = @account.entries.create!(
|
|
name: "Transaction to Delete",
|
|
amount: 10.00,
|
|
currency: "USD",
|
|
date: Date.current,
|
|
entryable: Transaction.new
|
|
)
|
|
transaction_to_delete = entry_to_delete.transaction
|
|
|
|
assert_difference("@account.entries.count", -1) do
|
|
delete api_v1_transaction_url(transaction_to_delete), headers: api_headers(@api_key)
|
|
end
|
|
|
|
assert_response :success
|
|
response_data = JSON.parse(response.body)
|
|
assert response_data.key?("message")
|
|
end
|
|
|
|
test "should reject destroy with read-only API key" do
|
|
delete api_v1_transaction_url(@transaction), headers: api_headers(@read_only_api_key)
|
|
assert_response :forbidden
|
|
end
|
|
|
|
test "should reject destroy for non-existent transaction" do
|
|
delete api_v1_transaction_url(999999), headers: api_headers(@api_key)
|
|
assert_response :not_found
|
|
end
|
|
|
|
test "should reject destroy without API key" do
|
|
delete api_v1_transaction_url(@transaction)
|
|
assert_response :unauthorized
|
|
end
|
|
|
|
# JSON structure tests
|
|
test "transaction JSON should have expected structure" do
|
|
get api_v1_transaction_url(@transaction), headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
transaction_data = JSON.parse(response.body)
|
|
|
|
# Basic fields
|
|
assert transaction_data.key?("id")
|
|
assert transaction_data.key?("date")
|
|
assert transaction_data.key?("amount")
|
|
assert transaction_data.key?("currency")
|
|
assert transaction_data.key?("name")
|
|
assert transaction_data.key?("classification")
|
|
assert transaction_data.key?("created_at")
|
|
assert transaction_data.key?("updated_at")
|
|
|
|
# Account information
|
|
assert transaction_data.key?("account")
|
|
assert transaction_data["account"].key?("id")
|
|
assert transaction_data["account"].key?("name")
|
|
assert transaction_data["account"].key?("account_type")
|
|
|
|
# Optional fields should be present (even if nil)
|
|
assert transaction_data.key?("category")
|
|
assert transaction_data.key?("merchant")
|
|
assert transaction_data.key?("tags")
|
|
assert transaction_data.key?("transfer")
|
|
assert transaction_data.key?("notes")
|
|
end
|
|
|
|
test "transactions with transfers should include transfer information" do
|
|
# Create a transfer between two accounts to test transfer rendering
|
|
from_account = @family.accounts.create!(
|
|
name: "Transfer From Account",
|
|
balance: 1000,
|
|
currency: "USD",
|
|
accountable: Depository.new
|
|
)
|
|
|
|
to_account = @family.accounts.create!(
|
|
name: "Transfer To Account",
|
|
balance: 0,
|
|
currency: "USD",
|
|
accountable: Depository.new
|
|
)
|
|
|
|
transfer = Transfer::Creator.new(
|
|
family: @family,
|
|
source_account_id: from_account.id,
|
|
destination_account_id: to_account.id,
|
|
date: Date.current,
|
|
amount: 100
|
|
).create
|
|
|
|
get api_v1_transaction_url(transfer.inflow_transaction), headers: api_headers(@api_key)
|
|
assert_response :success
|
|
|
|
transaction_data = JSON.parse(response.body)
|
|
assert_not_nil transaction_data["transfer"]
|
|
assert transaction_data["transfer"].key?("id")
|
|
assert transaction_data["transfer"].key?("amount")
|
|
assert transaction_data["transfer"].key?("currency")
|
|
assert transaction_data["transfer"].key?("other_account")
|
|
end
|
|
|
|
private
|
|
|
|
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
|
|
|
|
def create_disabled_account_transaction(name:, date: Date.current)
|
|
create_account_transaction(status: "disabled", name: name, date: date)
|
|
end
|
|
|
|
def create_account_transaction(status:, name:, date: Date.current)
|
|
account = @family.accounts.create!(
|
|
name: "#{status.titleize} Checking #{SecureRandom.hex(4)}",
|
|
balance: 0,
|
|
currency: "USD",
|
|
status: status,
|
|
accountable: Depository.new
|
|
)
|
|
|
|
entry = account.entries.create!(
|
|
name: name,
|
|
amount: 12.34,
|
|
currency: "USD",
|
|
date: date,
|
|
entryable: Transaction.new
|
|
)
|
|
|
|
entry.transaction
|
|
end
|
|
end
|