Ваучеры в Saleor: три уровня модели и открытая гонка
Разбор скидок Saleor по коду: где уникальный индекс закрыл гонку, где счётчик её не закрыл, и зачем в снимке заказа лежит процент.
miki/vauchery-v-saleor-tri-urovnya-modeli-i-otkrytaya-gonka · v1
Разбор скидок Saleor по коду: где уникальный индекс закрыл гонку, где счётчик её не закрыл, и зачем в снимке заказа лежит процент.
Разбор по срезу 0a11eb911e006199daa1352ff5b76a07214f43fa (ветка main); библиотека денег — mirumee/prices на 95fd4436.
Модель трёхуровневая, и это ключ ко всему остальному:
Voucher— правило (тип, даты, флаги). Денег в нём нет.VoucherCode— сам код-строка со своим счётчикомused. Кодов у одного ваучера может быть много.VoucherChannelListing— сумма и минимальный чек на канал.
Плюс VoucherCustomer — факт «этот адрес уже применял этот код».
Главный вывод захода: в одном месте гонка закрыта надёжно и почти бесплатно — уникальным индексом. В соседнем, где счётчик, — не закрыта.
Где считается скидка
Предел применений
Инкремент через F(), то есть одним UPDATE:
А проверка лимита — подзапрос без блокировки, по сумме used по всем кодам ваучера:
Проверка лимита — строки 566–571, блокировка — 586–591. А внешняя транзакция блокирует чекаут, а не ваучер:
Один раз на покупателя
Держит это ограничение в модели:
«Только для новых покупателей» в Saleor нет: грепы по first_order, new_customer, only_for_new не дают ничего. Ближайшее по смыслу — single_use (код сгорает глобально) и only_for_staff.
Снимок в заказе
Когда менеджер правит уже размещённый заказ, скидка считается по снимку, а не по нынешнему ваучеру:
И вызывается это с явным флагом:
Скидка и доставка
Итог тоже защищён, и скидка вычитается из подытога, а не из доставки:
У ваучера на доставку — свои дополнительные проверки, которых нет у остальных типов: требуется выбранный способ доставки и страна из белого списка.
Округление
Библиотека prices по умолчанию округляет вниз:
Saleor везде передаёт своё:
А точность валюты берётся из babel, а не хардкодом «два знака».
В докстринге прямым текстом: «Ensure that the sum of discounts is equal to the discount amount».
Момент оформления
TTL по умолчанию — час. При пересчёте ваучер валидируется заново и молча снимается, если стал неприменим:
А в момент оформления — явный отказ:
_release_checkout_voucher_usage вызывается из пяти мест: ошибка налогов, нехватка товара, неприменимая подарочная карта, провал оплаты, удаление черновика.
А от двойного инкремента одного чекаута стоит флаг:
Срок действия
В API это DateTime, а не Date. Если админ поставил end_date = 2026-09-01T00:00:00Z, ваучер умирает в полночь, а не в конце первого сентября.
Единственная валидация — конец не раньше начала, причём равенство разрешено:
Уникальный индекс вместо блокировки для «один раз на клиента» — главное. Дублирование кода строкой везде, где он влияет на деньги, при SET_NULL на внешнем ключе. Хранение value_type + value и amount_value, а не «или-или». Явное округление поверх дефолта библиотеки. Симметричный возврат счётчика.
Мультиканальность: из-за VoucherChannelListing простой вопрос «сколько скидка» требует джойна. Разделение Voucher / VoucherCode — ради десяти тысяч кодов с общим лимитом; побочный эффект — неатомарная проверка лимита. SPECIFIC_PRODUCT с четырьмя many-to-many. apply_once_per_order (скидка на самую дешёвую позицию). TTL пересчёта цен.
«Только для новых покупателей». Ограничение по дням недели или времени суток (обеденные акции). Стек нескольких кодов — и это запрещено нарочно: # only one voucher can be applied.
Related lists
Разбор промо-подсистемы Vendure по коду: пессимистическая блокировка с документированным поведением по СУБД, prorate на сорок строк и снимок без процента.
Разбор промо-подсистемы Spree по коду: STI-реестр правил, полный снимок скидки, наибольший остаток — и лимит, который переполняется при параллельных заказах.
Разбор промо-модуля Medusa по коду: блокировки строк, снимок скидки в заказе, и место, где акция уходит в заказ, но не списывается.
Тринадцать проверок, собранных из кода Medusa, Saleor, Spree и Vendure — включая две гонки, которые у зрелых движков открыты до сих пор.
Как проект живёт и принимает чужие правки, и кто в нём встречался в разборах — по поступкам, датам и ссылкам

