Class: ViewProductCatalog

Inherits:
ApplicationViewRecord show all
Includes:
Models::SaleDiscountable, Models::SearchableView, OrderQuery
Defined in:
app/models/view_product_catalog.rb

Overview

== Schema Information

Table name: view_product_catalogs
Database name: primary

id :integer primary key
all_pl_paths_ids :ltree is an Array
all_pl_paths_slugs :ltree is an Array
alternate_warehouse_stock_reporting_max :integer
amazon_asin :string(10)
amazon_desired_product_type :string
amazon_fnsku :string
amazon_product_type_divergent :boolean
amazon_reported_product_type :string
amazon_title :text
amazon_variation_sku :string
amazon_variation_theme_name :string
amps :text
business_days_to_fulfill :integer
catalog_item_is_discontinued :boolean
catalog_item_state :string(30)
catalog_item_stock_reserved :integer
catalog_name :string(255)
clearance :boolean
country_of_origin :string(255)
coupon_code :string(255)
currency :string(255)
currency_symbol :string(255)
descendants_catalog_ids :integer is an Array
effective_price :decimal(8, 2)
effective_price_with_vat :decimal(, )
future_coupon_code :string(255)
future_coupon_effective_date :date
future_coupon_expiration_date :date
future_sale_price :decimal(8, 2)
future_sale_price_with_vat :decimal(, )
goods_vat_rate :decimal(6, 4)
google_feed :boolean
google_feed_title :string
gtin13 :text
hide_from_feed :boolean
inherited_item_product_category_ids :integer is an Array
inherited_item_product_line_ids :integer is an Array
item_condition :string(20)
item_grouping_identifier :string
item_is_discontinued :boolean
item_is_kit :boolean
item_is_web_accessible :boolean
item_name :text
item_parcel_oversize :boolean
item_primary_product_line_lineage_expanded :string(255)
item_primary_product_line_slug_ltree :ltree
item_shipping_class :integer
item_sku :string
map_percentage :decimal(5, 4)
map_price :decimal(, )
map_violation :boolean
max_discount :integer
msrp :decimal(8, 2)
old_price :decimal(10, 2)
option_name :text
parent_catalog_discount :decimal(5, 4)
parent_catalog_name :string(255)
parent_catalog_price :decimal(8, 2)
parent_price_updated_at :datetime
parent_sku :string
pc_path_ids :ltree
pc_path_slugs :ltree
price :decimal(8, 2)
price_difference :decimal(, )
price_difference_factor :decimal(, )
price_diverging :boolean
price_updated_at :datetime
price_updated_parent_differential_in_days :float
price_with_vat :decimal(, )
primary_pl_path_ids :ltree
primary_pl_path_slugs :ltree
product_category_lineage_expanded :text
product_category_name :string(255)
product_category_priority :integer
product_category_url :string(255)
product_line_ids :integer is an Array
product_specifications :jsonb
product_stock_status :text
retail_price :decimal(8, 2)
retailer_type :integer
sale_price :decimal(8, 2)
sale_price_effective_date :date
sale_price_expiration_date :date
sale_price_in_effect :boolean
sale_price_with_vat :decimal(, )
shipping_height_in :decimal(6, 2)
shipping_length_in :decimal(6, 2)
shipping_weight_lbs :decimal(8, 4)
shipping_width_in :decimal(6, 2)
short_description :string(120)
store_item_is_discontinued :boolean
store_item_qty_available :integer
store_item_qty_committed :integer
store_item_qty_on_hand :integer
store_item_unit_cogs :decimal(, )
successor_item_sku :string
third_party_part_number :string(255)
third_party_promo_part_number :string
third_party_sku :string
title :text
upc :string(255)
volts :text
watts :text
created_at :datetime
updated_at :datetime
amazon_variation_id :bigint
catalog_id :integer
catalog_item_id :integer
coupon_id :integer
future_coupon_id :integer
item_id :integer
item_primary_product_line_id :integer
item_product_category_id :integer
item_sales_portal_product_line_id :integer
new_item_id :integer
parent_catalog_item_id :integer
refurbished_item_id :integer
store_id :integer
store_item_id :integer
successor_item_id :integer

Belongs to collapse

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::SaleDiscountable

#amount, #effective_price, #money_effective_price, #money_price, #money_sale_price, #sale_price_in_effect?, #sale_price_percentage_off

Methods included from Models::SearchableView

#crm_link, #crm_link_subtitle, #has_columns?, #main_resource, #readonly?

Methods inherited from ApplicationViewRecord

create, create!, #readonly?

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Class Method Details

.belongs_to_product_category_idActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are belongs to product category id. Active Record Scope

Returns:

See Also:



155
156
157
158
159
160
# File 'app/models/view_product_catalog.rb', line 155

scope :belongs_to_product_category_id, ->(*pc_ids) {
  ids = [pc_ids].flatten.compact.uniq
  return none if ids.empty?

where.ltree_contains(:pc_path_ids, ids, array: false)
}

.belongs_to_product_line_idActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are belongs to product line id. Active Record Scope

Returns:

See Also:



147
148
149
150
151
152
# File 'app/models/view_product_catalog.rb', line 147

scope :belongs_to_product_line_id, ->(*pl_ids) {
  ids = [pl_ids].flatten.compact.uniq
  return none if ids.empty?

where.ltree_contains(:all_pl_paths_ids, ids)
}

.clearanceActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are clearance. Active Record Scope

Returns:

See Also:



165
# File 'app/models/view_product_catalog.rb', line 165

scope :clearance, -> { where(clearance: true) }

.excluding_refurbishedActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are excluding refurbished. Active Record Scope

Returns:

See Also:



164
# File 'app/models/view_product_catalog.rb', line 164

scope :excluding_refurbished, -> { where.not('item_sku LIKE ?', '%-BTK') }

.locale_to_catalogActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are locale to catalog. Active Record Scope

Returns:

See Also:



144
# File 'app/models/view_product_catalog.rb', line 144

scope :locale_to_catalog, ->(locale = I18n.locale) { where(catalog_id: Catalog.locale_to_catalog_id(locale)) }

.main_resource_classObject



265
266
267
# File 'app/models/view_product_catalog.rb', line 265

def self.main_resource_class
  'CatalogItem'
end

.map_violationsActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are map violations. Active Record Scope

Returns:

See Also:



229
# File 'app/models/view_product_catalog.rb', line 229

scope :map_violations, -> { where(map_violation: true) }

.on_saleActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are on sale. Active Record Scope

Returns:

See Also:



163
# File 'app/models/view_product_catalog.rb', line 163

scope :on_sale, -> { where(sale_price_in_effect: true) }

.price_sortedActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are price sorted. Active Record Scope

Returns:

See Also:



142
# File 'app/models/view_product_catalog.rb', line 142

scope :price_sorted, -> { order(:sale_price, :price) }

.price_updated_since_daysActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are price updated since days. Active Record Scope

Returns:

See Also:



143
# File 'app/models/view_product_catalog.rb', line 143

scope :price_updated_since_days, ->(days) { where(ViewProductCatalog[:price_updated_at].gteq(days.to_i.days.ago)) }

.ransackable_scopes(_auth_object = nil) ⇒ Object

Expose ltree-powered scopes, spec filters, and search-form scopes to Ransack



250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'app/models/view_product_catalog.rb', line 250

def self.ransackable_scopes(_auth_object = nil)
  super + %i[
    belongs_to_product_line_id
    belongs_to_product_category_id
    price_updated_since_days
    spec_finish_eq
    spec_finish_in
    spec_towel_bars_eq
    spec_bar_shape_eq
    spec_connection_method_eq
    spec_mounting_method_eq
    spec_size_eq
  ]
end

.refurbishedActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are refurbished. Active Record Scope

Returns:

See Also:



162
# File 'app/models/view_product_catalog.rb', line 162

scope :refurbished, -> { where(item_condition: 'refurbished').where.not(new_item_id: nil) }

.refurbished_or_clearanceActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are refurbished or clearance. Active Record Scope

Returns:

See Also:



227
# File 'app/models/view_product_catalog.rb', line 227

scope :refurbished_or_clearance, -> { refurbished.or(clearance) }

.spec_bar_shape_eqActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are spec bar shape eq. Active Record Scope

Returns:

See Also:



187
188
189
190
191
# File 'app/models/view_product_catalog.rb', line 187

scope :spec_bar_shape_eq, ->(value) {
  return all if value.blank?

  where("product_specifications -> 'bar_shape' ->> 'raw' ILIKE ?", "%#{value}%")
}

.spec_connection_method_eqActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are spec connection method eq. Active Record Scope

Returns:

See Also:



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'app/models/view_product_catalog.rb', line 192

scope :spec_connection_method_eq, ->(value) {
  return all if value.blank?

  # Dual connection products ("Plug-in or Hardwired") work as both plug-in AND hardwired
  # So when filtering by Plug-in or Hardwired, include dual connection products too
  case value
  when 'Plug-in'
    where("product_specifications -> 'connection_method' ->> 'raw' ILIKE ? OR product_specifications -> 'connection_method' ->> 'raw' ILIKE ?",
          'Plug-in', 'Plug-in or Hardwired')
  when 'Hardwired'
    where("product_specifications -> 'connection_method' ->> 'raw' ILIKE ? OR product_specifications -> 'connection_method' ->> 'raw' ILIKE ?",
          'Hardwired', 'Plug-in or Hardwired')
  else
    # For "Plug-in or Hardwired" (dual connection) - exact match only
    where("product_specifications -> 'connection_method' ->> 'raw' ILIKE ?", value)
  end
}

.spec_finish_eqActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are spec finish eq. Active Record Scope

Returns:

See Also:



170
171
172
173
174
# File 'app/models/view_product_catalog.rb', line 170

scope :spec_finish_eq, ->(value) {
  return all if value.blank?

  where("product_specifications -> 'finish' ->> 'raw' ILIKE ?", value)
}

.spec_finish_inActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are spec finish in. Active Record Scope

Returns:

See Also:



175
176
177
178
179
180
# File 'app/models/view_product_catalog.rb', line 175

scope :spec_finish_in, ->(*args) {
  values = args.flatten
  return all if values.blank?

  where("product_specifications -> 'finish' ->> 'raw' ILIKE ANY (ARRAY[?])", values)
}

.spec_mounting_method_eqActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are spec mounting method eq. Active Record Scope

Returns:

See Also:



209
210
211
212
213
214
215
# File 'app/models/view_product_catalog.rb', line 209

scope :spec_mounting_method_eq, ->(value) {
  return all if value.blank?

  # Use partial matching with leading wildcard only for mounting
  # This allows "Wall Mounted" to match "Wall Mounted (20\" x 20 1/2\")" etc.
  where("product_specifications -> 'mounting_method' ->> 'raw' ILIKE ?", "#{value}%")
}

.spec_size_eqActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are spec size eq. Active Record Scope

Returns:

See Also:



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

scope :spec_size_eq, ->(slug) {
  return all if slug.blank?

  label = TowelWarmerFilterSlugs::SIZE_LABELS[slug]
  return none unless label

  where("product_specifications -> 'size_classification' ->> 'raw' = ?", label)
}

.spec_towel_bars_eqActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are spec towel bars eq. Active Record Scope

Returns:

See Also:



181
182
183
184
185
186
# File 'app/models/view_product_catalog.rb', line 181

scope :spec_towel_bars_eq, ->(value) {
  return all if value.blank?

  # Towel bars can be a range like "4-6" or "12+" - match if raw contains the value
  where("product_specifications -> 'towel_bars' ->> 'raw' ILIKE ?", "%#{value}%")
}

.valid_for_google_local_inventoryActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are valid for google local inventory. Active Record Scope

Returns:

See Also:



232
233
234
235
236
237
238
# File 'app/models/view_product_catalog.rb', line 232

scope :valid_for_google_local_inventory, -> {
  where(item_condition: 'new')
    .where(ViewProductCatalog[:shipping_weight_lbs].gt(0))
    .where(ViewProductCatalog[:shipping_height_in].gt(0))
    .where(ViewProductCatalog[:shipping_length_in].gt(0))
    .where(ViewProductCatalog[:shipping_width_in].gt(0))
}

.vendor_catalogsActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are vendor catalogs. Active Record Scope

Returns:

See Also:



228
# File 'app/models/view_product_catalog.rb', line 228

scope :vendor_catalogs, -> { where(retailer_type: 1) }

.visible_to_publicActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are visible to public. Active Record Scope

Returns:

See Also:



145
# File 'app/models/view_product_catalog.rb', line 145

scope :visible_to_public, -> { where(item_is_web_accessible: true) }

.with_stockActiveRecord::Relation<ViewProductCatalog>

A relation of ViewProductCatalogs that are with stock. Active Record Scope

Returns:

See Also:



161
# File 'app/models/view_product_catalog.rb', line 161

scope :with_stock, -> { where(store_item_qty_available: 1..) }

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


273
274
275
# File 'app/models/view_product_catalog.rb', line 273

def active?
  catalog_item_state == 'active'
end

#availabilityObject



348
349
350
# File 'app/models/view_product_catalog.rb', line 348

def availability
  product_stock_status
end

#bulky?Boolean

Returns:

  • (Boolean)


327
328
329
# File 'app/models/view_product_catalog.rb', line 327

def bulky?
  ships_freight? || item_parcel_oversize
end

#cache_keyObject



269
270
271
# File 'app/models/view_product_catalog.rb', line 269

def cache_key
  "#{self.class.model_name.cache_key}/#{id}-#{product_stock_status}-#{updated_at.utc.to_fs(:number)}"
end

#catalog_itemCatalogItem



133
# File 'app/models/view_product_catalog.rb', line 133

belongs_to :catalog_item, primary_key: :id, optional: true

#couponCoupon

Returns:

See Also:



140
# File 'app/models/view_product_catalog.rb', line 140

belongs_to :coupon

#effective_seo_descriptionObject

Alias for Catalog_item#effective_seo_description

Returns:

  • (Object)

    Catalog_item#effective_seo_description

See Also:



245
# File 'app/models/view_product_catalog.rb', line 245

delegate :seo_title, :effective_seo_description, :msrp, :msrp_with_vat, to: :catalog_item

#facetObject

Alias for Item#facet

Returns:

  • (Object)

    Item#facet

See Also:



244
# File 'app/models/view_product_catalog.rb', line 244

delegate :item_available_locales, :facet, :facet_tokens, :facet_sort_keys, to: :item

#facet_sort_keysObject

Alias for Item#facet_sort_keys

Returns:

  • (Object)

    Item#facet_sort_keys

See Also:



244
# File 'app/models/view_product_catalog.rb', line 244

delegate :item_available_locales, :facet, :facet_tokens, :facet_sort_keys, to: :item

#facet_tokensObject

Alias for Item#facet_tokens

Returns:

  • (Object)

    Item#facet_tokens

See Also:



244
# File 'app/models/view_product_catalog.rb', line 244

delegate :item_available_locales, :facet, :facet_tokens, :facet_sort_keys, to: :item

#itemItem

Returns:

See Also:



138
# File 'app/models/view_product_catalog.rb', line 138

belongs_to :item

#item_available_localesObject

Alias for Item#item_available_locales

Returns:

  • (Object)

    Item#item_available_locales

See Also:



244
# File 'app/models/view_product_catalog.rb', line 244

delegate :item_available_locales, :facet, :facet_tokens, :facet_sort_keys, to: :item

#localeObject

Helper method for now, TODO need to be something more robust in the future



278
279
280
# File 'app/models/view_product_catalog.rb', line 278

def locale
  Catalog.catalog_id_to_locale(catalog_id)
end

#msrpObject

Alias for Catalog_item#msrp

Returns:

  • (Object)

    Catalog_item#msrp

See Also:



245
# File 'app/models/view_product_catalog.rb', line 245

delegate :seo_title, :effective_seo_description, :msrp, :msrp_with_vat, to: :catalog_item

#msrp_with_vatObject

Alias for Catalog_item#msrp_with_vat

Returns:

  • (Object)

    Catalog_item#msrp_with_vat

See Also:



245
# File 'app/models/view_product_catalog.rb', line 245

delegate :seo_title, :effective_seo_description, :msrp, :msrp_with_vat, to: :catalog_item

#price_difference_factor_humanObject



331
332
333
334
335
# File 'app/models/view_product_catalog.rb', line 331

def price_difference_factor_human
  return unless price_difference_factor

  "#{(price_difference_factor * 100).round(2)} %"
end

#primary_product_lineProductLine



134
# File 'app/models/view_product_catalog.rb', line 134

belongs_to :primary_product_line, class_name: 'ProductLine', foreign_key: :item_primary_product_line_id, optional: true

#product_available?Boolean

Returns:

  • (Boolean)


344
345
346
# File 'app/models/view_product_catalog.rb', line 344

def product_available?
  product_stock_status == 'InStock'
end

#product_categoryProductCategory



135
# File 'app/models/view_product_catalog.rb', line 135

belongs_to :product_category, class_name: 'ProductCategory', foreign_key: :item_product_category_id, optional: true

#product_type(separator: nil, limit: nil) ⇒ Object



337
338
339
340
341
342
# File 'app/models/view_product_catalog.rb', line 337

def product_type(separator: nil, limit: nil)
  Feed::ProductTypeGenerator.process(product_line: primary_product_line,
                                     product_category:,
                                     separator:,
                                     limit:)
end

#refurbished_itemItem

Returns:

See Also:



139
# File 'app/models/view_product_catalog.rb', line 139

belongs_to :refurbished_item, class_name: 'Item'

#sales_portal_product_lineProductLine



136
# File 'app/models/view_product_catalog.rb', line 136

belongs_to :sales_portal_product_line, class_name: 'ProductLine', foreign_key: :item_sales_portal_product_line_id, optional: true

#seo_titleObject

Alias for Catalog_item#seo_title

Returns:

  • (Object)

    Catalog_item#seo_title

See Also:



245
# File 'app/models/view_product_catalog.rb', line 245

delegate :seo_title, :effective_seo_description, :msrp, :msrp_with_vat, to: :catalog_item

#ships_freight?Boolean

Returns:

  • (Boolean)


323
324
325
# File 'app/models/view_product_catalog.rb', line 323

def ships_freight?
  Item.shipping_classes.invert[item_shipping_class] == 'freight_service'
end

#spec(token) ⇒ Object



282
283
284
285
286
287
# File 'app/models/view_product_catalog.rb', line 282

def spec(token)
  return unless ps = product_specifications[token.to_sym]

  ps[:raw] = TypeCoercer.coerce(ps[:raw])
  ps
end

#spec_image(token) ⇒ Object



317
318
319
320
321
# File 'app/models/view_product_catalog.rb', line 317

def spec_image(token)
  return unless iid = spec(token)&.dig(:image_id)

  Image.find(iid)
end

#spec_output(token) ⇒ Object



289
290
291
# File 'app/models/view_product_catalog.rb', line 289

def spec_output(token)
  spec(token)&.dig(:output)&.dup
end

#spec_value(token, output_unit: nil) ⇒ Numeric, ...

Returns the raw value from the spec, optionally converted to a target unit.
Used by facet sorting to normalize values stored in different units.

Parameters:

  • token (Symbol, String)

    the spec token (e.g., :width, :length)

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

    target unit for conversion (e.g., 'in', 'ft', 'lbs')

Returns:

  • (Numeric, String, nil)

    the spec value, converted if output_unit specified



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'app/models/view_product_catalog.rb', line 298

def spec_value(token, output_unit: nil)
  return unless (ps = spec(token))
  return unless (v = ps.dig(:raw))

  v = TypeCoercer.coerce(v)

  if output_unit.present? && (current_unit = ps.dig(:units)).present? && current_unit.to_s != output_unit.to_s
    begin
      current = RubyUnits::Unit.new("#{v} #{current_unit}")
      target = RubyUnits::Unit.new("1 #{output_unit}")
      v = current.convert_to(output_unit.to_s).scalar.to_f if current.compatible?(target)
    rescue ArgumentError, RubyUnits::Unit::ArgumentError
      # Unit conversion failed (incompatible or unknown units), return original value
    end
  end

  v
end

#store_itemStoreItem

Returns:

See Also:



137
# File 'app/models/view_product_catalog.rb', line 137

belongs_to :store_item