Class: CatalogItemRetailerProbe
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- CatalogItemRetailerProbe
- Includes:
- Turbo::Broadcastable
- Defined in:
- app/models/catalog_item_retailer_probe.rb
Overview
== Schema Information
Table name: catalog_item_retailer_probes
Database name: primary
id :bigint not null, primary key
content_size_bytes :integer
currency :string(3)
error_message :string
geo_location :string(64)
page_accessible :boolean default(FALSE)
price :decimal(10, 2)
product_available :boolean
response_time_ms :integer
status :string default("pending"), not null
store_json :jsonb
url :string
created_at :datetime not null
updated_at :datetime not null
catalog_item_id :bigint not null
Indexes
idx_retailer_probes_item_created (catalog_item_id,created_at)
index_catalog_item_retailer_probes_on_created_at (created_at)
index_catalog_item_retailer_probes_on_status (status)
Foreign Keys
fk_rails_... (catalog_item_id => catalog_items.id)
Constant Summary collapse
- STATUSES =
Status values
{ pending: 'pending', # Probe queued but not yet performed success: 'success', # Page loaded and data extracted failed: 'failed', # Request failed (timeout, error) not_found: 'not_found', # Page returned 404 or product not on page product_mismatch: 'product_mismatch' # Page loaded but product identifiers not found }.freeze
- CURRENT_STATE_LOOKBACK_DAYS =
Lookback window for treating a probe as the item's current external state.
Retailer::DailyComplianceReport derives its probe-based columns (scrape
failures / unreachable / out of stock online) from this same window, and the
latest-probe scopes below back the deep links in that daily email, so the
report's counts and the lists those links open stay in lockstep. 14- STALE_PROBE_DAYS =
Age past which a probe-derived price is no longer decision-grade. Probed
catalogs run a daily batch, so a full week with no probe row at all means
the item silently fell out of the run — see
ListingIssues::RetailerProbeAdapter#stale_probe_ids. Matches the 7-day
freshness floor the repricer already applies to sibling retailer ceilings
(Catalog::AmazonPriceRaisingService#lowest_sibling_retailer). 7- GEO_LOCATION_MAX_LENGTH =
Matches the geo_location varchar length in db/structure.sql. Callers that
persist an externally-sourced geo_location (e.g. WebhookProcessors::OxylabsProcessor)
clamp to this so an over-long value can't raise on create (AppSignal #6019). 64- TAB_LISTING_LIMIT =
Probes rendered in the CRM retailer-probes tab, newest first. Shared by the
controller and by the completion broadcast so both paint the same set — the
broadcast replaces the whole table rather than prepending a row, so it has to
re-derive the identical query. 50
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
-
#currency ⇒ String?
ISO currency code of the captured price.
-
#geo_location ⇒ String?
Geographic location the probe ran from.
-
#status ⇒ String
Probe status.
Belongs to collapse
Class Method Summary collapse
-
.actively_probed ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are actively probed.
-
.current_out_of_stock_online_item_ids ⇒ ActiveRecord::Relation
catalog_item_ids whose most-recent probe in the window reported the item out of stock at the retailer.
-
.current_scrape_failure_item_ids ⇒ ActiveRecord::Relation
catalog_item_ids whose most-recent probe in the window loaded the page but could not yield a valid product/price — a scrape failure (product not on page / 404-on-page / product mismatch).
-
.current_unreachable_online_item_ids ⇒ ActiveRecord::Relation
catalog_item_ids whose most-recent probe in the window could not load the product page at all (request failed / timed out / blocked / Oxylabs job faulted → status 'failed').
-
.failed ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are failed.
-
.in_stock ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are in stock.
-
.latest ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are latest.
-
.latest_probe_ids_in_window ⇒ ActiveRecord::Relation
Probe-row ids of the single most-recent probe per catalog item within the current-state lookback window (Postgres DISTINCT ON).
-
.out_of_stock ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are out of stock.
-
.recent ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are recent.
-
.successful ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are successful.
-
.tab_listing ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are tab listing.
-
.today ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are today.
Instance Method Summary collapse
-
#amazon_probe? ⇒ Boolean
Check if this is an Amazon probe (has buy box tracking).
-
#availability_text ⇒ Object
Availability as text.
-
#buy_box? ⇒ Boolean
Check if this probe detected a buy box (Amazon-specific) Returns true only if we explicitly detected a buy box.
-
#in_stock? ⇒ Boolean
Check if product appears to be in stock.
-
#no_buy_box? ⇒ Boolean
Check if this probe explicitly detected NO buy box (Amazon-specific) Returns true only if we explicitly detected no buy box (not just nil/unknown).
-
#out_of_stock? ⇒ Boolean
Check if product is definitely out of stock.
-
#price_captured? ⇒ Boolean
Check if this represents a successful price capture.
-
#summary ⇒ Object
Human-readable summary.
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Models::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#currency ⇒ String?
Returns ISO currency code of the captured price.
Validations:
- Length ({ maximum: 3 })
88 |
# File 'app/models/catalog_item_retailer_probe.rb', line 88 validates :status, presence: true, inclusion: { in: STATUSES.values } |
#geo_location ⇒ String?
Returns Geographic location the probe ran from.
Validations:
- Length ({ maximum: GEO_LOCATION_MAX_LENGTH })
88 |
# File 'app/models/catalog_item_retailer_probe.rb', line 88 validates :status, presence: true, inclusion: { in: STATUSES.values } |
#status ⇒ String
Returns Probe status.
88 |
# File 'app/models/catalog_item_retailer_probe.rb', line 88 validates :status, presence: true, inclusion: { in: STATUSES.values } |
Class Method Details
.actively_probed ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are actively probed. Active Record Scope
121 122 123 124 125 126 |
# File 'app/models/catalog_item_retailer_probe.rb', line 121 scope :actively_probed, lambda { where(catalog_item_id: CatalogItem.where(skip_url_checks: false) .joins(:catalog) .where(catalogs: { external_price_check_enabled: true }) .select(:id)) } |
.current_out_of_stock_online_item_ids ⇒ ActiveRecord::Relation
catalog_item_ids whose most-recent probe in the window reported the item out
of stock at the retailer.
Raw retailer state — it says nothing about whether that is a PROBLEM. The
retailer being out of stock when we are too is expected; only disagreement
with our own inventory is actionable. Callers add that comparison:
ListingIssues::RetailerProbeAdapter#stock_mismatch_ids intersects with
CatalogItem.in_stock, and the dashboard's out_of_stock_online column /
ViewProductCatalog.latest_probe_out_of_stock_online exclude
product_stock_status = 'OutOfStock'.
186 187 188 |
# File 'app/models/catalog_item_retailer_probe.rb', line 186 def self.current_out_of_stock_online_item_ids where(id: latest_probe_ids_in_window).out_of_stock.select(:catalog_item_id) end |
.current_scrape_failure_item_ids ⇒ ActiveRecord::Relation
catalog_item_ids whose most-recent probe in the window loaded the page but
could not yield a valid product/price — a scrape failure (product not on
page / 404-on-page / product mismatch). Distinct from the page being
unreachable.
Includes auto-skipped items, because "the page loaded and our product wasn't
on it" is evidence about the LISTING that stays true after we stop probing —
ListingIssues::RetailerProbeAdapter wants that. Chain .actively_probed for
the scraper-health reading the dashboard's scrape_failures column shows.
157 158 159 160 161 |
# File 'app/models/catalog_item_retailer_probe.rb', line 157 def self.current_scrape_failure_item_ids where(id: latest_probe_ids_in_window) .where(status: %w[not_found product_mismatch]) .select(:catalog_item_id) end |
.current_unreachable_online_item_ids ⇒ ActiveRecord::Relation
catalog_item_ids whose most-recent probe in the window could not load the
product page at all (request failed / timed out / blocked / Oxylabs job
faulted → status 'failed'). Mirrors the unreachable_online column. Uses
status rather than the page_accessible flag, which the async batch path
historically left at its FALSE default even on success.
170 171 172 |
# File 'app/models/catalog_item_retailer_probe.rb', line 170 def self.current_unreachable_online_item_ids where(id: latest_probe_ids_in_window).where(status: 'failed').select(:catalog_item_id) end |
.failed ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are failed. Active Record Scope
93 |
# File 'app/models/catalog_item_retailer_probe.rb', line 93 scope :failed, -> { where(status: %w[failed not_found product_mismatch]) } |
.in_stock ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are in stock. Active Record Scope
97 |
# File 'app/models/catalog_item_retailer_probe.rb', line 97 scope :in_stock, -> { where(product_available: true) } |
.latest ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are latest. Active Record Scope
95 |
# File 'app/models/catalog_item_retailer_probe.rb', line 95 scope :latest, -> { order(created_at: :desc).first } |
.latest_probe_ids_in_window ⇒ ActiveRecord::Relation
Probe-row ids of the single most-recent probe per catalog item within the
current-state lookback window (Postgres DISTINCT ON). Shaped as a relation so
it can drive where(id: …) subqueries that evaluate each item's latest state.
140 141 142 143 144 |
# File 'app/models/catalog_item_retailer_probe.rb', line 140 def self.latest_probe_ids_in_window where(created_at: CURRENT_STATE_LOOKBACK_DAYS.days.ago..) .select('DISTINCT ON (catalog_item_id) id') .order(:catalog_item_id, created_at: :desc) end |
.out_of_stock ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are out of stock. Active Record Scope
98 |
# File 'app/models/catalog_item_retailer_probe.rb', line 98 scope :out_of_stock, -> { where(product_available: false) } |
.recent ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are recent. Active Record Scope
94 |
# File 'app/models/catalog_item_retailer_probe.rb', line 94 scope :recent, -> { where(created_at: 7.days.ago..) } |
.successful ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are successful. Active Record Scope
92 |
# File 'app/models/catalog_item_retailer_probe.rb', line 92 scope :successful, -> { where(status: 'success') } |
.tab_listing ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are tab listing. Active Record Scope
133 |
# File 'app/models/catalog_item_retailer_probe.rb', line 133 scope :tab_listing, -> { order(created_at: :desc).limit(TAB_LISTING_LIMIT) } |
.today ⇒ ActiveRecord::Relation<CatalogItemRetailerProbe>
A relation of CatalogItemRetailerProbes that are today. Active Record Scope
96 |
# File 'app/models/catalog_item_retailer_probe.rb', line 96 scope :today, -> { where(created_at: Time.current.beginning_of_day..) } |
Instance Method Details
#amazon_probe? ⇒ Boolean
Check if this is an Amazon probe (has buy box tracking)
237 238 239 |
# File 'app/models/catalog_item_retailer_probe.rb', line 237 def amazon_probe? has_buy_box != nil end |
#availability_text ⇒ Object
Availability as text
264 265 266 267 268 269 270 271 272 |
# File 'app/models/catalog_item_retailer_probe.rb', line 264 def availability_text if product_available.nil? 'Unknown' elsif product_available 'In Stock' else 'Out of Stock' end end |
#buy_box? ⇒ Boolean
Check if this probe detected a buy box (Amazon-specific)
Returns true only if we explicitly detected a buy box
226 227 228 |
# File 'app/models/catalog_item_retailer_probe.rb', line 226 def buy_box? has_buy_box == true end |
#catalog_item ⇒ CatalogItem
40 |
# File 'app/models/catalog_item_retailer_probe.rb', line 40 belongs_to :catalog_item |
#in_stock? ⇒ Boolean
Check if product appears to be in stock
215 216 217 |
# File 'app/models/catalog_item_retailer_probe.rb', line 215 def in_stock? product_available == true end |
#no_buy_box? ⇒ Boolean
Check if this probe explicitly detected NO buy box (Amazon-specific)
Returns true only if we explicitly detected no buy box (not just nil/unknown)
232 233 234 |
# File 'app/models/catalog_item_retailer_probe.rb', line 232 def no_buy_box? has_buy_box == false end |
#out_of_stock? ⇒ Boolean
Check if product is definitely out of stock
220 221 222 |
# File 'app/models/catalog_item_retailer_probe.rb', line 220 def out_of_stock? product_available == false end |
#price_captured? ⇒ Boolean
Check if this represents a successful price capture
210 211 212 |
# File 'app/models/catalog_item_retailer_probe.rb', line 210 def price_captured? status == 'success' && price.present? end |
#summary ⇒ Object
Human-readable summary
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'app/models/catalog_item_retailer_probe.rb', line 242 def summary case status when 'success' parts = [] if no_buy_box? parts << 'No Buy Box' elsif price.present? parts << format_price parts << "Seller: #{buy_box_seller}" if buy_box_seller.present? end parts << availability_text if product_available.present? parts.join(' | ') when 'failed' "Failed: #{&.truncate(50)}" when 'not_found' 'Product not found' else 'Pending' end end |