docs/developer/core-concepts/calculators.mdx
erDiagram
Calculator {
string type
string calculable_type
text preferences
}
TaxRate {
string name
decimal amount
boolean included_in_price
}
ShippingMethod {
string name
string display_on
}
PromotionAction {
string type
}
Adjustment {
decimal amount
string label
}
TaxRate ||--|| Calculator : "has one"
ShippingMethod ||--|| Calculator : "has one"
PromotionAction ||--|| Calculator : "has one"
TaxRate ||--o{ Adjustment : "creates"
ShippingMethod ||--o{ ShippingRate : "calculates"
PromotionAction ||--o{ Adjustment : "creates"
Key relationships:
Spree makes extensive use of the Spree::Calculator model and there are several subclasses provided to deal with various types of calculations flat rate, percentage discount, sales tax, VAT, etc. All calculators extend the Spree::Calculator class and must provide the following methods:
def self.description
# Human readable description of the calculator
end
def compute(object=nil)
# Returns the value after performing the required calculation
end
Calculators link to a calculable object, which are typically one of Spree::ShippingMethod, Spree::TaxRate, or Spree::Promotion::Actions::CreateAdjustment. These three classes use the Spree::CalculatedAdjustments module described below to provide an easy way to calculate adjustments for their objects.
The following are descriptions of the currently available calculators in Spree. If you would like to add your own, please see the Creating a New Calculator section.
For information about this calculator, please read the Taxes guide.
This calculator has one preference: flat_percent and can be set like this:
calculator.preferred_flat_percent = 10
This calculator takes an order and calculates an amount using this calculation:
[item total] x [flat percentage]
For example, if an order had an item total of $31 and the calculator was configured to have a flat percent amount of 10, the discount would be $3.10, because $31 x 10% = $3.10.
This calculator can be used to provide a flat rate discount.
This calculator has two preferences: amount and currency. These can be set like this:
calculator.preferred_amount = 10
calculator.preferred_currency = "USD"
The currency for this calculator is used to check to see if a shipping method is available for an order. If an order's currency does not match the shipping method's currency, then that shipping method will not be displayed on the frontend.
This calculator can take any object and will return simply the preferred amount.
This calculator is typically used for promotional discounts when you want a specific discount for the first product, and then subsequent discounts for other products, up to a certain amount.
This calculator takes three preferences:
first_item: The discounted price of the first items.additional_item: The discounted price of subsequent items.max_items: The maximum number of items this discount applies to.The calculator computes based on this:
[first item discount] + (([items_count*] - 1) x [additional item discount])
max_itemsThus, if you have ten items in your shopping cart, your first_item preference is set to $10, your additional_items preference is set to $5, and your max_items preference is set to 4, the total discount would be $25:
$10 for the first item$5 for each of the 3 subsequent items: $5 \* 3 = $15$0 for the remaining 6 itemsThe Per Item calculator (Spree::Calculator::Shipping::PerItem) is a shipping calculator that charges a flat amount for every item in a shipment.
This calculator takes two preferences:
amount: The flat amount charged per item.currency: The currency for this calculator.It computes a flat rate per item by multiplying the amount preference by the total item quantity in the shipment package:
[amount] x [total item quantity in package]
For example, with an amount of 5 and a package containing 3 items in total, the calculator computes an amount of 15 (5 x 3).
The Percent Per Item calculator (Spree::Calculator::PercentOnLineItem) applies a percentage discount to a single line item. It takes two preferences:
percent: The percentage to apply to the line item's amount.apply_only_on_full_priced_items: When enabled, skips line items that are already on sale.For each line item, the calculator computes line item amount x percent / 100, capped at the line item's amount so a promotion adjustment never pushes the total negative.
For example, a $30 line item at 10% yields a $3 discount ($30 x 10% = $3).
The Price Sack calculator is useful for when you want to provide a discount for an order which is over a certain price. The calculator has four preferences:
minimal_amount: The minimum amount for the line items total to trigger the calculator.discount_amount: The amount to discount from the order if the line items total is equal to or greater than the minimal_amount.normal_amount: The amount to discount from the order if the line items total is less than the minimal_amount.currency: The currency for this calculator. Defaults to the store currencySuppose you have a Price Sack calculator with a minimal_amount preference of $50, a normal_amount preference of $2, and a discount_amount of $5. An order with a line items total of $60 would result in a discount of $5 for the whole order. An order of $20 would result in a discount of $2.
To create a new calculator for Spree, you need to do two things. The first is to inherit from the Spree::Calculator class and define description and compute methods on that class:
class CustomCalculator < Spree::Calculator
def self.description
# Human readable description of the calculator
end
def compute(object=nil)
# Returns the value after performing the required calculation
end
end
If you are creating a new calculator for shipping methods, please be aware that you need to inherit from Spree::ShippingCalculator instead, and define a compute_package method:
class CustomCalculator < Spree::ShippingCalculator
def self.description
# Human readable description of the calculator
end
def compute_package(package)
# Returns the value after performing the required calculation
end
end
The second thing is to register this calculator as a tax, shipping, or promotion adjustment calculator by calling code like this at the end of config/initializers/spree.rb inside your application config variable defined for brevity:
For example if your calculator is placed in app/models/spree/calculator/shipping/my_own_calculator.rb you should call:
By default, all shipping method calculators are available at all times. If you wish to make this dependent on something from the order, you can re-define the available? method inside your calculator:
class CustomCalculator < Spree::Calculator
def available?(object)
object.currency == "USD"
end
end
If you wish to use Spree's calculator functionality for your own application, you can include the Spree::CalculatedAdjustments module into a model of your choosing.
class Plan < ActiveRecord::Base
include Spree::CalculatedAdjustments
end
To have calculators available for this class, you will need to register them. Spree.calculators is a fixed-member struct (SpreeCalculators, defined in spree/core/lib/spree/core/engine.rb) that exposes only the four built-in buckets:
shipping_methodstax_ratespromotion_actions_create_adjustmentspromotion_actions_create_item_adjustmentsPlan.calculators internally calls Spree.calculators.send(:plans) (the tableized model name), so both registration and lookup raise NoMethodError until you extend the struct to add a matching plans member. Ruby structs cannot gain members at runtime, so this means redefining the SpreeCalculators struct (or reassigning Spree.calculators to an object that responds to plans) before you register anything onto it.
Once the struct exposes a plans member you can register calculators:
Then you can access these calculators by calling this method:
Plan.calculators
Using this method, you can then display the calculators as you please. Each object for this new class will need to have a calculator associated so that adjustments can be calculated on them.
Spree::CalculatedAdjustments provides a has_one :calculator association (with accepts_nested_attributes_for and a presence validation), delegates compute to that calculator, exposes a with_calculator scope, calculator_type / calculator_type= accessors, and the Plan.calculators registry shown above. To work out what the calculator would compute an amount to be, call compute on an instance:
plan.compute(<calculable object>)
The module does not define create_adjustment, update_adjustment, or compute_amount. If you also need to build adjustments, include Spree::AdjustmentSource, which adds create_adjustment(order, adjustable, included = false). That method calls a compute_amount you define on the including model (as Spree::TaxRate and the Spree::Promotion::Actions classes do). There is no update_adjustment method in core.