Shipping and Tax Calculations with PHP 5 Ecommerce

Exclusive offer: get 50% off this eBook here
PHP 5 E-commerce Development

PHP 5 E-commerce Development — Save 50%

Create a flexible framework in PHP for a powerful ecommerce solution

$23.99    $12.00
by Michael Peacock | January 2010 | e-Commerce PHP

In this article by Michael Peacock, you will learn about how to process orders for shipping and tax calculations, once your e-commerce store has received the purchase order. The article includes the following topics:

  • How to calculate shipping costs based on Product, Weight, Location, and "Shipping rules"
  • About third-party shipping APIs
  • How to integrate shipping and tracking notifications on orders
  • How to integrate tax costs into our system

Shipping

Shipping is a very important aspect of an e-commerce system; without it customers will not accurately know the cost of their order. The only situation where we wouldn't want to include shipping costs is where we always offer free shipping. However, in that situation, we could either add provisions to ignore shipping costs, or we could set all values to zero, and remove references to shipping costs from the user interface.

Shipping methods

The first requirement to calculate shipping costs is a shipping method. We may wish to offer a number of different shipping methods to our customers, such as standard shipping, next-day shipping, International shipping, and so on.

The system will require a default shipping method, so when the customer visits their basket, they see shipping costs calculated based off the default method. There should be a suitable drop-down list on the basket page containing the list of shipping methods; when this is changed, the costs in the basket should be updated to reflect the selected method.

We should store the following details for each shipping method:

  • An ID number
  • A name for the shipping method
  • If the shipping method is active or not, indicating if it should be selectable by customers
  • If the shipping method is the default method for the store
  • A default shipping cost, this would:
    • Be pre-populated in a suitable field when creating new products; however, when the product is created through the administration interface, we would store the shipping cost for the product with the product.
    • Automatically be assigned to existing products in a store when a new shipping method is created to a store that already contains products.

This could be suitably stored in our database as the following:

Field

Type

Description

ID

Integer, Primary Key, Auto Increment

ID number for the shipping method

Name

Varchar

The name of the shipping method

Active

Boolean

Indicates if the shipping method is active

Default_cost

Float

The default cost for products for this shipping method

This can be represented in the database using the following SQL:

CREATE TABLE `shipping_methods` (
`ID` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`name` VARCHAR( 50 ) NOT NULL ,
`active` BOOL NOT NULL ,
`is_default` BOOL NOT NULL ,
`default_cost` DOUBLE NOT NULL ,
INDEX ( `active` , `is_default` )
) ENGINE = INNODB COMMENT = 'Shipping methods';

Shipping costs

There are several different ways to calculate the costs of shipping products to customers:

  • We could associate a cost to each product for each shipping method we have in our store
  • We could associate costs for each shipping method to ranges of weights, and either charge the customer based on the weight-based shipping cost for each product combined, or based on the combined weight of the order
  • We could base the cost on the customer's delivery address

The exact methods used, and the way they are used, depends on the exact nature of the store, as there are implications to these methods. If we were to use location-based shipping cost calculations, then the customer would not be aware of the total cost of their order until they entered their delivery address. There are a few ways this can be avoided: the system could assume a default delivery location and associated costs, and then update the customer's delivery cost at a later stage. Alternatively, if we enabled delivery methods for different locations or countries, we could associate the appropriate costs to these methods, although this does of course rely on the customer selecting the correct shipping method for their order to be approved; appropriate notifications to the customer would be required to ensure they do select the correct ones.

For this article we will implement:

  • Weight-based shipping costs: Here the cost of shipping is based on the weight of the products.
  • Product-based shipping costs: Here the cost of shipping is set on a per product basis for each product in the customer's basket.

We will also discuss location-based shipping costs, and look at how we may implement it. To account for international or long-distance shipping, we will use varying shipping methods; perhaps we could use:

  • Shipping within state X.
  • Shipping outside of state X.
  • International shipping. (This could be broken down per continent if we wanted, without imposing on the customer too much.)

Product-based shipping costs

Product-based shipping costs would simply require each product to have a shipping cost associated to it for each shipping method in the store. As discussed earlier, when a new method is added to an existing store, a default value will initially be used, so in theory the administrator only needs to alter products whose shipping costs shouldn't be the default cost, and when creating new products, the relevant text box for the shipping cost for that method will have the default cost pre-populated.

To facilitate these costs, we need a new table in our database storing:

  • Product IDs
  • Shipping method IDs
  • Shipping costs

The following SQL represents this table in our database:

CREATE TABLE `shipping_costs_product` (
`shipping_id` int(11) NOT NULL, `product_id` int(11) NOT NULL,
`cost` float NOT NULL, PRIMARY KEY (`shipping_id`,`product_id`) )
ENGINE=InnoDB DEFAULT CHARSET=latin1;

Weight-based shipping costs

Depending on the store being operated from our framework, we may need to base shipping costs on the weights of products. If a particular courier for a particular shipping method charges based on weights, then there isn't any point in creating costs for each product for that shipping method. Our framework can calculate the shipping costs based on the weight ranges and costs for the method, and the weight of the product.

Within our database we would need to store:

  • The shipping method in question
  • A lower bound for the product weight, so we know which cost to apply to a product
  • A cost associated for anything between this and the next weight bound

The table below illustrates these fields in our database:

Field

Type

Description

ID

Integer, primary key, Auto Increment

A unique reference for the weight range

Shipping_id

Integer

The shipping method the range applies to

Lower_weight

Float

For working out which products this weight range cost applies to

Cost

Float

The shipping cost for a product of this weight

The following SQL represents this table:

CREATE TABLE `shipping_costs_weight` (
`ID` int(11) NOT NULL auto_increment,
`shipping_id` int(11) NOT NULL,
`lower_weight` float NOT NULL,
`cost` float NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

To think about: Location-based shipping costs

One thing we should still think about is location-based shipping costs, and how we may implement this. There are two primary ways in which we can do this:

  • Assign shipping costs or cost surpluses/reductions to delivery addresses (either countries or states) and shipping methods
  • Calculate costs using third-party service APIs

These two methods have one issue, which is why we are not going to implement them—that is the costs are calculated later in the checkout process. We want our customers to be well informed and aware of all of their costs as early as possible.

As mentioned earlier, however, we could get round this by assuming a default delivery location and providing customers with a guideline shipping cost, which would be subject to change based on their delivery address. Alternatively, we could allow customers to select their delivery location region from a drop-down list on the main "shopping basket" page. This way they would know the costs right away.

Regional shipping costs

We could look at storing:

  • Shipping method IDs
  • Region types (states or countries)
  • Region values (an ID corresponding to a list of states or countries)
  • A priority (in some cases, we may need to only consider the state delivery costs, and not country costs; in others cases, it may be the other way around)
  • The associated costs changes (this could be a positive or negative value to be added to a product's delivery cost, as calculated by the other shipping systems already)

By doing this, we can then combine the delivery address with the products and lookup a price alteration, which is applied to the product's delivery cost, which has already been calculated. Ideally, we would use all the shipping cost calculation systems discussed, to make something as flexible as possible, based on the needs of a particular product, particular shipping method or courier, or of a particular store or business.

Third-party APIs

The most accurate method of charging delivery costs, encompassing weights and delivery addresses is via APIs provided by couriers themselves, such as UPS. The following web pages may be of reference:

Using such an API, means our shipping cost would be accurate, assuming our weight values were correct for our products, and we would not over or under charge customers for shipping costs. One additional consideration that third-party APIs may require would be dimensions of products, if their costs are also based on product sizes.

PHP 5 E-commerce Development Create a flexible framework in PHP for a powerful ecommerce solution
Published: January 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

Shipping rules

Hopefully by using product and/or weight-based shipping methods, we can provide accurate shipping costs; however, some couriers cap their shipping costs for dispatches, or we may wish to offer incentives such as free shipping on certain orders. We may also find that we need to charge more for shipping, depending on the customer's location.

To store these rules, we need to record:

  • A name for the rule
  • The shipping method the rule is associated with
  • The order of the rule, so if more than one rule were applicable, they would be applied in order
  • The type of match to perform, either against total product cost, or the shipping cost (product cost would allow us to offer free shipping for orders over $X, and against shipping costs allow us to cap the costs at $Y)
  • The amount to match against
  • The operator to compare the match amount against the product or basket cost (this would be an operator such as greater than, less than, less than or equal to, greater than or equal to, not equal to, or equal to)
  • The rule amount; this would be a value that would be applied to the shipping cost by a rule operator
  • The rule operator, to determine how the rule amount would be applied to the shipping cost (this would be an operator such as plus, minus, divide by, multiply by, or set value to)

The following SQL represents this in our database:

CREATE TABLE ` shipping_rules` (
`ID` int(11) NOT NULL auto_increment,
`shipping_id` int(11) NOT NULL,
`match_amount` float NOT NULL,
`match_type` enum('shipping','products') NOT NULL,
`match_operator` enum('<','>','<=','>=','<>','==') NOT NULL,
`rule` varchar(255) NOT NULL,
`rule_amount` float NOT NULL,
`rule_operator` enum('+','-','=','*','/') NOT NULL,
`order` int(11) NOT NULL,
PRIMARY KEY (`ID`),
KEY `shipping_id` (`shipping_id`,`order`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

Let's look at some example shipping rules, and potential values for these.

Free shipping

If we wished to offer free shipping to all customers whose orders were greater than or equal to $50, we would use the following values:

  • Name: Free Shipping
  • Order: 2 (assuming we use both this and the following rule)
  • Type of match: Product
  • Match amount: 50
  • Match comparison operator: Greater than or equal to
  • Rule amount: 0
  • Rule operator: Set equal to

Capped shipping

If we wished to cap shipping costs to $20, to ensure no customer paid more than that, we would use the following values:

  • Name: Max shipping cost
  • Order: 1
  • Type of match: Shipping
  • Match amount: 20
  • Match comparison operator: Greater than
  • Rule amount: 20
  • Rule operator: Set equal to

Of course we can also use these rules to do all sorts of calculations, such as discounted shipping for bulk orders, and so on. We could also extend these rules to take into account delivery locations.

Tracking

When products are shipped to customers, they may wish to be informed about tracking information. It may be possible for us to integrate with shipping provider APIs to do this. However, the simplest method (which could also eventually be integrated with such an API) is to allow store administrators to supply a message to the customer when they update an order's status to "dispatched".

Integrating shipping costs into the basket

We should integrate these shipping cost systems into our framework in the following stages:

  1. Prepare list of shipping methods and a default method.
  2. Calculate product-based shipping costs.
  3. Calculate weight-based shipping costs.
  4. Consider shipping rules and adjust shipping costs accordingly.

Shipping methods and a default

We can store a default shipping method in the framework's settings. When a customer selects an alternative shipping method, we should store that in an appropriate session variable. At this stage, all we need to do is check if the session variable is set. If the session variable is set, then that is the shipping method we must use; if it is not, then we must use the default shipping method.

// get the shipping method
if( isset( $_SESSION['shipping_method'] ) )
{
// user-selected
$this->shippingMethodID = intval( $_SESSION['shipping_method'] );
}
else
{
// system default
$this->shippingMethodID = $this->registry->
getSetting('default_shipping_method');
}

Calculating shipping costs based on products

To calculate shipping costs based on products, we need to lookup the shipping cost for each product in the basket associated with the current shipping method.

// shipping costs: product based
$shippingCosts = $this->getShippingProductCosts( $this->productIDs );

Once we have these shipping costs, it is a case of looking up the product ID in the $shippingCosts array to get the shipping cost, and multiplying this by the quantity of the product in the basket.

$this->shippingCost = $this->shippingCost + ( $shippingCosts[
$contents['product_id'] ] * $contents['product_quantity'] );

Calculating shipping costs based on product weights

To calculate shipping costs based on product weight, we must build an array of shipping costs based on weight ranges.

// shipping costs: weight based
$weightCosts = $this->getShippingWeightCosts();

Once we have our array of shipping weight costs, while iterating through products in the basket, we then iterate through the ordered weights until we find an upper limit to the product in question. Once found, we get our shipping cost. This cost is then multiplied by the quantity of the product in the basket, and added to the rolling shipping cost.

// shipping costs: weight based
$currentWeight = 0;
while( $weightFound == false )
{
if( $contents['product_weight'] >=
$weightCosts[$currentWeight]['weight'] )
{
$weightFound = true;
$this->shippingCost = $this->shippingCost +
( $weightCosts[$currentWeight]['cost'] *
$contents['product_quantity'] );
}
else
{
if( count( $weightCosts ) == $currentWeight )
{
// we don't want to do this forever!
$weightFound = true;
}
else
{
$currentWeight++;
}
}
}

Considering shipping rules, and adjusting prices accordingly

The final shipping feature is shipping rules; this requires us looking up the shipping rules from the database, and iterating through them. For each rule, we need to check the type of rule, then check if the shipping cost or the basket cost is at least that of the rule amount; if it is, then we perform our rule calculation.

/**
* Takes any shipping rules into account with regards to the shipping
costs
* @return void
*/
private function considerShippingRules()
{
// get the rules
$rules_sql = "SELECT * FROM shipping_rules
WHERE shipping_id={$this->shippingMethodUD}
ORDER BY `order`";
$this->registry->getObject('db')->executeQuery( $rules_sql );
// go through them
while( $rule = $this->registry->getObject('db')->getRows() )
{
// rule depends on the shipping cost

Here we have established that the current rule is based on shipping cost, which means we then check to see if the shipping cost meets the rule.

if( $rule['match_type'] == 'shipping' )
{
$match = false;
$match_operator = $rule['match_operator'];
// check to see our shipping cost meets the rule
if( $match_operator == '==' )
{ if( $this->shippingCost == $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '<>' )
{ if( $this->shippingCost <> $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '>=' )
{ if( $this->shippingCost >= $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '<=' )
{ if( $this->shippingCost <= $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '>' )
{ if( $this->shippingCost > $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '<' )
{ if( $this->shippingCost < $rule['match_amount'] )
{ $match = true; } }

If a rule match was found, we then take the rule into account.

if( $match == true )
{
// set the shipping cost based on the rule operator and
// the rule amount
$rule_operator = $rule['rule_operator'];
if( $rule_operator == '=' )
{ $this->shippingCost = $rule['rule_amount']; }
elseif( $rule_operator == '+' )
{ $this->shippingCost = $this->shippingCost
+ $rule['rule_amount']; }
elseif( $rule_operator == '-' )
{ $this->shippingCost = $this->shippingCost
- $rule['rule_amount']; }
elseif( $rule_operator == '*' )
{ $this->shippingCost = $this->shippingCost
* $rule['rule_amount']; }
elseif( $rule_operator == '/' )
{ $this->shippingCost = $this->shippingCost
/ $rule['rule_amount']; }
}
}

If the product is based on the basket cost, we then do the same as before, except that the rule matching depends on the cost of the shopping basket.

elseif( $rule['match_type'] == 'products' )
{
// rule depends on the basket cost
$match = false;
$match_operator = $rule['match_operator'];
// check to see our basket cost meets the rule
if( $match_operator == '==' )
{ if( $this->shippingCost == $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '<>' )
{ if( $this->cost <> $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '>=' )
{ if( $this->cost >= $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '<=' )
{ if( $this->cost <= $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '>' )
{ if( $this->cost > $rule['match_amount'] )
{ $match = true; } }
elseif( $match_operator == '<' )
{ if( $this->cost < $rule['match_amount'] )
{ $match = true; } }
if( $match == true )
{
// set the shipping cost based on the rule operator
// and the rule amount
$rule_operator = $rule['rule_operator'];
if( $rule_operator == '=' )
{ $this->shippingCost = $rule['rule_amount']; }
elseif( $rule_operator == '+' )
{ $this->shippingCost = $this->shippingCost
+ $rule['rule_amount']; }
elseif( $rule_operator == '-' )
{ $this->shippingCost = $this->shippingCost
- $rule['rule_amount']; }
elseif( $rule_operator == '*' )
{ $this->shippingCost = $this->shippingCost
* $rule['rule_amount']; }
elseif( $rule_operator == '/' )
{ $this->shippingCost = $this->shippingCost
/ $rule['rule_amount']; }
}
}
}
}
PHP 5 E-commerce Development Create a flexible framework in PHP for a powerful ecommerce solution
Published: January 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

Tax

There are three main ways to tackle tax costs in an e-commerce environment:

  • We include tax in our product prices
  • We assign tax codes to products to separately calculate and display tax costs
  • We calculate tax based on the location of the buyer

The exact requirement for a particular store depends on the store itself and the laws applicable in that country or state. In some situations we can include the tax for a product in its price; it doesn't need to be displayed to the customer. In others, we may wish for tax to be shown and calculated for the customer, if they are able to reclaim this tax (for example UK/EU VAT), and in some states in the US different states have different taxes depending on the buyer or seller, where some customers may be taxed, others not, or the tax may be based on the state the seller resides in themselves.

Most situations can be handled by associating products with tax calculations, so let's focus on that. However, we will also discuss how we may implement a location-based tax system, to charge tax depending on the customer's delivery or billing address.

Separately calculating tax values

We could either have:

  • Tax included in a product price, and a tax rule calculating how much of the product's price should be tax
  • Product costs stored without tax, and associated with their relevant tax calculations

The main difference to the way tax calculations would need to work and the shipping costs is that tax costs actually need to be integrated before the basket; that is, the products themselves should incorporate tax costs.

We will look at the second of these two options.

This would require:

  • Products to have a tax code associated with them
  • A table of tax codes to be stored in our database, along with calculation details

The tax codes (just a reference for the type of tax; for instance, at the time of writing in the UK we would have: zero-rated VAT—0%, standard rate VAT—15%, reduced rate VAT—5%, and different products may have different tax codes associated with them) would have a calculation value and operation associated with them, similar to our shipping rules, this allows the framework to easily add/subtract/divide/multiply the product cost with the calculation value.

Field

Type

Description

ID

Integer, Primary Key, Auto Increment

The ID for the tax code

Tax_code

Varchar

Name of the tax code

Calculation_value

Double

The value applied to the order cost

Calculation_operation

Enum

The arithmetic operation applied to the order cost and the calculation value, to compute the tax

The following SQL represents this table:

CREATE TABLE `tax_codes` (
`ID` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`tax_code` varchar(255) NOT NULL,
`calculation_value` DOUBLE NOT NULL,
`calculation_operation` ENUM( '+', '-', '*', '/', '=' ) NOT NULL,
INDEX ( `tax_code` )
) ENGINE = INNODB;

Again, for example, UK standard rate VAT would have a value of 1.15, and an operation of multiply by.

To create a truly flexible tax system for an e-commerce system would involve a book of its own. The simplest methods that we have discussed are relatively straightforward to implement, especially because we have done some very similar work with our shipping methods.

To think about: Location-based tax costs

In some situations, we may have different taxes applicable depending on the locations of the buyers and sellers respective to one another. This may be something we would wish to implement. Advice from a tax professional is recommended to determine if this is required for a particular use or implementation of your framework in a particular store.

A look at our basket now

Here's a view of our final basket:

PHP 5 e-commerce Development

Summary

In this article, we discussed different ways to approach shipping costs and tax values for products within our e-commerce store. This included:

  • Creating shipping methods
  • Creating shipping rules to cap, reduce, wipe, or alter shipping costs based on the cost of a basket, or have the shipping cost otherwise calculated
  • Setting shipping costs for each product based on the product and the shipping method
  • Setting shipping costs for products based on weights and the shipping method
  • How we would introduce tax costs to products

If you have read this article you may be interested to view :

About the Author :


Michael Peacock

Michael Peacock is a web developer from Newcastle, UK and has a degree in Software Engineering from the University of Durham. After meeting his business partner at Durham, he co-founded Peacock Carter, a Newcastle based creative consultancy specializing in web design, web development and corporate identity.

Michael loves working on web related projects. When he is not working on client projects, he is often tinkering with a web app of his own.

He has been involved with a number of books, having written two books himself (and working on his third): Selling online with Drupal e-Commerce Packt, and Building websites with TYPO3 Packt. He has also done technical reviews of two other books: Mobile Web Development Packt, and Drupal Education & E-Learning Packt.

You can follow Michael on Twitter.

Contact Michael Peacock

Books From Packt

Joomla! E-Commerce with VirtueMart
Joomla! E-Commerce with VirtueMart

Drupal E-commerce with Ubercart 2.x
Drupal E-commerce with Ubercart 2.x

AJAX and PHP: Building Modern Web Applications 2nd Edition
AJAX and PHP: Building Modern Web Applications 2nd Edition

Magento 1.3: PHP Developer's Guide
Magento 1.3: PHP Developer's Guide

TYPO3 4.3 Multimedia Cookbook
TYPO3 4.3 Multimedia Cookbook

CodeIgniter 1.7
CodeIgniter 1.7

jQuery 1.3 with PHP
jQuery 1.3 with PHP

MySQL Admin Cookbook
MySQL Admin Cookbook

Your rating: None Average: 4 (1 vote)

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
M
p
m
E
L
N
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software