In this series, I’ll be explaining the process by which Magento handles calculating and displaying totals in the cart (i.e., subtotal, tax, discount, grand total, etc), and how to customize this process for your own purposes.
At its core, handling product prices and totals in an online shopping cart seems simple. Multiply the cart quantities by product prices. Add shipping. Add tax. And there you have it. The first time you have reason to modify the way cart totals are calculated and displayed in Magento, however, what you’ll find is anything but simple. In fact, your first impulse may be to run far, far away.
Why the complexity? Well, the first and most obvious reason is that even straightforward totals calculations aren’t as simple as they appear. Is shipping a percentage or a flat rate? Is shipping taxed, and where do we get the information on the tax rate that applies to the customer? What kind of discounts are we applying, and is there a limit on the number of items they can be applied to?
And Magento’s robust feature set throws a ton of other factors into the mix: Special prices, tier prices, shopping cart price rules, coupon codes, gift cards . . . Building a stable system that makes all of these factors play nice, and leaves room for customization, is no easy feat. Magento’s technique for totals collection allows calculations to remain nicely segmented into their own modules, and it’s designed to help you keep your sanity, not lose it, once you understand how the process works.
Diving into the code responsible for adding a product to the cart, you’ll discover something peculiar: At no time during the direct execution of Mage_Sales_Model_Quote::addProduct is a price set on the quote item that represents the product in the cart. That’s because, for all the reasons listed above, the final price on an item can’t be determined except in the context of other information about the cart. And so the calculation of each item’s price, and every other piece of price information about the cart, is all done at once, kicked off by one method that’s called before saving the quote at various points in the cart updating and checkout process: Mage_Sales_Model_Quote::collectTotals.
To understand what happens there, let’s back up to see how a “total” is defined. What I’ll call a “total” or “total collector” is a particular pricing calculation that must be done on the cart (subtotal, tax, discount, grand total), and it has a model dedicated to two main jobs:
- Calculating and changing appropriate info on the quote, quote address and quote items
- Dictating how the specific total should be displayed in the cart (or check out, etc), if at all
Totals are defined in configuration XML, so if you take a look at app/code/core/Mage/Sales/etc/config.xml, you’ll find the following:
<global> . . . <sales> <quote> <totals> <nominal> <class>sales/quote_address_total_nominal</class> <before>subtotal</before> </nominal> <subtotal> <class>sales/quote_address_total_subtotal</class> <after>nominal</after> <before>grand_total</before> </subtotal> . . . </totals> </quote> . . . </sales> . . . </global>
Here we see multiple basic totals defined, along with the models that drive them. You can see that “before” and “after” nodes also dictate in what order totals calculations should be run. Browse the config files of other core modules like Mage_SalesRule and Mage_Tax, and you’ll see other totals defined.
Gaining an understanding of the totals collection process is valuable for varying degrees of customization. The obvious application would be if you need to create a new total collector of your own, and it’s important to understand that this need not be used only for a distinct total you want to store and display. You may wish to implement a total collector simply to modify the results of other calculations in the collection process. Or your customizations may be less intrusive, simply observing the “sales_quote_collect_totals_before” event to modify data before the process runs, in which case it’s key to know how that data will be acted upon.
In the next entry, we’ll take a more in-depth look into the config, models, and logic involved with totals collection.