-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This new rule allows for an easy way of providing "bulk" discounts through the promotion system. It will prevent a promotion from being applied until it meets a quantity threshold. The rule will also account for other rules that limit the applicable line items. (e.g.: Taxons, options values, etc.) Co-authored-by: Benjamin Wil <[email protected]>
- Loading branch information
1 parent
0a9f663
commit cb4692e
Showing
5 changed files
with
181 additions
and
0 deletions.
There are no files selected for viewing
5 changes: 5 additions & 0 deletions
5
backend/app/views/spree/admin/promotions/rules/_minimum_quantity.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<div class="field"> | ||
<% field_name = "#{param_prefix}[preferred_minimum_quantity]" %> | ||
<%= label_tag field_name, promotion_rule.model_name.human %> | ||
<%= number_field_tag field_name, promotion_rule.preferred_minimum_quantity, class: "fullwidth", min: 1 %> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# frozen_string_literal: true | ||
|
||
module Spree | ||
class Promotion | ||
module Rules | ||
# Promotion rule for ensuring an order contains a minimum quantity of | ||
# actionable items. | ||
# | ||
# This promotion rule is only compatible with the "all" match policy. It | ||
# doesn't make a lot of sense to use it without that policy as it reduces | ||
# it to a simple quantity check across the entire order which would be | ||
# better served by an item total rule. | ||
class MinimumQuantity < PromotionRule | ||
validates :preferred_minimum_quantity, numericality: { only_integer: true, greater_than: 0 } | ||
|
||
preference :minimum_quantity, :integer, default: 1 | ||
|
||
# What type of objects we should run our eligiblity checks against. In | ||
# this case, our rule only applies to an entire order. | ||
# | ||
# @param promotable [Spree::Order,Spree::LineItem] | ||
# @return [Boolean] true if promotable is a Spree::Order, false | ||
# otherwise | ||
def applicable?(promotable) | ||
promotable.is_a?(Spree::Order) | ||
end | ||
|
||
# Will look at all of the "actionable" line items in the order and | ||
# determine if the sum of their quantity is greater than the minimum. | ||
# | ||
# "Actionable" items are ones where they pass the "actionable?" check of | ||
# all rules on the promotion. (e.g.: Match product/taxon when one of | ||
# those rules is present.) | ||
# | ||
# When false is returned, the reason will be included in the | ||
# `eligibility_errors` object. | ||
# | ||
# @param order [Spree::Order] the order we want to check eligibility on | ||
# @param _options [Hash] ignored | ||
# @return [Boolean] true if promotion is eligible, false otherwise | ||
def eligible?(order, _options = {}) | ||
actionable_line_items = order.line_items.select do |line_item| | ||
promotion.rules.all? { _1.actionable?(line_item) } | ||
end | ||
|
||
if actionable_line_items.sum(&:quantity) < preferred_minimum_quantity | ||
eligibility_errors.add( | ||
:base, | ||
eligibility_error_message(:quantity_less_than_minimum, count: preferred_minimum_quantity), | ||
error_code: :quantity_less_than_minimum | ||
) | ||
end | ||
|
||
eligibility_errors.empty? | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
110 changes: 110 additions & 0 deletions
110
core/spec/models/spree/promotion/rules/minimum_quantity_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
|
||
RSpec.describe Spree::Promotion::Rules::MinimumQuantity do | ||
subject(:quantity_rule) { described_class.new(preferred_minimum_quantity: 2) } | ||
|
||
describe "#valid?" do | ||
let(:promotion) { build(:promotion) } | ||
|
||
before { promotion.rules << quantity_rule } | ||
|
||
it { is_expected.to be_valid } | ||
|
||
context "when minimum quantity is zero" do | ||
subject(:quantity_rule) { described_class.new(preferred_minimum_quantity: 0) } | ||
|
||
it { is_expected.not_to be_valid } | ||
end | ||
end | ||
|
||
describe "#applicable?" do | ||
subject { quantity_rule.applicable?(promotable) } | ||
|
||
context "when promotable is an order" do | ||
let(:promotable) { Spree::Order.new } | ||
|
||
it { is_expected.to be true } | ||
end | ||
|
||
context "when promotable is a line item" do | ||
let(:promotable) { Spree::LineItem.new } | ||
|
||
it { is_expected.to be false } | ||
end | ||
end | ||
|
||
describe "#eligible?" do | ||
subject { quantity_rule.eligible?(order) } | ||
|
||
let(:order) { | ||
create( | ||
:order_with_line_items, | ||
line_items_count: line_items.length, | ||
line_items_attributes: line_items | ||
) | ||
} | ||
let(:promotion) { build(:promotion) } | ||
|
||
before { promotion.rules << quantity_rule } | ||
|
||
context "when only the quantity rule is applied" do | ||
context "when the quantity is less than the minimum" do | ||
let(:line_items) { [{ quantity: 1 }] } | ||
|
||
it { is_expected.to be false } | ||
end | ||
|
||
context "when the quantity is equal to the minimum" do | ||
let(:line_items) { [{ quantity: 2 }] } | ||
|
||
it { is_expected.to be true } | ||
end | ||
|
||
context "when the quantity is greater than the minimum" do | ||
let(:line_items) { [{ quantity: 4 }] } | ||
|
||
it { is_expected.to be true } | ||
end | ||
end | ||
|
||
context "when another rule limits the applicable items" do | ||
let(:variant_1) { create(:variant) } | ||
let(:variant_2) { create(:variant) } | ||
let(:variant_3) { create(:variant) } | ||
|
||
let(:product_rule) { | ||
Spree::Promotion::Rules::Product.new( | ||
products: [variant_1.product, variant_2.product], | ||
preferred_match_policy: "any" | ||
) | ||
} | ||
|
||
before { promotion.rules << product_rule } | ||
|
||
context "when the applicable quantity is less than the minimum" do | ||
let(:line_items) do | ||
[ | ||
{ variant: variant_1, quantity: 1 }, | ||
{ variant: variant_3, quantity: 1 } | ||
] | ||
end | ||
|
||
it { is_expected.to be false } | ||
end | ||
|
||
context "when the applicable quantity is greater than the minimum" do | ||
let(:line_items) do | ||
[ | ||
{ variant: variant_1, quantity: 1 }, | ||
{ variant: variant_2, quantity: 1 }, | ||
{ variant: variant_3, quantity: 1 } | ||
] | ||
end | ||
|
||
it { is_expected.to be true } | ||
end | ||
end | ||
end | ||
end |