Class: ProductFilter

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/product_filter.rb

Overview

== Schema Information

Table name: product_filters
Database name: primary

id :integer not null, primary key
apply_discount :boolean
cached_item_ids :integer is an Array
exclude_item_ids :integer default([]), not null, is an Array
exclude_product_category_ids :integer default([]), not null, is an Array
exclude_product_line_ids :integer default([]), not null, is an Array
item_ids :integer default([]), is an Array
max_lin_ft :integer
max_msrp_amount :decimal(8, 2)
max_qty :integer
max_sq_ft :integer
min_lin_ft :integer
min_msrp_amount :decimal(8, 2)
min_qty :integer default(1), not null
min_sq_ft :integer
product_category_ids :integer default([]), is an Array
product_line_ids :integer default([]), is an Array
resource_type :string
created_at :datetime
updated_at :datetime
assortment_instruction_id :integer
creator_id :integer
resource_id :integer
updater_id :integer

Indexes

idx_assortment_instruction_id (assortment_instruction_id)
idx_resource_id_resource_type (resource_id,resource_type)

Foreign Keys

fk_rails_... (assortment_instruction_id => assortment_instructions.id)

Defined Under Namespace

Classes: LineExtractor, LineQualifier

Constant Summary

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#criteria_metObject

Returns the value of attribute criteria_met.



43
44
45
# File 'app/models/product_filter.rb', line 43

def criteria_met
  @criteria_met
end

#max_lin_ftObject (readonly)



58
# File 'app/models/product_filter.rb', line 58

validates :max_lin_ft, numericality: { greater_than_or_equal_to: :min_lin_ft, if: -> { max_lin_ft.present? } }

#max_msrp_amountObject (readonly)



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

validates :max_msrp_amount, numericality: { greater_than_or_equal_to: :min_msrp_amount, if: -> { max_msrp_amount.present? } }

#max_qtyObject (readonly)



54
# File 'app/models/product_filter.rb', line 54

validates :max_qty, numericality: { greater_than_or_equal_to: :min_qty, if: -> { max_qty.present? } }

#max_sq_ftObject (readonly)



56
# File 'app/models/product_filter.rb', line 56

validates :max_sq_ft, numericality: { greater_than_or_equal_to: :min_sq_ft, if: -> { max_sq_ft.present? } }

#min_lin_ftObject (readonly)



57
# File 'app/models/product_filter.rb', line 57

validates :min_lin_ft, numericality: { greater_than_or_equal_to: 1, if: -> { min_lin_ft.present? } }

#min_msrp_amountObject (readonly)



59
# File 'app/models/product_filter.rb', line 59

validates :min_msrp_amount, numericality: { greater_than_or_equal_to: 1, if: -> { min_msrp_amount.present? } }

#min_qtyObject (readonly)



52
# File 'app/models/product_filter.rb', line 52

validates :min_qty, presence: true

#min_sq_ftObject (readonly)



55
# File 'app/models/product_filter.rb', line 55

validates :min_sq_ft, numericality: { greater_than_or_equal_to: 1, if: -> { min_sq_ft.present? } }

Class Method Details

.presence_checkingActiveRecord::Relation<ProductFilter>

A relation of ProductFilters that are presence checking. Active Record Scope

Returns:

See Also:



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

scope :presence_checking, -> { where('product_filters.min_qty > 0 or product_filters.min_sq_ft > 0 or product_filters.min_lin_ft > 0 or product_filters.min_msrp_amount > 0') }

Instance Method Details

#any_items_match?(candidate_ids) ⇒ Boolean

Check if any of the given candidate item IDs match this filter's criteria.
More efficient than matching_item_ids when you only need a boolean result.

Parameters:

  • candidate_ids (Array<Integer>)

    The item IDs to check

Returns:

  • (Boolean)

    True if any candidate matches



157
158
159
160
161
# File 'app/models/product_filter.rb', line 157

def any_items_match?(candidate_ids)
  return false if candidate_ids.blank?

  applicable_items_scope.where(id: candidate_ids).exists?
end

#applicable_item_ids_setSet<Integer>

Returns a Set of applicable item IDs for fast O(1) lookup during batch operations.
NOTE: Prefer matching_item_ids(candidate_ids) when checking a small set of items
(e.g., order line items) as it's much more efficient.

Returns:

  • (Set<Integer>)

    Set of applicable item IDs



168
169
170
# File 'app/models/product_filter.rb', line 168

def applicable_item_ids_set
  @applicable_item_ids_set ||= applicable_items_scope.pluck(:id).to_set
end

#applicable_items_scopeActiveRecord::Relation

Returns an ActiveRecord relation of applicable items using real-time ltree queries.
This is the preferred way to get applicable items as it doesn't require cache maintenance.

Returns:

  • (ActiveRecord::Relation)

    Scope of applicable items



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'app/models/product_filter.rb', line 101

def applicable_items_scope
  base_scope = Item.non_publications.active
  has_pl_or_pc = product_line_ids.present? || product_category_ids.present?

  # Build scope based on what filters are present
  scope = if has_pl_or_pc
            # Product line and/or category filters present
            if product_line_ids.present? && product_category_ids.present?
              base_scope.by_product_line_id(product_line_ids).by_product_category_id(product_category_ids)
            elsif product_line_ids.present?
              base_scope.by_product_line_id(product_line_ids)
            else
              base_scope.by_product_category_id(product_category_ids)
            end
          elsif item_ids.present?
            # Only specific item_ids, no product line/category filters
            base_scope.where(id: item_ids)
          else
            # No inclusion criteria - return none (exclusions only make sense with inclusions)
            Item.none
          end

  # Union with specifically listed items (when we have PL/PC filters AND item_ids)
  scope = scope.or(base_scope.where(id: item_ids)) if has_pl_or_pc && item_ids.present?

  # Apply exclusions
  scope = scope.where.not(id: base_scope.by_product_line_id(exclude_product_line_ids).select(:id)) if exclude_product_line_ids.present?
  scope = scope.where.not(id: base_scope.by_product_category_id(exclude_product_category_ids).select(:id)) if exclude_product_category_ids.present?
  scope = scope.where.not(id: exclude_item_ids) if exclude_item_ids.present?

  scope
end

#assortment_instructionAssortmentInstruction



46
# File 'app/models/product_filter.rb', line 46

belongs_to :assortment_instruction, inverse_of: :product_filters, optional: true

#deep_dupObject



65
66
67
# File 'app/models/product_filter.rb', line 65

def deep_dup
  deep_clone
end

#effective_item_skusObject



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

def effective_item_skus
  effective_items.pluck(:sku)
end

#effective_itemsObject



73
74
75
# File 'app/models/product_filter.rb', line 73

def effective_items
  applicable_items_scope.order(:sku)
end

#exclude_itemsObject



189
190
191
# File 'app/models/product_filter.rb', line 189

def exclude_items
  Item.where(id: exclude_item_ids)
end

#exclude_product_categoriesObject



197
198
199
# File 'app/models/product_filter.rb', line 197

def exclude_product_categories
  ProductCategory.where(id: exclude_product_category_ids)
end

#exclude_product_linesObject



193
194
195
# File 'app/models/product_filter.rb', line 193

def exclude_product_lines
  ProductLine.where(id: exclude_product_line_ids)
end

#has_product_conditions?Boolean

Returns:

  • (Boolean)


81
82
83
# File 'app/models/product_filter.rb', line 81

def has_product_conditions?
  [*item_ids, *product_line_ids, *product_category_ids, *exclude_item_ids, *exclude_product_line_ids, *exclude_product_category_ids].compact.present?
end

#itemsObject



177
178
179
# File 'app/models/product_filter.rb', line 177

def items
  Item.where(id: item_ids)
end

#matching_item_ids(candidate_ids) ⇒ Set<Integer>

Returns which of the given candidate item IDs match this filter's criteria.
This is much more efficient than fetching ALL applicable items when you only
need to check a small set of items (e.g., items from an order).

Results are memoized per candidate set so the same filter evaluated against
the same line items (common during discount calculation across many coupons)
hits the DB only once instead of once per coupon.

Parameters:

  • candidate_ids (Array<Integer>)

    The item IDs to check

Returns:

  • (Set<Integer>)

    Set of matching item IDs from the candidates



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

def matching_item_ids(candidate_ids)
  return Set.new if candidate_ids.blank?

  cache_key = candidate_ids.sort
  @matching_item_ids_cache ||= {}
  @matching_item_ids_cache[cache_key] ||= applicable_items_scope.where(id: candidate_ids).pluck(:id).to_set
end

#meet_conditions?(item, applicable_ids: nil) ⇒ Boolean

Check if an item matches the product filter criteria.
Uses real-time ltree queries - no cache maintenance needed.

Parameters:

  • item (Item)

    The item to check

  • applicable_ids (Set, nil) (defaults to: nil)

    Pre-computed Set of applicable item IDs for batch operations.
    When provided, uses the Set for O(1) lookup (preferred for batch checks).
    When nil, computes applicable_item_ids_set on demand.

Returns:

  • (Boolean)


92
93
94
95
# File 'app/models/product_filter.rb', line 92

def meet_conditions?(item, applicable_ids: nil)
  ids = applicable_ids || applicable_item_ids_set
  ids.include?(item.id)
end

#product_categoriesObject



185
186
187
# File 'app/models/product_filter.rb', line 185

def product_categories
  ProductCategory.where(id: product_category_ids)
end

#product_linesObject



181
182
183
# File 'app/models/product_filter.rb', line 181

def product_lines
  ProductLine.where(id: product_line_ids)
end

#reset_applicable_item_ids_set!Object

Clear the memoized applicable_item_ids_set (call when filter criteria change)



173
174
175
# File 'app/models/product_filter.rb', line 173

def reset_applicable_item_ids_set!
  @applicable_item_ids_set = nil
end

#resourceResource

Returns:

  • (Resource)

See Also:



45
# File 'app/models/product_filter.rb', line 45

belongs_to :resource, polymorphic: true, optional: true

#to_sObject



69
70
71
# File 'app/models/product_filter.rb', line 69

def to_s
  "[ProductFilter:#{id}]"
end