Skip to content

Commit 1e26db3

Browse files
authored
Merge pull request solidusio#6371 from mamhoff/discountable-amounts-by-adjustments
Discountable amounts by adjustments
2 parents 5bc7c00 + ac699f3 commit 1e26db3

24 files changed

+419
-201
lines changed

promotions/app/models/concerns/solidus_promotions/discountable_amount.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@
22

33
module SolidusPromotions
44
module DiscountableAmount
5-
def discountable_amount
6-
amount + current_discounts.sum(&:amount)
7-
end
8-
95
def current_discounts
106
@current_discounts ||= []
117
end
8+
deprecate current_discounts: :previous_lane_discounts, deprecator: Spree.deprecator
129

1310
def current_discounts=(args)
1411
@current_discounts = args
1512
end
13+
deprecate :current_discounts=, deprecator: Spree.deprecator
1614

1715
def reset_current_discounts
1816
@current_discounts = []
1917
end
18+
deprecate :reset_current_discounts=, deprecator: Spree.deprecator
2019
end
2120
end

promotions/app/models/concerns/solidus_promotions/discounted_amount.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def initialize
2424
def discounted_amount
2525
amount + previous_lanes_discounts.sum(&:amount)
2626
end
27+
# The discountable amount is always equal to the discounted amount.
28+
alias_method :discountable_amount, :discounted_amount
2729

2830
# Returns discount objects from the current promotion lane.
2931
#

promotions/app/models/solidus_promotions/benefit.rb

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ def can_discount?(object)
9797
# @param adjustable [Object] The object to calculate the discount for (e.g., LineItem, Shipment, ShippingRate)
9898
# @param ... [args, kwargs] Additional arguments passed to the calculator's compute method
9999
#
100-
# @return [SolidusPromotions::ItemDiscount, nil] An ItemDiscount object if a discount applies, nil if the amount is zero
100+
# @return [Spree::Adjustment, SolidusPromotions::ShippingRateDiscount, nil] An ItemDiscount object if a discount applies, nil if the amount is zero
101101
#
102102
# @example Calculating a discount for a line item
103103
# benefit.discount(line_item)
104-
# # => #<SolidusPromotions::ItemDiscount item: #<Spree::LineItem>, amount: -10.00, ...>
104+
# # => #<Spree::Adjustment, adjustable: line_item, amount: -10.00, ...>
105105
#
106106
# @see #compute_amount
107107
# @see #adjustment_label
@@ -175,14 +175,48 @@ def compute_amount(adjustable, ...)
175175

176176
# Builds the localized label for adjustments created by this benefit.
177177
#
178-
# @param adjustable [Object]
179-
# @return [String]
180-
def adjustment_label(adjustable)
181-
I18n.t(
182-
"solidus_promotions.adjustment_labels.#{adjustable.class.name.demodulize.underscore}",
183-
promotion: SolidusPromotions::Promotion.model_name.human,
184-
promotion_customer_label: promotion.customer_label
185-
)
178+
# This method attempts to use a calculator-specific label method if available,
179+
# falling back to a localized string key based on the adjustable's class name.
180+
#
181+
# ## Calculator Override
182+
#
183+
# Calculators can provide custom labels by implementing a method named after the
184+
# adjustable type. For example, a calculator that discounts line items could
185+
# implement `line_item_adjustment_label`:
186+
#
187+
# @example Custom calculator with adjustment label
188+
# class MyCalculator < Spree::Calculator
189+
# def compute(adjustable, *args)
190+
# # calculation logic
191+
# end
192+
#
193+
# def line_item_adjustment_label(line_item, *args)
194+
# "Custom discount for #{line_item.product.name}"
195+
# end
196+
# end
197+
#
198+
# The method name follows the pattern: `{adjustable_type}_adjustment_label`
199+
# where `{adjustable_type}` is the underscored class name of the adjustable
200+
# (e.g., `line_item`, `shipment`, `shipping_rate`).
201+
#
202+
# If the calculator does not respond to the expected method, the benefit will
203+
# fall back to using an i18n translation key based on the adjustable's class.
204+
#
205+
# @param adjustable [Object] the object being discounted (e.g., Spree::LineItem, Spree::Shipment)
206+
# @param ... [args, kwargs] additional arguments forwarded to the calculator's label method
207+
# @return [String] a localized label suitable for display in adjustments
208+
#
209+
# @see #adjustment_label_method_for
210+
def adjustment_label(adjustable, ...)
211+
if calculator.respond_to?(adjustment_label_method_for(adjustable))
212+
calculator.send(adjustment_label_method_for(adjustable), adjustable, ...)
213+
else
214+
I18n.t(
215+
"solidus_promotions.adjustment_labels.#{adjustable.class.name.demodulize.underscore}",
216+
promotion: SolidusPromotions::Promotion.model_name.human,
217+
promotion_customer_label: promotion.customer_label
218+
)
219+
end
186220
end
187221

188222
# Partial path used for admin forms for this benefit type.
@@ -277,6 +311,10 @@ def discount_method_for(adjustable)
277311
:"discount_#{adjustable.class.name.demodulize.underscore}"
278312
end
279313

314+
def adjustment_label_method_for(adjustable)
315+
:"#{adjustable.class.name.demodulize.underscore}_adjustment_label"
316+
end
317+
280318
# Prevents destroying a benefit when it has adjustments on completed orders.
281319
#
282320
# Adds an error and aborts the destroy callback chain when such adjustments exist.

promotions/app/models/solidus_promotions/benefits/adjust_line_item.rb

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,10 @@ module SolidusPromotions
44
module Benefits
55
class AdjustLineItem < Benefit
66
def discount_line_item(line_item, ...)
7-
amount = compute_amount(line_item, ...)
8-
return if amount.zero?
9-
10-
ItemDiscount.new(
11-
item: line_item,
12-
label: adjustment_label(line_item),
13-
amount: amount,
14-
source: self
15-
)
7+
adjustment = find_adjustment(line_item) || build_adjustment(line_item)
8+
adjustment.amount = compute_amount(line_item, ...)
9+
adjustment.label = adjustment_label(line_item)
10+
adjustment
1611
end
1712

1813
def possible_conditions
@@ -23,6 +18,21 @@ def level
2318
:line_item
2419
end
2520
deprecate :level, deprecator: Spree.deprecator
21+
22+
private
23+
24+
def find_adjustment(line_item)
25+
line_item.adjustments.detect do |adjustment|
26+
adjustment.source == self
27+
end
28+
end
29+
30+
def build_adjustment(line_item)
31+
line_item.adjustments.build(
32+
order: line_item.order,
33+
source: self
34+
)
35+
end
2636
end
2737
end
2838
end

promotions/app/models/solidus_promotions/benefits/adjust_shipment.rb

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,17 @@ module SolidusPromotions
44
module Benefits
55
class AdjustShipment < Benefit
66
def discount_shipment(shipment, ...)
7-
amount = compute_amount(shipment, ...)
8-
return if amount.zero?
9-
10-
ItemDiscount.new(
11-
item: shipment,
12-
label: adjustment_label(shipment),
13-
amount: amount,
14-
source: self
15-
)
7+
adjustment = find_adjustment(shipment) || build_adjustment(shipment)
8+
adjustment.amount = compute_amount(shipment, ...)
9+
adjustment.label = adjustment_label(shipment)
10+
adjustment
1611
end
1712

1813
def discount_shipping_rate(shipping_rate, ...)
19-
amount = compute_amount(shipping_rate, ...)
20-
return if amount.zero?
21-
22-
ItemDiscount.new(
23-
item: shipping_rate,
24-
label: adjustment_label(shipping_rate),
25-
amount: amount,
26-
source: self
27-
)
14+
discount = find_discount(shipping_rate) || build_discount(shipping_rate)
15+
discount.amount = compute_amount(shipping_rate, ...)
16+
discount.label = adjustment_label(shipping_rate)
17+
discount
2818
end
2919

3020
def possible_conditions
@@ -35,6 +25,31 @@ def level
3525
:shipment
3626
end
3727
deprecate :level, deprecator: Spree.deprecator
28+
29+
private
30+
31+
def find_adjustment(shipment)
32+
shipment.adjustments.detect do |adjustment|
33+
adjustment.source == self
34+
end
35+
end
36+
37+
def build_adjustment(shipment)
38+
shipment.adjustments.build(
39+
order: shipment.order,
40+
source: self
41+
)
42+
end
43+
44+
def find_discount(shipping_rate)
45+
shipping_rate.discounts.detect do |discount|
46+
discount.benefit == self
47+
end
48+
end
49+
50+
def build_discount(shipping_rate)
51+
shipping_rate.discounts.build(benefit: self)
52+
end
3853
end
3954
end
4055
end

promotions/app/models/solidus_promotions/benefits/create_discounted_item.rb

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class CreateDiscountedItem < Benefit
1010
def perform(order)
1111
line_item = find_item(order) || build_item(order)
1212
set_quantity(line_item, determine_item_quantity(order))
13-
line_item.current_discounts << discount_line_item(line_item)
13+
discount_line_item(line_item)
1414
end
1515

1616
def remove_from(order)
@@ -25,13 +25,21 @@ def level
2525
private
2626

2727
def discount_line_item(line_item, ...)
28-
amount = compute_amount(line_item, ...)
29-
return if amount.zero?
28+
adjustment = find_adjustment(line_item) || build_adjustment(line_item)
29+
adjustment.amount = compute_amount(line_item, ...)
30+
adjustment.label = adjustment_label(line_item)
31+
adjustment
32+
end
33+
34+
def find_adjustment(line_item)
35+
line_item.adjustments.detect do |adjustment|
36+
adjustment.source == self
37+
end
38+
end
3039

31-
ItemDiscount.new(
32-
item: line_item,
33-
label: adjustment_label(line_item),
34-
amount: amount,
40+
def build_adjustment(line_item)
41+
line_item.adjustments.build(
42+
order: line_item.order,
3543
source: self
3644
)
3745
end

promotions/app/models/solidus_promotions/order_adjuster.rb

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,13 @@ def initialize(order, dry_run_promotion: nil)
1414
end
1515

1616
def call
17-
order.reset_current_discounts
18-
1917
return order unless SolidusPromotions::Promotion.order_activatable?(order)
2018

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)
2420

25-
order.reset_current_discounts
21+
DiscountOrder.new(order, promotions, dry_run: dry_run).call
2622

27-
unless dry_run
28-
# Since automations might have added a line item, we need to recalculate
29-
# item total and item count here.
30-
line_items = order.line_items.reject(&:marked_for_destruction?)
31-
order.item_total = line_items.sum(&:amount)
32-
order.item_count = line_items.sum(&:quantity)
33-
order.promo_total = (line_items + order.shipments).sum(&:promo_total)
34-
end
35-
order
23+
RecalculatePromoTotals.call(order)
3624
end
3725
end
3826
end

promotions/app/models/solidus_promotions/order_adjuster/discount_order.rb

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@ def call
1515
return order if order.shipped?
1616

1717
SolidusPromotions::Promotion.ordered_lanes.each 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)
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)
2625
end
2726
end
2827

@@ -49,26 +48,25 @@ def adjust_line_items(benefits)
4948
next unless line_item.variant.product.promotionable?
5049

5150
discounts = generate_discounts(benefits, line_item)
52-
chosen_item_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
53-
[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)
5453
end
5554
end
5655

5756
def adjust_shipments(benefits)
5857
order.shipments.map do |shipment|
5958
discounts = generate_discounts(benefits, shipment)
60-
chosen_item_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
61-
[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)
6261
end
6362
end
6463

6564
def adjust_shipping_rates(benefits)
6665
order.shipments.flat_map(&:shipping_rates).filter_map do |rate|
6766
next unless rate.cost
68-
6967
discounts = generate_discounts(benefits, rate)
70-
chosen_item_discounts = SolidusPromotions.config.discount_chooser_class.new(discounts).call
71-
[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)
7270
end
7371
end
7472

0 commit comments

Comments
 (0)