Class: Feed::OpenaiCommerce::ProductPresenter
- Inherits:
-
Feed::OpenaiAds::ProductPresenter
- Object
- Feed::OpenaiAds::ProductPresenter
- Feed::OpenaiCommerce::ProductPresenter
- 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
-
#review_entries_json ⇒ Object
Serialized
reviewslist injected by the generator (top product-line reviews, per the spec's{title, content, minRating, maxRating, rating}entries).
Instance Method Summary collapse
-
#dimensions_in ⇒ Object
dimensionsin the spec'sLxWxH unitform; nil unless all three exist. -
#eligible_search? ⇒ Boolean
is_eligible_search— same gate as ads eligibility (image_linkis a Required field in the commerce spec, so the imagery check stays in). -
#image_link ⇒ String?
The commerce spec validates
image_urlas JPEG/PNG only, while the inherited Googleimage_linkemits WebP — ship a JPEG derivative so rows aren't rejected. -
#item_group_title ⇒ Object
item_group_title— the variant family's display name. -
#item_weight ⇒ Object
weightas a bare number (the spec pairs it withitem_weight_unit). -
#item_weight_unit ⇒ Object
Emitted only alongside a weight, so a weightless row leaves both cells blank.
-
#offer_id ⇒ Object
offer_id— SKU + country keeps it unique across the US/CA catalog rows (the bare SKU appears once per catalog). -
#product_category_path ⇒ Object
Category taxonomy path in the spec's
>-separated form. - #sale_end_date ⇒ Object
-
#sale_start_date ⇒ Object
ISO dates for
sale_price_start_date/sale_price_end_date; blank unless a sale is in effect. -
#variant_dict ⇒ Object
variant_dictas 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. -
#video_url ⇒ String?
First product video as a publicly fetchable MP4 (the Cloudflare Stream download URL kept for SEO/fallback), backing the spec's
video_url.
Instance Attribute Details
#review_entries_json ⇒ Object
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_in ⇒ Object
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).
20 21 22 |
# File 'app/services/feed/openai_commerce/product_presenter.rb', line 20 def eligible_search? ads_eligible? end |
#image_link ⇒ String?
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.)
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_title ⇒ Object
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_weight ⇒ Object
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_unit ⇒ Object
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_id ⇒ Object
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_path ⇒ Object
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&. end |
#sale_end_date ⇒ Object
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_date ⇒ Object
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_dict ⇒ Object
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_url ⇒ String?
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.
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 |