Розробка сервісу купонів та скидок
Купони та скидки — це не просто поле «введіть промокод» на сторінці чекауту. Це система правил, яка управляє ціноутворенням: кому, коли, на що та у якому розмірі надавати скидку. Погано спроектована система перетворюється в дири для злоупотреб та хаос в аналітиці. Добре спроектована — в інструмент точкового маркетингу.
Типологія скидок
Перш ніж писати код, потрібно зафіксувати модель:
Купони (промокоди) — користувач вводить код вручну або посилання застосовує його автоматично. Код унікален або багаторазовий, привязаний до правила скидки.
Автоматичні скидки — застосовуються без кода при виконанні умов: «всі товари категорії X зі скидкою 15% у п'ятницю», «скидка 500 руб. від замовлення на 3000».
Накопичні програми — скидка залежить від історії покупок клієнта (кешбэк, бали, рівні лояльності).
Скидки по групах — оптові ціни для B2B-клієнтів, скидки для сотрудників, партнерські умови.
Модель даних
discount_rules (
id, name, type, -- coupon | automatic | loyalty
discount_type, -- percentage | fixed_amount | free_shipping | bxgy
discount_value NUMERIC,
min_order_amount NUMERIC,
min_qty INT,
max_uses INT, -- NULL = без обмежень
max_uses_per_user INT,
starts_at TIMESTAMPTZ,
ends_at TIMESTAMPTZ,
is_active BOOLEAN,
stackable BOOLEAN -- можна ли комбінувати з іншими
)
discount_conditions (
id, rule_id,
condition_type, -- product | category | tag | user_group | first_order
condition_operator, -- in | not_in | gte | lte
condition_value JSONB
)
coupons (
id, rule_id, code VARCHAR(32),
usage_count INT DEFAULT 0,
is_single_use BOOLEAN
)
coupon_uses (
id, coupon_id, order_id, user_id, used_at,
discount_amount NUMERIC -- записана у момент застосування
)
Розділення discount_rules та coupons дозволяє одному правилу мати много кодів (bulk generation для email-кампаній) або один код з різними обмеженнями.
Генерація купонів пачками
Для email-розсилок потрібні унікальні коди — по одному на кожного одержувача:
function generateCouponBatch(int $ruleId, int $count): array {
$codes = [];
while (count($codes) < $count) {
$code = strtoupper(Str::random(8)); // A-Z0-9, 8 символів
if (!Coupon::where('code', $code)->exists()) {
$codes[] = ['rule_id' => $ruleId, 'code' => $code, 'is_single_use' => true];
}
}
Coupon::insert($codes);
return array_column($codes, 'code');
}
Для великих розсилок (100 000+ кодів) генеруйте заздалегідь з перевіркою унікальності через індекс, а не SELECT EXISTS в циклі.
Валідація та застосування купона
При вводі кода в чекауті перевірте:
- Код існує та активен
- Дата початку/закінчення
- Ліміт використань не перевищен (
max_uses) - Користувач не вичерпав ліміт (
max_uses_per_user) - Сума корзини >=
min_order_amount - Товари в корзині відповідають умовам (
discount_conditions)
Перевірка повинна бути атомарною при застосуванні — race condition можливий, якщо два запросити одночасно застосовують останній доступний купон. Рішення:
UPDATE coupons
SET usage_count = usage_count + 1
WHERE code = :code
AND usage_count < max_uses -- для single-use: usage_count < 1
RETURNING id;
-- якщо 0 рядків — купон уже використаний
UPDATE ... RETURNING всередині транзакції виключає race condition.
Расчет скидки для корзини
Скидка рахується на сервері, ніколи не довіряємо клієнтскому расчету. Алгоритм:
1. Отримати застосовані правила (автоматичні + купон)
2. Для кожного правила визначити eligible позиції (з урахуванням умов)
3. Застосувати скидки у порядку пріоритету
4. Якщо stackable=false — застосуємо тільки найбільшу скидку
5. Повернути breakdown: яка скидка застосована до кожної позиції
Breakdown важливий для відображення користувачу («–500 руб. по купону SAVE500») та для аналітики.
BxGy (Buy X Get Y) — «купи 3, отримай 4-й у подарунок». Реалізується окремим типом правила: при qty >= X додаємо в корзину товар Y з нульовою ціною або знижуємо ціну N-ої одиниці.
Автоматичні скидки та пріоритети
Кілька автоматичних правил можуть спрацьовувати одночасно. Потрібна політика:
- Перше совпавшее — застосовується правило з найвищим пріоритетом
- Найкраща скидка — застосовується то правило, яке дає найбільшу вигоду
-
Всі сумісні — застосовуються всі правила з
stackable=true
Політика прописується на рівні магазина та може відрізнятися для різних типів правил.
Аналітика та звітність
Без аналітики маркетинг летить вслід за очима. Базовий набір метрик:
| Метрика | SQL |
|---|---|
| Використань купона | SELECT COUNT(*) FROM coupon_uses WHERE coupon_id = ? |
| Середній розмір скидки | SELECT AVG(discount_amount) FROM coupon_uses WHERE ... |
| Revenue з урахуванням скидки | SUM(order.total) vs SUM(order.total + discount_amount) |
| Конверсія з купоном vs без | Порівняння CR для сесій з applied_coupon та без |
Для маркетолога — дашборд з фільтрацією по періоду, типу скидки, каналу (utm_source).
Попередження злоупотреб
- Один купон на замовлення — стандартне обмеження, але якщо дозволен стек, потрібна явна конфігурація
- Верифікація email перед застосуванням скидки «для нових клієнтів» — інакше створять 100 аккаунтів
- Rate limiting на endpoint застосування купона — захист від брутфорсу кодів
- Алерти при різкому зростанні використань одного купона — можлива утечка
Кабінет маркетолога
Інтерфейс для управління акціями повинен дозволяти:
- Створювати правила скидок з візуальним конструктором умов
- Генерувати та виводити CSV пачки купонів
- Переглядати статистику по кожній акції в реальному часі
- Деактивувати акцію одним кліком (важливо при помилках в настройці)
Терміни
- Базова система купонів (промокод, відсоток/сума, дата закінчення): 1–2 тижні
- Повноцінна система (умови по категоріях/товарах, автоматичні скидки, BxGy, аналітика, кабінет маркетолога): 3–5 тижнів
- Програма лояльності з балами та рівнями додає 3–4 тижні







