Разработка системы скидок и акций для интернет-магазина
Ошибка в расчёте скидок может стоить магазину до 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"
}
}Хотите обсудить вашу систему скидок? Напишите нам — мы подготовим оценку за один рабочий день. Закажите разработку системы скидок под ключ и получите прозрачный расчёт маржи.







