mirror of
https://github.com/we-promise/sure.git
synced 2026-07-27 12:12:13 +00:00
* Add support for trading212 integration in investments sync * Respect Trading 212 history rate limits * Replace hex colors with design tokens * Changed withdrawal to withdraw * Address PR review feedback for Trading 212 integration - Replace all Rails.logger calls with DebugLogEntry.capture across syncer, importer, processor, provider, controller, and unlinking - Fix destroy action to surface unlink errors via DebugLogEntry - Replace hardcoded 'GBP' currency fallback with Current.family.currency - Replace hardcoded English status strings in Syncer with i18n - Add config/initializers/trading212.rb with DEBUG_RAW ENV toggle - Add comprehensive test coverage: item, account, data_helpers, holdings_processor, activities_processor, importer, syncer, and controller tests - Rewrite syncer and controller tests to match actual interfaces * Fix tests and bugs found during local test execution - Fix standard_ticker: empty string caused nil.upcase via [].first - Fix parse_date: DateTime < Date, so when Date caught DateTime first - Fix ActivitiesProcessor/HoldingsProcessor: missing instruments_map in DataHelpers caused NameError when processing dividends - Fix test: security fixture ticker collision with test data - Fix test: sync_status_summary i18n matching in assertions - Fix test: trading212_provider ConfigurationError test path - Fix test: controller invalid params needs Turbo-Frame header - Fix test: syncer test uses stubs instead of strict expects 116 tests, 257 assertions, 0 failures, 0 errors * Keep raw provider response bodies out of exception messages. * Fix Secrets leak into page HTML, prevents the API key/secret from appearing in the HTML source while keeping the "leave blank to keep existing" UX. * Address review findings: env gate, sync test, uniqueness test, destroy flow - Gate TRADING212_DEBUG_RAW behind Rails.env.local? so staging/production cannot accidentally enable raw payload logging - Assert SyncJob enqueue in sync controller test, not just redirect - Fix cross-item uniqueness test to actually use two different items - Remove rescue in destroy so unlink failures stop the flow (matching Brex/Akahu pattern) instead of proceeding to destroy_later silently * Add Trading212 tables to db/schema.rb for CI test database CI runs db:test:prepare which loads db/schema.rb, not migrations. Without these table definitions, fixture loading fails with PG::UndefinedTable: relation "trading212_accounts" does not exist. * Removed lint complaint * Register Trading212 in ProviderConnectionStatus::PROVIDERS Fixes CI failure: test_provider_registry_covers_syncable_family_provider_item_associations * Fixed bad merge --------- Signed-off-by: jdcdp <47483528+jdcdp@users.noreply.github.com> Co-authored-by: jdcdp <jdcdp@cdm4.net>
209 lines
5.4 KiB
Ruby
209 lines
5.4 KiB
Ruby
require "test_helper"
|
|
|
|
class Trading212HoldingsProcessorTest < ActiveSupport::TestCase
|
|
setup do
|
|
@family = families(:dylan_family)
|
|
@trading212_item = trading212_items(:configured_item)
|
|
@trading212_account = trading212_accounts(:main_account)
|
|
|
|
# Link to an investment account
|
|
@account = @family.accounts.create!(
|
|
name: "Test T212 Investment",
|
|
balance: 0,
|
|
cash_balance: 0,
|
|
currency: "USD",
|
|
accountable: Investment.new
|
|
)
|
|
@trading212_account.ensure_account_provider!(@account)
|
|
@trading212_account.reload
|
|
end
|
|
|
|
# === process ===
|
|
|
|
test "processor creates holdings from raw positions payload" do
|
|
@trading212_account.update!(
|
|
raw_positions_payload: [
|
|
{
|
|
"instrument" => {
|
|
"ticker" => "AAPL_US_EQ",
|
|
"isin" => "US0378331005",
|
|
"name" => "Apple Inc.",
|
|
"currency" => "USD"
|
|
},
|
|
"quantity" => "50",
|
|
"currentPrice" => "175.00",
|
|
"averagePricePaid" => "150.00"
|
|
}
|
|
]
|
|
)
|
|
|
|
processor = Trading212Account::HoldingsProcessor.new(@trading212_account)
|
|
processor.process
|
|
|
|
security = Security.find_by(ticker: "AAPL")
|
|
assert_not_nil security
|
|
|
|
holding = @account.holdings.find_by(security: security)
|
|
assert_not_nil holding
|
|
assert_equal BigDecimal("50"), holding.qty
|
|
assert_equal BigDecimal("175.00"), holding.price
|
|
end
|
|
|
|
test "processor stores cost basis when available" do
|
|
@trading212_account.update!(
|
|
raw_positions_payload: [
|
|
{
|
|
"instrument" => {
|
|
"ticker" => "TSLA_US_EQ",
|
|
"name" => "Tesla Inc."
|
|
},
|
|
"quantity" => "25",
|
|
"currentPrice" => "250.00",
|
|
"averagePricePaid" => "200.00"
|
|
}
|
|
]
|
|
)
|
|
|
|
processor = Trading212Account::HoldingsProcessor.new(@trading212_account)
|
|
processor.process
|
|
|
|
security = Security.find_by(ticker: "TSLA")
|
|
holding = @account.holdings.find_by(security: security)
|
|
assert_not_nil holding
|
|
assert_equal BigDecimal("200.00"), holding.cost_basis
|
|
assert_equal "provider", holding.cost_basis_source
|
|
end
|
|
|
|
test "processor skips position with blank ticker" do
|
|
@trading212_account.update!(
|
|
raw_positions_payload: [
|
|
{
|
|
"instrument" => {
|
|
"ticker" => "",
|
|
"name" => "No Ticker"
|
|
},
|
|
"quantity" => "100",
|
|
"currentPrice" => "50.00"
|
|
}
|
|
]
|
|
)
|
|
|
|
processor = Trading212Account::HoldingsProcessor.new(@trading212_account)
|
|
|
|
assert_nothing_raised do
|
|
processor.process
|
|
end
|
|
|
|
assert_equal 0, @account.holdings.count
|
|
end
|
|
|
|
test "processor skips position with zero quantity" do
|
|
@trading212_account.update!(
|
|
raw_positions_payload: [
|
|
{
|
|
"instrument" => {
|
|
"ticker" => "MSFT_US_EQ",
|
|
"name" => "Microsoft"
|
|
},
|
|
"quantity" => "0",
|
|
"currentPrice" => "400.00"
|
|
}
|
|
]
|
|
)
|
|
|
|
processor = Trading212Account::HoldingsProcessor.new(@trading212_account)
|
|
processor.process
|
|
|
|
assert_equal 0, @account.holdings.count
|
|
end
|
|
|
|
test "processor skips position with nil price" do
|
|
@trading212_account.update!(
|
|
raw_positions_payload: [
|
|
{
|
|
"instrument" => {
|
|
"ticker" => "GOOGL_US_EQ",
|
|
"name" => "Alphabet"
|
|
},
|
|
"quantity" => "10",
|
|
"currentPrice" => nil
|
|
}
|
|
]
|
|
)
|
|
|
|
processor = Trading212Account::HoldingsProcessor.new(@trading212_account)
|
|
processor.process
|
|
|
|
assert_equal 0, @account.holdings.count
|
|
end
|
|
|
|
test "processor is idempotent - does not duplicate holdings" do
|
|
@trading212_account.update!(
|
|
raw_positions_payload: [
|
|
{
|
|
"instrument" => {
|
|
"ticker" => "NVDA_US_EQ",
|
|
"name" => "Nvidia"
|
|
},
|
|
"quantity" => "30",
|
|
"currentPrice" => "800.00",
|
|
"averagePricePaid" => "600.00"
|
|
}
|
|
]
|
|
)
|
|
|
|
processor = Trading212Account::HoldingsProcessor.new(@trading212_account)
|
|
2.times { processor.process }
|
|
|
|
security = Security.find_by(ticker: "NVDA")
|
|
assert_equal 1, @account.holdings.where(security: security).count
|
|
end
|
|
|
|
test "processor handles empty positions payload" do
|
|
@trading212_account.update!(raw_positions_payload: [])
|
|
|
|
processor = Trading212Account::HoldingsProcessor.new(@trading212_account)
|
|
processor.process
|
|
|
|
assert_equal 0, @account.holdings.count
|
|
end
|
|
|
|
test "processor returns early when no account linked" do
|
|
@trading212_account.account_provider.destroy!
|
|
|
|
@trading212_account.update!(
|
|
raw_positions_payload: [
|
|
{
|
|
"instrument" => { "ticker" => "AAPL_US_EQ", "name" => "Apple" },
|
|
"quantity" => "10",
|
|
"currentPrice" => "150.00"
|
|
}
|
|
]
|
|
)
|
|
|
|
processor = Trading212Account::HoldingsProcessor.new(@trading212_account)
|
|
processor.process
|
|
|
|
assert_equal 0, @account.holdings.count
|
|
end
|
|
|
|
test "processor gracefully handles individual position errors" do
|
|
# Position without instrument at all
|
|
@trading212_account.update!(
|
|
raw_positions_payload: [
|
|
{
|
|
# Missing instrument key entirely
|
|
"quantity" => "10",
|
|
"currentPrice" => "150.00"
|
|
}
|
|
]
|
|
)
|
|
|
|
processor = Trading212Account::HoldingsProcessor.new(@trading212_account)
|
|
|
|
assert_nothing_raised do
|
|
processor.process
|
|
end
|
|
end
|
|
end
|