Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimum Quantity promotion rule #5452

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@
}
}

#actions_container {
.edit_promotion {
margin-top: 73px;
}
}

.promotion-block {
padding: 0 1.25rem 0.5rem;
background-color: lighten($color-border, 5);
Expand Down
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>
59 changes: 59 additions & 0 deletions core/app/models/spree/promotion/rules/minimum_quantity.rb
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
6 changes: 6 additions & 0 deletions core/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ en:
description: Order total meets these criteria
spree/promotion/rules/landing_page:
description: Customer must have visited the specified page
spree/promotion/rules/minimum_quantity:
description: Order contains minimum quantity of applicable items
spree/promotion/rules/nth_order:
description: Apply a promotion to every nth order a user has completed.
form_text: 'Apply this promotion on the users Nth order: '
Expand Down Expand Up @@ -652,6 +654,7 @@ en:
spree/promotion/rules/first_repeat_purchase_since: First Repeat Purchase Since
spree/promotion/rules/item_total: Item Total
spree/promotion/rules/landing_page: Landing Page
spree/promotion/rules/minimum_quantity: Minimum Quantity
spree/promotion/rules/nth_order: Nth Order
spree/promotion/rules/one_use_per_user: One Use Per User
spree/promotion/rules/option_value: Option Value(s)
Expand Down Expand Up @@ -1533,6 +1536,9 @@ en:
no_user_or_email_specified: You need to login or provide your email before applying this coupon code.
no_user_specified: You need to login before applying this coupon code.
not_first_order: This coupon code can only be applied to your first order.
quantity_less_than_minimum:
one: You need to add a least 1 applicable item to your order.
other: You need to add a least %{count} applicable items to your order.
email: Email
empty: Empty
empty_cart: Empty Cart
Expand Down
1 change: 1 addition & 0 deletions core/lib/spree/app_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ def environment
Spree::Promotion::Rules::UserLoggedIn
Spree::Promotion::Rules::OneUsePerUser
Spree::Promotion::Rules::Taxon
Spree::Promotion::Rules::MinimumQuantity
Spree::Promotion::Rules::NthOrder
Spree::Promotion::Rules::OptionValue
Spree::Promotion::Rules::FirstRepeatPurchaseSince
Expand Down
110 changes: 110 additions & 0 deletions core/spec/models/spree/promotion/rules/minimum_quantity_spec.rb
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
Loading