Class: Www::ProductCatalogPresenter
- Inherits:
-
BasePresenter
- Object
- BasePresenter
- Www::ProductCatalogPresenter
- Includes:
- ApplicationHelper, Memery, Presenters::ProductHelpers
- Defined in:
- app/presenters/www/product_catalog_presenter.rb
Overview
Presenter for WWW SKU detail pages. Encapsulates pricing, media,
related products, and Schema.org generation for a single catalog item.
Direct Known Subclasses
Defined Under Namespace
Classes: DocRecord
Constant Summary collapse
- CARD_IMAGE_ORDER =
Card image order.
%w[WYS_CARD WYS_MAIN].freeze
- RELATED_PRODUCT_SUBSECTIONS =
Related product subsections.
%i[installation_methods controls control_electrical_rough_in_kits upgrades accessories installation_kits insulating_underlayments].freeze
- LAZY_SECTIONS =
Sections deferred to Turbo Frame lazy-load endpoints for human visitors.
Bots still get full inline rendering (lazy_rendering? is false). %i[installation_plans showcases related_products documents reviews faq terms_and_conditions surcharges].freeze
Constants included from ApplicationHelper
ApplicationHelper::CF_WEB_ANALYTICS_SKIP_PATHS, ApplicationHelper::MARKDOWN_ALLOWED_ATTRIBUTES, ApplicationHelper::MARKDOWN_ALLOWED_TAGS
Constants included from SeoHelper
SeoHelper::AWARDS, SeoHelper::CA_ADDRESS, SeoHelper::CA_BUSINESS_HOURS, SeoHelper::CA_CONTACT_POINT, SeoHelper::CA_CURRENCIES, SeoHelper::CA_DESCRIPTION, SeoHelper::CA_FOUNDING_DATE, SeoHelper::CA_GLOBAL_LOCATION_NUMBER, SeoHelper::CA_LEGAL_NAME, SeoHelper::CA_LOCAL_BUSINESS, SeoHelper::CA_ONLINE_STORE, SeoHelper::CA_RETURN_POLICY, SeoHelper::CA_SALES_DEPARTMENT, SeoHelper::CA_SERVICE_AREA, SeoHelper::CA_URL, SeoHelper::CA_VAT_ID, SeoHelper::CA_WAREHOUSE_DEPARTMENT, SeoHelper::CA_WAREHOUSE_HOURS, SeoHelper::COMPANY_EMAIL, SeoHelper::COMPANY_LOGO, SeoHelper::COMPANY_NAME, SeoHelper::COMPANY_SLOGAN, SeoHelper::EXPERTISE, SeoHelper::FAX_NUMBER, SeoHelper::GS1_COMPANY_PREFIX, SeoHelper::ISO6523_CODE, SeoHelper::PAYMENT_METHODS, SeoHelper::PHONE_NUMBER, SeoHelper::PRIMARY_NAICS, SeoHelper::REFUND_TYPE, SeoHelper::RETURN_FEES, SeoHelper::RETURN_METHOD, SeoHelper::RETURN_POLICY_CATEGORY, SeoHelper::SECONDARY_NAICS, SeoHelper::SOCIAL_PROFILES, SeoHelper::US_ADDRESS, SeoHelper::US_BUSINESS_HOURS, SeoHelper::US_CONTACT_POINT, SeoHelper::US_CURRENCIES, SeoHelper::US_DESCRIPTION, SeoHelper::US_FOUNDING_DATE, SeoHelper::US_GLOBAL_LOCATION_NUMBER, SeoHelper::US_IMAGE, SeoHelper::US_LEGAL_NAME, SeoHelper::US_LOCAL_BUSINESS, SeoHelper::US_ONLINE_STORE, SeoHelper::US_RETURN_POLICY, SeoHelper::US_SALES_DEPARTMENT, SeoHelper::US_SERVICE_AREA, SeoHelper::US_TAX_ID, SeoHelper::US_URL, SeoHelper::US_WAREHOUSE_DEPARTMENT, SeoHelper::US_WAREHOUSE_HOURS
Constants included from IconHelper
IconHelper::CUSTOM_ICON_MAP, IconHelper::CUSTOM_SVG_DIR, IconHelper::DEFAULT_FAMILY
Delegated Instance Attributes collapse
-
#features ⇒ Object
Alias for Item#features.
-
#item ⇒ Object
Alias for Product#item.
-
#quote_builder_url ⇒ Object
Alias for Product_line_presenter#quote_builder_url.
-
#show_product_sample_request? ⇒ Object
Alias for Product_line_presenter#show_product_sample_request?.
Class Method Summary collapse
-
.load_sku(item_sku, catalog_id) ⇒ Object
Load sku.
Instance Method Summary collapse
-
#accessories ⇒ Object
Accessories.
-
#allow_add_to_cart? ⇒ Boolean
Whether allow add to cart.
-
#amazon_affiliate_link ⇒ Object
We are discontinuing sending sales to amazon at this point But code will remain here just in case this changes.
-
#available_products_scope ⇒ Object
Available products scope.
-
#available_products_scope_new_only ⇒ Object
Available products scope new only.
-
#available_services_scope ⇒ Object
Available services scope.
-
#axis_options ⇒ Object
Axis options.
-
#breadcrumb ⇒ Object
Breadcrumb.
-
#breadcrumb_links_array ⇒ Object
Breadcrumb links array.
-
#build_additional_properties_from_specs ⇒ Object
Build Schema.org PropertyValue array from our public specifications system.
-
#cable_help_config ⇒ Object
Cable help offcanvas configuration Returns nil if no cable help should be shown, otherwise returns config hash.
-
#cache_key ⇒ Object
Cache key.
-
#canonical_paths(request_path, request_url: nil) ⇒ Object
Removed: unused predicate helper roll_or_mat.
-
#carousel_assets ⇒ Object
Carousel assets.
-
#combined_repairability_notice_text ⇒ Object
Combined repairability notice text.
-
#compute_single_section(name) ⇒ Object
Compute data for a single section without loading all other sections.
-
#control_electrical_rough_in_kits ⇒ Object
Control electrical rough in kits.
-
#controls ⇒ Object
Controls.
-
#coverage ⇒ Object
Coverage.
-
#description_header ⇒ Object
Description header.
-
#description_html ⇒ Object
Description html.
-
#description_text ⇒ Object
Description text.
-
#dimensions ⇒ Object
Dimensions.
-
#discount_percentage(precision: 2) ⇒ Object
Discount percentage.
-
#discounted_product? ⇒ Boolean
Whether discounted product.
-
#display_best_product_tahoe? ⇒ Boolean
Whether display best product tahoe.
-
#display_dealers? ⇒ Boolean
Whether display dealers.
-
#display_features_prominently? ⇒ Boolean
This determines if we display features right below the price and above add to cart if there are no variations present we can do so.
-
#display_how_much_cable_will_i_need? ⇒ Boolean
Whether display how much cable will i need.
-
#display_pro_contact_us_for_quote? ⇒ Boolean
Whether display pro contact us for quote.
-
#display_roof_gutter_deicing_quote? ⇒ Boolean
Whether display roof gutter deicing quote.
-
#display_what_size_panel_do_i_need? ⇒ Boolean
Whether display what size panel do i need.
-
#display_where_to_buy? ⇒ Boolean
For items specifically sold through channels, which at present are only these towel warmers we check for a combination of product line + category match It is possible that we could use also will_be_restricted_for_sales on item to check for this so there is a code consolidation opportunity here Refurb items are always available to buy directly.
-
#documents ⇒ Object
Documents.
-
#documents_grouped_by_category ⇒ Object
Documents grouped by category.
-
#effective_price_formatted ⇒ Object
Effective price formatted.
-
#environ_mat_product? ⇒ Boolean
Inject for Environ Easy Mat products only.
-
#expected_soon? ⇒ Boolean
Whether expected soon.
-
#faqs ⇒ Object
Faqs.
-
#floor_heating_cable_product? ⇒ Boolean
Check if item belongs to a generic floor heating cable product line.
-
#floor_heating_mat_product? ⇒ Boolean
Check if item belongs to a generic floor heating mat product line (TempZone only — Environ and Slab Heat have dedicated landing pages).
-
#floor_plans ⇒ Object
Floor plans.
-
#free_shipping_regions ⇒ Object
Free shipping regions.
-
#identifier ⇒ Object
Identifier.
-
#image_asset_slug_for_card(forced_profile_type: nil) ⇒ Object
Product card thumbnail only — does not change PDP #images / carousel order.
-
#image_tag(options) ⇒ String?
Image tag for the item's primary image.
-
#image_url(options = {}) ⇒ String?
URL of the item's primary image.
-
#images ⇒ Object
Images.
-
#inject_category_landing_page_breadcrumb(links_array) ⇒ Object
Injects category-specific landing pages into breadcrumb when the item belongs to a product line that has show_in_sales_portal=false (and thus gets skipped by the normal breadcrumb builder), but we have a dedicated landing page for it.
-
#installation_kits ⇒ Object
Installation kits.
-
#installation_methods ⇒ Object
Installation methods.
-
#insulating_underlayments ⇒ Object
Insulating underlayments.
-
#item_group_name ⇒ Object
Item group name.
-
#item_gtin13_for_structured_data ⇒ Object
Item gtin13 for structured data.
-
#length ⇒ Object
Length.
-
#load_rating_snapshot ⇒ Object
Load rating snapshot.
-
#new_product ⇒ Object
New product.
-
#new_product_price ⇒ Object
New product price.
-
#new_product_price_formatted ⇒ Object
New product price formatted.
-
#non_refursbished_sku ⇒ Object
Non refursbished sku.
-
#notices_for_side_links ⇒ Object
Notices for side links.
-
#og_image ⇒ Object
Og image.
-
#original_price ⇒ Object
What is the original price of the item? it is the non refurb price if this is a refurb or the regular price.
-
#original_price_formatted ⇒ Object
Original price formatted.
-
#page_description ⇒ Object
Page description.
-
#page_title ⇒ Object
Page title.
-
#panel_size_help_config ⇒ Object
Panel size help offcanvas configuration.
-
#price_formatted ⇒ Object
Price formatted.
-
#price_non_refurbished ⇒ Object
Price non refurbished.
-
#price_non_refurbished_formatted ⇒ Object
Price non refurbished formatted.
-
#pricing_program_effective_price_formatted(discount) ⇒ Object
Pricing program effective price formatted.
-
#primary_image ⇒ Object
Primary image.
-
#product_country_iso ⇒ Object
Product country iso.
-
#product_dimension_image ⇒ Object
Product dimension image.
-
#product_line ⇒ Object
Product line.
-
#product_line_presenter ⇒ Object
Product line presenter.
-
#product_notices ⇒ Object
Product notices.
-
#product_review_info(page: 1, limit: 5) ⇒ Object
Product review info.
-
#product_type_path ⇒ Object
Removed: was unused in codebase.
-
#public_specifications ⇒ Object
Public specifications.
-
#public_specifications_grouped ⇒ Object
Public specifications grouped.
-
#rating(page: 1, limit: @options[:product_reviews_limit] || 5) ⇒ Object
Rating.
-
#rating_snapshot ⇒ Object
Rating snapshot.
-
#refurbished_item? ⇒ Boolean
Whether refurbished item.
-
#refurbished_price ⇒ Object
Refurbished price.
-
#refurbished_price_discount ⇒ Object
Refurbished price discount.
-
#refurbished_price_formatted ⇒ Object
Refurbished price formatted.
-
#refurbished_product ⇒ Object
Refurbished product.
-
#related_services ⇒ Object
Related services.
-
#repairability_product_line_for_item ⇒ Object
Repairability product line for item.
-
#restricted_to_trade? ⇒ Boolean
Whether restricted to trade.
-
#retrieve_documents(categories = [:all]) ⇒ Object
Retrieve documents.
-
#review_last_updated_at ⇒ Object
Review last updated at.
-
#reviews_link ⇒ Object
Reviews link.
-
#sale_price_formatted ⇒ Object
Sale price formatted.
-
#sale_price_in_effect? ⇒ Boolean
Whether sale price in effect.
-
#sale_price_percentage_off_display ⇒ Object
Sale price percentage off display.
-
#schema_availability ⇒ Object
Schema availability.
-
#schema_availability_url ⇒ Object
Schema availability url.
-
#schema_dot_org_merchant_return_policy ⇒ Object
Schema dot org merchant return policy.
-
#schema_dot_org_offer_shipping_details ⇒ Object
Build shipping details for standard (non-oversize) items: free shipping with a fixed $0 rate and delivery window.
-
#schema_dot_org_structure(exclude_segments: []) ⇒ Object
Schema dot org structure.
-
#schema_item_condition ⇒ Object
Schema item condition.
-
#schema_item_condition_url ⇒ Object
Schema item condition url.
-
#schema_offer_struc ⇒ Object
Schema offer struc.
-
#sections ⇒ Object
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity.
-
#shape ⇒ Object
Shape.
-
#show_cable_help? ⇒ Boolean
Whether show cable help.
-
#show_notices_section? ⇒ Boolean
Whether show notices section.
-
#show_quote_builder_link? ⇒ Boolean
Whether show quote builder link.
-
#show_support_portal_link? ⇒ Boolean
Whether show support portal link.
-
#showcases_for_item ⇒ Object
Fetch published showcases featuring this product (either directly or via quotes) Returns up to 4 showcases, ordered by most recently updated.
-
#single_sku? ⇒ Boolean
Whether single sku.
-
#size_overall ⇒ Object
Size overall.
-
#smart_service? ⇒ Boolean
Whether smart service.
-
#snow_melting_mat_product? ⇒ Boolean
Check if item belongs to the snow melting mat product line (PowerMat / OmniMat).
-
#spec_output_for_axis(token) ⇒ Object
Spec output for axis.
-
#stock ⇒ Object
Stock.
-
#stock? ⇒ Boolean
Whether stock.
-
#stock_indicator_bullet ⇒ String
Stock status indicator for facet buttons Returns a small colored bullet indicating stock level.
-
#stock_indicator_color ⇒ Symbol
Color class for stock indicator: green, orange, red.
-
#stock_indicator_title ⇒ String
Tooltip text for stock indicator.
-
#structured_data_product_image_url ⇒ Object
JSON-LD Product requires image for Google merchant listings.
-
#support_portal_url ⇒ Object
Support portal url.
-
#surcharges ⇒ Object
Surcharges.
-
#terms_and_conditions ⇒ Object
Terms and conditions.
-
#to_json_ld(exclude_segments: []) ⇒ Object
To json ld.
-
#top_rating(page: 1, limit: 10) ⇒ Object
Top rating.
-
#upgrades ⇒ Object
Upgrades.
-
#url ⇒ Object
Url.
-
#variants(include_self: false, axis_filters: {}) ⇒ Object
Variants.
-
#variants? ⇒ Boolean
Whether variants.
-
#variants_count ⇒ Object
Variants count.
-
#videos ⇒ Object
Videos.
-
#voltage ⇒ Object
Voltage.
-
#watts ⇒ Object
Watts.
-
#width ⇒ Object
Width.
Methods included from ApplicationHelper
#better_number_to_currency, #cf_web_analytics?, #check_or_cross, #check_or_times, #embedded_tab_frame_id, #error_messages, #general_disclaimer_on_product_installation_and_local_codes, #markdown_to_html, #parent_layout, #pass_or_fail, #render_error_messages_list, #render_video_card, #resolved_auth_form_turbo_frame, #return_path_or, #safe_css_color, #set_return_path_if_present, #set_section_if_present, #tab_frame_id, #to_underscore, #turbo_section_wrapper, #turbo_tabs_request?, #url_on_same_domain_as_request, #working_hours?, #yes_or_no, #yes_or_no_highlighted, #yes_or_no_with_check_or_cross
Methods included from UppyUploaderHelper
#file_uploader, #image_uploader, #large_file_uploader_s3, #lead_sketch_uploader, #public_warranty_card_uploader, #rma_image_uploader, #rma_image_uploader_s3, #uppy_uploader, #video_uploader, #warranty_card_uploader
Methods included from ImagesHelper
#image_asset_tag, #image_asset_url, #picture_asset_tag
Methods included from SeoHelper
#add_page_schema, #add_webpage_schema, #canada?, #collect_schema, #company_social_links, #ensure_context_json, #json_ld_script_tag, #local_business_schema, #online_store_id, #online_store_schema, #page_main_entity, #page_main_entity_json, #render_auto_collection_page_schema, #render_collection_page_schema, #render_local_business_schema, #render_online_store_schema, #render_page_schemas, #render_page_video_schemas, #render_webpage_schema, #render_webpage_schema_with_collections, #usa?
Methods included from UrlsHelper
#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?
Methods included from IconHelper
#account_nav_icon, #fa_icon, #star_rating_html
Class Method Details
.load_sku(item_sku, catalog_id) ⇒ Object
Load sku.
29 30 31 32 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 29 def self.load_sku(item_sku, catalog_id) p = ViewProductCatalog.find_by(item_sku:, catalog_id:) new(p) end |
Instance Method Details
#accessories ⇒ Object
Accessories.
1303 1304 1305 1306 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1303 def accessories res = Item::Materials::CompatibleAccessories.new.process(item:, context: :sales) available_products_scope_new_only.merge(res.items) end |
#allow_add_to_cart? ⇒ Boolean
Whether allow add to cart.
1792 1793 1794 1795 1796 1797 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1792 def allow_add_to_cart? return false if display_where_to_buy? return false if restricted_to_trade? item_is_web_accessible? end |
#amazon_affiliate_link ⇒ Object
We are discontinuing sending sales to amazon at this point
But code will remain here just in case this changes
1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1822 def amazon_affiliate_link nil # if catalog_id == CatalogConstants::US_CATALOG_ID # amz_catalog_ids = CatalogConstants::AMAZON_US_CATALOG_IDS # affiliate_code_url = "?tag=warmlyyours-20" # amz_url = "https://www.amazon.com/dp/" # else # amz_catalog_ids = CatalogConstants::AMAZON_CA_CATALOG_IDS # affiliate_code_url = "?tag=warmlyyours08-20" # amz_url = "https://www.amazon.ca/dp/" # end # asin = item.view_product_catalogs.where(catalog_id: amz_catalog_ids).where(catalog_item_state: 'active').where.not(third_party_part_number: nil).pick(:third_party_part_number) # link = amz_url + asin + affiliate_code_url if asin.present? # link end |
#available_products_scope ⇒ Object
Available products scope.
1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1182 def available_products_scope catalog = Catalog.locale_to_catalog raise 'Invalid catalog locale' unless catalog # Use preload instead of includes to avoid massive JOINed queries # ViewProductCatalog already has most data denormalized; preload # associations needed for rendering related products ViewProductCatalog .joins(:item) .merge(Item.goods.non_publications) .preload( :item, item: [ :image_profiles, { image_profiles: :image }, :primary_image, :primary_product_line, :product_category, { primary_product_line: [] }, { product_category: [] } ] ) .visible_to_public .where(catalog_id: catalog.id) end |
#available_products_scope_new_only ⇒ Object
Available products scope new only.
1211 1212 1213 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1211 def available_products_scope_new_only available_products_scope.excluding_refurbished end |
#available_services_scope ⇒ Object
Available services scope.
1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1253 def available_services_scope catalog = Catalog.locale_to_catalog raise 'Invalid catalog locale' unless catalog # Use preload instead of includes to avoid massive JOINed queries ViewProductCatalog .joins(:item) .merge(Item.services) .preload(:item) .visible_to_public .where(catalog_id: catalog.id) end |
#axis_options ⇒ Object
Axis options.
387 388 389 390 391 392 393 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 387 def return {} unless (vs = variants(include_self: true)).present? && variant_axis_tokens.any? variant_axis_tokens.index_with do |t| vs.filter_map { |v| v.spec_output(t) }.uniq.sort end end |
#breadcrumb ⇒ Object
Breadcrumb.
199 200 201 202 203 204 205 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 199 def h.safe_join( .map do |link_info| content_tag(:li, link_to(link_info[:name], link_info[:url]), class: 'breadcrumb-item') end ) end |
#breadcrumb_links_array ⇒ Object
Breadcrumb links array.
133 134 135 136 137 138 139 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 133 def links_array = [] return links_array unless primary_product_line links_array += product_line_presenter. (links_array) end |
#build_additional_properties_from_specs ⇒ Object
Build Schema.org PropertyValue array from our public specifications system
1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1347 def build_additional_properties_from_specs return [] if public_specifications.blank? public_specifications.first(12).filter_map do |spec| # Use the rendered output text (stripped) for value value_text = ActionView::Base.full_sanitizer.sanitize(spec.output.to_s).squish next if value_text.blank? SchemaDotOrg::PropertyValue.new(name: spec.name, value: value_text) end end |
#cable_help_config ⇒ Object
Cable help offcanvas configuration
Returns nil if no cable help should be shown, otherwise returns config hash
1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1841 def cable_help_config return nil unless show_cable_help? pl_ltree = item_primary_product_line_slug_ltree&.to_s if pl_ltree&.start_with?('floor_heating.tempzone') { offcanvas_id: 'offcanvas-cable-tempzone', offcanvas_partial: 'offcanvas_cable_tempzone', title: 'How Much Cable Will I Need?', needs_variants: true } elsif pl_ltree&.start_with?('snow_melting.cable') { offcanvas_id: 'offcanvas-cable-snow-melt', offcanvas_partial: 'offcanvas_cable_snow_melt', title: 'How Much Cable Will I Need?' } elsif pl_ltree&.include?('roof_and_gutter_deicing') { offcanvas_id: 'offcanvas-cable-roof-gutter', offcanvas_partial: 'offcanvas_cable_roof_gutter', title: 'How Much Cable Will I Need?' } elsif pl_ltree&.start_with?('floor_heating.slab_heat.cable') { offcanvas_id: 'offcanvas-cable-slab', offcanvas_partial: 'offcanvas_cable_slab', title: 'How Much Cable Will I Need?' } end end |
#cache_key ⇒ Object
Cache key.
36 37 38 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 36 def cache_key "#{product.cache_key}/#{r.locale}" end |
#canonical_paths(request_path, request_url: nil) ⇒ Object
Removed: unused predicate helper roll_or_mat
522 523 524 525 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 522 def canonical_paths(request_path, request_url: nil) # This piece of code avoids duplicate titles and descriptions for those items whose product line page is the same as the product code page h.build_canonical_paths(request_path, locales: item_available_locales, request_url:) end |
#carousel_assets ⇒ Object
Carousel assets.
261 262 263 264 265 266 267 268 269 270 271 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 261 def carousel_assets assets_images = images.dup assets_videos = videos.dup # Prefer two stills before the first video so shoppers see multiple angles before video. assets = assets_images.shift(2) first_video = assets_videos.shift assets << first_video if first_video assets += assets_images assets += assets_videos assets.compact end |
#combined_repairability_notice_text ⇒ Object
Combined repairability notice text.
773 774 775 776 777 778 779 780 781 782 783 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 773 def combined_repairability_notice_text product_line_with_notice = repairability_product_line_for_item return if product_line_with_notice.blank? notice_fr = product_line_with_notice.try(:repairability_notice_fr_ca) notice_en = product_line_with_notice.try(:repairability_notice_en_ca) combined_notice = [notice_fr, notice_en].compact_blank.join("\n\n").presence return combined_notice if combined_notice.present? ProductLine.default_repairability_notice end |
#compute_single_section(name) ⇒ Object
Compute data for a single section without loading all other sections.
Used by the lazy-loaded Turbo Frame endpoint to avoid the allocation cost
of building every section on each request.
820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 820 def compute_single_section(name) case name when :features f = item.features return { section_name: 'Features', features: f } if f.present? when :notices notices = product_notices if notices.any? label = I18n.t('products.notices.section_name', default: 'Notices') return { nav_name: label, section_name: label, notices: notices } end when :description return { section_name: description_header, description_html: description_html } when :specifications grouped = public_specifications_grouped return { section_name: section_name(name), specifications_grouped: grouped } if grouped.any? when :documents grouped_documents = documents_grouped_by_category return { section_name: section_name(name), documents_grouped_by_category: grouped_documents } if grouped_documents.any? when :videos return { section_name: section_name(name), videos: videos } if videos.any? when :faq section_faqs = faqs if section_faqs.any? return { nav_name: section_name(name), section_name: 'Frequently Asked Questions', faqs: section_faqs, top_faqs: section_faqs.first(5), product_name: product_line.display_name } end when :terms_and_conditions return { section_name: section_name(name), terms_and_conditions: terms_and_conditions } if terms_and_conditions.present? when :surcharges return { section_name: 'Potential Surcharges', surcharges: surcharges } if surcharges.present? when :reviews internal_reviews = .product_reviews return { section_name: section_name(name), top_reviews: .product_reviews, reviews: internal_reviews, average_rating: .avg_stars, review_count: .review_count, product_name: product_line.display_name } when :installation_plans return { section_name: section_name(name), floor_plans: floor_plans } if floor_plans.present? when :showcases if showcases_for_item.present? return { nav_name: 'Real Projects', section_name: section_name(name), showcases: showcases_for_item, product_name: product_line&.display_name } end when :related_products return end nil end |
#control_electrical_rough_in_kits ⇒ Object
Control electrical rough in kits.
1311 1312 1313 1314 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1311 def control_electrical_rough_in_kits res = Item::Materials::CompatibleControlElectricalRoughInKits.new.process(item:) available_products_scope_new_only.merge(res.items) end |
#controls ⇒ Object
Controls.
1269 1270 1271 1272 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1269 def controls res = Item::Materials::CompatibleControls.new.process(item:) available_products_scope_new_only.merge(res.items).reorder(:sale_price, :price) end |
#coverage ⇒ Object
Coverage.
535 536 537 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 535 def coverage spec_output(:coverage) end |
#description_header ⇒ Object
Description header.
70 71 72 73 74 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 70 def description_header return 'Description' unless primary_product_line "#{primary_product_line.public_name} Description" end |
#description_html ⇒ Object
Description html.
78 79 80 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 78 def description_html item.public_description_html end |
#description_text ⇒ Object
Description text.
84 85 86 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 84 def description_text item.public_description_text end |
#dimensions ⇒ Object
Dimensions.
529 530 531 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 529 def dimensions "#{width} × #{length}" end |
#discount_percentage(precision: 2) ⇒ Object
Discount percentage.
1619 1620 1621 1622 1623 1624 1625 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1619 def discount_percentage(precision: 2) return nil unless original_price && effective_price return nil if original_price == effective_price || original_price.zero? p = 100 - ((effective_price.to_r / original_price.to_r) * 100).floor h.number_with_precision(p, strip_insignificant_zeros: true, precision:, round_mode: :down) end |
#discounted_product? ⇒ Boolean
Whether discounted product.
1682 1683 1684 1685 1686 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1682 def discounted_product? return false unless effective_price && original_price effective_price != original_price end |
#display_best_product_tahoe? ⇒ Boolean
Whether display best product tahoe.
1785 1786 1787 1788 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1785 def display_best_product_tahoe? item.pl_descendant_of_path?(LtreePaths::PL_TOWEL_WARMER_CLASSIC_TAHOE) && item.pc_descendant_of_path?(LtreePaths::PC_TOWEL_WARMERS) end |
#display_dealers? ⇒ Boolean
Whether display dealers.
1814 1815 1816 1817 1818 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1814 def display_dealers? !refurbished_item? && item.pl_descendant_of_path?(LtreePaths::PL_TOWEL_WARMER_COSMOPOLITAN) && item.pc_descendant_of_path?(LtreePaths::PC_TOWEL_WARMERS) end |
#display_features_prominently? ⇒ Boolean
This determines if we display features right below the price and above add to cart
if there are no variations present we can do so
720 721 722 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 720 def display_features_prominently? features.present? && (refurbished_item? || single_sku?) end |
#display_how_much_cable_will_i_need? ⇒ Boolean
Whether display how much cable will i need.
1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1759 def display_how_much_cable_will_i_need? cable_paths = [ LtreePaths::PL_FLOOR_HEATING_TEMPZONE_CABLE, LtreePaths::PL_FLOOR_HEATING_TEMPZONE_RULER_CABLE, LtreePaths::PL_SNOW_MELTING_CABLE, LtreePaths::PL_FLOOR_HEATING_SLAB_HEAT_CABLE ] cable_paths.any? { |path| item.pl_descendant_of_path?(path) } && item.pc_descendant_of_path?(LtreePaths::PC_HEATING_ELEMENTS_HEATING_CABLES) end |
#display_pro_contact_us_for_quote? ⇒ Boolean
Whether display pro contact us for quote.
1801 1802 1803 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1801 def display_pro_contact_us_for_quote? restricted_to_trade? end |
#display_roof_gutter_deicing_quote? ⇒ Boolean
Whether display roof gutter deicing quote.
1778 1779 1780 1781 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1778 def display_roof_gutter_deicing_quote? item.pl_descendant_of_path?(LtreePaths::PL_ROOF_GUTTER_DEICING) && item.pc_descendant_of_path?(LtreePaths::PC_HEATING_ELEMENTS) end |
#display_what_size_panel_do_i_need? ⇒ Boolean
Whether display what size panel do i need.
1772 1773 1774 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1772 def display_what_size_panel_do_i_need? item.pc_descendant_of_path?(LtreePaths::PC_INFRARED_HEATING_PANELS) end |
#display_where_to_buy? ⇒ Boolean
For items specifically sold through channels, which at present are only these towel warmers
we check for a combination of product line + category match
It is possible that we could use also will_be_restricted_for_sales on item to check for this
so there is a code consolidation opportunity here
Refurb items are always available to buy directly
1748 1749 1750 1751 1752 1753 1754 1755 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1748 def display_where_to_buy? # !is_refurbished_item? && # ( # item_primary_product_line_url.starts_with?('towel-warmer-premier') || # item_primary_product_line_url.starts_with?('towel-warmer-elevate') # ) && product_category_url.starts_with?('goods-towel-warmers') false # Everything's up for grab end |
#documents ⇒ Object
Documents.
284 285 286 287 288 289 290 291 292 293 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 284 def documents retrieve_documents([:sales]).all_documents.map do |r| DocRecord.new(category: r.product_category_name, sku: r.sku, name: r.name, content_url: r.content_url, featured_position: r.featured_position, star?: r.star?) end end |
#documents_grouped_by_category ⇒ Object
Documents grouped by category.
298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 298 def documents_grouped_by_category res = {} retrieve_documents([:sales]).publications_grouped.each do |cat, documents| res[cat.name] = documents.map do |r| DocRecord.new(category: r.product_category_name, sku: r.sku, name: r.name, content_url: r.content_url, featured_position: r.featured_position, star?: r.star?) end end res end |
#effective_price_formatted ⇒ Object
Effective price formatted.
571 572 573 574 575 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 571 def effective_price_formatted return unless effective_price h.better_number_to_currency(effective_price, unit: currency_symbol) end |
#environ_mat_product? ⇒ Boolean
Inject for Environ Easy Mat products only.
166 167 168 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 166 def environ_mat_product? item.pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_ENVIRON_EASY_MAT) end |
#expected_soon? ⇒ Boolean
Whether expected soon.
1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1580 def expected_soon? return false unless catalog_item # Check if there are any items on order or expected soon catalog_item.next_available_by_warehouse_with_depth_limit(use_alternate_warehouse: true, max_depth: 5).any? do |_warehouse_name, on_order_data| on_order_data&.next_available_date && on_order_data.next_available_date <= 30.days.from_now end rescue StandardError => e # Log the recursion error and return false ErrorReporting.error(e, catalog_item_id: catalog_item&.id, product_sku: item_sku) false end |
#faqs ⇒ Object
Faqs.
324 325 326 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 324 def faqs Item::ArticleRetriever.new(article_types: %w[faq], sales: true, add_vote_data: true).process(item:).articles end |
#features ⇒ Object
Alias for Item#features
23 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 23 delegate :features, to: :item |
#floor_heating_cable_product? ⇒ Boolean
Check if item belongs to a generic floor heating cable product line.
181 182 183 184 185 186 187 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 181 def floor_heating_cable_product? [ LtreePaths::PL_FLOOR_HEATING_TEMPZONE_CABLE, LtreePaths::PL_FLOOR_HEATING_TEMPZONE_THIN_CABLE, LtreePaths::PL_FLOOR_HEATING_TEMPZONE_RULER_CABLE ].any? { |path| item.pl_descendant_of_path?(path) } end |
#floor_heating_mat_product? ⇒ Boolean
Check if item belongs to a generic floor heating mat product line
(TempZone only — Environ and Slab Heat have dedicated landing pages).
172 173 174 175 176 177 178 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 172 def floor_heating_mat_product? [ LtreePaths::PL_FLOOR_HEATING_TEMPZONE_FLEX_ROLL, LtreePaths::PL_FLOOR_HEATING_TEMPZONE_EASY_MAT, LtreePaths::PL_FLOOR_HEATING_TEMPZONE_SHOWER_MAT ].any? { |path| item.pl_descendant_of_path?(path) } end |
#floor_plans ⇒ Object
Floor plans.
315 316 317 318 319 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 315 def floor_plans return nil unless product_category.is_heating_element? Item::FloorPlanRetriever.new(max_rooms: 1).process(product_line).all_rooms end |
#free_shipping_regions ⇒ Object
Free shipping regions.
1450 1451 1452 1453 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1450 def free_shipping_regions # Based on coupon shipping restrictions FREEGND0520 store_item.store.country.states.where.not(code: %w[AK AS FM GU HI MH NU NL MP NT PW PE PR VI YT]).pluck(:code) end |
#identifier ⇒ Object
Identifier.
214 215 216 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 214 def identifier "sku:#{item_sku}" end |
#image_asset_slug_for_card(forced_profile_type: nil) ⇒ Object
Product card thumbnail only — does not change PDP #images / carousel order.
Order: WYS_CARD, then WYS_MAIN.
450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 450 def image_asset_slug_for_card(forced_profile_type: nil) locale_prefix = I18n.locale.to_s.split('-').first wys_profiles = item.image_profiles.to_a.select do |p| p.image_type.to_s.start_with?('WYS_') && p.locale == locale_prefix end if forced_profile_type.present? p = wys_profiles.find { |pr| pr.image_type == forced_profile_type } return p.image.slug if p&.image return nil end CARD_IMAGE_ORDER.each do |t| p = wys_profiles.find { |pr| pr.image_type == t } next unless p&.image return p.image.slug end nil end |
#image_tag(options) ⇒ String?
Image tag for the item's primary image.
557 558 559 560 561 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 557 def image_tag() return unless (img = item.primary_image) Www::ImagePresenter.new(img).image_tag() end |
#image_url(options = {}) ⇒ String?
URL of the item's primary image.
479 480 481 482 483 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 479 def image_url( = {}) return unless primary_image primary_image.image_url() end |
#images ⇒ Object
Images.
220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 220 def images # PDP carousel uses WYS (WarmlyYours Site) image profiles only — no tag-based fallback. # IMPORTANT: Filter in Ruby to avoid N+1 queries when image_profiles is preloaded. # The controller preloads item: [:image_profiles] so we filter in memory. all_profiles = item.image_profiles.to_a # Filter for WYS_* types (omit WYS_CARD — listing-only, not on PDP carousel) wys_profiles = all_profiles .select { |p| p.image_type.to_s.start_with?('WYS_') } .reject { |p| p.image_type == 'WYS_CARD' } .sort_by { |p| ImageProfile::IMAGE_TYPES.keys.map(&:to_s).index(p.image_type.to_s) || 999 } wys_profiles.map { |p| Www::ImagePresenter.new(p.image, h) } end |
#inject_category_landing_page_breadcrumb(links_array) ⇒ Object
Injects category-specific landing pages into breadcrumb when the item belongs
to a product line that has show_in_sales_portal=false (and thus gets skipped
by the normal breadcrumb builder), but we have a dedicated landing page for it.
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 146 def (links_array) # Only inject entries for product lines that have show_in_sales_portal=false and # therefore won't appear naturally in the breadcrumb from breadcrumb_links_array. link = if item.is_thermostat? && item.pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING) { name: 'Thermostats', url: h.cms_link('/floor-heating/thermostats') } elsif environ_mat_product? { name: 'Environ Mats', url: h.cms_link('/floor-heating/environ-mats') } elsif floor_heating_mat_product? { name: 'Heated Floor Mats', url: h.cms_link('/floor-heating/heated-floor-mat') } elsif floor_heating_cable_product? { name: 'Heating Cables', url: h.cms_link('/floor-heating/heating-cable') } elsif snow_melting_mat_product? { name: 'Snow Melt Mats', url: h.cms_link('/snow-melting/mat') } end links_array.insert(1, link) if link links_array end |
#installation_kits ⇒ Object
Installation kits.
1295 1296 1297 1298 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1295 def installation_kits res = Item::Materials::CompatibleInstallationKits.new.process(item:) available_products_scope_new_only.merge(res.items) end |
#installation_methods ⇒ Object
Installation methods.
1285 1286 1287 1288 1289 1290 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1285 def installation_methods res = Item::Materials::CompatibleMembranes.new.process(item:) all_item_skus = res.items.pluck(:sku) all_item_skus.presence&.<<(ItemConstants::TZ_CABLE_GRIPSTRIP_SKU) available_products_scope_new_only.where(item_sku: all_item_skus) end |
#insulating_underlayments ⇒ Object
Insulating underlayments.
1319 1320 1321 1322 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1319 def insulating_underlayments res = Item::Materials::CompatibleInsulations.new.process(item:) available_products_scope_new_only.merge(res.items) end |
#item ⇒ Object
Alias for Product#item
22 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 22 delegate :item, to: :product |
#item_group_name ⇒ Object
Item group name.
404 405 406 407 408 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 404 def item_group_name return unless variants? r.item.item_grouping_info&.item_group_name end |
#item_gtin13_for_structured_data ⇒ Object
Item gtin13 for structured data.
51 52 53 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 51 def item_gtin13_for_structured_data product.try(:gtin13) || product.upc ? "0#{product.try(:upc)}" : nil end |
#length ⇒ Object
Length.
496 497 498 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 496 def length spec_output(:length) end |
#load_rating_snapshot ⇒ Object
Load rating snapshot.
1233 1234 1235 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1233 def ::Reviews::RatingSnapshot.for_sku(item_sku, fallback: fallback_to_internal_reviews?) end |
#new_product ⇒ Object
New product.
1690 1691 1692 1693 1694 1695 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1690 def new_product return nil if item.new_item_id.blank? return unless (np = ViewProductCatalog.find_by(catalog_id:, item_id: item.new_item_id)) self.class.new(np) end |
#new_product_price ⇒ Object
New product price.
1700 1701 1702 1703 1704 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1700 def new_product_price return unless new_product new_product.price end |
#new_product_price_formatted ⇒ Object
New product price formatted.
1708 1709 1710 1711 1712 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1708 def new_product_price_formatted return unless new_product_price h.better_number_to_currency(new_product_price, unit: currency_symbol) end |
#non_refursbished_sku ⇒ Object
Non refursbished sku.
1647 1648 1649 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1647 def non_refursbished_sku new_product.item_sku end |
#notices_for_side_links ⇒ Object
Notices for side links.
760 761 762 763 764 765 766 767 768 769 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 760 def notices_for_side_links return [] unless show_notices_section? product_notices.map do |notice| { title: notice[:title], anchor: "##{notice[:id]}" } end end |
#og_image ⇒ Object
Og image.
108 109 110 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 108 def og_image primary_image&.image_url || DEFAULT_SOCIAL_ICON end |
#original_price ⇒ Object
What is the original price of the item? it is the non refurb price if this is a refurb or the regular price
1668 1669 1670 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1668 def original_price price_non_refurbished || price end |
#original_price_formatted ⇒ Object
Original price formatted.
1674 1675 1676 1677 1678 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1674 def original_price_formatted return unless original_price h.better_number_to_currency(original_price, unit: currency_symbol) end |
#page_description ⇒ Object
Page description.
64 65 66 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 64 def page_description product&.effective_seo_description end |
#page_title ⇒ Object
Page title.
57 58 59 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 57 def page_title product&.seo_title || product&.public_short_name end |
#panel_size_help_config ⇒ Object
Panel size help offcanvas configuration
2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 2002 def panel_size_help_config return nil unless display_what_size_panel_do_i_need? pl_ltree = item_primary_product_line_slug_ltree&.to_s if pl_ltree&.start_with?('infrared_heating_panels.lava') { offcanvas_id: 'offcanvas-size-lava', offcanvas_partial: 'offcanvas_size_lava', title: 'What Size Panel Do I Need?' } elsif pl_ltree&.start_with?('infrared_heating_panels.ember') { offcanvas_id: 'offcanvas-size-ember', offcanvas_partial: 'offcanvas_size_ember', title: 'What Size Panel Do I Need?' } end end |
#price_formatted ⇒ Object
Price formatted.
565 566 567 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 565 def price_formatted h.better_number_to_currency(price, unit: currency_symbol) end |
#price_non_refurbished ⇒ Object
Price non refurbished.
1653 1654 1655 1656 1657 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1653 def price_non_refurbished return unless new_product new_product.price end |
#price_non_refurbished_formatted ⇒ Object
Price non refurbished formatted.
1661 1662 1663 1664 1665 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1661 def price_non_refurbished_formatted return unless price_non_refurbished h.better_number_to_currency(price_non_refurbished, unit: currency_symbol) end |
#pricing_program_effective_price_formatted(discount) ⇒ Object
Pricing program effective price formatted.
580 581 582 583 584 585 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 580 def pricing_program_effective_price_formatted(discount) return unless discount p = original_price - (original_price * discount) h.better_number_to_currency(p, unit: currency_symbol) end |
#primary_image ⇒ Object
Primary image.
443 444 445 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 443 def primary_image images.first end |
#product_country_iso ⇒ Object
Product country iso.
1457 1458 1459 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1457 def product_country_iso store_item.store.country.iso end |
#product_dimension_image ⇒ Object
Product dimension image.
238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 238 def product_dimension_image retrieved_images = Item::ImageRetriever.new(tags: 'product-dimension', item_images_query_limit: 1, product_line_query_limit: 0, max_images: 1, ignore_kit_components: true, ignore_item_primary_image: true, ignore_vignette_installation_image: true, ignore_product_line_primary_image: true) .process(item).all_images retrieved_images.map { |img| Www::ImagePresenter.new(img, h) }&.first end |
#product_line ⇒ Object
Product line.
603 604 605 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 603 def product_line sales_portal_product_line || primary_product_line.root end |
#product_line_presenter ⇒ Object
Product line presenter.
127 128 129 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 127 def product_line_presenter Www::ProductLinePresenter.new(primary_product_line, h) end |
#product_notices ⇒ Object
Product notices.
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 726 def product_notices notices = [] # Quebec repairability notices (only for Canadian locale) if h.canada? repairability_notice_text = combined_repairability_notice_text if repairability_notice_text.present? notices << { id: 'quebec_repairability', title: 'Quebec Repairability Notice', text: repairability_notice_text } end end # Future notices can be added here following the same pattern # if condition_for_another_notice? # title = "Notice Title" # text = "Notice text..." # notices << { id: 'another_notice_key', title: title, text: text } if text.present? # end notices end |
#product_review_info(page: 1, limit: 5) ⇒ Object
Product review info.
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 618 def product_review_info(page: 1, limit: 5) review_info = ::ReviewsIo.fetch_reviews(sku: item_sku, page: page, limit: limit) apply_reviews_snapshot(review_info) rescue StandardError => e Rails.logger.error("Error fetching product reviews for #{item_sku}: #{e.}") begin ErrorReporting.error(e, item_sku: item_sku) rescue StandardError nil end # Return empty result to prevent breaking the page { reviews: [], star_avg: nil, num: 0, name: nil, item_sku: item_sku, updated_at: Time.current } end |
#product_type_path ⇒ Object
Removed: was unused in codebase
208 209 210 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 208 def product_type_path @product_type_path ||= Feed::ProductTypeGenerator.process(item:, separator: '/') end |
#public_specifications ⇒ Object
Public specifications.
420 421 422 423 424 425 426 427 428 429 430 431 432 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 420 def public_specifications # For kits we will do a special treatment and break it down per item if item_is_kit item.get_kit_items(spec_only: true).flat_map do |ki| ki.public_specifications.map do |s| s.grouping = "#{s.grouping} (#{ki.sku})" s end end else item.public_specifications end end |
#public_specifications_grouped ⇒ Object
Public specifications grouped.
437 438 439 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 437 def public_specifications_grouped public_specifications.sort_by(&:name).group_by(&:grouping).reject { |_group, specifications| specifications.empty? } end |
#quote_builder_url ⇒ Object
Alias for Product_line_presenter#quote_builder_url
21 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 21 delegate :quote_builder_url, :show_product_sample_request?, to: :product_line_presenter, allow_nil: true |
#rating(page: 1, limit: @options[:product_reviews_limit] || 5) ⇒ Object
Rating.
650 651 652 653 654 655 656 657 658 659 660 661 662 663 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 650 def (page: 1, limit: @options[:product_reviews_limit] || 5) Rating.new(product_review_info(page:, limit:), item) rescue StandardError => e Rails.logger.error("Error creating rating for #{item_sku}: #{e.}") # Return empty rating to prevent breaking the page Rating.new({ reviews: [], star_avg: nil, num: 0, name: nil, item_sku: item_sku, updated_at: Time.current }, item) end |
#rating_snapshot ⇒ Object
Rating snapshot.
677 678 679 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 677 def @rating_snapshot ||= end |
#refurbished_item? ⇒ Boolean
Whether refurbished item.
1641 1642 1643 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1641 def refurbished_item? item_condition == 'refurbished' && new_item_id.present? end |
#refurbished_price ⇒ Object
Refurbished price.
1606 1607 1608 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1606 def refurbished_price refurbished_product.price end |
#refurbished_price_discount ⇒ Object
Refurbished price discount.
1629 1630 1631 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1629 def refurbished_price_discount discount_percentage end |
#refurbished_price_formatted ⇒ Object
Refurbished price formatted.
1612 1613 1614 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1612 def refurbished_price_formatted h.better_number_to_currency(refurbished_product.price, unit: currency_symbol) end |
#refurbished_product ⇒ Object
Refurbished product.
1597 1598 1599 1600 1601 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1597 def refurbished_product return unless (p = available_products_scope.find_by(item_id: refurbished_item_id)) self.class.new(p) end |
#related_services ⇒ Object
Related services.
1327 1328 1329 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1327 def available_services_scope end |
#repairability_product_line_for_item ⇒ Object
Repairability product line for item.
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 787 def repairability_product_line_for_item item_primary_line = item.primary_product_line return if item_primary_line.blank? item_primary_line.self_and_ancestors.find do |product_line| next if product_line.blank? has_notice_for_ca = product_line.try(:repairability_notice_fr_ca).present? || product_line.try(:repairability_notice_en_ca).present? next unless has_notice_for_ca categories = Array(product_line.product_category_ids).compact next true if categories.blank? next false if item.product_category_id.blank? || item.pc_path_ids.blank? category_paths = ProductCategory.where(id: categories).pluck(:ltree_path_ids).compact category_paths.any? { |path| item.pc_path_ids.to_s.start_with?(path.to_s) } end end |
#restricted_to_trade? ⇒ Boolean
Whether restricted to trade.
1807 1808 1809 1810 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1807 def restricted_to_trade? item.pl_descendant_of_path?(LtreePaths::PL_FLOOR_HEATING_TEMPZONE_RULER_CABLE) && item.pc_descendant_of_path?(LtreePaths::PC_HEATING_ELEMENTS_HEATING_CABLES) end |
#retrieve_documents(categories = [:all]) ⇒ Object
Retrieve documents.
277 278 279 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 277 def retrieve_documents(categories = [:all]) Item::PublicationRetriever.new.process(item_sku, categories:, all_product_lines: false) end |
#review_last_updated_at ⇒ Object
Review last updated at.
642 643 644 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 642 def review_last_updated_at product_review_info[:updated_at] end |
#reviews_link ⇒ Object
Reviews link.
610 611 612 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 610 def reviews_link h.catalog_link(item, section: :reviews) end |
#sale_price_formatted ⇒ Object
Sale price formatted.
589 590 591 592 593 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 589 def sale_price_formatted return unless sale_price h.better_number_to_currency(sale_price, unit: currency_symbol) end |
#sale_price_in_effect? ⇒ Boolean
Whether sale price in effect.
597 598 599 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 597 def sale_price_in_effect? r.sale_price_in_effect end |
#sale_price_percentage_off_display ⇒ Object
Sale price percentage off display.
1635 1636 1637 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1635 def sale_price_percentage_off_display discount_percentage end |
#schema_availability ⇒ Object
Schema availability.
690 691 692 693 694 695 696 697 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 690 def schema_availability # InStock # PreOrder # Discontinued # OutOfStock # LimitedAvailability h.tag(:link, itemprop: 'availability', href: schema_availability_url) end |
#schema_availability_url ⇒ Object
Schema availability url.
684 685 686 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 684 def schema_availability_url "https://schema.org/#{availability}" end |
#schema_dot_org_merchant_return_policy ⇒ Object
Schema dot org merchant return policy.
1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1406 def schema_dot_org_merchant_return_policy # Reflects https://www.warmlyyours.com/company/return-policy SchemaDotOrg::MerchantReturnPolicy.new( applicableCountry: product_country_iso, # No restocking fees; customer covers shipping for remorse/damaged installation returnFees: 'https://schema.org/FreeReturn', # No general time limit on returns; 30-day window applies to advance replacement returnPolicyCategory: 'https://schema.org/MerchantReturnUnlimitedWindow', returnWithin: SchemaDotOrg::QuantitativeValue.new(value: 30, unitCode: 'DAY'), # Returns are mailed back; label available to download and print returnMethod: ['https://schema.org/ReturnByMail'], returnLabelSource: 'https://schema.org/ReturnLabelDownloadAndPrint', # Exchanges for damaged-in-transit are free return shipping returnShippingFees: 'https://schema.org/FreeReturn' ) end |
#schema_dot_org_offer_shipping_details ⇒ Object
Build shipping details for standard (non-oversize) items: free shipping with a
fixed $0 rate and delivery window. Oversize/freight items ship at a variable,
quote-based rate that can't be expressed as a fixed shippingRate — and Google
requires shippingRate on OfferShippingDetails — so we omit shippingDetails
entirely for oversize rather than emit an invalid node (was triggering
"shippingRate is required for OfferShippingDetails" rich-result errors).
1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1429 def schema_dot_org_offer_shipping_details return nil if item&.oversize? SchemaDotOrg::OfferShippingDetails.new( shippingRate: SchemaDotOrg::MonetaryAmount.new( value: 0.0, currency: currency ), shippingDestination: SchemaDotOrg::DefinedRegion.new( addressCountry: product_country_iso, addressRegion: free_shipping_regions ), deliveryTime: SchemaDotOrg::ShippingDeliveryTime.new( handlingTime: SchemaDotOrg::QuantitativeValue.new(minValue: 0, maxValue: 3, unitCode: 'DAY'), transitTime: SchemaDotOrg::QuantitativeValue.new(minValue: 1, maxValue: 5, unitCode: 'DAY') ) ) end |
#schema_dot_org_structure(exclude_segments: []) ⇒ Object
Schema dot org structure.
1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1464 def schema_dot_org_structure(exclude_segments: []) schema = SchemaDotOrg::Product.new # Stable product IRI so reviews and other entities can reference it via @id schema.id_iri = url schema.name = title schema.category = product_type_path schema.model = item_sku schema.sku = item_sku schema.gtin13 = item_gtin13_for_structured_data schema.itemCondition = schema_item_condition_url schema.image = structured_data_product_image_url schema.description = Oembed::ProductProvider.sanitize_schema_text( item.public_description_text.presence || title ) schema.brand = Brand.warmlyyours.schema_dot_org_structure schema.url = url schema.mpn = item_sku # Manufacturer Part Number schema.isAccessoryOrSparePartFor = nil # Not applicable for thermostats schema. = nil # Could be enhanced for related products schema.manufacturer = Brand.warmlyyours.schema_dot_org_structure schema.material = 'Electronic Components' schema.color = 'White' # Most thermostats are white additional_specs = build_additional_properties_from_specs schema.additionalProperty = additional_specs if additional_specs.present? schema.offers = schema_offer_struc # Add aggregate rating using cached rating_snapshot (avoids external API call) # Full reviews are no longer embedded in schema for performance reasons begin snapshot = if exclude_segments.exclude?(:aggregate_rating) && snapshot[:count].to_i.positive? schema.aggregateRating = SchemaDotOrg::AggregateRating.new( ratingValue: snapshot[:average].to_f, # Allow decimals (e.g., 4.5) reviewCount: snapshot[:count].to_i ) end # NOTE: Individual reviews are no longer embedded in Product schema. # This significantly improves page load performance by avoiding external API calls. # Google still picks up aggregate rating data for rich snippets. rescue StandardError => e # Log error but don't break schema generation Rails.logger.error("Error adding reviews to schema for #{item_sku}: #{e.}") begin ErrorReporting.error(e, item_sku: item_sku) rescue StandardError nil end end schema end |
#schema_item_condition ⇒ Object
Schema item condition.
714 715 716 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 714 def schema_item_condition h.tag(:link, itemprop: 'itemCondition', href: schema_item_condition_url) end |
#schema_item_condition_url ⇒ Object
Schema item condition url.
701 702 703 704 705 706 707 708 709 710 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 701 def schema_item_condition_url case item_condition when 'refurbished' 'https://schema.org/RefurbishedCondition' when 'damaged' 'https://schema.org/DamagedCondition' else 'https://schema.org/NewCondition' end end |
#schema_offer_struc ⇒ Object
Schema offer struc.
1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1361 def schema_offer_struc o = SchemaDotOrg::Offer.new o.url = url o.priceCurrency = currency o.availability = schema_availability_url o.itemCondition = schema_item_condition_url o.sku = item_sku o.gtin13 = item_gtin13_for_structured_data if sale_price_in_effect? sale_end = sale_price_expiration_date || Date.current.end_of_month # offers.price is the price the customer actually pays (the sale price), so # it matches the displayed price and avoids Merchant Center price mismatches. # The regular/list price (feed <g:price>) is conveyed as a StrikethroughPrice # priceSpecification — the type Google requires for the struck-through # reference price in product rich results (SalePrice here is rejected). o.price = sale_price o.priceValidUntil = sale_end o.priceSpecification = [ SchemaDotOrg::PriceSpecification.new( price: price.to_s, priceCurrency: currency, priceType: 'https://schema.org/StrikethroughPrice' ) ] else o.price = price o.priceValidUntil = (Date.current + 30.days).iso8601 end # Enhanced offer details o.hasMerchantReturnPolicy = schema_dot_org_merchant_return_policy o.seller = Www::SeoHelper.online_store_schema # Add free shipping details when applicable (non-oversize items) if (sd = schema_dot_org_offer_shipping_details) o.shippingDetails = sd end o end |
#sections ⇒ Object
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 871 def sections # Don't change this order unless you get clearance from Julia sections = %i[description specifications installation_plans showcases related_products documents surcharges faq terms_and_conditions reviews] sections.insert(0, :features) unless display_features_prominently? # Insert notices section between features and description if applicable if show_notices_section? features_index = sections.index(:features) if features_index sections.insert(features_index + 1, :notices) else # If features is not in sections (displayed prominently), insert before description description_index = sections.index(:description) sections.insert(description_index, :notices) if description_index end end = RELATED_PRODUCT_SUBSECTIONS main_category = product_category.name main_category_sym = main_category.delete(' ').underscore.to_sym sections.delete(main_category_sym) s = {} # We keep track here of products that were already rendered so we don't re-render them in multiple sections. rendered_catalog_item_ids = [] sections.each do |section| if lazy_rendering? && LAZY_SECTIONS.include?(section) = (section) s[section] = if next end case section when :features features = item.features if features.present? s[section] = { section_name: 'Features', features: } end when :notices if (notices = product_notices).any? section_name = I18n.t('products.notices.section_name', default: 'Notices') s[section] = { nav_name: section_name, section_name: section_name, notices: notices } end when :description s[section] = { section_name: description_header, description_html: } when :specifications if public_specifications_grouped.any? s[section] = { section_name: section_name(section), specifications_grouped: public_specifications_grouped } end when :documents if documents_grouped_by_category.any? s[section] = { section_name: section_name(section), documents_grouped_by_category: } end when :videos if videos.any? s[section] = { section_name: section_name(section), videos: } end when :faq if faqs.any? s[section] = { nav_name: section_name(section), section_name: 'Frequently Asked Questions', faqs:, top_faqs: faqs.first(5), product_name: product_line.display_name } end when :terms_and_conditions if terms_and_conditions.present? s[section] = { section_name: section_name(section), terms_and_conditions: } end when :surcharges if surcharges.present? s[section] = { section_name: 'Potential Surcharges', surcharges: } end when :reviews # Always show reviews section so users can write a review internal_reviews = .product_reviews s[section] = { section_name: section_name(section), top_reviews: .product_reviews, reviews: internal_reviews, average_rating: .avg_stars, review_count: .review_count, product_name: product_line.display_name } when :installation_plans if floor_plans.present? s[section] = { section_name: section_name(section), floor_plans: } end when :showcases if showcases_for_item.present? s[section] = { nav_name: 'Real Projects', section_name: section_name(section), showcases: showcases_for_item, product_name: product_line&.display_name } end when :related_products # Collect all related products first to batch-preload their roots = [] .each do || section_products = send() next if section_products.blank? section_products = section_products.reject { |p| rendered_catalog_item_ids.include?(p.id) } next if section_products.blank? .concat(section_products) end # Preload root system product lines and root product categories to avoid N+1 queries preload_product_type_roots() if .any? # Now create presenters and organize by section .each do || section_products = send() next if section_products.blank? section_products = section_products.reject { |p| rendered_catalog_item_ids.include?(p.id) } next if section_products.blank? s[section] ||= {} s[section][] = { section_name: section_name(), section_intro: section_intro(), products: section_products.map { |p| Www::ProductCatalogPresenter.new(p, h) } } rendered_catalog_item_ids += section_products.map(&:id) end else section_products = send(section) next if section_products.blank? section_products = section_products.reject { |p| rendered_catalog_item_ids.include?(p.id) } if section_products.present? # Preload root system product lines and root product categories to avoid N+1 queries preload_product_type_roots(section_products) # Wrap our products in a presenter s[section] = { section_name: section.to_s.titleize, section_intro: section_intro(section), products: section_products.map { |p| Www::ProductCatalogPresenter.new(p, h) }.sort_by(&:effective_price) } rendered_catalog_item_ids += section_products.map(&:id) end end end s end |
#shape ⇒ Object
Shape.
514 515 516 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 514 def shape spec_output(:shape) end |
#show_cable_help? ⇒ Boolean
Whether show cable help.
1875 1876 1877 1878 1879 1880 1881 1882 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1875 def show_cable_help? # Floor heating cables return true if display_how_much_cable_will_i_need? # Roof and gutter deicing return true if display_roof_gutter_deicing_quote? false end |
#show_notices_section? ⇒ Boolean
Whether show notices section.
754 755 756 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 754 def show_notices_section? product_notices.any? end |
#show_product_sample_request? ⇒ Object
Alias for Product_line_presenter#show_product_sample_request?
21 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 21 delegate :quote_builder_url, :show_product_sample_request?, to: :product_line_presenter, allow_nil: true |
#show_quote_builder_link? ⇒ Boolean
Whether show quote builder link.
1722 1723 1724 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1722 def show_quote_builder_link? item.pc_descendant_of_path?(LtreePaths::PC_HEATING_ELEMENTS) && product_line_presenter.show_quote_builder_link? end |
#show_support_portal_link? ⇒ Boolean
Whether show support portal link.
1728 1729 1730 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1728 def show_support_portal_link? support_portal_url.present? end |
#showcases_for_item ⇒ Object
Fetch published showcases featuring this product (either directly or via quotes)
Returns up to 4 showcases, ordered by most recently updated
1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1334 def showcases_for_item Showcase .published .for_item(item.id) .with_main_image .preload(:region_state) .order_by_images .order(updated_at: :desc) .limit(4) end |
#single_sku? ⇒ Boolean
Whether single sku.
121 122 123 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 121 def single_sku? variants_count <= 1 end |
#size_overall ⇒ Object
Size overall.
508 509 510 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 508 def size_overall spec_output(:size_overall) end |
#smart_service? ⇒ Boolean
Whether smart service.
1716 1717 1718 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1716 def smart_service? item.pc_descendant_of_path?(LtreePaths::PC_SERVICES) end |
#snow_melting_mat_product? ⇒ Boolean
Check if item belongs to the snow melting mat product line (PowerMat /
OmniMat). snow_melting.mat is show_in_sales_portal=false so it is skipped by
the normal breadcrumb and must be injected; snow_melting.cable is
sales-portal-visible and already appears, so it needs no injection.
193 194 195 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 193 def snow_melting_mat_product? item.pl_descendant_of_path?(LtreePaths::PL_SNOW_MELTING_MAT) end |
#spec_output_for_axis(token) ⇒ Object
Spec output for axis.
414 415 416 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 414 def spec_output_for_axis(token) spec_output(token) end |
#stock ⇒ Object
Stock.
1530 1531 1532 1533 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1530 def stock # this is only used for refurbished items catalog_item&.store_item&.qty_available end |
#stock? ⇒ Boolean
Whether stock.
1537 1538 1539 1540 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1537 def stock? # this is only used for refurbished items stock.positive? end |
#stock_indicator_bullet ⇒ String
Stock status indicator for facet buttons
Returns a small colored bullet indicating stock level
1545 1546 1547 1548 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1545 def stock_indicator_bullet color = stock_indicator_color h.tag.span('', class: "stock-bullet stock-bullet-#{color}", title: stock_indicator_title) end |
#stock_indicator_color ⇒ Symbol
Color class for stock indicator: green, orange, red
1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1552 def stock_indicator_color return :red unless product_available? qty = store_item_qty_available.to_i warn_threshold = item&.qty_warn_on_stock.to_i warn_threshold = 10 if warn_threshold.zero? # Orange for low stock (qty <= 3 or below warning threshold) qty <= warn_threshold ? :orange : :green end |
#stock_indicator_title ⇒ String
Tooltip text for stock indicator
1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1565 def stock_indicator_title return 'Out of Stock' unless product_available? qty = store_item_qty_available.to_i if qty <= 3 "Only #{qty} left" elsif qty <= 10 'Low Stock' else 'In Stock' end end |
#structured_data_product_image_url ⇒ Object
JSON-LD Product requires image for Google merchant listings. PDP carousel
images are WYS_* profiles only. If the item has no photos in the library,
we still emit a stable https URL (see PRODUCT_STRUCTURED_DATA_PLACEHOLDER_IMAGE).
488 489 490 491 492 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 488 def structured_data_product_image_url image_url(size: '600x600>').presence || item.primary_image_url(size: '600x600>').presence || PRODUCT_STRUCTURED_DATA_PLACEHOLDER_IMAGE end |
#support_portal_url ⇒ Object
Support portal url.
1734 1735 1736 1737 1738 1739 1740 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1734 def support_portal_url # Would this product be visible for support? return unless Item.goods_visible_for_support.exists?(sku: item_sku) # And build a support url h.catalog_link(item, section: :support) end |
#surcharges ⇒ Object
Surcharges.
99 100 101 102 103 104 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 99 def surcharges return nil unless smart_service? # Content originates from trusted CMS input product.item.terms_of_service&.html_safe end |
#terms_and_conditions ⇒ Object
Terms and conditions.
90 91 92 93 94 95 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 90 def terms_and_conditions return nil unless smart_service? # Content originates from trusted CMS input product.item.terms_and_conditions&.html_safe end |
#to_json_ld(exclude_segments: []) ⇒ Object
To json ld.
1520 1521 1522 1523 1524 1525 1526 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1520 def to_json_ld(exclude_segments: []) s = schema_dot_org_structure(exclude_segments: exclude_segments) s.to_s rescue StandardError => e ErrorReporting.error e Rails.logger.error e end |
#top_rating(page: 1, limit: 10) ⇒ Object
Top rating.
670 671 672 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 670 def (page: 1, limit: 10) Rating.new(product_review_info(page:, limit:), item) end |
#upgrades ⇒ Object
Upgrades.
1277 1278 1279 1280 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 1277 def upgrades res = Item::Materials::CompatibleUpgrades.new.process(item:) available_products_scope_new_only.merge(res.items).reorder(:sale_price, :price) end |
#url ⇒ Object
Url.
42 43 44 45 46 47 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 42 def url path = item.canonical_path return nil unless path h.cms_link("/#{path}", r.locale, scheme: 'https', host: WEB_HOSTNAME_WITHOUT_PORT, port: APP_PORT_NUMBER) end |
#variants(include_self: false, axis_filters: {}) ⇒ Object
Variants.
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 332 def variants(include_self: false, axis_filters: {}) # `catalog_item.variants` includes a `.order(product_categories.priority, ...)` # via `catalog_items_in_same_catalog`. AR's `.ids` adds an implicit # `GROUP BY catalog_items.id` for the join'd relation, which PG strict mode # rejects when the ORDER BY references a non-grouped column. We re-sort # the actual results below; drop the inherited order for the ID pluck. catalog_item_ids = catalog_item.variants(include_self:, axis_filters: axis_filters)&.reorder(nil)&.ids || [] all_sort_keys = variant_sort_keys.compact.uniq.presence || ['price asc', 'item_sku asc'] # Separate database column sort keys from spec-based sort keys db_sort_keys, spec_sort_keys = all_sort_keys.partition { |k| !k.to_s.start_with?('spec:') } db_sort_keys = ['price asc', 'item_sku asc'] if db_sort_keys.empty? results = ViewProductCatalog.visible_to_public .where(item_condition: 'new', catalog_item_id: catalog_item_ids, catalog_id:) .preload(:item) .reorder(*db_sort_keys) .map { |pc| Www::ProductCatalogPresenter.new(pc) } # Apply spec-based sorting in Ruby if specified - sort by all keys together # Supports format: "spec:token asc" or "spec:token:unit asc" (e.g., "spec:width:in asc") if spec_sort_keys.any? parsed_keys = spec_sort_keys.filter_map do |spec_key| match = spec_key.to_s.match(/^spec:(\w+)(?::(\w+))?\s+(asc|desc)$/i) next unless match { token: match[1].to_sym, unit: match[2], desc: match[3].downcase == 'desc' } end if parsed_keys.any? results = results.sort do |a, b| comparison = 0 parsed_keys.each do |key| va = a.spec_value(key[:token], output_unit: key[:unit]) vb = b.spec_value(key[:token], output_unit: key[:unit]) # Compare numerically when both are numeric, otherwise as strings comparison = if va.is_a?(Numeric) && vb.is_a?(Numeric) va <=> vb else va.to_s <=> vb.to_s end comparison = -comparison if key[:desc] break unless comparison.zero? end comparison end end end results end |
#variants? ⇒ Boolean
Whether variants.
398 399 400 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 398 def variants? variants.present? end |
#variants_count ⇒ Object
Variants count.
114 115 116 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 114 def variants_count variants(include_self: true).size end |
#videos ⇒ Object
Videos.
254 255 256 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 254 def videos Item::VideoRetriever.new(tags: (smart_service? ? 'for-service-page' : 'for-product-page'), ignore_individual_query_limits: true).process(item).all_videos end |
#voltage ⇒ Object
Voltage.
547 548 549 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 547 def voltage spec_output(:voltage) end |
#watts ⇒ Object
Watts.
541 542 543 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 541 def watts spec_output(:watts) end |
#width ⇒ Object
Width.
502 503 504 |
# File 'app/presenters/www/product_catalog_presenter.rb', line 502 def width spec_output(:width) end |