New package: archtechx/money

By @samuel · 2021-11-05 19:24 (edited)

New package: archtechx/money

This week we'll be releasing a new package: archtechx/money.

In short, it's a package for handling money math and currency conversions, but it comes with some unique benefits compared to the currently available solutions.

Why a money package?

Price calculations are a notoriously difficult thing in programming.

That sounds wrong at first... why would price calculations be difficult on a computer? Computers are literally made for working with numbers. Everything a computer does on the lowest level is just math.

So it should be accurate, right? It should... but it's only accurate for integers. Not floats.

Computers suck at float math. Almost every single programming language has strange behavior when it comes to floats.

1/3; // 0.333333333333333314829616256247390992939472198486328125
69.1-69.0; // 0.099999999999994 instead of 0.1
floor((0.1+0.7) * 10); // 7.0 instead of 8.0

These small inaccuracies happen due to how computers work under the hood. It's complex, so I won't explain the details, but in general you always want to avoid working with floats when doing sensitive calculations.

Most calculations can be a bit off and it won't matter much, so just using normal floats and math operators works for most things. But this obviously becomes a big issue when it comes to money math, so there are some standard ways of dealing with it.

The basic principle is storing all prices as cents (integers) and only converting them to floats when they're being displayed to the user. And when you do some math operations with the price, such as adding fees, taxes, or other things, you always want to round the result back to a full integer after each operation.

This works for simple apps, but it's clearly not a good way of dealing with money at scale (scale being any ecommerce app).

For that reason, there's a specific pattern in programming. It's called (you guessed it) the Money Pattern.

How does it work?

The main idea is that you create a Money object for these calculations. The object lets you add, subtract, multiply, and divide the underlying monetary value.

But an important caveat is that it always returns a new instance. Not only does it round the value after each calculation, it also returns a new Money instance. This ensures that your money doesn't magically change value when you use it elsewhere.

You wouldn't want calling $product->price->times(1 + $tax) to change the product's price.

Our package

I decided to create a custom package because I found the current alternatives unnecessarily complex/noisy. Which also makes them less flexible than what I like for my use cases. The implementation is something I wrote ~2 years ago, but now I polished it and turned it into a package.

Our package ships with two main classes:

  • Money which represents the monetary values
  • Currency which represents currencies

There's no existing currency config besides a default USD currency class. So it only ships with what's necessary, and if you want anything more, you can very easily add it.

Money

The Money class is used like this:

$money = money(1500);
$money->formatted(); // $15.00

$eur = money(1000, 'EUR');
$eur->formatted(); // 10.00 €

$czk = $money->convertTo('CZK');
$czk->formatted(); // 250 Kč
$eur->formatted(); // still 10.00 €

$money = Money::fromDecimals(250, 'CZK');
$money->equals($czk); // true
$money->equals($eur); // true
$money->is($eur); // false

Those are the basic methods. It has a few more that are interesting but I'll leave those out now to keep the article shorter.

Currencies

Adding currencies is as simple as:

Currencies::add(new Currency(code: 'FOO', name: 'Foobar', rate: 0.8));

// or
Currencies::add(['code' => 'FOO', 'name' => 'Foobar', 'rate' => 0.8]);

// or
class Foobar extends Currency
{
    protected string $code = 'FOO';
    protected string $name = 'Foobar';
    protected float $rate = 0.8;
}

Currencies::add(Foobar::class);
Currencies::add(new Foobar);

The package also provides a great API for setting the current and default currency. You can tell it where it should read the current currency from (e.g. session()->get()), how it should store it (e.g. session()->set()), and it will automatically run this behavior when you call getCurrent() and setCurrent().

Unique features

The above is the simplest explanation of the package's API. That part is similar to the existing packages, but here are some features that we specifically focused on.

More control over currency behavior

By making currencies objects and not just some config arrays, the package lets you add custom behavior. In one ecommerce site I built, there were special EUR currencies for different domains, since it worked as a kind of multistore website which had different prices on different brands. This is extremely simple to do with our package. You can just create separate EUR instances, or add extra logic to the the EUR currency such as:

public function rate(): float
{
    // ...
}

More nuanced decimal logic

Currencies like USD and EUR are simple. They just have two decimal points, and you round the result to those two decimal points (full cents) after each operation. But currencies like the Czech Crown (CZK) are more complex.

In CZK, we typically display prices in full crowns, because there are no cent coins anymore. But sometimes, prices are displayed with cents, and the rounding is only made on payment. So if you buy a 10.30 product, you'll pay 10 crowns, but if you buy three of those products, you'll pay 31, not 30.

But also, rounding to full crowns isn't done always. Digital payments generally are rounded, but sometimes they're not and include cents.

So when working with a currency like this, I generally want to do all math in cents (two decimals), but display prices in full crowns (no decimal points). But I also want to have the option to display it with decimals, e.g. in the order summary table (taxes need to be shown in exact numbers).

Our package solves this by having two separate values: mathDecimals and displayDecimals. CZK has 2 math decimals, and 0 display decimals.

$price = Money::fromDecimal(155.34, 'CZK');
$price->formatted(); // 155 Kč
$price->formattedRaw(); // 155,34 Kč

To keep this short, I won't go into detail about this here, but: the problems of CZK don't end here. You can legally round an invoice to full decimals, but when you do so you need to internally keep track of what the rounding was. So CZK invoices generally include a rounding column which has values like 30 or -46. The money package supports this out of the box, and you can just call the rounding() method to get these values.

Great IDE support

The money() helper accepts a price and (optionally) a currency. Currencies can be provided as new Currency(...), [...], MyCurrency::class, and new MyCurrency. Whichever approach you prefer, the package will accept it and your IDE will be happy.

Livewire support

And as with any of our packages — we provide perfect Livewire support. What does that mean in this case? You guessed it, it's possible to typehint properties as Money:

class UpdateProduct extends Component
{
    public Money $price;

    // ...
}

The property will remember the value as well as the selected currency. And then, on the frontend, you can just wire:model the right fields to the property, and everything will work perfectly.

Lean Admin support

This is the main reason for why I'm building the package now. We're working on a really nice Currency field for Lean, and I want a good package to handle the logic behind the scenes.

We'll also be releasing some huge packages in a few months that will make heavy use of money math, so a standardized money package across the ArchTech ecosystem will make integration much smoother.

Release

The package will (probably) be released this weekend, with the Currency field landing in Lean at the same time.

  • By @tr1pp0 · 2021-11-09 15:54

    On Money example please convert: $money = money(1000, 'EUR'); $money->formatted(); // 10.00 € into $eur = money(1000, 'EUR'); $eur->formatted(); // 10.00 €

  • By @micha · 2021-11-08 19:46

    did I miss the link to this package?

    • By @samuel · 2021-11-08 19:50

      You didn't! I got busy over the weekend fixing up some things with email deliverability, so I'm only finishing the package now, but it's pretty much done and I'm just writing the README. Will be making the repo public tomorrow and announcing it on Twitter 👍

      • By @io238 · 2021-11-13 12:00

        Hi @samual. This package looks very promising! Any updates on the launch date yet? Thanks

  • By @lostdesign · 2021-11-05 20:00

    There is a small error in your code example:

    $money = money(1500, 'EUR'); $money->formatted(); // 10.00 €

    Should it not be „1000“