Class: ProductCategory

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable, Models::LtreeLineage, Models::LtreePathBuilder
Defined in:
app/models/product_category.rb

Overview

== Schema Information

Table name: product_categories
Database name: primary

id :integer not null, primary key
auto_expire_days :integer
available_to_public :boolean default(TRUE), not null
cached_ancestor_ids :integer default([]), not null, is an Array
children_count :integer
custom_product :boolean default(FALSE), not null
inactive :boolean default(FALSE), not null
legacy_publication :boolean
level :integer
lineage_expanded :text
ltree_path_ids :ltree
ltree_path_slugs :ltree
name :string(255)
nmfc_class :string
nmfc_code :string
priority :integer default(100)
restricted_for_sales :boolean default(FALSE), not null
reviewable :boolean
show_in_sales_portal :boolean default(FALSE), not null
show_in_support_portal :boolean default(TRUE), not null
skip_follow_up :boolean default(FALSE), not null
skip_siblings :boolean default(FALSE), not null
surface_calculation_method :string(255)
url :string(255)
created_at :datetime
updated_at :datetime
item_image_id :integer
parent_id :integer

Indexes

idx_product_categories_ltree_path_ids (ltree_path_ids) USING gist
idx_product_categories_ltree_path_slugs (ltree_path_slugs) USING gist
index_product_categories_on_cached_ancestor_ids (cached_ancestor_ids) USING gin
index_product_categories_on_url (url) UNIQUE
reviewable_id (reviewable,id)

Defined Under Namespace

Classes: CacheFlusher

Constant Summary

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Attributes included from Models::LtreePathBuilder

#skip_ltree_rebuild

Has many collapse

Methods included from Models::LtreeLineage

#children

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #creator, #should_not_save_version, #stamp_record, #updater

Methods included from Models::LtreePathBuilder

#build_ltree_path_ids_value, #build_ltree_path_slugs_value, #build_slug_ltree_value, builds_ltree_path, #compute_ltree_ancestor_ids, #derive_cached_ancestor_ids, #ltree_descendant_ids, rebuild_all_ltree_paths!, rebuild_subtree_ltree_paths!

Methods included from Models::LtreeLineage

acts_as_ltree_lineage, ancestors_ids, #ancestors_ids, define_ltree_scopes, define_ordered_ltree_methods, #descendant_of_path?, descendants_ids, #descendants_ids, #generate_full_name, #generate_full_name_array, #lineage, #lineage_array, #lineage_simple, #ltree_ancestor_of?, #ltree_descendant_of?, #ltree_slug, #ltree_slug_path, #parent, #path_includes?, #root, #root?, #root_id, root_ids, self_ancestors_and_descendants_ids, #self_ancestors_and_descendants_ids, self_and_ancestors_ids, #self_and_ancestors_ids, #self_and_children, self_and_descendants_ids, #self_and_descendants_ids, #self_and_siblings, #siblings

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#nameObject (readonly)



60
# File 'app/models/product_category.rb', line 60

validates :name, presence: true

#urlObject (readonly)



61
# File 'app/models/product_category.rb', line 61

validates :url, uniqueness: true

Class Method Details

.all_non_publication_goods_idsObject



169
170
171
# File 'app/models/product_category.rb', line 169

def self.all_non_publication_goods_ids
  where(ProductCategory[:ltree_path_slugs].ltree_descendant(LtreePaths::PC_GOODS)).pluck(:id) - all_publication_ids
end

.all_publication_idsObject



151
152
153
# File 'app/models/product_category.rb', line 151

def self.all_publication_ids
  where(ProductCategory[:ltree_path_slugs].ltree_descendant(LtreePaths::PC_PUBLICATIONS)).pluck(:id)
end

.all_reviewable_idsObject

Uses ltree for efficient hierarchy queries (single query each)



144
145
146
147
148
149
# File 'app/models/product_category.rb', line 144

def self.all_reviewable_ids
  paths = where(reviewable: true).pluck(:ltree_path_ids).compact
  return [] if paths.empty?

  where(ProductCategory[:ltree_path_ids].ltree_descendant(paths)).pluck(:id)
end

.available_to_publicActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are available to public. Active Record Scope

Returns:

See Also:



98
# File 'app/models/product_category.rb', line 98

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

.by_name_with_descendantsActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are by name with descendants. Active Record Scope

Returns:

See Also:



76
77
78
79
# File 'app/models/product_category.rb', line 76

scope :by_name_with_descendants, ->(pc_n) {
  path = where(name: pc_n).pick(:ltree_path_ids)
  path ? where(ProductCategory[:ltree_path_ids].ltree_descendant(path)) : none
}

.by_url_with_descendantsActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are by url with descendants. Active Record Scope

Returns:

See Also:



81
82
83
84
85
86
# File 'app/models/product_category.rb', line 81

scope :by_url_with_descendants, ->(pc_url) {
  pc = find_by(url: pc_url)
  return none unless pc&.ltree_path_ids

  where(ProductCategory[:ltree_path_ids].ltree_descendant(pc.ltree_path_ids))
}

.custom_product_idsObject

Returns IDs of all custom product categories



163
164
165
166
167
# File 'app/models/product_category.rb', line 163

def self.custom_product_ids
  Rails.cache.fetch(:product_category_custom_product_ids, expires_in: 1.day) do
    where(custom_product: true).pluck(:id)
  end
end

.for_sales_portalActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are for sales portal. Active Record Scope

Returns:

See Also:



96
# File 'app/models/product_category.rb', line 96

scope :for_sales_portal,          -> { where(show_in_sales_portal: true) }

.for_support_portalActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are for support portal. Active Record Scope

Returns:

See Also:



93
# File 'app/models/product_category.rb', line 93

scope :for_support_portal,        -> { where(show_in_support_portal: true) }

.goodsActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are goods. Active Record Scope

Returns:

See Also:



90
# File 'app/models/product_category.rb', line 90

scope :goods,                     -> { where(ProductCategory[:ltree_path_slugs].ltree_descendant(LtreePaths::PC_GOODS)) }

.no_follow_ups_idsObject

Returns a list of product category ids which do not require order follow ups



240
241
242
243
244
# File 'app/models/product_category.rb', line 240

def self.no_follow_ups_ids
  Rails.cache.fetch([:product_categories_no_follow_ups_ids], expires_in: 1.month) do
    ProductCategory.where(skip_follow_up: true).map(&:self_and_descendants_ids).flatten
  end
end

.non_publicationsActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are non publications. Active Record Scope

Returns:

See Also:



88
# File 'app/models/product_category.rb', line 88

scope :non_publications,          -> { where.not(ProductCategory[:ltree_path_slugs].ltree_descendant(LtreePaths::PC_PUBLICATIONS)) }

.non_publications_select_optionsObject



110
111
112
# File 'app/models/product_category.rb', line 110

def self.non_publications_select_options
  ProductCategory.non_publications.order(:name).map { |pc| [pc.lineage_expanded, pc.id] }
end

.product_categories_hash(product_categories: nil, selected_product_category: nil, selected_product_category_ids: nil) ⇒ Object

Legacy cache methods removed - ltree queries are fast enough
See: doc/tasks/202512032250_LTREE_HIERARCHY_MIGRATION.md



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
# File 'app/models/product_category.rb', line 117

def self.product_categories_hash(product_categories: nil, selected_product_category: nil, selected_product_category_ids: nil)
  product_categories ||= ProductCategory.roots
  data = []
  selected_product_category_ids ||= selected_product_category&.self_and_ancestors_ids || []
  product_categories.each do |pc|
    expanded = selected_product_category_ids.include?(pc.id)
    pc_hsh = {
      text: pc.decorated_product_category_name,
      nodes: product_categories_hash(product_categories: pc.children, selected_product_category_ids: selected_product_category_ids),
      id: pc.url,
      state: {
        checked: false,
        disabled: false,
        expanded: expanded,
        selected: false
      },
      color: ('darkgreen' if expanded),
      dataAttr: {
        target: "/product_categories/#{pc.id}/edit"
      }
    }
    data << pc_hsh
  end
  data
end

.publication_select_optionsObject



106
107
108
# File 'app/models/product_category.rb', line 106

def self.publication_select_options
  ProductCategory.publications.order(:name).map { |pc| [pc.decorated_product_category_name, pc.id] }
end

.publicationsActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are publications. Active Record Scope

Returns:

See Also:



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

scope :publications,              -> { where(ProductCategory[:ltree_path_slugs].ltree_descendant(LtreePaths::PC_PUBLICATIONS)) }

.publications_root_idObject

Returns the root publications category ID (cached)



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

def self.publications_root_id
  Rails.cache.fetch(:product_category_publications_root_id, expires_in: 1.day) do
    find_by(ltree_path_slugs: LtreePaths::PC_PUBLICATIONS)&.id
  end
end

.reviewableActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are reviewable. Active Record Scope

Returns:

See Also:



95
# File 'app/models/product_category.rb', line 95

scope :reviewable,                -> { where(id: all_reviewable_ids) }

.sales_portal_sortedActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are sales portal sorted. Active Record Scope

Returns:

See Also:



97
# File 'app/models/product_category.rb', line 97

scope :sales_portal_sorted,       -> { for_sales_portal.order(:priority) }

.select_optionsObject



102
103
104
# File 'app/models/product_category.rb', line 102

def self.select_options
  ProductCategory.all.order('lineage_expanded').pluck(:lineage_expanded, :id)
end

.servicesActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are services. Active Record Scope

Returns:

See Also:



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

scope :services,                  -> { where(ProductCategory[:ltree_path_slugs].ltree_descendant(LtreePaths::PC_SERVICES)) }

.support_portal_sortedActiveRecord::Relation<ProductCategory>

A relation of ProductCategories that are support portal sorted. Active Record Scope

Returns:

See Also:



94
# File 'app/models/product_category.rb', line 94

scope :support_portal_sorted,     -> { for_support_portal.order(:priority) }

.syncs_items_on_ltree_change?Boolean

Enable item ltree sync when category paths change

Returns:

  • (Boolean)


56
57
58
# File 'app/models/product_category.rb', line 56

def self.syncs_items_on_ltree_change?
  true
end

Instance Method Details

#decorated_product_category_nameObject



262
263
264
265
266
267
268
269
# File 'app/models/product_category.rb', line 262

def decorated_product_category_name
  display_name = name
  display_name << " \u263c" if available_to_public
  display_name << ' $' if show_in_sales_portal
  display_name << '' if show_in_support_portal
  display_name << '' if reviewable
  display_name
end

#digital_assetsActiveRecord::Relation<DigitalAsset>

Returns:

See Also:



66
# File 'app/models/product_category.rb', line 66

has_and_belongs_to_many :digital_assets

#effective_nmfc_classObject



252
253
254
255
256
# File 'app/models/product_category.rb', line 252

def effective_nmfc_class
  return nmfc_class if nmfc_class.present?

  self_and_ancestors.where.not(nmfc_class: nil).order('level desc, priority asc').pick(:nmfc_class)
end

#effective_nmfc_codeObject



246
247
248
249
250
# File 'app/models/product_category.rb', line 246

def effective_nmfc_code
  return nmfc_code if nmfc_code.present?

  self_and_ancestors.where.not(nmfc_code: nil).order('level desc, priority asc').pick(:nmfc_code) || ProductCategoryConstants::FALLBACK_NMFC_CODE
end

#facetsActiveRecord::Relation<Facet>

Returns:

  • (ActiveRecord::Relation<Facet>)

See Also:



68
# File 'app/models/product_category.rb', line 68

has_and_belongs_to_many :facets

#follow_up?Boolean

Returns:

  • (Boolean)


235
236
237
# File 'app/models/product_category.rb', line 235

def follow_up?
  self.class.no_follow_ups_ids.exclude?(id)
end

#is_accessory?Boolean

Returns:

  • (Boolean)


202
203
204
# File 'app/models/product_category.rb', line 202

def is_accessory?
  descendant_of_path?(LtreePaths::PC_ACCESSORIES)
end

#is_control?Boolean

Returns:

  • (Boolean)


194
195
196
# File 'app/models/product_category.rb', line 194

def is_control?
  descendant_of_path?(LtreePaths::PC_CONTROLS)
end

#is_custom_mat?Boolean

Returns:

  • (Boolean)


223
224
225
# File 'app/models/product_category.rb', line 223

def is_custom_mat?
  descendant_of_path?(LtreePaths::PC_HEATING_ELEMENTS_CUSTOM_MATS)
end

#is_goods?Boolean

Predicate methods using ltree paths for hierarchy checks

Returns:

  • (Boolean)


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

def is_goods?
  descendant_of_path?(LtreePaths::PC_GOODS)
end

#is_heating_element?Boolean

Returns:

  • (Boolean)


190
191
192
# File 'app/models/product_category.rb', line 190

def is_heating_element?
  descendant_of_path?(LtreePaths::PC_HEATING_ELEMENTS)
end

#is_insulation?Boolean

Returns:

  • (Boolean)


219
220
221
# File 'app/models/product_category.rb', line 219

def is_insulation?
  descendant_of_path?(LtreePaths::PC_ACCESSORIES_INSULATIONS)
end

#is_mirror?Boolean

Returns:

  • (Boolean)


227
228
229
# File 'app/models/product_category.rb', line 227

def is_mirror?
  descendant_of_path?(LtreePaths::PC_MIRRORS)
end

#is_publication?Boolean

Returns:

  • (Boolean)


186
187
188
# File 'app/models/product_category.rb', line 186

def is_publication?
  descendant_of_path?(LtreePaths::PC_PUBLICATIONS)
end

#is_relay_panel?Boolean

Returns:

  • (Boolean)


215
216
217
# File 'app/models/product_category.rb', line 215

def is_relay_panel?
  descendant_of_path?(LtreePaths::PC_POWER_RELAY_PANELS)
end

#is_reviewable?Boolean

Returns:

  • (Boolean)


206
207
208
209
210
211
212
213
# File 'app/models/product_category.rb', line 206

def is_reviewable?
  return true if reviewable?

  # Check if any ancestor is reviewable using ltree
  self.class.where(reviewable: true)
      .where(ProductCategory[:ltree_path_ids].ltree_ancestor(ltree_path_ids))
      .exists?
end

#is_service?Boolean

Returns:

  • (Boolean)


178
179
180
# File 'app/models/product_category.rb', line 178

def is_service?
  descendant_of_path?(LtreePaths::PC_SERVICES)
end

#is_shipping?Boolean

Returns:

  • (Boolean)


182
183
184
# File 'app/models/product_category.rb', line 182

def is_shipping?
  descendant_of_path?(LtreePaths::PC_SHIPPING)
end

#is_spare_parts?Boolean

Returns:

  • (Boolean)


231
232
233
# File 'app/models/product_category.rb', line 231

def is_spare_parts?
  descendant_of_path?(LtreePaths::PC_SPARE_PARTS)
end

#is_upgrade?Boolean

Returns:

  • (Boolean)


198
199
200
# File 'app/models/product_category.rb', line 198

def is_upgrade?
  descendant_of_path?(LtreePaths::PC_UPGRADES)
end

#itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



63
# File 'app/models/product_category.rb', line 63

has_many :items, inverse_of: :product_category

#product_linesActiveRecord::Relation<ProductLine>

Returns:

See Also:



67
# File 'app/models/product_category.rb', line 67

has_and_belongs_to_many :product_lines

#product_specificationsActiveRecord::Relation<ProductSpecification>

Returns:

See Also:



64
# File 'app/models/product_category.rb', line 64

has_many :product_specifications, inverse_of: :product_category

#purge_cacheObject



271
272
273
274
275
# File 'app/models/product_category.rb', line 271

def purge_cache
  return unless saved_changes?

  CacheWorker.perform_async('cache_class' => 'ProductCategory::CacheFlusher', 'params' => id)
end

#root_product_categoryObject



277
278
279
280
281
# File 'app/models/product_category.rb', line 277

def root_product_category
  @root_product_category ||= begin
    self_and_ancestors.where(reviewable: true).order('level desc, priority asc').first || self
  end
end

#to_sObject



258
259
260
# File 'app/models/product_category.rb', line 258

def to_s
  lineage_expanded || generate_full_name
end