Class: HeatingSystemCalculator::AccessoriesFinder

Inherits:
BaseItemFinder show all
Defined in:
app/services/heating_system_calculator/accessories_finder.rb

Constant Summary collapse

FREEFORM_CABLE_ACCESSORY_SKUS =

SKUs for free-form cable accessories that require no membrane and existing concrete subfloor

[
  ItemConstants::TZ_CABLE_GRIPSTRIP_SKU,
  ItemConstants::TZ_CABLE_STRIP_PL_SKU,
  ItemConstants::TZ_CABLE_TAPE_SKU,
  ItemConstants::TZ_CABLE_CONCRETE_SCREW_LARGE_SKU,
  ItemConstants::TZ_CABLE_CONCRETE_SCREW_SMALL_SKU,
  ItemConstants::TZ_CABLE_CONCRETE_DRILL_BIT_SKU
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from BaseItemFinder

#get_number_of_poles_from_consolidated_elements, #get_total_from_solution

Constructor Details

#initialize(options) ⇒ AccessoriesFinder

Returns a new instance of AccessoriesFinder.



7
8
9
10
11
12
13
14
# File 'app/services/heating_system_calculator/accessories_finder.rb', line 7

def initialize(options)
  @heated_area = options[:heated_area]
  @heating_system = options[:heating_system]
  @accessory_set = options[:accessory_set]
  @underlayment_set = options[:underlayment_set]
  @rough_in_kit_for_tstat = options[:rough_in_kit_for_tstat]
  @rough_in_kit_for_power_module = options[:rough_in_kit_for_power_module]
end

Instance Attribute Details

#accessoriesObject (readonly)

Returns the value of attribute accessories.



5
6
7
# File 'app/services/heating_system_calculator/accessories_finder.rb', line 5

def accessories
  @accessories
end

#errorObject (readonly)

Returns the value of attribute error.



5
6
7
# File 'app/services/heating_system_calculator/accessories_finder.rb', line 5

def error
  @error
end

Instance Method Details

#calculate_elements_qty_and_tz_cable_accessories_qty(elements) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'app/services/heating_system_calculator/accessories_finder.rb', line 116

def calculate_elements_qty_and_tz_cable_accessories_qty(elements)
  # decide on quantities, generally one per heating element except for TZ cable guides
  qty = 0
  tz_cable_accessories_qtys = {}
  # decide if we find tz cable strips and related accessories
  find_tz_cable_strips = RoomConfiguration::PUBLIC_TZ_CABLE_SPACINGS.include?(@heating_system.cable_spacing) && !@heating_system.membrane_recommended ? true : false

  # Batch load all element items upfront to avoid N+1 queries
  element_skus = elements.map { |e| e['sku'] }.compact
  items_by_sku = Item.where(sku: element_skus).index_by(&:sku)

  elements.each do |e|
    qty += e['qty'] || 1
    next unless @heating_system.heating_system_product_line.is_cable_system?

    item = items_by_sku[e['sku']]
    next unless item && (item.is_tz_thin_cable? || item.is_tz_cable?) && find_tz_cable_strips

    cable_spacing = @heating_system.cable_spacing.to_f.round
    number_of_strips = item.calculate_number_of_fixing_strips_at_cable_spacing(cable_spacing)
    if number_of_strips&.> 0
      tz_cable_accessories_qtys[ItemConstants::TZ_CABLE_GRIPSTRIP_SKU] ||= 0
      tz_cable_accessories_qtys[ItemConstants::TZ_CABLE_GRIPSTRIP_SKU] += number_of_strips
    end
  end

  num_strips = ItemConstants::TZ_CABLE_STRIP_SKUS.map { |sku| tz_cable_accessories_qtys[sku].to_i }.sum

  if @heating_system.sub_floor_type&.seo_key&.index('concrete') && num_strips.positive?
    # we have concrete subfloor and TZ cable, we need concrete drill bits, screws and tape
    # figure out the number of screws
    num_screws = (num_strips * HeatingSystemItems::SCREWS_PER_STRIP_WITH_OVERAGE).round
    # figure out the length of tape
    length_tape = (num_strips * HeatingSystemItems::TAPE_FEET_PER_STRIP_WITH_OVERAGE).round
    qtys_by_sku = {}
    # calculate number of screw packs and drill bit of various skus
    # if we are most of the way into a 100 pack take that because of the included drill bit
    large_pack_size = HeatingSystemItems::CONCRETE_SCREW_PACK_LARGE_SIZE
    small_pack_size = HeatingSystemItems::CONCRETE_SCREW_PACK_SMALL_SIZE
    qtys_by_sku[ItemConstants::TZ_CABLE_CONCRETE_SCREW_LARGE_SKU] = (num_screws / large_pack_size).round
    # figure out if we need any small packs to reach the number of screws needed
    num_exc_screws = (num_screws - (large_pack_size * qtys_by_sku[ItemConstants::TZ_CABLE_CONCRETE_SCREW_LARGE_SKU]))
    qtys_by_sku[ItemConstants::TZ_CABLE_CONCRETE_SCREW_SMALL_SKU] = (num_exc_screws.to_f / small_pack_size).ceil if num_exc_screws.positive?
    # figure out if we need to enclose a drill bit
    qtys_by_sku[ItemConstants::TZ_CABLE_CONCRETE_DRILL_BIT_SKU] = 1 if qtys_by_sku[ItemConstants::TZ_CABLE_CONCRETE_SCREW_LARGE_SKU].zero?
    # calculate number of tape items of various skus
    # for now we assume only the TZ cable double-sided tape
    tape_roll_length = HeatingSystemItems::TZ_CABLE_TAPE_ROLL_LENGTH_FT
    qtys_by_sku[ItemConstants::TZ_CABLE_TAPE_SKU] = (length_tape.to_f / tape_roll_length).ceil
    # iterate over these to populate tz_cable_accessories_qtys, but only when qty > 0
    qtys_by_sku.each do |sku, q|
      tz_cable_accessories_qtys[sku] = q if q.to_i.positive?
    end
  end
  [qty, tz_cable_accessories_qtys]
end

#calculate_tstat_qty_and_pm_control_qty(controls) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
# File 'app/services/heating_system_calculator/accessories_finder.rb', line 104

def calculate_tstat_qty_and_pm_control_qty(controls)
  tstat_qty = 1
  pm_control_qty = 0
  # controls are an array of control hashes with a bundle sku, so group by that and take the first (nSpire series tstats use power modules in exactly the same way so they are all the same) which will yield an array with two values: a bundle sku and an array of control hashes, count the pms in the array of control hashes
  if controls.any? { |c| c['symbol'] == 'RELAY' }
    controls.each do |c|
      pm_control_qty += c['qty'] if c['symbol'] == 'RELAY'
    end
  end
  [tstat_qty, pm_control_qty]
end

#find_accessories(elements, controls) ⇒ Object



16
17
18
19
20
21
22
23
24
25
# File 'app/services/heating_system_calculator/accessories_finder.rb', line 16

def find_accessories(elements, controls)
  @accessories = []
  if @heating_system.heating_system_product_line.snow_melt?
    select_snow_melting_accessories(elements, controls)
  else
    select_floor_heating_accessories(elements, controls)
    select_underlayment(elements, controls)
  end
  self
end

#select_floor_heating_accessories(elements, controls) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'app/services/heating_system_calculator/accessories_finder.rb', line 55

def select_floor_heating_accessories(elements, controls)
  # calculate number of rough-in kits with 2 conduits for the tstat and with 1 conduit for each pm
  tstat_qty, pm_control_qty = calculate_tstat_qty_and_pm_control_qty(controls)

  # calculate quantities of elements and tz cable accessories, like strips, tape and screws
  qty, tz_cable_accessories_qtys = calculate_elements_qty_and_tz_cable_accessories_qty(elements)

  # Batch load all accessory items upfront to avoid N+1 queries
  accessory_skus = @accessory_set.map { |a| a['sku'] }.compact
  items_by_sku = Item.where(sku: accessory_skus)
                     .includes(:primary_product_line, :exclusive_item_group)
                     .index_by(&:sku)

  # Determine if free-form cable accessories should be shown
  # Only show fixing strips, tape, concrete screws/drill bits when:
  # 1. No membrane is being used (membrane_recommended is false)
  # 2. AND subfloor is existing concrete
  show_freeform_cable_accessories = !@heating_system.membrane_recommended &&
                                    @heating_system.sub_floor_type&.seo_key == 'existing-concrete-slab'

  # populate the accessories
  @accessory_set.each do |accessory|
    next unless (acc_item = items_by_sku[accessory['sku']])
    # Skip free-form cable accessories when membrane is used or subfloor is not existing concrete
    next if FREEFORM_CABLE_ACCESSORY_SKUS.include?(accessory['sku']) && !show_freeform_cable_accessories

    if @heating_system.heating_system_product_line.is_cable_system? && tz_cable_accessories_qtys[accessory['sku']].to_i.positive?
      # @accessories << accessory.merge({'qty' => tz_cable_accessories_qtys[accessory['sku']], 'symbol' => 'CABLESTRIP'})
      @accessories << accessory.merge({ 'qty' => tz_cable_accessories_qtys[accessory['sku']], 'symbol' => 'CABLESTRIP', 'price' => 0.0 }) # force TC stripts to be free with a quoted system
    elsif !acc_item.primary_product_line.is_cable_system? && !acc_item.is_membrane?
      # here we have non tz cable accessories
      @accessories << if acc_item.exclusive_item_group # installation kits, these need the exclusive groupings since you only choose one within the group ie radio button
                        accessory.merge({ 'qty' => 1, 'exclusive_group_type' => acc_item.exclusive_item_group.key,
                                          'exclusive_group_name' => acc_item.exclusive_item_group.name })
                      elsif acc_item.is_circuit_check? # circuit check: 1 per heating element
                        accessory.merge({ 'qty' => qty })
                      else # any other accessory
                        accessory.merge({ 'qty' => 1 })
                      end
    end
  end
  # treat tstat rough in kits separately since they are differently categorized
  @accessories << @rough_in_kit_for_tstat.merge({ 'qty' => tstat_qty }) if tstat_qty.positive? && @rough_in_kit_for_tstat
  # treat power module rough in kits separately since they are differently categorized
  return unless pm_control_qty.positive?

  @accessories << @rough_in_kit_for_power_module.merge({ 'qty' => pm_control_qty }) if @rough_in_kit_for_power_module
end

#select_snow_melting_accessories(elements, _controls) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'app/services/heating_system_calculator/accessories_finder.rb', line 27

def select_snow_melting_accessories(elements, _controls)
  # add SMP sku if present
  accessory = @accessory_set.detect { |a| a['sku'] == 'SMP' }
  @accessories << accessory.merge({ 'qty' => 1 }) if accessory
  # add junction box sku based on the number of heating elements
  selected_junction_box = 0
  selected_junction_box_num_cold_leads = HeatingSystemCalculator::HeatingSystemItems::JUNCTION_BOX_COLD_LEAD_MAX_NUMBERS[0]
  elements_count = elements.to_a.sum { |e| e['qty'] }
  HeatingSystemCalculator::HeatingSystemItems::JUNCTION_BOX_COLD_LEAD_MAX_NUMBERS.each_with_index do |jb_num_cold_leads, i|
    selected_junction_box = i
    selected_junction_box_num_cold_leads = HeatingSystemCalculator::HeatingSystemItems::JUNCTION_BOX_COLD_LEAD_MAX_NUMBERS[i]
    break if jb_num_cold_leads >= elements_count
  end
  accessory = @accessory_set.detect { |a| a['sku'] == ItemConstants::JUNCTION_BOX_SKUS[selected_junction_box] }
  qty = (elements_count.to_f / selected_junction_box_num_cold_leads).ceil
  @accessories << accessory.merge({ 'qty' => qty }) if accessory
end

#select_underlayment(_elements, _controls) ⇒ Object



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
# File 'app/services/heating_system_calculator/accessories_finder.rb', line 173

def select_underlayment(_elements, _controls)
  underlayment_options = HeatingElementProductLineOption.get_suggested_underlayments({
                                                                                       sqft: @heated_area.insulation_surface,
                                                                                       sub_floor_type_id: @heating_system.sub_floor_type&.id,
                                                                                       floor_type_id: @heating_system.floor_type.id,
                                                                                       heating_system_type_name: @heating_system.heating_system_type_name,
                                                                                       catalog_id: @heating_system.catalog.id,
                                                                                       store_id: @heating_system.catalog.store_id,
                                                                                       ideal_cable_spacing: @heating_system.cable_spacing,
                                                                                       membrane_recommended: @heating_system.membrane_recommended
                                                                                     })

  underlayment_options.each do |sku, line|
    underlayment = @underlayment_set.detect { |u| u['sku'] == sku }
    if underlayment
      # Calculate area from width x length if not pre-computed (width/length in inches)
      unit_area = underlayment['area'] || (underlayment['width'].to_f * underlayment['length'].to_f / HeatingSystemItems::SQUARE_INCHES_PER_SQUARE_FOOT)
      total_sqft = (unit_area * line[:quantity]).round
      name_appended_blurb = total_sqft.positive? ? ", total sq.ft.: #{total_sqft}" : ''
      # Mark as recommended since it came from get_suggested_underlayments
      @accessories << underlayment.merge({
                                           'qty' => line[:quantity],
                                           'name' => "#{underlayment['name']}#{name_appended_blurb}",
                                           'recommended' => true
                                         })
    else
      @error = { error_status: :underlayment_not_found, error_message: "Required underlayment with SKU #{sku} not found." }
      return self
    end
  end
end