Skip to content

Commit 7158b8d

Browse files
authored
Merge pull request #6363 from mamhoff/order-product-condition
Create single-responsibility conditions for products, taxons, and option values
2 parents f4654cf + a4db2b9 commit 7158b8d

39 files changed

+875
-353
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@ def self.included(klass)
88
end
99

1010
def applicable?(promotable)
11+
if preferred_line_item_applicable == false
12+
Spree.deprecator.warn <<~MSG
13+
Setting `#{self.class.name}#preferred_line_item_applicable` to false is deprecated.
14+
Please use a suitable condition that only checks the order instead, such as `OrderProduct`,
15+
`OrderTaxon`, or `OrderOptionValue`. If you have included the `LineItemApplicableOrderLevelCondition` module
16+
yourself, create a new condition that only checks orders:
17+
```
18+
class MyCondition < SolidusPromotions::Condition
19+
def order_eligible?(order, _options = {})
20+
# your logic here
21+
end
22+
end
23+
```
24+
MSG
25+
end
1126
promotable.is_a?(Spree::LineItem) ? preferred_line_item_applicable && super : super
1227
end
1328

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module SolidusPromotions
4+
module Conditions
5+
module OptionValueCondition
6+
def self.included(base)
7+
base.preference :eligible_values, :hash
8+
base.remove_method :preferred_eligible_values
9+
end
10+
11+
def preferred_eligible_values
12+
values = preferences[:eligible_values] || {}
13+
values.keys.map(&:to_i).zip(
14+
values.values.map do |value|
15+
(value.is_a?(Array) ? value : value.split(",")).map(&:to_i)
16+
end
17+
).to_h
18+
end
19+
end
20+
end
21+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
module SolidusPromotions
4+
module Conditions
5+
module ProductCondition
6+
def self.included(base)
7+
base.has_many :condition_products,
8+
dependent: :destroy,
9+
foreign_key: :condition_id,
10+
class_name: "SolidusPromotions::ConditionProduct",
11+
inverse_of: :condition
12+
base.has_many :products, class_name: "Spree::Product", through: :condition_products
13+
end
14+
15+
def preload_relations
16+
[:products]
17+
end
18+
19+
def product_ids_string
20+
product_ids.join(",")
21+
end
22+
23+
def product_ids_string=(product_ids)
24+
self.product_ids = product_ids.to_s.split(",").map(&:strip)
25+
end
26+
end
27+
end
28+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
module SolidusPromotions
4+
module Conditions
5+
module TaxonCondition
6+
def self.included(base)
7+
base.has_many :condition_taxons,
8+
class_name: "SolidusPromotions::ConditionTaxon",
9+
foreign_key: :condition_id,
10+
dependent: :destroy,
11+
inverse_of: :condition
12+
base.has_many :taxons, through: :condition_taxons, class_name: "Spree::Taxon"
13+
end
14+
15+
def preload_relations
16+
[:taxons]
17+
end
18+
19+
def taxon_ids_string
20+
taxon_ids.join(",")
21+
end
22+
23+
def taxon_ids_string=(taxon_ids)
24+
taxon_ids = taxon_ids.to_s.split(",").map(&:strip)
25+
self.taxons = Spree::Taxon.find(taxon_ids)
26+
end
27+
28+
private
29+
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
33+
end
34+
end
35+
end
36+
end

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

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class LineItemOptionValue < Condition
66
# TODO: Remove in Solidus 5
77
include LineItemLevelCondition
88

9-
preference :eligible_values, :hash
9+
include OptionValueCondition
1010

1111
def line_item_eligible?(line_item, _options = {})
1212
pid = line_item.product.id
@@ -15,15 +15,6 @@ def line_item_eligible?(line_item, _options = {})
1515
product_ids.include?(pid) && (value_ids(pid) & ovids).present?
1616
end
1717

18-
def preferred_eligible_values
19-
values = preferences[:eligible_values] || {}
20-
values.keys.map(&:to_i).zip(
21-
values.values.map do |value|
22-
(value.is_a?(Array) ? value : value.split(",")).map(&:to_i)
23-
end
24-
).to_h
25-
end
26-
2718
private
2819

2920
def product_ids

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

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,12 @@ class LineItemProduct < Condition
77
# TODO: Remove in Solidus 5
88
include LineItemLevelCondition
99

10-
MATCH_POLICIES = %w[include exclude].freeze
10+
include ProductCondition
1111

12-
has_many :condition_products,
13-
dependent: :destroy,
14-
foreign_key: :condition_id,
15-
class_name: "SolidusPromotions::ConditionProduct",
16-
inverse_of: :condition
17-
has_many :products,
18-
class_name: "Spree::Product",
19-
through: :condition_products
12+
MATCH_POLICIES = %w[include exclude].freeze
2013

2114
preference :match_policy, :string, default: MATCH_POLICIES.first
2215

23-
def preload_relations
24-
[:products]
25-
end
26-
2716
def line_item_eligible?(line_item, _options = {})
2817
order_includes_product = product_ids.include?(line_item.variant.product_id)
2918
success = inverse? ? !order_includes_product : order_includes_product
@@ -40,14 +29,6 @@ def line_item_eligible?(line_item, _options = {})
4029
success
4130
end
4231

43-
def product_ids_string
44-
product_ids.join(",")
45-
end
46-
47-
def product_ids_string=(product_ids)
48-
self.product_ids = product_ids.to_s.split(",").map(&:strip)
49-
end
50-
5132
private
5233

5334
def inverse?

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

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,14 @@ class LineItemTaxon < Condition
66
# TODO: Remove in Solidus 5
77
include LineItemLevelCondition
88

9-
has_many :condition_taxons,
10-
class_name: "SolidusPromotions::ConditionTaxon",
11-
foreign_key: :condition_id,
12-
dependent: :destroy,
13-
inverse_of: :condition
14-
has_many :taxons, through: :condition_taxons, class_name: "Spree::Taxon"
9+
include TaxonCondition
1510

1611
MATCH_POLICIES = %w[include exclude].freeze
1712

1813
validates :preferred_match_policy, inclusion: { in: MATCH_POLICIES }
1914

2015
preference :match_policy, :string, default: MATCH_POLICIES.first
2116

22-
def preload_relations
23-
[:taxons]
24-
end
25-
2617
def line_item_eligible?(line_item, _options = {})
2718
found = Spree::Classification.where(
2819
product_id: line_item.variant.product_id,
@@ -38,26 +29,6 @@ def line_item_eligible?(line_item, _options = {})
3829
raise "unexpected match policy: #{preferred_match_policy.inspect}"
3930
end
4031
end
41-
42-
def taxon_ids_string
43-
taxons.pluck(:id).join(",")
44-
end
45-
46-
def taxon_ids_string=(taxon_ids)
47-
taxon_ids = taxon_ids.to_s.split(",").map(&:strip)
48-
self.taxons = Spree::Taxon.find(taxon_ids)
49-
end
50-
51-
def updateable?
52-
true
53-
end
54-
55-
private
56-
57-
# ids of taxons conditions and taxons conditions children
58-
def condition_taxon_ids_with_children
59-
taxons.flat_map { |taxon| taxon.self_and_descendants.ids }.uniq
60-
end
6132
end
6233
end
6334
end

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,15 @@ module Conditions
55
class OptionValue < Condition
66
include LineItemApplicableOrderLevelCondition
77

8-
preference :eligible_values, :hash
8+
include OptionValueCondition
99

1010
def order_eligible?(order, _options = {})
11-
order.line_items.any? { |item| line_item_eligible?(item) }
11+
OrderOptionValue.new(preferred_eligible_values: preferred_eligible_values).eligible?(order)
1212
end
1313

1414
def line_item_eligible?(line_item, _options = {})
1515
LineItemOptionValue.new(preferred_eligible_values: preferred_eligible_values).eligible?(line_item)
1616
end
17-
18-
def preferred_eligible_values
19-
values = preferences[:eligible_values] || {}
20-
values.keys.map(&:to_i).zip(
21-
values.values.map do |value|
22-
(value.is_a?(Array) ? value : value.split(",")).map(&:to_i)
23-
end
24-
).to_h
25-
end
2617
end
2718
end
2819
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module SolidusPromotions
4+
module Conditions
5+
class OrderOptionValue < Condition
6+
include OptionValueCondition
7+
8+
def order_eligible?(order, _options = {})
9+
order.line_items.any? do |line_item|
10+
LineItemOptionValue.new(preferred_eligible_values: preferred_eligible_values).eligible?(line_item)
11+
end
12+
end
13+
14+
def to_partial_path
15+
"solidus_promotions/admin/condition_fields/option_value"
16+
end
17+
end
18+
end
19+
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
module SolidusPromotions
4+
module Conditions
5+
# A condition to limit a promotion based on products in the order. Can
6+
# require all or any of the products to be present. Valid products
7+
# either come from assigned product group or are assingned directly to
8+
# the condition.
9+
class OrderProduct < Condition
10+
include ProductCondition
11+
12+
MATCH_POLICIES = %w[any all none only].freeze
13+
14+
validates :preferred_match_policy, inclusion: { in: MATCH_POLICIES }
15+
16+
preference :match_policy, :string, default: MATCH_POLICIES.first
17+
18+
# scope/association that is used to test eligibility
19+
def eligible_products
20+
products
21+
end
22+
23+
def order_eligible?(order)
24+
return true if eligible_products.empty?
25+
26+
case preferred_match_policy
27+
when "all"
28+
unless eligible_products.all? { |product| order_products(order).include?(product) }
29+
eligibility_errors.add(:base, eligibility_error_message(:missing_product), error_code: :missing_product)
30+
end
31+
when "any"
32+
unless order_products(order).any? { |product| eligible_products.include?(product) }
33+
eligibility_errors.add(:base, eligibility_error_message(:no_applicable_products),
34+
error_code: :no_applicable_products)
35+
end
36+
when "none"
37+
unless order_products(order).none? { |product| eligible_products.include?(product) }
38+
eligibility_errors.add(:base, eligibility_error_message(:has_excluded_product),
39+
error_code: :has_excluded_product)
40+
end
41+
when "only"
42+
unless order_products(order).all? { |product| eligible_products.include?(product) }
43+
eligibility_errors.add(:base, eligibility_error_message(:has_excluded_product),
44+
error_code: :has_excluded_product)
45+
end
46+
end
47+
48+
eligibility_errors.empty?
49+
end
50+
51+
def to_partial_path
52+
"solidus_promotions/admin/condition_fields/product"
53+
end
54+
55+
private
56+
57+
def order_products(order)
58+
order.line_items.map(&:variant).map(&:product)
59+
end
60+
end
61+
end
62+
end

0 commit comments

Comments
 (0)