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.
Set to 6h so the nightly batch checker (~7am UTC) is still considered fresh
when the automation runs at ~11:15am UTC (4h15m gap), preventing every Buy
Box winner from triggering a blocking synchronous Oxylabs probe each run.

6.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::MARKETPLACE_CATALOGS, CatalogConstants::PRICE_CHECK_ENABLED_CATALOGS, CatalogConstants::RONA_CANADA, CatalogConstants::US_CATALOG_ID, CatalogConstants::VENDOR_CATALOGS, 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?, marketplace_catalog?, price_check_enabled?, vendor_catalog?, walmart_catalog?, wayfair_catalog?

Constructor Details

#initialize(options = {}) ⇒ SiblingPriceRefresher

Returns a new instance of SiblingPriceRefresher.



37
38
39
40
# File 'app/services/retailer/sibling_price_refresher.rb', line 37

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.



35
36
37
# File 'app/services/retailer/sibling_price_refresher.rb', line 35

def logger
  @logger
end

#price_checkerObject (readonly)

Returns the value of attribute price_checker.



35
36
37
# File 'app/services/retailer/sibling_price_refresher.rb', line 35

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



48
49
50
51
52
53
54
55
56
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
# File 'app/services/retailer/sibling_price_refresher.rb', line 48

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|
    begin
      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
  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