All articles Engineering

A quote and a charge must be one number

The moment those are two implementations, they diverge — and the customer finds out at the card.

Photo: Silver Dovelet (CC BY-SA 4.0) / Wikimedia Commons

Our change-plan dialog showed a price. Our server charged a price. For a long while these were computed in two different places, in two different languages, and they agreed.

Then we started rounding yearly prices down to the whole dollar, and they stopped. The dialog said $89.64. The card was charged $89.

We were lucky: the discrepancy was small and in the customer's favour. It could as easily have gone the other way, and then it is not a bug report, it is a chargeback and a conversation about whether you are trustworthy.

Why it happened

The JavaScript multiplied the monthly price by an interval multiplier. That is the same arithmetic the server did — right up until the server's version grew a rule the client's did not have.

The bug was not the rounding. The bug was that the rule had somewhere to *not* be applied.

The comment that should have warned us

The script carried this, written in good faith:

Mirrors interval_price_multiplier() in includes/billing.php.

That comment was accurate for about four months. It is also, in hindsight, a written admission that a divergence was possible and nobody had prevented it — a note saying *these two things must stay in step*, with no mechanism keeping them there.

If you find yourself writing "mirrors", "kept in sync with", or "must match", you have already lost. You are documenting a constraint instead of enforcing one.

The fix

The server now emits the exact figures it will bill, as data attributes on the form. The script reads the number rather than deriving it.

There is one implementation of the price, and the page is a view of it. The client cannot disagree with the server about the price because the client no longer computes the price.

The general principle

Anywhere a number is shown to a user and then acted on, there must be exactly one place that number is computed. Not two that agree — one.

This costs something. Emitting server-computed values as data is slightly more code than recomputing in the client, and it means a round trip where you might have avoided one. Pay it. The alternative is a class of bug that is invisible in testing — because in testing the two implementations do agree — and appears only after somebody changes one of them.

Start building

Lock a key to your domain in about five minutes.

Get started