Class: Www::ProductsController
- Inherits:
-
BasePortalController
- Object
- ActionController::Base
- ApplicationController
- BasePortalController
- Www::ProductsController
- Defined in:
- app/controllers/www/products_controller.rb
Overview
Controller: products.
Constant Summary
Constants included from Controllers::MasqueradeGuarded
Controllers::MasqueradeGuarded::DEFAULT_BLOCK_MESSAGE
Constants included from Controllers::AnalyticsEvents
Controllers::AnalyticsEvents::MAX_QUEUED_EVENTS, Controllers::AnalyticsEvents::SESSION_KEY
Constants included from Controllers::ErrorRendering
Controllers::ErrorRendering::NON_CONTENT_PATH_PREFIXES
Instance Method Summary collapse
-
#add_to_cart ⇒ void
Adds the requested SKU/quantity to the current user's cart.
-
#bulk_product_data ⇒ JSON
Consolidated bulk product data endpoint Returns product data for one or more SKUs with field filtering.
-
#cart_add_item(sku_code, quantity = nil, room_configuration_id = nil) ⇒ void
protected
Adds a SKU/quantity to
@cartand records it in@recently_added_items. -
#code ⇒ Object
Legacy route: 301 redirects to new hierarchical URL.
-
#index ⇒ void
Renders the all-products listing page.
-
#line ⇒ Object
Legacy route: 301 redirects to new hierarchical URL.
-
#reviews ⇒ Object
Legacy route: 301 redirects to new hierarchical URL.
-
#section ⇒ Object
Lazy-loaded section endpoint for below-the-fold content Renders a single section via Turbo Frame for performance.
Methods included from Controllers::TrackingDetection
#bot_request?, #gdpr_country?, #gdpr_country_data, #prevent_bots, #set_tracking_cookie, #track_visitor?
Methods inherited from BasePortalController
#current_ability, #portal_party, #set_webpack
Methods included from Controllers::MasqueradeGuarded
block_while_masquerading, #masquerade_blocks?
Methods inherited from ApplicationController
#account_impersonated?, #add_to_flash, #after_sign_in_path_for, #bypass_forgery_protection?, #chat_enabled?, #cloudflare_cleared?, #default_catalog, #default_url_options, #enable_turbo_frames, #find_publication, #fix_invalid_accept_header, #init_js_utils, #is_globals_call?, #layout_by_resource, #locale_store, #redirect_to, #require_employee_for_crm, #set_base_host, #set_real_ip, #set_report_errors_for, #should_render_layout?, #skip_layout_for_turbo_frame?, #stamp_impersonation_context, #tab_frame_breakout_request?, #warmlyyours_canada_ip?, #warmlyyours_ip?, #y
Methods included from Controllers::ReturnPathHandling
#check_for_return_path, #redirect_to_return_path_or_default
Methods included from Controllers::AnalyticsEvents
#consume_queued_analytics_events, #registration_lead_type, #track_event
Methods included from Controllers::DeviceDetection
Methods included from Controllers::SubdomainDetection
#is_crm_request?, #is_www_request?, #json_request?
Methods included from Controllers::TurboSafeRedirect
Methods included from Controllers::AcceleratedFileSending
#send_file_accelerated, #send_upload_accelerated
Methods included from Controllers::ErrorRendering
#excp_string, #mail_to_for_error_reporting, #render_400, #render_404, #render_406, #render_410, #render_500, #render_invalid_authenticity_token, #render_ip_spoof_error, #render_unpermitted_parameters, #safe_referer_or_fallback
Methods included from Controllers::TurnstileVerification
#load_turnstile_script_tag, #turnstile_lazy_widget, #turnstile_script_tag, #turnstile_widget, #validate_turnstile!
Methods included from Controllers::CloudflareCaching
edge_cached, #edge_cached_action?, #reset_cloudflare_cache, #set_cloudflare_cache, #skip_edge_cache!, #skip_session
Methods included from Controllers::Webpackable
#preload_webpack_fonts, #webpack_css_include, #webpack_css_url, #webpack_js_include, #wpd_is_running?
Methods included from Controllers::Localizable
#cloudflare_country_locale, #determine_request_locale, #geocoder_locale, #guest_user_locale_check, #locale_optional_www_auth_path?, #param_locale, #set_locale, #set_request_locale, #skip_localization?, #warmlyyours_ip_locale
Methods included from Controllers::Authenticable
#access_denied, #authenticate_account, #authenticate_account!, #authenticate_account_from_login_token!, #check_is_a_manager, #check_is_a_sales_manager, #check_is_an_admin, #check_is_an_employee, #check_party, #clear_mismatched_guest_user, #create_guest_user, #credentials?, #current_or_guest_user, #current_or_guest_user_id_read_only, #current_user, #devise_mapping, #fully_logged_in?, #generate_bot_id, #guest_user, #identifiable?, #init_current_user, #initialize_guest, #load_context_user, #logging_in, #resource, #resource_name, #restrict_access_for_non_employees, #scrubbed_request_path, #user_object, #warn_on_session_guest_id_leak
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?
Instance Method Details
#add_to_cart ⇒ void
This method returns an undefined value.
Adds the requested SKU/quantity to the current user's cart.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'app/controllers/www/products_controller.rb', line 62 def add_to_cart @cart = @context_user.cart @cart.save qty = params[:quantity].to_i.abs [qty, 99].min # Force to be max of 99 units to avoid issues cart_add_item(params[:sku], params[:quantity]) respond_to do |format| format.json do render json: { success: true } end end end |
#bulk_product_data ⇒ JSON
Consolidated bulk product data endpoint
Returns product data for one or more SKUs with field filtering
GET /products/bulk_product_data?skus[]=SKU1&skus[]=SKU2&fields=price
GET /products/bulk_product_data?skus[]=SKU1&fields=price,stock,shipping,cta
Reads from params:
skus[](Array): SKU codes (max 50)fields(String): Comma-separated list ofprice,stock,shipping,cta
(default: all)
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'app/controllers/www/products_controller.rb', line 155 def bulk_product_data skus = Array(params[:skus]).compact.uniq.first(50) # Limit to 50 SKUs max if skus.empty? render json: { products: {}, has_tier_pricing: false, fetched_at: Time.current.iso8601 } return end # Parse requested fields - defaults to all if not specified. Safelisted to the # client-requestable fields: `schema` is intentionally excluded so it can only # be enabled internally, per-SKU, via schema_for (below) — a raw `fields=schema` # must not trigger the (DB-touching) offer build for every SKU. allowed_fields = %w[price stock shipping cta] requested_fields = if params[:fields].present? params[:fields].split(',').map(&:strip).map(&:downcase) & allowed_fields else allowed_fields end # Get customer for tier pricing customer = current_account&.customer has_tier_pricing = customer&.pricing_program_discount.to_f.positive? # Fetch all products from the appropriate catalog catalog_id = Catalog.locale_to_catalog_id(I18n.locale) products_by_sku = ViewProductCatalog .where(catalog_id: catalog_id, item_sku: skus) .includes(:catalog_item) .preload(item: [ :primary_image, { image_profiles: :image } ]) .index_by(&:item_sku) # Offer JSON-LD is only patched onto the single Product schema on a product # detail page, so build it only for that SKU (schema_for) — not every card # SKU in the request. Each offer build hits the DB (free_shipping_regions), # so scoping avoids a query-per-card on listing/related-product fetches. # Ignore schema_for unless it's actually one of the requested SKUs, so a stray # value can't add the offer build nor fragment the response cache. schema_for = params[:schema_for].presence schema_for = nil unless schema_for && skus.include?(schema_for) # Build response for each SKU products_data = {} skus.each do |sku| vpc = products_by_sku[sku] next unless vpc # Set @pcp for consolidated_product_payload to use @pcp = Www::ProductCatalogPresenter.new(vpc, view_context) sku_fields = requested_fields.dup sku_fields << 'schema' if sku == schema_for products_data[sku] = consolidated_product_payload(fields: sku_fields, customer: customer) end # Build cache key for HTTP caching # Include customer ID if they have tier pricing to make cache personalized schema_key = schema_for ? "/schema/#{schema_for}" : '' cache_key = if has_tier_pricing "bulk_product_data/#{skus.sort.join(',')}/#{requested_fields.sort.join(',')}#{schema_key}/customer/#{customer.id}" else "bulk_product_data/#{skus.sort.join(',')}/#{requested_fields.sort.join(',')}#{schema_key}" end # Use HTTP caching with ETag return unless stale?(etag: cache_key, public: !has_tier_pricing) # Set cache time - private for tier pricing customers if has_tier_pricing expires_in 1.minute, private: true else expires_in 1.minute, public: true end render json: { products: products_data, has_tier_pricing: has_tier_pricing, program_name: has_tier_pricing ? customer.tier2_program_pricing&.title : nil, fetched_at: Time.current.iso8601 }.compact end |
#cart_add_item(sku_code, quantity = nil, room_configuration_id = nil) ⇒ void (protected)
This method returns an undefined value.
Adds a SKU/quantity to @cart and records it in @recently_added_items.
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'app/controllers/www/products_controller.rb', line 246 def cart_add_item(sku_code, quantity = nil, room_configuration_id = nil) # add item @recently_added_items ||= [] return if sku_code.blank? sku = sku_code quantity = quantity.presence quantity = (quantity || 1).to_i room_configuration_id = room_configuration_id return unless sku.present? && (quantity > 0) # find catalog by locale ci = begin @cart.catalog.catalog_items.public_catalog_items.by_skus(sku).first rescue StandardError nil end if ci @cart.recalculate_shipping = true # doesn't hurt to set it @cart.recalculate_discounts = true # ensure tier2 and auto-apply discounts are calculated @cart.force_total_reset = true @new_line = @cart.add_line_item(catalog_item_id: ci.id, quantity: quantity, room_configuration_id: room_configuration_id, do_not_autosave: true) @recently_added_items << { id: @new_line.id, sku: @new_line.sku, name: @new_line.name, category: @new_line.reported_category_name, quantity: @new_line.quantity } else flash[:error] = "We could not find sku #{sku} in catalog." end end |
#code ⇒ Object
Legacy route: 301 redirects to new hierarchical URL
50 51 52 53 54 55 56 57 |
# File 'app/controllers/www/products_controller.rb', line 50 def code result = CatalogPathResolver.new.resolve_legacy_sku(params[:sku]) if result.redirect? redirect_to result.redirect_to, status: :moved_permanently else redirect_to cms_link('/products'), status: :moved_permanently end end |
#index ⇒ void
This method returns an undefined value.
Renders the all-products listing page.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'app/controllers/www/products_controller.rb', line 12 def index @catalog = default_catalog @product_lines = @catalog.product_lines_for_sales_portal.reorder(:priority, :public_name, :slug_ltree) @h1 = 'All WarmlyYours Products' @page_title = "All WarmlyYours Products in #{Country.country_name_from_locale}" @page_description = 'Find all WarmlyYours products in a single page. Electric Radiant Floor Heating, Outdoor Snow Melting Products, Roof and Gutter Deicing Systems, Pipe Freeze Protection, Mirrors and Defoggers, Towel Warmers and Panel Heaters' @filters = [] b = { type: 'checkbox', title: 'Product Line', options: ['Floor Heating', 'Snow Melting', 'Towel Warmer', 'LED Mirror', 'Pipe Freeze Protection', 'Roof & Gutter Deicing', 'Infrared Heating Panels', 'Countertop Heater', 'Underlayment', 'Third Party Control Integration'] } @filters << b set_cloudflare_cache(time_in_secs: 4.hours.to_i, tags: %w[sale product]) fresh_when(etag: [@product_lines, I18n.locale], last_modified: @product_lines.maximum(:updated_at), public: true) end |
#line ⇒ Object
Legacy route: 301 redirects to new hierarchical URL
40 41 42 43 44 45 46 47 |
# File 'app/controllers/www/products_controller.rb', line 40 def line result = CatalogPathResolver.new.resolve_legacy_product_line(params[:product_line_url]) if result.redirect? redirect_to result.redirect_to, status: :moved_permanently else redirect_to cms_link('/products'), status: :moved_permanently end end |
#reviews ⇒ Object
Legacy route: 301 redirects to new hierarchical URL
78 79 80 81 82 83 84 85 |
# File 'app/controllers/www/products_controller.rb', line 78 def reviews result = CatalogPathResolver.new.resolve_legacy_sku(params[:sku], section: :reviews) if result.redirect? redirect_to result.redirect_to, status: :moved_permanently else redirect_to cms_link('/products'), status: :moved_permanently end end |
#section ⇒ Object
Lazy-loaded section endpoint for below-the-fold content
Renders a single section via Turbo Frame for performance
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'app/controllers/www/products_controller.rb', line 89 def section load_product(params[:sku], web_accessible_only: false, light: true) return head :not_found unless @product&.item_is_web_accessible @section_name = params[:section].to_sym @section_data = @pcp.compute_single_section(@section_name) @section_data = nil if @section_name == :related_products && @section_data.blank? @p = @pcp # Browser TTL, NOT the 3-day default: these fragments are fetched with a # `Turbo-Frame` header, which routes them through the www-edge worker's # handleTurboRequest branch — that branch returns the origin response as-is # and never applies the `Cache-Control: no-cache` + content-aware ETag # rewrite page HTML gets. So the default `public_cache_expires_in: 3.days` # became a real 3-day copy in every visitor's browser, and no edge purge # (CRM site_maps, admin bar, Item#purge_edge_cache — all of which DO cascade # to these URLs via SiteMap#section_cache_urls) could reach it. The edge TTL # is unchanged at 4h via Cloudflare-CDN-Cache-Control. set_cloudflare_cache(time_in_secs: 4.hours.to_i, public_cache_expires_in: 1.minute, tags: %w[sale product]) # Deliberately no fresh_when: the only validator available here is @product, # but a section's content turns on OTHER records — compatible controls and # accessories, product lines, publications — so an unchanged product row # would 304 browsers straight back onto content a purge had just dropped. # The fragment is edge-cached, so revalidation costs a CDN hit, not origin. end |