Class: SolidusPromotions::Condition Abstract

Inherits:
Spree::Base
  • Object
show all
Includes:
Spree::Preferences::Persistable
Defined in:
app/models/solidus_promotions/condition.rb

Overview

This class is abstract.

Subclass and override #applicable? and #eligible? to implement a custom condition.

Base class for all promotion conditions.

Conditions determine whether a promotion is eligible to be applied to a specific promotable object (such as an order or line item). Each condition subclass implements the eligibility logic and specifies what type of objects it can be applied to.

Conditions work at different levels:

  • Order-level conditions (include OrderLevelCondition): Check entire orders

  • Line item-level conditions (include LineItemLevelCondition): Check individual line items

  • Hybrid conditions (include LineItemApplicableOrderLevelCondition): Check orders but can also filter which line items are eligible

Examples:

Creating an order-level condition

class MinimumPurchaseCondition < Condition
  include OrderLevelCondition

  preference :minimum_amount, :decimal, default: 50.00

  def eligible?(order, _options = {})
    if order.item_total < preferred_minimum_amount
      eligibility_errors.add(:base, "Order total too low")
    end
    eligibility_errors.empty?
  end
end

Creating a line item-level condition

class SpecificProductCondition < Condition
  include LineItemLevelCondition

  preference :product_id, :integer

  def eligible?(line_item, _options = {})
    if line_item.product_id != preferred_product_id
      eligibility_errors.add(:base, "Wrong product")
    end
    eligibility_errors.empty?
  end
end

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.applicable_to(classes) ⇒ Array<SolidusPromotions::Condition>

Returns the subset of conditions that can compute eligibility instances of the provided array of classes.

Examples:

SolidusPromotions::Condition.applicable_to([Spree::Order, Spree::LineItem])

Parameters:

  • An (Array<Class>)

    array of classes to compute eligibility for

Returns:



64
65
66
67
68
# File 'app/models/solidus_promotions/condition.rb', line 64

def applicable_to(classes)
  SolidusPromotions.config.conditions.select do |condition_class|
    (condition_class.instance_methods & classes.map { |k| eligible_method_for(k) }).any?
  end
end

.eligible_method_for(promotable_class) ⇒ Symbol

Generates the eligibility method name for a promotable

Returns:

  • (Symbol)

    the method name



73
74
75
# File 'app/models/solidus_promotions/condition.rb', line 73

def eligible_method_for(promotable_class)
  :"#{promotable_class.name.demodulize.underscore}_eligible?"
end

.inherited(klass) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'app/models/solidus_promotions/condition.rb', line 125

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

Instance Method Details

#applicable?(promotable) ⇒ Boolean

Determines if this condition can be applied to a given promotable object.

Examples:

Condition applicability

condition.applicable?(order)      # => true
condition.applicable?(line_item)  # => false

Parameters:

  • _promotable (Object)

    The object to check (e.g., Spree::Order, Spree::LineItem)

Returns:

  • (Boolean)

    true if this condition applies to the promotable type



102
103
104
# File 'app/models/solidus_promotions/condition.rb', line 102

def applicable?(promotable)
  respond_to?(eligible_method_for(promotable))
end

#eligibility_errorsActiveModel::Errors

Returns an errors object for tracking eligibility failures.

When #eligible? determines that a promotable doesn’t meet the condition, it should add descriptive errors to this object. These errors are used to provide feedback about why a promotion isn’t being applied.

Examples:

Adding an eligibility error

def eligible?(order, _options = {})
  if order.item_total < 50
    eligibility_errors.add(:base, "Minimum order is $50", error_code: :item_total_too_low)
  end
  eligibility_errors.empty?
end

Returns:

  • (ActiveModel::Errors)

    An errors collection for this condition



176
177
178
# File 'app/models/solidus_promotions/condition.rb', line 176

def eligibility_errors
  @eligibility_errors ||= ActiveModel::Errors.new(self)
end

#eligible?(promotable) ⇒ Boolean

Determines if the promotable object meets this condition’s eligibility requirements.

This typically dispatches to a specific eligibility method defined on a subclass, such as ‘#order_eligible?` or `line_item_eligible?`.

Parameters:

  • _promotable (Object)

    The object to evaluate (e.g., Spree::Order, Spree::LineItem)

  • _options (Hash)

    Additional options for eligibility checking

Returns:

  • (Boolean)

    true if the promotable meets the condition, false otherwise

See Also:



117
118
119
120
121
122
123
# File 'app/models/solidus_promotions/condition.rb', line 117

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

#levelObject

Raises:

  • (NotImplementedError)


156
157
158
# File 'app/models/solidus_promotions/condition.rb', line 156

def level
  raise NotImplementedError, "level should be implemented in a sub-class of SolidusPromotions::Condition"
end

#preload_relationsArray<Symbol>

Returns relations that should be preloaded for this condition.

Override this method in subclasses to specify associations that should be eager loaded to avoid N+1 queries when evaluating conditions.

Examples:

Preloading products association

def preload_relations
  [:products]
end

Returns:

  • (Array<Symbol>)

    An array of association names to preload



89
90
91
# File 'app/models/solidus_promotions/condition.rb', line 89

def preload_relations
  []
end

#to_partial_pathString

Returns the partial path for rendering this condition in the admin interface.

Examples:

# For SolidusPromotions::Conditions::ItemTotal
# => "solidus_promotions/admin/condition_fields/item_total"

Returns:

  • (String)

    The path to the admin form partial for this condition



187
188
189
# File 'app/models/solidus_promotions/condition.rb', line 187

def to_partial_path
  "solidus_promotions/admin/condition_fields/#{model_name.element}"
end

#updateable?Boolean

Determines if this condition can be updated in the admin interface.

A condition is considered updateable if it has any preferences that can be configured.

Returns:

  • (Boolean)

    true if the condition has configurable preferences



196
197
198
# File 'app/models/solidus_promotions/condition.rb', line 196

def updateable?
  preferences.any?
end