Class: SolidusPromotions::Benefit
- Inherits:
-
Spree::Base
- Object
- Spree::Base
- SolidusPromotions::Benefit
- Includes:
- Spree::AdjustmentSource, Spree::CalculatedAdjustments, Spree::Preferences::Persistable
- Defined in:
- app/models/solidus_promotions/benefit.rb
Overview
Base class for all promotion benefits.
A Benefit is the active part of a promotion: once a promotion becomes eligible for a given promotable (order, line item, or shipment), the benefit determines how much discount to apply and produces the corresponding adjustments.
Subclasses specialize the discounting target (orders, line items, or shipments) and usually include one of the following mixins to integrate with Solidus’ adjustment system:
-
SolidusPromotions::Benefits::AdjustLineItem
-
SolidusPromotions::Benefits::AdjustShipment
-
SolidusPromotions::Benefits::CreateDiscountedItem
A benefit can discount any object for which #can_discount? returns true. Implementors must provide a calculator via Spree::CalculatedAdjustments and may override methods such as #adjustment_label.
Usage example
benefit = SolidusPromotions::Benefits::AdjustLineItem.new(promotion: promo)
if benefit.can_discount?(line_item)
discount = benefit.discount(line_item)
# => #<SolidusPromotions::ItemDiscount ...>
end
Direct Known Subclasses
SolidusPromotions::Benefits::AdjustLineItem, SolidusPromotions::Benefits::AdjustShipment, SolidusPromotions::Benefits::AdvertisePrice, SolidusPromotions::Benefits::CreateDiscountedItem
Instance Attribute Summary collapse
-
#adjustments ⇒ ActiveRecord::Associations::CollectionProxy<Spree::Adjustment>
readonly
Adjustments created by this benefit.
-
#conditions ⇒ ActiveRecord::Associations::CollectionProxy<SolidusPromotions::Condition>
readonly
Conditions attached to this benefit.
-
#original_promotion_action ⇒ Spree::PromotionAction?
Back-reference to the original Solidus (Spree) promotion action, when migrated.
-
#promotion ⇒ SolidusPromotions::Promotion
The owning promotion.
-
#shipping_rate_discounts ⇒ ActiveRecord::Associations::CollectionProxy<SolidusPromotions::ShippingRateDiscount>
readonly
Shipping-rate-level discounts generated by this benefit.
Class Method Summary collapse
-
.applicable_conditions ⇒ Enumerable<Class<SolidusPromotions::Condition>>
Base set of order-level condition classes available to all benefits.
- .inherited(klass) ⇒ Object
-
.of_type(type) ⇒ ActiveRecord::Relation<SolidusPromotions::Benefit>
Restricts benefits to the given STI type(s).
Instance Method Summary collapse
-
#adjustment_label(adjustable) ⇒ String
Builds the localized label for adjustments created by this benefit.
-
#applicable_line_items(order) ⇒ Array<Spree::LineItem>
All line items of the order that are eligible for this benefit.
-
#available_calculators ⇒ Array<Class>
Returns the calculators allowed for this benefit type.
-
#available_conditions ⇒ Set<Class<SolidusPromotions::Condition>>
Returns the set of condition classes that can still be attached to this benefit.
-
#can_discount?(object) ⇒ Boolean
Whether this benefit can discount the given object.
-
#compute_amount(adjustable) ⇒ BigDecimal
Computes the discount amount for the given adjustable.
-
#discount(adjustable) ⇒ Spree::Adjustment, ...
Calculates and returns a discount for the given adjustable object.
-
#eligible_by_applicable_conditions?(promotable, dry_run: false) ⇒ Boolean
Verifies if the promotable satisfies all applicable conditions of this benefit.
- #level ⇒ Object
-
#possible_conditions ⇒ Set<Class<SolidusPromotions::Condition>>
Base set of order-level condition classes available to all benefits.
-
#preload_relations ⇒ Array<Symbol>
Returns relations that should be preloaded for this condition.
-
#to_partial_path ⇒ String
Partial path used for admin forms for this benefit type.
Instance Attribute Details
#adjustments ⇒ ActiveRecord::Associations::CollectionProxy<Spree::Adjustment> (readonly)
Adjustments created by this benefit.
51 |
# File 'app/models/solidus_promotions/benefit.rb', line 51 has_many :adjustments, class_name: "Spree::Adjustment", as: :source, dependent: :restrict_with_error |
#conditions ⇒ ActiveRecord::Associations::CollectionProxy<SolidusPromotions::Condition> (readonly)
Conditions attached to this benefit.
59 |
# File 'app/models/solidus_promotions/benefit.rb', line 59 has_many :conditions, class_name: "SolidusPromotions::Condition", inverse_of: :benefit, dependent: :destroy |
#original_promotion_action ⇒ Spree::PromotionAction?
Back-reference to the original Solidus (Spree) promotion action, when migrated.
47 |
# File 'app/models/solidus_promotions/benefit.rb', line 47 belongs_to :original_promotion_action, class_name: "Spree::PromotionAction", optional: true |
#promotion ⇒ SolidusPromotions::Promotion
The owning promotion.
43 |
# File 'app/models/solidus_promotions/benefit.rb', line 43 belongs_to :promotion, inverse_of: :benefits |
#shipping_rate_discounts ⇒ ActiveRecord::Associations::CollectionProxy<SolidusPromotions::ShippingRateDiscount> (readonly)
Shipping-rate-level discounts generated by this benefit.
55 |
# File 'app/models/solidus_promotions/benefit.rb', line 55 has_many :shipping_rate_discounts, class_name: "SolidusPromotions::ShippingRateDiscount", inverse_of: :benefit, dependent: :restrict_with_error |
Class Method Details
.applicable_conditions ⇒ Enumerable<Class<SolidusPromotions::Condition>>
Base set of order-level condition classes available to all benefits.
These generic order conditions apply regardless of the concrete benefit type, as every benefit ultimately operates within the context of an order. Concrete benefit subclasses may extend or override this to include additional applicable conditions that are specific to their discount target (e.g., line-item or shipment conditions).
76 77 78 |
# File 'app/models/solidus_promotions/benefit.rb', line 76 def self.applicable_conditions SolidusPromotions::Condition.applicable_to([Spree::Order]) end |
.inherited(klass) ⇒ Object
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 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'app/models/solidus_promotions/benefit.rb', line 129 def self.inherited(klass) def klass.method_added(method_added) if method_added == :discount Spree.deprecator.warn <<~MSG Please refactor `#{name}`. You're defining `#discount`. Instead, define a method for each type of discountable that your benefit can discount. For example: ``` class MyBenefit < SolidusPromotions::Benefit def can_discount?(discountable) discountable.is_a?(Spree::LineItem) end def discount(order, _options = {}) amount = compute_amount(line_item, ...) return if amount.zero? ItemDiscount.new( item: line_item, label: adjustment_label(line_item), amount: amount, source: self ) end ``` can now become ``` class MyBenefit < SolidusPromotions::Benefit def discount_line_item(order, ...) amount = compute_amount(line_item, ...) return if amount.zero? ItemDiscount.new( item: line_item, label: adjustment_label(line_item), amount: amount, source: self ) end end ``` MSG end super end super end |
.of_type(type) ⇒ ActiveRecord::Relation<SolidusPromotions::Benefit>
Restricts benefits to the given STI type(s).
65 |
# File 'app/models/solidus_promotions/benefit.rb', line 65 scope :of_type, ->(type) { where(type: Array.wrap(type).map(&:to_s)) } |
Instance Method Details
#adjustment_label(adjustable) ⇒ String
Builds the localized label for adjustments created by this benefit.
This method attempts to use a calculator-specific label method if available, falling back to a localized string key based on the adjustable’s class name.
## Calculator Override
Calculators can provide custom labels by implementing a method named after the adjustable type. For example, a calculator that discounts line items could implement ‘line_item_adjustment_label`:
The method name follows the pattern: ‘adjustable_type_adjustment_label` where `adjustable_type` is the underscored class name of the adjustable (e.g., `line_item`, `shipment`, `shipping_rate`).
If the calculator does not respond to the expected method, the benefit will fall back to using an i18n translation key based on the adjustable’s class.
223 224 225 226 227 228 229 230 231 232 233 |
# File 'app/models/solidus_promotions/benefit.rb', line 223 def adjustment_label(adjustable, ...) if calculator.respond_to?(adjustment_label_method_for(adjustable)) calculator.send(adjustment_label_method_for(adjustable), adjustable, ...) else I18n.t( "solidus_promotions.adjustment_labels.#{adjustable.class.name.demodulize.underscore}", promotion: SolidusPromotions::Promotion.model_name.human, promotion_customer_label: promotion.customer_label ) end end |
#applicable_line_items(order) ⇒ Array<Spree::LineItem>
All line items of the order that are eligible for this benefit.
301 302 303 304 305 |
# File 'app/models/solidus_promotions/benefit.rb', line 301 def applicable_line_items(order) order.discountable_line_items.select do |line_item| eligible_by_applicable_conditions?(line_item) end end |
#available_calculators ⇒ Array<Class>
Returns the calculators allowed for this benefit type.
259 260 261 |
# File 'app/models/solidus_promotions/benefit.rb', line 259 def available_calculators SolidusPromotions.config.promotion_calculators[self.class] || [] end |
#available_conditions ⇒ Set<Class<SolidusPromotions::Condition>>
Returns the set of condition classes that can still be attached to this benefit. Already-persisted conditions are excluded.
252 253 254 |
# File 'app/models/solidus_promotions/benefit.rb', line 252 def available_conditions self.class.applicable_conditions - conditions.select(&:persisted?) end |
#can_discount?(object) ⇒ Boolean
Whether this benefit can discount the given object.
Subclasses must implement this according to the kinds of objects they are able to discount.
100 101 102 |
# File 'app/models/solidus_promotions/benefit.rb', line 100 def can_discount?(object) respond_to?(discount_method_for(object)) end |
#compute_amount(adjustable) ⇒ BigDecimal
Computes the discount amount for the given adjustable.
Ensures the returned amount is negative and does not exceed the adjustable’s discountable amount.
184 185 186 187 |
# File 'app/models/solidus_promotions/benefit.rb', line 184 def compute_amount(adjustable, ...) promotion_amount = calculator.compute(adjustable, ...) || Spree::ZERO [adjustable.discountable_amount, promotion_amount.abs].min * -1 end |
#discount(adjustable) ⇒ Spree::Adjustment, ...
Calculates and returns a discount for the given adjustable object.
This method computes the discount amount using the benefit’s calculator and returns an ItemDiscount object representing the discount to be applied. If the computed amount is zero, no discount is returned.
121 122 123 124 125 126 127 |
# File 'app/models/solidus_promotions/benefit.rb', line 121 def discount(adjustable, ...) if can_discount?(adjustable) send(discount_method_for(adjustable), adjustable, ...) else raise NotImplementedError, "Please implement #{discount_method_for(adjustable)} in your condition" end end |
#eligible_by_applicable_conditions?(promotable, dry_run: false) ⇒ Boolean
Verifies if the promotable satisfies all applicable conditions of this benefit.
When dry_run is true, an EligibilityResults entry is recorded for each condition with success/error details; otherwise, the evaluation short-circuits on the first failure.
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'app/models/solidus_promotions/benefit.rb', line 272 def eligible_by_applicable_conditions?(promotable, dry_run: false) conditions.map do |condition| next unless condition.applicable?(promotable) eligible = condition.eligible?(promotable) break [false] if !eligible && !dry_run if dry_run if condition.eligibility_errors.details[:base].first code = condition.eligibility_errors.details[:base].first[:error_code] = condition.eligibility_errors..first end promotion.eligibility_results.add( item: promotable, condition: condition, success: eligible, code: eligible ? nil : (code || :coupon_code_unknown_error), message: eligible ? nil : ( || I18n.t(:coupon_code_unknown_error, scope: [:solidus_promotions, :eligibility_errors])) ) end eligible end.compact.all? end |
#level ⇒ Object
242 243 244 245 |
# File 'app/models/solidus_promotions/benefit.rb', line 242 def level raise NotImplementedError, "Please implement the correct interface, or include one of the `SolidusPromotions::Benefits::OrderBenefit`, " \ "`SolidusPromotions::Benefits::LineItemBenefit` or `SolidusPromotions::Benefits::ShipmentBenefit` modules" end |
#possible_conditions ⇒ Set<Class<SolidusPromotions::Condition>>
Base set of order-level condition classes available to all benefits.
These generic order conditions apply regardless of the concrete benefit type, as every benefit ultimately operates within the context of an order. Concrete benefit subclasses may extend or override this to include additional applicable conditions that are specific to their discount target (e.g., line-item or shipment conditions).
316 317 318 319 |
# File 'app/models/solidus_promotions/benefit.rb', line 316 def possible_conditions Spree.deprecator.warn("Use #{self.class.name}.applicable_conditions instead.") self.class.applicable_conditions end |
#preload_relations ⇒ Array<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 computing discounts or performing automations.
86 87 88 |
# File 'app/models/solidus_promotions/benefit.rb', line 86 def preload_relations [:calculator] end |
#to_partial_path ⇒ String
Partial path used for admin forms for this benefit type.
238 239 240 |
# File 'app/models/solidus_promotions/benefit.rb', line 238 def to_partial_path "solidus_promotions/admin/benefit_fields/#{model_name.element}" end |