Adjustments

This guide covers the use of adjustments in Spree. After reading it, you should be familiar with:

  • The important role calculators play in the creation of charges and credits.
  • The basic role adjustments play in Spree
  • How to create your own custom calculators and adjustments

1 Overview

Adjustments play a crucial role in Spree. They are the means by which an order total changes to reflect any type of change that is necessary after calculating the item_total. They are the building blocks for creating shipping and tax-related charges as well as for applying discounts for promotions and coupons.

Adjustments can be either positive or negative. Adjustments with a positive value are sometimes referred to as “charges” while adjustments with a negative value are sometimes referred to as “credits.” These are just terms of convenience since there is only one Spree::Adjustment model in Spree which handles this by allowing either positive or negative values.

Prior to Spree 0.30.x there were separate Credit and Charge models. This distinction has been abandoned since it made things unnecessarily complicated.

2 Totals

Spree has several important total values that are recorded at the Spree::Order level. The following table represents a list of attributes which are available to the Spree application and stored in the database for reporting purposes.

Name Description
item_total The sum total of all line items (price * quantity for all line items)
adjustment_total The sum total of all adjustments (which can be positive or negative.) Examples include shipping charges and coupon credits.
total This represents the final total of the order (item_total + adjustment_total).
payment_total The sum total of all valid payments (including possible negative payments due to credit card refunds.) This total automatically excludes payments in the processing, pending and failed states.

There are also a few interesting “calculated totals.” These totals are not stored in the database (and thus not easily searchable) but they can be displayed on a per record basis through the Ruby methods provided by Spree::Order.

Name Description
ship_total The sum total of all adjustments where the adjustment label = ‘shipping’
tax_total The sum total of all adjustments where the adjustment label = ‘tax’

3 Types of Adjustments

Spree supports several types of adjustments – in fact, it supports an open-ended number of adjustment types to handle pretty much any type of situation. From a programming standpoint these are all represented by a single Spree::Adjustment class with a label attribute used to identify what type of adjustment it is.

The following Ruby console output helps to illustrate this fact.


>> Spree::Adjustment.last
=> #<Spree::Adjustment id: 684130, source_id: 968652773, amount: #<BigDecimal:7fff25851228,'0.5E1',9(36)>, label: "Shipping", source_type: "Spree::Shipment", adjustable_id: 0, created_at: "2012-03-25 04:12:08", updated_at: "2012-03-25 04:12:08", mandatory: true, locked: true, originator_id: 574015644, originator_type: "Spree::ShippingMethod", eligible: true, adjustable_type: "Spree::Order">

This may seem somewhat simplistic at first but our experience has shown that most scenarios can be handled with adequate labels and a few other tricks (including providing some handy scopes at the model level.)

3.1 Taxation

Tax adjustments are any adjustments with a label of “Tax.” This is just a naming convention but its a standard enough case that we have a built in scope for them in Spree. You can use the following console command to list an order’s tax adjustments:


>> Spree::Order.last.adjustments.tax
=> [ ... ]

Tax related charges are considered frozen by default.

3.2 Shipping

Shipping adjustments are any adjustments with a label of “Shipping.” This is just a naming convention but its a standard enough case that we have a built in scope for them in Spree. You can use the following console command to list an order’s shipping adjustments:


>> Spree::Order.last.adjustments.shipping
=> [ ... ]

Shipping related charges are considered frozen by default.

3.3 Coupons and Promotions

[TODO – provide a brief summary and link to new promotions documentation here once that is complete]

4 Calculating Adjustments

Adjustments are typically calculated according to a specified set of rules (although it is possible to create adjustments with a fixed, arbitrary amount.) The following sections will detail the use of Spree::Calculators as well as the rules for when calculations are performed.

4.1 Calculators

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

4.2 Registration

The core calculators for Spree are stored in the app/models/spree/calculator directory. There are several calculators included that meet many of the standard store owner needs. Developers are encouraged to write their own extensions to supply additional functionality or to consider using a third party extension written by members of the Spree community.

Calculators need to be “registered” with Spree in order to be made available in the admin interface for various configuration options. The recommended approach for doing this is via an extension. Custom calculators will typically be written as extensions so you need to add some registration logic to the extension containing the calculator. This will allow the calculator to do a one time registration during the standard extension activation process.

Spree itself contains a good example of how this can be achieved. For instance, in the spree_core gem there is logic to register calculators. The following code can be found in the Rails spree.register.calculators initializer defined in spree/core/lib/spree/core/engine.rb:


initializer 'spree.register.calculators' do |app|
  app.config.spree.calculators.shipping_methods = [
      Calculator::FlatPercentItemTotal,
      Calculator::FlatRate,
      Calculator::FlexiRate,
      Calculator::PerItem,
      Calculator::PriceBucket]

   app.config.spree.calculators.tax_rates = [
      Calculator::SalesTax,
      Calculator::Vat]
end

This registers calculators to the Rails.application.config.spree.calculators array. Extension authors should be sure to register any new calculators within the Rails.application.config.spree.calculators by defining new class of aray in lib/extension_name/engine.rb.


app.config.spree.calculators.add_class('product_customization_types') 

app.config.spree.calculators.product_customization_types = [ 
  Calculator::CustomCalculatorOne, 
  Calculator::CustomCalculatorTwo 
] 

If you do not intend to distribute your calculator as an extension you can simply register it inside a Rails initializer spree.register.calculators or the Rails.application.config.spree.calculators method of the default site engine.

Spree automatically configures your calculators for you when using the basic install and/or third party extensions. This discussion is intended to help developers and others interested in understanding the design decisions surrounding calculators.

The register method is removed. Calculators are now registered by appending custom array to Rails.application.config.spree.calculators.

4.3 Spree::CalculatedAdjustments Module

Spree includes a helpful Spree::CalculatedAdjustments module that can be used to introduce calculator-like functionality into a Rails model. Classes that require this type of functionality simply need to declare calculated_adjustments in their class definition.


module Spree
  class ShippingMethod < ActiveRecord::Base
    ...
    calculated_adjustments
    ...
  end
end
4.3.1 Class Methods

When a Spree class uses this helper method it automatically adds the following class functionality:

  • The ability to associate a calculator with an instance of that class (through a has_one :calculator association)
  • Validation rules and the ability to accept configuration params through Rails accepts_nested_attributes_for

Lets look at how this works with a specific example taken from the Spree source code itself. Specifically we’ll be looking at Spree::ShippingMethod.


module Spree
  class ShippingMethod < ActiveRecord::Base
    ...
    calculated_adjustments
    ...
  end
end

Here we see that Spree::ShippingMethod declares that its going to be involved in calculations by declaring calculated_adjustments in the class definition. What does that give us exactly?

Lets start by firing up the Ruby console and creating a new instance of Spree::ShippingMethod.


>> s = Spree::ShippingMethod.new
=> #<Spree::ShippingMethod id: nil, name: nil, zone_id: nil, created_at: nil, updated_at: nil, display_on: nil, shipping_category_id: nil, match_none: nil, match_all: nil, match_one: nil>

Now we can verify that this instance of Spree::ShippingMethod in fact has a Spree::Calculator association (although its currently nil because we haven’t assigned anything to it.)


>> s.calculator
=> nil

If you are creating new Rails models you should never declare has_one :calculator in order to take advantage of calculators. Just use calculated_adjustments instead since you get this association and several other goodies for free. You’ll also be doing things the standard Spree way which will make it easier to maintain over the long run.

Prior to Spree 1.0, classes that extend Spree::Calculator that declares calculated_adjustments can be registered with self.register method. This method is removed in favor of registering classes by via Rails.application.config.spree.calculators. Let’s look at how spree_core engine define the registration in lib/extension_name/engine.rb:


initializer 'spree.register.calculators' do |app|
  app.config.spree.calculators.shipping_methods = [
      Spree::Calculator::FlatPercentItemTotal,
      Spree::Calculator::FlatRate,
      Spree::Calculator::FlexiRate,
      Spree::Calculator::PerItem,
      Spree::Calculator::PriceSack
  ]

  ....
end

You see that the initializer define a class shipping_methods for app.config.spree.calculators. The shipping_method is an array enlist all shipping calculators classes. This registers the calculator with Spree but the following line registers the calculator with Spree::ShippingMethod as well.

You may wonder to yourself “why is that important?” The answer is that you can perform operations such as the following:


>> Spree::ShippingMethod.calculators
=> { ... }
>> Spree::ShippingMethod.calculators.size
=> 5

By registering your calculators in this way, then they will become available as options in the appropriate admin screens.

Choosing a calculator

4.3.2 Instance Methods

Use of the calculated_adjustments helper also provides additional methods to instances of the class declaring it. Specifically they gain the following instance methods:

Method Name Description
create_adjustment Responsible for creating a new adjustment for the specified ‘target’
update_adjustment Updates the amount of the adjustment using the instance’s Calculator
calculator_type Utility method to help with admin screens that need to set the Calculator for a particular instance of the class.

The create_adjustment is the process by which all adjustments should be created. Lets take a look at how this mechanism works inside the Spree::Order class.


def create_tax_charge!
  # destroy any previous adjustments (eveything is recalculated from scratch)
  adjustments.tax.each { |e| e.destroy }
  price_adjustments.each { |p| p.destroy }

  Spree::TaxRate.match(self).each { |rate| rate.adjust(self) }
end

Here we see that an instance of Spree::TaxRate is being used to create the adjustment for the order. Spree::TaxRate declares calculated_adjustments in its class definition which makes this all possible.

You do not need to understand this process to make use of adjustments in Spree. The detailed information provided here for those who wish to understand the inner workings of Spree a little better. If you are interested in creating a new Spree extension that will create or affect adjustments in some way, this understanding is crucial.

4.4 Computation

Computation for adjustments comes down to a very straightforward compute method that is provided by an instance of Spree::Calculator. In some cases the calculation can be quite straight forward. Take a look at Spree::Calculator::FlatRate to see such a simple case.


def compute(object = nil)
  self.preferred_amount
end

This calculator basically just returns the same amount every time its called (note the amount is configurable of course.) Other calculators (such as Spree::Calculator::DefaultTax) are a bit more interesting.


def compute(computable)
  case computable
    when Spree::Order
      compute_order(computable)
    when Spree::LineItem
      compute_line_item(computable)
  end
end

private

  def compute_order(order)
    matched_line_items = order.line_items.select do |line_item|
      line_item.product.tax_category == rate.tax_category
    end

    line_items_total = matched_line_items.sum(&:total)
    round_to_two_places(line_items_total * rate.amount)
  end

  def compute_line_item(line_item)
    if line_item.product.tax_category == rate.tax_category
      deduced_total_by_rate(line_item.total, rate)
    else
      0
    end
  end

You see the compute is more complex with polymorphic handler.

Calculators should never return a value of nil in their compute method. Return 0 in these situations instead.

4.5 Locked Adjustments

Spree also has the notion of so-called “locked” adjustments. The concept behind this is there are certain types of adjustments which should never be changed once calculated. There are also situations where it is appropriate for an adjustment that was once subject to recalculation to be recalculated.

Locked adjustments are currently considered experimental. We’re currently exploring the ability to configure Spree to honor certain locking strategies. For instance, locking a promotion calculation in place so that its not lost if the order is updated after the promotion expires. There are also some interesting options to allow admins with the appropriate permissions to have the ability to lock/unlock adjustments when appropriate.

5 Configuration

Since calculators are an instances of ActiveRecord::Base they can be configured with preferences. Each instance of Spree::ShippingMethod is now stored in the database along with the configured values for its preferences. This allows the same calculator (ex. Spree::Calculator::FlatRate) to be used with multiple Spree::ShippingMethods, and yet each can be configured with different values (ex. different amounts per calculator.)

Calculators are configured using Spree’s flexible preference system. Default values for the preferences are configured through the class definition. For example, the flat rate calculator class definition specifies an amount with a default value of 0.


module Spree
  class Calculator::FlatRate < Calculator
    preference :amount, :decimal, :default => 0
    ...
  end
end

Spree now contains a standard mechanism by which calculator preferences can be edited. The screenshot below shows how the amounts for the flat rate calculator are editable directly in the admin interface.

Changing Shipping Config

This project is maintained by a core team of developers and is freely available for commercial use under the terms of the New BSD License.