Unfun with Drupal discounts and taxes

A 5 minute read written by Kim May 4, 2015

An illustration of a blueberry cheesecake with a slice cut out

Unless you’re an accountant or magician, the mere mention of the word “taxes” might induce anxiety or the need for excessive cheesecake eating (OK – maybe that’s just me). But that’s why add-ons like Drupal Commerce exist: to do the math for you.

As exciting as taxes sounds (no offence, accountant friends), this blog post is actually about tricky interactions between the commerce framework, custom pricing rules, taxes, and discounts. That’s a little more interesting, right?

What is Drupal Commerce?

Drupal Commerce is a content-driven highly extensible add-on to the Drupal CMS. The most basic implementation of Drupal Commerce is built with a few modules, but it can be expanded to the needs of organizations.

From shopping carts to shipping

The basic commerce module consists of product configuration, order management, a shopping cart, taxes, payment integration, and customer profiles. Each of these is a submodule that’s provided out of the box; however, these do not all necessarily need to be used.

On the flip side, additional contributed or custom modules can be added to extend the basic functionality even further. Some examples of additional modules include discounts and coupon codes, fees, shipping, donation forms, registration, additional payment options, and more. Due to the open-source nature of Drupal, any highly customized features required can be easily added and integrated with the system.

Everything doesn’t add up

The out-of-the-box coupon discounts and tax modules work pretty great, and with a patch you can properly apply tax before the discount (the default is the opposite). However, how tax and coupon discounts should work together properly has been an ongoing discussion since 2012, which partly leads to my problem.

The out-of-the-box is great, and I generally try to extend these when creating a custom coupon, but they don’t always fit the bill. It’s hard to figure out exactly how to get all the components on the order screen to play together nicely and add up properly. We definitely want our math to be correct for the sake of our customers and our tax auditors.

Understanding the pricing structure

At first, it seems relatively easy to decipher the pricing structure. A product has a price, you add the product to the cart, taxes and discounts get calculated, and voila – purchase made!

Unfortunately, appearances can be deceptive. Behind the scenes, a line item unit price is actually made up of various components. There’s the base price that comes from the product (if applicable), the tax from the tax module, any discounts that are applied, shipping, and so on. All items are tallied towards the calculation of a product’s price. Then all the “calculate the sell price of a product” rules are fired. So rather than tallying the product prices then applying tax and discounts to the whole order, Drupal Commerce does the calculation on a per line item basis.

For the most part this is fine. A 20 per cent coupon takes off the correct amount from each line item and adds that as a coupon discount component to each line item. A fee line item is added with just a base price.

But what if we have altered our line items to be different amounts depending on the quantity added? Or what if we want to give a user X free products for every Y she purchases? (How is this not a module that exists? I’ll be cleaning up and uploading a version soon for others to use.) This is where the pricing gets a little hairy and definitely unfun. We’re already manipulating the line item prices and we need our tax to recalculate.

Forcing a fix

I first tried to force the tax to recalculate when I applied the discount. This “sort of” worked, but due to the amount of price calculation rules firing (on a very complex commerce system) this led to a few frustrating days (and me eating a whole turtle cheesecake).

It seems obvious now, but because we have the line item, we have all the price components. It is much easier to manipulate the data at the line item level than it is at the whole order level. Just telling your order to go recalculate the tax doesn’t really work, especially if you already have custom pricing rules. Using the “calculate the sell price of a product” rule can be really handy, or if you are creating a custom discount or coupon code you can do this in a custom action.

Making rules

Sometimes we don’t need to create custom code, and this can still be achieved with rules. In the case of my Buy X Get Y Free coupon code (for distinct line items), we actually just need to set the price of the coupon line item to 0. This is easily done with the “set a data value” option.

If this is done in custom code, sometimes we can just simply call the function and apply tax to the line item. This is how I achieved tax calculation for my custom admin fee for transactions. It wasn’t a product, so initially it wasn’t being included in the tax calculation. Here’s how it looks:

$gst = commerce_tax_rate_load('gst');
commerce_tax_rate_apply($gst, $line_item);

But again, we’re dealing with complex discounts here. Sometimes we need to do a little more math. In that case, we don’t need to actually change the existing components – we can just add an adjustment component! Are you having fun yet?

Pushing it further

For this example, I extended the pricing attributes module to allow the user to specify a particular price per quantity of item. In general, the user sets the base price to 0 then specifies a set price for each quantity value.

Because the base price is 0, tax will always be 0 for these items. Then, on top of that, apply a percentage discount that generally comes off of the base price component. Twenty percent off of 0 is still always 0.

Without pasting all my code here, the first step was calculating the sell price of a product, based on the quantity in the cart and adding the correct price to the line item. Rather than trying to re-calculate the line item price when a discount is applied, I check to see if the order has a discount or coupon within this function. Once I load the discount rate I can take the amount and spread it across the line items to calculate the new discounted price. This is done by adding both a discount adjustment and a tax adjustment component to the line item, which can be achieved using the commerce_price_component_add function.

$price_component_name = 'tax|gst';
$adjusted_tax = $discounted_price * $gst['rate'];

$line_item_wrapper->commerce_unit_price->data = commerce_price_component_add(
    $line_item_wrapper->commerce_unit_price->value(),
    $price_component_name,
    array(
        'amount' => $adjusted_tax,
        'currency_code' => $line_item_wrapper->commerce_unit_price->currency_code->value(),
        'data' => array(),
    ),
    FALSE,
    FALSE
);

The line item might end up having multiple base price and multiple tax price components, but that’s OK! That’s why the price was made up of components in the first place. We don’t know what will be needed in price calculations, so it is flexible enough to allow duplicate and custom component types to be added to the data array.

Generally, it is advisable to use the tools that are already available to you before trying a custom implementation. In this particular situation it got a little crazy trying to sort out all the custom discounts with the custom product pricing rules. I suggest trying to stick with the easiest solution first by using rules to configure and modify prices first before jumping to a custom solution. But being able to extend and add custom modules and discounts is also part of what makes Drupal so great to work with. Maybe taxes can be fun after all?