Class: Retailer::SiblingPriceRefresher

Inherits:
Object
  • Object
show all
Includes:
CatalogConstants
Defined in:
app/services/retailer/sibling_price_refresher.rb

Overview

Service to synchronously refresh retailer prices for sibling catalog items.
Used by the repricer to ensure fresh external price data before deciding
whether to raise Amazon prices.

This uses the REALTIME (synchronous) Oxylabs API, not the batch webhook system.
It should only be called for items that are candidates for price increases
to minimize API calls.

Examples:

Refresh sibling prices for an Amazon catalog item

refresher = Retailer::SiblingPriceRefresher.new
result = refresher.refresh_for(amazon_catalog_item)
lowest_price = result.lowest_price

Defined Under Namespace

Classes: Result

Constant Summary collapse

MAX_SIBLINGS_TO_PROBE =

Maximum sibling items to probe in one call (to limit API costs)

10
FRESHNESS_THRESHOLD =

Only refresh probes that are older than this threshold.

Bumped from 6h to 24h in May 2026 after audit showed the 6h window was
being defeated by the ~10–15% probe failure rate: items that returned
failed/not_found from the nightly batch checker had no
retailer_price_updated_at or url_last_checked update, so they were
treated as stale and re-probed by every Amazon pricing run. With
WebhookResultProcessor now stamping url_last_checked on every outcome
and the threshold at 24h, a single successful nightly batch satisfies
freshness for the next day's automation. Chronic-failure items are
bounded separately by Retailer::ProbeAutoSkipper.

24.hours

Constants included from CatalogConstants

CatalogConstants::ALL_MAIN_CATALOG_IDS, CatalogConstants::AMAZON_CATALOG_IDS, CatalogConstants::AMAZON_CA_CATALOG_IDS, CatalogConstants::AMAZON_EU_CATALOG_IDS, CatalogConstants::AMAZON_NA_SELLER_IDS, CatalogConstants::AMAZON_SC_BE_CATALOG_ID, CatalogConstants::AMAZON_SC_CATALOG_IDS, CatalogConstants::AMAZON_SC_CA_CATALOG_ID, CatalogConstants::AMAZON_SC_DE_CATALOG_ID, CatalogConstants::AMAZON_SC_ES_CATALOG_ID, CatalogConstants::AMAZON_SC_FR_CATALOG_ID, CatalogConstants::AMAZON_SC_IT_CATALOG_ID, CatalogConstants::AMAZON_SC_NL_CATALOG_ID, CatalogConstants::AMAZON_SC_PL_CATALOG_ID, CatalogConstants::AMAZON_SC_SE_CATALOG_ID, CatalogConstants::AMAZON_SC_UK_CATALOG_ID, CatalogConstants::AMAZON_SC_US_CATALOG_ID, CatalogConstants::AMAZON_SELLER_IDS, CatalogConstants::AMAZON_US_CATALOG_IDS, CatalogConstants::AMAZON_VC_CATALOG_IDS, CatalogConstants::AMAZON_VC_CA_CATALOG_ID, CatalogConstants::AMAZON_VC_CA_CATALOG_IDS, CatalogConstants::AMAZON_VC_DIRECT_FULFILLMENT_CATALOG_IDS, CatalogConstants::AMAZON_VC_US_CATALOG_IDS, CatalogConstants::AMAZON_VC_US_WASN4_CATALOG_ID, CatalogConstants::AMAZON_VC_US_WAX7V_CATALOG_ID, CatalogConstants::AMAZON_VC_WAT0F_CA_CATALOG_ID, CatalogConstants::AMAZON_VC_WAT4D_CA_CATALOG_ID, CatalogConstants::AMAZON_VENDOR_CODE_TO_CATALOG_ID, CatalogConstants::BESTBUY_CANADA, CatalogConstants::BUILD_COM, CatalogConstants::CANADIAN_TIRE, CatalogConstants::CA_CATALOG_ID, CatalogConstants::COSTCO_CANADA, CatalogConstants::COSTCO_CATALOGS, CatalogConstants::COSTCO_USA, CatalogConstants::EU_CATALOG_ID, CatalogConstants::HOME_DEPOT_CANADA, CatalogConstants::HOME_DEPOT_CATALOGS, CatalogConstants::HOME_DEPOT_USA, CatalogConstants::HOUZZ, CatalogConstants::LOCALE_TO_CATALOG, CatalogConstants::LOWES_CANADA, CatalogConstants::LOWES_USA, CatalogConstants::RONA_CANADA, CatalogConstants::US_CATALOG_ID, CatalogConstants::WALMART_CATALOGS, CatalogConstants::WALMART_SELLER_CANADA, CatalogConstants::WALMART_SELLER_USA, CatalogConstants::WAYFAIR_CANADA, CatalogConstants::WAYFAIR_CATALOGS, CatalogConstants::WAYFAIR_GERMANY, CatalogConstants::WAYFAIR_USA

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from CatalogConstants

amazon_catalog?, amazon_seller_catalog?, costco_catalog?, home_depot_catalog?, walmart_catalog?, wayfair_catalog?

Constructor Details

#initialize(options = {}) ⇒ SiblingPriceRefresher

Returns a new instance of SiblingPriceRefresher.



46
47
48
49
# File 'app/services/retailer/sibling_price_refresher.rb', line 46

def initialize(options = {})
  @price_checker = options[:price_checker] || Retailer::PriceChecker.new
  @logger = options[:logger] || Rails.logger
end

Instance Attribute Details

#loggerObject (readonly)

Returns the value of attribute logger.



44
45
46
# File 'app/services/retailer/sibling_price_refresher.rb', line 44

def logger
  @logger
end

#price_checkerObject (readonly)

Returns the value of attribute price_checker.



44
45
46
# File 'app/services/retailer/sibling_price_refresher.rb', line 44

def price_checker
  @price_checker
end

Instance Method Details

#refresh_for(catalog_item, force: false) ⇒ Result

Refresh sibling retailer prices for a given catalog item.
Only refreshes items from catalogs with external_price_check_enabled.

Parameters:

  • catalog_item (CatalogItem)

    The Amazon catalog item to find siblings for

  • force (Boolean) (defaults to: false)

    Force refresh even if data is fresh (default: false)

Returns:

  • (Result)

    with lowest_price and list of all refreshed prices



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'app/services/retailer/sibling_price_refresher.rb', line 57

def refresh_for(catalog_item, force: false)
  item_id = catalog_item.item_id
  store_id = catalog_item.catalog.store_id

  unless item_id
    @logger.warn "[SiblingPriceRefresher] No item_id for catalog_item #{catalog_item.id}"
    return Result.new(refreshed_count: 0, lowest_price: nil, prices: [], errors: ['No item_id'])
  end

  # Find sibling catalog items in non-Amazon catalogs with price checking enabled
  siblings = find_sibling_catalog_items(item_id, store_id, catalog_item.catalog_id)

  if siblings.empty?
    @logger.info "[SiblingPriceRefresher] No sibling catalog items found for item #{item_id}"
    return Result.new(refreshed_count: 0, lowest_price: nil, prices: [], errors: [])
  end

  # Filter to only items that need refresh (stale data)
  to_refresh = force ? siblings : siblings.select { |s| needs_refresh?(s) }

  @logger.info "[SiblingPriceRefresher] Found #{siblings.size} siblings, #{to_refresh.size} need refresh"

  # Limit to MAX_SIBLINGS_TO_PROBE to control costs
  to_refresh = to_refresh.first(MAX_SIBLINGS_TO_PROBE)

  # Synchronously probe each sibling
  refreshed_count = 0
  prices = []
  errors = []

  to_refresh.each do |sibling|
    probe = @price_checker.check(sibling)
    if probe&.price_captured?
      refreshed_count += 1
      prices << {
        catalog_item_id: sibling.id,
        catalog_name: sibling.catalog.name,
        price: probe.price,
        currency: probe.currency
      }
      @logger.info "[SiblingPriceRefresher] Refreshed #{sibling.catalog.name}: $#{probe.price}"
    elsif probe
      @logger.debug "[SiblingPriceRefresher] Probe completed but no price: #{probe.status}"
    end
  rescue StandardError => e
    @logger.error "[SiblingPriceRefresher] Error probing #{sibling.id}: #{e.message}"
    errors << "#{sibling.catalog.name}: #{e.message}"
  end

  # Get the lowest price from refreshed items + existing fresh data
  all_prices = collect_all_sibling_prices(item_id, store_id, catalog_item.catalog_id)
  lowest_price = all_prices.min

  Result.new(
    refreshed_count: refreshed_count,
    lowest_price: lowest_price,
    prices: prices,
    errors: errors
  )
end