Skip to content

Commit e4ee677

Browse files
authored
Merge pull request #6359 from mamhoff/add-yard-docs-to-promotions-benefit
Add YARD docs to SolidusPromotions::Benefit
2 parents ca16162 + 092dfd4 commit e4ee677

File tree

1 file changed

+112
-4
lines changed
  • promotions/app/models/solidus_promotions

1 file changed

+112
-4
lines changed

promotions/app/models/solidus_promotions/benefit.rb

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,90 @@
11
# frozen_string_literal: true
22

33
module SolidusPromotions
4-
# Base class for all types of benefit.
4+
# Base class for all promotion benefits.
55
#
6-
# Benefits perform the necessary tasks when a promotion is activated
7-
# by an event and determined to be eligible.
6+
# A Benefit is the active part of a promotion: once a promotion becomes
7+
# eligible for a given promotable (order, line item, or shipment), the benefit
8+
# determines how much discount to apply and produces the corresponding
9+
# adjustments.
10+
#
11+
# Subclasses specialize the discounting target (orders, line items, or
12+
# shipments) and usually include one of the following mixins to integrate with
13+
# Solidus' adjustment system:
14+
# - SolidusPromotions::Benefits::OrderBenefit
15+
# - SolidusPromotions::Benefits::LineItemBenefit
16+
# - SolidusPromotions::Benefits::ShipmentBenefit
17+
#
18+
# A benefit can discount any object for which {#can_discount?} returns true.
19+
# Implementors must provide a calculator via Spree::CalculatedAdjustments and
20+
# may override methods such as {#adjustment_label}.
21+
#
22+
# Usage example
23+
#
24+
# benefit = SolidusPromotions::Benefits::AdjustLineItem.new(promotion: promo)
25+
# if benefit.can_discount?(line_item)
26+
# discount = benefit.discount(line_item)
27+
# # => #<SolidusPromotions::ItemDiscount ...>
28+
# end
29+
#
30+
# @see SolidusPromotions::Promotion
31+
# @see Spree::CalculatedAdjustments
832
class Benefit < Spree::Base
933
include Spree::Preferences::Persistable
1034
include Spree::CalculatedAdjustments
1135
include Spree::AdjustmentSource
36+
1237
before_destroy :remove_adjustments_from_incomplete_orders
1338
before_destroy :raise_for_adjustments_for_completed_orders
1439

40+
# @!attribute [rw] promotion
41+
# The owning promotion.
42+
# @return [SolidusPromotions::Promotion]
1543
belongs_to :promotion, inverse_of: :benefits
44+
# @!attribute [rw] original_promotion_action
45+
# Back-reference to the original Solidus (Spree) promotion action, when migrated.
46+
# @return [Spree::PromotionAction, nil]
1647
belongs_to :original_promotion_action, class_name: "Spree::PromotionAction", optional: true
48+
# @!attribute [r] adjustments
49+
# Adjustments created by this benefit.
50+
# @return [ActiveRecord::Associations::CollectionProxy<Spree::Adjustment>]
1751
has_many :adjustments, class_name: "Spree::Adjustment", as: :source, dependent: :restrict_with_error
52+
# @!attribute [r] shipping_rate_discounts
53+
# Shipping-rate-level discounts generated by this benefit.
54+
# @return [ActiveRecord::Associations::CollectionProxy<SolidusPromotions::ShippingRateDiscount>]
1855
has_many :shipping_rate_discounts, class_name: "SolidusPromotions::ShippingRateDiscount", inverse_of: :benefit, dependent: :restrict_with_error
56+
# @!attribute [r] conditions
57+
# Conditions attached to this benefit.
58+
# @return [ActiveRecord::Associations::CollectionProxy<SolidusPromotions::Condition>]
1959
has_many :conditions, class_name: "SolidusPromotions::Condition", inverse_of: :benefit, dependent: :destroy
2060

61+
# @!method self.of_type(type)
62+
# Restricts benefits to the given STI type(s).
63+
# @param type [String, Symbol, Class, Array<String,Symbol,Class>] a single type or list of types
64+
# @return [ActiveRecord::Relation<SolidusPromotions::Benefit>]
2165
scope :of_type, ->(type) { where(type: Array.wrap(type).map(&:to_s)) }
2266

67+
# Returns relations that should be preloaded for this condition.
68+
#
69+
# Override this method in subclasses to specify associations that should be eager loaded
70+
# to avoid N+1 queries when evaluating conditions.
71+
#
72+
# @return [Array<Symbol>] An array of association names to preload
2373
def preload_relations
2474
[:calculator]
2575
end
2676

77+
# Whether this benefit can discount the given object.
78+
#
79+
# Subclasses must implement this according to the kinds
80+
# of objects they are able to discount.
81+
#
82+
# @param object [Object] a potential adjustable (order, line item, or shipment)
83+
# @return [Boolean]
84+
# @raise [NotImplementedError] when not implemented by the subclass/mixin
85+
# @see SolidusPromotions::Benefits::OrderBenefit,
86+
# SolidusPromotions::Benefits::LineItemBenefit,
87+
# SolidusPromotions::Benefits::ShipmentBenefit
2788
def can_discount?(object)
2889
raise NotImplementedError, "Please implement the correct interface, or include one of the `SolidusPromotions::Benefits::OrderBenefit`, " \
2990
"`SolidusPromotions::Benefits::LineItemBenefit` or `SolidusPromotions::Benefits::ShipmentBenefit` modules"
@@ -57,12 +118,23 @@ def discount(adjustable, ...)
57118
)
58119
end
59120

60-
# Ensure a negative amount which does not exceed the object's amount
121+
# Computes the discount amount for the given adjustable.
122+
#
123+
# Ensures the returned amount is negative and does not exceed the
124+
# adjustable's discountable amount.
125+
#
126+
# @param adjustable [#discountable_amount] the adjustable to compute for
127+
# @param ... [args, kwargs] additional arguments forwarded to the calculator
128+
# @return [BigDecimal] a negative amount suitable for creating an adjustment
61129
def compute_amount(adjustable, ...)
62130
promotion_amount = calculator.compute(adjustable, ...) || Spree::ZERO
63131
[adjustable.discountable_amount, promotion_amount.abs].min * -1
64132
end
65133

134+
# Builds the localized label for adjustments created by this benefit.
135+
#
136+
# @param adjustable [Object]
137+
# @return [String]
66138
def adjustment_label(adjustable)
67139
I18n.t(
68140
"solidus_promotions.adjustment_labels.#{adjustable.class.name.demodulize.underscore}",
@@ -71,6 +143,9 @@ def adjustment_label(adjustable)
71143
)
72144
end
73145

146+
# Partial path used for admin forms for this benefit type.
147+
#
148+
# @return [String]
74149
def to_partial_path
75150
"solidus_promotions/admin/benefit_fields/#{model_name.element}"
76151
end
@@ -80,14 +155,30 @@ def level
80155
"`SolidusPromotions::Benefits::LineItemBenefit` or `SolidusPromotions::Benefits::ShipmentBenefit` modules"
81156
end
82157

158+
# Returns the set of condition classes that can still be attached to this benefit.
159+
# Already-persisted conditions are excluded.
160+
#
161+
# @return [Set<Class<SolidusPromotions::Condition>>]
83162
def available_conditions
84163
possible_conditions - conditions.select(&:persisted?)
85164
end
86165

166+
# Returns the calculators allowed for this benefit type.
167+
#
168+
# @return [Array<Class>] calculator classes
87169
def available_calculators
88170
SolidusPromotions.config.promotion_calculators[self.class] || []
89171
end
90172

173+
# Verifies if the promotable satisfies all applicable conditions of this benefit.
174+
#
175+
# When dry_run is true, an {SolidusPromotions::EligibilityResults} entry is
176+
# recorded for each condition with success/error details; otherwise, the
177+
# evaluation short-circuits on the first failure.
178+
#
179+
# @param promotable [Object] the entity being evaluated (e.g., Spree::Order, Spree::LineItem)
180+
# @param dry_run [Boolean] whether to collect detailed eligibility information
181+
# @return [Boolean] true when all applicable conditions are eligible
91182
def eligible_by_applicable_conditions?(promotable, dry_run: false)
92183
applicable_conditions = conditions.select do |condition|
93184
condition.applicable?(promotable)
@@ -116,18 +207,35 @@ def eligible_by_applicable_conditions?(promotable, dry_run: false)
116207
end.all?
117208
end
118209

210+
# All line items of the order that are eligible for this benefit.
211+
#
212+
# @param order [Spree::Order]
213+
# @return [Array<Spree::LineItem>] eligible line items
119214
def applicable_line_items(order)
120215
order.discountable_line_items.select do |line_item|
121216
eligible_by_applicable_conditions?(line_item)
122217
end
123218
end
124219

220+
# Base set of order-level condition classes available to all benefits.
221+
#
222+
# These generic order conditions apply regardless of the concrete benefit
223+
# type, as every benefit ultimately operates within the context of an order.
224+
# Concrete benefit subclasses may extend or override this to include
225+
# additional applicable conditions that are specific to their discount
226+
# target (e.g., line-item or shipment conditions).
227+
#
228+
# @return [Set<Class<SolidusPromotions::Condition>>]
125229
def possible_conditions
126230
Set.new(SolidusPromotions.config.order_conditions)
127231
end
128232

129233
private
130234

235+
# Prevents destroying a benefit when it has adjustments on completed orders.
236+
#
237+
# Adds an error and aborts the destroy callback chain when such adjustments exist.
238+
# @api private
131239
def raise_for_adjustments_for_completed_orders
132240
if adjustments.joins(:order).merge(Spree::Order.complete).any?
133241
errors.add(:base, :cannot_destroy_if_order_completed)

0 commit comments

Comments
 (0)