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.
- checksums.yaml +4 -4
- data/MIGRATING.md +10 -4
- data/README.md +7 -0
- data/app/javascript/backend/solidus_promotions/controllers/product_option_values_controller.js +3 -26
- data/app/javascript/backend/solidus_promotions/web_components/option_value_picker.js +36 -19
- data/app/javascript/backend/solidus_promotions/web_components/product_picker.js +6 -1
- data/app/jobs/solidus_promotions/promotion_code_batch_job.rb +1 -1
- data/app/models/concerns/solidus_promotions/adjustment_discounts.rb +20 -0
- data/app/models/concerns/solidus_promotions/benefits/line_item_benefit.rb +5 -0
- data/app/models/concerns/solidus_promotions/benefits/order_benefit.rb +5 -0
- data/app/models/concerns/solidus_promotions/benefits/shipment_benefit.rb +5 -0
- data/app/models/concerns/solidus_promotions/calculators/promotion_calculator.rb +7 -0
- data/app/models/concerns/solidus_promotions/conditions/line_item_applicable_order_level_condition.rb +17 -5
- data/app/models/concerns/solidus_promotions/conditions/line_item_level_condition.rb +15 -2
- data/app/models/concerns/solidus_promotions/conditions/option_value_condition.rb +21 -0
- data/app/models/concerns/solidus_promotions/conditions/order_level_condition.rb +15 -2
- data/app/models/concerns/solidus_promotions/conditions/product_condition.rb +28 -0
- data/app/models/concerns/solidus_promotions/conditions/shipment_level_condition.rb +15 -2
- data/app/models/concerns/solidus_promotions/conditions/taxon_condition.rb +77 -0
- data/app/models/concerns/solidus_promotions/coupon_code_normalizer.rb +37 -0
- data/app/models/concerns/solidus_promotions/discountable_amount.rb +3 -4
- data/app/models/concerns/solidus_promotions/discounted_amount.rb +54 -0
- data/app/models/solidus_promotions/benefit.rb +257 -36
- data/app/models/solidus_promotions/benefits/adjust_line_item.rb +28 -3
- data/app/models/solidus_promotions/benefits/adjust_line_item_quantity_groups.rb +1 -0
- data/app/models/solidus_promotions/benefits/adjust_shipment.rb +45 -3
- data/app/models/solidus_promotions/benefits/advertise_price.rb +28 -0
- data/app/models/solidus_promotions/benefits/create_discounted_item.rb +30 -7
- data/app/models/solidus_promotions/calculators/distributed_amount.rb +34 -8
- data/app/models/solidus_promotions/calculators/flat_rate.rb +52 -6
- data/app/models/solidus_promotions/calculators/flexi_rate.rb +69 -6
- data/app/models/solidus_promotions/calculators/percent.rb +40 -4
- data/app/models/solidus_promotions/calculators/percent_with_cap.rb +44 -3
- data/app/models/solidus_promotions/calculators/tiered_flat_rate.rb +81 -19
- data/app/models/solidus_promotions/calculators/tiered_percent.rb +96 -25
- data/app/models/solidus_promotions/calculators/tiered_percent_on_eligible_item_quantity.rb +101 -16
- data/app/models/solidus_promotions/condition.rb +186 -7
- data/app/models/solidus_promotions/conditions/first_order.rb +5 -3
- data/app/models/solidus_promotions/conditions/first_repeat_purchase_since.rb +3 -2
- data/app/models/solidus_promotions/conditions/item_total.rb +2 -1
- data/app/models/solidus_promotions/conditions/line_item_option_value.rb +4 -12
- data/app/models/solidus_promotions/conditions/line_item_product.rb +4 -22
- data/app/models/solidus_promotions/conditions/line_item_taxon.rb +7 -38
- data/app/models/solidus_promotions/conditions/minimum_quantity.rb +3 -2
- data/app/models/solidus_promotions/conditions/nth_order.rb +3 -2
- data/app/models/solidus_promotions/conditions/one_use_per_user.rb +2 -1
- data/app/models/solidus_promotions/conditions/option_value.rb +6 -11
- data/app/models/solidus_promotions/conditions/order_option_value.rb +19 -0
- data/app/models/solidus_promotions/conditions/order_product.rb +62 -0
- data/app/models/solidus_promotions/conditions/order_taxon.rb +60 -0
- data/app/models/solidus_promotions/conditions/price_option_value.rb +26 -0
- data/app/models/solidus_promotions/conditions/price_product.rb +36 -0
- data/app/models/solidus_promotions/conditions/price_taxon.rb +28 -0
- data/app/models/solidus_promotions/conditions/product.rb +17 -59
- data/app/models/solidus_promotions/conditions/shipping_method.rb +3 -5
- data/app/models/solidus_promotions/conditions/store.rb +2 -1
- data/app/models/solidus_promotions/conditions/taxon.rb +24 -73
- data/app/models/solidus_promotions/conditions/user.rb +2 -1
- data/app/models/solidus_promotions/conditions/user_logged_in.rb +1 -3
- data/app/models/solidus_promotions/conditions/user_role.rb +1 -3
- data/app/models/solidus_promotions/distributed_amounts_handler.rb +2 -6
- data/app/models/solidus_promotions/eligibility_results.rb +1 -0
- data/app/models/solidus_promotions/item_discount.rb +1 -0
- data/app/models/solidus_promotions/order_adjuster/discount_order.rb +29 -35
- data/app/models/solidus_promotions/order_adjuster/recalculate_promo_totals.rb +45 -0
- data/app/models/solidus_promotions/order_adjuster/set_discounts_to_zero.rb +33 -0
- data/app/models/solidus_promotions/order_adjuster.rb +4 -14
- data/app/models/solidus_promotions/order_promotion.rb +1 -0
- data/app/models/solidus_promotions/product_advertiser.rb +57 -0
- data/app/models/solidus_promotions/promotion.rb +12 -10
- data/app/models/solidus_promotions/promotion_code/batch_builder.rb +1 -1
- data/app/models/solidus_promotions/promotion_code.rb +4 -4
- data/app/models/solidus_promotions/promotion_code_batch.rb +1 -1
- data/app/models/solidus_promotions/promotion_handler/coupon.rb +1 -1
- data/app/models/solidus_promotions/promotion_handler/page.rb +1 -1
- data/app/models/solidus_promotions/promotion_lane.rb +48 -0
- data/app/models/solidus_promotions/shipping_rate_discount.rb +3 -0
- data/app/patches/models/solidus_promotions/line_item_patch.rb +2 -0
- data/app/patches/models/solidus_promotions/order_patch.rb +8 -0
- data/app/patches/models/solidus_promotions/order_recalculator_patch.rb +3 -1
- data/app/patches/models/solidus_promotions/price_patch.rb +31 -0
- data/app/patches/models/solidus_promotions/shipment_patch.rb +2 -0
- data/app/patches/models/solidus_promotions/shipping_rate_patch.rb +15 -0
- data/config/locales/en.yml +47 -11
- data/config/routes.rb +1 -1
- data/db/migrate/20230703101637_create_promotions.rb +2 -2
- data/db/migrate/20230703113625_create_promotion_benefits.rb +3 -3
- data/db/migrate/20230703141116_create_promotion_categories.rb +1 -1
- data/db/migrate/20230703143943_create_promotion_conditions.rb +1 -1
- data/db/migrate/20230704083830_add_condition_join_tables.rb +8 -8
- data/db/migrate/20230704102444_create_promotion_codes.rb +1 -1
- data/db/migrate/20230704102656_create_promotion_code_batches.rb +1 -1
- data/db/migrate/20230705171556_create_order_promotions.rb +3 -3
- data/db/migrate/20230725074235_create_shipping_rate_discounts.rb +2 -2
- data/db/migrate/20231104135812_add_managed_by_order_benefit_to_line_items.rb +1 -1
- data/db/migrate/20251104170913_update_promotion_code_value_collation.rb +38 -0
- data/db/migrate/20251104214304_separate_out_order_only_conditions.rb +41 -0
- data/eslint.config.mjs +29 -0
- data/lib/components/admin/solidus_promotions/promotion_categories/index/component.rb +6 -6
- data/lib/components/admin/solidus_promotions/promotions/index/component.rb +5 -5
- data/lib/solidus_promotions/configuration.rb +57 -12
- data/lib/solidus_promotions/promotion_map.rb +14 -14
- data/lib/solidus_promotions/testing_support/shared_examples/option_value_condition.rb +18 -0
- data/lib/solidus_promotions/testing_support/shared_examples/product_condition.rb +37 -0
- data/lib/solidus_promotions/testing_support/shared_examples/promotion_calculator.rb +11 -0
- data/lib/solidus_promotions/testing_support/shared_examples/taxon_condition.rb +37 -0
- data/lib/solidus_promotions/testing_support/shared_examples.rb +6 -0
- data/lib/views/backend/solidus_promotions/admin/benefit_fields/_advertise_price.html.erb +7 -0
- data/lib/views/backend/solidus_promotions/admin/calculator_fields/_default_fields.html.erb +1 -1
- data/lib/views/backend/solidus_promotions/admin/calculator_fields/percent/_fields.html.erb +1 -1
- data/lib/views/backend/solidus_promotions/admin/condition_fields/_line_item_option_value.html.erb +6 -5
- data/lib/views/backend/solidus_promotions/admin/condition_fields/_option_value.html.erb +6 -12
- data/lib/views/backend/solidus_promotions/admin/condition_fields/_price_option_value.html.erb +26 -0
- data/lib/views/backend/solidus_promotions/admin/condition_fields/_price_product.html.erb +21 -0
- data/lib/views/backend/solidus_promotions/admin/condition_fields/_price_taxon.html.erb +17 -0
- data/lib/views/backend/solidus_promotions/admin/condition_fields/_product.html.erb +0 -7
- data/lib/views/backend/solidus_promotions/admin/condition_fields/_taxon.html.erb +0 -7
- data/lib/views/backend/solidus_promotions/admin/condition_fields/line_item_option_value/_option_value_fields.html.erb +10 -4
- data/solidus_promotions.gemspec +1 -1
- metadata +37 -6
- data/.eslintrc.json +0 -10
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aaae8dc702bc52d537b6e0e3ab7a77ec52091890f443a6a30a63c3de58c6a097
|
|
4
|
+
data.tar.gz: ea14d817d57406914450fe4690b9dbf4db628f11cb456c15b7a92b53fdf293a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
117
|
+
def order_eligible?(order, _options = {})
|
|
118
|
+
order.total > 100
|
|
119
|
+
end
|
|
114
120
|
|
|
115
|
-
def
|
|
116
|
-
|
|
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:
|
data/app/javascript/backend/solidus_promotions/controllers/product_option_values_controller.js
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
2
|
+
"use strict";
|
|
3
3
|
|
|
4
4
|
// Default options
|
|
5
|
-
options = options || {}
|
|
6
|
-
var multiple = typeof
|
|
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(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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(
|
|
26
|
-
datatype:
|
|
27
|
+
url: Spree.pathFor("api/option_values"),
|
|
28
|
+
datatype: "json",
|
|
27
29
|
data: function (term, page) {
|
|
28
|
-
var productId =
|
|
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(
|
|
69
|
+
customElements.define("option-value-picker", OptionValuePicker, { extends: "input" });
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
class ProductPicker extends HTMLInputElement {
|
|
2
2
|
connectedCallback() {
|
|
3
|
-
|
|
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
|
|
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
|
data/app/models/concerns/solidus_promotions/conditions/line_item_applicable_order_level_condition.rb
CHANGED
|
@@ -8,16 +8,28 @@ module SolidusPromotions
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def applicable?(promotable)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
7
|
-
|
|
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
|
|
7
|
-
|
|
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
|
|
7
|
-
|
|
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
|