Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions promotions/app/models/solidus_promotions/load_promotions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
module SolidusPromotions
class LoadPromotions
def initialize(order:, dry_run_promotion: nil)
if dry_run_promotion
Spree.deprecator.warn <<~MSG
Passing `dry_run_promotion` to `SolidusPromotions::LoadPromotions` is deprecated.
Use `Spree::Config.promotions.eligibility_checker_class.new(order: order, promotion: promotion).call` instead.
MSG
end
@order = order
@dry_run_promotion = dry_run_promotion
end
Expand Down
6 changes: 6 additions & 0 deletions promotions/app/models/solidus_promotions/order_adjuster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ class OrderAdjuster
attr_reader :order, :promotions, :dry_run

def initialize(order, dry_run_promotion: nil)
if dry_run_promotion
Spree.deprecator.warn <<~MSG
Passing `dry_run_promotion` to `SolidusPromotions::OrderAdjuster` is deprecated.
Use `Spree::Config.promotions.eligibility_checker_class.new(order: order, promotion: promotion).call` instead.
MSG
end
@order = order
@dry_run = !!dry_run_promotion
@promotions = SolidusPromotions::LoadPromotions.new(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ class DiscountOrder
attr_reader :order, :promotions, :dry_run

def initialize(order, promotions, dry_run: false)
if dry_run
Spree.deprecator.warn <<~MSG
Passing `dry_run` to `SolidusPromotions::OrderAdjuster::DiscountOrder` is deprecated.
Use `Spree::Config.promotions.eligibility_checker_class.new(order: order, promotion: promotion).call` instead.
MSG
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 🐝

end
@order = order
@promotions = promotions
@dry_run = dry_run
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module SolidusPromotions
class PromotionEligibilityChecker
attr_reader :order, :promotion, :results

def initialize(order:, promotion:)
@order = order
@promotion = promotion
@results = SolidusPromotions::EligibilityResults.new(promotion)
end

def call
SolidusPromotions::PromotionLane.set(current_lane: promotion.lane) do
promotion.benefits.any? do |benefit|
# We're running this first and storing the result so the following
# block does not short-circuit on ineligible items, and we get all errors.
order_eligible = applicable_conditions_eligible?(order, benefit)
(
order.line_items.any? do |line_item|
check_item(line_item, benefit)
end || order.shipments.any? do |shipment|
check_item(shipment, benefit)
end
Comment on lines +20 to +24
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(order.line_items + order.shipments).any? do |item|
  check_item(item, benefit)
end && order_eligible

) && order_eligible
end
end
end

def check_item(item, benefit)
benefit.can_discount?(item) &&
applicable_conditions_eligible?(item, benefit)
end

def applicable_conditions_eligible?(item, benefit)
benefit.conditions.map do |condition|
next unless condition.applicable?(item)
eligible = !!condition.eligible?(item)

if condition.eligibility_errors.details[:base].first
code = condition.eligibility_errors.details[:base].first[:error_code]
message = condition.eligibility_errors.full_messages.first
end
results.add(
item: item,
condition: condition,
success: eligible,
code: eligible ? nil : (code || :coupon_code_unknown_error),
message: eligible ? nil : (message || I18n.t(:coupon_code_unknown_error, scope: [:solidus_promotions, :eligibility_errors]))
)

eligible
end.compact.all?
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,24 @@ def handle_present_promotion
return promotion_usage_limit_exceeded if promotion.usage_limit_exceeded? || promotion_code.usage_limit_exceeded?
return promotion_applied if promotion_exists_on_order?(order, promotion)

# Try applying this promotion, with no effects
Spree::Config.promotions.order_adjuster_class.new(order, dry_run_promotion: promotion).call
# Check promotion applicability
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*eligibility

checker = Spree::Config.promotions.eligibility_checker_class.new(order: order, promotion: promotion)

if promotion.eligibility_results.success?
if checker.call
order.solidus_order_promotions.create!(
promotion: promotion,
promotion_code: promotion_code
)
order.recalculate
set_success_code :coupon_code_applied
else
set_promotion_eligibility_error(promotion)
set_promotion_eligibility_error(checker.results)
end
end

def set_promotion_eligibility_error(promotion)
eligibility_error = promotion.eligibility_results.detect { |result| !result.success }
set_error_code(eligibility_error.code, error: eligibility_error.message, errors: promotion.eligibility_results.error_messages)
def set_promotion_eligibility_error(results)
eligibility_error = results.detect { |result| !result.success }
set_error_code(eligibility_error.code, error: eligibility_error.message, errors: results.error_messages)
end

def promotion_usage_limit_exceeded
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
module SolidusPromotions
module PromotionHandler
class Page
attr_reader :order, :path
attr_reader :order, :path, :checker

def initialize(order, path)
@order = order
@path = path.delete_prefix("/")
@checker = Spree::Config.promotions.eligibility_checker_class.new(order: order, promotion: promotion)
end

delegate :results, to: :checker

def activate
if promotion
Spree::Config.promotions.order_adjuster_class.new(order, dry_run_promotion: promotion).call
if promotion.eligibility_results.success?
order.solidus_promotions << promotion
order.recalculate
end
if promotion && checker.call
order.solidus_promotions << promotion
order.recalculate
end
end

Expand Down
5 changes: 5 additions & 0 deletions promotions/lib/solidus_promotions/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ class Configuration < Spree::Preferences::Configuration

class_name_attribute :order_adjuster_class, default: "SolidusPromotions::OrderAdjuster"

# Service object that checks whether a promotion is eligible for an order.
# @!attribute [rw] eligibility_checker_class
# @return [String] Defaults to "SolidusPromotions::PromotionEligibilityChecker".
class_name_attribute :eligibility_checker_class, default: "SolidusPromotions::PromotionEligibilityChecker"

class_name_attribute :coupon_code_handler_class, default: "SolidusPromotions::PromotionHandler::Coupon"

# The class used to normalize coupon codes before saving or lookup.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
end
end

describe ".eligibility_checker_class" do
it "is the standard eligibility checker" do
expect(subject.eligibility_checker_class).to eq(SolidusPromotions::PromotionEligibilityChecker)
end
end

describe ".advertiser_class" do
it "is the standard advertiser" do
expect(subject.advertiser_class).to eq(SolidusPromotions::PromotionAdvertiser)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,178 +32,4 @@
expect { subject }.not_to raise_exception
end
end

describe "collecting eligibility results in a dry run" do
let(:shirt) { create(:product, name: "Shirt") }
let(:order) { create(:order_with_line_items, line_items_attributes: [{variant: shirt.master, quantity: 1}]) }
let(:conditions) { [product_condition] }
let!(:promotion) { create(:solidus_promotion, :with_adjustable_benefit, conditions: conditions, name: "20% off Shirts", apply_automatically: true) }
let(:product_condition) { SolidusPromotions::Conditions::OrderProduct.new(products: [shirt]) }
let(:promotions) { [promotion] }
let(:discounter) { described_class.new(order, promotions, dry_run: true) }

subject { discounter.call }

it "will collect eligibility results" do
subject

expect(promotion.eligibility_results.first.success).to be true
expect(promotion.eligibility_results.first.code).to be nil
expect(promotion.eligibility_results.first.condition).to eq(product_condition)
expect(promotion.eligibility_results.first.message).to be nil
expect(promotion.eligibility_results.first.item).to eq(order)
end

it "can tell us about success" do
subject
expect(promotion.eligibility_results.success?).to be true
end

context "with two conditions" do
let(:conditions) { [product_condition, item_total_condition] }
let(:item_total_condition) { SolidusPromotions::Conditions::ItemTotal.new(preferred_amount: 2000) }

it "will collect eligibility results" do
subject

expect(promotion.eligibility_results.first.success).to be true
expect(promotion.eligibility_results.first.code).to be nil
expect(promotion.eligibility_results.first.condition).to eq(product_condition)
expect(promotion.eligibility_results.first.message).to be nil
expect(promotion.eligibility_results.first.item).to eq(order)
expect(promotion.eligibility_results.last.success).to be false
expect(promotion.eligibility_results.last.condition).to eq(item_total_condition)
expect(promotion.eligibility_results.last.code).to eq :item_total_less_than_or_equal
expect(promotion.eligibility_results.last.message).to eq "This coupon code can't be applied to orders less than or equal to $2,000.00."
expect(promotion.eligibility_results.last.item).to eq(order)
end

it "can tell us about success" do
subject
expect(promotion.eligibility_results.success?).to be false
end

it "has errors for this promo" do
subject
expect(promotion.eligibility_results.error_messages).to eq([
"This coupon code can't be applied to orders less than or equal to $2,000.00."
])
end
end

context "with an order with multiple line items and an item-level condition" do
let(:pants) { create(:product, name: "Pants") }
let(:order) do
create(
:order_with_line_items,
line_items_attributes: [{variant: shirt.master, quantity: 1}, {variant: pants.master, quantity: 1}]
)
end

let(:shirt_product_condition) { SolidusPromotions::Conditions::LineItemProduct.new(products: [shirt]) }
let(:conditions) { [shirt_product_condition] }

it "can tell us about success" do
subject
# This is successful, because one of the line item conditions matches
expect(promotion.eligibility_results.success?).to be true
end

it "has no errors for this promo" do
subject
expect(promotion.eligibility_results.error_messages).to be_empty
end

context "with a second line item level condition" do
let(:hats) { create(:taxon, name: "Hats", products: [hat]) }
let(:hat) { create(:product) }
let(:hat_product_condition) { SolidusPromotions::Conditions::LineItemTaxon.new(taxons: [hats]) }
let(:conditions) { [shirt_product_condition, hat_product_condition] }

it "can tell us about success" do
subject
expect(promotion.eligibility_results.success?).to be false
end

it "has errors for this promo" do
subject
expect(promotion.eligibility_results.error_messages).to eq([
"This coupon code could not be applied to the cart at this time."
])
end
end
end

context "when the order must not contain a shirt" do
let(:no_shirt_condition) { SolidusPromotions::Conditions::OrderProduct.new(products: [shirt], preferred_match_policy: "none") }
let(:conditions) { [no_shirt_condition] }

it "can tell us about success" do
subject
# This is successful, because the order has a shirt
expect(promotion.eligibility_results.success?).to be false
end
end

context "where one benefit succeeds and another errors" do
let(:usps) { create(:shipping_method) }
let(:ups_ground) { create(:shipping_method) }
let(:order) { create(:order_with_line_items, line_items_attributes: [{variant: shirt.master, quantity: 1}], shipping_method: ups_ground) }
let(:product_condition) { SolidusPromotions::Conditions::OrderProduct.new(products: [shirt]) }
let(:shipping_method_condition) { SolidusPromotions::Conditions::ShippingMethod.new(preferred_shipping_method_ids: [usps.id]) }
let(:ten_off_items) { SolidusPromotions::Calculators::Percent.create!(preferred_percent: 10) }
let(:ten_off_shipping) { SolidusPromotions::Calculators::Percent.create!(preferred_percent: 10) }
let(:shipping_benefit) { SolidusPromotions::Benefits::AdjustShipment.new(calculator: ten_off_shipping) }
let(:line_item_benefit) { SolidusPromotions::Benefits::AdjustLineItem.new(calculator: ten_off_items) }
let(:benefits) { [shipping_benefit, line_item_benefit] }
let(:conditions) { [product_condition, shipping_method_condition] }
let!(:promotion) { create(:solidus_promotion, benefits: benefits, name: "10% off Shirts and USPS Shipping", apply_automatically: true) }

before do
shipping_benefit.conditions << shipping_method_condition
line_item_benefit.conditions << product_condition
end

it "can tell us about success" do
subject
expect(promotion.eligibility_results.success?).to be true
end

it "can tell us about errors" do
subject
expect(promotion.eligibility_results.error_messages).to eq(["This coupon code could not be applied to the cart at this time."])
end
end

context "with no conditions" do
let(:conditions) { [] }

it "has no errors for this promo" do
subject
expect(promotion.eligibility_results.error_messages).to be_empty
end
end

context "with an ineligible order-level condition" do
let(:mug) { create(:product) }
let(:order_condition) { SolidusPromotions::Conditions::NthOrder.new(preferred_nth_order: 2) }
let(:line_item_condition) { SolidusPromotions::Conditions::LineItemProduct.new(products: [mug]) }
let(:conditions) { [order_condition, line_item_condition] }

it "can tell us about success" do
subject
expect(promotion.eligibility_results.success?).to be false
end

it "can tell us about all the errors", :pending do
subject
expect(promotion.eligibility_results.error_messages).to eq(
[
"This coupon code could not be applied to the cart at this time.",
"You need to add an applicable product before applying this coupon code."
]
)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
end
end

context "when on a dry run" do
context "when on a dry run", :silence_deprecations do
let(:dry_run_promotion) { promotion }

subject do
Expand Down
Loading