Files
sure/app/models/snaptrade_item/provided.rb
LPW a83f70425f Add SnapTrade brokerage integration with full trade history support (#737)
* Introduce SnapTrade integration with models, migrations, views, and activity processing logic.

* Refactor SnapTrade activities processing: improve activity fetching flow, handle pending states, and update UI elements for enhanced user feedback.

* Update Brakeman ignore file to include intentional redirect for SnapTrade OAuth portal.

* Refactor SnapTrade models, views, and processing logic: add currency extraction helper, improve pending state handling, optimize migration checks, and enhance user feedback in UI.

* Remove encryption for SnapTrade `snaptrade_user_id`, as it is an identifier, not a secret.

* Introduce `SnaptradeConnectionCleanupJob` to asynchronously handle SnapTrade connection cleanup and improve i18n for SnapTrade item status messages.

* Update SnapTrade encryption: make `snaptrade_user_secret` non-deterministic to enhance security.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-01-22 20:52:49 +01:00

135 lines
4.7 KiB
Ruby

module SnaptradeItem::Provided
extend ActiveSupport::Concern
included do
before_destroy :delete_snaptrade_user
end
def snaptrade_provider
return nil unless credentials_configured?
Provider::Snaptrade.new(
client_id: client_id,
consumer_key: consumer_key
)
end
# Clean up SnapTrade user when item is destroyed
def delete_snaptrade_user
return unless user_registered?
provider = snaptrade_provider
return unless provider
Rails.logger.info "SnapTrade: Deleting user #{snaptrade_user_id} for family #{family_id}"
provider.delete_user(user_id: snaptrade_user_id)
Rails.logger.info "SnapTrade: Successfully deleted user #{snaptrade_user_id}"
rescue => e
# Log but don't block deletion - user may not exist or credentials may be invalid
Rails.logger.warn "SnapTrade: Failed to delete user #{snaptrade_user_id}: #{e.class} - #{e.message}"
end
# User ID and secret for SnapTrade API calls
def snaptrade_credentials
return nil unless snaptrade_user_id.present? && snaptrade_user_secret.present?
{
user_id: snaptrade_user_id,
user_secret: snaptrade_user_secret
}
end
# Check if user is registered with SnapTrade
def user_registered?
snaptrade_user_id.present? && snaptrade_user_secret.present?
end
# Register user with SnapTrade if not already registered
# Returns true if registration succeeded or already registered
# If existing credentials are invalid (user was deleted), clears them and re-registers
def ensure_user_registered!
# If we think we're registered, verify the user still exists
if user_registered?
if verify_user_exists?
return true
else
# User was deleted from SnapTrade API - clear local credentials and re-register
Rails.logger.warn "SnapTrade: User #{snaptrade_user_id} no longer exists, clearing credentials and re-registering"
update!(snaptrade_user_id: nil, snaptrade_user_secret: nil)
end
end
provider = snaptrade_provider
raise StandardError, "SnapTrade provider not configured" unless provider
# Use family ID with current timestamp to ensure uniqueness (avoids conflicts from previous deletions)
unique_user_id = "family_#{family_id}_#{Time.current.to_i}"
Rails.logger.info "SnapTrade: Registering user #{unique_user_id} for family #{family_id}"
result = provider.register_user(unique_user_id)
Rails.logger.info "SnapTrade: Successfully registered user #{result[:user_id]}"
update!(
snaptrade_user_id: result[:user_id],
snaptrade_user_secret: result[:user_secret]
)
true
rescue Provider::Snaptrade::ApiError => e
Rails.logger.error "SnapTrade user registration failed: #{e.class} - #{e.message}"
# Log status code but not response_body to avoid credential exposure
Rails.logger.error "SnapTrade error details: status=#{e.status_code}" if e.respond_to?(:status_code)
Rails.logger.debug { "SnapTrade response body: #{e.response_body&.truncate(500)}" } if e.respond_to?(:response_body)
# Check if user already exists (shouldn't happen with timestamp suffix, but handle gracefully)
if e.message.include?("already registered") || e.message.include?("already exists")
Rails.logger.warn "SnapTrade: User already exists. Generating new unique ID."
raise StandardError, "User registration conflict. Please try again."
end
raise
end
# Verify that the stored user actually exists in SnapTrade
# Returns false if user doesn't exist, credentials are invalid, or verification fails
def verify_user_exists?
return false unless snaptrade_user_id.present?
provider = snaptrade_provider
return false unless provider
# Try to list connections - this will fail with 401/403 if user doesn't exist
provider.list_connections(
user_id: snaptrade_user_id,
user_secret: snaptrade_user_secret
)
true
rescue Provider::Snaptrade::AuthenticationError => e
Rails.logger.warn "SnapTrade: User verification failed - #{e.message}"
false
rescue Provider::Snaptrade::ApiError => e
# Return false on API errors - caller can retry registration if needed
Rails.logger.warn "SnapTrade: User verification error - #{e.message}"
false
end
# Get the connection portal URL for linking brokerages
def connection_portal_url(redirect_url:, broker: nil)
raise StandardError, "User not registered with SnapTrade" unless user_registered?
provider = snaptrade_provider
raise StandardError, "SnapTrade provider not configured" unless provider
provider.get_connection_url(
user_id: snaptrade_user_id,
user_secret: snaptrade_user_secret,
redirect_url: redirect_url,
broker: broker
)
end
end