Skip to content

Commit 3c50325

Browse files
authored
Merge pull request #6383 from mamhoff/in-memory-conditions
Allow promotion conditions to work with in-memory line items, improve performance
2 parents 57e8ef9 + 18fd749 commit 3c50325

File tree

8 files changed

+94
-33
lines changed

8 files changed

+94
-33
lines changed

promotions/app/models/concerns/solidus_promotions/conditions/taxon_condition.rb

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,52 @@ def taxon_ids_string=(taxon_ids)
2525
self.taxons = Spree::Taxon.find(taxon_ids)
2626
end
2727

28+
def taxons_ids_with_children=(args)
29+
@taxon_ids_with_children = args
30+
end
31+
2832
private
2933

30-
# ids of taxons conditions and taxons conditions children
31-
def condition_taxon_ids_with_children
32-
taxons.flat_map { |taxon| taxon.self_and_descendants.ids }.uniq
34+
# Returns the cached list of taxon subtree id collections for the selected taxons.
35+
#
36+
# Executes a single SQL query using the nested set (lft/rgt) boundaries to
37+
# fetch each root taxon (one of this condition's taxons) together with all
38+
# of its descendants. The result is memoized for the lifetime of the
39+
# condition instance.
40+
#
41+
# Each inner array contains the IDs of a root taxon and all of its
42+
# descendants (including the root itself). The outer array is ordered by
43+
# the root taxon id.
44+
#
45+
# @return [Array<Array<Integer>>] array of arrays of taxon ids, one per root taxon
46+
# @example
47+
# # For condition with taxons [10, 42]
48+
# condition.condition_taxon_ids_with_children
49+
# # => [[10, 11, 12], [42, 43]]
50+
def taxon_ids_with_children
51+
@taxon_ids_with_children ||= load_taxon_ids_with_children
52+
end
53+
54+
def load_taxon_ids_with_children
55+
aggregation_function = if ActiveRecord::Base.connection.adapter_name.downcase.match?(/postgres/)
56+
"string_agg(child.id::text, ',')"
57+
else
58+
"group_concat(child.id, ',')"
59+
end
60+
61+
sql = <<~SQL
62+
SELECT
63+
parent.id AS root_id,
64+
#{aggregation_function} AS descendant_ids
65+
FROM spree_taxons AS parent
66+
JOIN spree_taxons AS child
67+
ON child.lft BETWEEN parent.lft AND parent.rgt
68+
WHERE parent.id IN (#{taxon_ids.join(',')})
69+
GROUP BY parent.id
70+
ORDER BY parent.id
71+
SQL
72+
rows = ActiveRecord::Base.connection.exec_query(sql)
73+
rows.map { |r| r["descendant_ids"].split(",").map(&:to_i) }
3374
end
3475
end
3576
end

promotions/app/models/solidus_promotions/conditions/line_item_option_value.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class LineItemOptionValue < Condition
1010

1111
def line_item_eligible?(line_item, _options = {})
1212
pid = line_item.product.id
13-
ovids = line_item.variant.option_values.pluck(:id)
13+
ovids = line_item.variant.option_value_ids
1414

1515
product_ids.include?(pid) && (value_ids(pid) & ovids).present?
1616
end

promotions/app/models/solidus_promotions/conditions/line_item_taxon.rb

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,13 @@ class LineItemTaxon < Condition
1515
preference :match_policy, :string, default: MATCH_POLICIES.first
1616

1717
def line_item_eligible?(line_item, _options = {})
18-
found = Spree::Classification.where(
19-
product_id: line_item.variant.product_id,
20-
taxon_id: condition_taxon_ids_with_children
21-
).exists?
18+
line_item_taxon_ids = line_item.variant.product.classifications.map(&:taxon_id)
2219

2320
case preferred_match_policy
2421
when "include"
25-
found
22+
taxon_ids_with_children.any? { |taxon_and_descendant_ids| (line_item_taxon_ids & taxon_and_descendant_ids).any? }
2623
when "exclude"
27-
!found
24+
taxon_ids_with_children.none? { |taxon_and_descendant_ids| (line_item_taxon_ids & taxon_and_descendant_ids).any? }
2825
else
2926
raise "unexpected match policy: #{preferred_match_policy.inspect}"
3027
end

promotions/app/models/solidus_promotions/conditions/order_taxon.rb

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,26 @@ class OrderTaxon < Condition
1212
preference :match_policy, :string, default: MATCH_POLICIES.first
1313

1414
def order_eligible?(order, _options = {})
15-
order_taxons = taxons_in_order(order)
15+
line_item_taxon_ids = taxon_ids_in_order(order)
1616

1717
case preferred_match_policy
1818
when "all"
19-
matches_all = taxons.all? do |condition_taxon|
20-
order_taxons.where(id: condition_taxon.self_and_descendants.ids).exists?
21-
end
19+
unless taxon_ids_with_children.all? { |taxon_and_descendant_ids| (line_item_taxon_ids & taxon_and_descendant_ids).any? }
2220

23-
unless matches_all
2421
eligibility_errors.add(:base, eligibility_error_message(:missing_taxon), error_code: :missing_taxon)
2522
end
2623
when "any"
27-
unless order_taxons.where(id: condition_taxon_ids_with_children).exists?
24+
if taxon_ids_with_children.none? { |taxon_and_descendant_ids| (line_item_taxon_ids & taxon_and_descendant_ids).any? }
25+
2826
eligibility_errors.add(
2927
:base,
3028
eligibility_error_message(:no_matching_taxons),
3129
error_code: :no_matching_taxons
3230
)
3331
end
3432
when "none"
35-
if order_taxons.where(id: condition_taxon_ids_with_children).exists?
33+
if taxon_ids_with_children.any? { |taxon_and_descendant_ids| (line_item_taxon_ids & taxon_and_descendant_ids).any? }
34+
3635
eligibility_errors.add(
3736
:base,
3837
eligibility_error_message(:has_excluded_taxon),
@@ -50,12 +49,11 @@ def to_partial_path
5049

5150
private
5251

53-
# All taxons in an order
54-
def taxons_in_order(order)
55-
Spree::Taxon
56-
.joins(products: { variants_including_master: :line_items })
57-
.where(spree_line_items: { order_id: order.id })
58-
.distinct
52+
# All taxon IDs in an order
53+
def taxon_ids_in_order(order)
54+
order.line_items.flat_map do |line_item|
55+
line_item.variant.product.classifications.map(&:taxon_id)
56+
end
5957
end
6058
end
6159
end

promotions/app/models/solidus_promotions/conditions/taxon.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ class Taxon < Condition
1515

1616
def order_eligible?(order, _options = {})
1717
order_condition = OrderTaxon.new(taxons:, preferred_match_policy:)
18+
19+
# Hydrate the instance cache with our @taxon_ids_with_children cache
20+
order_condition.taxons_ids_with_children = taxon_ids_with_children
21+
1822
order_condition.order_eligible?(order)
1923
@eligibility_errors = order_condition.eligibility_errors
2024
eligibility_errors.empty?
@@ -23,6 +27,10 @@ def order_eligible?(order, _options = {})
2327
def line_item_eligible?(line_item, _options = {})
2428
line_item_match_policy = preferred_match_policy.in?(%w[any all]) ? "include" : "exclude"
2529
line_item_condition = LineItemTaxon.new(taxons:, preferred_match_policy: line_item_match_policy)
30+
31+
# Hydrate the instance cache with our @taxon_ids_with_children cache\
32+
line_item_condition.taxons_ids_with_children = taxon_ids_with_children
33+
2634
result = line_item_condition.line_item_eligible?(line_item)
2735
@eligibility_errors = line_item_condition.eligibility_errors
2836
result

promotions/app/models/solidus_promotions/distributed_amounts_handler.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def initialize(line_items, total_amount)
1212
# @param line_item [LineItem] one of the line_items distributed over
1313
# @return [BigDecimal] the weighted adjustment for this line_item
1414
def amount(line_item)
15-
distributed_amounts[line_item.id].to_d
15+
distributed_amounts[line_item].to_d
1616
end
1717

1818
private
@@ -21,11 +21,7 @@ def amount(line_item)
2121
# @return [Hash<Integer, BigDecimal>] a hash of line item IDs and their
2222
# corresponding weighted adjustments
2323
def distributed_amounts
24-
line_item_ids.zip(allocated_amounts).to_h
25-
end
26-
27-
def line_item_ids
28-
line_items.map(&:id)
24+
line_items.zip(allocated_amounts).to_h
2925
end
3026

3127
def elligible_amounts

promotions/spec/models/solidus_promotions/conditions/order_taxon_spec.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88
let(:condition) do
99
described_class.create!(benefit: promotion_benefit)
1010
end
11-
let(:product) { order.products.first }
12-
let(:order) { create :order_with_line_items }
11+
let(:product) { create(:product) }
12+
let(:order) { create :order }
1313
let(:taxon_one) { create :taxon, name: "first" }
1414
let(:taxon_two) { create :taxon, name: "second" }
1515

1616
it_behaves_like "a taxon condition"
1717

18+
before do
19+
order.line_items.new(quantity: 1, variant: product.master)
20+
end
21+
1822
it { is_expected.to be_updateable }
1923

2024
describe "#eligible?(order)" do
@@ -66,7 +70,7 @@
6670

6771
it "is eligible order has all prefered taxons" do
6872
product.taxons << taxon_two
69-
order.products.last.taxons << taxon_one
73+
product.taxons << taxon_one
7074

7175
condition.taxons = [taxon_one, taxon_two]
7276

@@ -121,7 +125,7 @@
121125

122126
context "one of the order's products is in a listed taxon" do
123127
before do
124-
order.products.first.taxons << taxon_one
128+
product.taxons << taxon_one
125129
condition.taxons << taxon_one
126130
end
127131

promotions/spec/models/solidus_promotions/conditions/taxon_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,21 @@
187187
end
188188
end
189189
end
190+
191+
describe "performance" do
192+
let(:product_one) { create(:product, taxons: [taxon_one]) }
193+
let(:product_two) { create(:product, taxons: [taxon_two]) }
194+
let!(:promotion) { create(:solidus_promotion, :with_adjustable_benefit, apply_automatically: true) }
195+
let(:benefit) { promotion.benefits.first }
196+
let!(:condition) { described_class.create!(taxons: [taxon_one, taxon_two], benefit:) }
197+
let!(:order) { create(:order_with_line_items, line_items_attributes: [{ variant: product_one.master }, { variant: product_two.master }]) }
198+
199+
subject { order.recalculate }
200+
201+
it "only loads the taxon IDs with children once" do
202+
expect_any_instance_of(SolidusPromotions::Conditions::TaxonCondition).to receive(:load_taxon_ids_with_children).once { [[taxon_one.id], [taxon_two.id]] }
203+
204+
subject
205+
end
206+
end
190207
end

0 commit comments

Comments
 (0)