Laravel Discount: промокоди, ліміти використання та stacking знижок

Перекладено ШІ 0 Laravel News 12 серпня, 2026

Пакет Laravel Discount спрощує впровадження складних акцій та купонів через зручні Eloquent-моделі з підтримкою лімітів і стекування. Дізнайтеся, як автоматизувати сезонні знижки та інтегрувати систему лояльності у ваш проект.

Деяким Laravel-застосункам недостатньо простої знижки у відсотках. Для сезонних розпродажів або запуску нових продуктів потрібні гнучкі інструменти: обмеження максимальної суми, мінімальний чек, ліміти використання та терміни дії. Пакет Laravel Discount від Milwad Khosravi зберігає ці параметри в Eloquent-моделях та дозволяє керувати ними через єдиний фасад.

Ось основні можливості пакета:

  • Два типи знижок: DiscountType::Percentage (відсоткова) та DiscountType::Fixed (фіксована) з можливістю встановити ліміт max_discount_amount.
  • Промокоди та автоматичні знижки: наявність code перетворює знижку на купон, інакше вона застосовується автоматично.
  • Часові межі: колонки starts_at та expires_at разом зі scope-запитом valid() для перевірки актуальності.
  • Ліміти використання: загальний usage_limit та персональний usage_limit_per_user для кожного клієнта.
  • Підтримка гостей: використання session ID для контролю лімітів неавторизованих користувачів.
  • Правила сумування: якщо знижка позначена як is_stackable, пакет сам обчислить найвигіднішу комбінацію для клієнта.
  • Знижки для моделей: трейт HasDiscounts дозволяє прив'язати знижки до будь-якої Eloquent-моделі.
  • Інтеграція з кошиком: сервіс CartDiscount для застосування кодів до всього кошика або окремих позицій.

# Відсоткові та фіксовані знижки

Знижка — це стандартна Eloquent-модель, тому створити її можна як звичайний запис:

use Binafy\LaravelDiscount\Enums\DiscountType;
use Binafy\LaravelDiscount\Models\Discount;
 
$discount = Discount::query()->create([
    'name' => 'Summer Sale',
    'type' => DiscountType::Percentage,
    'value' => 20,
]);

Застосування відбувається через фасад LaravelDiscount, який валідує знижку, проводить обчислення та повертає об'єкт DiscountResult:

use Binafy\LaravelDiscount\Facades\LaravelDiscount;
 
$result = LaravelDiscount::apply($discount, 200);
 
$result->originalAmount;   // 200.0
$result->discountAmount;   // 40.0
$result->payableAmount();  // 160.0

Об'єкт DiscountResult містить інформацію про застосовані знижки, початкову суму та розраховану вигоду. Метод payableAmount() віднімає знижку від загальної суми, гарантуючи, що результат не буде меншим за нуль.

Будь-який тип знижки може мати «стелю» завдяки max_discount_amount. Це ідеально підходить для акцій на кшталт «-20%, але не більше 100 грн», що позбавляє необхідності прописувати таку логіку в контролерах:

$discount = Discount::query()->create([
    'code' => 'SAVE20',
    'type' => DiscountType::Percentage,
    'value' => 20,
    'max_discount_amount' => 100,
]);
 
LaravelDiscount::apply($discount, 300)->discountAmount;  // 60.0
LaravelDiscount::apply($discount, 1000)->discountAmount; // 100.0

# Промокоди, терміни дії та ліміти

Додавання значення в колонку code робить знижку промокодом. Метод applyCode() перевіряє код і викидає DiscountNotFoundException, якщо його не знайдено:

$result = LaravelDiscount::applyCode('WELCOME10', 200, $user);

Якщо для кампанії потрібні унікальні коди для кожного клієнта, пакет генерує їх за допомогою random_int(), виключаючи символи, які легко переплутати (наприклад, 0/O або 1/I):

LaravelDiscount::generateCode();            // "8FJ2K9QW"
LaravelDiscount::generateCodes(100, 'VIP'); // Колекція зі 100 унікальних кодів

Довжину, алфавіт, префікс та роздільник можна налаштувати у файлі config/laravel-discount.php.

Для обмежених у часі пропозицій використовуються starts_at та expires_at. Якщо спробувати застосувати знижку завчасно або після дедлайну, пакет викине відповідний виняток (DiscountNotStartedException або DiscountExpiredException) та запустить подію DiscountExpired. Отримати список актуальних знижок можна через scope-запит:

Discount::query()->valid()->get();

Загальний ліміт використання (usage_limit) та ліміт на користувача (usage_limit_per_user) остаточно фіксуються методом redeem() під час завершення замовлення:

LaravelDiscount::redeem($discount, $user, $result->discountAmount);

Цей процес виконується в межах транзакції. База даних сама вирішує, чи можна інкрементувати used_count. Якщо ліміт вичерпано, виникне DiscountUsageLimitReachedException. Це гарантує, що два клієнти не зможуть одночасно використати останній доступний купон.

Для гостей замість моделі користувача використовується session ID, що відстежується в колонці session_id таблиці discount_usages:

$result = LaravelDiscount::applyCode('GUEST10', $total, sessionId: session()->getId());
 
LaravelDiscount::redeem($discount, amount: $result->discountAmount, sessionId: session()->getId());

# Умови та сумування знижок

Колонка min_order_value дозволяє встановити поріг суми замовлення, нижче якого знижка не діятиме. Також є JSON-колонка conditions для зберігання кастомних умов. Якщо ви віддаєте перевагу складним умовам у вигляді PHP-об'єктів, варто звернути увагу на пакет Discountify.

Знижки можна прив’язувати безпосередньо до моделей через трейт HasDiscounts та поліморфний зв'язок у таблиці discountables:

use Binafy\LaravelDiscount\Traits\HasDiscounts;
 
class Product extends Model
{
    use HasDiscounts;
}
$product->discounts()->attach($discount);
$product->validDiscounts();
$product->hasDiscount('TECH10');
 
$result = $product->applyDiscounts($product->price);

Метод applyDiscounts використовує логіку applyMany() для сумування. Пакет відсіює невалідні знижки, додає всі доступні до сумування (stackable), порівнює результат із найкращою некумулятивною знижкою та повертає найбільш вигідний для клієнта варіант.

# Інтеграція з Laravel Cart

Якщо ви використовуєте binafy/laravel-cart від того ж автора, вам стає доступним сервіс CartDiscount для роботи зі знижками на рівні кошика або окремих товарів:

use Binafy\LaravelDiscount\Integrations\LaravelCart\CartDiscount;
 
$cartDiscount = app(CartDiscount::class);
 
$result = $cartDiscount->applyToCart($cart, 'SUMMER-8FJ2K9QW');
 
$result = $cartDiscount->applyToItem($cartItem, $discount);
 
$result = $cartDiscount->applyItemDiscounts($cart);

Метод applyToCart() автоматично перевіряє мінімальну суму замовлення та ліміти користувача. applyToItem() розраховує знижку для конкретного рядка кошика, а applyItemDiscounts() проходить по всьому кошику та застосовує знижки, прив'язані до моделей товарів через трейт HasDiscounts.

# Валідація, винятки та події

Для перевірки промокодів у формах передбачено правило ValidDiscountCode. Воно повертає конкретну причину відмови, а не просто загальне повідомлення про помилку:

use Binafy\LaravelDiscount\Rules\ValidDiscountCode;
 
public function rules(): array
{
    return [
        'code' => ['required', new ValidDiscountCode(
            orderAmount: $this->cartTotal(),
            user: $this->user(),
        )],
    ];
}

Кожен випадок невдачі має свій виняток (Exception), що наслідується від DiscountException. Ви можете перехоплювати конкретні помилки (наприклад, DiscountExpiredException), щоб виводити кастомні повідомлення для користувачів.

Життєвий цикл знижки супроводжується трьома подіями: DiscountApplied (застосовано до суми), DiscountRedeemed (використано в замовленні) та DiscountExpired (спроба використати прострочену знижку). Подія DiscountRedeemed передає дані про знижку та запис про її використання, що зручно для аналітики або сповіщень.

# Встановлення

Laravel Discount потребує PHP 8.1+ та Laravel 9-13. Встановіть пакет через Composer та запустіть міграції:

composer require binafy/laravel-discount
php artisan migrate

Пакет також містить дві Artisan-команди: discount:generate для створення кодів через термінал та discount:prune для видалення застарілих знижок, яку варто додати до планувальника завдань:

Schedule::command('discount:prune --days=30')->daily();

Повну документацію та опис конфігурації можна знайти в репозиторії Laravel Discount на GitHub.

Популярні

Інше, що варто прочитати

14 Оновлено 26 червня, 2026

Локальні моделі та їх скоупи в Laravel за допомогою атрибута Scope

В Laravel 12 ми отримали можливість використовувати новий підхід для визначення локальних скоупів у моделях Eloquent. Дізнайтеся, як новий атрибут #[Scope] спрощує цей процес і зберігає ваші назви методів незмінними

105 Оновлено 26 червня, 2026

Все, що потрібно знати про Laravel 13

Laravel 13 вийде в березні 2026 року й вимагатиме мінімум PHP 8.3. Хочете дізнатися, як PHP‑атрибути для моделей, нові налаштування черг і метод Cache::touch() вплинуть на вашу розробку?

Використання повнотекстового пошуку в Laravel
180 Оновлено 26 червня, 2026

Використання повнотекстового пошуку в Laravel

Laravel пропонує потужні можливості повнотекстового пошуку за допомогою методів whereFullText та orWhereFullText, що дозволяють здійснювати складні запити до бази даних. Дізнайтеся, як реалізувати ефективний пошук для вашого блогу чи системи управління контентом