Class: Oembed::ProductProvider

Inherits:
Object
  • Object
show all
Defined in:
app/services/oembed/product_provider.rb

Overview

Service object: product provider.

Defined Under Namespace

Classes: ProductNotFoundError, ProductUnavailableError

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.curl_typographic_quotes(text) ⇒ String

Convert straight ASCII quotes to curly typographic quotes per the
project's curly-quote convention
(https://typographyforlawyers.com/straight-and-curly-quotes.html).

Kept as a thin shim around Heatwave::TypographicQuotes.curl_plain
so existing call sites continue to work; new callers should use
the module method directly.

Parameters:

  • text (String)

    Plain-text input with straight " / ' quotes.

Returns:

  • (String)

    Same text with opening quotes (start of string or
    after whitespace/open-bracket) converted to / and remaining
    quotes to /.



320
321
322
# File 'app/services/oembed/product_provider.rb', line 320

def self.curl_typographic_quotes(text)
  Heatwave::TypographicQuotes.curl_plain(text)
end

.sanitize_schema_text(text) ⇒ String?

Prepare description text for embedding in a JSON-LD <script> tag.

Two concerns:

  1. Word boundaries — replace block-level tags with spaces before
    stripping so <p>foo</p><p>bar</p> becomes foo bar, not
    foobar.
  2. Quote safety — convert straight ASCII quotes to curly typographic
    quotes via Heatwave::TypographicQuotes.curl_plain. Even with
    to_json escaping straight " as \", any tool that round-trips
    the JSON-LD as HTML text (Redactor, Nokogiri-based refreshers, etc.)
    can unescape \" back to ", leaving invalid JSON inside the
    script tag. Curly quotes are different code points entirely and
    survive every round-trip intact.

Idempotent: re-running on already-sanitized text yields the same
output (curly quotes pass through, block tags are already gone).

Parameters:

  • text (String, nil)

    Raw description text — may include HTML
    tags, HTML entities, and straight quotes.

Returns:

  • (String, nil)

    Plain-text description with words spaced,
    entities decoded, whitespace squished, and quotes curled. Returns
    the input unchanged when blank.



299
300
301
302
303
304
305
306
# File 'app/services/oembed/product_provider.rb', line 299

def self.sanitize_schema_text(text)
  return text if text.blank?

  spaced = text.to_s.gsub(BLOCK_TAG_REGEX, ' ')
  sanitized = ActionView::Base.full_sanitizer.sanitize(spaced)
  decoded = CGI.unescapeHTML(sanitized).squish
  Heatwave::TypographicQuotes.curl_plain(decoded)
end

Instance Method Details

#get(options = {}) ⇒ Hash

Get rendered product HTML

Parameters:

  • options (Hash) (defaults to: {})

    Options hash

Options Hash (options):

  • :sku (String)

    Product SKU (required)

  • :locale (String)

    Locale for pricing ('en-US' or 'en-CA')

  • :embedded_asset_uuid (String)

    EmbeddedAsset UUID for tracking (optional)

Returns:

  • (Hash)

    oEmbed-like response with :html, :type, :provider_name, etc.

Raises:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'app/services/oembed/product_provider.rb', line 40

def get(options = {})
  sku = options[:sku].to_s.strip
  locale = options[:locale].to_s.presence || 'en-US'
  embedded_asset_uuid = options[:embedded_asset_uuid].presence

  raise ProductNotFoundError, 'No SKU provided' if sku.blank?

  # Try exact match first, then case-insensitive
  item = Item.find_by(sku: sku) || Item.where('UPPER(sku) = ?', sku.upcase).first
  raise ProductNotFoundError, "Product not found: #{sku}" if item.nil?

  # For CRM editor preview, show the product with locale-aware pricing
  # The frontend rendering will handle final availability checks
  build_response(item, sku, locale, embedded_asset_uuid: embedded_asset_uuid)
end

#render_for_locale(sku, locale = 'en-US', include_schema: true) ⇒ String?

Render product embed HTML for the frontend (blog display)
This is called during content refresh and normal rendering
Handles locale-aware pricing and availability checks

Parameters:

  • sku (String)

    Product SKU

  • locale (String, Symbol) (defaults to: 'en-US')

    Locale ('en-US' or 'en-CA')

  • include_schema (Boolean) (defaults to: true)

    Whether to include JSON-LD schema (default: true)

Returns:

  • (String, nil)

    HTML string or nil if product unavailable



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'app/services/oembed/product_provider.rb', line 64

def render_for_locale(sku, locale = 'en-US', include_schema: true)
  sku_stripped = sku.to_s.strip
  item = Item.find_by(sku: sku_stripped) || Item.where('UPPER(sku) = ?', sku_stripped.upcase).first
  return nil if item.nil?

  catalog_id = catalog_id_for_locale(locale)
  catalog_item = item.catalog_items.find_by(catalog_id: catalog_id)

  # Check availability - return nil if product shouldn't be displayed
  return nil unless product_available?(item, catalog_item)

  # Render the product card
  card_html = render_product_card(item, catalog_item, locale)
  return nil if card_html.nil?

  # Optionally include JSON-LD schema for SEO
  if include_schema
    schema_html = render_product_schema(item, catalog_item, locale)
    "#{card_html}#{schema_html}"
  else
    card_html
  end
end