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.
- 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 +3 -1
- 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
|
@@ -14,15 +14,14 @@ module SolidusPromotions
|
|
|
14
14
|
def call
|
|
15
15
|
return order if order.shipped?
|
|
16
16
|
|
|
17
|
-
SolidusPromotions::Promotion.ordered_lanes.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
53
|
-
line_item.variant.product.promotionable?
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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).
|
|
65
|
+
order.shipments.flat_map(&:shipping_rates).filter_map do |rate|
|
|
66
|
+
next unless rate.cost
|
|
71
67
|
discounts = generate_discounts(benefits, rate)
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
86
|
-
benefit.can_discount?(item)
|
|
87
|
-
end
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
PersistDiscountedOrder.new(discounted_order).call unless dry_run
|
|
19
|
+
SetDiscountsToZero.call(order)
|
|
24
20
|
|
|
25
|
-
order.
|
|
21
|
+
DiscountOrder.new(order, promotions, dry_run: dry_run).call
|
|
26
22
|
|
|
27
|
-
|
|
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: {
|
|
23
|
-
validates :usage_limit, numericality: {
|
|
24
|
-
validates :per_code_usage_limit, numericality: {
|
|
25
|
-
validates :description, length: {
|
|
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(
|
|
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 |
|
|
59
|
-
[human_enum_name(:lane,
|
|
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
|
|
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: {
|
|
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: {
|
|
110
|
+
.where.not(spree_orders: {state: :canceled})
|
|
109
111
|
.exists?
|
|
110
112
|
end
|
|
111
113
|
|
|
@@ -9,7 +9,7 @@ module SolidusPromotions
|
|
|
9
9
|
|
|
10
10
|
before_validation :normalize_code
|
|
11
11
|
|
|
12
|
-
validates :value, presence: true, uniqueness: {
|
|
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: {
|
|
35
|
+
.where.not(spree_orders: {state: :canceled})
|
|
36
36
|
.joins(:solidus_order_promotions)
|
|
37
|
-
.where(SolidusPromotions::OrderPromotion.table_name => {
|
|
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 =
|
|
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: {
|
|
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?
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|