Class: Feed::OpenaiCommerce::CatalogFeedGenerator

Inherits:
Feed::OpenaiAds::CatalogFeedGenerator
  • Object
show all
Defined in:
app/services/feed/openai_commerce/catalog_feed_generator.rb

Overview

Builds the OpenAI Commerce (non-Ads) product feed — the discovery feed that
keeps our catalog surfaced in ChatGPT search with accurate pricing,
availability, and ratings — as a single CSV for delivery via SFTP, file
upload, or hosted URL (https://developers.openai.com/commerce/specs).

Subclasses Feed::OpenaiAds::CatalogFeedGenerator: the catalog scoping,
presenter pipeline, and review-stats injection are identical; only the column
set (commerce-spec names, not GMC names) and the presenter class differ.
Instant Checkout was sunset in March 2026, so is_eligible_checkout is
always false and the checkout-only merchant fields are omitted.

Deliberately omitted spec-optional fields:

  • q_and_a — our FAQs are support articles (ArticleFaq), not clean Q/A
    pairs; dumping them risks junk in the feed.
  • shipping — no per-item rate matrix to express country:service:price.
  • return_deadline_in_days — our policy is "no time limits", which the
    positive-integer field cannot express; accepts_returns + return_policy
    carry the signal.
  • model_3d_url, pickup_method/pickup_sla, geo_price/geo_availability,
    popularity_score/return_rate, marketplace_seller, size_system,
    gender/age_group — not applicable to our catalog.

Usage:
Feed::OpenaiCommerce::CatalogFeedGenerator.new.call(output_file_path: path)

Constant Summary collapse

COLUMNS =

Ordered CSV columns from the commerce flat-file spec. Required fields:
the eligibility flags, item_id, title, description, url, brand, image_url,
price, availability, seller_name, seller_url, return_policy,
target_countries, store_country. Everything else enriches discovery.

{
  # OpenAI Flags
  'is_eligible_search'    => ->(p) { p.eligible_search? },
  'is_eligible_checkout'  => ->(_p) { false }, # Instant Checkout sunset 2026-03
  # Basic Product Data
  'item_id'               => ->(p) { p.id },
  'gtin'                  => ->(p) { p.upc },
  'mpn'                   => ->(p) { p.mpn },
  'title'                 => ->(p) { p.title },
  'description'           => ->(p) { p.description },
  'url'                   => ->(p) { p.url },
  # Item Information
  'brand'                 => ->(p) { p.brand },
  'condition'             => ->(p) { p.condition },
  'product_category'      => ->(p) { p.product_category_path },
  'dimensions'            => ->(p) { p.dimensions_in },
  'weight'                => ->(p) { p.item_weight },
  'item_weight_unit'      => ->(p) { p.item_weight_unit },
  # Media
  'image_url'             => ->(p) { p.image_link },
  'additional_image_urls' => ->(p) { p.additional_image_link },
  'video_url'             => ->(p) { p.video_url },
  # Price & Promotions
  'price'                 => ->(p) { p.price_with_currency },
  'sale_price'            => ->(p) { p.sale_price_with_currency if p.sale_price_in_effect? },
  'sale_price_start_date' => ->(p) { p.sale_start_date },
  'sale_price_end_date'   => ->(p) { p.sale_end_date },
  # Availability & Inventory
  'availability'          => ->(p) { p.availability },
  # Variants
  'group_id'              => ->(p) { p.item_group_name },
  'listing_has_variations' => ->(p) { p.has_variants? },
  'variant_dict'          => ->(p) { p.variant_dict },
  'item_group_title'      => ->(p) { p.item_group_title },
  'color'                 => ->(p) { p.color_info },
  'size'                  => ->(p) { p.size_info },
  'offer_id'              => ->(p) { p.offer_id },
  # Fulfillment
  'is_digital'            => ->(_p) { false },
  # Merchant Info + Returns
  'seller_name'           => ->(_p) { 'WarmlyYours' },
  'seller_url'            => ->(_p) { Feed::OpenaiCommerce::ProductPresenter::SELLER_URL },
  'return_policy'         => ->(_p) { Feed::OpenaiCommerce::ProductPresenter::RETURN_POLICY_URL },
  'accepts_returns'       => ->(_p) { true },
  # Geo Tagging
  'target_countries'      => ->(p) { p.ships_from_country_iso },
  'store_country'         => ->(_p) { 'US' },
  # Reviews and Q&A
  'review_count'          => ->(p) { p.review_count },
  'star_rating'           => ->(p) { p.star_rating },
  'store_review_count'    => ->(p) { p.store_review_count },
  'store_star_rating'     => ->(p) { p.store_star_rating },
  'reviews'               => ->(p) { p.review_entries_json }
}.freeze
REVIEW_ENTRIES_PER_LINE =

Number of review entries embedded per product line in the reviews column.

5

Instance Method Summary collapse

Instance Method Details

#assign_review_stats(presenters, store_stats: ReviewsIo.store_review_stats) ⇒ void

This method returns an undefined value.

Adds the spec's reviews list on top of the aggregate stats: top reviews
per product line, memoized per line so variant rows share one lookup.

Parameters:



97
98
99
100
101
102
103
104
105
106
107
108
# File 'app/services/feed/openai_commerce/catalog_feed_generator.rb', line 97

def assign_review_stats(presenters, store_stats: ReviewsIo.store_review_stats)
  super

   = {}
  presenters.each do |presenter|
    product_line = presenter.item.primary_product_line
    next unless product_line

    [product_line.id] ||= fetch_review_entries(product_line)
    presenter.review_entries_json = [product_line.id]
  end
end

#presenter_classObject



90
# File 'app/services/feed/openai_commerce/catalog_feed_generator.rb', line 90

def presenter_class = Feed::OpenaiCommerce::ProductPresenter