PHPackages                             lalalili/discount - 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. lalalili/discount

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

lalalili/discount
=================

Config-driven discount and coupon kernel for Laravel projects.

v2.1.1(2mo ago)10proprietaryPHPPHP ^8.4

Since Feb 15Pushed 2mo agoCompare

[ Source](https://github.com/lalalili/discount)[ Packagist](https://packagist.org/packages/lalalili/discount)[ RSS](/packages/lalalili-discount/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (3)Versions (4)Used By (0)

lalalili/discount
=================

[](#lalalilidiscount)

Config-driven discount and coupon kernel for Laravel projects.

Scope
-----

[](#scope)

This package provides reusable promotion engines, coupon application orchestration, and DTO/context objects:

- Product price calculation (`DiscountEngineInterface`)
- Cart promotion condition generation (`CartPromotionEngineInterface`)
- Coupon eligibility validation (`CouponEligibilityInterface`)
- Coupon code generation (`CouponCodeGeneratorInterface`)
- Coupon discount calculation (`CouponDiscountEngineInterface`)
- Coupon validation orchestration (`CouponApplicationServiceInterface`)

Out of scope (kept in application adapter layer):

- Eloquent queries and persistence implementation details
- Session/Cookie/Auth orchestration
- Admin UI
- Domain-specific flow control (events, jobs, notification orchestration)

`CouponApplicationServiceInterface` is provided by this package, but coupon data lookup / usage checks / inventory updates must be implemented by your app via `CouponRepositoryInterface`.

Requirements
------------

[](#requirements)

- PHP `^8.4`
- Laravel `^12.0`

Public Interfaces
-----------------

[](#public-interfaces)

The package API is stable at interface level:

- `DiscountEngineInterface::price(ProductContext $product, PromotionSet $promotions): PriceResult`
- `CartPromotionEngineInterface::apply(CartContext $cart, PromotionSet $promotions): CartAdjustmentResult`
- `CouponEligibilityInterface::validate(CouponContext $coupon, CartContext $cart, UserContext $user): EligibilityResult`
- `CouponCodeGeneratorInterface::generate(CodeContext $context): string`
- `CouponDiscountEngineInterface::discount(float $orderTotal, CouponContext $coupon): CouponDiscountResult`
- `CouponApplicationServiceInterface::validate(CouponKind $kind, string $code, CartContext $cart, UserContext $user): CouponValidationResult`

Core Types
----------

[](#core-types)

### Enums

[](#enums)

- `CouponAmountMode`: `auto | fixed | rate`
- `CouponKind`: `member | promotion`

### DTOs

[](#dtos)

- `CouponData`
    - `code`, `kind`, `scope`, `triggerAmount`, `amount`, `amountMode`, `status`, `limitQty`, `leftQty`, `userId`, `attributes`
- `CouponDiscountResult`
    - `valid`, `discount`, `finalTotal`, `reason`, `reasonCode`
- `CouponValidationResult`
    - `eligible`, `coupon`, `discount`, `finalTotal`, `reason`, `reasonCode`

### Context Changes

[](#context-changes)

`CouponContext` now supports:

- `scope`
- `triggerAmount`
- `amount`
- `amountMode` (`CouponAmountMode|string|null`, optional, default `auto`)

Install
-------

[](#install)

### Option A: Local path repository

[](#option-a-local-path-repository)

In application `composer.json`:

```
{
  "repositories": [
    {
      "type": "path",
      "url": "packages/discount",
      "options": {
        "symlink": true
      }
    }
  ],
  "require": {
    "lalalili/discount": "^2.1"
  }
}
```

Then run:

```
composer update lalalili/discount
```

### Option B: Private VCS repository (recommended for other projects)

[](#option-b-private-vcs-repository-recommended-for-other-projects)

In application `composer.json`:

```
{
  "repositories": [
    {
      "type": "vcs",
      "url": "git@github.com:lalalili/discount.git"
    }
  ],
  "require": {
    "lalalili/discount": "^2.1"
  }
}
```

Then run:

```
composer update lalalili/discount
```

Laravel Setup
-------------

[](#laravel-setup)

Publish default config (optional):

```
php artisan vendor:publish --tag=discount-config
```

The package reads mappings from `config/discount.php`.

### Required Binding for Coupon Application Service

[](#required-binding-for-coupon-application-service)

To use `CouponApplicationServiceInterface`, your app must bind `CouponRepositoryInterface`:

```
use Discount\Kernel\Contracts\CouponRepositoryInterface;
use App\Services\Events\Support\EloquentCouponRepository;

$this->app->singleton(CouponRepositoryInterface::class, EloquentCouponRepository::class);
```

Config-Driven Model
-------------------

[](#config-driven-model)

`config/discount.php` sections:

- `event.type_role_map`
- `event.priorities`
- `coupon.scope_map`
- `coupon.code.prefixes`
- `coupon.code.templates`
- `coupon.code.tokens`
- `cart.roles`
- `cart.gift_resolver`

With this design, another project only needs config changes (type mapping, scope mapping, code template, cart role mapping) without rewriting engine logic.

Minimal Usage
-------------

[](#minimal-usage)

### Product pricing (79折)

[](#product-pricing-79折)

```
use Discount\Kernel\Contexts\ProductContext;
use Discount\Kernel\Contexts\PromotionContext;
use Discount\Kernel\Contexts\PromotionSet;
use Discount\Kernel\Engines\DefaultDiscountEngine;

$engine = new DefaultDiscountEngine();

$result = $engine->price(
    new ProductContext(1000),
    new PromotionSet([
        new PromotionContext(type: 1, sort: 1, discountAmount: 0.79),
    ])
);

$price = $result->price; // 790
```

### Coupon discount calculation (fixed + rate)

[](#coupon-discount-calculation-fixed--rate)

```
use Discount\Kernel\Contexts\CouponContext;
use Discount\Kernel\Engines\DefaultCouponDiscountEngine;
use Discount\Kernel\Enums\CouponAmountMode;

$engine = new DefaultCouponDiscountEngine();

$fixed = $engine->discount(
    1000,
    new CouponContext(scope: 0, triggerAmount: null, amount: 100, amountMode: CouponAmountMode::Fixed)
);

$rate = $engine->discount(
    1000,
    new CouponContext(scope: 0, triggerAmount: null, amount: 0.9, amountMode: CouponAmountMode::Rate)
);
```

### Coupon application service (member / promotion)

[](#coupon-application-service-member--promotion)

```
use Discount\Kernel\Contexts\CartContext;
use Discount\Kernel\Contexts\UserContext;
use Discount\Kernel\Contracts\CouponApplicationServiceInterface;
use Discount\Kernel\Enums\CouponKind;

$service = app(CouponApplicationServiceInterface::class);

$result = $service->validate(
    CouponKind::Promotion,
    'PROMO123',
    new CartContext(
        orderTotal: 1200,
        allAmount: 1200,
        bookAmount: 1200,
        ebookAmount: 0,
        specificProductsAmount: 1200,
        hasBook: true,
        hasEbook: false,
        hasSpecificProducts: true,
    ),
    new UserContext(123),
);

$isEligible = $result->eligible;
$discount = $result->discount;
```

### Coupon code generation

[](#coupon-code-generation)

```
use Discount\Kernel\Contexts\CodeContext;
use Discount\Kernel\Engines\DefaultCouponCodeGenerator;

$engine = new DefaultCouponCodeGenerator();

$code = $engine->generate(new CodeContext(
    typeValue: 13,
    userId: 123,
    count: 1,
    existsChecker: fn (string $candidate): bool => false,
));
```

### Cart adjustment generation

[](#cart-adjustment-generation)

```
use Discount\Kernel\Contexts\CartContext;
use Discount\Kernel\Contexts\PromotionContext;
use Discount\Kernel\Contexts\PromotionSet;
use Discount\Kernel\Engines\DefaultCartPromotionEngine;

$engine = new DefaultCartPromotionEngine();

$result = $engine->apply(
    new CartContext(
        orderTotal: 0,
        allAmount: 0,
        bookAmount: 0,
        ebookAmount: 0,
        specificProductsAmount: 0,
        hasBook: false,
        hasEbook: false,
        hasSpecificProducts: false,
        productId: 1001,
        productPrice: 1200,
        selectedGroupRebateEventId: null,
    ),
    new PromotionSet([
        new PromotionContext(type: 1, eventId: 201, name: 'Single discount', discountAmount: 0.8),
    ])
);

$adjustments = $result->adjustments;
```

Stable Reason Codes
-------------------

[](#stable-reason-codes)

`CouponValidationResult::reasonCode` and `CouponDiscountResult::reasonCode` are stable public contract fields.

- `COUPON_NOT_FOUND`
- `AUTH_REQUIRED`
- `COUPON_ALREADY_USED`
- `COUPON_OUT_OF_STOCK`
- `DISCOUNT_INVALID`
- `ELIGIBILITY_FAILED`

Coupon Flows (App Adapter Layer)
--------------------------------

[](#coupon-flows-app-adapter-layer)

Keep these flows in your application and call kernel engines:

- `PROMOTION (2)`: admin-created promo coupon flow
- `REGISTER (11)`: issue after user registration
- `BIRTHDAY (12)`: monthly birthday scheduler
- `FIRST_ORDER (13)`: issue after first completed order (once per lifetime)

Recommended `FIRST_ORDER (13)` dedup rule:

- Do not issue if existing coupon for same user with `created_by=CouponForFirstOrder` and usable/used status already exists.

Recommended deprecated runtime guard:

- If coupon type in `[21,22]`, skip and log warning (for example `legacy_coupon_type_detected`).

Versioning Note
---------------

[](#versioning-note)

Current package `composer.json` version is `2.1.0`. This release is a minor update from `2.0.x` and does not introduce breaking API changes. See `CHANGELOG.md` for release notes and `RELEASING.md` for sync/tag SOP.

Local Quality Checks
--------------------

[](#local-quality-checks)

Inside package directory:

```
composer install
composer analyse
```

Quick Onboarding for Another Project
------------------------------------

[](#quick-onboarding-for-another-project)

1. Install `lalalili/discount` via VCS + tag.
2. Publish or create `config/discount.php`.
3. Map local event/coupon enum values in config.
4. Bind `CouponRepositoryInterface` to your adapter implementation.
5. Set `cart.gift_resolver` (or `null` if gift not used).
6. Keep issuing flows in app adapters (`register`, `birthday`, `first order`).
7. Add runtime skip policy for deprecated coupon types.
8. Run smoke tests for checkout pricing and coupon issuance.

###  Health Score

39

—

LowBetter than 86% of packages

Maintenance86

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 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

Every ~7 days

Total

3

Last Release

69d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/6d12612270f160b9b015d603ed637f88cfc5edd78f2e287b29ac9ae22a71e8f1?d=identicon)[lalalili](/maintainers/lalalili)

---

Top Contributors

[![lalalili](https://avatars.githubusercontent.com/u/7522570?v=4)](https://github.com/lalalili "lalalili (7 commits)")

###  Code Quality

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

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

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

###  Alternatives

[barryvdh/laravel-ide-helper

Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.

14.9k123.0M684](/packages/barryvdh-laravel-ide-helper)[orchestra/canvas

Code Generators for Laravel Applications and Packages

21017.2M157](/packages/orchestra-canvas)[illuminate/pipeline

The Illuminate Pipeline package.

9446.6M210](/packages/illuminate-pipeline)[illuminate/pagination

The Illuminate Pagination package.

10532.5M858](/packages/illuminate-pagination)[spatie/laravel-pjax

A pjax middleware for Laravel 5

513371.8k11](/packages/spatie-laravel-pjax)[spatie/laravel-mix-preload

Add preload and prefetch links based your Mix manifest

169176.0k2](/packages/spatie-laravel-mix-preload)

PHPackages © 2026

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