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)

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 Models::EventPublishable

#publish_event

Class Method Details

.accessoriesActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are accessories. Active Record Scope

Returns:

See Also:



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

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

.controlsActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are controls. Active Record Scope

Returns:

See Also:



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

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

.elementsActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are elements. Active Record Scope

Returns:

See Also:



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

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:



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

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:



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

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:



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

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:



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

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

.get_accessories(catalog_id:, product_line_id:) ⇒ Object

Get accessories for a product line



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

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



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

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



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

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



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

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)



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

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



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

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)



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

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



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

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



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

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)



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

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



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

def self.get_snow_melt_controls(catalog_id:, skus:, successor_to_original: {})
  bundle_skus = ItemConstants::SNOWMELT_CONTROL_BUNDLES.map { |b| b[: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



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

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



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

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



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

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 { |li| li.catalog_item_id }
  quantities_by_id = line_items.each_with_object({}) { |li, h| h[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.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.compact
end

.order_by_areaActiveRecord::Relation<ViewQuoteBomItem>

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

Returns:

See Also:



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

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:



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

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:



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

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:



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

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

.refreshObject

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



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

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:



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

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

.servicesActiveRecord::Relation<ViewQuoteBomItem>

A relation of ViewQuoteBomItems that are services. Active Record Scope

Returns:

See Also:



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

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

.to_bom_arrayObject

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



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

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



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

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



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/models/view_quote_bom_item.rb', line 134

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