Class: Feed::OpenaiAds::CatalogFeedGenerator
- Inherits:
-
BaseService
- Object
- BaseService
- Feed::OpenaiAds::CatalogFeedGenerator
- Defined in:
- app/services/feed/openai_ads/catalog_feed_generator.rb
Overview
Builds the OpenAI Ads product feed — a Google-Merchant-Center-compatible CSV
of our sellable catalog — and optionally writes it to a file for delivery to
OpenAI (uploaded in the Ads Manager Feeds area, or pushed to the SFTP location
OpenAI provisions there).
Mirrors Google::ListGenerator (same catalog scope + presenter
pipeline), but emits a single CSV document rather than caching per-item XML:
OpenAI ingests an uploaded/pushed catalog file, not a hosted feed URL, so
there's no per-request assembly to optimize and no cache table to maintain.
Usage:
Feed::OpenaiAds::CatalogFeedGenerator.new.call(output_file_path: path)
Direct Known Subclasses
Defined Under Namespace
Classes: Result
Constant Summary collapse
- BATCH_SIZE =
Feed::Google::ProductBatchLoader::BATCH_SIZE
- COLUMNS =
Ordered CSV columns. GMC-standard attribute names (OpenAI's ads feed is
GMC-shaped) plus OpenAI's canonicalis_ads_eligibleflag. Each value is a
lambda over the ProductPresenter. { 'id' => ->(p) { p.id }, 'title' => ->(p) { p.title }, 'description' => ->(p) { p.description }, 'link' => ->(p) { p.url }, 'image_link' => ->(p) { p.image_link }, 'additional_image_link' => ->(p) { p.additional_image_link }, 'availability' => ->(p) { p.availability }, 'price' => ->(p) { p.price_with_currency }, 'sale_price' => ->(p) { p.sale_price_with_currency if p.sale_price_in_effect? }, 'brand' => ->(p) { p.brand }, 'gtin' => ->(p) { p.upc }, 'mpn' => ->(p) { p.mpn }, 'condition' => ->(p) { p.condition }, 'product_type' => ->(p) { p.product_type }, 'google_product_category' => ->(p) { p.google_product_category }, 'item_group_id' => ->(p) { p.item_group_name }, 'color' => ->(p) { p.color_info }, 'size' => ->(p) { p.size_info }, 'shipping_weight' => ->(p) { p.shipping_weight }, 'is_ads_eligible' => ->(p) { p.ads_eligible? }, 'seller_name' => ->(_p) { 'WarmlyYours' }, # required merchant identity field (OpenAI case #10328641) # "Reviews and Q&A" fields powering the refreshed product card's star # rating. Product-line aggregates per row; store aggregates uniform. 'review_count' => ->(p) { p.review_count }, 'star_rating' => ->(p) { p. }, 'store_review_count' => ->(p) { p.store_review_count }, 'store_star_rating' => ->(p) { p. } }.freeze
Instance Method Summary collapse
-
#assign_review_stats(presenters, store_stats: ReviewsIo.store_review_stats) ⇒ void
Batch-loads Reviews.io aggregates and injects them into each presenter: product-line stats (keyed by line id, from ReviewsIo.stats_for_product_lines) for the per-item
review_count/star_ratingcells, and one store-wide stats hash for the uniformstore_review_count/store_star_ratingcells. -
#build_csv(presenters) ⇒ String
self.class::COLUMNS(not the lexical constant) so subclasses like Feed::OpenaiCommerce::CatalogFeedGenerator get their own column set. - #call(catalogs: nil, output_file_path: nil, limit: nil) ⇒ Result
-
#each_product(catalog, limit: nil, batch_size: BATCH_SIZE, store_stats: ReviewsIo.store_review_stats) {|presenter| ... } ⇒ Enumerator, void
Yields one bounded batch of presenters at a time.
-
#load_products(catalog, limit: nil) ⇒ Array<Feed::OpenaiAds::ProductPresenter>
Catalog-item presenters for one catalog, in the catalog's locale, restricted to products that belong in the feed file.
-
#presenter_class ⇒ Class
Presenter class built per catalog item.
-
#row_for(presenter) ⇒ Array
One CSV row aligned to COLUMNS.
Instance Method Details
#assign_review_stats(presenters, store_stats: ReviewsIo.store_review_stats) ⇒ void
This method returns an undefined value.
Batch-loads Reviews.io aggregates and injects them into each presenter:
product-line stats (keyed by line id, from
ReviewsIo.stats_for_product_lines) for the per-item review_count /
star_rating cells, and one store-wide stats hash for the uniform
store_review_count / store_star_rating cells. One grouped query per
bounded batch instead of per-row lookups; store stats are loaded once.
142 143 144 145 146 147 148 149 150 |
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 142 def assign_review_stats(presenters, store_stats: ReviewsIo.store_review_stats) product_lines = presenters.filter_map { |p| p.item.primary_product_line }.uniq line_stats = ReviewsIo.stats_for_product_lines(product_lines) presenters.each do |presenter| presenter.review_stats = line_stats[presenter.item.primary_product_line&.id] presenter.store_review_stats = store_stats end end |
#build_csv(presenters) ⇒ String
self.class::COLUMNS (not the lexical constant) so subclasses like
Feed::OpenaiCommerce::CatalogFeedGenerator get their own column set.
156 157 158 159 160 161 |
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 156 def build_csv(presenters) CSV.generate do |csv| csv << self.class::COLUMNS.keys presenters.each { |presenter| csv << row_for(presenter) } end end |
#call(catalogs: nil, output_file_path: nil, limit: nil) ⇒ Result
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 69 def call(catalogs: nil, output_file_path: nil, limit: nil) catalogs ||= Catalog.for_google_feed product_count = 0 store_stats = ReviewsIo.store_review_stats csv = CSV.generate do |output| output << self.class::COLUMNS.keys catalogs.each do |catalog| each_product(catalog, limit:, store_stats:) do |presenter| output << row_for(presenter) product_count += 1 end end end if output_file_path logger.info "Writing OpenAI Ads product feed (#{product_count} products) to #{output_file_path}" File.write(output_file_path, csv) end Result.new(csv:, output_file_path: output_file_path&.to_s, product_count:) end |
#each_product(catalog, limit: nil, batch_size: BATCH_SIZE, store_stats: ReviewsIo.store_review_stats) {|presenter| ... } ⇒ Enumerator, void
Yields one bounded batch of presenters at a time. Query caching is disabled
for the traversal so item/image association results do not remain reachable
until the whole Sidekiq job ends.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 114 def each_product(catalog, limit: nil, batch_size: BATCH_SIZE, store_stats: ReviewsIo.store_review_stats, &block) return enum_for(__method__, catalog, limit:, batch_size:, store_stats:) unless block I18n.with_locale(catalog.locale_for_catalog) do Feed::Google::ProductBatchLoader.new.each_batch(catalog, limit:, batch_size:) do |batch| presenters = batch.records.map do |record| presenter_class.new( record, nil, country_iso: batch.country_iso, variant_metadata: batch.[record.item_id] ) end.select(&:feed_includable?) assign_review_stats(presenters, store_stats:) presenters.each(&block) end end end |
#load_products(catalog, limit: nil) ⇒ Array<Feed::OpenaiAds::ProductPresenter>
Catalog-item presenters for one catalog, in the catalog's locale, restricted
to products that belong in the feed file.
101 102 103 |
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 101 def load_products(catalog, limit: nil) each_product(catalog, limit:).to_a end |
#presenter_class ⇒ Class
Presenter class built per catalog item. Feed::OpenaiCommerce::CatalogFeedGenerator
overrides this to reuse the whole loading/scoping pipeline.
94 |
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 94 def presenter_class = Feed::OpenaiAds::ProductPresenter |
#row_for(presenter) ⇒ Array
Returns one CSV row aligned to COLUMNS.
165 166 167 |
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 165 def row_for(presenter) self.class::COLUMNS.values.map { |extractor| format_value(extractor.call(presenter)) } end |