Skip to content

Latest commit

 

History

History
286 lines (195 loc) · 14.7 KB

adjustments.textile

File metadata and controls

286 lines (195 loc) · 14.7 KB

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

endprologue.

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 Adjustment model in Spree which handles this by allowing either positive or negative values.

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

Totals

Spree has several important total values that are recorded at the 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 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’

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 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.

>> Adjustment.last
=> #<Adjustment id: 1073088110, order_id: 52160, amount:
#<BigDecimal:2b38e1c62dd0,‘0.0’,9(18)>, label: “Shipping”,
created_at: “2010-11-30 20:22:35”, updated_at: “2010-11-30
20:22:35”, source_id: 43065, source_type: “Shipment”,
mandatory: true, locked: nil, originator_id: 445378512,
originator_type: "ShippingMethod">

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.)

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:

>> Order.last.adjustments.tax
=> [ … ]

INFO. Tax related charges are considered frozen by default.

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:

>> Order.last.adjustments.shipping
=> [ … ]

INFO. Shipping related charges are considered frozen by default.

Coupons and Promotions

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

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 Calculators as well as the rules for when calculations are performed.

Calculators

Spree makes extensive use of the 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 Calculator class and must provide the following methods

def self.description

  1. Human readable description of the calculator
    end

def self.register

  1. Class method for registering the calculator with Spree
    end

def compute(object=nil)

  1. Returns the value after performing the required calculation
    end

Registration

The core calculators for Spree are stored in the app/models/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 activate method defined in spree_core/lib/spree_core.rb:

#register all calculators
[
Calculator::FlatPercentItemTotal,
Calculator::FlatRate,
Calculator::FlexiRate,
Calculator::PerItem,
Calculator::SalesTax,
Calculator::Vat,
Calculator::PriceBucket
].each{|c_model|
begin
c_model.register if c_model.table_exists?
rescue Exception => e
$stderr.puts “Error registering calculator #{c_model}”
end
}

This calls the register method on the calculators that we intend to register. Extension authors should be sure to register any new calculators within the self.activate method defined in lib/your_extension_name.rb. If you do not intend to distribute your calculator as an extension you can simply register it inside a Rails initializer or the self.activate method of the default SpreeSite engine.

INFO. 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.

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.

class ShippingMethod < ActiveRecord::Base

calculated_adjustments

end

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
  • The ability to register Calculators with the class itself (this is different from registering with Spree at large)

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

class ShippingMethod < ActiveRecord::Base

calculated_adjustments

end

Here we see that 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 ShippingMethod.

>> s = ShippingMethod.new
=> #<ShippingMethod id: nil, zone_id: nil, name: nil, created_at: nil, updated_at: nil, display_on: nil>

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

>> s.calculator
=> nil

NOTE. 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.

We can also register classes that extend Calculator with a class that declares calculated_adjustments. Lets look at Calculator::FlatRate to see what this means.

class Calculator::FlatRate < Calculator

def self.register super ShippingMethod.register_calculator(self) end …

end

Like any Calculator there is a self.register method. If you look inside the implementation of that method, however, you see that we call super and which calls the implementation that is automatically provided when you extend Calculator. This registers the calculator with Spree but the following line registers the calculator with ShippingMethod as well.

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

>> ShippingMethod.calculators
=> { … }
>> 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

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 Order class.

def create_tax_charge!
return unless rate = TaxRate.match(bill_address) or adjustments.tax.present?
if old_charge = adjustments.tax.first
old_charge.destroy unless old_charge.originator == rate
return
end
rate.create_adjustment(I18n.t(:tax), self, self, true)
end

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

INFO. 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.

Computation

Computation for adjustments comes down to a very straightforward compute method that is provided by an instance of Calculator. In some cases the calculation can be quite straight forward. Take a look at 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 Calculator::SalesTax) are a bit more interesting.

def compute(order)
rate = self.calculable
line_items = order.line_items.select { |i| i.product.tax_category == rate.tax_category }
line_items.inject(0) {|sum, line_item|
sum += line_item.total * rate.amount
}
end

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

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.

WARNING. 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.

Configuration

Since calculators are an instances of ActiveRecord::Base they can be configured with preferences. Each instance of ShippingMethod is now stored in the database along with the configured values for its preferences. This allows the same calculator (ex. Calculator::FlatRate) to be used with multiple 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.

class Calculator::FlatRate < Calculator preference :amount, :decimal, :default => 0 … 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