Скидки в Spree 6: типизированные строки и COUNT вместо счётчика
Разбор промо-подсистемы Spree по коду: STI-реестр правил, полный снимок скидки, наибольший остаток — и лимит, который переполняется при параллельных заказах.
miki/skidki-v-spree-6-tipizirovannye-stroki-i-count-vmesto-schetc · v1
Разбор промо-подсистемы Spree по коду: STI-реестр правил, полный снимок скидки, наибольший остаток — и лимит, который переполняется при параллельных заказах.
Разбор по срезу e1839c40630742ed5f497a15188a37294d31717b.
Важное предупреждение: модели Spree::Adjustment, про которую написаны все старые статьи, в этом коммите больше нет. Её заменили типизированные строки — Spree::Discount, Spree::TaxLine, Spree::Fee. Всё ниже про эту, новую архитектуру 6.0.
Главный контраст захода: снимок скидки у Spree самый полный из четырёх движков, а защита лимита — самая слабая.
Архитектура правил
Реестр — обычный массив в конфиге Rails, куда расширение дописывает свой класс из инициализатора:
В коробке тринадцать правил, среди них FirstOrder и OneUsePerUser.
Обычная строковая колонка без validates :inclusion — хотя у соседних CollectionRule и TaxonRule такая валидация есть.
Предел применений
То есть на каждой проверке выполняется SELECT COUNT(DISTINCT COALESCE(order_group_id, id)) FROM spree_discounts JOIN spree_orders.
Поиск with_advisory_lock по репозиторию не даёт ни одного совпадения; SELECT ... FOR UPDATE на промоакции тоже нет.
Гашение одноразовых кодов — обычный update_all, а не условный UPDATE с проверкой числа строк:
Один раз на покупателя
Зато есть встроенное правило FirstOrder — единственный из четырёх движков, у кого оно вообще есть.
Снимок в заказе
Комментарий к модели говорит всё:
И в схеме — CHECK:
Снимок снимается явно:
Скидка и доставка
В калькуляторе:
В действии:
В адъюстере — против остаточной базы, чтобы скидки не компаундились:
Бесплатная доставка — отдельный discount_scope, всегда 100 % стоимости, без калькулятора:
Копейки
Одиннадцать строк, которые стоит унести целиком:
Вызов — в целых копейках, с ограничением суммой баз:
Момент оформления
А перед созданием заказа — сверка с ожиданием клиента:
А после размещения деньги замораживаются: step :regenerate_typed_rows unless money_frozen?
А контроллер принимает такой код с отдельным состоянием:
Полный снимок (amount, label, code, value, value_type) с обнуляемыми FK и CHECK на знак. Алгоритм наибольшего остатка. Эшелонированный клампинг с идеей остаточной базы. Пересчёт с нуля внутри замка плюс cart_changed при расхождении с ожиданием клиента. Заморозка денег после размещения. Сохранённый код, который сработает позже.
Лимит применений не атомарен, и это не мелочь: COUNT без блокировки, замок на корзине, update_all без проверки числа строк. Если вы пишете своё — это тот случай, когда чужое решение надо не копировать, а исправить.
STI-реестр правил: вся eligibility гоняется в Ruby при каждом пересчёте, match_policy плоский и не валидируется, одно правило каждого типа на промо. Акции на конкретные товары через четыре many-to-many. Мультивендорность (order_group_id, sibling orders).
Related lists
Разбор промо-подсистемы Vendure по коду: пессимистическая блокировка с документированным поведением по СУБД, prorate на сорок строк и снимок без процента.
Разбор скидок Saleor по коду: где уникальный индекс закрыл гонку, где счётчик её не закрыл, и зачем в снимке заказа лежит процент.
Разбор промо-модуля Medusa по коду: блокировки строк, снимок скидки в заказе, и место, где акция уходит в заказ, но не списывается.

