Handling Fractional Cents in Integrations

Whole-cent integers do not guarantee that an order total divides into equal unit prices. In our POS integration, splitting a 2,625-cent total across two items required keeping the one-cent remainder explicit so the product total still matched the payment.

I’m back with another story about a “silent error” in our systems. In my previous post, I talked about how implicit timezone conversions can bite you. Today, let’s talk about a different type of error: a design flaw when dealing with money.

The POS contract expects whole-cent unit prices

We are building a Central Reservation System (CRS) for a tourist park. One of our golden rules when dealing with money is simple: always use integers. We convert everything to cents before pushing it to our cash-register API.

For example, if a customer buys a buffet for 26.25 €, we treat it as 2625 cents. This has been working great, and a typical request to our cash register looks like this:

{
  "transactionId": "3316139954296091",
  "payments": [
    {
      "type": "CB",
      "amount": 2625
    }
  ],
  "products": [
    {
      "id": "327057",
      "name": "Adult Buffet",
      "quantity": 1,
      "unitPriceWithTax": 2625, // 26.25 €
      "totalAmount": 2625
    }
  ]
}

Everything was fine until we had to integrate with a new Point of Sale (POS) system for room service.

A total divided by quantity can contain fractional cents

The new POS system didn’t speak our “integer-only” language in the way we expected. Instead of sending us a clean unit price, they sent us the Total Included Tax (TotalInc) and the Quantity (Qty). They left it up to us to figure out the unit price.

Here is what their payload looked like:

{
  "$type": "Sales.Export.Data.OrderLine",
  "ProductName": "Kids Buffet",
  "Qty": 2, // Quantity is 2
  "TotalInc": 26.25, // Total is 26.25 €
  "Price": 17.5 // Base price (irrelevant for tax calc here)
}

Do you see the problem?

We have a total of 26.25 € for 2 items. To get the unit price for our system (which requires unitPrice * quantity = total), we do the math:

$$ 26.25 / 2 = 13.125 $$

13.125 € per item? That doesn’t exist. We can’t represent half a cent in our integer-based system.

Rounding each unit changes the payment total

If we try to round it standardly:

  • Round Down: 13.12 € -> 1312 cents.
    • 1312 * 2 = 2624 cents.
    • Result: We are missing 1 cent (0.01 €).
  • Round Up: 13.13 € -> 1313 cents.
    • 1313 * 2 = 2626 cents.
    • Result: We are over by 1 cent (0.01 €).

Trace the rejected payment to the line-item sum

When we sent these rounded values to our cash-register API, it immediately rejected the transaction. It performed a simple validation check:

if sum(product.UnitPrice * product.Quantity) != payment.Amount {
    return error("Payment mismatch")
}

Because 26.24 != 26.25 (or 26.26 != 26.25), the payment failed. A simple 0.01 € discrepancy caused the entire transaction to be lost.

Where the two pricing models disagree

Is this a system design error? Absolutely. But whose?

  • The POS System? Maybe. Sending aggregate totals without precise unit prices assumes infinite precision or non-integer currency handling on the receiving end.
  • Our System? Maybe. We enforced a strict Unit Price * Qty model that perfectly matches real-world physical cash but struggles with abstract split bills.

This “split check” problem is common. Imagine 3 people splitting a 10 € bill. 3.33 + 3.33 + 3.33 = 9.99. Who pays the extra cent?

Preserve the total with a remainder line item

We can’t change the POS system, and we can’t change the laws of math. So, we changed how we represent the order.

Instead of declaring “2 items at X price”, we split the line item. We calculate the floor price for all items, and then add the remainder to one of them.

The Algorithm:

  1. Calculate base unit price: floor(2625 / 2) = 1312 cents.
  2. Calculate the remainder: 2625 - (1312 * 2) = 1 cent.
  3. Split the item into (Qty - 1) items at the base price, and 1 item at base price + remainder.

So our request to the cash register became:

"products": [
  {
    "name": "Kids Buffet (1/2)",
    "quantity": 1,
    "unitPriceWithTax": 1312 // 13.12 €
  },
  {
    "name": "Kids Buffet (2/2)",
    "quantity": 1,
    "unitPriceWithTax": 1313 // 13.13 € (includes the remainder)
  }
]

Total: 1312 + 1313 = 2625. It matches exactly. Validated. Paid.

Design the data model for the remainder

Dealing with money is never just about float vs int (though please, always use int or decimal). It’s about how your data structure represents reality. In this example, 13.125 € is 1,312.5 cents. The unit prices must be 1,312 and 1,313 cents to preserve the 2,625-cent total.

When integrating systems, never assume that Total / Qty yields a clean number. Always design for the remainder.

The same boundary problem appears in Go and MongoDB timezone conversions. For another payment failure mode, see why retry logic needs judgment about idempotency.