Class: ListingIssues::RetailerProbeAdapter

Inherits:
BaseAdapter
  • Object
show all
Defined in:
app/services/listing_issues/retailer_probe_adapter.rb

Overview

Cross-retailer listing issues derived from our OWN daily Oxylabs probe
(Retailer::PriceCheckerCatalogItemRetailerProbe). This is the only
"is the listing live" signal a scrape-only retailer (Home Depot, Rona,
Costco, … — no channel API to ask) has, and it applies identically wherever
a catalog is probed, so the counts are directly comparable retailer to
retailer. Complements the channel-sourced adapters
(WayfairAdapter#not_live_issue, AmazonAdapter#issues_from_suppression):
those trust what the marketplace API says; this trusts our own observation.

Reads stored probe rows only — never probes — so it can't block and stays
consistent with whatever the last probe run recorded. Everything is computed
as memoized id Sets from CatalogItemRetailerProbe's current-state scopes, so
the bulk sweep does a handful of set queries, not one per item.

Two severities, split by CONFIDENCE:

  • critical — the retailer page LOADED (ruling out a scraper/network blip)
    but the product is broken: not on the page / a wrong product
    (not_found / product_mismatch), or the retailer shows it out of stock
    while OUR inventory says in stock (a stuck/broken listing, not a real
    stockout). Self-verified live revenue loss.

  • error — not_confirmable: a human set skip_url_checks on the item, so
    nothing will ever re-confirm the listing. Lower confidence
    than critical — the evidence is frozen at whatever we last saw and can no
    longer be refreshed — but a real "we've given up confirming this" signal.

    This claims EVERY opted-out item, including ones whose last probe
    loaded the page and found the product missing. Those look like critical
    not_buyable evidence, but we stopped probing them, so the finding can't
    be re-confirmed and doesn't belong in a metric of what is failing now.
    Keeping them here is also what lets the /retailers dashboard count each
    problem once: not_buyable and out_of_stock_mismatch exist only for
    actively-probed items and are therefore fully represented by the Scrape
    Failures / Out of Stock Online columns, so
    Retailer::DailyComplianceReport::COLUMN_BACKED_ISSUE_CODES excludes
    them from the Listing Issues count while these stay.

  • error — probe_stale: no probe row at all inside
    CatalogItemRetailerProbe::STALE_PROBE_DAYS. Every signal above reads a
    probe row (or skip_url_checks), so an item that stops being probed
    entirely is invisible to all
    of them — it silently keeps serving whatever price its last probe wrote.
    Last in the precedence chain: a recent probe that found something wrong
    is always the better signal.

Constant Summary collapse

PROVIDER =

Provider key stamped on issues produced by this adapter.

'retailer_probe'

Instance Method Summary collapse

Constructor Details

#initialize(item_ids: nil) ⇒ RetailerProbeAdapter

Returns a new instance of RetailerProbeAdapter.

Parameters:

  • item_ids (Array<Integer>, nil) (defaults to: nil)

    restrict every internal set query
    to these catalog items — used by the per-item event-driven reconcile
    (ListingIssues::RetailerProbeCompletedHandler) so a single probe
    doesn't trigger the bulk sweep's full-catalog set queries. Nil (the
    daily sweep) keeps the unscoped bulk behavior.



57
58
59
# File 'app/services/listing_issues/retailer_probe_adapter.rb', line 57

def initialize(item_ids: nil)
  @item_ids = item_ids
end

Instance Method Details

#candidate_item_idsArray<Integer>

Active, probe-eligible items currently in any flagged probe state, plus any
item already carrying an open issue (so a recovered listing auto-resolves).
In item-scoped mode the open-issue lookup is unscoped, so intersect here.

Returns:

  • (Array<Integer>)


68
69
70
71
72
# File 'app/services/listing_issues/retailer_probe_adapter.rb', line 68

def candidate_item_ids
  ids = (scrape_failure_ids.to_a + stock_mismatch_ids.to_a + skipped_ids.to_a +
         exhausted_ids.to_a + stale_probe_ids.to_a + open_issue_item_ids).uniq
  @item_ids ? ids & @item_ids.map(&:to_i) : ids
end

#eager_loadArray

Returns associations the bulk sweep eager-loads before #issues_for.

Returns:

  • (Array)

    associations the bulk sweep eager-loads before #issues_for



94
95
96
# File 'app/services/listing_issues/retailer_probe_adapter.rb', line 94

def eager_load
  [:catalog]
end

#issues_for(catalog_item) ⇒ Array<ListingIssues::Issue>

Parameters:

Returns:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'app/services/listing_issues/retailer_probe_adapter.rb', line 76

def issues_for(catalog_item)
  return [] unless catalog_item.state == 'active'

  id = catalog_item.id
  # Exhaustion outranks the per-probe classification. An exhausted item will
  # nearly always ALSO look like a scrape failure — that is how it got there
  # — so classifying it as not_buyable would make the escalation
  # unreachable in exactly the case it exists for.
  return [needs_manual_confirmation_issue(catalog_item)] if exhausted_ids.include?(id)
  return [not_buyable_issue(catalog_item)] if scrape_failure_ids.include?(id)
  return [out_of_stock_mismatch_issue(catalog_item)] if stock_mismatch_ids.include?(id)
  return [not_confirmable_issue(catalog_item)] if skipped_ids.include?(id)
  return [probe_stale_issue(catalog_item)] if stale_probe_ids.include?(id)

  []
end

#providerString

Returns:

  • (String)


62
# File 'app/services/listing_issues/retailer_probe_adapter.rb', line 62

def provider = PROVIDER