Class: Retailer::DailyComplianceReport
- Inherits:
-
Object
- Object
- Retailer::DailyComplianceReport
- Defined in:
- app/services/retailer/daily_compliance_report.rb
Overview
Per-retailer compliance digest emailed daily after the retailer probe runs
(see RetailerComplianceReportWorker + the cron in
config/sidekiq_production_schedule.yml).
Scope: catalogs the probe checks (external_price_check_enabled = true), the
Amazon Seller catalogs (US / Canada / Europe), and any catalog with an open
listing_issues row. One row per catalog, grouped by region (US / Canada /
Europe), each column a count of active catalog items in a problem state,
sourced from:
- view_product_catalogs — map_violation, sale_price_in_effect,
price_diverging, product_stock_status (our inventory) - listing_issues — open marketplace-reported listing problems
(Amazon suppression / SP-API issues, Walmart unpublished reasons, …);
this is the column that covers the (unprobed) Amazon catalogs. - the latest CatalogItemRetailerProbe per item (last 14 days) — status:
'not_found'/'product_mismatch' (scrape failures), 'failed' (unreachable
online), product_available = FALSE (out of stock online). Amazon
catalogs are not Oxylabs-probed, so the probe columns read 0 for them.
MAP-violation counts depend on view_product_catalogs resolving MSRP from each
catalog's tree root (fixed in view v54) — before that, all CA/EU catalogs
reported zero.
Constant Summary collapse
- TZ =
America/Chicago — the server/report timezone.
'America/Chicago'- PROBE_LOOKBACK_DAYS =
Only consider probes from this lookback window as the item's current state.
Shared with CatalogItemRetailerProbe's latest-probe scopes (which back the
deep links below) so the counts and the linked lists use one window. CatalogItemRetailerProbe::CURRENT_STATE_LOOKBACK_DAYS
- REGIONS =
Root catalog id => region label / display order. Roots: US=1, CA=2, EU=125.
{ 1 => { label: 'United States', order: 0 }, 2 => { label: 'Canada', order: 1 }, 125 => { label: 'Europe', order: 2 } }.freeze
- OTHER_REGION =
Fallback bucket for any catalog not rooted at a known region.
{ label: 'Other', order: 99 }.freeze
- COUNT_COLUMNS =
Numeric metric columns, in display order. Two distinct "things are wrong
with the listing" signals are kept separate on purpose:- listing_issues — the marketplace reports a problem with our listing
(Amazon suppression / SP-API issues, Walmart unpublished reasons, …),
sourced from the listing_issues table (ListingIssues::Sync). - scrape_failures — our Oxylabs probe loaded the page but couldn't read
a valid product/price (not_found / product_mismatch). A scraper-health
metric, not a retailer listing defect.
- listing_issues — the marketplace reports a problem with our listing
%i[ active_public map_violations promotions price_diverging out_of_stock_active listing_issues scrape_failures unreachable_online out_of_stock_online ].freeze
- CLICKABLE_QUERY_PARAMS =
Every metric here maps to a ProductCatalogSearch query so its email count
links straight to the filtered CRM list. View-derived columns use Ransack
attribute predicates; the probe-derived columns (scrape failures /
unreachable / out of stock online) use the latest-probe Ransack scopes on
ViewProductCatalog, which reuse this report's lookback window so the linked
list reproduces the count. Each lambda takes a catalog id and returns the
ProductCatalogSearch query_params. (listing_issues links to the dedicated
dashboard instead — see .listing_issues_url.) { active_public: ->(cid) { { catalog_id_in: [cid], catalog_item_state_in: ['active'] } }, map_violations: lambda { |cid| { catalog_id_in: [cid], catalog_item_state_in: ['active'], map_violation_eq: true } }, promotions: lambda { |cid| { catalog_id_in: [cid], catalog_item_state_in: ['active'], sale_price_in_effect_eq: true } }, price_diverging: lambda { |cid| { catalog_id_in: [cid], catalog_item_state_in: ['active'], price_diverging_eq: true } }, out_of_stock_active: lambda { |cid| { catalog_id_in: [cid], catalog_item_state_in: ['active'], product_stock_status_eq: 'OutOfStock' } }, # listing_issues links to the CRM Listing Issues dashboard (review + # resolve), not a ProductCatalogSearch — handled in the email view, so it # is intentionally absent here. scrape_failures: lambda { |cid| { catalog_id_in: [cid], catalog_item_state_in: ['active'], latest_probe_scrape_failure: true } }, unreachable_online: lambda { |cid| { catalog_id_in: [cid], catalog_item_state_in: ['active'], latest_probe_unreachable_online: true } }, out_of_stock_online: lambda { |cid| { catalog_id_in: [cid], catalog_item_state_in: ['active'], latest_probe_out_of_stock_online: true } } }.freeze
Instance Attribute Summary collapse
-
#generated_at ⇒ ActiveSupport::TimeWithZone
readonly
When the report was generated.
Delegated Instance Attributes collapse
-
#empty? ⇒ Object
Alias for Rows#empty?.
Class Method Summary collapse
-
.for_scheduled_run ⇒ Retailer::DailyComplianceReport
Entry point for the scheduled worker.
-
.listing_issues_url(catalog_id, host) ⇒ String
CRM Listing Issues dashboard URL for a catalog's open issues — the "Listing Issues" column links here (review + mark-fixed) rather than to a ProductCatalogSearch list.
-
.query_params_for(metric, catalog_id) ⇒ Hash?
ProductCatalogSearch query_params for a clickable metric, or nil.
Instance Method Summary collapse
-
#flagged_rows ⇒ Array<Hash>
Retailers carrying at least one MAP violation or marketplace listing issue — the rows worth eyeballing first.
-
#initialize(generated_at: nil) ⇒ DailyComplianceReport
constructor
A new instance of DailyComplianceReport.
-
#rows ⇒ Array<Hash>
One hash per probed/Amazon catalog with its compliance counts and region.
-
#rows_by_region ⇒ Array<Array(String, Array<Hash>)>
Rows grouped by region label, in display order (US, Canada, Europe, …).
-
#totals ⇒ Hash
Column-wise totals across every retailer.
-
#totals_for(region_rows) ⇒ Hash
Column-wise totals for one region's rows.
Constructor Details
#initialize(generated_at: nil) ⇒ DailyComplianceReport
Returns a new instance of DailyComplianceReport.
113 114 115 |
# File 'app/services/retailer/daily_compliance_report.rb', line 113 def initialize(generated_at: nil) @generated_at = generated_at || ActiveSupport::TimeZone[TZ].now end |
Instance Attribute Details
#generated_at ⇒ ActiveSupport::TimeWithZone (readonly)
Returns when the report was generated.
111 112 113 |
# File 'app/services/retailer/daily_compliance_report.rb', line 111 def generated_at @generated_at end |
Class Method Details
.for_scheduled_run ⇒ Retailer::DailyComplianceReport
Entry point for the scheduled worker.
119 120 121 |
# File 'app/services/retailer/daily_compliance_report.rb', line 119 def self.for_scheduled_run new end |
.listing_issues_url(catalog_id, host) ⇒ String
CRM Listing Issues dashboard URL for a catalog's open issues — the
"Listing Issues" column links here (review + mark-fixed) rather than to a
ProductCatalogSearch list.
105 106 107 108 |
# File 'app/services/retailer/daily_compliance_report.rb', line 105 def self.listing_issues_url(catalog_id, host) query = { catalog_id:, status: 'open' }.to_query "https://#{host}/listing_issues?#{query}" end |
.query_params_for(metric, catalog_id) ⇒ Hash?
ProductCatalogSearch query_params for a clickable metric, or nil.
127 128 129 |
# File 'app/services/retailer/daily_compliance_report.rb', line 127 def self.query_params_for(metric, catalog_id) CLICKABLE_QUERY_PARAMS[metric]&.call(catalog_id) end |
Instance Method Details
#empty? ⇒ Object
Alias for Rows#empty?
171 |
# File 'app/services/retailer/daily_compliance_report.rb', line 171 delegate :empty?, to: :rows |
#flagged_rows ⇒ Array<Hash>
Retailers carrying at least one MAP violation or marketplace listing issue
— the rows worth eyeballing first.
166 167 168 |
# File 'app/services/retailer/daily_compliance_report.rb', line 166 def flagged_rows rows.select { |r| r[:map_violations].to_i.positive? || r[:listing_issues].to_i.positive? } end |
#rows ⇒ Array<Hash>
One hash per probed/Amazon catalog with its compliance counts and region.
133 134 135 136 137 138 139 140 |
# File 'app/services/retailer/daily_compliance_report.rb', line 133 def rows @rows ||= ActiveRecord::Base .connection .select_all(report_sql) .to_a .map(&:symbolize_keys) .map { |r| decorate(r) } end |
#rows_by_region ⇒ Array<Array(String, Array<Hash>)>
Rows grouped by region label, in display order (US, Canada, Europe, …).
144 145 146 147 |
# File 'app/services/retailer/daily_compliance_report.rb', line 144 def rows_by_region rows.group_by { |r| r[:region_label] } .sort_by { |label, _| region_order_for_label(label) } end |
#totals ⇒ Hash
Column-wise totals across every retailer.
151 152 153 154 155 |
# File 'app/services/retailer/daily_compliance_report.rb', line 151 def totals @totals ||= COUNT_COLUMNS.index_with do |col| rows.sum { |r| r[col].to_i } end end |
#totals_for(region_rows) ⇒ Hash
Column-wise totals for one region's rows.
159 160 161 |
# File 'app/services/retailer/daily_compliance_report.rb', line 159 def totals_for(region_rows) COUNT_COLUMNS.index_with { |col| region_rows.sum { |r| r[col].to_i } } end |