We started selling in Japan in March. By the end of the first day, fulfilment had put every order paid in yen on hold as underpaid, and customers who had been charged the correct amount were getting emails asking them to complete their payment.
Our payment provider reports amounts in a currency's smallest unit, as an integer. For pounds and euros that is pence and cents, two decimal places. Yen has no minor unit at all, so a 3,400 yen payment arrives as 3400. Our webhook handler, written four years earlier when we sold only in pounds and euros, divided every amount by a hundred before storing it. The order said 3,400 yen, the payment said 34 yen, and the rule that holds underpaid orders did exactly what it was built to do.
Two hundred and six orders were held over twenty six hours. The refund path had the opposite bug, multiplying by a hundred, and we only escaped that one because the provider rejects a refund larger than the original charge. Their validation caught our mistake. Ours did not.
Earlier I wrote here that behind our API boundary every amount is an integer number of minor units. That is still how we work. What I had not said, because we had not learned it yet, is that the size of a minor unit belongs to the currency, and a hundred is only one of the possible answers. Yen and won use none. Bahraini dinar and Kuwaiti dinar use three.
The conversion now happens in one place, our shared money library, which reads the exponent from the ISO 4217 table and refuses a currency it does not know. An amount is never passed around as a bare number: it travels as a value and a currency together, and the only way to turn it into something else is through that library. Contract tests run the whole payment flow in a currency with zero, two and three decimal places. And a daily job compares, per currency, the total of orders with the total of payments, which would have shown this one as a gap of ninety nine percent on the first morning.
The handler had the currency code right next to the amount on every single call for four years. It just never needed to read it until the day it did.
– Sergey Shinder
Top comments (0)