diff --git a/promotions/app/models/solidus_promotions/calculators/percent.rb b/promotions/app/models/solidus_promotions/calculators/percent.rb index 8f3c799f9f2..26761143dfe 100644 --- a/promotions/app/models/solidus_promotions/calculators/percent.rb +++ b/promotions/app/models/solidus_promotions/calculators/percent.rb @@ -4,14 +4,45 @@ module SolidusPromotions module Calculators + # A calculator that applies a percentage-based discount. + # + # This calculator computes the discount as a percentage of the item's discountable amount, + # rounded to the appropriate currency precision. + # + # @example + # calculator = Percent.new(preferred_percent: 15) + # # Line item with discountable_amount of $100 + # calculator.compute_item(line_item) # => 15.00 (15% of $100) class Percent < Spree::Calculator include PromotionCalculator preference :percent, :decimal, default: 0 - def compute(object) + # Computes the percentage-based discount for an item. + # + # Calculates the discount by applying the preferred percentage to the item's + # discountable amount, then rounds the result to the appropriate precision + # for the order's currency. + # + # @param object [Object] The object to calculate the discount for (e.g., LineItem, Shipment, ShippingRate) + # + # @return [BigDecimal] The discount amount, rounded to the order's currency precision + # + # @example Computing a 20% discount on a $50 line item + # calculator = Percent.new(preferred_percent: 20) + # line_item.discountable_amount # => 50.00 + # calculator.compute_item(line_item) # => 10.00 + # + # @example Computing a 15% discount on a shipment + # calculator = Percent.new(preferred_percent: 15) + # shipment.discountable_amount # => 25.00 + # calculator.compute_item(shipment) # => 3.75 + def compute_item(object) round_to_currency(object.discountable_amount * preferred_percent / 100, object.order.currency) end + alias_method :compute_line_item, :compute_item + alias_method :compute_shipment, :compute_item + alias_method :compute_shipping_rate, :compute_item end end end diff --git a/promotions/app/models/solidus_promotions/calculators/percent_with_cap.rb b/promotions/app/models/solidus_promotions/calculators/percent_with_cap.rb index 55066da2027..06ca7dd4f06 100644 --- a/promotions/app/models/solidus_promotions/calculators/percent_with_cap.rb +++ b/promotions/app/models/solidus_promotions/calculators/percent_with_cap.rb @@ -2,11 +2,52 @@ module SolidusPromotions module Calculators - class PercentWithCap < Percent + # A calculator that applies a percentage-based discount with a maximum cap. + # + # This calculator computes a discount as a percentage of the line item's discountable amount, + # but limits the total discount to a maximum amount distributed across all applicable line items. + # The actual discount applied is the lesser of the percentage discount and the proportional + # share of the maximum cap. + # + # @example + # calculator = PercentWithCap.new(preferred_percent: 20, preferred_max_amount: 50) + # # Line item with $100 discountable amount + # # Percentage would be $20 (20% of $100) + # # But if the max cap distributes only $15 to this item, it gets $15 + class PercentWithCap < Spree::Calculator + include PromotionCalculator + + preference :percent, :decimal, default: 0 preference :max_amount, :integer, default: 100 - def compute(line_item) - percent_discount = super + # Computes the discount for a line item, capped at a maximum amount. + # + # Calculates both a percentage-based discount and a distributed maximum discount, + # then returns whichever is smaller. This ensures the discount never exceeds + # the line item's proportional share of the maximum cap, even if the percentage + # would result in a larger discount. + # + # @param line_item [Spree::LineItem] The line item to calculate the discount for + # + # @return [BigDecimal] The discount amount, limited by both the percentage and the max cap + # + # @example Computing discount when percentage is lower than cap + # calculator = PercentWithCap.new(preferred_percent: 10, preferred_max_amount: 100) + # line_item.discountable_amount # => 50.00 + # # Percent discount: $5 (10% of $50) + # # Max distributed: $25 (assuming equal distribution) + # calculator.compute_line_item(line_item) # => 5.00 + # + # @example Computing discount when cap is lower than percentage + # calculator = PercentWithCap.new(preferred_percent: 50, preferred_max_amount: 10) + # line_item.discountable_amount # => 100.00 + # # Percent discount: $50 (50% of $100) + # # Max distributed: $10 (assuming single line item) + # calculator.compute_line_item(line_item) # => 10.00 + # + # @see DistributedAmount + def compute_line_item(line_item) + percent_discount = round_to_currency(line_item.discountable_amount * preferred_percent / 100, line_item.order.currency) max_discount = DistributedAmount.new( calculable:, preferred_amount: preferred_max_amount diff --git a/promotions/spec/models/solidus_promotions/calculators/percent_spec.rb b/promotions/spec/models/solidus_promotions/calculators/percent_spec.rb index aa03277db72..27418c03d6a 100644 --- a/promotions/spec/models/solidus_promotions/calculators/percent_spec.rb +++ b/promotions/spec/models/solidus_promotions/calculators/percent_spec.rb @@ -6,13 +6,26 @@ RSpec.describe SolidusPromotions::Calculators::Percent, type: :model do context "compute" do let(:currency) { "USD" } - let(:order) { double(currency: currency) } - let(:line_item) { double("Spree::LineItem", discountable_amount: 100, order: order) } + let(:order) { Spree::Order.new(currency:) } + let(:item) { Spree::LineItem.new(price: 9.99, quantity: 10, order: order) } + let(:calculator) { described_class.new(preferred_percent: 15) } - before { subject.preferred_percent = 15 } + subject { calculator.compute(item) } it "computes based on item price and quantity" do - expect(subject.compute(line_item)).to eq 15 + expect(subject).to eq 14.99 + end + + context "with a shipment" do + let(:item) { build(:shipment, cost: 29) } + + it { is_expected.to eq(4.35) } + end + + context "with a shipping rate" do + let(:item) { build(:shipping_rate, cost: 38) } + + it { is_expected.to eq(5.70) } end end