Монета, которой нечем заплатить
Saleor: три человека за три года просят денег без копеек, потому что таких монет нет в обращении
miki/moneta-kotoroy-nechem-zaplatit · v1
Saleor: три человека за три года просят денег без копеек, потому что таких монет нет в обращении
Saleor, расчёт цен и скидок, июль 2021 — октябрь 2024.
Saleor — открытый движок магазина, на котором работают настоящие витрины. Цены он хранит с десятичной точностью, а сколько знаков показывать — решает валюта: у иены ноль, у злотого два.
Разумно и скучно ровно до того дня, когда магазину понадобится своё.
Приходит разработчик под ником uroybd и объясняет положение, которое кодом не чинится (#7663):
In some currencies (at least in BDT), fractional payment amount is a problem (especially in cash on delivery). You can't even pay in the fractions since we don't have coins for them in circulation anymore.
В некоторых валютах (как минимум в бангладешской таке) дробная сумма — это проблема, особенно при оплате курьеру. Заплатить дробью просто нечем: таких монет больше нет в обращении.
Обратите внимание, чего он не делает: он не называет это багом. Он честно пишет, что решения у него нет:
I don't have any. However, for my current project I have to modify the total checkout amount calculation
У меня решения нет. Но в своём проекте мне придётся переписать расчёт итога корзины.
Полтора года спустя задачу заводит уже сам мейнтейнер, Ирина Карбовяк (IKarbowiak) — и заводит на свой же движок (#12040):
The discount rounding policy right now is
ROUND_DOWN, but it should useROUND_HALF_UPmode.
Скидка сейчас округляется вниз, а должна — к ближайшему.
Воспроизведение занимает пять строк: цена 19.99, скидка 25%, ожидается 14.99, получается 15.00.
Считаем вслух. Четверть от 19.99 — это 4.9975. Округлили вниз до 4.99 — значит покупатель платит 15.00 вместо 14.99. Копейка осталась магазину. И так на каждой цене, кончающейся на .99, то есть примерно на каждой цене в мире.
Починили за два дня. Сколько лет оно так работало до того, в задаче не сказано.
Ещё через полтора года приходит ilonamanole и делает ровно то, что сделал бы любой: находит настройку с говорящим именем и ставит ноль (#16273).
- Update
DEFAULT_DECIMAL_PLACES = 0in settings.py- Use GraphQL playground and run a
checkoutCreatemutation
Корзина перестаёт создаваться совсем:
{ "field": "discountAmount", "code": "INVALID" }
Мейнтейнер спрашивает, зачем это вообще понадобилось. Ответ — та же нужда, что и в 2021-м, только словами продавца:
If for e.g. I apply 15% to 3642, the price should be calculated as 3095 and not 3095.7
Если я применяю 15% к 3642, цена должна получиться 3095, а не 3095.7
The
DEFAULT_DECIMAL_PLACESshouldn't be changed. It only determines how many decimal places will be saved on DB. In the API the prices are returned with the rounding that depends on the currency, so e.g. if your prices are inJPY0 decimal places will be returned.
DEFAULT_DECIMAL_PLACES менять не следует. Он лишь задаёт число знаков в базе. В API цены отдаются с округлением по валюте: например, в иенах будет ноль знаков.
Дальше — короткая реплика, в которой всё:
My currency is RON, that means 2 decimals. So, it's not possible for me to have the prices calculated and saved in DB with no decimals?
У меня RON, а это два знака. То есть посчитать и сохранить цены без копеек мне нельзя?
Ответа на этот вопрос в задаче нет. 16 октября 2024-го, через три месяца тишины, её закрывают со статусом not planned и советом настроить канал с валютой RON — то есть ровно с теми двумя знаками, из-за которых человек и пришёл.
Три человека за три года просят одного и того же: денег без копеек — потому что копеек нет ни в кассе, ни в кошельке у курьера. Ответ каждый раз один: округление задаёт валюта, а не магазин.
Это не халатность, это последовательность: разреши каждому магазину своё округление — и налоги, отчёты и сверки с платẻжной системой начнут расходиться по копейке. Цена последовательности в том, что бангладешскому продавцу нечем дать сдачу.
А в единственном месте, где магазину всё-таки оставили решать — режим округления скидки, — оно молча забирало у покупателя копейку. Пока это не заметил не покупатель, а свой же разработчик.
Общее у всех трёх кадров одно: округление — решение о деньгах, а не о показе. Пока оно живёт в форматировании, его никто не принимает — оно случается.
Что с этим делать у себя
Ищите Math.round, toFixed, maximumFractionDigits в форматировании денег. Каждое такое место — тихое решение о деньгах, принятое без человека.
- Для каждого места названо, кто гарантирует, что округлять нечего
Возьмите цену с половиной единицы и два товара. Если карточка показывает одно, а строка — другое, витрина спорит сама с собой на глазах у гостя.
- Сценарий прогнан и результат записан
- Если расхождение есть — оно закрыто тестом, а не правкой на глазок
Если в вашей кассе копеек нет — не давайте их ввести, и объясните кассой, а не типами: «копейками в кассе не сдать».
- Правило стоит на ВСЕХ вводах денег, а не на одном
- Чтение осталось терпимым: уже сохранённое значение не роняет страницу
Скидки, бонусы и налоги родят дробные единицы честно. Тогда менять надо показ (показывать дробные, когда они есть), а не снимать проверку на вводе.
- Рядом с правилом написано, что менять при появлении процентов
Related lists
Найти в открытом проекте настоящую историю поломки, восстановить ход мысли по написанному и закончить проверкой своего кода
graphile-worker: очередь на Postgres, замеры в комментариях и настройка, не пережившая полутора лет
Medusa: защита от повтора отказывает там, где можно было ответить, и пропускает там, где повтор стоит денег
Better Auth: ограничение частоты считалось от последнего запроса, включая отклонённые, — и разблокировка не наступала никогда
Medusa: кнопка «убрать скидку» выдавала максимальную, потому что ноль в JavaScript ложный
Как Cal.com год чинил расписание, переходящее за полночь, и чем это кончилось

