Module: FloorHeatingCompatibility

Defined in:
app/lib/floor_heating_compatibility.rb

Overview

Floor Heating Compatibility - Hybrid Database/Presentation Layer

This module provides floor type / heating system compatibility data by:

  1. Querying HeatingElementProductLineOption for actual compatibility (source of truth)
  2. Layering presentation config on top (icons, labels, footnotes, column headers)

Used by Www::FloorCompatibilityTableComponent to render comparison tables
on /floor-heating, /floor-heating/heating-cable, and /floor-heating/heated-floor-mat.

DATABASE SYNC:
Compatibility is derived from HeatingElementProductLineOption records where:

  • environment = 'Indoor'
  • is_public = true
    If a record exists for (floor_type_id, product_line_id), they're compatible.

PRESENTATION CONFIG:
Display-specific data (icons, column headers, footnotes) is maintained here
since these are presentation-layer concerns not stored in the database.

Constant Summary collapse

HEATING_SYSTEMS =

Heating system presentation config
:product_line_slug maps to product_lines.slug_ltree in database
:category is :cable or :mat (for filtering on subpages)
:column_header is the HTML-safe column header with optional subtitle
:variant distinguishes installation methods for the same product line
Order: Mats first (TempZone, Environ, Slab Heat Mat), then Cables (TempZone Grip, Prodeso, Slab Heat Cable)

[
  {
    key: :tempzone_mat,
    name: 'TempZone Mat',
    product_line_slug: LtreePaths::PL_FLOOR_HEATING_TEMPZONE_FLEX_ROLL,
    category: :mat,
    wattage: '15W/sq ft',
    column_header: 'TempZone™ Mats<br><small class="fw-normal text-muted">(Flex Roll & Easy Mats) 15W/sq ft</small>'
  },
  {
    key: :environ_mat,
    name: 'Environ Mat',
    product_line_slug: LtreePaths::PL_FLOOR_HEATING_ENVIRON_FLEX_ROLL,
    category: :mat,
    wattage: '12W/sq ft',
    column_header: 'Environ™ Mats<br><small class="fw-normal text-muted">12W/sq ft</small>'
  },
  {
    key: :tempzone_cable_grip,
    name: 'TempZone Cable with Grip Strips',
    product_line_slug: LtreePaths::PL_FLOOR_HEATING_TEMPZONE_CABLE,
    category: :cable,
    variant: :grip_strips,
    wattage: '9-15W/sq ft',
    column_header: 'TempZone™ Cable<br><small class="fw-normal text-muted">with Grip Strips</small>'
  },
  {
    key: :tempzone_cable_prodeso,
    name: 'TempZone Cable + Prodeso',
    product_line_slug: LtreePaths::PL_FLOOR_HEATING_UNDERLAYMENT_PRODESO,
    category: :cable,
    variant: :prodeso,
    wattage: '9-15W/sq ft',
    column_header: 'TempZone™ Cable<br><small class="fw-normal text-muted">+ Prodeso Membrane</small>'
  },
  {
    key: :slab_heat_mat,
    name: 'Slab Heat Mat',
    product_line_slug: LtreePaths::PL_FLOOR_HEATING_SLAB_HEAT_MAT,
    category: :mat,
    wattage: '20W/sq ft',
    column_header: 'Slab Heat Mat<br><small class="fw-normal text-muted">20W/sq ft</small>'
  },
  {
    key: :slab_heat_cable,
    name: 'Slab Heat Cable',
    product_line_slug: LtreePaths::PL_FLOOR_HEATING_SLAB_HEAT_CABLE,
    category: :cable,
    wattage: '15-20W/sq ft',
    column_header: 'Slab Heat Cable<br><small class="fw-normal text-muted">15-20W/sq ft</small>'
  }
].freeze
FLOOR_TYPES =

Floor type presentation config
Maps database floor types to display groupings with icons and URLs
:db_seo_keys lists all floor_types.seo_key values that belong to this group

[
  {
    key: :tile,
    name: 'Tile, Marble, Stone',
    icon: 'grid-2',
    url: '/floor-heating/tile-marble-or-stone',
    db_seo_keys: %w[tile-marble-or-stone stone]
  },
  {
    key: :hardwood,
    name: 'Hardwood',
    icon: 'tree',
    url: '/floor-heating/nailed-hardwood',
    db_seo_keys: %w[wood-nailed wood-glued]
  },
  {
    key: :engineered,
    name: 'Engineered Wood',
    icon: 'layer-group',
    url: '/floor-heating/engineered-wood',
    db_seo_keys: %w[engineered-nailed engineered-glued engineered-floating]
  },
  {
    key: :bamboo,
    name: 'Bamboo',
    icon: 'leaf',
    url: '/floor-heating/bamboo',
    db_seo_keys: %w[bamboo-nailed bamboo-glued bamboo-floating]
  },
  {
    key: :laminate,
    name: 'Laminate',
    icon: 'th',
    url: '/floor-heating/laminate',
    db_seo_keys: %w[laminate-click-together-floating laminate-glued-together-floating laminate-nailed]
  },
  {
    key: :lvt_vinyl,
    name: 'LVT / Vinyl',
    icon: 'table-columns',
    url: '/floor-heating/luxury-vinyl-tiles',
    db_seo_keys: %w[resilients-vinyl-and-luxury-vinyl-tile-lvt lvt-floating]
  },
  {
    key: :carpet,
    name: 'Carpet',
    icon: 'rug',
    url: '/floor-heating/carpet',
    db_seo_keys: %w[carpet carpet-glued]
  },
  {
    key: :cork,
    name: 'Cork',
    icon: 'circle',
    url: nil, # No dedicated cork page
    db_seo_keys: %w[cork]
  },
  {
    key: :concrete,
    name: 'Concrete Slab',
    icon: 'cubes-stacked',
    url: '/floor-heating/concrete',
    db_seo_keys: %w[unfinished-stamped-or-colored-concrete]
  }
].freeze
INSTALLATION_LABELS =

Installation method presentation labels

{
  nailed: 'Nailed',
  glued: 'Glued',
  floating: 'Floating',
  thinset: 'Thinset',
  stretch_in: 'Stretch-In',
  in_slab: 'In-slab'
}.freeze
SEO_KEY_TO_INSTALLATION =

Maps floor_type seo_key to installation method symbol
This extracts the installation method from the database floor type name

{
  'tile-marble-or-stone' => :thinset,
  'stone' => :thinset,
  'wood-nailed' => :nailed,
  'wood-glued' => :glued,
  'engineered-nailed' => :nailed,
  'engineered-glued' => :glued,
  'engineered-floating' => :floating,
  'bamboo-nailed' => :nailed,
  'bamboo-glued' => :glued,
  'bamboo-floating' => :floating,
  'laminate-click-together-floating' => :floating,
  'laminate-glued-together-floating' => :floating,
  'laminate-nailed' => :nailed,
  'resilients-vinyl-and-luxury-vinyl-tile-lvt' => :glued,
  'lvt-floating' => :floating,
  'carpet' => :stretch_in,
  'carpet-glued' => :glued,
  'cork' => :floating,
  'unfinished-stamped-or-colored-concrete' => :in_slab
}.freeze
FOOTNOTES_ORDER =

Footnote definitions for special compatibility notes
These provide additional context beyond simple yes/no compatibility
Note: Markers are assigned dynamically by the component based on which footnotes are used
Order here determines the display order when multiple footnotes are shown

%i[slab_any_floor prodeso_tile_only floating_environ environ_flooring_check floating_over_slc mat_sleepers mat_with_slc carpet_us_only].freeze
FOOTNOTES =
{
  slab_any_floor: {
    text: 'Slab Heat systems are embedded in the concrete slab; any finished flooring can be installed over the cured slab.'
  },
  prodeso_tile_only: {
    text: 'Prodeso membrane is designed for tile/stone with thinset. Wood/vinyl/carpet floors use Grip Strips instead.'
  },
  floating_environ: {
    text: 'Floating floors and stretch-in carpet require Environ™ mats, or use Slab Heat Cable for in-slab heating.'
  },
  environ_flooring_check: {
    text: 'Consult your flooring manufacturer to ensure the flooring can be installed directly over a heating system. If embedding is required, you must use a TempZone or Slab Heat system.'
  },
  floating_over_slc: {
    text: 'Floating floor can be installed over an embedded TempZone system in 3/8–½" self-leveling cement (SLC), fully cured.'
  },
  mat_sleepers: {
    text: 'Nailed floors: Install wood sleepers across subfloor, lay mat between sleepers, cover with self-leveling cement, then nail flooring into sleepers only.'
  },
  mat_with_slc: {
    text: 'Glued floors: Embed mat in 3/8–½" self-leveling cement, allow to cure, then install flooring with adhesive.'
  },
  carpet_us_only: {
    text: 'Carpet floor heating is available in the U.S. only.'
  }
}.freeze
COMPATIBILITY_NOTES =

Special compatibility overrides and notes
Format: { [floor_key, installation, system_key] => { status:, footnote:, note: } }
Use this for cases where database compatibility needs presentation enhancement

Key distinction:

  • TempZone Cable with Grip Strips: Works with any floor where cable goes in thinset/SLC
  • TempZone Cable + Prodeso: ONLY for tile/stone (Prodeso membrane is tile-specific)
{
  # === TempZone Cable with Grip Strips ===
  # Works with tile and adhered floors (cable in thinset/SLC)
  %i[tile thinset tempzone_cable_grip] => { status: :yes },
  %i[hardwood nailed tempzone_cable_grip] => { status: :yes },
  %i[hardwood glued tempzone_cable_grip] => { status: :yes },
  %i[engineered nailed tempzone_cable_grip] => { status: :yes },
  %i[engineered glued tempzone_cable_grip] => { status: :yes },
  %i[bamboo nailed tempzone_cable_grip] => { status: :yes },
  %i[bamboo glued tempzone_cable_grip] => { status: :yes },
  %i[laminate nailed tempzone_cable_grip] => { status: :yes },
  %i[lvt_vinyl glued tempzone_cable_grip] => { status: :yes },
  %i[lvt_vinyl floating tempzone_cable_grip] => { status: :yes, footnote: :floating_over_slc },
  %i[carpet glued tempzone_cable_grip] => { status: :yes, us_only: true },
  # Compatible with floating floors when embedded in SLC
  %i[engineered floating tempzone_cable_grip] => { status: :yes, footnote: :floating_over_slc },
  %i[bamboo floating tempzone_cable_grip] => { status: :yes, footnote: :floating_over_slc },
  %i[laminate floating tempzone_cable_grip] => { status: :yes, footnote: :floating_over_slc },
  %i[carpet stretch_in tempzone_cable_grip] => { status: :no, footnote: :floating_environ, us_only: true },
  %i[cork floating tempzone_cable_grip] => { status: :yes, footnote: :floating_over_slc },
  %i[concrete in_slab tempzone_cable_grip] => { status: :no },

  # === TempZone Cable + Prodeso Membrane ===
  # ONLY works with tile/stone - Prodeso is a tile uncoupling membrane
  %i[tile thinset tempzone_cable_prodeso] => { status: :yes, note: 'Recommended' },
  %i[hardwood nailed tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[hardwood glued tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[engineered nailed tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[engineered glued tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[engineered floating tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[bamboo nailed tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[bamboo glued tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[bamboo floating tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[laminate floating tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[laminate nailed tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[lvt_vinyl glued tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[lvt_vinyl floating tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[carpet glued tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only, us_only: true },
  %i[carpet stretch_in tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only, us_only: true },
  %i[cork floating tempzone_cable_prodeso] => { status: :no, footnote: :prodeso_tile_only },
  %i[concrete in_slab tempzone_cable_prodeso] => { status: :no },

  # === Slab Heat Cable ===
  # Works with everything (goes under the slab) - show as warning since it's indirect
  %i[tile thinset slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[hardwood nailed slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[hardwood glued slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[engineered nailed slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[engineered glued slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[engineered floating slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[bamboo nailed slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[bamboo glued slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[bamboo floating slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[laminate floating slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[laminate nailed slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[lvt_vinyl glued slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[lvt_vinyl floating slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[carpet glued slab_heat_cable] => { status: :warning, footnote: :slab_any_floor, us_only: true },
  %i[carpet stretch_in slab_heat_cable] => { status: :warning, footnote: :slab_any_floor, us_only: true },
  %i[cork floating slab_heat_cable] => { status: :warning, footnote: :slab_any_floor },
  %i[concrete in_slab slab_heat_cable] => { status: :yes, note: 'Primary' },

  # === TempZone Mats ===
  # Works with adhered floors (tile direct, others with SLC)
  %i[tile thinset tempzone_mat] => { status: :yes, note: 'Direct' },
  %i[hardwood nailed tempzone_mat] => { status: :yes, footnote: :mat_sleepers, note: 'with sleepers' },
  %i[hardwood glued tempzone_mat] => { status: :yes, footnote: :mat_with_slc, note: 'with SLC' },
  %i[engineered nailed tempzone_mat] => { status: :yes, footnote: :mat_sleepers, note: 'with sleepers' },
  %i[engineered glued tempzone_mat] => { status: :yes, footnote: :mat_with_slc, note: 'with SLC' },
  %i[engineered floating tempzone_mat] => { status: :yes, footnote: :floating_over_slc },
  %i[bamboo nailed tempzone_mat] => { status: :yes, footnote: :mat_sleepers, note: 'with sleepers' },
  %i[bamboo glued tempzone_mat] => { status: :yes, footnote: :mat_with_slc, note: 'with SLC' },
  %i[bamboo floating tempzone_mat] => { status: :yes, footnote: :floating_over_slc },
  %i[laminate nailed tempzone_mat] => { status: :yes, footnote: :mat_sleepers, note: 'with sleepers' },
  %i[laminate floating tempzone_mat] => { status: :yes, footnote: :floating_over_slc },
  %i[lvt_vinyl glued tempzone_mat] => { status: :yes, footnote: :mat_with_slc, note: 'with SLC' },
  %i[lvt_vinyl floating tempzone_mat] => { status: :yes, footnote: :floating_over_slc },
  %i[carpet glued tempzone_mat] => { status: :yes, footnote: :mat_with_slc, note: 'with SLC', us_only: true },
  %i[carpet stretch_in tempzone_mat] => { status: :no, us_only: true },
  %i[cork floating tempzone_mat] => { status: :yes, footnote: :floating_over_slc },
  %i[concrete in_slab tempzone_mat] => { status: :no },

  # === Environ Mats ===
  # Works with floating floors and stretch-in carpet (direct installation)
  # Note: Floating floor entries include footnote to check with flooring manufacturer
  %i[tile thinset environ_mat] => { status: :no },
  %i[hardwood nailed environ_mat] => { status: :no },
  %i[hardwood glued environ_mat] => { status: :no },
  %i[engineered nailed environ_mat] => { status: :no },
  %i[engineered glued environ_mat] => { status: :no },
  %i[engineered floating environ_mat] => { status: :yes, footnote: :environ_flooring_check },
  %i[bamboo nailed environ_mat] => { status: :no },
  %i[bamboo glued environ_mat] => { status: :no },
  %i[bamboo floating environ_mat] => { status: :yes, footnote: :environ_flooring_check },
  %i[laminate nailed environ_mat] => { status: :no },
  %i[laminate floating environ_mat] => { status: :yes, footnote: :environ_flooring_check },
  %i[lvt_vinyl glued environ_mat] => { status: :no },
  %i[lvt_vinyl floating environ_mat] => { status: :warning, footnote: :environ_flooring_check },
  %i[carpet glued environ_mat] => { status: :no, us_only: true },
  %i[carpet stretch_in environ_mat] => { status: :yes, note: 'Direct', us_only: true },
  %i[cork floating environ_mat] => { status: :yes, footnote: :environ_flooring_check },
  %i[concrete in_slab environ_mat] => { status: :no },

  # === Slab Heat Mat ===
  # Works with everything (goes in the concrete slab) - show as warning since it's indirect
  %i[tile thinset slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[hardwood nailed slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[hardwood glued slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[engineered nailed slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[engineered glued slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[engineered floating slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[bamboo nailed slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[bamboo glued slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[bamboo floating slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[laminate floating slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[laminate nailed slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[lvt_vinyl glued slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[lvt_vinyl floating slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[carpet glued slab_heat_mat] => { status: :warning, footnote: :slab_any_floor, us_only: true },
  %i[carpet stretch_in slab_heat_mat] => { status: :warning, footnote: :slab_any_floor, us_only: true },
  %i[cork floating slab_heat_mat] => { status: :warning, footnote: :slab_any_floor },
  %i[concrete in_slab slab_heat_mat] => { status: :yes, note: 'Primary' }
}.freeze

Class Method Summary collapse

Class Method Details

.clear_cache!Object

Cache clearing for development/testing



442
443
444
# File 'app/lib/floor_heating_compatibility.rb', line 442

def clear_cache!
  @compatibility_cache = nil
end

.compatibility_for(floor_key, installation, system_key) ⇒ Object

Check compatibility from database with presentation overlay
Returns: [status, footnote_key, note, us_only]



370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'app/lib/floor_heating_compatibility.rb', line 370

def compatibility_for(floor_key, installation, system_key)
  # First check if there's a presentation override
  override = COMPATIBILITY_NOTES[[floor_key, installation, system_key]]
  if override
    return [
      override[:status],
      override[:footnote],
      override[:note],
      override[:us_only]
    ]
  end

  # Query database for actual compatibility
  compatible = database_compatible?(floor_key, installation, system_key)

  if compatible
    [:yes, nil, nil, nil]
  else
    [:no, nil, nil, nil]
  end
end

.database_compatible?(floor_key, installation, system_key) ⇒ Boolean

Query HeatingElementProductLineOption to check if compatible

Returns:

  • (Boolean)


393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'app/lib/floor_heating_compatibility.rb', line 393

def database_compatible?(floor_key, installation, system_key)
  # Get the floor type's db_seo_keys for this installation
  floor_config = FLOOR_TYPES.find { |f| f[:key] == floor_key }
  return false unless floor_config

  # Find matching seo_keys for this installation method
  matching_seo_keys = floor_config[:db_seo_keys].select do |seo_key|
    SEO_KEY_TO_INSTALLATION[seo_key] == installation
  end
  return false if matching_seo_keys.empty?

  # Get the product line slug for this heating system
  system_config = HEATING_SYSTEMS.find { |s| s[:key] == system_key }
  return false unless system_config

  product_line_slug = system_config[:product_line_slug]

  # Check if any matching floor type is compatible with this product line
  HeatingElementProductLineOption
    .joins(:floor_type, :product_line)
    .where(
      environment: 'Indoor',
      is_public: true
    )
    .where(floor_types: { seo_key: matching_seo_keys })
    .where(product_lines: { slug_ltree: product_line_slug })
    .exists?
end

.floor_typesObject

Returns floor types with their installation methods derived from database



358
359
360
361
362
363
364
365
366
# File 'app/lib/floor_heating_compatibility.rb', line 358

def floor_types
  FLOOR_TYPES.map do |ft|
    installations = ft[:db_seo_keys].filter_map do |seo_key|
      SEO_KEY_TO_INSTALLATION[seo_key]
    end.uniq

    ft.merge(installations: installations)
  end
end

.footnotes_for_systems(system_keys) ⇒ Object

Returns footnotes relevant to the displayed systems



423
424
425
426
427
428
429
430
431
432
433
434
# File 'app/lib/floor_heating_compatibility.rb', line 423

def footnotes_for_systems(system_keys)
  used_footnote_keys = Set.new

  # Check all compatibility notes for used footnotes
  COMPATIBILITY_NOTES.each do |(floor_key, installation, sys_key), config|
    next unless system_keys.include?(sys_key) && config[:footnote]

    used_footnote_keys << config[:footnote]
  end

  FOOTNOTES.slice(*used_footnote_keys.to_a)
end

.heating_systems(category: nil) ⇒ Object

Returns heating systems, optionally filtered by category



351
352
353
354
355
# File 'app/lib/floor_heating_compatibility.rb', line 351

def heating_systems(category: nil)
  systems = HEATING_SYSTEMS
  systems = systems.select { |s| s[:category] == category } if category
  systems
end

.installation_label(key) ⇒ Object

Returns human-readable installation method label



437
438
439
# File 'app/lib/floor_heating_compatibility.rb', line 437

def installation_label(key)
  INSTALLATION_LABELS[key] || key.to_s.titleize
end