Tax Proposal Last Update: 7th April, 2005Article ID: 5

• Introduction
• Example Tax Calculations
• The Logic Involved
• Calculating Product Prices
• Development Issues

Introduction

osCommerce is not a solution made just for a specific market such as the American market or the European market, but is a general solution that can be configured to meet the legal requirements of most, if not all, countries. The logic used for the tax implementation is flexible to be able to deal with the taxes involved on a global scale - not only to suit customer needs locally, but also on an international basis as online stores on the Internet are accessible world wide.

Taxes are not only dealt with on a per country and zone basis, but also on a per product and per service basis.

The implementation includes the following key issues:

• Tax free product and service prices
• Flexible tax setup
• Flexible zone setup
• Parameter usage and default values

Tax Free Product And Service Prices

In order to realize a flexible tax implementation, product and service prices that are stored in the database are stored tax free.

This allows for the appropriate taxes to be calculated depending on the customer and customer location, and focus of business (business-to-consumer vs business-to-business).

Prices are stored in the database with 4 decimal places to prevent display discrepancies due to rounding, and provide greater control and precision on how the rounded result values are displayed to the customer.

Flexible Tax Setup

Products and services are assigned one Tax Class. Tax Classes can contain any number of Tax Rates which are to be applied to the price involved.

Tax Rates not only contain the percentage rate of the tax involved, but also the zone in which the tax applies to, and the priority of the tax rate in regard to other rates existing in the same Tax Class.

Priorities play an important role in a Tax Class as it states how the multiple tax rates in the same class are to be treated; either adding each rate together when the priorities are the same, or compounding the rates together in the defined priority order.

For example, when two rates of 7% and 7.5% exist in the same tax class, the end result will be 14.5% when the priorities are the same (0.07 + 0.075), or 15.025% when compounded in order of priority (0.075 x 0.07 + 0.07 + 0.075).

Flexible Zone Setup

Tax Zones are groupped in a country -> zone relationship.

Flexibility is provided when defining countries in a Tax Zone, or zones in a country, by assigning wildcard entries of All Countries or All Zones respectively.

European Tax Zone Example

To apply a tax rate of 17.5% to all European countries, a Tax Zone titled European Union Tax Zone is created which would contain all European countries listed individually, and all country zones setup as All Zones.

A Tax Rate is setup that would contain the European Union Tax Zone zone and a rate of 17.5%.

Customers located in Quebec must pay a federal tax rate of 7% and a compounded local tax rate of 7.5%.

To compound the two rates successfully when appropriate (for Quebec customers only), two Tax Zones need to be setup. The first Tax Zone, titled Canadian Federal Tax Zone, includes a Canada->All Zones relationship and the second Tax Zone, titled Quebec Local Tax Zone, would include a Canada->Quebec relationship.

As the local tax rate is compounded with the federal tax rate, the priorities of the Tax Rates needs to be different.

The first Tax Rate is setup for the Canadian Federal Tax Zone zone with a rate of 7% and a priority of 1, and a second Tax Rate is setup for the Quebec Local Tax Zone zone with a rate of 7.5% and a priority of 2.

As both Tax Rates share the same Tax Class, both will be applied.

Parameter Usage And Default Values

Configuration parameters are used to control how calculations are made in the flexible implementation of taxes. Below is a list of parameters involved and the location in the Administration Tool to configure them.

Display Prices With Tax

Key: DISPLAY_PRICE_WITH_TAX
Location: Configuration -> My Store

This parameter controls whether the prices shown should include the taxes involved.

For North American based stores this parameter should be disabled, whereas for European based stores it should be enabled.

Tax Decimal Places

Key: TAX_DECIMAL_PLACES
Location: Configuration -> My Store

This parameter pads the tax rate percentage to the set decimal places for displaying/cosmetic reasons only.

If numerous tax rates are involved such as 7% and 7.25%, it may be more appealing to set this parameter a value of 2 so that 7% is shown as 7.00%.

Country and Zone

Key: STORE_COUNTRY and STORE_ZONE
Location: Configuration -> My Store

These parameters set the country and zone where the online store is located.

When a guest navigates through the online store, and display prices with tax is enabled, these values are used by default to calculate the tax rates involved as the country and zone of the guest is currently not known.

When the guest logs into the online store, or creates an account, their country and zone values are then taken to base the tax calculations on.

Example Tax Calculations

Below are examples of tax calculations with the expected results produced.

The net prices shown below are tax free prices stored in the database with 4 decimal places. The gross prices shown are the end prices including the taxes applied and formated to a currency with 2 decimal places.

Price (Net): 5.0000
Tax: 7.5%

1.075 x 5.0000 = 5.375

Tax: 0.375
Price (Gross): 5.38

------------------------------------

Price (Net): 4.3103
Tax: 16%

1.16 x 4.3103 = 4.99995

Tax: 0.689648
Price (Gross): 5.00

------------------------------------

Price (Net): 100.0000
Tax: 7% and 7.5% compounded

1.07 x 100.0000 = 107.00

1.075 x 107.00 = 115.025

or

1.075 x 0.07 + 0.075 = 0.15025

1.15025 x 100.0000 = 115.025

Tax: 7.00 + 8.025
Price (Gross): 115.03

The results of the examples above can vary when calculations on quantities are made. End prices will also vary depending on the setting of the DISPLAY_PRICE_WITH_TAX parameter.

When an item is sold for \$10.00, it is expected that quantities of 10 will cost \$100.00, and quantities of 100 will cost \$1000.00; with either taxes included (when DISPLAY_PRICE_WITH_TAX is enabled) or with taxes excluded (when DISPLAY_PRICE_WITH_TAX is disabled).

To produce these results, calculations are first made to retrieve the rounded end price of the single amount, which is then multiplied by the quantity.

Price (Net): 4.3103
Tax: 16%

1.16 x 4.3103 = 4.99995

Tax: 0.689648
Price (Gross): 5.00

With DISPLAY_PRICE_WITH_TAX Disabled

10 x 4.31 = 43.10 (excl. 6.90 tax; end price 50.00)
100 x 4.31 = 431.00 (excl. 68.96 tax; end price 499.96)
1000 x 4.31 = 4310.00 (excl. 689.60 tax; end price 4999.60)

To calculate the tax excluded from the total, the following logic is used (following the 16% example):

tax = 0.16 x total

With DISPLAY_PRICE_WITH_TAX Enabled

10 x 5.00 = 50.00 (incl. 6.90 tax)
100 x 5.00 = 500.00 (incl. 68.97 tax)
1000 x 5.00 = 5000.00 (incl. 689.66 tax)

To calculate the tax included in the total, the following logic is used (following the 16% example):

tax = total - (total / 1.16)

The Logic Involved

Calculating Product Prices

Various functions and classes are used to calculate the end price of a product including or excluding the taxes involved.

The most basic form of displaying a products price is by using the Currencies class as:

``` <?php   \$products_price = 5.00;   \$products_tax_class_id = 1;   \$products_tax = tep_get_tax_rate(\$products_tax_class_id);   echo \$currencies->display_price(\$products_price, \$products_tax); ?> ```

The ID of the assigned products Tax Class is passed to the tep_get_tax_rate() function to calculate the total tax percentage to use on the products price.

The tep_get_tax_rate() function first checks to see if the client is logged onto the online store - if they are, their country and zone values are used, whereas if they are not, the default values of the store country and zone are used.

If the client changes the shipping address during the checkout procedure, the zone values for the new shipping address are used for that order only.

The country and zone values are used to only apply the desired Tax Rates in the assigned product Tax Class.

The tep_get_tax_rate() function also takes into consideration the priorities of the Tax Rates in the assigned products Tax Class, and either adds any multiple existing Tax Rates together, or compounds them together.

The \$products_tax result is then passed to the display_price() method of the Currencies class along with the products price.

The display_price() method accepts three parameters in order:

1. \$products_price
2. \$products_tax
3. \$quantity (default of 1)

The display_price() method calculates the end price of the product and multiples it by the quantity. It then passes the result to the format() method (in the same Currencies class) for currency formatting.

``` <?php   function display_price(\$products_price, \$products_tax, \$quantity = 1) {     \$end_price = tep_add_tax(\$products_price, \$products_tax);     return \$this->format(\$end_price * \$quantity);   } ?> ```

The end price is calculated with the tep_add_tax() function, which takes into consideration the DISPLAY_PRICE_WITH_TAX parameter. If enabled, the end price returned is with the tax included, whereas if it is disabled, the end price returned is without the tax included.

``` <?php   function tep_add_tax(\$price, \$tax) {     global \$currencies;     if ( (DISPLAY_PRICE_WITH_TAX == 'true') && (\$tax > 0) ) {       return tep_round(\$price, \$currencies->currencies[DEFAULT_CURRENCY]['decimal_places']) + tep_calculate_tax(\$price, \$tax);     } else {       return tep_round(\$price, \$currencies->currencies[DEFAULT_CURRENCY]['decimal_places']);     }   } ?> ```

The calculations are rounded to the set decimal places of the default currency.

All internal calculations are made in regard with the default currency. The end price is then calculated to the selected currency if the client has chosen to display prices in a different currency.

This is done with the format() method of the Currencies class.

If DISPLAY_PRICE_WITH_TAX is enabled, the products price is rounded to the currencies decimal places, and is added to the tax rate which the tep_calculate_tax() function returns.

``` <?php   function tep_calculate_tax(\$price, \$tax) {     global \$currencies;     return tep_round(\$price * \$tax / 100, \$currencies->currencies[DEFAULT_CURRENCY]['decimal_places']);   } ?> ```

The tep_calculate_tax() function calculates the tax rate involved and rounds it to the currencies decimal places.

When the products price and tax rate have been rounded, they are added together in the tep_add_tax() function, multiplied by the quantity in the display_price() method of the Currencies class, and passed to the format() method to calculate the end price with the selected currency.

``` <?php   function format(\$number, \$currency_type = '') {     global \$currency;     if (empty(\$currency_type)) \$currency_type = \$currency;     \$rate = \$this->currencies[\$currency_type]['value'];     \$decimal_places = \$this->currencies[\$currency_type]['decimal_places'];     \$decimal_point = \$this->currencies[\$currency_type]['decimal_point'];     \$thousands_point = \$this->currencies[\$currency_type]['thousands_point'];     \$symbol_left = \$this->currencies[\$currency_type]['symbol_left'];     \$symbol_right = \$this->currencies[\$currency_type]['symbol_right'];     \$price = number_format(tep_round(\$number * \$rate, \$decimal_places), \$decimal_places, \$decimal_point, \$thousands_point);     return \$symbol_left . \$price . \$symbol_right;   } ?> ```

The format() method above has been edited to show only the minimum logic involved for a better presentation.

Development Issues

Rounding issues have been experienced which was found in PHPs number_format() function producing different results when floats and strings were parsed.

Due to the rounding issues, a custom function titled tep_round() has been written that rounds a number without using number_format() or round().

The test script below shows how the results of number_format() and tep_round() vary:

``` <?php   function tep_round(\$number, \$precision) {     if (strpos(\$number, '.') && (strlen(substr(\$number, strpos(\$number, '.')+1)) > \$precision)) {       \$number = substr(\$number, 0, strpos(\$number, '.') + 1 + \$precision + 1);       if (substr(\$number, -1) >= 5) {         \$number = substr(\$number, 0, -1) + ('0.' . str_repeat(0, \$precision-1) . '1');       } else {         \$number = substr(\$number, 0, -1);       }     }     return \$number;   }   \$price = '100.0000'; // Canada GST 7%   \$price = '1.07' * \$price; // Canada/Quebec PST 7.5%   \$price = '1.075' * \$price;   echo '\$price: ' . \$price . '<br>' . "n" .        'number_format(\$price, 2): ' . number_format(\$price, 2) . '<br>' . "n" .        'number_format((string)\$price, 2): ' . number_format((string)\$price, 2) . '<br>' . "n" .        'tep_round(\$price, 2): ' . tep_round(\$price, 2);   echo '<br><br>';   \$price = '5.0000';   \$price = '1.085' * \$price;   echo '\$price: ' . \$price . '<br>' . "n" .        'number_format(\$price, 2): ' . number_format(\$price, 2) . '<br>' . "n" .        'number_format((string)\$price, 2): ' . number_format((string)\$price, 2) . '<br>' . "n" .        'tep_round(\$price, 2): ' . tep_round(\$price, 2); ?> ```