Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама
Новини 12 серпня 2026

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

E-commerce магазини часто потребують більше, ніж просту знижку у відсотках. Інтернет-магазин може запускати сезонний розпродаж, SaaS-продукт - роздавати промокоди на запуск, а знижка в 10% може мати максимальний ліміт, прив'язуватися до мінімальної суми замовлення, обмежуватися кількістю використань на клієнта та мати термін дії. Пакет Laravel Discount від Milwad Khosravi зберігає всю цю інформацію в Eloquent-моделях і опрацьовує через єдиний фасад.

Основні можливості

Пакет охоплює наступний функціонал:

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

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

Знижка - це стандартна 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 перетворює знижку на promotional купон. Метод applyCode() валідує код і викидає DiscountNotFoundException, якщо відповідності не знайдено:

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

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

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

Довжина, алфавіт, префікс та роздільник налаштовуються під ключем codes у 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 з перевіркою ліміту в where, тому база даних вирішує, чи відбудеться інкремент взагалі. Якщо оновлено нуль рядків - ліміт вже досягнуто, що проявляється як DiscountUsageLimitReachedException. Два клієнти, які претендують на сотий слот одночасно, не можуть обидва його отримати.

Гості отримують такий самий підхід через ID сесії замість моделі користувача, що відстежується в колонці session_id таблиці discount_usages поряд з nullable user_id:

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

Умовні та стекувальні знижки

Колонка min_order_value блокує знижку за порогом витрат і викидає MinimumOrderValueException, коли сума недостатня. Також є 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);

Останній виклик маршрутизується через applyMany() - це резолвер стекування. Він відкидає знижки, що не пройшли валідацію, ділить решту на стекувальні та не-стекувальні, сумує стекувальні (з обмеженням загальної суми замовлення), знаходить одну найкращу не-стекувальну і повертає ту сторону, яка економить більше:

$result = LaravelDiscount::applyMany([$tenPercent, $tenFixed, $bigSolo], 100);
$result->discounts;      // ті, що фактично застосувалися
$result->discountAmount; // переможна сума

Колекція discounts у результаті важлива саме тут. Після рішення про стекування часто потрібно показати клієнту, які коди пройшли, і ця колекція - відповідь.

Інтеграція з 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() перевіряє загальну суму кошика щодо min_order_value і витягує користувача кошика для ліміту на користувача, тому вам не потрібно передавати жодне з них. applyToItem() працює з ціною, помноженою на кількість для одного рядка. applyItemDiscounts() проходить кошик і застосовує все, що базова модель кожного товару має прикріпленим через HasDiscounts - так ви запускаєте розпродаж на рівні продукту по всьому кошику без торкання загальної суми.

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

Форми чекауту повинні відхиляти недійсний код до того, як щось інше станеться, і пакет включає правило ValidDiscountCode для цього. Його повідомлення називає фактичну причину замість загального провалу:

use Binafy\LaravelDiscount\Rules\ValidDiscountCode;

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

Це стандартний об'єкт правила, тому він компонується з рештою form request так само, як будь-яке custom validation rule.

Поза валідацією кожен випадок провалу має власне виключення, що розширює DiscountException: DiscountNotFoundException, DiscountNotActiveException, DiscountNotStartedException, DiscountExpiredException, DiscountUsageLimitReachedException та MinimumOrderValueException. Кожне відкриває знижку, що провалилася, через getDiscount(), тому ви можете спіймати конкретний випадок, який хочете повідомити інакше, і дозволити базовому класу обробити решту:

try {
    $result = LaravelDiscount::applyCode($code, $total, $user);
} catch (DiscountExpiredException $e) {
    return back()->withErrors("Code {$e->getDiscount()->code} has expired.");
} catch (DiscountException $e) {
    return back()->withErrors($e->getMessage());
}

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

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

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

composer require binafy/laravel-discount
php artisan migrate

Сервіс-провайдер реєструється автоматично, а міграції створюють таблиці discounts, discount_usages та discountables. Публікація конфігурації опціональна і має сенс лише якщо потрібно змінити назви таблиць, вказати на іншу модель користувача або налаштувати defaults генерації кодів:

php artisan vendor:publish --tag="laravel-discount-config"

Дві Artisan-команди йдуть разом з пакетом. discount:generate генерує коди з терміналу, а discount:prune видаляє прострочені знижки разом з їхніми записами використання, що розумно покласти в scheduler:

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

Повна документація, включаючи довідку конфігурації, знаходиться в репозиторії Laravel Discount на GitHub.

14

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

PayZephyr
Новини 12 вересня 2026

PayZephyr: Єдиний API для роботи з Stripe, Paystack та PayPal

PayZephyr - Laravel-пакет від Nwaneri Chukwunyere Kenneth, який об'єднує вісім платіжних провайдерів під одним зручним API. Підтримує автоматичне перемикання, захист від подвійної оплати, підписки та повернення коштів.

2
Laravel Telescope
Новини 11 вересня 2026

Artisan-команди для дебагу в Laravel Telescope 5.24.0

Laravel Telescope 5.24.0 додає дві нові Artisan-команди для роботи із записами із терміналу. Тепер можна переглядати запити, винятки, джоби та запити до бази даних без відкриття веб-інтерфейсу, а також отримувати дані у форматі JSON для скриптів та AI-агентів.

3

Вакансії за темою

CommerceCore Нова
3 дні тому

Senior PHP Developer

Senior PHP Developer для розробки масштабованих backend-систем у e-commerce. Основний стек: PHP (Laravel/Symfony), MySQL, API-дизайн, мікросервіси. Вимоги: 5+ років досвіду з high-load системами, глибокі знання PHP і БД, розуміння безпеки та архітектури. Менторство команди та вирішення складних технічних задач.

Гарант-Інфо
4 дні тому

Middle PHP Developer (Laravel)

Middle PHP Developer для міжнародного B2B-продукту у телекомунікаціях. Розробка нового функціоналу на PHP 8.x/Laravel, підтримка кодової бази, робота з REST API, платіжними інтеграціями та MySQL. Вимоги: 2+ років комерційного досвіду PHP, впевнене знання Laravel, ООП, SOLID, тестування (PHPUnit/Pest), Git, Docker.

Dinarys
4 дні тому

AI Technical Lead (PHP/Java)

Фракційний AI Technical Lead для проєктування enterprise-grade AI-рішень у e-commerce. Розроблятимете архітектури з LLMs, RAG, AI Agents для міжнародних клієнтів. Вимоги: 4+ років досвіду в архітектурі, production-grade системи на PHP (Laravel/Symfony) та Java (Spring), глибоке розуміння GenAI, OpenAI/Azure/AWS Bedrock, лідерський досвід. Upper-Intermediate англійська.

Пакети за темою

Bagisto

bagisto/bagisto

Bagisto — це платформа для електронної комерції, побудована на Laravel. Вона надає готове рішення для створення та управління інтернет-магазинами з підтримкою каталогу товарів, замовлень, платежів та клієнтів.

28,086 v2.5.0-beta1 13 25

Lang

laravel-lang/lang

Список 126 мов для Laravel Framework, Laravel Jetstream, Laravel Fortify, Laravel Breeze, Laravel Cashier, Laravel Nova, Laravel Spark та Laravel UI.

7,774 15.34.8 11