Fix your e-commerce app before it's too late!

How to avoid common mistakes with handling money
8
Feb

Fix your e-commerce app before it’s too late!

One software bug regarding risk calculations caused 217 million dollars in investor losses[1]. It was a mistake in basic decimal operations programmed by a software developer. This shows us how important and responsible is a developer’s job. Are you sure that you know how to avoid such mistakes in your own PHP applications?

I’ve spent 12 years in the software development industry so far, and every project I’ve been working on involved processing money. Be it a web shop, a news portal, an affiliate marketing platform or an international payment gateway – every project did some monetary calculations inside. It’s a responsible job obviously, yet I’ve seen many times people tried to reinvent the wheel in a wrong way. Is proper money handling so difficult? Are there any reliable and verified solutions we can choose?

We are going to dig deeper into this subject by performing a very basic job – creating a receipt for a customer.

Money around the world

With over 200 currencies in the world, there are plenty of pitfalls you must be aware of when developing a financial application. Even if you use only one currency, some details might surprise you. Things become especially difficult when you try to explore international markets.

Let’s start from the beginning. How do people write and read monetary amounts? Most currencies have subunits, for example 1 euro equals 100 cents, so it is convenient to use decimal fractions. For example, if you have 1 euro and 23 cents, you can write it down as EUR 1.23, or 1,23 €.

A cent cannot be split in half, so sometimes you have to perform rounding. If you want to add 19% tax to 1,23 €, you get 1.4637 first and then you decide what to do – whether to round up or down. According to the local law, you might either end up with 1,46 or 1,47. This is the first thing you should discuss with your product owners, business experts or lawyers. Never make this call by yourself – your job is to implement business requirements, not guess them!

Things get interesting when you try to handle different currencies. Not only you must distinguish, for example, euros from dollars – you also need to perform a proper conversion. Just like you can’t add kilograms and pounds directly, you can’t sum up euros and dollars without knowing the exchange rate between these two currencies. The rates are perhaps somewhere on the Internet, you have to fetch them from a trusted source and do the proper math.

We’ve already talked about several business issues. How can we solve them without reinventing the wheel? When millions of developers around the world cope with similar problems, I’m sure there are some ready-to-use solutions!

Why you shouldn’t invent it all by yourself

At a first glance, calculating a receipt looks easy: it’s just about adding some numbers, right? But how can we make sure the calculations are correct?

Unfortunately, computers don’t make it that easy. The CPUs do not know what a decimal number is! If you assign a number like 1.23 to a variable, you don’t have a guarantee that a CPU will hold that exact value! This is because CPUs and programming languages use the IEEE 754 standard to store numbers with fractions. They are real numbers, and the data type is called floating point[2] (or shortly, float).

When the IEEE standard was created back in the 1980s, computers had very limited processing power and memory. Engineers wanted to squeeze as wide range of real numbers as possible into just 32 bits. This is why they decided to use a binary type rather than a decimal. Every float number is represented as . If you convert this value to a decimal, you will very likely have a precision error.

Such small errors are fine for scientific calculations, but are totally unacceptable for the finance world! Unfortunately the only way to make precise decimal arithmetic with PHP is to use a library like BC Math or PHP Decimal. It’s not built into the language itself.

Another problem, especially in an international web application, is how to distinguish currencies. If you mix up an array of numbers without having an idea about their currencies, your calculations will be totally wrong!

Using the principles of the Object-Oriented Programming, it should be fairly easy to create a dedicated data structure for money.

Learn more about International PHP Conference 2021

Using the “Money” pattern

In his 2002 book, Martin Fowler proposed a “Money”[3] structure to combine a monetary amount with currency. With OOP you can encapsulate that value in an object and thus make sure the calculations are protected by the Money class.

The Money Pattern has been implemented for several languages like PHP, Java and JavaScript. It’s easy to guess that a PHP implementation is called MoneyPHP[4]. It’s a mature library that has been around for almost ten years. Here’s an example usage:

use Money\Money;

// create an object of 1,23 EUR
$net = new Money(123, new Currency('EUR'));

// add 19% of tax, round up
$gross = $net->multiply('1.19', Money::ROUND_UP);

// output is 147, which means 1,47 EUR
echo $gross->getAmount();

With MoneyPHP, we can be certain of three things: proper decimal calculations, avoiding mistakes when using multiple currencies and avoiding accidental value overwrites because Money is an immutable object. Every time you perform addition, subtraction or any other operation, you receive a new instance of the Money class. This prevents bugs especially when your Money objects travel between multiple classes implementing complex business logic of your app.

Internally, a money object always stores the amount as a string, which is the amount in the smallest subunit of a currency. For euro, this will be the amount of cents like in the example above, where 1,23 € equals 123 cents. So when you receive an amount of euros as a user’s input, you just have to multiply it by 100 or remove the decimal separator to create the money object.

However, it’s more complicated for currencies like the Japanese yen or Hungarian forint. Yens don’t have subunits now (they had in the past), so MoneyPHP will expect the amount of yens. Forint doesn’t have coins smaller than 5 Ft in circulation, but the 1/100 subunit is still used for calculations. When using forints, you still have to multiply them by 100 for MoneyPHP.

When using any currency in your system, always get familiar with its details! Refer to the ISO 4217 list of currencies[5].

Formatting the currency

When presenting a monetary amount to the user, we must take into account the language and region the user prefers. It’s not enough to do a simple echo like in the first listing. Different regions have different number and currency formatting standards. Some people use dot instead of a comma to separate the fractional part. Also, some people put currency before the amount:

Language and region Example localized amount
German (Germany) 12.345,67 €
Dutch (Netherlands) € 12.345,67
Polish (Poland) 12 345,67 €

Table 1: Example monetary formats for different languages and regions

Most “money” libraries like MoneyPHP offer localized formatting of the monetary amounts. They combine number formatting rules and currency formatting rules specific for a country and a language:

use Money\Currencies\ISOCurrencies;
use Money\Currency;
use Money\Formatter\IntlMoneyFormatter;
use Money\Money;

// we need a repository of ISO standard currencies
$currencies = new ISOCurrencies();

// create an object of 1,23 EUR
$money = new Money(123, new Currency('EUR'));

// use a PHP "intl" number formatter for Germany...
$numberFormatter = new \NumberFormatter('de_DE', \NumberFormatter::CURRENCY);

// ...together with MoneyPHP formatter
$moneyFormatter = new IntlMoneyFormatter($numberFormatter, $currencies);

// output: 1,23 €
echo $moneyFormatter->format($money);

In the example above, MoneyPHP introduces a concept of currency repositories. Earlier, we used only a currency code, but now the formatter needs to know exactly how many digits after the decimal point are expected for a currency. The creators of MoneyPHP didn’t want to restrict us only to the standard ISO currencies. We will use the ISO repository for now, but we could also implement our own repository, for example cryptocurrencies.

One more use case of a money formatter is to send data to an API that expects a decimal number as input. Some APIs might follow the Schema.org PriceSpecification[6] which expects a decimal with a dot. Instead of doing some crazy formatting magic on your own, you can use a class called DecimalMoneyFormatter:

use Money\Currency;
use Money\Formatter\DecimalMoneyFormatter;
use Money\Money;

// create an object of 1,23 EUR
$money = new Money(123, new Currency('EUR'));

// use a decimal number formatter
$decimalFormatter = new DecimalMoneyFormatter($currencies);

// output: 1.23
echo $decimalFormatter->format($money);

Integrating backend and frontend layers

So far we’ve found a nice solution to store money in PHP and output it to a user. However, most web applications utilize multiple systems. A user starts their journey with the frontend layer which then connects to your backend, and then there is usually some data storage, for example MySQL. The data travels back and forth. On every level there is a danger of losing some part of the monetary data! You should care about your data from the very beginning – when you receive it as user input.

Let’s say there is an administrator who adds products to a web shop and defines their prices. The simplest scenario is when an HTML form is directly sent via POST method to your PHP script. In this case you usually receive the price as a localized number like 1,23 for example. You have to correctly parse it to create a Money object.

A serious mistake that I once witnessed was to convert that value to a float, then multiply by 100 and finally cast the result to an int. Because the float type was not precise, and the int cast always rounds down, we couldn’t set a price of 4,10 € – it was always changed to 4,09.

To avoid such bugs we will use a dedicated money parser:

use Money\Currency;
use Money\Currencies\ISOCurrencies;
use Money\Parser\IntlLocalizedDecimalParser;

// we need a repository of ISO standard currencies
$currencies = new ISOCurrencies();

// use a PHP "intl" number formatter for Germany...
$numberFormatter = new \NumberFormatter('de_DE', \NumberFormatter::DECIMAL);

// ...together with MoneyPHP parser
$moneyParser = new IntlLocalizedDecimalParser($numberFormatter, $currencies);

// parse user input as euro
$money = $moneyParser->parse('1.234,56', new Currency('EUR'));

// output: 123456
echo $money->getAmount();

Of course the frontend layer can be a lot more complicated than that. There can be client-side validation or even some calculations done in JavaScript. Like I said before, there are already implementations of the Money pattern for JS. You can choose between several libraries like currency.js, Dinero.js or Money.js, however this is not the scope of this article.

Learn more about International PHP Conference 2021

On the reverse side, you might need to serialize a money object to JSON and send this data to the frontend layer. The Money class implements the JsonSerializable interface, so you just need to use the json_encode() function to receive the JSON:

use Money\Money;

// create an object of 1,23 EUR
$net = new Money(123, new Currency('EUR'));

// output: {"amount":"123","currency":"EUR"}
echo json_encode($net);

How to integrate systems without mistakes?

When you exchange monetary data between systems, it is always crucial to know if both systems use the same unit. Sometimes one API might expect the amount of euros and another might expect cents. These discrepancies can lead to confusion and serious mistakes. It’s better to use more descriptive field names like amountDecimal or amountInteger instead of a plain amount.

Storing monetary amounts in a SQL database

For the SQL databases, the solution is more tricky because none of them actually implements the Money pattern! Some database systems have monetary data types that use a globally set currency, but that’s not what we’re aiming for. We want to store any currencies in the system without messing them up. Even if our app uses only one currency, chances are that some day there will be a new currency introduced in our country. We need to store meaningful values in the database to avoid trouble during migration or any other operations involving currency conversion.

One solution would be to store the amount in one column, usually a DECIMAL or BIGINT type (definitely not FLOAT nor DOUBLE!), and then use a CHAR(3) column to store the ISO currency code. This allows performing some calculations directly in the database, but at the same time it’s easier to mistake the currencies. So another solution is to use a single VARCHAR column to store an encoded value like EUR 123, for example. Then you are going to decode the string in PHP. One way or another, you still must be careful to use either euros or cents consistently.

It’s not easy to decide how big should be the data type for storing currency. A 32-bit unsigned INT type would allow amounts over 4 billion euros which seems more than enough. However, currencies in the world have different value and they can be subject to hyperinflation[7]. In 2009 in Zimbabwe, one US dollar costed 1025 of a local currency. Yes, that’s a number 1 with 25 zeros! Such extreme cases can quickly lead to an integer overflow. You should regularly monitor if your systems still can handle current monetary amounts.

Creating a receipt model

It doesn’t matter if you call it a receipt, an invoice or a bill – the point is the same. We want to create a legal document to confirm a customer’s purchase. The structure of this document is usually defined by the law. Some fields, like the buyer and seller names or a total sum, are mandatory. Some additional fields can be optional, but they can still hold valuable data. To get started, just grab a random receipt lying on your desk or in your wallet and start examining its contents.

The same receipt will most likely exist in multiple forms. A customer will see it on a web page directly after a purchase and later in a My orders section. Perhaps he or she is going to receive an e-mail confirmation with a PDF attached. We have at least four places where similar data will be presented.

It is convenient to create an abstraction of a document. Let’s develop a class called Receipt which will contain all the information required by the law, by your company and the customers. At this point we will not care about SQL, JSON, HTML nor PDF. It will be only a pure data structure with some basic behaviors implemented. It is an abstract model.

use Money\Currency;
use Money\Money;

final class Contractor
{
    private string $name;
    private string $address;
    // constructor and getters below...
}

final class ReceiptItem
{
    private Money $unitPrice;
    private int $quantity;
    private int $tax;

    public function __construct(Money $unitPrice, int $quantity, int $tax)
    {
        // verify data correctness
        if ($quantity < 0 || $tax < 0) { throw new \Exception('Quantity and tax cannot be negative'); } $this->unitPrice = $unitPrice;
        $this->quantity = $quantity;
        $this->tax = $tax;
    }

    // calculate total value of an item/row
    public function getTotalPrice(): Money
    {
        return $this->unitPrice
            ->multiply($this->quantity)
            ->multiply('1.' . $this->tax, Money::ROUND_UP);
    }
}

final class Receipt
{
    private Currency $currency;
    private Contractor $seller;
    private Contractor $buyer;
    private \DateTimeImmutable $issueDate;
    private \DateTimeImmutable $dueDate;
    /** @var array|ReceiptItem[] */
    private array $items = [];

    public function __construct(Currency $currency)
    {
        $this->currency = $currency;
    }

    public function addItem(Money $unitPrice, int $quantity, int $tax)
    {
        // check if the currencies match
        if (!$this->currency->equals($unitPrice->getCurrency())) {
            throw new \Exception('Currencies must match');
        }
        $this->items[] = new ReceiptItem($unitPrice, $quantity, $tax);
    }

    public function getTotalAmount(): Money
    {
        $sum = new Money(0, $this->currency);
        foreach ($this->items as $item) {
            $sum = $sum->add($item->getTotalPrice());
        }
        return $sum;
    }

    // other getters and setters below...
}

In the example above, we’ve created a simple Receipt class that contains basic seller and buyer data, document issue date, payment due date, and a list of items (rows). The document will be based on a single currency, so we added an additional check to make sure all items have the same currency. There is one method that calculates the total amount to pay.

It’s very easy to write tests for such a simple class that doesn’t have connections to any external systems. Let’s use PHPUnit[8] for this:

use Money\Currency;
use Money\Money;
use PHPUnit\Framework\TestCase;

final class ReceiptTest extends TestCase
{
    public function testTotalAmount(): void
    {
        // given
        $euro = new Currency('EUR');
        $receipt = new Receipt($euro);
        $receipt->addItem(new Money(123, $euro), 10, 19);

        // when
        $total = $receipt->getTotalAmount();

        // then
        self::assertEquals('1464', $total->getAmount());
        self::assertTrue($euro->equals($total->getCurrency()));
    }
}

The above test is very simple. It creates a Receipt object with euro as a currency, then adds one item with a unit price of 1,23 € times 10 pieces. Given the tax rate for that item is 19%, the test calculates the total amount which should be equal to 14,64 €.

Now we can test and implement any remaining logic for calculating taxes, subtotals and any other amounts we have to present on paper. In general, we want to encapsulate all this financial logic inside our Receipt class. That’s why we do not accept the calculated values as user input. We’d like to make sure that our Receipt objects always contain a valid state.

To make your Receipt object interchangeable with other systems, you will have to implement an additional infrastructure layer. To encode the object as JSON or decode it from a JSON string you will make your own serializer and deserializer. However, the encoding job can be also done by implementing the JsonSerializable interface in the Receipt class. The resulting data should be ready to send to the frontend layer:

final class Receipt implements JsonSerializable
{
    // prepare an array that will be encoded to JSON
    public function jsonSerialize(): array
    {
        return [
            'currency' => $this->currency->getCode(),
            'totalAmountInteger' => $this->getTotalAmount()->getAmount(),
            // more fields here...
        ];
    }
}

Another class, called a repository, would be responsible to store the receipt inside a SQL database and retrieve that data back. You can either rely on existing ORM implementations like Doctrine or Eloquent, or implement the database logic yourself. It depends on how clean you want the Receipt class to be. With Doctrine you would only add some annotations to that class and implement a separate repository. With Eloquent, the Receipt class would have to implement the Active Record pattern and thus change a lot. This would break our core assumption which was not to tie the Receipt class with the database too much.

Converting between currencies

When your application works for international markets, you might need to convert between different currencies. Even in the European Union not every country has euro. Some customers might prefer being charged in their local currency, and this often requires declaring that converted amount somewhere on paper. You might need to add more fields into your Receipt class.

First of all, you have to fetch current exchange rates from some external API. There’s a variety of services to choose from, including free and paid plans, for example Fixer, European Central Bank or Open Exchange Rates.

As currency conversion should rather be a backend job, we will use the MoneyPHP converter. With some help from the Swap library, we will pull the exchange rates from your chosen API and feed the converter with data:

use Money\Converter;
use Money\Currencies\ISOCurrencies;
use Money\Currency;
use Money\Exchange\SwapExchange;
use Money\Money;
use Swap\Builder;

// prepare the API client with your credentials
$swap = (new Builder())
   ->add('some-api', ['access_key' => 'your-key'])
   ->build();

// build a currency exchange object
$exchange = new SwapExchange($swap);

// build a converter working on ISO currencies
$converter = new Converter(new ISOCurrencies(), $exchange);

// convert euros to dollars
$eur = new Money(123, new Currency('EUR'));
$usd = $converter->convert($eur, new Currency('USD'));

How to manage exchange rates?

Usually it’s fine to fetch the complete rates table once a minute, or even once an hour and store it in your own database, so you don’t overload the API. Discuss the interval of pulling the exchange rates with your business experts. Always store the exact date and time declared by the API you are using. Also, whenever performing a currency conversion, you should be able to prove which rate was used and from what time.

Remember about using data types big enough to store any currency you need and precise enough to store the exchange rates. They usually require four digits after the decimal point. Use reliable decimal types and not float nor double.

Rendering HTML and PDF receipts

After purchasing goods in your web store, most customers will expect a confirmation, a receipt or an invoice. It can be displayed directly after purchase, inside an e-mail or an attached PDF file. Similar data will be duplicated in multiple places. Perhaps you can reuse some HTML fragments to avoid code duplication. For example, the customer’s data box or products list will most likely be the same everywhere.

Your application is probably already using some templating engine like Twig or Blade, so we will not cover generating the HTML code here. If you prepared some handy methods in your Receipt class, it should be easy now to embed all the relevant data in proper places of the HTML template.

Learn more about International PHP Conference 2021

At this stage you should not perform any calculations or decisions inside HTML – this is the responsibility of the Receipt class! If some parts of the document are conditional, implement this logic inside your model. The template must only ask simple questions to the model, for example: “Should I display this section or not?”.

Once you have an HTML document prepared, it should be easy to create a PDF receipt from it. There’s plenty of solutions to convert HTML directly to a PDF. Receipts usually have simple layouts, so for example a Dompdf[9] library will be enough. It has its own rendering engine which understands most of the HTML 5 and CSS 2.1 syntax:

$dompdf = new Dompdf\Dompdf();

// load the HTML document and set paper size
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');

// render the document and get the PDF contents
$dompdf->render();
$pdf = $dompdf->output();

Testing the PDFs we created

Even though we have implemented unit tests for our Receipt class, we should also make sure that our HTML templates are correct. When operating on multiple monetary amounts and multiple terms like “total”, “subtotal” and so on, you could make mistakes that are hard to defend before the tax office!

Testing documents is a lot harder than unit testing classes. A final receipt combines business data together with HTML markup and perhaps even some CSS tricks. This is why thinking over a clean HTML template structure gives you a benefit of easier testing.

The best way to test an HTML template with PHP would be to use the bundled DOM extension. DOM stands for Document Object Model and is a standard mechanism for representing an HTML document structure in memory. So after rendering a template, you create a DOMDocument object and then use XPath queries to look up all the elements containing important data:

use PHPUnit\Framework\TestCase;

final class TemplateTest extends TestCase
{
    public function testTemplateRendersProperly(): void
    {
        // given
        $html = '<html><body>
<address class="seller">Foobar GmbH</address>

</body></html>';
        $document = new \DOMDocument();
        $document->loadHTML($html);
        $xpath = new \DOMXPath($document);

        // when
        $element = $xpath->query('//address[@class="seller"]')->item(0);

        // then
        self::assertEquals('Foobar GmbH', $element->textContent);
    }
}

To ensure your final receipt’s layout is correct, another test can either capture a screenshot of a result PDF and compare it to the expected image, or extract text from a rectangular region to see if an expected block of data was positioned properly. This way you can make sure that customer’s data is somewhere in the top-right corner, products list is somewhere in the middle of a page, and so on.

It’s not easy to extract text from a PDF file because the physical order of elements in a PDF often differs from what we did in HTML. A PDF does not have a text flow; it operates on fixed blocks of content. You can experiment with the pdftotext tool from the Poppler-utils[10] package, or Sébastien Malot’s PdfParser[11].

Should you trust third-party tools?

You could ask why you should test final PDF files when you are using a popular library to generate them from HTML. The question is: how much do you trust the solutions prepared by other people? Did you check how well the library is tested? Are you sure that upgrading that PDF library won’t break your layout? It happened to me after a minor upgrade of some other PDF tool.

Remember that these programs are made by people who make mistakes just like everyone else. Your clients will blame you for the mistakes of others. You are responsible for the tools you are using in your projects!

Wrapping up

Developing e-commerce and financial applications is a responsible task. You don’t want to put yourself or your client in a risk of losing money and reputation. You can greatly improve the reliability of your code by applying some basic rules when processing money. There are many ready-to-use solutions that will make your job easier, so there’s no point in reinventing the wheel.


Links & Literature

[1] https://www.theregister.com/2011/09/22/software_bug_fine/

[2] https://floating-point-gui.de/

[3] Fowler Martin, „Patterns of Enterprise Application Architecture“, Addison-Wesley, 2002

[4] https://moneyphp.org/

[5] https://www.iso.org/iso-4217-currency-codes.html

[6] https://schema.org/PriceSpecification

[7] https://de.wikipedia.org/wiki/Hyperinflation

[8] https://phpunit.de/

[9] https://github.com/dompdf/dompdf

[10] https://poppler.freedesktop.org/

[11] https://www.pdfparser.org/

Stay tuned!

Behind the Tracks of IPC

PHP Core Technology
Best Practices & Application

General Web Development
General Web Development & more

Agile & Company Culture
Agility has become mainstream

Software Architecture
Concepts & Environments

Security
All about Web Security

Testing & Test Driven Development
More about software testing tools and strategies

DevOps & Continuous Delivery
DevOps is a philosophy

Docker, Kubernetes, Cloud
Cloud-based & native apps

#slideless (pure coding)
Showing how technology really works

PHP Frameworks
All about PHP Frameworks

Content Management Systems
Sessions on Content Management Systems