Skip to content
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
14 changes: 10 additions & 4 deletions promotions/MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,14 @@ If you have custom promotion rules or actions, you need to create new conditions

In our experience, using the three actions can do almost all the things necessary, since they are customizable using calculators.

Rules share a lot of the previous API. If you make use of `#actionable?`, you might want to migrate your rule to be a line-item level rule:
Rules share a lot of the previous API. If you make use of `#actionable?`, you might want to migrate your rule to be a combined order and line-item level rule:

```rb
class MyRule < Spree::PromotionRule
def eligible?(order)
order.total > 100
end

def actionable?(promotable)
promotable.quantity > 3
end
Expand All @@ -110,10 +114,12 @@ would become:

```rb
class MyCondition < SolidusPromotions::Condition
include LineItemLevelCondition
def order_eligible?(order, _options = {})
order.total > 100
end

def eligible?(promotable)
promotable.quantity > 3
def line_item_eligible?(line_item, _options = {})
line_item.quantity > 3
end
end
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ def self.included(klass)
end

def applicable?(promotable)
promotable.is_a?(Spree::Order) || preferred_line_item_applicable && promotable.is_a?(Spree::LineItem)
end

def eligible?(promotable)
send(:"#{promotable.class.name.demodulize.underscore}_eligible?", promotable)
promotable.is_a?(Spree::LineItem) ? preferred_line_item_applicable && super : super
end

def level
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@
module SolidusPromotions
module Conditions
module LineItemLevelCondition
def applicable?(promotable)
promotable.is_a?(Spree::LineItem)
def self.included(base)
def base.method_added(method)
if method == :eligible?
Spree.deprecator.warn <<~MSG
Defining `eligible?` on a promotion along with including the `LineItemLevelCondition` module is deprecated.
Rename `eligible?` to `line_item_eligible?` and stop including the `LineItemLevelCondition` module.
MSG
define_method(:applicable?) do |promotable|
promotable.is_a?(Spree::LineItem)
end
end

super
end
end

def level
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@
module SolidusPromotions
module Conditions
module OrderLevelCondition
def applicable?(promotable)
promotable.is_a?(Spree::Order)
def self.included(base)
def base.method_added(method)
if method == :eligible?
Spree.deprecator.warn <<~MSG
Defining `eligible?` on a promotion along with including the `OrderLevelCondition` module is deprecated.
Rename `eligible?` to `order_eligible?` and stop including the `OrderLevelCondition` module.
MSG
define_method(:applicable?) do |promotable|
promotable.is_a?(Spree::Order)
end
end

super
end
end

def level
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@
module SolidusPromotions
module Conditions
module ShipmentLevelCondition
def applicable?(promotable)
promotable.is_a?(Spree::Shipment)
def self.included(base)
def base.method_added(method)
if method == :eligible?
Spree.deprecator.warn <<~MSG
Defining `eligible?` on a promotion along with including the `ShipmentLevelCondition` module is deprecated.
Rename `eligible?` to `shipment_eligible?` and stop including the `ShipmentLevelCondition` module.
MSG
define_method(:applicable?) do |promotable|
promotable.is_a?(Spree::Shipment)
end
end

super
end
end

def level
Expand Down
84 changes: 49 additions & 35 deletions promotions/app/models/solidus_promotions/condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,58 +71,65 @@ def preload_relations

# Determines if this condition can be applied to a given promotable object.
#
# This method is typically implemented by including one of the level modules
# (OrderLevelCondition, LineItemLevelCondition, or LineItemApplicableOrderLevelCondition)
# rather than being overridden directly.
#
# @param _promotable [Object] The object to check (e.g., Spree::Order, Spree::LineItem)
#
# @return [Boolean] true if this condition applies to the promotable type
#
# @raise [NotImplementedError] if not implemented in subclass
#
# @example Order-level condition applicability
# @example Condition applicability
# condition.applicable?(order) # => true
# condition.applicable?(line_item) # => false
#
# @see OrderLevelCondition
# @see LineItemLevelCondition
# @see LineItemApplicableOrderLevelCondition
def applicable?(_promotable)
raise NotImplementedError, "applicable? should be implemented in a sub-class of SolidusPromotions::Condition"
def applicable?(promotable)
respond_to?(eligible_method_for(promotable))
end

# Determines if the promotable object meets this condition's eligibility requirements.
#
# This is the core method that implements the condition's logic. When the promotable
# is not eligible, this method should add errors to {#eligibility_errors} explaining why.
# This typically dispatches to a specific eligibility method defined on a subclass, such as
# `#order_eligible?` or `line_item_eligible?`.
#
# @param _promotable [Object] The object to evaluate (e.g., Spree::Order, Spree::LineItem)
# @param _options [Hash] Additional options for eligibility checking
#
# @return [Boolean] true if the promotable meets the condition, false otherwise
#
# @raise [NotImplementedError] if not implemented in subclass
#
# @example Order total condition
# def eligible?(order, _options = {})
# if order.item_total < preferred_minimum
# eligibility_errors.add(:base, "Order total too low")
# end
# eligibility_errors.empty?
# end
#
# @example First order condition
# def eligible?(order, _options = {})
# if order.user.orders.complete.count > 1
# eligibility_errors.add(:base, "Not first order")
# end
# eligibility_errors.empty?
# end
#
# @see #eligibility_errors
def eligible?(_promotable, _options = {})
raise NotImplementedError, "eligible? should be implemented in a sub-class of SolidusPromotions::Condition"
def eligible?(promotable, ...)
if applicable?(promotable)
send(eligible_method_for(promotable), promotable, ...)
else
raise NotImplementedError, "Please implement #{eligible_method_for(promotable)} in your condition"
end
end

def self.inherited(klass)
def klass.method_added(method_added)
if method_added == :eligible?
Spree.deprecator.warn <<~MSG
Please refactor `#{name}`. You're defining `eligible?`. Instead, define method for each type of promotable
that your condition can be applied to. For example:
```
class MyCondition < SolidusPromotions::Condition
def applicable?(promotable)
promotable.is_a?(Spree::Order)
end

def eligible?(order)
order.total > 20
end
```
can now become
```
class MyCondition < SolidusPromotions::Condition
def order_eligible?(order)
order.total > 20
end
end
```
MSG
end
super
end
super
end

def level
Expand Down Expand Up @@ -170,6 +177,13 @@ def updateable?

private

# Generates the eligibility method name for a promotable
#
# @return [Symbol] the method name
def eligible_method_for(promotable)
:"#{promotable.class.name.demodulize.underscore}_eligible?"
end

# Validates that only one instance of this condition type exists per benefit.
#
# Prevents duplicate conditions of the same type from being added to a single benefit.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
module SolidusPromotions
module Conditions
class FirstOrder < Condition
# TODO: Remove in Solidus 5
include OrderLevelCondition

attr_reader :user, :email

def eligible?(order, options = {})
def order_eligible?(order, options = {})
@user = order.try(:user) || options[:user]
@email = order.email

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module SolidusPromotions
module Conditions
class FirstRepeatPurchaseSince < Condition
# TODO: Remove in Solidus 5
include OrderLevelCondition

preference :days_ago, :integer, default: 365
Expand All @@ -12,7 +13,7 @@ class FirstRepeatPurchaseSince < Condition
#
# This is eligible if the user's most recently completed order is more than the preferred days ago
# @param order [Spree::Order]
def eligible?(order, _options = {})
def order_eligible?(order, _options = {})
return false unless order.user

last_order = last_completed_order(order.user)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Conditions
# To add extra operators please override `self.operators_map` or any other helper method.
# To customize the error message you can also override `ineligible_message`.
class ItemTotal < Condition
# TODO: Remove in Solidus 5
include OrderLevelCondition

preference :amount, :decimal, default: 100.00
Expand All @@ -28,7 +29,7 @@ def self.operator_options
end
end

def eligible?(order, _options = {})
def order_eligible?(order, _options = {})
return false unless order.currency == preferred_currency

unless total_for_order(order).send(operator, threshold)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
module SolidusPromotions
module Conditions
class LineItemOptionValue < Condition
# TODO: Remove in Solidus 5
include LineItemLevelCondition

preference :eligible_values, :hash

def eligible?(line_item, _options = {})
def line_item_eligible?(line_item, _options = {})
pid = line_item.product.id
ovids = line_item.variant.option_values.pluck(:id)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module SolidusPromotions
module Conditions
# A condition to apply a promotion only to line items with or without a chosen product
class LineItemProduct < Condition
# TODO: Remove in Solidus 5
include LineItemLevelCondition

MATCH_POLICIES = %w[include exclude].freeze
Expand All @@ -23,7 +24,7 @@ def preload_relations
[:products]
end

def eligible?(line_item, _options = {})
def line_item_eligible?(line_item, _options = {})
order_includes_product = product_ids.include?(line_item.variant.product_id)
success = inverse? ? !order_includes_product : order_includes_product

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module SolidusPromotions
module Conditions
class LineItemTaxon < Condition
# TODO: Remove in Solidus 5
include LineItemLevelCondition

has_many :condition_taxons,
Expand All @@ -22,7 +23,7 @@ def preload_relations
[:taxons]
end

def eligible?(line_item, _options = {})
def line_item_eligible?(line_item, _options = {})
found = Spree::Classification.where(
product_id: line_item.variant.product_id,
taxon_id: condition_taxon_ids_with_children
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module Conditions
# it to a simple quantity check across the entire order which would be
# better served by an item total condition.
class MinimumQuantity < Condition
# TODO: Remove in Solidus 5
include OrderLevelCondition

validates :preferred_minimum_quantity, numericality: { only_integer: true, greater_than: 0 }
Expand All @@ -26,7 +27,7 @@ class MinimumQuantity < Condition
#
# @param order [Spree::Order] the order we want to check eligibility on
# @return [Boolean] true if promotion is eligible, false otherwise
def eligible?(order)
def order_eligible?(order, _options = {})
if benefit.applicable_line_items(order).sum(&:quantity) < preferred_minimum_quantity
eligibility_errors.add(
:base,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
module SolidusPromotions
module Conditions
class NthOrder < Condition
# TODO: Remove in Solidus 5
include OrderLevelCondition

preference :nth_order, :integer, default: 2
Expand All @@ -14,7 +15,7 @@ class NthOrder < Condition
#
# Use the first order condition if you want a promotion to be applied to the first order for a user.
# @param order [Spree::Order]
def eligible?(order, _options = {})
def order_eligible?(order, _options = {})
return false unless order.user

nth_order?(order)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
module SolidusPromotions
module Conditions
class OneUsePerUser < Condition
# TODO: Remove in Solidus 5
include OrderLevelCondition

def eligible?(order, _options = {})
def order_eligible?(order, _options = {})
if order.user.present?
if promotion.used_by?(order.user, [order])
eligibility_errors.add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ class OptionValue < Condition

preference :eligible_values, :hash

def order_eligible?(order)
def order_eligible?(order, _options = {})
order.line_items.any? { |item| line_item_eligible?(item) }
end

def line_item_eligible?(line_item)
def line_item_eligible?(line_item, _options = {})
LineItemOptionValue.new(preferred_eligible_values: preferred_eligible_values).eligible?(line_item)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def eligible_products
products
end

def order_eligible?(order)
def order_eligible?(order, _options = {})
return true if eligible_products.empty?

case preferred_match_policy
Expand Down
Loading
Loading