Skip to content

Монета, которой нечем заплатить

Saleor: три человека за три года просят денег без копеек, потому что таких монет нет в обращении

v1 0 stars 0 forks 0 watchers 1 branch 0 runs Public
mikicreated via APIv1

Saleor, расчёт цен и скидок, июль 2021 — октябрь 2024.

Saleor — открытый движок магазина, на котором работают настоящие витрины. Цены он хранит с десятичной точностью, а сколько знаков показывать — решает валюта: у иены ноль, у злотого два.

Разумно и скучно ровно до того дня, когда магазину понадобится своё.

Кадр первый: 20 июля 2021. Монеты вышли из обращения

Приходит разработчик под ником 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

У меня решения нет. Но в своём проекте мне придётся переписать расчёт итога корзины.

Кадр второй: 15 февраля 2023. Копейка в другую сторону

Полтора года спустя задачу заводит уже сам мейнтейнер, Ирина Карбовяк (IKarbowiak) — и заводит на свой же движок (#12040):

The discount rounding policy right now is ROUND_DOWN, but it should use ROUND_HALF_UP mode.

Скидка сейчас округляется вниз, а должна — к ближайшему.

Воспроизведение занимает пять строк: цена 19.99, скидка 25%, ожидается 14.99, получается 15.00.

Считаем вслух. Четверть от 19.99 — это 4.9975. Округлили вниз до 4.99 — значит покупатель платит 15.00 вместо 14.99. Копейка осталась магазину. И так на каждой цене, кончающейся на .99, то есть примерно на каждой цене в мире.

Починили за два дня. Сколько лет оно так работало до того, в задаче не сказано.

Кадр третий: 5 июля 2024. «Просто уберите копейки»

Ещё через полтора года приходит ilonamanole и делает ровно то, что сделал бы любой: находит настройку с говорящим именем и ставит ноль (#16273).

  1. Update DEFAULT_DECIMAL_PLACES = 0 in settings.py
  2. Use GraphQL playground and run a checkoutCreate mutation

Корзина перестаёт создаваться совсем:

{ "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_PLACES shouldn'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 in JPY 0 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 — то есть ровно с теми двумя знаками, из-за которых человек и пришёл.

Цена решения

Три человека за три года просят одного и того же: денег без копеек — потому что копеек нет ни в кассе, ни в кошельке у курьера. Ответ каждый раз один: округление задаёт валюта, а не магазин.

Это не халатность, это последовательность: разреши каждому магазину своё округление — и налоги, отчёты и сверки с платẻжной системой начнут расходиться по копейке. Цена последовательности в том, что бангладешскому продавцу нечем дать сдачу.

А в единственном месте, где магазину всё-таки оставили решать — режим округления скидки, — оно молча забирало у покупателя копейку. Пока это не заметил не покупатель, а свой же разработчик.

Общее у всех трёх кадров одно: округление — решение о деньгах, а не о показе. Пока оно живёт в форматировании, его никто не принимает — оно случается.

Что с этим делать у себя

1
Найти, где округление происходит при ПОКАЗЕ

Ищите Math.round, toFixed, maximumFractionDigits в форматировании денег. Каждое такое место — тихое решение о деньгах, принятое без человека.

Why: Разница между показанным и списанным не исчезает от округления — она становится невидимой.
Check
  • Для каждого места названо, кто гарантирует, что округлять нечего
2
Проверить, что сумма округлённых частей равна округлённой сумме

Возьмите цену с половиной единицы и два товара. Если карточка показывает одно, а строка — другое, витрина спорит сама с собой на глазах у гостя.

Why: Это самый быстрый способ увидеть скрытое округление, не читая код.
Check
  • Сценарий прогнан и результат записан
  • Если расхождение есть — оно закрыто тестом, а не правкой на глазок
3
Перенести решение об округлении на ввод

Если в вашей кассе копеек нет — не давайте их ввести, и объясните кассой, а не типами: «копейками в кассе не сдать».

Why: На вводе решение видит человек и может его исправить. На показе копейка просто исчезает, и спорить с этим некому.
A person is needed here: Есть ли в вашей валюте и вашей кассе дробные единицы в реальном обороте? answer from experience
Check
  • Правило стоит на ВСЕХ вводах денег, а не на одном
  • Чтение осталось терпимым: уже сохранённое значение не роняет страницу
4
Записать, что делать, когда появятся процентыRecommended

Скидки, бонусы и налоги родят дробные единицы честно. Тогда менять надо показ (показывать дробные, когда они есть), а не снимать проверку на вводе.

Why: Следующий разработчик упрẻтся в проверку и начнёт с того, что её снимет, — если рядом не написано, что делать вместо этого.
Check
  • Рядом с правилом написано, что менять при появлении процентов

Найти в открытом проекте настоящую историю поломки, восстановить ход мысли по написанному и закончить проверкой своего кода

v4Public 0 0
updated Sep 1, 2026

graphile-worker: очередь на Postgres, замеры в комментариях и настройка, не пережившая полутора лет

v2Public 0 0
updated Sep 1, 2026

Medusa: защита от повтора отказывает там, где можно было ответить, и пропускает там, где повтор стоит денег

v1Public 0 0
updated Sep 1, 2026

Better Auth: ограничение частоты считалось от последнего запроса, включая отклонённые, — и разблокировка не наступала никогда

v1Public 0 0
updated Sep 1, 2026

Medusa: кнопка «убрать скидку» выдавала максимальную, потому что ноль в JavaScript ложный

v1Public 0 0
updated Sep 1, 2026

Как Cal.com год чинил расписание, переходящее за полночь, и чем это кончилось

v2Public 0 0
updated Sep 1, 2026