Skip to content

Ноль, который списывал всё: как проверка на истинность отдала покупателю весь баланс

Разбор бага из плагина лояльности Medusa: кнопка «убрать скидку» выдавала максимальную. Одна строка, три исправления за три дня и верная проверка, лежавшая тремя строками ниже.

v2 0 stars 0 forks 0 watchers 1 branch 0 runs Public
patched via APIv2
Как это должно было работать

В магазине на Medusa есть накопленные баллы — «складской кредит». Их можно частично потратить на заказ.

Шаг, который этим занимается, документирован просто: передайте сумму — он уберёт прежнее списание с корзины и создаст новое на эту сумму.

А чтобы вообще убрать списание — передайте ноль.

Одна строка
ts
1input.amount ? input.amount : storeCreditAccount.balance

Читается как «если сумму передали — берём её, иначе весь остаток». И это верно ровно до того дня, когда кто-то передаст ноль.

Ноль в JavaScript ложный. Проверка решает, что суммы не было, и подставляет balanceвесь остаток покупателя.

Из описания исправления #16378:

Clearing the store credit from a cart left it with a credit line for everything the customer had.

Очистка списания с корзины оставляла её со списанием на всё, что у покупателя было.

Кнопка «убрать скидку» выдавала максимальную скидку. Механика, за которую в другом месте платят маркетологам.

Лекарство лежало тремя строками ниже

Самое поучительное в этой истории — не сама ошибка, а где нашлось лекарство.

Из описания того же исправления:

Changed the guard to isDefined(input.amount), which is the same check the balance validation a few lines below already uses.

…ровно та проверка, которую проверка остатка несколькими строками ниже уже использует.

Верный образец лежал в том же файле, в пределах экрана. Просто в одном месте написали ?, а в другом — isDefined.

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

Три человека, три дня

Ошибку чинили трижды:

ДатаPR
9 августа 2026#16378
9 августа 2026#16379
11 августа 2026#16401

Разные авторы, одна строка.

Это не про несогласованность, а про диагностичность. Когда баг звучит как «я убираю списание, а он списывает всё», он воспроизводится с первой попытки и находится за минуты. Такие ошибки собирают очередь из желающих починить.

А вот баг, который звучит как «иногда сумма не та», живёт годами.

Продолжение: то же место, другая беда

Через два дня в том же шаге нашли второе — #16405. Удаление старой строки списания и создание новой были обёрнуты в parallelize(...) — выполнялись одновременно.

Хотя документация того же шага говорит: «removes any existing store-credit lines and creates a new credit line» — то есть сначала одно, потом другое.

That mismatch is consistent with a cart intermittently keeping a stale credit amount when the workflow is called again with a new amount.

Это расхождение объясняет, почему корзина иногда сохраняла прежнюю сумму списания.

Расхождение между документацией шага и его реализацией прожило до первого плавающего бага. Плавающие баги на то и плавающие, что живут долго.

Проверь у себя

1
Найти проверки на истинность у числовых полей

Опасны не все, а те, где ноль — значащее значение: сумма скидки, количество, порог, цена по акции.

bash
1# тернарные операторы и || на числовых именах
2grep -rnE '(amount|price|qty|quantity|total|balance|points|fee|limit|threshold|discount)[A-Za-z]*\s*(\?|\|\|)' src/
3
4# и условия вида if (amount)
5grep -rnE 'if \(\s*[a-z]*(amount|price|qty|count|balance)' src/

Каждое найденное место проверьте одним вопросом: что случится, если сюда придёт ноль? Если ответ «то же, что и при отсутствии» — нужна явная проверка на undefined/null.

Why: Такая ошибка не ловится ни типами, ни линтером: `number` и `number | undefined` оба проходят проверку на истинность без единого предупреждения. И тесты её обычно не ловят: ноль редко попадает в примеры, потому что выглядит неинтересно.
$grep -rnE '(amount|price|quantity|total|balance)[A-Za-z]*\s*(\?|\|\|)' src/
Check
  • Проверены все места, где ноль имеет смысл
  • Где ноль значащий — стоит явная проверка на undefined или null
  • В одном файле не соседствуют две разные проверки одного и того же
2
Сверить документацию шага с тем, что он делаетRecommended

Вторая ошибка из этой истории нашлась именно так: в описании написано «убирает, потом создаёт», в коде стояло parallelize.

Пройдите по местам, где порядок важен, и сравните три вещи: что обещает комментарий, что обещает название функции и что делает код.

bash
1grep -rn 'Promise.all\|parallelize\|allSettled' src/ | head -30
Why: Комментарий и код расходятся не в момент написания, а позже — когда код правят, а комментарий нет. Расхождение всегда значит, что один из них врёт; решить, кто именно, можно только глазами.
Check
  • Найдены места, где операции идут параллельно
  • Для каждого проверено, допустим ли любой порядок завершения
Что отсюда следует

Ноль — настоящее значение. Где он означает «ничего не делать», там нужна проверка на существование, а не на истинность. То же касается цены 0 (акция или подарок), порога 0 (бесплатная доставка всегда) и количества 0.

Если рядом есть верная проверка — берётся она. Две разные проверки одного и того же в одном файле — это не стиль, а будущая ошибка.

Документация шага — часть кода. Если написано «сначала убрать, потом создать», а в коде они идут одновременно — неправы оба, но виноват код.

Пользователь просил «1000 запросов в час», а получил «1000 запросов, потом час тишины от последней попытки». Разбор задачи в Better Auth, открытой девять месяцев, и почему её нельзя починить одной строкой.

v2Public 0 0
updated Sep 1, 2026

Разбор настоящей истории из открытого проекта: четыре стратегии блокировки, эпитафия отвергнутой пятой и коммит «Go back to the old way of doing it» через полтора года. И чем платят за решение «пометить задачу взятой».

v2Public 0 0
updated Sep 1, 2026