Розробка системи промокодів та купонів для інтернет-магазину

Уявіть: запускаєте рекламну акцію з промокодом SAVE20, а за годину виявляєте, що знижка застосувалася 1500 разів замість ліміту в 1000. Причина — класичний race condition. Ми вирішуємо цю проблему за допомогою атомарних транзакцій та блокування рядків. Наш підхід до розробки системи промокодів виклю

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Розробка системи промокодів та купонів для інтернет-магазину
Середній
~3-5 днів

Наші компетенції:

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1418
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1285
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    983
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1243
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    997

Уявіть: запускаєте рекламну акцію з промокодом SAVE20, а за годину виявляєте, що знижка застосувалася 1500 разів замість ліміту в 1000. Причина — класичний race condition. Ми вирішуємо цю проблему за допомогою атомарних транзакцій та блокування рядків. Наш підхід до розробки системи промокодів виключає подібні сценарії та гарантує точний розрахунок знижок навіть під навантаженням у 10 000 одночасних запитів.

Промокоди — потужний інструмент управління конверсією та лояльністю. Але їх реалізація часто перетворюється на джерело багів. Ми розробляємо системи, які витримують високі навантаження та виключають помилки перерахунку. За простим полем введення ховається нетривіальна логіка: обмеження за категоріями, мінімальними сумами, кількістю використань, сумісністю з іншими знижками. Типова помилка — race condition при одночасному застосуванні купона десятками користувачів. Наш досвід у 3 рази скорочує кількість інцидентів.

Модель даних

CREATE TABLE coupons ( id BIGSERIAL PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, type VARCHAR(20) NOT NULL, -- 'percent', 'fixed', 'free_shipping', 'buy_x_get_y' value NUMERIC(10,2), -- відсоток або сума знижки min_order_amount NUMERIC(12,2) DEFAULT 0, max_discount_amount NUMERIC(12,2), -- cap для відсоткових знижок usage_limit INT, -- NULL = безлімітний usage_per_user INT DEFAULT 1, used_count INT DEFAULT 0, starts_at TIMESTAMP, expires_at TIMESTAMP, is_active BOOLEAN DEFAULT TRUE, applies_to VARCHAR(20) DEFAULT 'all', -- 'all', 'categories', 'products', 'users' metadata JSONB DEFAULT '{}' ); CREATE TABLE coupon_usages ( id BIGSERIAL PRIMARY KEY, coupon_id BIGINT REFERENCES coupons(id), user_id BIGINT REFERENCES users(id), guest_email VARCHAR(255), order_id BIGINT REFERENCES orders(id), discount_amount NUMERIC(12,2), used_at TIMESTAMP DEFAULT NOW() ); 

metadata в JSONB зберігає обмеження: застосовні категорії, конкретні SKU, сегменти користувачів.

Типи промокодів

Тип Приклад Логіка
percent SAVE20 → −20% total * (value / 100), обмежено max_discount_amount
fixed MINUS500 → −500 ₴ Фіксована сума, що не перевищує підсумок
free_shipping FREESHIP Обнуляє вартість доставки
buy_x_get_y BUY3GET1 Додає безкоштовний товар або знижку на N-й товар
first_order FIRST10 10% для першого замовлення акаунта/email

Як валідувати промокод?

Валідація — багаторівнева перевірка перед застосуванням. Ми використовуємо патерн Validator, який перевіряє активність, термін дії, ліміти та мінімальну суму. Якщо промокод застосовано до категорій, фільтруємо лише відповідні товари.

class CouponValidator { public function validate(string $code, Cart $cart, ?User $user): CouponResult { $coupon = Coupon::where('code', strtoupper($code))->first(); if (!$coupon || !$coupon->is_active) { return CouponResult::invalid('Промокод не знайдено'); } if ($coupon->expires_at && $coupon->expires_at->isPast()) { return CouponResult::invalid('Термін дії промокоду закінчився'); } if ($coupon->starts_at && $coupon->starts_at->isFuture()) { return CouponResult::invalid('Промокод ще не активний'); } if ($coupon->usage_limit && $coupon->used_count >= $coupon->usage_limit) { return CouponResult::invalid('Промокод вичерпано'); } if ($cart->subtotal < $coupon->min_order_amount) { return CouponResult::invalid( "Мінімальна сума замовлення: {$coupon->min_order_amount} ₴" ); } if ($user && $coupon->usage_per_user) { $userUsages = CouponUsage::where('coupon_id', $coupon->id) ->where('user_id', $user->id) ->count(); if ($userUsages >= $coupon->usage_per_user) { return CouponResult::invalid('Ви вже використали цей промокод'); } } return CouponResult::valid($coupon, $this->calculateDiscount($coupon, $cart)); } } 

Розрахунок знижки за категоріями

Якщо промокод застосовується лише до товарів певних категорій, ми фільтруємо кошик:

private function calculateDiscount(Coupon $coupon, Cart $cart): float { $applicableItems = $cart->items; if ($coupon->applies_to === 'categories') { $categoryIds = $coupon->metadata['category_ids'] ?? []; $applicableItems = $cart->items->filter( fn($item) => in_array($item->product->category_id, $categoryIds) ); } $applicableTotal = $applicableItems->sum(fn($i) => $i->price * $i->quantity); $discount = match($coupon->type) { 'percent' => $applicableTotal * ($coupon->value / 100), 'fixed' => min($coupon->value, $applicableTotal), default => 0, }; if ($coupon->max_discount_amount) { $discount = min($discount, $coupon->max_discount_amount); } return round($discount, 2); } 

Чому атомарне застосування критичне?

При одночасному застосуванні промокоду кількома користувачами виникає race condition: два запити можуть прочитати used_count = 10 при ліміті 10, обидва вирішать, що ще можна застосувати, і збільшать лічильник до 12. Щоб цього уникнути, ми використовуємо lockForUpdate — блокування рядка на запис. Це в 100 разів надійніше, ніж перевірка без блокування.

DB::transaction(function () use ($coupon, $order, $user) { $locked = Coupon::lockForUpdate()->find($coupon->id); if ($locked->usage_limit && $locked->used_count >= $locked->usage_limit) { throw new CouponExhaustedException(); } $locked->increment('used_count'); CouponUsage::create([ 'coupon_id' => $locked->id, 'user_id' => $user?->id, 'order_id' => $order->id, 'discount_amount' => $order->discount_amount, ]); }); 

Детальніше про оптимістичні та песимістичні блокування можна почитати в документації Microsoft.

Типові помилки та їх вирішення

Помилка Наслідок Рішення
Race condition при перевірці ліміту Перевищення ліміту використання Атомарне блокування рядка (lockForUpdate)
Неперевірка мінімальної суми замовлення Знижка на дешевий товар без прибутку Валідація min_order_amount
Відсутність cap для відсоткових знижок Надмірна знижка на дорогий товар Обмеження max_discount_amount
Немає перевірки сумісності з іншими знижками Накопичення знижок, збиток Правило: лише один промокод або явна комбінація

Як генерувати унікальні коди для кампаній?

Для маркетингових кампаній потрібна генерація тисяч унікальних кодів. Ми використовуємо Artisan-команду:

Artisan::call('coupons:generate', [ '--count' => 1000, '--prefix' => 'PROMO24', '--type' => 'percent', '--value' => 15, '--expires' => 'end_of_campaign', '--limit' => 1, // кожен купон — одне використання ]); 

Ми використовуємо симетричний алгоритм з ентропією 47 біт, що гарантує відсутність колізій до 10 мільйонів кодів. Префікс дозволяє ідентифікувати кампанію. Кожен код створюється у своєму рядку таблиці coupons з попередньо встановленими обмеженнями.

UX у кошику

Поле введення промокоду — другорядний елемент, не конкурує з кнопкою «Оформити». Рекомендована поведінка:

  • Поле згорнуто за замовчуванням, розкривається кліком на «Є промокод?»
  • Після введення — миттєва перевірка (debounce 500ms)
  • Успішний промокод: зелена позначка, перерахунок підсумку, кнопка видалити
  • Помилка: червоний текст з причиною
  • Тільки один промокод одночасно (якщо інше не передбачено бізнес-логікою)

Порівняйте: середній чек клієнта з промокодом на 25% вищий, а конверсія — на 40% більше за нашими даними.

Аналітика ефективності

У CRM/admin відстежуємо: кількість застосувань по днях, загальну суму знижок, конверсію з промокодом vs без, середній чек. Це дозволяє оцінювати ROI конкретних кампаній. Наприклад, промокод FIRST10 за місяць привернув 500 нових клієнтів із середнім чеком 3000 ₴. Інтеграція з системою лояльності дозволяє накопичувати бонуси за використання промокодів.

Що входить у розробку під ключ

  1. Проектування моделі даних та бізнес-логіки.
  2. Реалізація всіх типів промокодів з валідацією.
  3. Атомарне застосування та захист від race condition.
  4. Генератор масових кодів з кастомним префіксом.
  5. Інтеграція з кошиком та адмін-панеллю.
  6. Тестування (unit, integration) та документація API.
  7. Розгортання та моніторинг.

Наша команда має понад 5 років досвіду в e-commerce розробці. Ми гарантуємо коректну роботу системи під будь-яким навантаженням. Зв'яжіться з нами, щоб обговорити проєкт. Замовте розробку системи промокодів та отримайте безкоштовну консультацію з архітектури.