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
@@ -14,15 +14,14 @@ module SolidusPromotions
14
14
  def call
15
15
  return order if order.shipped?
16
16
 
17
- SolidusPromotions::Promotion.ordered_lanes.each_key do |lane|
18
- lane_promotions = promotions.select { |promotion| promotion.lane == lane }
19
- lane_benefits = eligible_benefits_for_promotable(lane_promotions.flat_map(&:benefits), order)
20
- perform_order_benefits(lane_benefits, lane) unless dry_run
21
- line_item_discounts = adjust_line_items(lane_benefits)
22
- shipment_discounts = adjust_shipments(lane_benefits)
23
- shipping_rate_discounts = adjust_shipping_rates(lane_benefits)
24
- (line_item_discounts + shipment_discounts + shipping_rate_discounts).each do |item, chosen_discounts|
25
- item.current_discounts.concat(chosen_discounts)
17
+ SolidusPromotions::Promotion.ordered_lanes.each do |lane|
18
+ SolidusPromotions::PromotionLane.set(current_lane: lane) do
19
+ lane_promotions = promotions.select { |promotion| promotion.lane == lane }
20
+ lane_benefits = eligible_benefits_for_promotable(lane_promotions.flat_map(&:benefits), order)
21
+ perform_order_benefits(lane_benefits, lane) unless dry_run
22
+ adjust_line_items(lane_benefits)
23
+ adjust_shipments(lane_benefits)
24
+ adjust_shipping_rates(lane_benefits)
26
25
  end
27
26
  end
28
27
 
@@ -32,45 +31,42 @@ module SolidusPromotions
32
31
  private
33
32
 
34
33
  def perform_order_benefits(lane_benefits, lane)
35
- lane_benefits.select { |benefit| benefit.level == :order }.each do |benefit|
36
- benefit.perform(order)
34
+ lane_benefits.filter_map do |benefit|
35
+ benefit.respond_to?(:perform) && benefit.perform(order)
37
36
  end
38
37
 
39
- automated_line_items = order.line_items.select(&:managed_by_order_benefit)
40
- return if automated_line_items.empty?
41
-
42
- ineligible_line_items = automated_line_items.select do |line_item|
43
- line_item.managed_by_order_benefit.promotion.lane == lane && !line_item.managed_by_order_benefit.in?(lane_benefits)
44
- end
45
-
46
- ineligible_line_items.each do |line_item|
47
- line_item.managed_by_order_benefit.remove_from(order)
38
+ order.line_items.filter_map do |line_item|
39
+ line_item.managed_by_order_benefit &&
40
+ line_item.managed_by_order_benefit.promotion.lane == lane &&
41
+ !line_item.managed_by_order_benefit.in?(lane_benefits) &&
42
+ line_item.managed_by_order_benefit.remove_from(order)
48
43
  end
49
44
  end
50
45
 
51
46
  def adjust_line_items(benefits)
52
- order.discountable_line_items.select do |line_item|
53
- line_item.variant.product.promotionable?
54
- end.map do |line_item|
47
+ order.discountable_line_items.filter_map do |line_item|
48
+ next unless line_item.variant.product.promotionable?
49
+
55
50
  discounts = generate_discounts(benefits, line_item)
56
- chosen_item_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
57
- [line_item, chosen_item_discounts]
51
+ chosen_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
52
+ (line_item.current_lane_discounts - chosen_discounts).each(&:mark_for_destruction)
58
53
  end
59
54
  end
60
55
 
61
56
  def adjust_shipments(benefits)
62
57
  order.shipments.map do |shipment|
63
58
  discounts = generate_discounts(benefits, shipment)
64
- chosen_item_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
65
- [shipment, chosen_item_discounts]
59
+ chosen_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
60
+ (shipment.current_lane_discounts - chosen_discounts).each(&:mark_for_destruction)
66
61
  end
67
62
  end
68
63
 
69
64
  def adjust_shipping_rates(benefits)
70
- order.shipments.flat_map(&:shipping_rates).select(&:cost).map do |rate|
65
+ order.shipments.flat_map(&:shipping_rates).filter_map do |rate|
66
+ next unless rate.cost
71
67
  discounts = generate_discounts(benefits, rate)
72
- chosen_item_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
73
- [rate, chosen_item_discounts]
68
+ chosen_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
69
+ (rate.current_lane_discounts - chosen_discounts).each(&:mark_for_destruction)
74
70
  end
75
71
  end
76
72
 
@@ -82,11 +78,9 @@ module SolidusPromotions
82
78
 
83
79
  def generate_discounts(possible_benefits, item)
84
80
  eligible_benefits = eligible_benefits_for_promotable(possible_benefits, item)
85
- eligible_benefits.select do |benefit|
86
- benefit.can_discount?(item)
87
- end.map do |benefit|
88
- benefit.discount(item)
89
- end.compact
81
+ eligible_benefits.filter_map do |benefit|
82
+ benefit.can_discount?(item) && benefit.discount(item)
83
+ end
90
84
  end
91
85
  end
92
86
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ class OrderAdjuster
5
+ module RecalculatePromoTotals
6
+ extend self
7
+
8
+ def call(order)
9
+ order.line_items.each do |line_item|
10
+ line_item.adjustments.select { _1.amount.zero? }.each(&:mark_for_destruction)
11
+
12
+ line_item.promo_total = calculate_promo_total_for_adjustable(line_item)
13
+ end
14
+
15
+ order.shipments.each do |shipment|
16
+ shipment.adjustments.select { _1.amount.zero? }.each(&:mark_for_destruction)
17
+ shipment.shipping_rates.each do |shipping_rate|
18
+ shipping_rate.discounts.select { _1.amount.zero? }.each(&:mark_for_destruction)
19
+ end
20
+
21
+ shipment.promo_total = calculate_promo_total_for_adjustable(shipment)
22
+ end
23
+
24
+ line_items = order.line_items.reject(&:marked_for_destruction?)
25
+ order.item_total = line_items.sum(&:amount)
26
+ order.item_count = line_items.sum(&:quantity)
27
+
28
+ order.promo_total = (line_items + order.shipments.reject(&:marked_for_destruction?)).sum(&:promo_total)
29
+
30
+ order.adjustment_total = order.promo_total
31
+ order
32
+ end
33
+
34
+ private
35
+
36
+ def calculate_promo_total_for_adjustable(adjustable)
37
+ adjustable
38
+ .adjustments
39
+ .select(&:promotion?)
40
+ .reject(&:marked_for_destruction?)
41
+ .sum(&:amount)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ class OrderAdjuster
5
+ module SetDiscountsToZero
6
+ extend self
7
+
8
+ def call(order)
9
+ order.line_items.each do |line_item|
10
+ reset_item_adjustments(line_item)
11
+ end
12
+
13
+ order.shipments.each do |shipment|
14
+ reset_item_adjustments(shipment)
15
+ shipment.shipping_rates.each do |shipping_rate|
16
+ reset_shipping_rate_discounts(shipping_rate)
17
+ end
18
+ end
19
+ order
20
+ end
21
+
22
+ private
23
+
24
+ def reset_item_adjustments(item)
25
+ item.adjustments.select(&:promotion?).each { |adjustment| adjustment.amount = 0 }
26
+ end
27
+
28
+ def reset_shipping_rate_discounts(rate)
29
+ rate.discounts.each { |discount| discount.amount = 0 }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -13,24 +13,14 @@ module SolidusPromotions
13
13
  ).call
14
14
  end
15
15
 
16
- def call
17
- order.reset_current_discounts
18
-
16
+ def call(persist: true) # rubocop:disable Lint/UnusedMethodArgument
19
17
  return order unless SolidusPromotions::Promotion.order_activatable?(order)
20
18
 
21
- discounted_order = DiscountOrder.new(order, promotions, dry_run: dry_run).call
22
-
23
- PersistDiscountedOrder.new(discounted_order).call unless dry_run
19
+ SetDiscountsToZero.call(order)
24
20
 
25
- order.reset_current_discounts
21
+ DiscountOrder.new(order, promotions, dry_run: dry_run).call
26
22
 
27
- unless dry_run
28
- # Since automations might have added a line item, we need to recalculate item total and item count here.
29
- order.item_total = order.line_items.sum(&:amount)
30
- order.item_count = order.line_items.sum(&:quantity)
31
- order.promo_total = (order.line_items + order.shipments).sum(&:promo_total)
32
- end
33
- order
23
+ RecalculatePromoTotals.call(order)
34
24
  end
35
25
  end
36
26
  end
@@ -11,6 +11,7 @@ module SolidusPromotions
11
11
  belongs_to :promotion_code, class_name: "SolidusPromotions::PromotionCode", optional: true
12
12
 
13
13
  validates :promotion_code, presence: true, if: :require_promotion_code?
14
+ validates :promotion_id, uniqueness: {scope: :order_id}
14
15
 
15
16
  self.allowed_ransackable_associations = %w[promotion_code]
16
17
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ class ProductAdvertiser
5
+ attr_reader :order, :product, :promotions, :quantity
6
+
7
+ def initialize(product:, order:, quantity: 1)
8
+ @product = product
9
+ @order = order
10
+ @quantity = quantity
11
+ @promotions = SolidusPromotions::LoadPromotions.new(order:).call
12
+ end
13
+
14
+ def call
15
+ return unless product.promotionable?
16
+
17
+ SolidusPromotions::Promotion.ordered_lanes.each do |lane|
18
+ SolidusPromotions::PromotionLane.set(current_lane: lane) do
19
+ lane_promotions = promotions.select { |promotion| promotion.lane == lane }
20
+ lane_benefits = eligible_benefits_for_promotable(lane_promotions.flat_map(&:benefits), order)
21
+
22
+ if product.has_variants?
23
+ product.variants.each { |variant| discount_variant(variant, lane_benefits) }
24
+ else
25
+ discount_variant(product.master, lane_benefits)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def discount_variant(variant, benefits)
34
+ variant.prices.each do |price|
35
+ next if price.discarded?
36
+ discounts = generate_discounts(benefits, price)
37
+ chosen_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
38
+ price.discounts.concat(chosen_discounts)
39
+ end
40
+ end
41
+
42
+ def eligible_benefits_for_promotable(possible_benefits, promotable)
43
+ possible_benefits.select do |candidate|
44
+ candidate.eligible_by_applicable_conditions?(promotable)
45
+ end
46
+ end
47
+
48
+ def generate_discounts(possible_benefits, item)
49
+ eligible_benefits = eligible_benefits_for_promotable(possible_benefits, item)
50
+ eligible_benefits.filter_map do |benefit|
51
+ next unless benefit.can_discount?(item)
52
+
53
+ benefit.discount(item, order:, quantity:)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -19,10 +19,10 @@ module SolidusPromotions
19
19
  has_many :order_promotions, class_name: "SolidusPromotions::OrderPromotion", dependent: :destroy
20
20
 
21
21
  validates :name, :customer_label, presence: true
22
- validates :path, uniqueness: { allow_blank: true, case_sensitive: true }
23
- validates :usage_limit, numericality: { greater_than: 0, allow_nil: true }
24
- validates :per_code_usage_limit, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
25
- validates :description, length: { maximum: 255 }
22
+ validates :path, uniqueness: {allow_blank: true, case_sensitive: true}
23
+ validates :usage_limit, numericality: {greater_than: 0, allow_nil: true}
24
+ validates :per_code_usage_limit, numericality: {greater_than_or_equal_to: 0, allow_nil: true}
25
+ validates :description, length: {maximum: 255}
26
26
  validate :apply_automatically_disallowed_with_paths
27
27
  validate :apply_automatically_disallowed_with_promotion_codes
28
28
 
@@ -46,7 +46,9 @@ module SolidusPromotions
46
46
 
47
47
  def self.with_coupon_code(val)
48
48
  joins(:codes).where(
49
- SolidusPromotions::PromotionCode.arel_table[:value].eq(val.downcase)
49
+ SolidusPromotions::PromotionCode.arel_table[:value].eq(
50
+ SolidusPromotions.config.coupon_code_normalizer_class.call(val)
51
+ )
50
52
  ).first
51
53
  end
52
54
 
@@ -55,13 +57,13 @@ module SolidusPromotions
55
57
  end
56
58
 
57
59
  def self.lane_options
58
- ordered_lanes.map do |lane_name, _index|
59
- [human_enum_name(:lane, lane_name), lane_name]
60
+ ordered_lanes.map do |lane|
61
+ [human_enum_name(:lane, lane), lane]
60
62
  end
61
63
  end
62
64
 
63
65
  def self.ordered_lanes
64
- lanes.sort_by(&:last).to_h
66
+ lanes.keys.sort_by { |lane| lanes[lane] }
65
67
  end
66
68
 
67
69
  def self.order_activatable?(order)
@@ -96,7 +98,7 @@ module SolidusPromotions
96
98
  discounted_orders
97
99
  .complete
98
100
  .where.not(id: [excluded_orders.map(&:id)])
99
- .where.not(spree_orders: { state: :canceled })
101
+ .where.not(spree_orders: {state: :canceled})
100
102
  .count
101
103
  end
102
104
 
@@ -105,7 +107,7 @@ module SolidusPromotions
105
107
  .complete
106
108
  .where.not(id: excluded_orders.map(&:id))
107
109
  .where(user: user)
108
- .where.not(spree_orders: { state: :canceled })
110
+ .where.not(spree_orders: {state: :canceled})
109
111
  .exists?
110
112
  end
111
113
 
@@ -22,7 +22,7 @@ module SolidusPromotions
22
22
  def build_promotion_codes
23
23
  generate_random_codes
24
24
  promotion_code_batch.update!(state: "completed")
25
- rescue StandardError => e
25
+ rescue => e
26
26
  promotion_code_batch.update!(
27
27
  error: e.inspect,
28
28
  state: "failed"
@@ -9,7 +9,7 @@ module SolidusPromotions
9
9
 
10
10
  before_validation :normalize_code
11
11
 
12
- validates :value, presence: true, uniqueness: { allow_blank: true, case_sensitive: true }
12
+ validates :value, presence: true, uniqueness: {allow_blank: true, case_sensitive: true}
13
13
  validate :promotion_not_apply_automatically, on: :create
14
14
 
15
15
  self.allowed_ransackable_attributes = ["value"]
@@ -32,9 +32,9 @@ module SolidusPromotions
32
32
  promotion
33
33
  .discounted_orders
34
34
  .complete
35
- .where.not(spree_orders: { state: :canceled })
35
+ .where.not(spree_orders: {state: :canceled})
36
36
  .joins(:solidus_order_promotions)
37
- .where(SolidusPromotions::OrderPromotion.table_name => { promotion_code_id: id })
37
+ .where(SolidusPromotions::OrderPromotion.table_name => {promotion_code_id: id})
38
38
  .where.not(id: excluded_orders.map(&:id))
39
39
  .count
40
40
  end
@@ -50,7 +50,7 @@ module SolidusPromotions
50
50
  private
51
51
 
52
52
  def normalize_code
53
- self.value = value.downcase.strip
53
+ self.value = SolidusPromotions.config.coupon_code_normalizer_class.call(value)
54
54
  end
55
55
  end
56
56
  end
@@ -8,7 +8,7 @@ module SolidusPromotions
8
8
  belongs_to :promotion
9
9
  has_many :promotion_codes, dependent: :destroy
10
10
 
11
- validates :number_of_codes, numericality: { greater_than: 0 }
11
+ validates :number_of_codes, numericality: {greater_than: 0}
12
12
  validates :base_code, :number_of_codes, presence: true
13
13
 
14
14
  def finished?
@@ -9,7 +9,7 @@ module SolidusPromotions
9
9
  def initialize(order)
10
10
  @order = order
11
11
  @errors = []
12
- @coupon_code = order&.coupon_code&.downcase
12
+ @coupon_code = SolidusPromotions.config.coupon_code_normalizer_class.call(order&.coupon_code)
13
13
  end
14
14
 
15
15
  def apply
@@ -7,7 +7,7 @@ module SolidusPromotions
7
7
 
8
8
  def initialize(order, path)
9
9
  @order = order
10
- @path = path.delete_prefix('/')
10
+ @path = path.delete_prefix("/")
11
11
  end
12
12
 
13
13
  def activate
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ # PromotionLane is a thread-safe current attributes class that manages the current promotion lane context.
5
+ #
6
+ # This class extends ActiveSupport::CurrentAttributes to provide thread-local storage for the current
7
+ # promotion lane. It allows setting and retrieving the current lane, as well as getting all lanes
8
+ # that come before the current one.
9
+ #
10
+ # @example Setting and retrieving the current lane
11
+ # PromotionLane.current_lane = :pre
12
+ # PromotionLane.current_lane # => "pre"
13
+ #
14
+ # @example Getting lanes before the current one
15
+ # PromotionLane.current_lane = :post
16
+ # PromotionLane.previous_lanes # => ["pre"]
17
+ #
18
+ # @see ActiveSupport::CurrentAttributes
19
+ class PromotionLane < ActiveSupport::CurrentAttributes
20
+ attribute :current_lane
21
+
22
+ def current_lane=(arg)
23
+ if arg.present?
24
+ super(arg.to_s)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ # Retrieves the lanes that occur before the current lane in the promotion flow.
31
+ #
32
+ # Delegates to `before(current_lane)` to compute the preceding lanes.
33
+ #
34
+ # Special considerations:
35
+ # - If `current_lane` is `nil`, all lanes are returned.
36
+ #
37
+ # @return [Array<String>] the set of lanes preceding the current lane; all lanes if no current lane is set
38
+ def previous_lanes
39
+ before(current_lane)
40
+ end
41
+
42
+ private
43
+
44
+ def before(lane)
45
+ Promotion.ordered_lanes.split(lane).first
46
+ end
47
+ end
48
+ end
@@ -5,7 +5,10 @@ module SolidusPromotions
5
5
  belongs_to :shipping_rate, inverse_of: :discounts, class_name: "Spree::ShippingRate"
6
6
  belongs_to :benefit, inverse_of: :shipping_rate_discounts
7
7
 
8
+ alias_method :source, :benefit
9
+
8
10
  extend Spree::DisplayMoney
11
+
9
12
  money_methods :amount
10
13
  end
11
14
  end
@@ -22,6 +22,8 @@ module SolidusPromotions
22
22
  end
23
23
 
24
24
  Spree::LineItem.prepend self
25
+ Spree::LineItem.prepend SolidusPromotions::AdjustmentDiscounts
26
+ Spree::LineItem.prepend SolidusPromotions::DiscountedAmount
25
27
  Spree::LineItem.prepend SolidusPromotions::DiscountableAmount
26
28
  end
27
29
  end
@@ -36,6 +36,14 @@ module SolidusPromotions
36
36
  !line_item.managed_by_order_benefit
37
37
  end
38
38
 
39
+ def coupon_code=(code)
40
+ @coupon_code = begin
41
+ SolidusPromotions.config.coupon_code_normalizer_class.call(code)
42
+ rescue
43
+ nil
44
+ end
45
+ end
46
+
39
47
  Spree::Order.singleton_class.prepend self::ClassMethods
40
48
  Spree::Order.prepend self
41
49
  end
@@ -10,6 +10,8 @@ module SolidusPromotions
10
10
  end
11
11
  super
12
12
  end
13
- Spree::Config.order_recalculator_class.prepend self
13
+
14
+ Spree::OrderUpdater.prepend self
15
+ Spree::InMemoryOrderUpdater.prepend self
14
16
  end
15
17
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module PricePatch
5
+ def self.prepended(base)
6
+ base.money_methods :discounted_amount
7
+ end
8
+
9
+ def discounts
10
+ @discounts ||= []
11
+ end
12
+
13
+ attr_writer :discounts
14
+
15
+ private
16
+
17
+ # Returns discounts from specified promotion lanes.
18
+ #
19
+ # @param lanes [Array] An array of lanes to filter discounts by.
20
+ # @return [Array<SolidusPromotions::ShippingRateDiscount] An array of discounts from the
21
+ # specified lans that are not marked for destruction.
22
+ def discounts_by_lanes(lanes)
23
+ discounts.select do |discount|
24
+ discount.source.promotion.lane.in?(lanes)
25
+ end
26
+ end
27
+
28
+ Spree::Price.prepend SolidusPromotions::DiscountedAmount
29
+ Spree::Price.prepend self
30
+ end
31
+ end
@@ -10,5 +10,7 @@ module SolidusPromotions
10
10
  end
11
11
 
12
12
  Spree::Shipment.prepend self
13
+ Spree::Shipment.prepend SolidusPromotions::AdjustmentDiscounts
14
+ Spree::Shipment.prepend SolidusPromotions::DiscountedAmount
13
15
  end
14
16
  end
@@ -23,7 +23,22 @@ module SolidusPromotions
23
23
  discounts.sum(&:amount)
24
24
  end
25
25
 
26
+ private
27
+
28
+ # Returns discounts from specified promotion lanes.
29
+ #
30
+ # @param lanes [Array] An array of lanes to filter discounts by.
31
+ # @return [Array<SolidusPromotions::ShippingRateDiscount] An array of discounts from the
32
+ # specified lans that are not marked for destruction.
33
+ def discounts_by_lanes(lanes)
34
+ discounts.select do |discount|
35
+ !discount.marked_for_destruction? &&
36
+ discount.benefit.promotion.lane.in?(lanes)
37
+ end
38
+ end
39
+
26
40
  Spree::ShippingRate.prepend SolidusPromotions::DiscountableAmount
41
+ Spree::ShippingRate.prepend SolidusPromotions::DiscountedAmount
27
42
  Spree::ShippingRate.prepend self
28
43
  end
29
44
  end