mirror of
https://github.com/we-promise/sure.git
synced 2026-04-17 02:54:10 +00:00
* [FEATURE] Add CoinStats exchange portfolios and normalize linked investment charts * [BUGFIX] Fix CoinStats PR regressions * [BUGFIX] Fix CoinStats PR review findings * [BUGFIX] Address follow-up CoinStats PR feedback * [REFACTO] Extract CoinStats exchange account helpers * [BUGFIX] Batch linked CoinStats chart normalization * [BUGFIX] Fix CoinStats processor lint --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
336 lines
10 KiB
Ruby
336 lines
10 KiB
Ruby
require "test_helper"
|
|
|
|
class CoinstatsAccountTest < ActiveSupport::TestCase
|
|
setup do
|
|
@family = families(:dylan_family)
|
|
@coinstats_item = CoinstatsItem.create!(
|
|
family: @family,
|
|
name: "Test CoinStats Connection",
|
|
api_key: "test_api_key_123"
|
|
)
|
|
@coinstats_account = @coinstats_item.coinstats_accounts.create!(
|
|
name: "Test Wallet",
|
|
currency: "USD",
|
|
current_balance: 1000.00
|
|
)
|
|
end
|
|
|
|
test "belongs to coinstats_item" do
|
|
assert_equal @coinstats_item, @coinstats_account.coinstats_item
|
|
end
|
|
|
|
test "can have account through account_provider" do
|
|
crypto = Crypto.create!
|
|
account = @family.accounts.create!(
|
|
accountable: crypto,
|
|
name: "Linked Crypto Account",
|
|
balance: 1000,
|
|
currency: "USD"
|
|
)
|
|
AccountProvider.create!(account: account, provider: @coinstats_account)
|
|
|
|
assert_equal account, @coinstats_account.account
|
|
assert_equal account, @coinstats_account.current_account
|
|
end
|
|
|
|
test "requires name to be present" do
|
|
coinstats_account = @coinstats_item.coinstats_accounts.build(
|
|
currency: "USD"
|
|
)
|
|
coinstats_account.name = nil
|
|
|
|
assert_not coinstats_account.valid?
|
|
assert_includes coinstats_account.errors[:name], "can't be blank"
|
|
end
|
|
|
|
test "requires currency to be present" do
|
|
coinstats_account = @coinstats_item.coinstats_accounts.build(
|
|
name: "Test"
|
|
)
|
|
coinstats_account.currency = nil
|
|
|
|
assert_not coinstats_account.valid?
|
|
assert_includes coinstats_account.errors[:currency], "can't be blank"
|
|
end
|
|
|
|
test "account_id is unique per coinstats_item" do
|
|
@coinstats_account.update!(account_id: "unique_account_id_123")
|
|
|
|
duplicate = @coinstats_item.coinstats_accounts.build(
|
|
name: "Duplicate",
|
|
currency: "USD",
|
|
account_id: "unique_account_id_123"
|
|
)
|
|
|
|
assert_not duplicate.valid?
|
|
assert_includes duplicate.errors[:account_id], "has already been taken"
|
|
end
|
|
|
|
test "allows nil account_id for multiple accounts" do
|
|
second_account = @coinstats_item.coinstats_accounts.build(
|
|
name: "Second Account",
|
|
currency: "USD",
|
|
account_id: nil
|
|
)
|
|
|
|
assert second_account.valid?
|
|
end
|
|
|
|
test "upsert_coinstats_snapshot updates balance and metadata" do
|
|
snapshot = {
|
|
balance: 2500.50,
|
|
currency: "USD",
|
|
name: "Updated Wallet Name",
|
|
status: "active",
|
|
provider: "coinstats",
|
|
institution_logo: "https://example.com/logo.png"
|
|
}
|
|
|
|
@coinstats_account.upsert_coinstats_snapshot!(snapshot)
|
|
@coinstats_account.reload
|
|
|
|
assert_equal BigDecimal("2500.50"), @coinstats_account.current_balance
|
|
assert_equal "USD", @coinstats_account.currency
|
|
assert_equal "Updated Wallet Name", @coinstats_account.name
|
|
assert_equal "active", @coinstats_account.account_status
|
|
assert_equal "coinstats", @coinstats_account.provider
|
|
assert_equal({ "logo" => "https://example.com/logo.png" }, @coinstats_account.institution_metadata)
|
|
assert_equal snapshot.stringify_keys, @coinstats_account.raw_payload
|
|
end
|
|
|
|
test "upsert_coinstats_snapshot handles symbol keys" do
|
|
snapshot = {
|
|
balance: 3000.0,
|
|
currency: "USD",
|
|
name: "Symbol Key Wallet"
|
|
}
|
|
|
|
@coinstats_account.upsert_coinstats_snapshot!(snapshot)
|
|
@coinstats_account.reload
|
|
|
|
assert_equal BigDecimal("3000.0"), @coinstats_account.current_balance
|
|
assert_equal "Symbol Key Wallet", @coinstats_account.name
|
|
end
|
|
|
|
test "upsert_coinstats_snapshot handles string keys" do
|
|
snapshot = {
|
|
"balance" => 3500.0,
|
|
"currency" => "USD",
|
|
"name" => "String Key Wallet"
|
|
}
|
|
|
|
@coinstats_account.upsert_coinstats_snapshot!(snapshot)
|
|
@coinstats_account.reload
|
|
|
|
assert_equal BigDecimal("3500.0"), @coinstats_account.current_balance
|
|
assert_equal "String Key Wallet", @coinstats_account.name
|
|
end
|
|
|
|
test "upsert_coinstats_snapshot sets account_id from id if not already set" do
|
|
@coinstats_account.update!(account_id: nil)
|
|
|
|
snapshot = {
|
|
id: "new_token_id_123",
|
|
balance: 1000.0,
|
|
currency: "USD",
|
|
name: "Test"
|
|
}
|
|
|
|
@coinstats_account.upsert_coinstats_snapshot!(snapshot)
|
|
@coinstats_account.reload
|
|
|
|
assert_equal "new_token_id_123", @coinstats_account.account_id
|
|
end
|
|
|
|
test "upsert_coinstats_snapshot preserves existing account_id" do
|
|
@coinstats_account.update!(account_id: "existing_id")
|
|
|
|
snapshot = {
|
|
id: "different_id",
|
|
balance: 1000.0,
|
|
currency: "USD",
|
|
name: "Test"
|
|
}
|
|
|
|
@coinstats_account.upsert_coinstats_snapshot!(snapshot)
|
|
@coinstats_account.reload
|
|
|
|
assert_equal "existing_id", @coinstats_account.account_id
|
|
end
|
|
|
|
test "upsert_coinstats_transactions_snapshot stores transactions array" do
|
|
transactions = [
|
|
{ type: "Received", date: "2025-01-01T10:00:00.000Z", hash: { id: "0xabc" } },
|
|
{ type: "Sent", date: "2025-01-02T11:00:00.000Z", hash: { id: "0xdef" } }
|
|
]
|
|
|
|
@coinstats_account.upsert_coinstats_transactions_snapshot!(transactions)
|
|
@coinstats_account.reload
|
|
|
|
assert_equal 2, @coinstats_account.raw_transactions_payload.count
|
|
# Keys may be strings after DB round-trip
|
|
first_tx = @coinstats_account.raw_transactions_payload.first
|
|
assert_equal "0xabc", first_tx.dig("hash", "id") || first_tx.dig(:hash, :id)
|
|
end
|
|
|
|
test "upsert_coinstats_transactions_snapshot extracts result from hash response" do
|
|
response = {
|
|
meta: { page: 1, limit: 100 },
|
|
result: [
|
|
{ type: "Received", date: "2025-01-01T10:00:00.000Z", hash: { id: "0xabc" } }
|
|
]
|
|
}
|
|
|
|
@coinstats_account.upsert_coinstats_transactions_snapshot!(response)
|
|
@coinstats_account.reload
|
|
|
|
assert_equal 1, @coinstats_account.raw_transactions_payload.count
|
|
assert_equal "0xabc", @coinstats_account.raw_transactions_payload.first["hash"]["id"].to_s
|
|
end
|
|
|
|
test "upsert_coinstats_transactions_snapshot handles empty result" do
|
|
response = {
|
|
meta: { page: 1, limit: 100 },
|
|
result: []
|
|
}
|
|
|
|
@coinstats_account.upsert_coinstats_transactions_snapshot!(response)
|
|
@coinstats_account.reload
|
|
|
|
assert_equal [], @coinstats_account.raw_transactions_payload
|
|
end
|
|
|
|
# Multi-wallet tests
|
|
test "account_id is unique per coinstats_item and wallet_address" do
|
|
@coinstats_account.update!(
|
|
account_id: "ethereum",
|
|
wallet_address: "0xAAA123"
|
|
)
|
|
|
|
duplicate = @coinstats_item.coinstats_accounts.build(
|
|
name: "Duplicate Ethereum",
|
|
currency: "USD",
|
|
account_id: "ethereum",
|
|
wallet_address: "0xAAA123"
|
|
)
|
|
|
|
assert_not duplicate.valid?
|
|
assert_includes duplicate.errors[:account_id], "has already been taken"
|
|
end
|
|
|
|
test "allows same account_id with different wallet_address" do
|
|
@coinstats_account.update!(
|
|
account_id: "ethereum",
|
|
wallet_address: "0xAAA123"
|
|
)
|
|
|
|
different_wallet = @coinstats_item.coinstats_accounts.build(
|
|
name: "Different Ethereum Wallet",
|
|
currency: "USD",
|
|
account_id: "ethereum",
|
|
wallet_address: "0xBBB456"
|
|
)
|
|
|
|
assert different_wallet.valid?
|
|
end
|
|
|
|
test "allows multiple accounts with nil wallet_address for backwards compatibility" do
|
|
first_account = @coinstats_item.coinstats_accounts.create!(
|
|
name: "Legacy Account 1",
|
|
currency: "USD",
|
|
account_id: "bitcoin",
|
|
wallet_address: nil
|
|
)
|
|
|
|
second_account = @coinstats_item.coinstats_accounts.build(
|
|
name: "Legacy Account 2",
|
|
currency: "USD",
|
|
account_id: "ethereum",
|
|
wallet_address: nil
|
|
)
|
|
|
|
assert second_account.valid?
|
|
assert second_account.save
|
|
end
|
|
|
|
test "deleting one wallet does not affect other wallets with same token but different address" do
|
|
# Create two wallets with the same token (ethereum) but different addresses
|
|
wallet_a = @coinstats_item.coinstats_accounts.create!(
|
|
name: "Ethereum Wallet A",
|
|
currency: "USD",
|
|
account_id: "ethereum",
|
|
wallet_address: "0xAAA111",
|
|
current_balance: 1000.00
|
|
)
|
|
|
|
wallet_b = @coinstats_item.coinstats_accounts.create!(
|
|
name: "Ethereum Wallet B",
|
|
currency: "USD",
|
|
account_id: "ethereum",
|
|
wallet_address: "0xBBB222",
|
|
current_balance: 2000.00
|
|
)
|
|
|
|
# Verify both wallets exist
|
|
assert_equal 3, @coinstats_item.coinstats_accounts.count # includes @coinstats_account from setup
|
|
|
|
# Delete wallet A
|
|
wallet_a.destroy!
|
|
|
|
# Verify only wallet A was deleted, wallet B still exists
|
|
@coinstats_item.reload
|
|
assert_equal 2, @coinstats_item.coinstats_accounts.count
|
|
|
|
# Verify wallet B is still intact with correct data
|
|
wallet_b.reload
|
|
assert_equal "Ethereum Wallet B", wallet_b.name
|
|
assert_equal "0xBBB222", wallet_b.wallet_address
|
|
assert_equal BigDecimal("2000.00"), wallet_b.current_balance
|
|
|
|
# Verify wallet A no longer exists
|
|
assert_nil CoinstatsAccount.find_by(id: wallet_a.id)
|
|
end
|
|
|
|
test "portfolio exchange account derives total and cash balances from embedded coins" do
|
|
@family.update!(currency: "EUR")
|
|
|
|
portfolio_account = @coinstats_item.coinstats_accounts.create!(
|
|
name: "Bitvavo",
|
|
currency: "EUR",
|
|
account_id: "exchange_portfolio:test",
|
|
wallet_address: "portfolio-test",
|
|
raw_payload: {
|
|
source: "exchange",
|
|
portfolio_account: true,
|
|
portfolio_id: "portfolio-test",
|
|
exchange_name: "Bitvavo",
|
|
coins: [
|
|
{
|
|
coin: { identifier: "bitcoin", symbol: "BTC", name: "Bitcoin" },
|
|
count: "0.00335845",
|
|
price: { EUR: "57950.0491" }
|
|
},
|
|
{
|
|
coin: { identifier: "ethereum", symbol: "ETH", name: "Ethereum" },
|
|
count: "0.05580825",
|
|
price: { EUR: "1728.952252246" }
|
|
},
|
|
{
|
|
coin: { identifier: "FiatCoin:eur", symbol: "EUR", name: "Euro", isFiat: true },
|
|
count: "2.58",
|
|
price: { EUR: "1" }
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
assert portfolio_account.exchange_portfolio_account?
|
|
refute portfolio_account.fiat_asset?
|
|
assert_equal "EUR", portfolio_account.inferred_currency
|
|
assert_in_delta 293.69214193130284, portfolio_account.inferred_current_balance.to_f, 0.0001
|
|
assert_in_delta 2.58, portfolio_account.inferred_cash_balance.to_f, 0.0001
|
|
assert_equal 2, portfolio_account.portfolio_non_fiat_coins.size
|
|
assert_equal 1, portfolio_account.portfolio_fiat_coins.size
|
|
end
|
|
end
|