Class: ViewQuoteBomItem

Inherits:
ApplicationViewRecord show all
Defined in:
app/models/view_quote_bom_item.rb

Overview

== Schema Information

Table name: view_quote_bom_items
Database name: primary

id :integer
all_product_line_path_slugs :ltree is an Array
all_product_line_paths :ltree is an Array
amps :float
area_sqft :float
area_sqin :float
coupon_description :text
coupon_expiration :date
coupon_title :text
effective_public_name :string
effective_short_description :string
effective_tag_line :string
feature_1 :string
feature_2 :string
feature_3 :string
feature_4 :string
feature_5 :string
hero_url :text
img_url :text
is_dual_voltage :boolean
item_type :text
last_updated_at :datetime
length :float
length_ft :float
name :string(255)
price :float
product_category_path_ids :ltree
product_category_path_slugs :ltree
product_category_url :string(255)
product_line_path_ids :ltree
product_line_path_slugs :ltree
product_line_slug_ltree :ltree
product_line_tagline :string
qty_available :integer
quote_builder_video_uid :string
sale_in_effect :boolean
sale_price :float
short_description :string(120)
sku :string
symbol :text
third_party_part_number :string(255)
third_party_sku :string
unlimited_inventory :boolean
watts :float
width :float
width_ft :float
catalog_id :integer
coupon_id :integer
item_id :integer
primary_product_line_id :integer
store_item_id :integer
voltage_id :integer

Indexes

index_view_quote_bom_items_on_all_pl_path_slugs (all_product_line_path_slugs) USING gin
index_view_quote_bom_items_on_all_pl_paths_gist (all_product_line_paths) USING gist
index_view_quote_bom_items_on_catalog_type (catalog_id,item_type)
index_view_quote_bom_items_on_id (id) UNIQUE
index_view_quote_bom_items_on_last_updated (last_updated_at)
index_view_quote_bom_items_on_pc_path_gist (product_category_path_ids) USING gist
index_view_quote_bom_items_on_pc_path_slugs_gist (product_category_path_slugs) USING gist
index_view_quote_bom_items_on_pl_path_gist (product_line_path_ids) USING gist
index_view_quote_bom_items_on_pl_path_slugs_gist (product_line_path_slugs) USING gist
index_view_quote_bom_items_on_pl_slug_ltree (product_line_slug_ltree) USING gist
index_view_quote_bom_items_on_sku (sku)
index_view_quote_bom_items_on_voltage (voltage_id) WHERE (item_type = 'element'::text)

Constant Summary

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationViewRecord

create, create!, #readonly?

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Class Method Details

.accessoriesActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are accessories. Active Record Scope

Returns:

See Also:



85
# File 'app/models/view_quote_bom_item.rb', line 85

scope :accessories, -> { where(item_type: 'accessory') }

.controlsActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are controls. Active Record Scope

Returns:

See Also:



82
# File 'app/models/view_quote_bom_item.rb', line 82

scope :controls, -> { where(item_type: 'control') }

.elementsActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are elements. Active Record Scope

Returns:

See Also:



81
# File 'app/models/view_quote_bom_item.rb', line 81

scope :elements, -> { where(item_type: 'element') }

.for_any_product_line_pathActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are for any product line path. Active Record Scope

Returns:

See Also:



105
106
107
108
109
110
# File 'app/models/view_quote_bom_item.rb', line 105

scope :for_any_product_line_path, ->(ltree_paths) {
  paths = [ltree_paths].flatten.compact
  return none if paths.empty?

  where('all_product_line_paths && ARRAY[?]::ltree[]', paths)
}

.for_catalogActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are for catalog. Active Record Scope

Returns:

See Also:



92
# File 'app/models/view_quote_bom_item.rb', line 92

scope :for_catalog, ->(catalog_id) { where(catalog_id: catalog_id) }

.for_product_line_hierarchyActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are for product line hierarchy. Active Record Scope

Returns:

See Also:



96
97
98
# File 'app/models/view_quote_bom_item.rb', line 96

scope :for_product_line_hierarchy, ->(ltree_path) {
  where(ViewQuoteBomItem[:product_line_path_ids].ltree_descendant(ltree_path))
}

.for_voltageActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are for voltage. Active Record Scope

Returns:

See Also:



101
# File 'app/models/view_quote_bom_item.rb', line 101

scope :for_voltage, ->(voltage_id) { where(voltage_id: voltage_id) }

.get_accessories(catalog_id:, product_line_id:) ⇒ Object

Get accessories for a product line



255
256
257
258
259
260
261
262
263
264
# File 'app/models/view_quote_bom_item.rb', line 255

def self.get_accessories(catalog_id:, product_line_id:)
  pl_path = product_line_ltree_path(product_line_id)
  return [] if pl_path.blank?

  for_catalog(catalog_id)
    .accessories
    .for_any_product_line_path(pl_path)
    .order_by_price
    .to_bom_array
end

.get_by_skus(catalog_id:, skus:, item_type: nil) ⇒ Array<Hash>

Get items by SKU (for snow melt controls, sensors, junction boxes)

Parameters:

  • catalog_id (Integer)
  • skus (Array<String>)

    SKUs to find

  • item_type (String, nil) (defaults to: nil)

    Optional filter by item type

Returns:

  • (Array<Hash>)

    BOM format array



280
281
282
283
284
# File 'app/models/view_quote_bom_item.rb', line 280

def self.get_by_skus(catalog_id:, skus:, item_type: nil)
  scope = for_catalog(catalog_id).where(sku: skus)
  scope = scope.where(item_type: item_type) if item_type.present?
  scope.order_by_price.to_bom_array
end

.get_controls(catalog_id:, product_line_id:) ⇒ Object

Get controls (thermostats) for a product line



219
220
221
222
223
224
225
226
227
228
# File 'app/models/view_quote_bom_item.rb', line 219

def self.get_controls(catalog_id:, product_line_id:)
  pl_path = product_line_ltree_path(product_line_id)
  return [] if pl_path.blank?

  for_catalog(catalog_id)
    .controls
    .for_any_product_line_path(pl_path)
    .order_by_price
    .to_bom_array
end

.get_elements(catalog_id:, product_line_url:, voltage_id: nil, cable_spacing: nil, width_filter: nil) ⇒ Array<Hash>

Get heating elements for a product line (uses ltree descendant matching)

Parameters:

  • catalog_id (Integer)
  • product_line_url (String)
  • voltage_id (Integer, nil) (defaults to: nil)
  • cable_spacing (Float, nil) (defaults to: nil)
    • for cables, runtime area calculation
  • width_filter (Float, nil) (defaults to: nil)
    • filter by specific width (e.g., 24.0 for tire tracks)

Returns:

  • (Array<Hash>)

    BOM format array



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'app/models/view_quote_bom_item.rb', line 195

def self.get_elements(catalog_id:, product_line_url:, voltage_id: nil, cable_spacing: nil, width_filter: nil)
  pl_path = product_line_ltree_path_by_url(product_line_url)
  return [] if pl_path.blank?

  # Use ltree <@ operator to find elements where product_line_path_ids is descendant of parent
  scope = for_catalog(catalog_id)
          .elements
          .for_product_line_hierarchy(pl_path)
          .order_by_watts

  scope = scope.for_voltage(voltage_id) if voltage_id.present?
  scope = scope.where(width: width_filter) if width_filter.present?

  scope.map do |item|
    bom = item.to_bom_hash

    # Recalculate area for cables based on cable_spacing
    bom['area'] = ((item.length / 12.0) * (cable_spacing / 12.0)).round(2) if cable_spacing.present? && cable_spacing.positive? && item.length.present?

    bom
  end
end

.get_junction_boxes(catalog_id:, skus: ItemConstants::JUNCTION_BOX_SKUS + ['SMP']) ⇒ Object

Get junction boxes by SKU (for snow melt)



312
313
314
# File 'app/models/view_quote_bom_item.rb', line 312

def self.get_junction_boxes(catalog_id:, skus: ItemConstants::JUNCTION_BOX_SKUS + ['SMP'])
  get_by_skus(catalog_id: catalog_id, skus: skus)
end

.get_power(catalog_id:, product_line_id:) ⇒ Object

Get power modules (relays, relay panels) for a product line



231
232
233
234
235
236
237
238
239
240
# File 'app/models/view_quote_bom_item.rb', line 231

def self.get_power(catalog_id:, product_line_id:)
  pl_path = product_line_ltree_path(product_line_id)
  return [] if pl_path.blank?

  for_catalog(catalog_id)
    .power_modules
    .for_any_product_line_path(pl_path)
    .order_by_price
    .to_bom_array
end

.get_relay_panels(catalog_id:, skus: ItemConstants::RELAY_PANEL_SKUS) ⇒ Object

Get relay panels by SKU (shared across all product lines)



267
268
269
270
271
272
273
# File 'app/models/view_quote_bom_item.rb', line 267

def self.get_relay_panels(catalog_id:, skus: ItemConstants::RELAY_PANEL_SKUS)
  for_catalog(catalog_id)
    .power_modules
    .where(sku: skus)
    .order_by_price
    .to_bom_array
end

.get_rough_in_kits(catalog_id:, skus:) ⇒ Object

Get rough-in kits



317
318
319
# File 'app/models/view_quote_bom_item.rb', line 317

def self.get_rough_in_kits(catalog_id:, skus:)
  get_by_skus(catalog_id: catalog_id, skus: skus, item_type: 'tool')
end

.get_sensors(catalog_id:, product_line_id:) ⇒ Object

Get sensors for a product line



243
244
245
246
247
248
249
250
251
252
# File 'app/models/view_quote_bom_item.rb', line 243

def self.get_sensors(catalog_id:, product_line_id:)
  pl_path = product_line_ltree_path(product_line_id)
  return [] if pl_path.blank?

  for_catalog(catalog_id)
    .sensors
    .for_any_product_line_path(pl_path)
    .order_by_price
    .to_bom_array
end

.get_smart_services(catalog_id:, skus: ItemConstants::SMART_SERVICE_SKUS) ⇒ Object

Get smart services (installation services)



322
323
324
# File 'app/models/view_quote_bom_item.rb', line 322

def self.get_smart_services(catalog_id:, skus: ItemConstants::SMART_SERVICE_SKUS)
  get_by_skus(catalog_id: catalog_id, skus: skus, item_type: 'service')
end

.get_snow_melt_controls(catalog_id:, skus:, successor_to_original: {}) ⇒ Object

Get snow melt controls by SKU
Note: Adds 'original_bundle_sku' required by SnowMeltingControlsFinder

Parameters:

  • catalog_id (Integer)

    The catalog ID

  • skus (Array<String>)

    List of SKUs to fetch

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

    Optional mapping from successor SKU to original bundle SKU



291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'app/models/view_quote_bom_item.rb', line 291

def self.get_snow_melt_controls(catalog_id:, skus:, successor_to_original: {})
  bundle_skus = ItemConstants::SNOWMELT_CONTROL_BUNDLES.pluck(:sku).to_set

  get_by_skus(catalog_id: catalog_id, skus: skus, item_type: 'control').map do |control|
    # For snow melt controls, add the original_bundle_sku used by SnowMeltingControlsFinder
    # Priority: 1) Direct bundle SKU match, 2) Successor mapping, 3) nil
    original_bundle_sku = if bundle_skus.include?(control['sku'])
                            control['sku']
                          else
                            successor_to_original[control['sku']]
                          end
    control.merge('original_bundle_sku' => original_bundle_sku)
  end
end

.get_snow_melt_sensors(catalog_id:, skus: ItemConstants::SNOWMELT_SENSOR_SKUS) ⇒ Object

Get snow melt sensors by SKU



307
308
309
# File 'app/models/view_quote_bom_item.rb', line 307

def self.get_snow_melt_sensors(catalog_id:, skus: ItemConstants::SNOWMELT_SENSOR_SKUS)
  get_by_skus(catalog_id: catalog_id, skus: skus, item_type: 'sensor')
end

.get_underlayments(catalog_id:) ⇒ Object

Get underlayments (items under the Underlayment product line hierarchy)
Includes Prodeso membranes, ThermalSheet, Cork, CeraZorb



328
329
330
331
332
333
334
# File 'app/models/view_quote_bom_item.rb', line 328

def self.get_underlayments(catalog_id:)
  for_catalog(catalog_id)
    .accessories
    .where(ViewQuoteBomItem[:product_line_path_slugs].ltree_descendant(LtreePaths::PL_FLOOR_HEATING_UNDERLAYMENT))
    .order_by_price
    .to_bom_array
end

.line_items_to_bom(line_items, cable_spacing: nil) ⇒ Array<Hash>

Convert line items to BOM format with quantities

Parameters:

  • line_items (Array<LineItem>)

    Array of LineItem objects with catalog_item association

  • cable_spacing (Float, nil) (defaults to: nil)

    For recalculating area on cables

Returns:

  • (Array<Hash>)

    BOM format array with 'qty' field



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'app/models/view_quote_bom_item.rb', line 344

def self.line_items_to_bom(line_items, cable_spacing: nil)
  return [] if line_items.blank?

  # Get catalog item IDs and quantities
  catalog_item_ids = line_items.map(&:catalog_item_id)
  quantities_by_id = line_items.to_h { |li| [li.catalog_item_id, li.quantity] }

  # Fetch all matching items from matview in one query
  items_by_id = where(id: catalog_item_ids).index_by(&:id)

  # Build BOM array preserving order
  line_items.filter_map do |li|
    item = items_by_id[li.catalog_item_id]
    next nil unless item

    bom = item.to_bom_hash
    bom['qty'] = quantities_by_id[li.catalog_item_id]

    # Recalculate area for cables
    bom['area'] = ((item.length / 12.0) * (cable_spacing / 12.0)).round(2) if cable_spacing.present? && cable_spacing.positive? && item.length.present? && item.item_type == 'element'

    bom
  end
end

.order_by_areaActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are order by area. Active Record Scope

Returns:

See Also:



118
# File 'app/models/view_quote_bom_item.rb', line 118

scope :order_by_area, -> { order(area_sqft: :asc) }

.order_by_priceActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are order by price. Active Record Scope

Returns:

See Also:



116
# File 'app/models/view_quote_bom_item.rb', line 116

scope :order_by_price, -> { order(price: :asc) }

.order_by_wattsActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are order by watts. Active Record Scope

Returns:

See Also:



117
# File 'app/models/view_quote_bom_item.rb', line 117

scope :order_by_watts, -> { order(watts: :asc) }

.power_modulesActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are power modules. Active Record Scope

Returns:

See Also:



83
# File 'app/models/view_quote_bom_item.rb', line 83

scope :power_modules, -> { where(item_type: 'power') }

.refreshObject

Refresh the materialized view (concurrently to avoid locking)
Called by MatviewRefreshWorker on schedule



126
127
128
# File 'app/models/view_quote_bom_item.rb', line 126

def self.refresh
  Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
end

.sensorsActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are sensors. Active Record Scope

Returns:

See Also:



84
# File 'app/models/view_quote_bom_item.rb', line 84

scope :sensors, -> { where(item_type: 'sensor') }

.servicesActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are services. Active Record Scope

Returns:

See Also:



86
# File 'app/models/view_quote_bom_item.rb', line 86

scope :services, -> { where(item_type: 'service') }

.to_bom_arrayObject

Bulk convert to BOM format (avoids N+1 by using select)



180
181
182
# File 'app/models/view_quote_bom_item.rb', line 180

def self.to_bom_array
  all.map(&:to_bom_hash)
end

Instance Method Details

#build_features_arrayObject

Build array of non-nil features for display



175
176
177
# File 'app/models/view_quote_bom_item.rb', line 175

def build_features_array
  [feature_1, feature_2, feature_3, feature_4, feature_5].compact
end

#to_bom_hashObject

Convert a row to the hash format expected by HeatingSystemCalculator



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
172
# File 'app/models/view_quote_bom_item.rb', line 135

def to_bom_hash
  {
    'sku' => sku,
    'name' => name,
    'description' => short_description,
    'short_description' => short_description,
    'img' => img_url,
    'hero' => hero_url,
    'volts' => voltage_id.to_f,
    'is_dual_voltage' => is_dual_voltage,
    'price' => price&.round(2),
    'cat_id' => id,
    'class' => 'Item',
    'third_party_part_number' => third_party_part_number,
    'third_party_sku' => third_party_sku,
    'sale_price_in_effect' => sale_in_effect,
    'sale_price' => sale_price&.round(2),
    'sale_title' => coupon_title,
    'sale_description' => coupon_description,
    'sale_expiration' => coupon_expiration,
    'sale_coupon_id' => coupon_id,
    'symbol' => symbol,
    'tagline' => product_line_tagline,
    # Element-specific fields
    'width' => width,
    'length' => length,
    'area' => area_sqft,
    'watts' => watts,
    'amps' => amps,
    # Feature fields (for benefits display)
    'features' => build_features_array,
    # Inherited product line metadata (from ltree ancestors)
    'product_line_name' => effective_public_name,
    'product_line_tagline' => effective_tag_line,
    'product_line_description' => effective_short_description,
    'product_line_video_uid' => quote_builder_video_uid
  }
end