Във всеки платежен проект има по един такъв ред. Написан е в тих следобед от напълно разумен човек:
long cents = (amount * 100).longValue()
Компилира се. Минава теста с 10.00. Качва се на продукция. И докато съществува, тихо взима по една стотинка от част от плащанията.
В Bakshish го имаше на шест места. Намерихме ги при един одит и ги махнахме всичките. Ето с какво ги заменихме - и кои са другите две правила, които вървят с него.
Правило 1: парите се закръглят, не се режат
longValue() не закръглява. Отрязва. Вземете 5% такса на платформата върху бакшиш от 5,55:
- 5,55 x 0,05 = 0,2775
- В центове: 27,75
longValue()казва 27. Закръгляването "нагоре" казва 28.
Една стотинка. На едно плащане не се вижда. За месец всяка такса, която не е цяло число, върви в една и съща посока: винаги надолу. Това не е шум, а систематична грешка - и счетоводната ви книга започва да се разминава с платежния доставчик с малко повече всеки ден.
Решението е една функция, през която минава всяка сума:
amount.setScale(2, RoundingMode.HALF_UP).movePointRight(2).longValueExact()
Две подробности тук са от значение:
HALF_UPе написано изрично - никой не гадае какво закръгляване се има предвид.longValueExact()хвърля грешка, ако числото не се побира, вместо тихо да върне грешна стойност.
Шумна грешка в три следобед е за предпочитане пред тиха в сметките.
Правило 2: десетичните знаци не винаги са два
Нашата функция получава и валутата. Еврото има два знака след запетаята. Японската йена няма нито един: 500 йени е 500, не 50000. Умножете всичко по 100 и бакшиш от 500 йени става такса от 50 000.
Ние работим в евро, затова това никога не ни се е случвало. Но точно затова му е мястото в помощната функция: в деня, в който някой добави нова валута, грешката не трябва да го чака. Списък с валутите без десетични знаци и fractionDigits(currency) са точно дванайсет реда код.
Правило 3: записвайте плащането точно веднъж
Третото правило не е за аритметика. То е за ситуациите, в които едно и също плащане пристига два пъти.
Бакшишът се потвърждава от браузъра, но платежният доставчик го съобщава и чрез уебхук (webhook) - в случай че браузърът така и не е върнал отговор. Двата пътя свършват в един и същ метод. Обикновено единият печели със секунда-две. Понякога обаче идват едновременно.
Затова записването на плащане трябва да е безопасно при повторение (идемпотентно):
- Потърсете плащането по номера му при доставчика. Ако вече е записано, върнете записа и спрете.
- В противен случай го вмъкнете с уникален индекс върху този номер в базата данни.
- Ако две заявки минат заедно покрай стъпка 1, индексът отхвърля втората. Тя опитва още веднъж, намира вече записания победител и го връща.
Проверката в кода е просто любезност. Индексът е истинската гаранция. Искате базата данни да каже „не“, защото само тя вижда абсолютно всички заявки.
Със събитията от уебхуци е същото: всяко се пази по източник и номер на събитие. Така доставчик, който изпрати събитие повторно (а всички го правят рано или късно), не може да плати на някого два пъти.
Накратко
Кодът за пари е код, в който скучните случаи имат голямо значение. Правилата са прости:
- Преобразувайте стойностите на едно място, закръглявайте изрично, чупете се шумно.
- Нека валутата решава колко са десетичните знаци.
- Направете „запиши това плащане“ безопасно за двойно извикване и оставете базата данни да го налага.
Нищо от това не е прекалено хитро. Именно в това е смисълът. Хитростта е за онези части на системата, където една стотинка няма значение.
Потърсете longValue() в кода си още днес. Ако стои до цена, имате какво да оправите преди обяд.
