Class: Retailer::PriceChecker
- Inherits:
-
Object
- Object
- Retailer::PriceChecker
- Includes:
- CatalogConstants
- Defined in:
- app/services/retailer/price_checker.rb
Overview
Service to check product prices and availability on retailer websites.
Uses Oxylabs Web Scraper API to fetch page content and extract data.
Constant Summary collapse
- DEFAULT_GEO_LOCATIONS =
Default ZIP codes by country
{ 'USA' => '60047', # Lake Zurich, IL (WarmlyYours HQ area) 'CAN' => 'M5V3A8', # Toronto, ON 'DEU' => '10115', # Berlin 'GBR' => 'SW1A1AA', # London 'FRA' => '75001', # Paris 'ITA' => '00100', # Rome 'ESP' => '28001', # Madrid 'NLD' => '1011', # Amsterdam 'POL' => '00-001', # Warsaw 'SWE' => '11120', # Stockholm 'BEL' => '1000' # Brussels }.freeze
- CATALOG_RETAILER_TYPES =
Retailer type detection based on catalog ID
Uses CatalogConstants for maintainability { # Home Depot HOME_DEPOT_USA => :home_depot_usa, HOME_DEPOT_CANADA => :home_depot_canada, # Costco COSTCO_CANADA => :costco_canada, # Wayfair WAYFAIR_USA => :wayfair, WAYFAIR_CANADA => :wayfair, WAYFAIR_GERMANY => :wayfair, # Rona/Lowe's RONA_CANADA => :rona, LOWES_USA => :lowes, LOWES_CANADA => :lowes, # Build.com (Ferguson Home) BUILD_COM => :build_com, # Amazon Seller Marketplaces AMAZON_SC_US_CATALOG_ID => :amazon, AMAZON_SC_CA_CATALOG_ID => :amazon, AMAZON_SC_FR_CATALOG_ID => :amazon, AMAZON_SC_IT_CATALOG_ID => :amazon, AMAZON_SC_ES_CATALOG_ID => :amazon, AMAZON_SC_DE_CATALOG_ID => :amazon, AMAZON_SC_NL_CATALOG_ID => :amazon, AMAZON_SC_PL_CATALOG_ID => :amazon, AMAZON_SC_UK_CATALOG_ID => :amazon, AMAZON_SC_SE_CATALOG_ID => :amazon, AMAZON_SC_BE_CATALOG_ID => :amazon, # Walmart Seller Marketplaces WALMART_SELLER_USA => :walmart_usa, WALMART_SELLER_CANADA => :walmart_canada, # Canadian Tire CANADIAN_TIRE => :canadian_tire, # Houzz HOUZZ => :houzz, # Best Buy Canada BESTBUY_CANADA => :bestbuy_canada }.freeze
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
-
#api ⇒ Object
readonly
Returns the value of attribute api.
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#url_constructor ⇒ Object
readonly
Returns the value of attribute url_constructor.
Instance Method Summary collapse
-
#check(catalog_item, geo_location: nil) ⇒ CatalogItemRetailerProbe
Check a single catalog item and record the result.
-
#check_all_retailers ⇒ Hash
Check all active items across all retailer catalogs.
-
#check_catalog(catalog) ⇒ Array<CatalogItemRetailerProbe>
Check all active items in a catalog.
-
#initialize(options = {}) ⇒ PriceChecker
constructor
A new instance of PriceChecker.
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 = {}) ⇒ PriceChecker
Returns a new instance of PriceChecker.
75 76 77 78 79 |
# File 'app/services/retailer/price_checker.rb', line 75 def initialize( = {}) @api = [:api] || Retailer::OxylabsApi.new(timeout: 120) @url_constructor = [:url_constructor] || Retailer::UrlConstructor.new @logger = [:logger] || Rails.logger end |
Instance Attribute Details
#api ⇒ Object (readonly)
Returns the value of attribute api.
73 74 75 |
# File 'app/services/retailer/price_checker.rb', line 73 def api @api end |
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
73 74 75 |
# File 'app/services/retailer/price_checker.rb', line 73 def logger @logger end |
#url_constructor ⇒ Object (readonly)
Returns the value of attribute url_constructor.
73 74 75 |
# File 'app/services/retailer/price_checker.rb', line 73 def url_constructor @url_constructor end |
Instance Method Details
#check(catalog_item, geo_location: nil) ⇒ CatalogItemRetailerProbe
Check a single catalog item and record the result
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'app/services/retailer/price_checker.rb', line 85 def check(catalog_item, geo_location: nil) return nil if catalog_item.skip_url_checks? catalog = catalog_item.catalog retailer_type = detect_retailer_type(catalog) # Get URL - prefer hardcoded, fall back to generated url = url_constructor.product_url(catalog_item) return nil if url.blank? && retailer_type != :amazon geo_location ||= default_geo_location(catalog_item) start_time = Time.current # Create the check record with the URL that will be used check = catalog_item.retailer_probes.build( status: 'pending', geo_location: geo_location, url: url ) begin result = fetch_product(catalog_item, catalog, retailer_type, geo_location) process_result(check, result, start_time, retailer_type, catalog_item) rescue StandardError => e check.status = 'failed' check. = "#{e.class}: #{e.}" @logger.error "[PriceChecker] Error checking #{catalog_item.id}: #{e.}" end check.save! # Update the catalog item's url_valid, url_last_checked, and retail_price fields update_attrs = { url_valid: check.page_accessible?, url_last_checked: Time.current } # Only store retail_price for vendor (1P) catalogs where the retailer # sets the price. For marketplace (3P) catalogs the price is ours, so # writing it back as "advertised price" is circular and confusing. if check.status == 'success' && check.price.present? && catalog.retailer_type_vendor? update_attrs[:retail_price] = check.price update_attrs[:retailer_price_updated_at] = Time.current end # Update URL if we discovered a better one (e.g., from search results) if @discovered_url.present? && is_direct_product_url?(@discovered_url) clean_url = strip_tracking_params(@discovered_url) if catalog_item.url.blank? || catalog_item.url.include?('search') update_attrs[:url] = clean_url @logger.info "[PriceChecker] Discovered URL for item #{catalog_item.id}: #{clean_url}" end end catalog_item.update_columns(update_attrs) check end |
#check_all_retailers ⇒ Hash
Check all active items across all retailer catalogs
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'app/services/retailer/price_checker.rb', line 160 def check_all_retailers results = { total: 0, success: 0, failed: 0, catalogs: {} } retailer_catalog_items.find_each do |item| check_record = check(item) next unless check_record results[:total] += 1 results[:success] += 1 if check_record.status == 'success' results[:failed] += 1 if check_record.status.in?(%w[failed not_found]) catalog_name = item.catalog.name results[:catalogs][catalog_name] ||= { success: 0, failed: 0 } results[:catalogs][catalog_name][:success] += 1 if check_record.status == 'success' results[:catalogs][catalog_name][:failed] += 1 if check_record.status.in?(%w[failed not_found]) end results end |
#check_catalog(catalog) ⇒ Array<CatalogItemRetailerProbe>
Check all active items in a catalog
147 148 149 150 151 152 153 154 155 156 |
# File 'app/services/retailer/price_checker.rb', line 147 def check_catalog(catalog) items = catalog.catalog_items .where(state: 'active') .where(skip_url_checks: false) # For non-Amazon catalogs, require a URL items = items.where.not(url: [nil, '']) unless catalog.name.match?(/amazon/i) items.map { |item| check(item) }.compact end |