mirror of
https://github.com/we-promise/sure.git
synced 2026-05-10 14:15:01 +00:00
Merge branch 'main' into copilot/fix-twelvedata-api-limit-bug
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
This commit is contained in:
82
app/models/security/plan_restriction_tracker.rb
Normal file
82
app/models/security/plan_restriction_tracker.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
# Tracks securities that require a higher plan to fetch prices from data providers.
|
||||
# Uses Rails cache to store restriction info, keyed by provider and security ID.
|
||||
# This allows the settings page to warn users about tickers that need a paid plan.
|
||||
#
|
||||
# Note: Currently API keys are configured at the instance level (not per-family),
|
||||
# so restrictions are shared across all families using the same provider.
|
||||
module Security::PlanRestrictionTracker
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
CACHE_KEY_PREFIX = "security_plan_restriction"
|
||||
CACHE_EXPIRY = 7.days
|
||||
|
||||
# Map provider names to their classes for plan detection
|
||||
PROVIDER_CLASSES = {
|
||||
"TwelveData" => Provider::TwelveData
|
||||
}.freeze
|
||||
|
||||
class_methods do
|
||||
# Records that a security requires a higher plan to fetch data
|
||||
# @param security_id [Integer] The security ID
|
||||
# @param error_message [String] The error message from the provider
|
||||
# @param provider [String] The provider name (e.g., "TwelveData")
|
||||
def record_plan_restriction(security_id:, error_message:, provider:)
|
||||
provider_class = PROVIDER_CLASSES[provider]
|
||||
return unless provider_class&.respond_to?(:extract_required_plan)
|
||||
|
||||
required_plan = provider_class.extract_required_plan(error_message)
|
||||
return unless required_plan
|
||||
|
||||
cache_key = plan_restriction_cache_key(provider, security_id)
|
||||
Rails.cache.write(cache_key, {
|
||||
required_plan: required_plan,
|
||||
provider: provider,
|
||||
recorded_at: Time.current.iso8601
|
||||
}, expires_in: CACHE_EXPIRY)
|
||||
end
|
||||
|
||||
# Clears the plan restriction for a security (e.g., if user upgrades their plan)
|
||||
# @param security_id [Integer] The security ID
|
||||
# @param provider [String] The provider name
|
||||
def clear_plan_restriction(security_id, provider:)
|
||||
Rails.cache.delete(plan_restriction_cache_key(provider, security_id))
|
||||
end
|
||||
|
||||
# Returns the plan restriction info for a security, or nil if none
|
||||
# @param security_id [Integer] The security ID
|
||||
# @param provider [String] The provider name
|
||||
def plan_restriction_for(security_id, provider:)
|
||||
Rails.cache.read(plan_restriction_cache_key(provider, security_id))
|
||||
end
|
||||
|
||||
# Returns all plan-restricted securities from a collection of security IDs for a provider
|
||||
# @param security_ids [Array<Integer>] Security IDs to check
|
||||
# @param provider [String] The provider name
|
||||
# @return [Hash] security_id => restriction_info
|
||||
def plan_restrictions_for(security_ids, provider:)
|
||||
return {} if security_ids.blank?
|
||||
|
||||
restrictions = {}
|
||||
security_ids.each do |id|
|
||||
restriction = plan_restriction_for(id, provider: provider)
|
||||
restrictions[id] = restriction if restriction.present?
|
||||
end
|
||||
restrictions
|
||||
end
|
||||
|
||||
# Checks if an error message indicates a plan upgrade is required for a provider
|
||||
# @param error_message [String] The error message
|
||||
# @param provider [String] The provider name
|
||||
def plan_upgrade_required?(error_message, provider:)
|
||||
provider_class = PROVIDER_CLASSES[provider]
|
||||
return false unless provider_class&.respond_to?(:plan_upgrade_required?)
|
||||
provider_class.plan_upgrade_required?(error_message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def plan_restriction_cache_key(provider, security_id)
|
||||
"#{CACHE_KEY_PREFIX}/#{provider.downcase}/#{security_id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -111,6 +111,7 @@ class Security::Price::Importer
|
||||
)
|
||||
|
||||
if response.success?
|
||||
Security.clear_plan_restriction(security.id, provider: security_provider.class.name.demodulize)
|
||||
response.data.index_by(&:date)
|
||||
else
|
||||
error = response.error
|
||||
@@ -120,7 +121,17 @@ class Security::Price::Importer
|
||||
raise error
|
||||
end
|
||||
|
||||
Rails.logger.warn("#{security_provider.class.name} could not fetch prices for #{security.ticker} between #{provider_fetch_start_date} and #{end_date}. Provider error: #{error.message}")
|
||||
error_message = response.error.message
|
||||
Rails.logger.warn("#{security_provider.class.name} could not fetch prices for #{security.ticker} between #{provider_fetch_start_date} and #{end_date}. Provider error: #{error_message}")
|
||||
|
||||
if Security.plan_upgrade_required?(error_message, provider: security_provider.class.name.demodulize)
|
||||
Security.record_plan_restriction(
|
||||
security_id: security.id,
|
||||
error_message: error_message,
|
||||
provider: security_provider.class.name.demodulize
|
||||
)
|
||||
end
|
||||
|
||||
Sentry.capture_exception(MissingSecurityPriceError.new("Could not fetch prices for ticker"), level: :warning) do |scope|
|
||||
scope.set_tags(security_id: security.id)
|
||||
scope.set_context("security", { id: security.id, start_date: start_date, end_date: end_date })
|
||||
|
||||
Reference in New Issue
Block a user