cult3

Creating a PHP Shopping Basket Part 5 - The Reconciliation Process

Feb 18, 2015

Table of contents:

  1. The shopping basket analogy so far
  2. The Reconciler interface
  3. The DefaultReconciler implementation
  4. Testing
  5. Conclusion

Over the past couple of weeks we’ve looked at some of the integral aspects of modelling a shopping basket in code.

We looked at how to model the Product object so that we can encapsulate the data and behaviour that would be expected in the real world (Creating a PHP Shopping Basket Part 3 - Creating the Product object).

Next we looked at creating the Basket object that should be responsible for encapsulating the product list including adding, updating, and removing products (Creating a PHP Shopping Basket Part 4 - The Basket object).

In the real world it is not the responsibility for the product to know how to calculate it’s own totals and liabilities and it’s not the basket’s responsibility to calculate the totals for the entire order.

Instead we need to pass the basket through a reconciliation process.

In today’s tutorial we will be looking at writing a reconciliation process for our PHP shopping basket.

The shopping basket analogy so far

When modelling a real world phenomenon, it’s important to stay close to reality. When your code diverges from reality, it makes it difficult to work with in ways you would expect it should work.

In order to set the scene for today’s tutorial, lets do a quick review of the analogy so far.

The Basket object is responsible for encapsulating the product list including adding, updating, and removing products.

The Product object is responsible for encapsulating the current state and behaviour of the product. This includes the data and associated objects as “potential energy”.

In the real world, when you have finished your shopping you go through a checkout process. This process takes each product and calculates the totals, delivery charges and taxes owed to create an order.

The reconciliation process should accept each Product object from the Basket, calculate and return the appropriate totals.

The reconciliation process should be a stateless service that does not alter the original values of the Product.

Hopefully that all makes sense, but if not, don’t worry, I’m sure everything will fall into place as we start to write the implementation for the reconciliation process.

The Reconciler interface

Something that I discovered when researching into ecommerce development is that the reconciliation process is yet another area of contention.

Yet again, it seems that there is no agreed upon method for calculating totals in ecommerce software.

As we’ve seen in the previous tutorials of this mini-series, you can’t please everyone so you should aim to write your code in a way that allows other developers to extend it without having to make a Pull Request of hack around with internal logic.

This means we should define an interface that states what the class should be capable of and a default implementation to show how it can be used.

But if a developer wants to use their own implementation to satisfy their requirements, they are free to do so as as long as satisfies the interface.

So the first thing to do is to define the interface:

<?php namespace PhilipBrown\Basket;

interface Reconciler
{
    /**
     * Return the value of the Product
     *
     * @param Product $product
     * @return Money
     */
    public function value(Product $product);

    /**
     * Return the discount of the Product
     *
     * @param Product $product
     * @return Money
     */
    public function discount(Product $product);

    /**
     * Return the delivery charge of the Product
     *
     * @param Product $product
     * @return Money
     */
    public function delivery(Product $product);

    /**
     * Return the tax of the Product
     *
     * @param Product $product
     * @return Money
     */
    public function tax(Product $product);

    /**
     * Return the subtotal of the Product
     *
     * @param Product $product
     * @return Money
     */
    public function subtotal(Product $product);

    /**
     * Return the total of the Product
     *
     * @param Product $product
     * @return Money
     */
    public function total(Product $product);
}

The reconciliation process should be a stateless service. This means it should not hold a reference to any objects, but instead accept an input and return an output.

Each method of the Reconciler interface accepts an instance of Product.

Each method is then responsible for calculating the total in whatever way it deems suitable. The outside world does not need to be concerned with this logic.

The DefaultReconciler implementation

In order to show how this package should be used as part of an ecommerce application we should include a default implementation:

<?php namespace PhilipBrown\Basket\Reconcilers;

use PhilipBrown\Basket\Reconciler;

class DefaultReconciler implements Reconciler
{
}

The first method I will write will be a private method to generate a new Money object of zero value:

/**
 * Create an initial zero money value
 *
 * @param Product $product
 * @return Money
 */
private function money(Product $product)
{
    return new Money(0, $product->price->getCurrency());
}

We’re going to need to create a new zero value Money object in a couple of the methods so this is really just for convenience.

The money() method is not part of the Reconciler interface for a couple of reasons.

Firstly, this method is just for our convenience and so it’s a detail of the implementation. The outside world does not need to know it exists.

Secondly, the interface is stating that the object is “capable” of performing each of the methods. Generating a zero Money value instance should not be defined as one of the object’s capabilities.

The Value method

The first method we will look at will be for calculating the value of the products:

/**
 * Return the value of the Product
 *
 * @param Product $product
 * @return Money
 */
public function value(Product $product)
{
    return $product->price->multiply($product->quantity);
}

This method calculates the raw value of the products. So if we had a product that was valued at $10 and we had 3 of them, the total “value” of the order would be $30. The value is the raw value of the products before any taxes or discounts have been applied.

Due to the fact that Money objects are immutable, calling the multiply() method will return a new Money object.

The discount method

Next we have the discount() method for calculating the total discount of the product:

/**
 * Return the discount of the Product
 *
 * @param Product $product
 * @return Money
 */
public function discount(Product $product)
{
    $discount = $this->money($product);

    if ($product->discount) {
        $discount = $product->discount->product($product);
        $discount = $discount->multiply($product->quantity);
    }

    return $discount;
}

First we create a new zero value Money object using the money() method from earlier.

Next we check to see if the Product object has a Discount. By default this value will be set to NULL.

If there is no Discount set we can simply return the zero value Money object.

If there is a Discount we can calculate the value of it and then multiply it by the quantity of the products.

The Delivery method

Some products will have an associated delivery charge and so we can calculate that value using the delivery() method:

/**
 * Return the delivery charge of the Product
 *
 * @param Product $product
 * @return Money
 */
public function delivery(Product $product)
{
    $delivery = $product->delivery->multiply($product->quantity);

    return $delivery;
}

By default the delivery property of the Product object will already be set to a zero value instance of Money.

This means we can simply multiply the applied delivery value by the quantity of products to get a total.

The Tax method

The tax() method is slightly more complicated than the previous methods because we have to take a couple of things into consideration:

/**
 * Return the tax of the Product
 *
 * @param Product $product
 * @return Money
 */
public function tax(Product $product)
{
    $tax = $this->money($product);

    if (! $product->taxable || $product->freebie) {
        return $tax;
    }

    $value = $this->value($product);
    $discount = $this->discount($product);

    $value = $value->subtract($discount)
    $tax = $value->multiply($product->rate->float());

    return $tax;
}

Firstly we need to generate a new zero value Money object.

Next we can check to see if the product is not taxable or if the product is a freebie. If either of these conditions are true, we can simply return the zero value Money object because no tax should be added.

Next we calculate the $value and the $discount from the value() and discount() methods from earlier.

Finally we can calculate the $value minus the $discount and then calculate the $tax by multipling that value by the tax rate.

The subtotal method

Next the subtotal() method is again slightly more complicated:

/**
 * Return the subtotal of the Product
 *
 * @param Product $product
 * @return Money
 */
public function subtotal(Product $product)
{
    $subtotal = $this->money($product);

    if (! $product->freebie) {
        $value = $this->value($product);
        $discount = $this->discount($product);
        $subtotal = $subtotal->add($value)->subtract($discount);
    }

    $delivery = $this->delivery($product);
    $subtotal = $subtotal->add($delivery);

    return $subtotal;
}

First we create a zero value Money instance.

Next we check to make sure the product is not a freebie. If the product is not a freebie we can calculate the subtotal from the value and the discount.

Next we can calculate the delivery charge and finally add that value to the subtotal.

The total method

Finally, the total() method is as follows:

/**
 * Return the total of the Product
 *
 * @param Product $product
 * @return Money
 */
public function total(Product $product)
{
    $tax = $this->tax($product);
    $subtotal = $this->subtotal($product);
    $total = $subtotal->add($tax);

    return $total;
}

To calculate the total we need to first calculate the tax and the subtotal, and then add those two values together. Fortunately we can just leverage the existing methods on this service class so we don’t have to repeat the logic.

Testing

You’re probably thinking, “where are the tests?!”.

Whilst creating this package I found that it was quite difficult to test due to the different set ups required to test every possible permutation.

To solve this problem I create a fixtures file that would generate new Product objects.

I then ran each of those fixtures through the reconciliation process in this test file.

Rather than repeating all of that code here, I would urge you to check out the source code to see how I’ve went about testing this functionality.

Conclusion

In the real world, everyone has an opinion on how something should work. You are probably reading this article and thinking you would of calculated the totals in a different way too.

At the end of the day, you will never please everyone. If you try to please everyone, you will end up pleasing no one.

Instead of trying to find the “truth”, simply provide a way for the developer to suit her own needs.

The reconciliation process is a stateless service for calculating totals. when you go through the checkout process in a shop, the checkout doesn’t hold a reference to your order, it’s just input, output.

The Product object holds the current state of the product, but it should not be responsible for calculating it’s own totals. We need a way of calculating the totals without corrupting the state of the product.

Instead of dumping this responsibility on the Product object we can instead define a service to do the heavy lifting for us.

The beauty of this is you can then allow anyone to develop their own set of rules for reconciliation.

If you would like to see the full source code for this package, take a look at GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.