Разработка системы скидок и акций для интернет-магазина
Ошибка в расчёте скидок может стоить магазину до 10% выручки — клиент видит неправильную цену и теряет доверие. Мы разработали движок, который исключает такие ситуации. За 5 лет мы реализовали более 30 проектов со сложным ценообразованием: от ступенчатых цен для B2B до BOGO-акций для ритейла. Наша методология гарантирует прозрачный расчёт маржи и отсутствие конфликтов правил. Разработка системы скидок занимает от 5 до 9 рабочих дней — срок зависит от набора акций и объёма товарной библиотеки.
Неправильная реализация скидок приводит к хаосу: одна акция может отменить другую, скидка на категорию не применится к подкатегории, а промокод суммируется там, где не должен. Чтобы этого избежать, мы строим чёткую иерархию и используем паттерн Visitor для расчёта.
Как избежать конфликтов правил скидок?
В любой товарной библиотеке сосуществуют несколько типов скидок. Критично задать порядок и правила совместимости. Мы используем такую приоритизацию:
| Приоритет | Тип скидки | Пример | Стекирование |
|---|---|---|---|
| 1 | Акционная цена SKU | sale_price на товаре |
Всегда применяется |
| 2 | Скидка за объём | 5% при покупке от 3 единиц | stackable |
| 3 | Скидка по категории | 10% на Electronics | exclusive |
| 4 | Скидка по сегменту | 15% для VIP-клиентов | override |
| 5 | Промокод | WELCOME10 | зависит от настройки |
Правило стекирования задаётся на уровне акции: exclusive (не совмещается), stackable (суммируется с предыдущими), override (отменяет все остальные). Благодаря этому движок принимает однозначное решение.
Модель данных акций
CREATE TABLE promotions ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, type VARCHAR(30) NOT NULL, -- 'product_discount', 'category_discount', 'volume_discount', 'bundle', 'bogo', 'tiered' priority INT DEFAULT 0, stackable BOOLEAN DEFAULT FALSE, conditions JSONB DEFAULT '{}', actions JSONB DEFAULT '{}', starts_at TIMESTAMP, ends_at TIMESTAMP, is_active BOOLEAN DEFAULT TRUE ); Поля conditions и actions в формате JSONB позволяют задавать произвольные правила без изменения схемы. Пример условия для акции «купи 3 товара из списка и получи скидку 15%»:
{ "conditions": { "min_qty": 3, "product_ids": [101, 102, 103], "user_segments": ["wholesale"] }, "actions": { "type": "percent_discount", "value": 15, "applies_to": "matching_items" } } Почему JSONB подходит для правил скидок?
JSONB-поля conditions и actions позволяют хранить произвольные правила без миграций. Это даёт гибкость: можно добавить новые типы условий (например, погодные факторы или LTV клиента) без изменения схемы базы. Кроме того, JSONB индексируется — запросы к правилам остаются быстрыми даже при 10 000 активных акций.
Движок применения скидок
Класс DiscountEngine последовательно обходит правила, отсортированные по приоритету. Мы используем паттерн Factory для создания объектов правил.
class DiscountEngine { /** @var PromotionRule[] */ private array $rules = []; public function __construct(Collection $activePromotions) { foreach ($activePromotions->sortByDesc('priority') as $promo) { $this->rules[] = PromotionRuleFactory::make($promo); } } public function apply(Cart $cart): DiscountResult { $result = new DiscountResult($cart); foreach ($this->rules as $rule) { if (!$rule->matches($cart)) continue; if (!$rule->isStackable() && $result->hasDiscount()) continue; $result->addDiscount($rule->calculate($cart)); if ($rule->isExclusive()) break; } return $result; } } Наш движок обрабатывает 1000 правил за 200 мс, что в 5 раз быстрее типичной реализации на вложенных if-else. Как отмечается в документации Laravel, паттерн Factory упрощает создание объектов без привязки к конкретным классам.
Скидка за объём (volume pricing)
Ступенчатые цены — частый запрос в B2B и B2C. Каждому количеству товара назначается своя цена, хранимая в таблице volume_tiers.
class VolumePricingRule implements PromotionRule { public function calculate(Cart $cart): array { $discounts = []; foreach ($cart->items as $item) { $tier = $this->getTier($item->product_id, $item->quantity); if ($tier) { $discounts[] = [ 'item_id' => $item->id, 'amount' => ($item->price - $tier->price) * $item->quantity, 'label' => "Скидка за объём (×{$item->quantity})", ]; } } return $discounts; } private function getTier(int $productId, int $qty): ?VolumeTier { return VolumeTier::where('product_id', $productId) ->where('min_qty', '<=', $qty) ->orderByDesc('min_qty') ->first(); } } Акционные периоды с таймером и отображение скидок
Для «горящих» акций на фронтенде отображается таймер обратного отсчёта. Акционная цена передаётся из API вместе с временем окончания. На странице товара и в каталоге показываем зачёркнутую оригинальную цену и бейдж с размером скидки. Фильтр «Только акционные» проверяет sale_price IS NOT NULL AND sale_ends_at > NOW().
Как происходит инвалидация кеша?
Акции запускаются и завершаются автоматически через Laravel Scheduler с интервалом в минуту. При активации или деактивации акции инвалидируется кеш актуальных цен. Для каталогов от 10 000 SKU используем очередь — это предотвращает пиковую нагрузку на Redis.
Что входит в работу
В рамках разработки мы:
- проектируем модель данных под ваш ассортимент (до 100 000 товаров);
- реализуем движок с поддержкой произвольных правил на JSONB;
- настраиваем административную панель для управления акциями;
- подключаем автоматический планировщик активации/деактивации;
- обеспечиваем инвалидацию кеша цен;
- создаём отчётность по эффективности акций (количество применений, сумма скидок, средний чек).
По окончании предоставляем документацию по API и консольным командам, а также обучаем вашу команду работе с панелью.
Отчётность по акциям
Административная панель показывает по каждой акции: количество применений, общую сумму скидок, средний чек с акцией и без, топ товаров по выручке. Эти данные — минимальная основа для объективной оценки и принятия решений о повторном запуске.
Этапы разработки
| Этап | Длительность | Результат |
|---|---|---|
| Анализ правил ценообразования | 1 день | Спецификация акций |
| Проектирование модели данных | 1-2 дня | Схема БД, миграции |
| Разработка движка скидок | 2-3 дня | Рабочий DiscountEngine |
| Интеграция с админ-панелью | 1-2 дня | CRUD для акций |
| Тестирование и отладка | 1-2 дня | Покрытие тестами 90%+ |
| Документация и обучение | 0.5 дня | API docs, инструкция |
Пример сложного правила: скидка для сегмента "Wholesale" на товары из категории "Electronics" с минимальным количеством 5 единиц
{ "conditions": { "user_segments": ["wholesale"], "categories": ["electronics"], "min_qty": 5 }, "actions": { "type": "percent_discount", "value": 12, "applies_to": "matching_items" } } Хотите обсудить вашу систему скидок? Напишите нам — мы подготовим оценку за один рабочий день. Закажите разработку системы скидок под ключ и получите прозрачный расчёт маржи.







