Class: ProductFilter::LineQualifier

Inherits:
Object
  • Object
show all
Defined in:
app/services/product_filter/line_qualifier.rb

Overview

Logic extraction for determining if a set of line items
qualify according to the rules set by an array of
product filters

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(line_items, product_filters, options = {}) ⇒ LineQualifier

Returns a new instance of LineQualifier.



7
8
9
10
11
12
# File 'app/services/product_filter/line_qualifier.rb', line 7

def initialize(line_items, product_filters, options = {})
  @line_items = line_items.reject(&:marked_for_destruction?)
  @product_filters = product_filters
  @options = options
  @logger = options[:logger] || Rails.logger
end

Instance Attribute Details

#line_itemsObject (readonly)

Returns the value of attribute line_items.



5
6
7
# File 'app/services/product_filter/line_qualifier.rb', line 5

def line_items
  @line_items
end

#optionsObject (readonly)

Returns the value of attribute options.



5
6
7
# File 'app/services/product_filter/line_qualifier.rb', line 5

def options
  @options
end

#product_filtersObject (readonly)

Returns the value of attribute product_filters.



5
6
7
# File 'app/services/product_filter/line_qualifier.rb', line 5

def product_filters
  @product_filters
end

#qty_min_repeatObject (readonly)

Returns the value of attribute qty_min_repeat.



5
6
7
# File 'app/services/product_filter/line_qualifier.rb', line 5

def qty_min_repeat
  @qty_min_repeat
end

Instance Method Details

#codeObject



22
23
24
# File 'app/services/product_filter/line_qualifier.rb', line 22

def code
  @options[:code]
end

#coupon_tierObject



18
19
20
# File 'app/services/product_filter/line_qualifier.rb', line 18

def coupon_tier
  @options[:coupon_tier]
end

#exclusion_levelObject



14
15
16
# File 'app/services/product_filter/line_qualifier.rb', line 14

def exclusion_level
  @options[:exclusion_level]
end

#qualifying_line_items?Boolean

Returns:

  • (Boolean)


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'app/services/product_filter/line_qualifier.rb', line 26

def qualifying_line_items?
  # if we have an exclusive tier coupon and there's already a coupon of the same tier present, qualify is false
  if exclusion_level.to_s == 'exclusive_per_tier' &&
     coupon_tier.present? &&
     li = line_items.detect { |li| li.discounts.any? { |lid| lid.coupon.tier == coupon_tier && lid.coupon.code != code && lid.coupon.exclusion_level == exclusion_level.to_s } }
    @logger.debug "exclusive_per_tier: Already has a coupon of tier #{coupon_tier} on line id #{li.id}"
    return false
  end

  # Shortcut, true if any lines are present in case of empty product filters
  return @line_items.present? if @product_filters.empty?

  # Pre-compute: group line items by item and extract unique item IDs once
  # This avoids N+1 queries on item associations and enables targeted filter queries
  @lines_by_item ||= @line_items.group_by(&:item)
  candidate_item_ids = @lines_by_item.keys.map(&:id)

  # Go through each filter and attempt to match a line.  Filters are all AND conditions on a given line and multiple filter must be met within the itemizable's lines
  qty_min_repeat_counters = []

  qualifying = true

  @product_filters.each do |pf|
    pf.criteria_met = false
    total_qty_in_lines = 0
    total_sqft_in_lines = 0.0
    total_linear_ft_in_lines = 0.0
    total_msrp_in_lines = 0.0

    # Query only the candidate items (from order's line items) instead of ALL applicable items.
    # This is much faster: ~0.5-2ms for a targeted query vs ~1-5ms fetching potentially hundreds of IDs.
    matching_ids = pf.matching_item_ids(candidate_item_ids)

    # We group to tally up all lines for a given item.
    @lines_by_item.each do |item, lines|
      # If we have an exclusive coupon tier for this item on any of the line we skip the coupon
      if exclusion_level.to_s == 'exclusive_per_item_per_tier' &&
         coupon_tier.present? &&
         li = lines.detect { |li| li.discounts.any? { |lid| lid.coupon.tier == coupon_tier } }
        # If there is already a coupon on this item in this tier, ignore this qualifier and move to the next
        @logger.debug "exclusive_per_item_per_tier: #{item.id} already has a coupon of tier #{coupon_tier} on line id #{li.id}"
        next
      end
      # Check if this item matches the filter using the pre-computed matching IDs
      next unless matching_ids.include?(item.id)

      qty_in_lines = lines.sum(&:quantity).to_i
      total_qty_in_lines += qty_in_lines
      # Here we're going to skip over items that have length x width but focus only
      # on those that have coverage or sqft specified as a spec
      if item.has_spec?(:sqft) || item.has_spec?(:coverage)
        begin
          total_sqft_in_lines += qty_in_lines * item.sqft.to_f
        rescue StandardError => e
          ErrorReporting.error e, "Unable to calculate sqft for #{item.sku}"
        end
      end

      begin
        lin_ft = item.linear_ft || 0.0
        total_linear_ft_in_lines += qty_in_lines * lin_ft
      rescue StandardError => e
        ErrorReporting.error e, "Unable to calculate lin_ft for #{item.sku}"
      end
      total_msrp_in_lines += (line_msrp_total = lines.sum(&:msrp_total))
    end
    # Item quantities requirements
    item_qty_cond_met = ((pf.min_qty.nil? or total_qty_in_lines >= pf.min_qty) and (pf.max_qty.nil? or total_qty_in_lines <= pf.max_qty))
    # Store how many times the item condition is met.
    qty_min_repeat_counters << (total_qty_in_lines / [pf.min_qty, 1].max).floor
    # item sq ft requirements
    total_sqft_in_lines = total_sqft_in_lines.ceil # Rounding up to next integer
    item_sqft_cond_met = ((pf.min_sq_ft.nil? or total_sqft_in_lines >= pf.min_sq_ft.to_i) and (pf.max_sq_ft.nil? or total_sqft_in_lines <= pf.max_sq_ft))
    # item lin ft requirements
    total_linear_ft_in_lines = total_linear_ft_in_lines.ceil # Rounding up to next integer
    item_linear_ft_cond_met = ((pf.min_lin_ft.nil? or total_linear_ft_in_lines >= pf.min_lin_ft) and (pf.max_lin_ft.nil? or total_linear_ft_in_lines <= pf.max_lin_ft))
    # item msrp amount requirements
    item_msrp_cond_met = ((pf.min_msrp_amount.nil? or total_msrp_in_lines >= pf.min_msrp_amount) and (pf.max_msrp_amount.nil? or total_msrp_in_lines <= pf.max_msrp_amount))

    pf.criteria_met = (item_qty_cond_met and item_sqft_cond_met and item_msrp_cond_met and item_linear_ft_cond_met)
    @logger.debug "Product Filter #{pf.id} criteria met : #{pf.criteria_met}"
    qualifying = pf.criteria_met

    break unless qualifying # no need to continue processing if we are no longer qualifying.
  end
  @qty_min_repeat = qty_min_repeat_counters.min || 0 # Smallest occurence

  qualifying
end