Class: Feed::OpenaiCommerce::ProductPresenter

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

Overview

Presents a catalog item as a row in OpenAI's non-Ads Commerce product feed
(https://developers.openai.com/commerce/specs/feed) — the discovery feed that
keeps products surfacing in ChatGPT search with accurate price, availability,
and ratings. Instant Checkout was sunset in March 2026, so
is_eligible_checkout ships false and the checkout-only merchant fields
(seller_tos / seller_privacy_policy) are intentionally omitted.

Reuses Feed::OpenaiAds::ProductPresenter wholesale — same catalog mapping,
same review aggregates — and adds only the discovery-specific fields. Column
names follow the commerce spec (item_id / url), not the GMC-shaped Ads
feed (id / link).

Constant Summary collapse

SELLER_URL =
'https://www.warmlyyours.com'
RETURN_POLICY_URL =
'https://www.warmlyyours.com/en-US/company/return-policy'

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#review_entries_jsonObject

Serialized reviews list injected by the generator (top product-line
reviews, per the spec's {title, content, minRating, maxRating, rating}
entries). Nil means "no reviews" → blank cell.



72
73
74
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 72

def review_entries_json
  @review_entries_json
end

Instance Method Details

#dimensions_inObject

dimensions in the spec's LxWxH unit form; nil unless all three exist.



80
81
82
83
84
85
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 80

def dimensions_in
  dims = [shipping_length_in, shipping_width_in, shipping_height_in]
  return unless dims.all?

  "#{dims.map(&:ceil).join('x')} in"
end

#eligible_search?Boolean

is_eligible_search — same gate as ads eligibility (image_link is a
Required field in the commerce spec, so the imagery check stays in).

Returns:

  • (Boolean)


20
21
22
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 20

def eligible_search?
  ads_eligible?
end

The commerce spec validates image_url as JPEG/PNG only, while the
inherited Google image_link emits WebP — ship a JPEG derivative so rows
aren't rejected. (additional_image_urls has no format constraint.)

Returns:

  • (String, nil)


38
39
40
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 38

def image_link
  all_images.first&.image_url(encode_format: :jpeg, width: 1280)
end

#item_group_titleObject

item_group_title — the variant family's display name.



59
60
61
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 59

def item_group_title
  item.variant_group&.name if has_variants?
end

#item_weightObject

weight as a bare number (the spec pairs it with item_weight_unit).



30
31
32
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 30

def item_weight
  shipping_weight_lbs
end

#item_weight_unitObject

Emitted only alongside a weight, so a weightless row leaves both cells blank.



75
76
77
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 75

def item_weight_unit
  'lb' if shipping_weight_lbs
end

#offer_idObject

offer_id — SKU + country keeps it unique across the US/CA catalog rows
(the bare SKU appears once per catalog).



65
66
67
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 65

def offer_id
  [item_sku, ships_from_country_iso].join('-')
end

#product_category_pathObject

Category taxonomy path in the spec's >-separated form.



25
26
27
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 25

def product_category_path
  item.product_category&.lineage_expanded
end

#sale_end_dateObject



94
95
96
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 94

def sale_end_date
  sale_price_expiration_date || Date.current.end_of_month if sale_price_in_effect?
end

#sale_start_dateObject

ISO dates for sale_price_start_date / sale_price_end_date; blank unless
a sale is in effect. OpenAI wants a real end date, so fall back to
end-of-month like Google::GoogleProductPresenter#sale_price_date_range.



90
91
92
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 90

def sale_start_date
  sale_price_effective_date if sale_price_in_effect?
end

#variant_dictObject

variant_dict as a JSON object of the family's axis tokens to this item's
spec values (e.g. {"voltage":"120V","size":"3 sq ft"}), per the spec.
Nil for ungrouped items and for groups with no axis tokens (an empty {}
is junk in the feed).



54
55
56
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 54

def variant_dict
  item.variant_group&.axis_values_for(item).presence&.to_json if has_variants?
end

#video_urlString?

First product video as a publicly fetchable MP4 (the Cloudflare Stream
download URL kept for SEO/fallback), backing the spec's video_url.
Nil when the item has no videos.

Returns:

  • (String, nil)


46
47
48
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 46

def video_url
  videos.first&.then { |video| Www::VideoPresenter.new(video).cloudflare_mp4_url }
end