Files
sure/app/models/snaptrade_item/provided.rb
LPW e6d8112278 Add SnapTrade connection management UI for freeing up connection slots (#747)
* Add SnapTrade connection management with lazy-loading and deletion functionality.

* Refactor lazy-load controller to simplify event handling and enhance loading state management; improve SnapTrade deletion logic with additional safeguards and logging.

* Improve SnapTrade connection error handling and centralize unknown brokerage message using i18n.

* Centralize SnapTrade connection default name and missing authorization ID messages using i18n.

* Enhance SnapTrade connection deletion logic with improved error handling, i18n support for API deletion failures, and consistent Turbo Stream responses.

---------

Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
2026-01-23 08:55:34 +01:00

178 lines
6.1 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
# Fetch all brokerage connections from SnapTrade API
# Returns array of connection objects
def fetch_connections
return [] unless credentials_configured? && user_registered?
provider = snaptrade_provider
creds = snaptrade_credentials
provider.list_connections(user_id: creds[:user_id], user_secret: creds[:user_secret])
rescue Provider::Snaptrade::ApiError => e
Rails.logger.error "SnaptradeItem #{id} - Failed to list connections: #{e.message}"
raise
end
# List all SnapTrade users registered under this client ID
def list_all_users
return [] unless credentials_configured?
snaptrade_provider.list_users
rescue Provider::Snaptrade::ApiError => e
Rails.logger.error "SnaptradeItem #{id} - Failed to list users: #{e.message}"
[]
end
# Find orphaned SnapTrade users (registered but not current user)
def orphaned_users
return [] unless credentials_configured? && user_registered?
all_users = list_all_users
all_users.reject { |uid| uid == snaptrade_user_id }
end
# Delete an orphaned SnapTrade user and all their connections
def delete_orphaned_user(user_id)
return false unless credentials_configured?
return false if user_id == snaptrade_user_id # Don't delete current user
snaptrade_provider.delete_user(user_id: user_id)
true
rescue Provider::Snaptrade::ApiError => e
Rails.logger.error "SnaptradeItem #{id} - Failed to delete orphaned user #{user_id}: #{e.message}"
false
end
end