Skip to content

Commit 5498135

Browse files
committed
Change Benefit#discount to dynamically call methods on benefits
Similar to the work in #6360, this improves the DSL for creating benefits. Rather than having to define one method that cares for applicability to a discountable, we now simply define that a benefit `#can_discount?` an object if its public interface has `#discount_{discountable_type}` defined. This does not change the implementation of the different methods, but that will come when we refactor the promotion system to use a single system of record for discounts. Then, different objects will need different discount records (line items and shipment use adjustments, but shipping rates use shipping rate discounts etc.). I'm not doing the work for adding deprecation warnings here, as this code was never meant to be overridden up to now.
1 parent f54d7ff commit 5498135

File tree

6 files changed

+128
-37
lines changed

6 files changed

+128
-37
lines changed

promotions/app/models/solidus_promotions/benefit.rb

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ module SolidusPromotions
1111
# Subclasses specialize the discounting target (orders, line items, or
1212
# shipments) and usually include one of the following mixins to integrate with
1313
# Solidus' adjustment system:
14-
# - SolidusPromotions::Benefits::OrderBenefit
15-
# - SolidusPromotions::Benefits::LineItemBenefit
16-
# - SolidusPromotions::Benefits::ShipmentBenefit
14+
# - SolidusPromotions::Benefits::AdjustLineItem
15+
# - SolidusPromotions::Benefits::AdjustShipment
16+
# - SolidusPromotions::Benefits::CreateDiscountedItem
1717
#
1818
# A benefit can discount any object for which {#can_discount?} returns true.
1919
# Implementors must provide a calculator via Spree::CalculatedAdjustments and
@@ -81,13 +81,11 @@ def preload_relations
8181
#
8282
# @param object [Object] a potential adjustable (order, line item, or shipment)
8383
# @return [Boolean]
84-
# @raise [NotImplementedError] when not implemented by the subclass/mixin
85-
# @see SolidusPromotions::Benefits::OrderBenefit,
86-
# SolidusPromotions::Benefits::LineItemBenefit,
87-
# SolidusPromotions::Benefits::ShipmentBenefit
84+
# @see SolidusPromotions::Benefits::AdjustLineItem,
85+
# SolidusPromotions::Benefits::AdjustShipment,
86+
# SolidusPromotions::Benefits::CreateDiscountedItem
8887
def can_discount?(object)
89-
raise NotImplementedError, "Please implement the correct interface, or include one of the `SolidusPromotions::Benefits::OrderBenefit`, " \
90-
"`SolidusPromotions::Benefits::LineItemBenefit` or `SolidusPromotions::Benefits::ShipmentBenefit` modules"
88+
respond_to?(discount_method_for(object))
9189
end
9290

9391
# Calculates and returns a discount for the given adjustable object.
@@ -96,7 +94,7 @@ def can_discount?(object)
9694
# an ItemDiscount object representing the discount to be applied. If the computed
9795
# amount is zero, no discount is returned.
9896
#
99-
# @param adjustable [Object] The object to calculate the discount for (e.g., LineItem, Order, Shipment)
97+
# @param adjustable [Object] The object to calculate the discount for (e.g., LineItem, Shipment, ShippingRate)
10098
# @param ... [args, kwargs] Additional arguments passed to the calculator's compute method
10199
#
102100
# @return [SolidusPromotions::ItemDiscount, nil] An ItemDiscount object if a discount applies, nil if the amount is zero
@@ -108,14 +106,11 @@ def can_discount?(object)
108106
# @see #compute_amount
109107
# @see #adjustment_label
110108
def discount(adjustable, ...)
111-
amount = compute_amount(adjustable, ...)
112-
return if amount.zero?
113-
ItemDiscount.new(
114-
item: adjustable,
115-
label: adjustment_label(adjustable),
116-
amount: amount,
117-
source: self
118-
)
109+
if can_discount?(adjustable)
110+
send(discount_method_for(adjustable), adjustable, ...)
111+
else
112+
raise NotImplementedError, "Please implement #{discount_method_for(adjustable)} in your condition"
113+
end
119114
end
120115

121116
# Computes the discount amount for the given adjustable.
@@ -231,6 +226,10 @@ def possible_conditions
231226

232227
private
233228

229+
def discount_method_for(adjustable)
230+
:"discount_#{adjustable.class.name.demodulize.underscore}"
231+
end
232+
234233
# Prevents destroying a benefit when it has adjustments on completed orders.
235234
#
236235
# 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: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,26 @@
33
module SolidusPromotions
44
module Benefits
55
class AdjustLineItem < Benefit
6-
include SolidusPromotions::Benefits::LineItemBenefit
6+
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+
)
16+
end
717

818
def possible_conditions
919
super + SolidusPromotions.config.line_item_conditions
1020
end
21+
22+
def level
23+
:line_item
24+
end
25+
deprecate :level, deprecator: Spree.deprecator
1126
end
1227
end
1328
end

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,38 @@
33
module SolidusPromotions
44
module Benefits
55
class AdjustShipment < Benefit
6-
include SolidusPromotions::Benefits::ShipmentBenefit
6+
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+
)
16+
end
17+
18+
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+
)
28+
end
729

830
def possible_conditions
931
super + SolidusPromotions.config.shipment_conditions
1032
end
33+
34+
def level
35+
:shipment
36+
end
37+
deprecate :level, deprecator: Spree.deprecator
1138
end
1239
end
1340
end

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,40 @@
33
module SolidusPromotions
44
module Benefits
55
class CreateDiscountedItem < Benefit
6-
include OrderBenefit
76
preference :variant_id, :integer
87
preference :quantity, :integer, default: 1
98
preference :necessary_quantity, :integer, default: 1
109

1110
def perform(order)
1211
line_item = find_item(order) || create_item(order)
1312
set_quantity(line_item, determine_item_quantity(order))
14-
line_item.current_discounts << discount(line_item)
13+
line_item.current_discounts << discount_line_item(line_item)
1514
end
1615

1716
def remove_from(order)
1817
line_item = find_item(order)
1918
order.line_items.destroy(line_item)
2019
end
2120

21+
def level
22+
:order
23+
end
24+
deprecate :level, deprecator: Spree.deprecator
25+
2226
private
2327

28+
def discount_line_item(line_item, ...)
29+
amount = compute_amount(line_item, ...)
30+
return if amount.zero?
31+
32+
ItemDiscount.new(
33+
item: line_item,
34+
label: adjustment_label(line_item),
35+
amount: amount,
36+
source: self
37+
)
38+
end
39+
2440
def find_item(order)
2541
order.line_items.detect { |line_item| line_item.managed_by_order_benefit == self }
2642
end

promotions/spec/models/solidus_promotions/benefit_spec.rb

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,22 @@
1212
it { is_expected.to respond_to :can_discount? }
1313

1414
describe "#can_adjust?" do
15-
subject { described_class.new.can_discount?(double) }
15+
let(:adjustable) { Spree::LineItem.new }
16+
let(:benefit_class) do
17+
Class.new(described_class) do
18+
def discount_line_item(line_item, options = {})
19+
end
20+
end
21+
end
1622

17-
it "raises a NotImplementedError" do
18-
expect { subject }.to raise_exception(NotImplementedError)
23+
subject { benefit_class.new.can_discount?(adjustable) }
24+
25+
it { is_expected.to be true }
26+
27+
context "if passing in an incompatible object" do
28+
let(:adjustable) { Spree::Shipment.new }
29+
30+
it { is_expected.to be false }
1931
end
2032
end
2133

@@ -69,22 +81,23 @@
6981
describe "#discount" do
7082
subject { benefit.discount(discountable) }
7183

84+
let(:benefit_class) do
85+
Class.new(described_class) do
86+
def discount_line_item(line_item, options = {})
87+
end
88+
end
89+
end
90+
7291
let(:variant) { create(:variant) }
7392
let(:order) { create(:order) }
7493
let(:discountable) { Spree::LineItem.new(order: order, variant: variant, price: 10, quantity: 1) }
7594
let(:promotion) { SolidusPromotions::Promotion.new(customer_label: "20 Perzent off") }
7695
let(:calculator) { SolidusPromotions::Calculators::Percent.new(preferred_percent: 20) }
77-
let(:benefit) { described_class.new(promotion: promotion, calculator: calculator) }
78-
79-
it "returns an discount to the discountable" do
80-
expect(subject).to eq(
81-
SolidusPromotions::ItemDiscount.new(
82-
item: discountable,
83-
label: "Promotion (20 Perzent off)",
84-
source: benefit,
85-
amount: -2
86-
)
87-
)
96+
let(:benefit) { benefit_class.new(promotion: promotion, calculator: calculator) }
97+
98+
it "passes adjustable to discount_line_item" do
99+
expect(benefit).to receive(:discount_line_item).with(discountable)
100+
subject
88101
end
89102

90103
context "if the calculator returns nil" do
@@ -106,15 +119,20 @@
106119
end
107120

108121
context "if passing in extra options" do
122+
let(:benefit_class) { SolidusPromotions::Benefits::AdjustLineItem }
109123
let(:calculator_class) do
110124
Class.new(Spree::Calculator) do
111125
def compute_line_item(_line_item, _options) = 1
112126
end
113127
end
114128
let(:calculator) { calculator_class.new }
115-
let(:discountable) { build(:line_item) }
129+
let(:promotion) { build(:solidus_promotion) }
130+
let(:benefit) { benefit_class.new(promotion:, calculator:) }
131+
let(:order) { Spree::Order.new }
132+
let(:discountable) { build(:line_item, order:) }
116133

117134
subject { benefit.discount(discountable, extra_data: "foo") }
135+
118136
it "passes the option on to the calculator" do
119137
expect(calculator).to receive(:compute_line_item).with(discountable, extra_data: "foo").and_return(1)
120138
subject

promotions/spec/models/solidus_promotions/benefits/create_discounted_item_spec.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,28 @@
55
RSpec.describe SolidusPromotions::Benefits::CreateDiscountedItem do
66
it { is_expected.to respond_to(:preferred_variant_id) }
77

8+
describe "#can_discount?" do
9+
let(:benefit) { described_class.new }
10+
let(:discountable) { Spree::Order.new }
11+
12+
subject { benefit.can_discount?(discountable) }
13+
14+
it { is_expected.to be false }
15+
16+
context "with a line item" do
17+
let(:discountable) { Spree::LineItem.new }
18+
19+
it { is_expected.to be false }
20+
end
21+
end
22+
823
describe "#perform" do
924
let(:order) { create(:order_with_line_items) }
1025
let(:promotion) { create(:solidus_promotion) }
1126
let(:benefit) { SolidusPromotions::Benefits::CreateDiscountedItem.new(preferred_variant_id: goodie.id, calculator: hundred_percent, promotion: promotion) }
1227
let(:hundred_percent) { SolidusPromotions::Calculators::Percent.new(preferred_percent: 100) }
1328
let(:goodie) { create(:variant) }
29+
1430
subject { benefit.perform(order) }
1531

1632
it "creates a line item with a hundred percent discount" do

0 commit comments

Comments
 (0)