Skip to content

Commit

Permalink
Backports minimum quantity promotion rule from Solidus
Browse files Browse the repository at this point in the history
Original PR: solidusio#5452

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]>
Co-authored-by: Adam Mueller <[email protected]>
  • Loading branch information
3 people committed Jul 1, 2024
1 parent b0379a7 commit 3ca0b98
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

module SolidusFriendlyPromotions
module Rules
# Promotion rule for ensuring an order contains a minimum quantity of
# applicable 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
include OrderLevelRule

validates :preferred_minimum_quantity, numericality: {only_integer: true, greater_than: 0}

preference :minimum_quantity, :integer, default: 1

# Will look at all of the "applicable" line items in the order and
# determine if the sum of their quantity is greater than the minimum.
#
# "Applicable" items are ones that pass all eligibility checks of applicable rules.
#
# 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
# @return [Boolean] true if promotion is eligible, false otherwise
def eligible?(order)
applicable_line_items = order.line_items.select do |line_item|
promotion.rules.select do |rule|
rule.applicable?(line_item)
end.all? { _1.eligible?(line_item) }
end

if applicable_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
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>
7 changes: 7 additions & 0 deletions friendly_promotions/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ en:
solidus_friendly_promotions/rules/user_logged_in:
no_user_specified: You need to login before applying this coupon code.
solidus_friendly_promotions/rules/user_role:
solidus_friendly_promotions/rules/minimum_quantity:
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."
product_rule:
choose_products: Choose products
label: Order must contain %{select} these products
Expand Down Expand Up @@ -157,6 +161,7 @@ en:
solidus_friendly_promotions/rules/item_total: Item Total
solidus_friendly_promotions/rules/discounted_item_total: Item Total after previous lanes
solidus_friendly_promotions/rules/landing_page: Landing Page
solidus_friendly_promotions/rules/minimum_quantity: Minimum Quantity
solidus_friendly_promotions/rules/nth_order: Nth Order
solidus_friendly_promotions/rules/one_use_per_user: One Use Per User
solidus_friendly_promotions/rules/option_value: Option Value(s)
Expand Down Expand Up @@ -201,6 +206,8 @@ en:
preferred_line_item_applicable: Should also apply to line items
solidus_friendly_promotions/rules/line_item_option_value:
description: Line Item has specified product with matching option value
solidus_friendly_promotions/rules/minimum_quantity:
description: Order contains minimum quantity of applicable items
solidus_friendly_promotions/rules/product:
description: Order includes specified product(s)
line_item_level_description: 'Line item matches the specified products:'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"SolidusFriendlyPromotions::Rules::FirstRepeatPurchaseSince",
"SolidusFriendlyPromotions::Rules::ItemTotal",
"SolidusFriendlyPromotions::Rules::DiscountedItemTotal",
"SolidusFriendlyPromotions::Rules::MinimumQuantity",
"SolidusFriendlyPromotions::Rules::NthOrder",
"SolidusFriendlyPromotions::Rules::OneUsePerUser",
"SolidusFriendlyPromotions::Rules::OptionValue",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# frozen_string_literal: true

RSpec.describe SolidusFriendlyPromotions::Rules::MinimumQuantity do
subject(:quantity_rule) { described_class.new(preferred_minimum_quantity: 2) }

describe "#valid?" do
let(:promotion) { build(:friendly_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) do
create(
:order_with_line_items,
line_items_count: line_items.length,
line_items_attributes: line_items
)
end
let(:promotion) { build(:friendly_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(:carry_on) { create(:variant) }
let(:other_carry_on) { create(:variant) }
let(:everywhere_bag) { create(:product).master }

let(:product_rule) {
SolidusFriendlyPromotions::Rules::LineItemProduct.new(
products: [carry_on.product, other_carry_on.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: carry_on, quantity: 1},
{variant: everywhere_bag, 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: carry_on, quantity: 1},
{variant: other_carry_on, quantity: 1},
{variant: everywhere_bag, quantity: 1}
]
end

it { is_expected.to be true }
end
end
end
end

0 comments on commit 3ca0b98

Please sign in to comment.