solidus_promotions 4.6.1 → 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 +5 -3
  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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 358bcbcde0ecbd8969cf3a23f9f68e1188b6b54b4a4b9a3c58253def1d3ab29e
4
- data.tar.gz: '01182178d5ec81589d8079c88db347de3dd4e94c53ccfcaf76683de82f0ed8d7'
3
+ metadata.gz: aaae8dc702bc52d537b6e0e3ab7a77ec52091890f443a6a30a63c3de58c6a097
4
+ data.tar.gz: ea14d817d57406914450fe4690b9dbf4db628f11cb456c15b7a92b53fdf293a7
5
5
  SHA512:
6
- metadata.gz: 8d2ef2442707d6193bf3bfc10a58203960a4950c3dc2e02a6006bbbe0ca8bf16b19885023f33a0fbd8caadbf30782e1a5c52d1511fcbda88c72867ebedf5f4a8
7
- data.tar.gz: 240167352cd02bd3b82ce6e24608b1d252cf223d08f92827c0ca7942e622fca392a50bc78afc8f54e8269a39d14f56d350d347ce50d7816096ee14b407ce8e4a
6
+ metadata.gz: 60687b2516232af51dc68e97acbb2ef345eab68265917435991ce48bca89c5cc015c12e96fd2268e65c06ed3b3d38b8b1a2c4a49760fd5d09823886d9866d978
7
+ data.tar.gz: 017a4995a3feafd499cff0ccadd46665c003fdc45396f385e639222da15684fb177ee2f0a7bddedb6663a42a392c5301a386f1667281e21e85c45521e7e27070
data/MIGRATING.md CHANGED
@@ -96,10 +96,14 @@ If you have custom promotion rules or actions, you need to create new conditions
96
96
 
97
97
  In our experience, using the three actions can do almost all the things necessary, since they are customizable using calculators.
98
98
 
99
- Rules share a lot of the previous API. If you make use of `#actionable?`, you might want to migrate your rule to be a line-item level rule:
99
+ Rules share a lot of the previous API. If you make use of `#actionable?`, you might want to migrate your rule to be a combined order and line-item level rule:
100
100
 
101
101
  ```rb
102
102
  class MyRule < Spree::PromotionRule
103
+ def eligible?(order)
104
+ order.total > 100
105
+ end
106
+
103
107
  def actionable?(promotable)
104
108
  promotable.quantity > 3
105
109
  end
@@ -110,10 +114,12 @@ would become:
110
114
 
111
115
  ```rb
112
116
  class MyCondition < SolidusPromotions::Condition
113
- include LineItemLevelCondition
117
+ def order_eligible?(order, _options = {})
118
+ order.total > 100
119
+ end
114
120
 
115
- def eligible?(promotable)
116
- promotable.quantity > 3
121
+ def line_item_eligible?(line_item, _options = {})
122
+ line_item.quantity > 3
117
123
  end
118
124
  end
119
125
  ```
data/README.md CHANGED
@@ -70,6 +70,13 @@ SolidusPromotions.configure do |config|
70
70
  end
71
71
  ```
72
72
 
73
+ ### Coupon Code Normalization
74
+
75
+ Solidus Promotions provides a configurable coupon code normalizer that controls how coupon codes are processed before saving and lookup. By default, codes are case-insensitive (e.g., "SAVE20" and "save20" are treated as the same).
76
+ You can customize this behavior to support case-sensitive codes, remove special characters, apply formatting rules, or implement other normalization strategies based on your business requirements.
77
+
78
+ See the `coupon_code_normalizer_class` configuration option for implementation details.
79
+
73
80
  ## Installation
74
81
 
75
82
  Add solidus_promotions to your Gemfile:
@@ -6,8 +6,6 @@ export default class extends Controller {
6
6
  connect() {
7
7
  this.wrapperClass =
8
8
  this.data.get("wrapperClass") || "promo-condition-option-value";
9
-
10
- this.element.querySelectorAll("." + this.wrapperClass).forEach((element) => this.buildSelects(element))
11
9
  }
12
10
 
13
11
  add_row(event) {
@@ -15,7 +13,6 @@ export default class extends Controller {
15
13
 
16
14
  var content = this.templateTarget.innerHTML;
17
15
  this.linksTarget.insertAdjacentHTML("beforebegin", content);
18
- this.buildSelects(this.linksTarget.previousElementSibling)
19
16
  }
20
17
 
21
18
  propagate_product_id_to_value_input(event) {
@@ -24,7 +21,9 @@ export default class extends Controller {
24
21
  // we first need to greedily match all other square brackets
25
22
  const regEx = /(\[.*\])\[.*?\]$/;
26
23
  let wrapper = event.target.closest("." + this.wrapperClass);
27
- let optionValuesInput = wrapper.querySelector(".option-values-select[type='hidden']");
24
+ let optionValuesInput = wrapper.querySelector("[is=option-value-picker]");
25
+ optionValuesInput.dataset.productId = event.target.value;
26
+ optionValuesInput.value = "";
28
27
  optionValuesInput.name = optionValuesInput.name.replace(
29
28
  regEx,
30
29
  `$1[${event.target.value}]`
@@ -37,26 +36,4 @@ export default class extends Controller {
37
36
  let wrapper = event.target.closest("." + this.wrapperClass);
38
37
  wrapper.remove();
39
38
  }
40
-
41
- // helper functions
42
-
43
- buildSelects(wrapper) {
44
- let productSelect = wrapper.querySelector(".product-select")
45
- let optionValueSelect = wrapper.querySelector(".option-values-select[type='hidden']")
46
- this.buildProductSelect(productSelect)
47
- $(optionValueSelect).optionValueAutocomplete({ productSelect });
48
- }
49
-
50
- buildProductSelect(productSelect) {
51
- var jQueryProductSelect = $(productSelect)
52
- jQueryProductSelect.productAutocomplete({
53
- multiple: false,
54
- })
55
- // capture the jQuery "change" event and re-emit it as DOM event "select2Change"
56
- // so that Stimulus can capture it
57
- jQueryProductSelect.on('change', function () {
58
- let event = new Event('select2Change', { bubbles: true }) // fire a native event
59
- productSelect.dispatchEvent(event);
60
- });
61
- }
62
39
  }
@@ -1,11 +1,9 @@
1
1
  $.fn.optionValueAutocomplete = function (options) {
2
- 'use strict';
2
+ "use strict";
3
3
 
4
4
  // Default options
5
- options = options || {}
6
- var multiple = typeof(options['multiple']) !== 'undefined' ? options['multiple'] : true;
7
- var productSelect = options['productSelect'];
8
-
5
+ options = options || {};
6
+ var multiple = typeof options["multiple"] !== "undefined" ? options["multiple"] : true;
9
7
  function formatOptionValue(option_value) {
10
8
  return Select2.util.escapeMarkup(option_value.name);
11
9
  }
@@ -14,39 +12,58 @@ $.fn.optionValueAutocomplete = function (options) {
14
12
  minimumInputLength: 3,
15
13
  multiple: multiple,
16
14
  initSelection: function (element, callback) {
17
- $.get(Spree.pathFor('api/option_values'), {
18
- ids: element.val().split(','),
19
- token: Spree.api_key
20
- }, function (data) {
21
- callback(multiple ? data : data[0]);
22
- });
15
+ $.get(
16
+ Spree.pathFor("api/option_values"),
17
+ {
18
+ ids: element.val().split(","),
19
+ token: Spree.api_key,
20
+ },
21
+ function (data) {
22
+ callback(multiple ? data : data[0]);
23
+ }
24
+ );
23
25
  },
24
26
  ajax: {
25
- url: Spree.pathFor('api/option_values'),
26
- datatype: 'json',
27
+ url: Spree.pathFor("api/option_values"),
28
+ datatype: "json",
27
29
  data: function (term, page) {
28
- var productId = typeof(productSelect) !== 'undefined' ? $(productSelect).select2('val') : null;
30
+ var productId = this[0].dataset.productId;
29
31
  return {
30
32
  q: {
31
33
  name_cont: term,
32
- variants_product_id_eq: productId
34
+ variants_product_id_eq: productId,
33
35
  },
34
- token: Spree.api_key
36
+ token: Spree.api_key,
35
37
  };
36
38
  },
37
39
  results: function (data, page) {
38
40
  return { results: data };
39
- }
41
+ },
40
42
  },
41
43
  formatResult: formatOptionValue,
42
- formatSelection: formatOptionValue
44
+ formatSelection: formatOptionValue,
43
45
  });
44
46
  };
45
47
 
46
48
  class OptionValuePicker extends HTMLInputElement {
47
49
  connectedCallback() {
48
50
  $(this).optionValueAutocomplete();
51
+
52
+ this.observer = new MutationObserver((muts) => {
53
+ for (const m of muts) {
54
+ if (m.attributeName.startsWith("data-product-id")) {
55
+ this.restart();
56
+ }
57
+ }
58
+ });
59
+
60
+ this.observer.observe(this, { attributes: true });
61
+ }
62
+
63
+ restart() {
64
+ $(this).select2("destroy");
65
+ $(this).optionValueAutocomplete();
49
66
  }
50
67
  }
51
68
 
52
- customElements.define('option-value-picker', OptionValuePicker, { extends: 'input' });
69
+ customElements.define("option-value-picker", OptionValuePicker, { extends: "input" });
@@ -1,6 +1,11 @@
1
1
  class ProductPicker extends HTMLInputElement {
2
2
  connectedCallback() {
3
- $(this).productAutocomplete();
3
+ const multiple = this.dataset.multiple !== "false";
4
+ $(this).productAutocomplete({ multiple });
5
+ $(this).on("change", (_) => {
6
+ let event = new Event('select2Change', { bubbles: true }) // fire a native event
7
+ this.dispatchEvent(event)
8
+ })
4
9
  }
5
10
  }
6
11
 
@@ -14,7 +14,7 @@ module SolidusPromotions
14
14
  .promotion_code_batch_finished(promotion_code_batch)
15
15
  .deliver_now
16
16
  end
17
- rescue StandardError => e
17
+ rescue => e
18
18
  if promotion_code_batch.email?
19
19
  SolidusPromotions.config.promotion_code_batch_mailer_class
20
20
  .promotion_code_batch_errored(promotion_code_batch)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ module AdjustmentDiscounts
5
+ private
6
+
7
+ # Returns adjustments from specified promotion lanes.
8
+ #
9
+ # @param lanes [Array<String>] the promotion lanes to filter by
10
+ # @return [Array<Spree::Adjustment>] promotions adjustments from the
11
+ # specified lanes that are not marked for destruction
12
+ def discounts_by_lanes(lanes)
13
+ adjustments.select do |adjustment|
14
+ !adjustment.marked_for_destruction? &&
15
+ adjustment.source_type == "SolidusPromotions::Benefit" &&
16
+ adjustment.source.promotion.lane.in?(lanes)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -3,6 +3,10 @@
3
3
  module SolidusPromotions
4
4
  module Benefits
5
5
  module LineItemBenefit
6
+ def self.included(_base)
7
+ Spree.deprecator.warn("Including #{name} is deprecated.")
8
+ end
9
+
6
10
  def can_discount?(object)
7
11
  object.is_a? Spree::LineItem
8
12
  end
@@ -10,6 +14,7 @@ module SolidusPromotions
10
14
  def level
11
15
  :line_item
12
16
  end
17
+ deprecate :level, deprecator: Spree.deprecator
13
18
  end
14
19
  end
15
20
  end
@@ -3,6 +3,10 @@
3
3
  module SolidusPromotions
4
4
  module Benefits
5
5
  module OrderBenefit
6
+ def self.included(_base)
7
+ Spree.deprecator.warn("Including #{name} is deprecated.")
8
+ end
9
+
6
10
  def can_discount?(_)
7
11
  false
8
12
  end
@@ -10,6 +14,7 @@ module SolidusPromotions
10
14
  def level
11
15
  :order
12
16
  end
17
+ deprecate :level, deprecator: Spree.deprecator
13
18
  end
14
19
  end
15
20
  end
@@ -3,6 +3,10 @@
3
3
  module SolidusPromotions
4
4
  module Benefits
5
5
  module ShipmentBenefit
6
+ def self.included(_base)
7
+ Spree.deprecator.warn("Including #{name} is deprecated.")
8
+ end
9
+
6
10
  def can_discount?(object)
7
11
  object.is_a?(Spree::Shipment) || object.is_a?(Spree::ShippingRate)
8
12
  end
@@ -10,6 +14,7 @@ module SolidusPromotions
10
14
  def level
11
15
  :shipment
12
16
  end
17
+ deprecate :level, deprecator: Spree.deprecator
13
18
  end
14
19
  end
15
20
  end
@@ -6,6 +6,13 @@ module SolidusPromotions
6
6
  def description
7
7
  self.class.human_attribute_name(:description)
8
8
  end
9
+
10
+ private
11
+
12
+ def round_to_currency(number, currency)
13
+ currency_exponent = ::Money::Currency.find(currency).exponent
14
+ number.round(currency_exponent)
15
+ end
9
16
  end
10
17
  end
11
18
  end
@@ -8,16 +8,28 @@ module SolidusPromotions
8
8
  end
9
9
 
10
10
  def applicable?(promotable)
11
- promotable.is_a?(Spree::Order) || preferred_line_item_applicable && promotable.is_a?(Spree::LineItem)
12
- end
13
-
14
- def eligible?(promotable)
15
- send(:"#{promotable.class.name.demodulize.underscore}_eligible?", 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
26
+ promotable.is_a?(Spree::LineItem) ? preferred_line_item_applicable && super : super
16
27
  end
17
28
 
18
29
  def level
19
30
  :order
20
31
  end
32
+ deprecate :level, deprecator: Spree.deprecator
21
33
  end
22
34
  end
23
35
  end
@@ -3,13 +3,26 @@
3
3
  module SolidusPromotions
4
4
  module Conditions
5
5
  module LineItemLevelCondition
6
- def applicable?(promotable)
7
- promotable.is_a?(Spree::LineItem)
6
+ def self.included(base)
7
+ def base.method_added(method)
8
+ if method == :eligible?
9
+ Spree.deprecator.warn <<~MSG
10
+ Defining `eligible?` on a promotion along with including the `LineItemLevelCondition` module is deprecated.
11
+ Rename `eligible?` to `line_item_eligible?` and stop including the `LineItemLevelCondition` module.
12
+ MSG
13
+ define_method(:applicable?) do |promotable|
14
+ promotable.is_a?(Spree::LineItem)
15
+ end
16
+ end
17
+
18
+ super
19
+ end
8
20
  end
9
21
 
10
22
  def level
11
23
  :line_item
12
24
  end
25
+ deprecate :level, deprecator: Spree.deprecator
13
26
  end
14
27
  end
15
28
  end
@@ -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
@@ -3,13 +3,26 @@
3
3
  module SolidusPromotions
4
4
  module Conditions
5
5
  module OrderLevelCondition
6
- def applicable?(promotable)
7
- promotable.is_a?(Spree::Order)
6
+ def self.included(base)
7
+ def base.method_added(method)
8
+ if method == :eligible?
9
+ Spree.deprecator.warn <<~MSG
10
+ Defining `eligible?` on a promotion along with including the `OrderLevelCondition` module is deprecated.
11
+ Rename `eligible?` to `order_eligible?` and stop including the `OrderLevelCondition` module.
12
+ MSG
13
+ define_method(:applicable?) do |promotable|
14
+ promotable.is_a?(Spree::Order)
15
+ end
16
+ end
17
+
18
+ super
19
+ end
8
20
  end
9
21
 
10
22
  def level
11
23
  :order
12
24
  end
25
+ deprecate :level, deprecator: Spree.deprecator
13
26
  end
14
27
  end
15
28
  end
@@ -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
@@ -3,13 +3,26 @@
3
3
  module SolidusPromotions
4
4
  module Conditions
5
5
  module ShipmentLevelCondition
6
- def applicable?(promotable)
7
- promotable.is_a?(Spree::Shipment)
6
+ def self.included(base)
7
+ def base.method_added(method)
8
+ if method == :eligible?
9
+ Spree.deprecator.warn <<~MSG
10
+ Defining `eligible?` on a promotion along with including the `ShipmentLevelCondition` module is deprecated.
11
+ Rename `eligible?` to `shipment_eligible?` and stop including the `ShipmentLevelCondition` module.
12
+ MSG
13
+ define_method(:applicable?) do |promotable|
14
+ promotable.is_a?(Spree::Shipment)
15
+ end
16
+ end
17
+
18
+ super
19
+ end
8
20
  end
9
21
 
10
22
  def level
11
23
  :shipment
12
24
  end
25
+ deprecate :level, deprecator: Spree.deprecator
13
26
  end
14
27
  end
15
28
  end
@@ -0,0 +1,77 @@
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
+ def taxons_ids_with_children=(args)
29
+ @taxon_ids_with_children = args
30
+ end
31
+
32
+ private
33
+
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) }
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusPromotions
4
+ # Normalizes coupon codes before saving or looking up promotions.
5
+ #
6
+ # By default, this class strips whitespace and downcases the code
7
+ # to ensure case-insensitive behavior. You can override this class
8
+ # or provide a custom normalizer class to change behavior (e.g.,
9
+ # case-sensitive codes) via:
10
+ #
11
+ # SolidusPromotions.configure do |config|
12
+ # config.coupon_code_normalizer_class = YourCustomNormalizer
13
+ # end
14
+ #
15
+ # @example Default usage
16
+ # CouponCodeNormalizer.call(" SAVE20 ") # => "save20"
17
+ #
18
+ # @example Custom case-sensitive usage
19
+ # class CaseSensitiveNormalizer
20
+ # def self.call(value)
21
+ # value&.strip
22
+ # end
23
+ # end
24
+ #
25
+ # SolidusPromotions.configure do |config|
26
+ # config.coupon_code_normalizer_class = CaseSensitiveNormalizer
27
+ # end
28
+ class CouponCodeNormalizer
29
+ # Normalizes the given coupon code.
30
+ #
31
+ # @param value [String, nil] the coupon code to normalize
32
+ # @return [String, nil] the normalized coupon code
33
+ def self.call(value)
34
+ value&.strip&.downcase
35
+ end
36
+ end
37
+ end
@@ -2,20 +2,19 @@
2
2
 
3
3
  module SolidusPromotions
4
4
  module DiscountableAmount
5
- def discountable_amount
6
- amount + current_discounts.sum(&:amount)
7
- end
8
-
9
5
  def current_discounts
10
6
  @current_discounts ||= []
11
7
  end
8
+ deprecate current_discounts: :previous_lane_discounts, deprecator: Spree.deprecator
12
9
 
13
10
  def current_discounts=(args)
14
11
  @current_discounts = args
15
12
  end
13
+ deprecate :current_discounts=, deprecator: Spree.deprecator
16
14
 
17
15
  def reset_current_discounts
18
16
  @current_discounts = []
19
17
  end
18
+ deprecate :reset_current_discounts=, deprecator: Spree.deprecator
20
19
  end
21
20
  end