Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Promocodes Laravel Package

zgabievi/laravel-promocodes

A Laravel package for creating and managing promo codes: generate unique codes, attach them to any Eloquent model, set fixed or percentage discounts, apply expiration dates and usage limits, then validate or redeem codes through a simple, expressive API.

View on GitHub
Deep Wiki
Context7

Getting Started

The 9.1.0 release is additive — nothing here is breaking — but it meaningfully changes the day-to-day workflow: you can now validate a promocode at the edge of your application (in a FormRequest) and list/query promocodes without hand-rolling Eloquent queries. Start by reading the new retrieval methods and the validation rule before writing any new checkout code.

Minimal setup:

composer require <vendor>/<package>   # upgrade to ^9.1
php artisan vendor:publish --tag=<package>-config
php artisan vendor:publish --tag=<package>-migrations
php artisan migrate

Where to look first, in this order:

  1. The config file — specifically the user model / user association key. The 9.1.0 fix for custom user models means this is now configurable and respected; confirm it points at your App\Models\User (or whatever you actually authenticate with) rather than the default.
  2. The migrations — check the code, usages_left, expires_at and user-bound columns so you know what the retrieval methods can filter on.
  3. The facade / service class — the retrieval methods are the new surface area; skim their signatures to see what they return (collection vs. query builder vs. single model).
  4. The validation rule class — the new rule is the single biggest workflow improvement in this release.

First use case to implement: a promocode field on your checkout/cart form. Validate it with the new rule in a FormRequest, then redeem it inside the controller action. That's the whole loop, and it's now much shorter than before.

Implementation Patterns

Validate at the request layer, redeem in the action. The new validation rule is designed for exactly this, so move promocode checks out of controllers and into FormRequests:

class ApplyPromocodeRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'code' => ['required', 'string', 'max:64', new PromocodeRule()],
        ];
    }
}

Treat the rule as a pre-flight check only. It validates that the promocode exists and is applicable for the current context; it does not reserve or consume it. Always redeem inside the action, and inside a transaction (see Gotchas).

Use the new retrieval methods instead of local scopes. Prior to 9.1.0 most projects accumulated a Promocode::where('active', true)->where(...) scope in the app layer. Migrate those to the package methods so your eligibility logic matches the package's own rules:

  • For a customer-facing "available offers" list, use the retrieval method that returns currently applicable promocodes, filtered by the authenticated user.
  • For admin listings, use the retrieval method that returns all/expired/exhausted ones so admins can audit.
  • For a "my codes" page, use the user-scoped retrieval now that custom user models resolve correctly.

Keep a thin app-level PromocodeService that wraps these calls; that's your seam for caching and for swapping logic later.

Wrap apply/redeem in a transaction and make it idempotent. The classic pattern:

return DB::transaction(function () use ($code, $user, $cart) {
    $promocode = Promocodes::redeem($code, $user); // or the bound-user API
    $cart->applyDiscount($promocode);
    return $promocode;
});

Pass the user explicitly where the API allows it — this is more reliable than relying on auth() resolution, especially in queued jobs, API tokens, and impersonation flows.

Use the validation rule in API and Livewire contexts too. Because it's a standard Laravel rule object, it composes with Rule::when, bail, and sometimes, and it works identically in Livewire validate() calls and JSON:API style endpoints. Centralising it kills the "different eligibility logic per entry point" bug class.

Test with the rule, not with raw DB assertions. Feature tests can now assert assertSessionHasErrors('code') against the rule instead of manually constructing promocodes and asserting on a service exception. For unit tests, seed promocodes via the model factory (or the package's create method) and drive the rule directly.

Gotchas and Tips

  • Validation ≠ reservation. The new rule is a read-only check. Two concurrent requests can both pass validation and both attempt to redeem. Always re-check and decrement inside a transaction with a row lock (lockForUpdate), or rely on the package's atomic redeem path. This is the number-one source of oversubscription bugs with promocodes.
  • Custom user models: verify, don't assume. The 9.1.0 fix means custom models should now associate correctly, but the user model is resolved via config. If you changed it, run php artisan config:clear — a stale cached config is the most common reason "the fix doesn't work." Also check for morph-map conflicts if you use Relation::morphMap() and your user model is aliased.
  • Case sensitivity of codes. Decide on normalisation (uppercase on input, uppercase on store) before you have data. If codes are stored mixed-case and you filter by exact match, users typing lowercase will fail validation and file bugs.
  • Expiry and timezones. expires_at is almost always compared against now() in the app timezone. If your app timezone differs from UTC storage, an expiry "today" can behave unexpectedly near midnight. Add an explicit test at the boundary.
  • usages_left semantics. Confirm whether null means "unlimited" or "zero" in your version, and whether validation treats exhausted codes as invalid or merely "not applicable." Assert both in tests — this is where package upgrades typically introduce subtle behaviour changes.
  • Uniqueness of the code column. Ensure a DB-level unique index exists. The create methods won't necessarily guard against duplicates under concurrency, and duplicate codes make retrieval methods return ambiguous results.
  • Config cache in CI. Any test that touches the user model association or an eligibility threshold must run with a fresh config. Add php artisan config:clear to your test bootstrap if you've been bitten before.
  • Extension points. If the new retrieval methods don't cover a query you need, prefer composing them (calling the method and adding ->where(...)) over reimplementing eligibility. If they return a collection rather than a builder, filter in PHP — don't reach into the model and duplicate the rules, or you'll drift on the next release.
  • Debugging. For a failing redemption, log the three states separately: the model found by code, the computed eligibility, and the post-redeem usages_left. Nine times out of ten the failure is in eligibility (user binding, expiry, exhausted), not in the write itself.
  • Upgrade note. 9.1.0 is additive, so the upgrade should be a patch-level composer bump with no code changes required. Regression-test promocode redemption against a bound user after upgrading — that's the exact path the custom-user-model fix touched.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console