Shopping cart calculations library

This library aims to help calculating prices and taxes in shopping carts (or orders, invoices, ...).

It provides a framework to build a cart from different "items" (products, fees, discounts, ...), where the price calculations of one item may depend on another item. Finally, the taxes are calculated from the sum of all items per tax class.

The code is available on github. A demo shop application shows a usage example.

Status

Currently under development

How it works

From a mathematical point of view, this kind of calculations are trivial. However, there are a few decisions that have to be made, regarding of what values should be exposed as rounded price amounts. If intermediate results are exposed as rounded prices, eventually there will be some inconsistencies. This library provides an API that gives you as much freedom as possible to calculate your prices, and always results in a consistent representation of a calculated cart. It does so by defining, what is considered an intermediate result and what is not. The calculation can be seen as one function. The user provides all the inputs, and the function returns a completely calculated cart as output. The output contains everything an end user might want to see, but does not contain intermediate results.

Input parameters are:

  • A set of tax classes, and a tax system which is responsible to define a tax rate for each of its tax classes.
  • The price mode (net or gross) that is used to calculate the cart. The calculation's results will contain prices in this mode.1
  • The cart items, i.e. products, discounts, fees, ...; each combined with an algorithm2 that defines how to calculate the price(s) for this item.
  • (no currency: it is expected that all items are of the same currency. Any price converting logic can be done when the prices are calculated for a given item).

Output values are:

  • Success or failure (if any item's calculation failed, the whole cart calculation failed).
  • Success case:
    • All items and their price results
    • The sum of all items per tax class
    • The combined tax amount per tax class
    • The grand total (the total sum of all items in the cart's price mode)
    • The gross total (equals the grand total in case of price mode == gross)
    • The net total (equals the grand total in case of price mode == net)
  • Failure case: the intermediate cart is returned, which contains all the intermediate results, i.e. the successful and the failed item results.

Intermediate results (not exposed, but listed here for clarity):

  • tax amount per cart item
  • unit price per item (may be added easily by the library user if it does not lead to confusion...)

The maths in detail

Here's what the library calculates, by example:

Input:

Items         Tax Class      Price
x             A (10 %) ->    100
              B (20 %) ->    100
y             A (10 %) ->    200  

---
price mode: gross

Output:

// Output
Tax Class       Sum per tax class     Tax Amount
A               300                   round(10 % * 300) = 30 
B               100                   round(20 % * 100) = 20

Totals
Grand total:    (300 + 100) = 400
Taxes:                                (30 + 20) = 50
Gross total     400 (= Grand total)
Net total       (400 - 50) = 350

That's easy, and it's more or less what you see on every invoice. The library helps not to make any mistakes, and to cleanly separate the algorithms used to calculate prices per item from calculating totals.

Conclusion

It took me quite some time to build a suitable "framework" for this kind of calculation. The goal was to keep things as simple as possible (i.e. as close as possible to the simple description in this document), while allowing to easily "bind" the calculations to the real world user data. In the real world, there are:

  • Sellable goods of any form and type, stored in any database...
  • prices depending on the customer, the currency, the country, ...
  • B2B and B2C (which often means different tax rules)
  • Discounts depending on different conditions
  • ...

I tried to precisely describe the types of input parameters and the output values, without any additional information that is not really needed for the algorithm. For example, I started out with an implementation that included currencies. Because: what is a money amount without a currency? Or a cart without money amount? It meant a dependency to a money/currency library/api, and it made the types more complicated. Later I realized, that I cannot really do maths with prices in different currencies. They need to be converted somehow (or you have to calculate two separate "carts"). But as the prices of each item is defined by the user, the conversion could and should be done before. The actual algorithms of this library never need that information. By removing any dependency on currencies, the library also removes assumptions on the potential user's use case. If he only uses one currency, he doesn't have to bother with this things. If he uses more than one, he can use any representation for the currency, and handle conversions any way he likes.


Footnotes:

  1. It is possible to get the cart's total sum as net and as gross price. However, the cart's price mode determines the prices of the single cart items, which are rounded (because the user most often expects them to be rounded), and then further used to calculate the total sum in the given price mode. The other price mode is then obtained by adding/subtracting the total amount of taxes. See also the "output values" described in the article.
  2. This algorithm is a function which gets the intermediate cart as input (i.e. the calculation results of all previous cart items, plus the price mode), and returns a Map[TaxClass, Long]. This gives the possibility to split the price by tax class, which may be useful for discounts or other "supplementary" amounts that have no inherent tax class. The function may alternatively return an error message (the full return type is: Either[String, Map[TaxClass, Long]]. The returned prices have the type Long, representing a price in the currency's smallest unit (e.g. "cents"), and in the price mode defined by the cart.