Prorating a Subscription Charge Without Floating Point

Prorating a Subscription Charge Without Floating Point

October 9, 2026

A customer on a yearly plan adds four seats in month five. What do you charge them?

The answer is obvious — the remaining fraction of the year, times four — and it is where a surprising number of billing systems quietly lose money, gain money, or produce an invoice that does not reconcile with what the payment gateway actually took.

Float is the wrong type

Table showing floating point precision errors in currency calculations
Floating point arithmetic on a rupee amount.

This is well known and still widespread. A floating point number cannot represent most decimal fractions exactly, so every operation on a money value stored as a float introduces a small error, and the errors accumulate in a direction you did not choose.

For one charge it rounds away and nobody notices. Across twenty seats, twelve months and a few upgrades, it becomes a figure in your accounts that does not match the figure in your gateway, and finding out which one is wrong takes a day.

The fix is not better rounding. It is not storing money in a float at all.

Store paise, not rupees

Keep every money value as an integer in the smallest unit. Rs 499 is 49900. There are no fractions, so there is nothing to round until you deliberately round.

If the money is already in a float column, that migration is the first job, and it is worth doing before the proration logic rather than after. Correct arithmetic on a corrupted store does not help.

The calculation

Table showing a prorated charge calculated in integer paise
Integer arithmetic, with the division at the end.

Three rules, and the whole thing falls out of them.

Multiply before dividing. 49900 * 220 / 365 keeps full precision until the final step. 49900 * (220 / 365) throws the precision away in the brackets and then multiplies the error.

Divide once. Every division is a rounding decision. One per charge is manageable; three is a mystery.

Round deliberately, and write down which way. Integer division truncates, which rounds in the customer’s favour. That is a defensible choice and it should be a choice, stated in a comment, not an accident of the operator you used.

For four seats, calculate per-seat and multiply, or calculate the total and divide once. Pick one and use it everywhere, because the two differ by a paisa or two and reconciliation is easier when everything was computed the same way.

Days, not months

“Five months used, seven remaining” is tempting and wrong, because months are different lengths and a term that starts on the 31st has no clean answer.

Work in days. Days remaining over days in the term. It handles leap years, it handles a term starting on any date, and it gives the customer an answer they can verify by counting.

Decide one boundary question and apply it consistently: is the day of the change charged at the old rate or the new one? Either is fine. Both, in different parts of the code, is not.

What to store

Store the inputs, not just the result.

When a prorated charge is created, record the base amount, the days remaining, the days in the term, and the resulting figure. Then when someone asks in March why they were charged Rs 300.76, the answer is in the row rather than reconstructed from a calendar and an assumption about the code at the time.

This matters more than it sounds. Prorating logic changes. A charge from eight months ago was computed by code that no longer exists, and without the inputs there is no way to explain it.

The gateway has its own opinion

Whatever you compute, the payment gateway charges an integer in the smallest unit, and that is the number that actually left the customer’s account.

Store the gateway’s amount alongside yours and compare them. If they ever differ, the gateway is right about what happened and your record is wrong, and you want to know that from a mismatch alert rather than from a customer.

Make the webhook idempotent

Adjacent to proration and the thing that actually causes duplicate charges in practice: payment webhooks arrive more than once. Gateways retry, networks fail halfway, and a success callback can land twice.

Key the activation on the gateway’s own payment id and make applying it twice do nothing the second time. Without that, a retried webhook extends a subscription by two terms, and the customer notices long before you do.

A short checklist

  • Money is an integer in paise, everywhere, including in memory.
  • Multiply first, divide last, divide once.
  • Days remaining over days in term, never months.
  • Rounding direction is a decision with a comment next to it.
  • Store the inputs beside the result.
  • Reconcile against the gateway amount.
  • Webhook handlers are idempotent on the gateway’s payment id.

None of it is difficult. All of it is much easier to put in before the first real customer than to retrofit around eighteen months of invoices that were computed slightly differently.