Files
sure/app/models/trading212_account/holdings_processor.rb
jdcdp 849578a84a Add support for trading212 integration in investments sync (#2513)
* 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>
2026-07-25 04:54:30 +02:00

77 lines
2.4 KiB
Ruby

class Trading212Account::HoldingsProcessor
include Trading212Account::DataHelpers
def initialize(trading212_account)
@trading212_account = trading212_account
end
def process
return unless account.present?
Array(@trading212_account.raw_positions_payload).each do |position|
process_position(position.with_indifferent_access)
end
end
private
def account
@trading212_account.current_account
end
def import_adapter
@import_adapter ||= Account::ProviderImportAdapter.new(account)
end
def currency
@trading212_account.currency
end
def process_position(position)
instrument = (position[:instrument] || {}).with_indifferent_access
t212_ticker = instrument[:ticker].to_s
return if t212_ticker.blank?
isin = instrument[:isin].presence
ticker = standard_ticker(t212_ticker)
name = instrument[:name].presence || ticker
position_ccy = instrument[:currency].presence || currency
security = resolve_security_direct(isin, ticker, name)
return unless security
quantity = parse_decimal(position[:quantity])
price = parse_decimal(position[:currentPrice])
return unless quantity && price && quantity > 0
amount = quantity * price
date = Date.current
external_id = "trading212_position_#{@trading212_account.trading212_account_id}_#{t212_ticker}_#{date}"
import_adapter.import_holding(
security: security,
quantity: quantity,
amount: amount,
currency: position_ccy,
date: date,
price: price,
cost_basis: parse_decimal(position[:averagePricePaid]),
external_id: external_id,
source: "trading212",
account_provider_id: @trading212_account.account_provider&.id,
delete_future_holdings: false
)
rescue => e
DebugLogEntry.capture(
category: "sync",
level: "error",
message: "Trading212Account::HoldingsProcessor - Failed to process position #{t212_ticker}: #{e.message}",
source: "trading212",
family: @trading212_account.trading212_item.family,
provider_key: "trading212",
metadata: { ticker: t212_ticker, trading212_account_id: @trading212_account.id }
)
end
end