Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама
Новини 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.

15

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

Коментарі

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

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

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

Новини 27 жовтня 2026

Filament v5.11.0: поля й віджети на React, Vue і Svelte та жорсткіший захист Livewire

Filament v5.11.0 дозволяє писати власні поля форм, компоненти схем і віджети на React, Vue, Svelte або чистому JavaScript. Паралельно v5.11.0 і v4.16.0 закривають для клієнта внутрішні Livewire-методи та стан і додають опційні ліміти для репітерів і пагінації таблиць.

2
Новини 27 жовтня 2026

Livewire 4.4.8 закриває обхід #[Authorize], а NativePHP Mobile - сім вразливостей

Livewire 4.4.8 виправляє GHSA-mhf7-xp6r-6qrc (CVSS 6.5): атрибут #[Authorize] можна було обійти й виконати дію над чужим записом. Того ж тижня NativePHP опублікувала сім попереджень для NativePHP for Mobile - виправлення в nativephp/mobile 4.6.1 і 3.3.11, але до користувачів вони дійдуть лише з новою збіркою застосунку.

1

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

Intetics
7 днів тому

Senior Full-Stack PHP Developer (Laravel + React/Vue)

Senior Full-Stack PHP розробник створює й підтримує масштабовані веб-застосунки: бекенд на Laravel та фронтенд на React або Vue. Відповідає за архітектурні рішення (дизайн сервісів, моделювання даних, кешування, черги), проєктує REST API та інтеграції, рефакторить legacy-код і оптимізує запити. Вимоги: 5+ років досвіду з сучасним PHP 8.x, 3+ роки з Laravel, 3+ роки з React або Vue, сильний TypeScript, досвід з Redis і чергами, автотести та CI/CD.

Logiflow
8 днів тому

Backend developer

Розробка та підтримка REST API на Laravel для публічного API, адмін-панелі та мобільних клієнтів, проєктування БД, робота з чергами, подіями та real-time повідомленнями, інтеграція зовнішніх сервісів. Основний стек: PHP 8.1+, Laravel 10+, Redis, Docker, REST API з OpenAPI. Вимоги: глибоке знання PHP, досвід комерційної розробки на Laravel, дотримання код-стайлу.

Dinarys
9 днів тому

AI Technical Lead (PHP)

Спеціаліст проєктуватиме наскрізні AI-архітектури для e-commerce продуктів, включно з LLM, AI-агентами, RAG та векторними базами даних, і керуватиме технічною командою як Tech Lead. Основний стек - PHP з Laravel або Symfony, REST API, мікросервіси та хмарні архітектури. Потрібен досвід продакшн-систем на PHP, інтеграції AI-сервісів провайдерів на кшталт OpenAI чи Anthropic та впровадження AI-практик у команді розробки. Англійська не нижче Upper-Intermediate, досвід роботи з міжнародними клієнтам

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

Bagisto

bagisto/bagisto

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

28,282 v2.5.0 13 26

Lang

laravel-lang/lang

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

7,780 15.37.4 12