solidus_promotions 4.6.2 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/MIGRATING.md +10 -4
  3. data/README.md +7 -0
  4. data/app/javascript/backend/solidus_promotions/controllers/product_option_values_controller.js +3 -26
  5. data/app/javascript/backend/solidus_promotions/web_components/option_value_picker.js +36 -19
  6. data/app/javascript/backend/solidus_promotions/web_components/product_picker.js +6 -1
  7. data/app/jobs/solidus_promotions/promotion_code_batch_job.rb +1 -1
  8. data/app/models/concerns/solidus_promotions/adjustment_discounts.rb +20 -0
  9. data/app/models/concerns/solidus_promotions/benefits/line_item_benefit.rb +5 -0
  10. data/app/models/concerns/solidus_promotions/benefits/order_benefit.rb +5 -0
  11. data/app/models/concerns/solidus_promotions/benefits/shipment_benefit.rb +5 -0
  12. data/app/models/concerns/solidus_promotions/calculators/promotion_calculator.rb +7 -0
  13. data/app/models/concerns/solidus_promotions/conditions/line_item_applicable_order_level_condition.rb +17 -5
  14. data/app/models/concerns/solidus_promotions/conditions/line_item_level_condition.rb +15 -2
  15. data/app/models/concerns/solidus_promotions/conditions/option_value_condition.rb +21 -0
  16. data/app/models/concerns/solidus_promotions/conditions/order_level_condition.rb +15 -2
  17. data/app/models/concerns/solidus_promotions/conditions/product_condition.rb +28 -0
  18. data/app/models/concerns/solidus_promotions/conditions/shipment_level_condition.rb +15 -2
  19. data/app/models/concerns/solidus_promotions/conditions/taxon_condition.rb +77 -0
  20. data/app/models/concerns/solidus_promotions/coupon_code_normalizer.rb +37 -0
  21. data/app/models/concerns/solidus_promotions/discountable_amount.rb +3 -4
  22. data/app/models/concerns/solidus_promotions/discounted_amount.rb +54 -0
  23. data/app/models/solidus_promotions/benefit.rb +257 -36
  24. data/app/models/solidus_promotions/benefits/adjust_line_item.rb +28 -3
  25. data/app/models/solidus_promotions/benefits/adjust_line_item_quantity_groups.rb +1 -0
  26. data/app/models/solidus_promotions/benefits/adjust_shipment.rb +45 -3
  27. data/app/models/solidus_promotions/benefits/advertise_price.rb +28 -0
  28. data/app/models/solidus_promotions/benefits/create_discounted_item.rb +30 -7
  29. data/app/models/solidus_promotions/calculators/distributed_amount.rb +34 -8
  30. data/app/models/solidus_promotions/calculators/flat_rate.rb +52 -6
  31. data/app/models/solidus_promotions/calculators/flexi_rate.rb +69 -6
  32. data/app/models/solidus_promotions/calculators/percent.rb +40 -4
  33. data/app/models/solidus_promotions/calculators/percent_with_cap.rb +44 -3
  34. data/app/models/solidus_promotions/calculators/tiered_flat_rate.rb +81 -19
  35. data/app/models/solidus_promotions/calculators/tiered_percent.rb +96 -25
  36. data/app/models/solidus_promotions/calculators/tiered_percent_on_eligible_item_quantity.rb +101 -16
  37. data/app/models/solidus_promotions/condition.rb +186 -7
  38. data/app/models/solidus_promotions/conditions/first_order.rb +3 -1
  39. data/app/models/solidus_promotions/conditions/first_repeat_purchase_since.rb +3 -2
  40. data/app/models/solidus_promotions/conditions/item_total.rb +2 -1
  41. data/app/models/solidus_promotions/conditions/line_item_option_value.rb +4 -12
  42. data/app/models/solidus_promotions/conditions/line_item_product.rb +4 -22
  43. data/app/models/solidus_promotions/conditions/line_item_taxon.rb +7 -38
  44. data/app/models/solidus_promotions/conditions/minimum_quantity.rb +3 -2
  45. data/app/models/solidus_promotions/conditions/nth_order.rb +3 -2
  46. data/app/models/solidus_promotions/conditions/one_use_per_user.rb +2 -1
  47. data/app/models/solidus_promotions/conditions/option_value.rb +6 -11
  48. data/app/models/solidus_promotions/conditions/order_option_value.rb +19 -0
  49. data/app/models/solidus_promotions/conditions/order_product.rb +62 -0
  50. data/app/models/solidus_promotions/conditions/order_taxon.rb +60 -0
  51. data/app/models/solidus_promotions/conditions/price_option_value.rb +26 -0
  52. data/app/models/solidus_promotions/conditions/price_product.rb +36 -0
  53. data/app/models/solidus_promotions/conditions/price_taxon.rb +28 -0
  54. data/app/models/solidus_promotions/conditions/product.rb +17 -59
  55. data/app/models/solidus_promotions/conditions/shipping_method.rb +3 -5
  56. data/app/models/solidus_promotions/conditions/store.rb +2 -1
  57. data/app/models/solidus_promotions/conditions/taxon.rb +24 -73
  58. data/app/models/solidus_promotions/conditions/user.rb +2 -1
  59. data/app/models/solidus_promotions/conditions/user_logged_in.rb +1 -3
  60. data/app/models/solidus_promotions/conditions/user_role.rb +1 -3
  61. data/app/models/solidus_promotions/distributed_amounts_handler.rb +2 -6
  62. data/app/models/solidus_promotions/eligibility_results.rb +1 -0
  63. data/app/models/solidus_promotions/item_discount.rb +1 -0
  64. data/app/models/solidus_promotions/order_adjuster/discount_order.rb +29 -35
  65. data/app/models/solidus_promotions/order_adjuster/recalculate_promo_totals.rb +45 -0
  66. data/app/models/solidus_promotions/order_adjuster/set_discounts_to_zero.rb +33 -0
  67. data/app/models/solidus_promotions/order_adjuster.rb +4 -14
  68. data/app/models/solidus_promotions/order_promotion.rb +1 -0
  69. data/app/models/solidus_promotions/product_advertiser.rb +57 -0
  70. data/app/models/solidus_promotions/promotion.rb +12 -10
  71. data/app/models/solidus_promotions/promotion_code/batch_builder.rb +1 -1
  72. data/app/models/solidus_promotions/promotion_code.rb +4 -4
  73. data/app/models/solidus_promotions/promotion_code_batch.rb +1 -1
  74. data/app/models/solidus_promotions/promotion_handler/coupon.rb +1 -1
  75. data/app/models/solidus_promotions/promotion_handler/page.rb +1 -1
  76. data/app/models/solidus_promotions/promotion_lane.rb +48 -0
  77. data/app/models/solidus_promotions/shipping_rate_discount.rb +3 -0
  78. data/app/patches/models/solidus_promotions/line_item_patch.rb +2 -0
  79. data/app/patches/models/solidus_promotions/order_patch.rb +8 -0
  80. data/app/patches/models/solidus_promotions/order_recalculator_patch.rb +3 -1
  81. data/app/patches/models/solidus_promotions/price_patch.rb +31 -0
  82. data/app/patches/models/solidus_promotions/shipment_patch.rb +2 -0
  83. data/app/patches/models/solidus_promotions/shipping_rate_patch.rb +15 -0
  84. data/config/locales/en.yml +47 -11
  85. data/config/routes.rb +1 -1
  86. data/db/migrate/20230703101637_create_promotions.rb +2 -2
  87. data/db/migrate/20230703113625_create_promotion_benefits.rb +3 -3
  88. data/db/migrate/20230703141116_create_promotion_categories.rb +1 -1
  89. data/db/migrate/20230703143943_create_promotion_conditions.rb +1 -1
  90. data/db/migrate/20230704083830_add_condition_join_tables.rb +8 -8
  91. data/db/migrate/20230704102444_create_promotion_codes.rb +1 -1
  92. data/db/migrate/20230704102656_create_promotion_code_batches.rb +1 -1
  93. data/db/migrate/20230705171556_create_order_promotions.rb +3 -3
  94. data/db/migrate/20230725074235_create_shipping_rate_discounts.rb +2 -2
  95. data/db/migrate/20231104135812_add_managed_by_order_benefit_to_line_items.rb +1 -1
  96. data/db/migrate/20251104170913_update_promotion_code_value_collation.rb +38 -0
  97. data/db/migrate/20251104214304_separate_out_order_only_conditions.rb +41 -0
  98. data/eslint.config.mjs +29 -0
  99. data/lib/components/admin/solidus_promotions/promotion_categories/index/component.rb +6 -6
  100. data/lib/components/admin/solidus_promotions/promotions/index/component.rb +5 -5
  101. data/lib/solidus_promotions/configuration.rb +57 -12
  102. data/lib/solidus_promotions/promotion_map.rb +14 -14
  103. data/lib/solidus_promotions/testing_support/shared_examples/option_value_condition.rb +18 -0
  104. data/lib/solidus_promotions/testing_support/shared_examples/product_condition.rb +37 -0
  105. data/lib/solidus_promotions/testing_support/shared_examples/promotion_calculator.rb +11 -0
  106. data/lib/solidus_promotions/testing_support/shared_examples/taxon_condition.rb +37 -0
  107. data/lib/solidus_promotions/testing_support/shared_examples.rb +6 -0
  108. data/lib/views/backend/solidus_promotions/admin/benefit_fields/_advertise_price.html.erb +7 -0
  109. data/lib/views/backend/solidus_promotions/admin/calculator_fields/_default_fields.html.erb +1 -1
  110. data/lib/views/backend/solidus_promotions/admin/calculator_fields/percent/_fields.html.erb +1 -1
  111. data/lib/views/backend/solidus_promotions/admin/condition_fields/_line_item_option_value.html.erb +6 -5
  112. data/lib/views/backend/solidus_promotions/admin/condition_fields/_option_value.html.erb +6 -12
  113. data/lib/views/backend/solidus_promotions/admin/condition_fields/_price_option_value.html.erb +26 -0
  114. data/lib/views/backend/solidus_promotions/admin/condition_fields/_price_product.html.erb +21 -0
  115. data/lib/views/backend/solidus_promotions/admin/condition_fields/_price_taxon.html.erb +17 -0
  116. data/lib/views/backend/solidus_promotions/admin/condition_fields/_product.html.erb +0 -7
  117. data/lib/views/backend/solidus_promotions/admin/condition_fields/_taxon.html.erb +0 -7
  118. data/lib/views/backend/solidus_promotions/admin/condition_fields/line_item_option_value/_option_value_fields.html.erb +10 -4
  119. data/solidus_promotions.gemspec +1 -1
  120. metadata +37 -6
  121. data/.eslintrc.json +0 -10
  122. data/app/models/solidus_promotions/order_adjuster/persist_discounted_order.rb +0 -79
@@ -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
@@ -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,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ class OrderTaxon < Condition
6
+ include TaxonCondition
7
+
8
+ MATCH_POLICIES = %w[any all none].freeze
9
+
10
+ validates :preferred_match_policy, inclusion: {in: MATCH_POLICIES}
11
+
12
+ preference :match_policy, :string, default: MATCH_POLICIES.first
13
+
14
+ def order_eligible?(order, _options = {})
15
+ line_item_taxon_ids = taxon_ids_in_order(order)
16
+
17
+ case preferred_match_policy
18
+ when "all"
19
+ unless taxon_ids_with_children.all? { |taxon_and_descendant_ids| (line_item_taxon_ids & taxon_and_descendant_ids).any? }
20
+
21
+ eligibility_errors.add(:base, eligibility_error_message(:missing_taxon), error_code: :missing_taxon)
22
+ end
23
+ when "any"
24
+ if taxon_ids_with_children.none? { |taxon_and_descendant_ids| (line_item_taxon_ids & taxon_and_descendant_ids).any? }
25
+
26
+ eligibility_errors.add(
27
+ :base,
28
+ eligibility_error_message(:no_matching_taxons),
29
+ error_code: :no_matching_taxons
30
+ )
31
+ end
32
+ when "none"
33
+ if taxon_ids_with_children.any? { |taxon_and_descendant_ids| (line_item_taxon_ids & taxon_and_descendant_ids).any? }
34
+
35
+ eligibility_errors.add(
36
+ :base,
37
+ eligibility_error_message(:has_excluded_taxon),
38
+ error_code: :has_excluded_taxon
39
+ )
40
+ end
41
+ end
42
+
43
+ eligibility_errors.empty?
44
+ end
45
+
46
+ def to_partial_path
47
+ "solidus_promotions/admin/condition_fields/taxon"
48
+ end
49
+
50
+ private
51
+
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
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ class PriceOptionValue < Condition
6
+ include OptionValueCondition
7
+
8
+ def price_eligible?(price, _options = {})
9
+ pid = price.variant.product_id
10
+ ovids = price.variant.option_value_ids
11
+
12
+ product_ids.include?(pid) && (value_ids(pid) & ovids).present?
13
+ end
14
+
15
+ private
16
+
17
+ def product_ids
18
+ preferred_eligible_values.keys
19
+ end
20
+
21
+ def value_ids(product_id)
22
+ preferred_eligible_values[product_id]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ # A condition to apply a promotion only to prices with or without selected products
6
+ class PriceProduct < Condition
7
+ include ProductCondition
8
+
9
+ MATCH_POLICIES = %w[include exclude].freeze
10
+
11
+ preference :match_policy, :string, default: MATCH_POLICIES.first
12
+
13
+ def price_eligible?(price, _options = {})
14
+ price_matches_products = products.include?(price.variant.product)
15
+ success = exclude_configured_products? ? !price_matches_products : price_matches_products
16
+
17
+ unless success
18
+ message_code = exclude_configured_products? ? :has_excluded_product : :no_applicable_products
19
+ eligibility_errors.add(
20
+ :base,
21
+ eligibility_error_message(message_code),
22
+ error_code: message_code
23
+ )
24
+ end
25
+
26
+ success
27
+ end
28
+
29
+ private
30
+
31
+ def exclude_configured_products?
32
+ preferred_match_policy == "exclude"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module Conditions
5
+ class PriceTaxon < Condition
6
+ include TaxonCondition
7
+
8
+ MATCH_POLICIES = %w[include exclude].freeze
9
+
10
+ validates :preferred_match_policy, inclusion: {in: MATCH_POLICIES}
11
+
12
+ preference :match_policy, :string, default: MATCH_POLICIES.first
13
+
14
+ def price_eligible?(price, _options = {})
15
+ price_taxon_ids = price.variant.product.classifications.map(&:taxon_id)
16
+
17
+ case preferred_match_policy
18
+ when "include"
19
+ taxon_ids_with_children.any? { |taxon_and_descendant_ids| (price_taxon_ids & taxon_and_descendant_ids).any? }
20
+ when "exclude"
21
+ taxon_ids_with_children.none? { |taxon_and_descendant_ids| (price_taxon_ids & taxon_and_descendant_ids).any? }
22
+ else
23
+ raise "unexpected match policy: #{preferred_match_policy.inspect}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -9,77 +9,35 @@ module SolidusPromotions
9
9
  class Product < Condition
10
10
  include LineItemApplicableOrderLevelCondition
11
11
 
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, class_name: "Spree::Product", through: :condition_products
18
-
19
- def preload_relations
20
- [:products]
21
- end
12
+ include ProductCondition
22
13
 
23
14
  MATCH_POLICIES = %w[any all none only].freeze
24
15
 
25
- validates :preferred_match_policy, inclusion: { in: MATCH_POLICIES }
16
+ validates :preferred_match_policy, inclusion: {in: MATCH_POLICIES}
26
17
 
27
18
  preference :match_policy, :string, default: MATCH_POLICIES.first
28
19
 
29
- # scope/association that is used to test eligibility
30
- def eligible_products
31
- products
32
- end
33
-
34
- def order_eligible?(order)
35
- return true if eligible_products.empty?
36
-
37
- case preferred_match_policy
38
- when "all"
39
- unless eligible_products.all? { |product| order_products(order).include?(product) }
40
- eligibility_errors.add(:base, eligibility_error_message(:missing_product), error_code: :missing_product)
41
- end
42
- when "any"
43
- unless order_products(order).any? { |product| eligible_products.include?(product) }
44
- eligibility_errors.add(:base, eligibility_error_message(:no_applicable_products),
45
- error_code: :no_applicable_products)
46
- end
47
- when "none"
48
- unless order_products(order).none? { |product| eligible_products.include?(product) }
49
- eligibility_errors.add(:base, eligibility_error_message(:has_excluded_product),
50
- error_code: :has_excluded_product)
51
- end
52
- when "only"
53
- unless order_products(order).all? { |product| eligible_products.include?(product) }
54
- eligibility_errors.add(:base, eligibility_error_message(:has_excluded_product),
55
- error_code: :has_excluded_product)
56
- end
57
- else
58
- raise "unexpected match policy: #{preferred_match_policy.inspect}"
59
- end
60
-
20
+ def order_eligible?(order, _options = {})
21
+ order_condition = OrderProduct.new(products:, preferred_match_policy:)
22
+ order_condition.order_eligible?(order)
23
+ @eligibility_errors = order_condition.eligibility_errors
61
24
  eligibility_errors.empty?
62
25
  end
63
26
 
64
27
  def line_item_eligible?(line_item, _options = {})
65
- # The order level eligibility check happens first, and if none of the products
66
- # are in the order, then no line items should be available to check.
67
- raise "This should not happen" if preferred_match_policy == "none"
68
- product_ids.include?(line_item.variant.product_id)
69
- end
70
-
71
- def product_ids_string
72
- product_ids.join(",")
73
- end
74
-
75
- def product_ids_string=(product_ids)
76
- self.product_ids = product_ids.to_s.split(",").map(&:strip)
28
+ line_item_match_policy = preferred_match_policy.in?(%w[any all only]) ? "include" : "exclude"
29
+ line_item_condition = LineItemProduct.new(products:, preferred_match_policy: line_item_match_policy)
30
+ line_item_condition.line_item_eligible?(line_item)
31
+ @eligibility_errors = line_item_condition.eligibility_errors
32
+ eligibility_errors.empty?
77
33
  end
78
34
 
79
- private
80
-
81
- def order_products(order)
82
- order.line_items.map(&:variant).map(&:product)
35
+ def price_eligible?(price, _options = {})
36
+ price_match_policy = preferred_match_policy.in?(%w[any all only]) ? "include" : "exclude"
37
+ price_condition = PriceProduct.new(products:, preferred_match_policy: price_match_policy)
38
+ price_condition.price_eligible?(price)
39
+ @eligibility_errors = price_condition.eligibility_errors
40
+ eligibility_errors.empty?
83
41
  end
84
42
  end
85
43
  end
@@ -3,17 +3,15 @@
3
3
  module SolidusPromotions
4
4
  module Conditions
5
5
  class ShippingMethod < Condition
6
+ # TODO: Remove in Solidus 5
6
7
  include ShipmentLevelCondition
7
8
 
8
9
  preference :shipping_method_ids, type: :array, default: []
9
10
 
10
- def applicable?(promotable)
11
- promotable.is_a?(Spree::Shipment) || promotable.is_a?(Spree::ShippingRate)
12
- end
13
-
14
- def eligible?(promotable)
11
+ def shipment_eligible?(promotable, _options = {})
15
12
  promotable.shipping_method&.id&.in?(preferred_shipping_method_ids.map(&:to_i))
16
13
  end
14
+ alias_method :shipping_rate_eligible?, :shipment_eligible?
17
15
  end
18
16
  end
19
17
  end
@@ -3,6 +3,7 @@
3
3
  module SolidusPromotions
4
4
  module Conditions
5
5
  class Store < Condition
6
+ # TODO: Remove in Solidus 5
6
7
  include OrderLevelCondition
7
8
 
8
9
  has_many :condition_stores,
@@ -16,7 +17,7 @@ module SolidusPromotions
16
17
  [:stores]
17
18
  end
18
19
 
19
- def eligible?(order, _options = {})
20
+ def order_eligible?(order, _options = {})
20
21
  stores.none? || stores.include?(order.store)
21
22
  end
22
23
 
@@ -5,96 +5,47 @@ module SolidusPromotions
5
5
  class Taxon < Condition
6
6
  include LineItemApplicableOrderLevelCondition
7
7
 
8
- has_many :condition_taxons,
9
- class_name: "SolidusPromotions::ConditionTaxon",
10
- foreign_key: :condition_id,
11
- dependent: :destroy,
12
- inverse_of: :condition
13
- has_many :taxons, through: :condition_taxons, class_name: "Spree::Taxon"
14
-
15
- def preload_relations
16
- [:taxons]
17
- end
8
+ include TaxonCondition
18
9
 
19
10
  MATCH_POLICIES = %w[any all none].freeze
20
11
 
21
- validates :preferred_match_policy, inclusion: { in: MATCH_POLICIES }
12
+ validates :preferred_match_policy, inclusion: {in: MATCH_POLICIES}
22
13
 
23
14
  preference :match_policy, :string, default: MATCH_POLICIES.first
24
15
 
25
- def order_eligible?(order)
26
- order_taxons = taxons_in_order(order)
27
-
28
- case preferred_match_policy
29
- when "all"
30
- matches_all = taxons.all? do |condition_taxon|
31
- order_taxons.where(id: condition_taxon.self_and_descendants.ids).exists?
32
- end
16
+ def order_eligible?(order, _options = {})
17
+ order_condition = OrderTaxon.new(taxons:, preferred_match_policy:)
33
18
 
34
- unless matches_all
35
- eligibility_errors.add(:base, eligibility_error_message(:missing_taxon), error_code: :missing_taxon)
36
- end
37
- when "any"
38
- unless order_taxons.where(id: condition_taxon_ids_with_children).exists?
39
- eligibility_errors.add(
40
- :base,
41
- eligibility_error_message(:no_matching_taxons),
42
- error_code: :no_matching_taxons
43
- )
44
- end
45
- when "none"
46
- if order_taxons.where(id: condition_taxon_ids_with_children).exists?
47
- eligibility_errors.add(
48
- :base,
49
- eligibility_error_message(:has_excluded_taxon),
50
- error_code: :has_excluded_taxon
51
- )
52
- end
53
- else
54
- raise "unexpected match policy: #{preferred_match_policy.inspect}"
55
- end
19
+ # Hydrate the instance cache with our @taxon_ids_with_children cache
20
+ order_condition.taxons_ids_with_children = taxon_ids_with_children
56
21
 
22
+ order_condition.order_eligible?(order)
23
+ @eligibility_errors = order_condition.eligibility_errors
57
24
  eligibility_errors.empty?
58
25
  end
59
26
 
60
- def line_item_eligible?(line_item)
61
- # The order level eligibility check happens first, and if none of the taxons
62
- # are in the order, then no line items should be available to check.
63
- raise "This should not happen" if preferred_match_policy == "none"
64
-
65
- raise "unexpected match policy: #{preferred_match_policy.inspect}" unless preferred_match_policy.in?(MATCH_POLICIES)
66
-
67
- Spree::Classification.where(
68
- product_id: line_item.variant.product_id,
69
- taxon_id: condition_taxon_ids_with_children
70
- ).exists?
71
- end
72
-
73
- def taxon_ids_string
74
- taxon_ids.join(",")
75
- end
27
+ def line_item_eligible?(line_item, _options = {})
28
+ line_item_match_policy = preferred_match_policy.in?(%w[any all]) ? "include" : "exclude"
29
+ line_item_condition = LineItemTaxon.new(taxons:, preferred_match_policy: line_item_match_policy)
76
30
 
77
- def taxon_ids_string=(taxon_ids)
78
- self.taxon_ids = taxon_ids.to_s.split(",").map(&:strip)
79
- end
31
+ # Hydrate the instance cache with our @taxon_ids_with_children cache\
32
+ line_item_condition.taxons_ids_with_children = taxon_ids_with_children
80
33
 
81
- def updateable?
82
- true
34
+ result = line_item_condition.line_item_eligible?(line_item)
35
+ @eligibility_errors = line_item_condition.eligibility_errors
36
+ result
83
37
  end
84
38
 
85
- private
39
+ def price_eligible?(price, _options = {})
40
+ price_match_policy = preferred_match_policy.in?(%w[any all]) ? "include" : "exclude"
41
+ price_condition = PriceTaxon.new(taxons:, preferred_match_policy: price_match_policy)
86
42
 
87
- # All taxons in an order
88
- def taxons_in_order(order)
89
- Spree::Taxon
90
- .joins(products: { variants_including_master: :line_items })
91
- .where(spree_line_items: { order_id: order.id })
92
- .distinct
93
- end
43
+ # Hydrate the instance cache with our @taxon_ids_with_children cache
44
+ price_condition.taxons_ids_with_children = taxon_ids_with_children
94
45
 
95
- # ids of taxons conditions and taxons conditions children
96
- def condition_taxon_ids_with_children
97
- taxons.flat_map { |taxon| taxon.self_and_descendants.ids }.uniq
46
+ result = price_condition.price_eligible?(price)
47
+ @eligibility_errors = price_condition.eligibility_errors
48
+ result
98
49
  end
99
50
  end
100
51
  end
@@ -3,6 +3,7 @@
3
3
  module SolidusPromotions
4
4
  module Conditions
5
5
  class User < Condition
6
+ # TODO: Remove in Solidus 5
6
7
  include OrderLevelCondition
7
8
 
8
9
  has_many :condition_users,
@@ -16,7 +17,7 @@ module SolidusPromotions
16
17
  [:users]
17
18
  end
18
19
 
19
- def eligible?(order, _options = {})
20
+ def order_eligible?(order, _options = {})
20
21
  users.include?(order.user)
21
22
  end
22
23
 
@@ -3,9 +3,7 @@
3
3
  module SolidusPromotions
4
4
  module Conditions
5
5
  class UserLoggedIn < Condition
6
- include OrderLevelCondition
7
-
8
- def eligible?(order, _options = {})
6
+ def order_eligible?(order, _options = {})
9
7
  if order.user.blank?
10
8
  eligibility_errors.add(:base, eligibility_error_message(:no_user_specified), error_code: :no_user_specified)
11
9
  end
@@ -3,14 +3,12 @@
3
3
  module SolidusPromotions
4
4
  module Conditions
5
5
  class UserRole < Condition
6
- include OrderLevelCondition
7
-
8
6
  preference :role_ids, :array, default: []
9
7
 
10
8
  MATCH_POLICIES = %w[any all].freeze
11
9
  preference :match_policy, default: MATCH_POLICIES.first
12
10
 
13
- def eligible?(order, _options = {})
11
+ def order_eligible?(order, _options = {})
14
12
  return false unless order.user
15
13
 
16
14
  if all_match_policy?
@@ -12,7 +12,7 @@ module SolidusPromotions
12
12
  # @param line_item [LineItem] one of the line_items distributed over
13
13
  # @return [BigDecimal] the weighted adjustment for this line_item
14
14
  def amount(line_item)
15
- distributed_amounts[line_item.id].to_d
15
+ distributed_amounts[line_item].to_d
16
16
  end
17
17
 
18
18
  private
@@ -21,11 +21,7 @@ module SolidusPromotions
21
21
  # @return [Hash<Integer, BigDecimal>] a hash of line item IDs and their
22
22
  # corresponding weighted adjustments
23
23
  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
29
25
  end
30
26
 
31
27
  def elligible_amounts
@@ -3,6 +3,7 @@
3
3
  module SolidusPromotions
4
4
  class EligibilityResults
5
5
  include Enumerable
6
+
6
7
  attr_reader :results, :promotion
7
8
 
8
9
  def initialize(promotion)
@@ -12,6 +12,7 @@ module SolidusPromotions
12
12
  # @attr_reader [BigDecimal] amount the amount of discount applied to the item
13
13
  class ItemDiscount
14
14
  include ActiveModel::Model
15
+
15
16
  attr_accessor :item, :label, :source, :amount
16
17
 
17
18
  def ==(other)