Class: Retailer::BestBuyUrlDiscovery

Inherits:
Object
  • Object
show all
Defined in:
app/services/retailer/best_buy_url_discovery.rb

Overview

Discovers bestbuy.ca product URLs for WarmlyYours catalog items via Best Buy's
own public JSON API — no Oxylabs spend, no HTML scraping, no search-results
page to mis-resolve.

Best Buy Canada serves an unauthenticated JSON API that answers from a plain
datacenter IP (unlike the HTML site, which is bot-gated):

/api/v2/json/search?query=warmlyyours → { products: [{ sku, productUrl }] }
/api/v2/json/product/{sku}{ sku, modelNumber, productUrl }

sku is Best Buy's own product id and the trailing segment of every PDP URL,
and it is what we store in CatalogItem#third_party_part_number. modelNumber
is OUR manufacturer SKU, which gives a second, equally hard identity for items
whose part number was never captured.

== Matching

Two exact-equality matches, in order — never a fuzzy title match, because a
wrong match here writes a wrong price into MAP reporting (the failure mode
PR #1480 removed the search fallback over):

  1. third_party_part_number == Best Buy sku
  2. our item SKU == Best Buy modelNumber

An item matching neither is genuinely not in Best Buy's catalogue — it should
be reported as a listing problem, not guessed at, so #discover_url returns nil.

Examples:

Retailer::BestBuyUrlDiscovery.new.discover_url(catalog_item)
#=> "https://www.bestbuy.ca/en-ca/product/led-backlit-mirror-ingrid.../17373192"

Constant Summary collapse

BASE_URL =
'https://www.bestbuy.ca'
SEARCH_ENDPOINT =
"#{BASE_URL}/api/v2/json/search".freeze
PRODUCT_ENDPOINT =
"#{BASE_URL}/api/v2/json/product".freeze
BRAND_QUERY =

Brand query. Best Buy returns the full WarmlyYours assortment (65 products
as of 2026-08-08), so one page covers it.

'warmlyyours'
PAGE_SIZE =
100
USER_AGENT =

The API rejects an obviously-scripted client; a normal desktop UA is served
the same public JSON a browser gets.

'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' \
'(KHTML, like Gecko) Chrome/128.0 Safari/537.36'
TIMEOUT =
20
MAX_DETAIL_FETCHES =

Ceiling on per-product detail calls in one run. At ~10s each this bounds the
model-number fallback to a couple of minutes even if Best Buy's assortment
grows or every part number goes missing at once.

25

Instance Method Summary collapse

Constructor Details

#initialize(logger: Rails.logger) ⇒ BestBuyUrlDiscovery

Returns a new instance of BestBuyUrlDiscovery.

Parameters:

  • logger (Logger) (defaults to: Rails.logger)


56
57
58
# File 'app/services/retailer/best_buy_url_discovery.rb', line 56

def initialize(logger: Rails.logger)
  @logger = logger
end

Instance Method Details

#discover_url(catalog_item) ⇒ String?

Returns absolute PDP URL, or nil when Best Buy doesn't list it.

Parameters:

Returns:

  • (String, nil)

    absolute PDP URL, or nil when Best Buy doesn't list it



62
63
64
65
66
# File 'app/services/retailer/best_buy_url_discovery.rb', line 62

def discover_url(catalog_item)
  part_number = catalog_item.third_party_part_number.to_s.strip
  matched = by_sku[part_number].presence if part_number.present?
  matched || by_model_number[normalize(catalog_item.sku)].presence
end