mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 16:42:18 +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>
148 lines
4.5 KiB
Ruby
148 lines
4.5 KiB
Ruby
class Trading212Item < ApplicationRecord
|
|
include Syncable, Provided, Unlinking, Encryptable
|
|
|
|
enum :status, { good: "good", requires_update: "requires_update" }, default: :good
|
|
enum :environment, { live: "live", demo: "demo" }, default: :live
|
|
|
|
if encryption_ready?
|
|
encrypts :api_key, deterministic: true
|
|
encrypts :api_secret
|
|
encrypts :raw_instruments_payload
|
|
end
|
|
|
|
belongs_to :family
|
|
has_many :trading212_accounts, dependent: :destroy
|
|
|
|
validates :api_key, presence: true, on: :create
|
|
validates :api_secret, presence: true, on: :create
|
|
|
|
scope :active, -> { where(scheduled_for_deletion: false) }
|
|
scope :syncable, -> { active.where.not(api_key: [ nil, "" ]) }
|
|
scope :ordered, -> { order(created_at: :desc) }
|
|
scope :needs_update, -> { where(status: :requires_update) }
|
|
|
|
def destroy_later
|
|
update!(scheduled_for_deletion: true)
|
|
DestroyJob.perform_later(self)
|
|
end
|
|
|
|
def credentials_configured?
|
|
api_key.present? && api_secret.present?
|
|
end
|
|
|
|
def import_latest_data
|
|
provider = trading212_provider
|
|
raise StandardError, "Trading 212 provider is not configured" unless provider
|
|
|
|
Trading212Item::Importer.new(self, provider: provider).import
|
|
rescue => e
|
|
DebugLogEntry.capture(
|
|
category: "sync",
|
|
level: "error",
|
|
message: "Trading212Item #{id} - Failed to import data: #{e.message}",
|
|
source: "trading212",
|
|
family: family,
|
|
provider_key: "trading212"
|
|
)
|
|
raise
|
|
end
|
|
|
|
def process_accounts
|
|
return [] if trading212_accounts.empty?
|
|
|
|
linked_trading212_accounts.includes(account_provider: :account).each_with_object([]) do |t212_account, results|
|
|
account = t212_account.current_account
|
|
next unless account
|
|
next if account.pending_deletion? || account.disabled?
|
|
|
|
begin
|
|
result = Trading212Account::Processor.new(t212_account).process
|
|
results << { trading212_account_id: t212_account.id, success: true, result: result }
|
|
rescue => e
|
|
DebugLogEntry.capture(
|
|
category: "sync",
|
|
level: "error",
|
|
message: "Trading212Item #{id} - Failed to process account #{t212_account.id}: #{e.message}",
|
|
source: "trading212",
|
|
family: family,
|
|
provider_key: "trading212",
|
|
account_provider_id: t212_account.account_provider&.id
|
|
)
|
|
results << { trading212_account_id: t212_account.id, success: false, error: e.message }
|
|
end
|
|
end
|
|
end
|
|
|
|
def schedule_account_syncs(parent_sync: nil, window_start_date: nil, window_end_date: nil)
|
|
accounts.reject { |account| account.pending_deletion? || account.disabled? }.each_with_object([]) do |account, results|
|
|
begin
|
|
account.sync_later(
|
|
parent_sync: parent_sync,
|
|
window_start_date: window_start_date,
|
|
window_end_date: window_end_date
|
|
)
|
|
results << { account_id: account.id, success: true }
|
|
rescue => e
|
|
DebugLogEntry.capture(
|
|
category: "sync",
|
|
level: "error",
|
|
message: "Trading212Item #{id} - Failed to schedule sync for account #{account.id}: #{e.message}",
|
|
source: "trading212",
|
|
family: family,
|
|
provider_key: "trading212",
|
|
account_id: account.id
|
|
)
|
|
results << { account_id: account.id, success: false, error: e.message }
|
|
end
|
|
end
|
|
end
|
|
|
|
def accounts
|
|
trading212_accounts.includes(account_provider: :account).filter_map(&:current_account).uniq
|
|
end
|
|
|
|
def linked_trading212_accounts
|
|
trading212_accounts.joins(:account_provider)
|
|
end
|
|
|
|
def linked_accounts_count
|
|
trading212_accounts.joins(:account_provider).count
|
|
end
|
|
|
|
def unlinked_accounts_count
|
|
trading212_accounts.left_joins(:account_provider).where(account_providers: { id: nil }).count
|
|
end
|
|
|
|
def total_accounts_count
|
|
trading212_accounts.count
|
|
end
|
|
|
|
def has_completed_initial_setup?
|
|
accounts.any?
|
|
end
|
|
|
|
def sync_status_summary
|
|
total = total_accounts_count
|
|
linked = linked_accounts_count
|
|
unlinked = unlinked_accounts_count
|
|
|
|
if total.zero?
|
|
I18n.t("trading212_items.sync_status.no_accounts")
|
|
elsif unlinked.zero?
|
|
I18n.t("trading212_items.sync_status.all_linked", count: linked)
|
|
else
|
|
I18n.t("trading212_items.sync_status.partial", linked: linked, unlinked: unlinked)
|
|
end
|
|
end
|
|
|
|
def institution_display_name
|
|
I18n.t("trading212_items.defaults.name")
|
|
end
|
|
|
|
def instruments_map
|
|
@instruments_map ||= Array(raw_instruments_payload).each_with_object({}) do |instrument, map|
|
|
map[instrument["ticker"]] = instrument
|
|
end
|
|
end
|
|
end
|