PHPackages                             mvonline/discount-laravel - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. mvonline/discount-laravel

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

mvonline/discount-laravel
=========================

A flexible Laravel package for managing discount codes and calculating discounts using a pipeline-based engine.

v0.0.2(3mo ago)00[1 issues](https://github.com/mvonline/discount-laravel/issues)MITPHPPHP ^8.2CI failing

Since Apr 4Pushed 3mo ago1 watchersCompare

[ Source](https://github.com/mvonline/discount-laravel)[ Packagist](https://packagist.org/packages/mvonline/discount-laravel)[ RSS](/packages/mvonline-discount-laravel/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (4)Versions (2)Used By (0)

discount-laravel
================

[](#discount-laravel)

A Laravel package for managing discount codes and calculating cart discounts using a **pipeline** of validation and application steps. Built for **Laravel 12** and **PHP 8.2+**.

**Repository:** [github.com/mvonline/discount-laravel](https://github.com/mvonline/discount-laravel)
**Package name:** `mvonline/discount-laravel`
**Root namespace:** `Mvonline\DiscountLaravel`

Features
--------

[](#features)

- CRUD-style HTTP API under `/api/discount-codes` (enable/disable via `discount-manager.routes.enabled`)
- Discount calculation via Laravel’s `Pipeline`, with ordered pipes (validate code, expiry, usage limits, basket minimums, user/group/first-time rules, then apply amount)
- DTOs for cart, customer, and calculation requests/results
- `DiscountType` enum covering many strategies (full reference table below); `DiscountType::...->isCalculationImplemented()` marks types with built-in amount math in `ApplyDiscountCalculation`
- Migrations for `discount_codes` and related tables (usage / conditions) for future use
- Optional OpenAPI annotations and a `discount-manager:generate-docs` command (requires `zircote/swagger-php` in dev)

Installation
------------

[](#installation)

### From GitHub (before Packagist)

[](#from-github-before-packagist)

```
{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/mvonline/discount-laravel.git"
        }
    ],
    "require": {
        "mvonline/discount-laravel": "dev-main"
    }
}
```

Then:

```
composer update mvonline/discount-laravel
```

### After Packagist publication

[](#after-packagist-publication)

```
composer require mvonline/discount-laravel
```

The service provider is auto-discovered. Publish config and migrations:

```
php artisan vendor:publish --tag="discount-manager-config"
php artisan vendor:publish --tag="discount-manager-migrations"
php artisan migrate
```

Configuration file: `config/discount-manager.php`. Important options:

KeyPurpose`models.discount_code`Eloquent model class (extend `DiscountCode` in your app if needed).`calculation.apply_mode``stack` (default): apply all requested codes in order. `best_single`: evaluate each requested code alone and return the best discount.`routes.enabled``true` (default): register package routes. Set `false` to ship routes yourself.`routes.middleware`Default `['api']`. Add e.g. `auth:sanctum` for protected admin APIs.`pipeline.pipes`Ordered list of pipeline classes.`getMaximumDiscount()` always evaluates **each eligible code alone** and returns the single best valid result (highest `total_discount`).

Supported discount types
------------------------

[](#supported-discount-types)

Values are stored on `discount_codes.type` and match `Mvonline\DiscountLaravel\Enums\DiscountType`.

**Built-in discount math** in `ApplyDiscountCalculation`: `percentage`, `fixed_amount`, `percentage_with_cap` (`DiscountType::...->isCalculationImplemented()`). Other types return `0` from the default matcher until you extend pipes—many still use **validation pipes** and `conditions` / `metadata`.

All examples assume:

```
use Mvonline\DiscountLaravel\Enums\DiscountType;
use Mvonline\DiscountLaravel\Models\DiscountCode;
```

### `percentage`

[](#percentage)

Percent off the applicable total (`value` = percent). Applied to the running remainder when multiple codes stack.

```
DiscountCode::create([
    'code' => 'SAVE20',
    'name' => '20% off',
    'type' => DiscountType::PERCENTAGE,
    'value' => 20,
    'is_active' => true,
    'can_be_combined' => true,
]);
```

### `fixed_amount`

[](#fixed_amount)

Fixed currency amount off the total (cannot exceed what remains on the cart).

```
DiscountCode::create([
    'code' => 'FLAT15',
    'name' => '$15 off',
    'type' => DiscountType::FIXED_AMOUNT,
    'value' => 15,
    'is_active' => true,
]);
```

### `percentage_with_cap`

[](#percentage_with_cap)

Percent in `value`; max discount in `metadata` (see also [Percentage with cap](#percentage-with-cap) under Usage).

```
DiscountCode::create([
    'code' => 'PCT25CAP',
    'name' => '25% off, max $30',
    'type' => DiscountType::PERCENTAGE_WITH_CAP,
    'value' => 25,
    'is_active' => true,
    'metadata' => ['max_discount_amount' => 30.00],
]);
```

### `specific_user`

[](#specific_user)

Restricts redemption to listed user IDs (`CheckSpecificUserDiscount`). Pair with custom amount logic or extend `ApplyDiscountCalculation`—default amount for this type is `0`.

```
DiscountCode::create([
    'code' => 'USER42ONLY',
    'name' => 'User 42 perk',
    'type' => DiscountType::SPECIFIC_USER,
    'value' => 10,
    'is_active' => true,
    'conditions' => ['allowed_users' => [42]],
]);
```

### `specific_group`

[](#specific_group)

Requires the customer to be in one of the allowed groups (`CheckSpecificGroupDiscount`).

```
DiscountCode::create([
    'code' => 'VIPONLY',
    'name' => 'VIP discount',
    'type' => DiscountType::SPECIFIC_GROUP,
    'value' => 15,
    'is_active' => true,
    'conditions' => ['allowed_groups' => ['vip', 'gold']],
]);
```

### `specific_product`

[](#specific_product)

Line-item / SKU rules—implement in your app or custom pipes using `cart.items` and `conditions`.

```
DiscountCode::create([
    'code' => 'SKU123',
    'name' => 'SKU 123 promo',
    'type' => DiscountType::SPECIFIC_PRODUCT,
    'value' => 5,
    'is_active' => true,
    'conditions' => ['product_ids' => [123, 456]],
]);
```

### `bogo`

[](#bogo)

BOGO rules in `metadata`; fulfillment typically needs custom pipes or checkout logic.

```
DiscountCode::create([
    'code' => 'BOGOHAT',
    'name' => 'BOGO hats',
    'type' => DiscountType::BOGO,
    'value' => 0,
    'is_active' => true,
    'metadata' => ['buy_qty' => 1, 'get_qty' => 1, 'scope' => 'sku:HAT-01'],
]);
```

### `buy_x_get_y`

[](#buy_x_get_y)

Threshold promotion; encode X/Y in `metadata` / `conditions`.

```
DiscountCode::create([
    'code' => 'BUY3GET1',
    'name' => 'Buy 3 get 1',
    'type' => DiscountType::BUY_X_GET_Y,
    'value' => 0,
    'is_active' => true,
    'metadata' => ['buy' => 3, 'get' => 1, 'discount_on_get' => 100],
]);
```

### `with_expiry`

[](#with_expiry)

Campaign label; real validity is `starts_at` / `expires_at` (see **CheckExpiryDate**).

```
DiscountCode::create([
    'code' => 'SUMMER24',
    'name' => 'Summer sale',
    'type' => DiscountType::WITH_EXPIRY,
    'value' => 10,
    'is_active' => true,
    'starts_at' => now()->startOfMonth(),
    'expires_at' => now()->addMonths(3),
]);
```

### `first_n_users`

[](#first_n_users)

Cap redemptions with `usage_limit` (first N successful uses).

```
DiscountCode::create([
    'code' => 'FIRST500',
    'name' => 'First 500 customers',
    'type' => DiscountType::FIRST_N_USERS,
    'value' => 50,
    'is_active' => true,
    'usage_limit' => 500,
    'usage_count' => 0,
]);
```

### `minimum_basket`

[](#minimum_basket)

**CheckMinimumBasketValue** enforces `minimum_basket_value` for any type. For built-in percent/fixed math, use `PERCENTAGE` or `FIXED_AMOUNT` with `minimum_basket_value` set. The `MINIMUM_BASKET` enum value is a semantic label if you extend amount logic yourself.

```
DiscountCode::create([
    'code' => 'MIN100P10',
    'name' => '10% on $100+',
    'type' => DiscountType::PERCENTAGE,
    'value' => 10,
    'minimum_basket_value' => 100,
    'is_active' => true,
]);
```

### `bundle`

[](#bundle)

Define which SKUs must appear together; allocation in custom pipes.

```
DiscountCode::create([
    'code' => 'BUNDLEABC',
    'name' => 'A+B+C bundle',
    'type' => DiscountType::BUNDLE,
    'value' => 20,
    'is_active' => true,
    'conditions' => ['required_skus' => ['A-1', 'B-2', 'C-3']],
]);
```

### `category_based`

[](#category_based)

Limit to categories; cart lines should carry category ids your pipes can read.

```
DiscountCode::create([
    'code' => 'CATSALE',
    'name' => 'Electronics sale',
    'type' => DiscountType::CATEGORY_BASED,
    'value' => 12,
    'is_active' => true,
    'conditions' => ['category_ids' => [10, 11]],
]);
```

### `shipping`

[](#shipping)

Flag shipping-focused promos; discount shipping via `CartDTO` + custom logic or fixed amount on shipping component.

```
DiscountCode::create([
    'code' => 'FREESHIP',
    'name' => 'Free shipping',
    'type' => DiscountType::SHIPPING,
    'value' => 0,
    'is_active' => true,
    'metadata' => ['shipping_discount_type' => 'free', 'max_shipping_value' => 9.99],
]);
```

### `loyalty_points`

[](#loyalty_points)

Map points to money in `metadata`; validate balance outside the package.

```
DiscountCode::create([
    'code' => 'POINTS500',
    'name' => '500 points',
    'type' => DiscountType::LOYALTY_POINTS,
    'value' => 5.00,
    'is_active' => true,
    'metadata' => ['points_cost' => 500, 'points_per_currency' => 100],
]);
```

### `referral`

[](#referral)

Referral campaign id / terms in `metadata`.

```
DiscountCode::create([
    'code' => 'REF-ALICE',
    'name' => 'Alice refers you',
    'type' => DiscountType::REFERRAL,
    'value' => 10,
    'is_active' => true,
    'metadata' => ['referrer_id' => 101, 'campaign' => 'spring'],
]);
```

### `bulk_purchase`

[](#bulk_purchase)

Volume tiers in `conditions` / `metadata`.

```
DiscountCode::create([
    'code' => 'BULK',
    'name' => 'Bulk tiers',
    'type' => DiscountType::BULK_PURCHASE,
    'value' => 0,
    'is_active' => true,
    'metadata' => ['tiers' => [['min_qty' => 10, 'percent' => 5], ['min_qty' => 50, 'percent' => 12]]],
]);
```

### `payment_method`

[](#payment_method)

Allowed methods in `conditions`; validate in a custom pipe (e.g. read from request).

```
DiscountCode::create([
    'code' => 'CARDONLY',
    'name' => 'Card discount',
    'type' => DiscountType::PAYMENT_METHOD,
    'value' => 3,
    'is_active' => true,
    'conditions' => ['allowed_payment_methods' => ['card', 'apple_pay']],
]);
```

### `first_time_buyer`

[](#first_time_buyer)

**CheckFirstTimeBuyerDiscount** requires `CustomerDTO::isFirstTimeBuyer === true`. Amount still needs `PERCENTAGE`/`FIXED` or custom pipe unless you extend the matcher.

```
DiscountCode::create([
    'code' => 'WELCOME',
    'name' => 'First order',
    'type' => DiscountType::FIRST_TIME_BUYER,
    'value' => 15,
    'is_active' => true,
]);
```

### `seasonal`

[](#seasonal)

Label + date window for holiday/season campaigns.

```
DiscountCode::create([
    'code' => 'BLACKFRIDAY',
    'name' => 'Black Friday',
    'type' => DiscountType::SEASONAL,
    'value' => 30,
    'is_active' => true,
    'starts_at' => '2026-11-24 00:00:00',
    'expires_at' => '2026-11-30 23:59:59',
]);
```

### `limited_quantity`

[](#limited_quantity)

Global or per-code stock using `usage_limit` and your inventory rules.

```
DiscountCode::create([
    'code' => 'LIMIT50',
    'name' => 'Limited stock',
    'type' => DiscountType::LIMITED_QUANTITY,
    'value' => 20,
    'is_active' => true,
    'usage_limit' => 50,
    'metadata' => ['sku_stock' => ['SKU-1' => 20]],
]);
```

### `location_based`

[](#location_based)

Regions / stores in `conditions`.

```
DiscountCode::create([
    'code' => 'NYCONLY',
    'name' => 'NYC stores',
    'type' => DiscountType::LOCATION_BASED,
    'value' => 8,
    'is_active' => true,
    'conditions' => ['regions' => ['US-NY'], 'store_ids' => [7, 8]],
]);
```

### `customer_segment`

[](#customer_segment)

Match CRM segments from `CustomerDTO` / `conditions`.

```
DiscountCode::create([
    'code' => 'HIGHVALUE',
    'name' => 'High-value segment',
    'type' => DiscountType::CUSTOMER_SEGMENT,
    'value' => 12,
    'is_active' => true,
    'conditions' => ['segments' => ['high_value', 'repeat_buyer']],
]);
```

### `membership`

[](#membership)

Club / tier offers—often aligned with `customer.groups`.

```
DiscountCode::create([
    'code' => 'CLUB',
    'name' => 'Members club',
    'type' => DiscountType::MEMBERSHIP,
    'value' => 10,
    'is_active' => true,
    'conditions' => ['membership_tiers' => ['gold', 'platinum']],
]);
```

### `gift_card`

[](#gift_card)

Treat as stored value; integrate ledger in your app.

```
DiscountCode::create([
    'code' => 'GC-9F3A',
    'name' => 'Gift card',
    'type' => DiscountType::GIFT_CARD,
    'value' => 50.00,
    'is_active' => true,
    'metadata' => ['balance' => 50.00, 'currency' => 'USD'],
]);
```

### `tiered`

[](#tiered)

Spend- or quantity-based tiers in `metadata`; implement tier resolution in a custom pipe.

```
DiscountCode::create([
    'code' => 'TIERED',
    'name' => 'Spend tiers',
    'type' => DiscountType::TIERED,
    'value' => 0,
    'is_active' => true,
    'metadata' => ['tiers' => [['min' => 0, 'pct' => 5], ['min' => 200, 'pct' => 10]]],
]);
```

### `flash`

[](#flash)

Short window flash sale.

```
DiscountCode::create([
    'code' => 'FLASH2H',
    'name' => '2h flash',
    'type' => DiscountType::FLASH,
    'value' => 40,
    'is_active' => true,
    'starts_at' => now(),
    'expires_at' => now()->addHours(2),
]);
```

### `user_anniversary`

[](#user_anniversary)

Eligibility from account/signup dates—validate in a custom pipe.

```
DiscountCode::create([
    'code' => 'ANNIV',
    'name' => 'Anniversary',
    'type' => DiscountType::USER_ANNIVERSARY,
    'value' => 15,
    'is_active' => true,
    'metadata' => ['match' => 'signup_anniversary_month'],
]);
```

### `app_exclusive`

[](#app_exclusive)

Channel gate—pass e.g. `channel` in `CustomerDTO::metadata`.

```
DiscountCode::create([
    'code' => 'APPONLY',
    'name' => 'App exclusive',
    'type' => DiscountType::APP_EXCLUSIVE,
    'value' => 7,
    'is_active' => true,
    'conditions' => ['channels' => ['ios', 'android']],
]);
```

### `free_gift`

[](#free_gift)

Adds a gift line in order management; amount here is often `0` until you model gift SKU in metadata.

```
DiscountCode::create([
    'code' => 'GIFTMUG',
    'name' => 'Free mug',
    'type' => DiscountType::FREE_GIFT,
    'value' => 0,
    'is_active' => true,
    'metadata' => ['gift_sku' => 'MUG-01', 'max_gifts' => 1],
]);
```

### `upgrade`

[](#upgrade)

Upgrade path between products/plans.

```
DiscountCode::create([
    'code' => 'UPGRADE',
    'name' => 'Pro upgrade',
    'type' => DiscountType::UPGRADE,
    'value' => 99.00,
    'is_active' => true,
    'conditions' => ['from_plan' => 'basic', 'to_plan' => 'pro'],
]);
```

### `subscription`

[](#subscription)

Subscription billing hooks—eligibility flags for your biller.

```
DiscountCode::create([
    'code' => 'SUB3MO',
    'name' => '3 months off',
    'type' => DiscountType::SUBSCRIPTION,
    'value' => 10,
    'is_active' => true,
    'metadata' => ['interval' => 'month', 'duration_cycles' => 3],
]);
```

### `milestone`

[](#milestone)

Lifetime spend / order count thresholds.

```
DiscountCode::create([
    'code' => 'MILE10K',
    'name' => '$10k lifetime spend',
    'type' => DiscountType::MILESTONE,
    'value' => 25,
    'is_active' => true,
    'metadata' => ['min_lifetime_spend' => 10000],
]);
```

### `refer_a_friend`

[](#refer_a_friend)

Double-sided referral metadata for your referral service.

```
DiscountCode::create([
    'code' => 'RAF2026',
    'name' => 'Refer a friend',
    'type' => DiscountType::REFER_A_FRIEND,
    'value' => 20,
    'is_active' => true,
    'metadata' => ['referee_reward' => 20, 'referrer_reward' => 20],
]);
```

### `product_launch`

[](#product_launch)

Launch window + catalog flags.

```
DiscountCode::create([
    'code' => 'NEWPHONE',
    'name' => 'Launch week',
    'type' => DiscountType::PRODUCT_LAUNCH,
    'value' => 50,
    'is_active' => true,
    'conditions' => ['launch_product_ids' => [9001]],
    'expires_at' => now()->addWeek(),
]);
```

### `donation_based`

[](#donation_based)

Round-up / charity—custom amount rules.

```
DiscountCode::create([
    'code' => 'ROUNDUP',
    'name' => 'Round up for charity',
    'type' => DiscountType::DONATION_BASED,
    'value' => 0,
    'is_active' => true,
    'metadata' => ['charity_id' => 'red-cross', 'round_up_to' => 1.00],
]);
```

### `buy_more_save_more`

[](#buy_more_save_more)

Progressive table in `metadata`.

```
DiscountCode::create([
    'code' => 'MORESAVE',
    'name' => 'Buy more save more',
    'type' => DiscountType::BUY_MORE_SAVE_MORE,
    'value' => 0,
    'is_active' => true,
    'metadata' => ['steps' => [['min' => 100, 'pct' => 5], ['min' => 250, 'pct' => 10]]],
]);
```

### `combo`

[](#combo)

Fixed combo price / discount for a preset basket mix.

```
DiscountCode::create([
    'code' => 'COMBOPIZZA',
    'name' => 'Pizza combo',
    'type' => DiscountType::COMBO,
    'value' => 19.99,
    'is_active' => true,
    'conditions' => ['combo_line_items' => [['sku' => 'PZ-1'], ['sku' => 'DR-2']]]],
]);
```

### `exchange`

[](#exchange)

Trade-in value and partner SKUs—integrate with ERP/POS.

```
DiscountCode::create([
    'code' => 'TRADEOLD',
    'name' => 'Trade-in',
    'type' => DiscountType::EXCHANGE,
    'value' => 200,
    'is_active' => true,
    'metadata' => ['trade_in_sku' => 'OLD-PHONE', 'valuation_source' => 'pos'],
]);
```

Usage
-----

[](#usage)

### Conditions &amp; metadata: who passes what

[](#conditions--metadata-who-passes-what)

There are **two different places** “conditions” and “metadata” show up:

WhereWhen it is setPurpose**`discount_codes.conditions` and `discount_codes.metadata`**When you **create or update** the code (admin, seeder, or `POST /api/discount-codes`)Rules and extra data **for that code** (allowed users, groups, caps, tiers, etc.). Shoppers do **not** send these at checkout—they are loaded from the database by code string.**`CartDTO::metadata`, `CustomerDTO::metadata`, `DiscountCalculationRequestDTO::metadata`**On each **calculation request** (checkout / quote)**Runtime context** for your app or custom pipes (channel, device, payment method already chosen, A/B flags, etc.). Built-in pipes mostly use typed fields (`customer.groups`, `customer.id`, …), not these bags.**At checkout the buyer only passes the code(s)**—for example `['VIP2024']`. The engine runs `DiscountCode::whereIn('code', …)`, so stored `conditions` / `metadata` on each row are always applied automatically.

**Built-in pipes and stored `conditions` keys (on the `DiscountCode` model):**

PipeReads from the loaded codeReads from DTOsCheckSpecificUserDiscount`conditions['allowed_users']``customer.id`CheckSpecificGroupDiscount`conditions['allowed_groups']``customer.groups`CheckMinimumBasketValue`minimum_basket_value``cart.total`ApplyDiscountCalculation (percentage with cap)`metadata['max_discount_amount']` or `metadata['cap']``cart` totals via context#### 1) Define rules on the code (admin / API)

[](#1-define-rules-on-the-code-admin--api)

```
use Mvonline\DiscountLaravel\Enums\DiscountType;
use Mvonline\DiscountLaravel\Models\DiscountCode;

DiscountCode::create([
    'code' => 'VIP15',
    'name' => 'VIP 15%',
    'type' => DiscountType::SPECIFIC_GROUP,
    'value' => 15,
    'is_active' => true,
    'conditions' => [
        'allowed_groups' => ['vip', 'gold'],
    ],
    'metadata' => [
        'marketing_campaign' => 'spring-2026',
    ],
]);
```

#### 2) Apply codes at checkout (PHP)—only codes + cart + customer context

[](#2-apply-codes-at-checkout-phponly-codes--cart--customer-context)

```
use Mvonline\DiscountLaravel\DTOs\CartDTO;
use Mvonline\DiscountLaravel\DTOs\CustomerDTO;
use Mvonline\DiscountLaravel\DTOs\DiscountCalculationRequestDTO;
use Mvonline\DiscountLaravel\Services\DiscountCalculator;

$cartDTO = new CartDTO(
    items: collect([['id' => 1, 'name' => 'Item', 'price' => 50, 'quantity' => 2]]),
    subtotal: 100.00,
    tax: 10.00,
    shipping: 5.00,
    total: 115.00,
    currency: 'USD',
    metadata: ['store_id' => 'NYC-07'],
);

$customerDTO = new CustomerDTO(
    id: 42,
    type: 'user',
    groups: ['vip'],
    segments: ['high_value'],
    location: 'US-NY',
    paymentMethod: 'card',
    isFirstTimeBuyer: false,
    metadata: ['channel' => 'ios'],
);

$request = new DiscountCalculationRequestDTO(
    cart: $cartDTO,
    customer: $customerDTO,
    discountCodes: ['VIP15'],
    metadata: ['request_id' => 'uuid-for-logging'],
);

$result = app(DiscountCalculator::class)->calculate($request);
```

`conditions` / `metadata` on the **code** are **not** repeated in `$request`—they are read from the `DiscountCode` rows loaded for `VIP15`.

#### 3) Same request via HTTP (`POST /api/discount-codes/validate`)

[](#3-same-request-via-http-post-apidiscount-codesvalidate)

Optional fields match `CartDTO::fromArray` / `CustomerDTO::fromArray`: `cart.metadata`, `cart.currency`, `customer.segments`, `customer.location`, `customer.payment_method`, `customer.metadata`, and top-level `metadata`.

```
{
  "cart": {
    "items": [
      { "id": 1, "name": "Item", "price": 50, "quantity": 2 }
    ],
    "subtotal": 100,
    "tax": 10,
    "shipping": 5,
    "total": 115,
    "currency": "USD",
    "metadata": { "store_id": "NYC-07" }
  },
  "customer": {
    "id": 42,
    "type": "user",
    "groups": ["vip"],
    "segments": ["high_value"],
    "location": "US-NY",
    "payment_method": "card",
    "is_first_time_buyer": false,
    "metadata": { "channel": "ios" }
  },
  "discount_codes": ["VIP15"],
  "metadata": { "request_id": "optional-for-your-app" }
}
```

To set **`conditions` / `metadata` on the code itself**, use **`POST /api/discount-codes`** (create) or **`PUT /api/discount-codes/{id}`** with a JSON body that includes `conditions` and `metadata`:

```
{
  "code": "VIP15",
  "name": "VIP 15%",
  "type": "specific_group",
  "value": 15,
  "is_active": true,
  "can_be_combined": true,
  "conditions": {
    "allowed_groups": ["vip", "gold"]
  },
  "metadata": {
    "marketing_campaign": "spring-2026"
  }
}
```

### Calculate a discount in PHP

[](#calculate-a-discount-in-php)

```
use Mvonline\DiscountLaravel\DTOs\CartDTO;
use Mvonline\DiscountLaravel\DTOs\CustomerDTO;
use Mvonline\DiscountLaravel\DTOs\DiscountCalculationRequestDTO;
use Mvonline\DiscountLaravel\Services\DiscountCalculator;

$cartDTO = new CartDTO(
    items: collect([/* cart line items */]),
    subtotal: 100.00,
    tax: 10.00,
    shipping: 5.00,
    total: 115.00
);

$customerDTO = new CustomerDTO(
    id: 1,
    type: 'user',
    groups: ['vip'],
    isFirstTimeBuyer: false
);

$request = new DiscountCalculationRequestDTO(
    cart: $cartDTO,
    customer: $customerDTO,
    discountCodes: ['SUMMER2024']
);

$result = app(DiscountCalculator::class)->calculate($request);

if ($result->isValid) {
    // $result->totalDiscount, $result->finalTotal, $result->toArray() for JSON
} else {
    // $result->errorMessage
}
```

### HTTP API (package routes)

[](#http-api-package-routes)

Routes are registered with the `api` middleware group and prefix `api`:

MethodPathDescriptionGET`/api/discount-codes`Paginated listPOST`/api/discount-codes`CreateGET`/api/discount-codes/{discountCode}`ShowPUT`/api/discount-codes/{discountCode}`UpdateDELETE`/api/discount-codes/{discountCode}`Soft deletePOST`/api/discount-codes/validate`Validate cart + codesPOST`/api/discount-codes/maximum-discount`Best **single** eligible code for the cart (highest discount)POST`/api/discount-codes/{discountCode}/track-usage`Increment `usage_count`Validation endpoints return the same shape as `DiscountCalculationResultDTO::toArray()` (snake\_case keys).

### Custom pipeline pipes

[](#custom-pipeline-pipes)

Override `discount-manager.pipeline.pipes` in config with your own classes extending `Mvonline\DiscountLaravel\Pipeline\Pipe` and implementing `handle(DiscountCalculationContext $context, Closure $next)`.

### Percentage with cap

[](#percentage-with-cap)

Store the percentage in `value` and set a monetary cap in JSON metadata, for example:

```
{
  "max_discount_amount": 25.00
}
```

Alternatively use the key `cap` for the same purpose.

Project layout
--------------

[](#project-layout)

- `src/Services/DiscountCalculator.php` — orchestrates the pipeline
- `src/Pipeline/Pipes/*` — individual steps
- `src/Models/DiscountCode.php` — Eloquent model
- `routes/api.php` — package routes

Testing
-------

[](#testing)

```
composer test
```

Uses [Orchestra Testbench](https://github.com/orchestra/testbench) and PHPUnit 11.

Contributing
------------

[](#contributing)

Issues and pull requests are welcome on [GitHub](https://github.com/mvonline/discount-laravel).

License
-------

[](#license)

See [LICENSE](LICENSE) (MIT).

###  Health Score

32

—

LowBetter than 69% of packages

Maintenance82

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity36

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

92d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/1967927?v=4)[Masoud Vafaei](/maintainers/mvonline)[@mvonline](https://github.com/mvonline)

---

Top Contributors

[![masoudimmensegroup](https://avatars.githubusercontent.com/u/212504447?v=4)](https://github.com/masoudimmensegroup "masoudimmensegroup (5 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/mvonline-discount-laravel/health.svg)

```
[![Health](https://phpackages.com/badges/mvonline-discount-laravel/health.svg)](https://phpackages.com/packages/mvonline-discount-laravel)
```

###  Alternatives

[markwalet/nova-modal-response

A Laravel Nova asset for Modal responses on an action.

17878.9k](/packages/markwalet-nova-modal-response)[crumbls/layup

A visual page builder plugin for Filament 5 — Divi-style grid layouts with extensible widgets.

592.7k2](/packages/crumbls-layup)[team-nifty-gmbh/tall-datatables

Server-side rendered datatables for Laravel and Livewire

1320.9k4](/packages/team-nifty-gmbh-tall-datatables)[tomshaw/electricgrid

A feature-rich Livewire package designed for projects that require dynamic, interactive data tables.

119.4k](/packages/tomshaw-electricgrid)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
