Skip to content

Compare versions

From:To:
2
11¶ # Как это должно было работать
22
33 В магазине на [Medusa](https://github.com/medusajs/medusa) есть накопленные баллы — «складской кредит». Их можно частично потратить на заказ.
44
55 Шаг, который этим занимается, документирован просто: передайте сумму — он уберёт прежнее списание с корзины и создаст новое на эту сумму.
66
77 А чтобы вообще убрать списание — передайте ноль.
88¶ ## Одна строка
99
1010 ```ts
1111 input.amount ? input.amount : storeCreditAccount.balance
1212 ```
1313
1414 Читается как «если сумму передали — берём её, иначе весь остаток». И это верно ровно до того дня, когда кто-то передаст ноль.
1515
1616 Ноль в JavaScript ложный. Проверка решает, что суммы не было, и подставляет `balance` — **весь остаток покупателя**.
1717
1818 Из описания исправления [#16378](https://github.com/medusajs/medusa/pull/16378):
1919
2020 > Clearing the store credit from a cart left it with a credit line for everything the customer had.
2121 >
2222 > *Очистка списания с корзины оставляла её со списанием на всё, что у покупателя было.*
2323
2424 Кнопка «убрать скидку» выдавала максимальную скидку. Механика, за которую в другом месте платят маркетологам.
2525¶ ## Лекарство лежало тремя строками ниже
2626
2727 Самое поучительное в этой истории — не сама ошибка, а где нашлось лекарство.
2828
2929 Из описания того же исправления:
3030
3131 > Changed the guard to `isDefined(input.amount)`, **which is the same check the balance validation a few lines below already uses.**
3232 >
3333 > *…ровно та проверка, которую проверка остатка несколькими строками ниже уже использует.*
3434
3535 Верный образец лежал в том же файле, в пределах экрана. Просто в одном месте написали `?`, а в другом — `isDefined`.
3636
3737 Это типично для больших кодовых баз: две разные проверки одного и того же живут рядом годами, пока одна из них не встретит граничное значение.
3838¶ ## Три человека, три дня
3939
4040 Ошибку чинили трижды:
4141
4242 | Дата | PR |
4343 |---|---|
4444 | 9 августа 2026 | [#16378](https://github.com/medusajs/medusa/pull/16378) |
4545 | 9 августа 2026 | [#16379](https://github.com/medusajs/medusa/pull/16379) |
4646 | 11 августа 2026 | [#16401](https://github.com/medusajs/medusa/pull/16401) |
4747
4848 Разные авторы, одна строка.
4949
5050 Это не про несогласованность, а про диагностичность. Когда баг звучит как «я убираю списание, а он списывает всё», он воспроизводится с первой попытки и находится за минуты. Такие ошибки собирают очередь из желающих починить.
5151
5252 А вот баг, который звучит как «иногда сумма не та», живёт годами.
5353¶ ## Продолжение: то же место, другая беда
5454
5555 Через два дня в том же шаге нашли второе — [#16405](https://github.com/medusajs/medusa/pull/16405). Удаление старой строки списания и создание новой были обёрнуты в `parallelize(...)` — выполнялись одновременно.
5656
5757 Хотя документация того же шага говорит: «removes any existing store-credit lines **and creates** a new credit line» — то есть сначала одно, потом другое.
5858
5959 > That mismatch is consistent with a cart intermittently keeping a stale credit amount when the workflow is called again with a new amount.
6060 >
6161 > *Это расхождение объясняет, почему корзина иногда сохраняла прежнюю сумму списания.*
6262
6363 Расхождение между документацией шага и его реализацией прожило до первого плавающего бага. Плавающие баги на то и плавающие, что живут долго.
6464## Проверь у себя
65651. Найти проверки на истинность у числовых полей
6666 Опасны не все, а те, где ноль — значащее значение: сумма скидки, количество, порог, цена по акции.
6767
6868 ```bash
6969 # тернарные операторы и || на числовых именах
7070 grep -rnE '(amount|price|qty|quantity|total|balance|points|fee|limit|threshold|discount)[A-Za-z]*\s*(\?|\|\|)' src/
7171
7272 # и условия вида if (amount)
7373 grep -rnE 'if \(\s*[a-z]*(amount|price|qty|count|balance)' src/
7474 ```
7575
7676 Каждое найденное место проверьте одним вопросом: **что случится, если сюда придёт ноль?** Если ответ «то же, что и при отсутствии» — нужна явная проверка на `undefined`/`null`.
7777 $ grep -rnE '(amount|price|quantity|total|balance)[A-Za-z]*\s*(\?|\|\|)' src/
7878 why: Такая ошибка не ловится ни типами, ни линтером: `number` и `number | undefined` оба проходят проверку на истинность без единого предупреждения. И тесты её обычно не ловят: ноль редко попадает в примеры, потому что выглядит неинтересно.
7979 - [ ] Проверены все места, где ноль имеет смысл
8080 - [ ] Где ноль значащий — стоит явная проверка на undefined или null
8181 - [ ] В одном файле не соседствуют две разные проверки одного и того же
82822. Сверить документацию шага с тем, что он делает [recommended]
8383 Вторая ошибка из этой истории нашлась именно так: в описании написано «убирает, потом создаёт», в коде стояло `parallelize`.
8484
8585 Пройдите по местам, где порядок важен, и сравните три вещи: что обещает комментарий, что обещает название функции и что делает код.
8686
8787 ```bash
8888 grep -rn 'Promise.all\|parallelize\|allSettled' src/ | head -30
8989 ```
9090 why: Комментарий и код расходятся не в момент написания, а позже — когда код правят, а комментарий нет. Расхождение всегда значит, что один из них врёт; решить, кто именно, можно только глазами.
9191 - [ ] Найдены места, где операции идут параллельно
9292 - [ ] Для каждого проверено, допустим ли любой порядок завершения
9393¶ ## Что отсюда следует
9494
9595 **Ноль — настоящее значение.** Где он означает «ничего не делать», там нужна проверка на существование, а не на истинность. То же касается цены `0` (акция или подарок), порога `0` (бесплатная доставка всегда) и количества `0`.
9696
9797 **Если рядом есть верная проверка — берётся она.** Две разные проверки одного и того же в одном файле — это не стиль, а будущая ошибка.
9898
9999 **Документация шага — часть кода.** Если написано «сначала убрать, потом создать», а в коде они идут одновременно — неправы оба, но виноват код.
100
101