Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@

module SolidusPromotions
module Calculators
# A calculator that applies tiered flat-rate discounts based on discountable amount thresholds.
#
# This calculator allows defining multiple discount tiers where each tier specifies a minimum
# discountable amount threshold and the corresponding discount amount to apply. The calculator
# selects the highest tier that the item qualifies for based on its discountable amount.
#
# If the item doesn't meet any tier threshold, the base amount is used. The discount is only
# applied if the currency matches the preferred currency.
#
# @example Use case: Volume-based shipping discounts
# # Free shipping on orders over $100, $5 off on orders over $50
# calculator = TieredFlatRate.new(
# preferred_base_amount: 0,
# preferred_tiers: {
# 50 => 5, # $5 discount when amount >= $50
# 100 => 15 # $15 discount when amount >= $100
# },
# preferred_currency: 'USD'
# )
#
# @example Use case: Bulk purchase incentives
# # Tiered discounts for line items based on total line value
# calculator = TieredFlatRate.new(
# preferred_base_amount: 2,
# preferred_tiers: {
# 25 => 5, # $5 off when line total >= $25
# 50 => 12, # $12 off when line total >= $50
# 100 => 30 # $30 off when line total >= $100
# },
# preferred_currency: 'USD'
# )
class TieredFlatRate < Spree::Calculator
include PromotionCalculator

Expand All @@ -22,6 +53,40 @@ class TieredFlatRate < Spree::Calculator

validate :preferred_tiers_content

# Computes the tiered flat-rate discount for an item.
#
# Evaluates the item's discountable amount against all defined tiers and selects
# the highest tier threshold that the item meets or exceeds. Returns the discount
# amount associated with that tier, or the base amount if no tier threshold is met.
# Returns 0 if the currency doesn't match.
#
# @param object [Object] The object to calculate the discount for (e.g., LineItem, Shipment)
#
# @return [BigDecimal] The discount amount from the matching tier, base amount, or 0
#
# @example Computing discount with tier matching
# calculator = TieredFlatRate.new(
# preferred_base_amount: 2,
# preferred_tiers: { 25 => 5, 50 => 10, 100 => 20 }
# )
# line_item.discountable_amount # => 75.00
# calculator.compute_item(line_item) # => 10.00 (matches $50 tier)
#
# @example Computing discount below all tiers
# calculator = TieredFlatRate.new(
# preferred_base_amount: 2,
# preferred_tiers: { 25 => 5, 50 => 10 }
# )
# line_item.discountable_amount # => 15.00
# calculator.compute_item(line_item) # => 2.00 (base amount)
#
# @example Computing discount with currency mismatch
# calculator = TieredFlatRate.new(
# preferred_currency: 'USD',
# preferred_tiers: { 50 => 10 }
# )
# line_item.currency # => 'EUR'
# calculator.compute_item(line_item) # => 0
def compute_item(object)
_base, amount = preferred_tiers.sort.reverse.detect do |value, _|
object.discountable_amount >= value
Expand All @@ -38,10 +103,17 @@ def compute_item(object)

private

# Converts a value to a BigDecimal.
#
# @param value [String, Numeric] The value to convert
# @return [BigDecimal] The converted decimal value
def cast_to_d(value)
value.to_s.to_d
end

# Validates that preferred_tiers is a hash with positive numeric keys.
#
# Ensures the tiers preference is properly formatted for tier-based calculations.
def preferred_tiers_content
if preferred_tiers.is_a? Hash
unless preferred_tiers.keys.all? { |key| key.is_a?(Numeric) && key > 0 }
Expand Down