Class: Feed::OpenaiAds::CatalogFeedGenerator

Inherits:
BaseService
  • Object
show all
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.process(output_file_path: path)

Defined Under Namespace

Classes: Result

Constant Summary collapse

COLUMNS =

Ordered CSV columns. GMC-standard attribute names (OpenAI's ads feed is
GMC-shaped) plus OpenAI's canonical is_ads_eligible flag. 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? }
}.freeze

Instance Method Summary collapse

Instance Method Details

#build_csv(presenters) ⇒ String

Returns CSV with a header row + one row per presenter.

Parameters:

Returns:

  • (String)

    CSV with a header row + one row per presenter.



85
86
87
88
89
90
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 85

def build_csv(presenters)
  CSV.generate do |csv|
    csv << COLUMNS.keys
    presenters.each { |presenter| csv << row_for(presenter) }
  end
end

#load_products(catalog, limit: nil) ⇒ Object

Catalog-item presenters for one catalog, in the catalog's locale, restricted
to products that belong in the feed file.



72
73
74
75
76
77
78
79
80
81
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 72

def load_products(catalog, limit: nil)
  I18n.with_locale(catalog.locale_for_catalog) do
    scope = catalog.view_product_catalogs
                   .includes(:catalog_item, :item)
                   .joins(:catalog_item)
                   .merge(CatalogItem.for_google_feed)
    scope = scope.limit(limit) if limit.present?
    scope.map { |ci| Feed::OpenaiAds::ProductPresenter.new(ci) }.select(&:feed_includable?)
  end
end

#process(catalogs: nil, output_file_path: nil, limit: nil) ⇒ Result

Parameters:

  • catalogs (ActiveRecord::Relation<Catalog>, nil) (defaults to: nil)

    defaults to the main
    (US + CA) catalogs, matching the Google Shopping feed scope.

  • output_file_path (String, Pathname, nil) (defaults to: nil)

    when present, the CSV is
    written here (UTF-8).

  • limit (Integer, nil) (defaults to: nil)

    cap products per catalog (dev/debug).

Returns:



57
58
59
60
61
62
63
64
65
66
67
68
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 57

def process(catalogs: nil, output_file_path: nil, limit: nil)
  catalogs ||= Catalog.for_google_feed
  presenters = catalogs.flat_map { |catalog| load_products(catalog, limit:) }
  csv = build_csv(presenters)

  if output_file_path
    logger.info "Writing OpenAI Ads product feed (#{presenters.size} 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: presenters.size)
end

#row_for(presenter) ⇒ Array

Returns one CSV row aligned to COLUMNS.

Returns:

  • (Array)

    one CSV row aligned to COLUMNS.



93
94
95
# File 'app/services/feed/openai_ads/catalog_feed_generator.rb', line 93

def row_for(presenter)
  COLUMNS.values.map { |extractor| format_value(extractor.call(presenter)) }
end