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

Filament Coupons Laravel Package

noxoua/filament-coupons

Filament plugin for creating and managing coupons and discount codes in your admin panel. Define discounts, attach them to records, track redemptions, and control validity from a simple Filament resource.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the stack before typing composer require. This package targets Filament 3.x on Laravel 10/11, PHP 8.1+. If your panel is on Filament v2 or you are evaluating v4, stop — that is a hard blocker, not a configuration problem. Check the composer.json constraints on the repo against your composer.lock (especially your Filament/Livewire pins) before anything else; a resolver conflict here is the most common early failure.

Install and publish.

composer require noxoua/filament-coupons:1.0.3
php artisan vendor:publish --provider="Noxoua\FilamentCoupons\FilamentCouponsServiceProvider"

Publish tags vary between Filament plugins — run php artisan vendor:publish with no arguments and pick the coupons config/migration entries rather than guessing a tag name. Then:

php artisan migrate

Register the plugin on the panel (not globally) in your PanelProvider:

->plugin(\Noxoua\FilamentCoupons\FilamentCouponsPlugin::make())

Verify the exact plugin class name against the package's src/ — the README is thin and the namespace is the thing you will most likely typo.

Where to look first, in this order:

  1. The migrations. The schema is the real contract. Before you write a line of checkout code, read the coupons table: how is the discount value stored (integer minor units or decimal)? Is there a type enum/string, expires_at, usage_limit, used_count, is_active? Is there a separate redemptions table, or just a counter column? Everything downstream depends on this answer.
  2. The Coupon model — casts, fillable, any scopes for "active" / "not expired".
  3. The Filament resource — this tells you what the package considers a valid coupon, and it is what your ops team will actually touch.
  4. Any strategy/calculator classes — the description mentions "customizable strategies." Find the interface. This is your primary extension point.

First use case to validate end-to-end: open the panel, create a 10% coupon with an expiry date and a usage cap of 5, then read it back in php artisan tinker and confirm the field values and types your checkout will consume. If that round-trip works, the admin half is done and you can scope the pricing half honestly.

Implementation Patterns

Admin-only first, wire checkout second. Ship the resource to internal users with zero integration. It delivers immediate ops value (marketing can issue codes) and carries no money-path risk. Then integrate under a feature flag. This sequencing is the single highest-value decision in the whole adoption.

Never let checkout code touch the package's models directly. Wrap it:

interface DiscountCalculator {
    public function quote(string $code, Cart $cart): DiscountResult;
}

Bind a PackageDiscountCalculator implementation that reads the coupon, validates it, and delegates to the package's strategy if one exists. Call sites depend on the interface, not the package. When the upstream goes quiet or you outgrow it, you swap one binding.

Lookup and validation belong in a service, not a controller. A CouponService::findRedeemable(string $code, ?Customer $customer) that checks, in order: exists → active → not expired → under usage limit → (optional) per-customer limit. Return a typed result object, not a boolean, so the UI can say why a code failed.

Redemption must be atomic. Do not read used_count, compare, then increment. Use a conditional update inside a transaction:

$affected = DB::table('coupons')
    ->where('id', $coupon->id)
    ->where(fn ($q) => $q->whereNull('usage_limit')->orWhereColumn('used_count', '<', 'usage_limit'))
    ->increment('used_count');

if ($affected === 0) {
    throw new CouponExhausted();
}

If you need per-customer limits or idempotency on order retries, add your own coupon_redemptions table with a unique constraint on (coupon_id, order_id) rather than patching the package's migrations. That gives you a DB-level guarantee plus an audit trail finance will eventually ask for.

Extend the resource by subclassing, not editing vendor files. Filament resources are overridable in form(), table(), and getPages():

class CouponResource extends \Noxoua\FilamentCoupons\Resources\CouponResource
{
    public static function form(Form $form): Form { /* parent::form($form)->schema([...]) */ }
}

Then register yours in the panel and disable/omit the vendor one. Prefer adding fields via a Section appended to the parent schema over rewriting the whole form — smaller diffs when you upgrade.

Use factories for tests, seeders for demos. Whatever model the package ships, wrap it in a factory that produces a known-good coupon, then write one end-to-end test that creates a coupon through the service, redeems it, asserts the counter moved, and asserts a second redemption past the limit fails. That test catches both upstream breaks and your own regressions — its absence is the biggest gap in this package's own quality story.

Custom strategies go behind the interface, always. If the package exposes a strategy contract, implement it for your business rules (stacking, minimum cart value, category restrictions) but keep the implementation in your app namespace. If it does not expose one — verify before assuming — write your own calculator entirely and use the package purely as storage + admin UX. Either way, the package should never be the place your pricing policy lives.

Multi-panel and tenancy: register the plugin only on the panel that needs it, and add a CouponPolicy so the resource respects your role model. Filament's default authorization will otherwise let anyone with panel access edit coupons.

Gotchas and Tips

  • Pin the exact version (1.0.3), not ^1.0. The 1.0.3 release notes are a bare compare link — no changelog body. A silent rounding change in a patch release is plausible. Read 1.0.2...1.0.3 before upgrading, every time; budget half a day per release for that review.
  • Published migrations become yours. Once they run in production, do not edit them. Extend with new migrations. Also test them against a copy of production schema first — index and collation conflicts on coupon/redemption tables are a realistic spike failure.
  • Don't let other services join to coupons directly. Expose reads through your repository/service. Cheapest insurance against a schema change you don't control.
  • Expiry and timezones. Store expires_at in UTC, compare with now() consistently, and decide explicitly whether a coupon expires at end-of-day in the customer's timezone or yours. "Expired mid-session" is a real support ticket.
  • Clamp the discount. A fixed-amount coupon larger than the cart total will produce a negative total if nothing guards it. Clamp to subtotal in your calculator, not in the view.
  • Rounding precedence is yours. Percentage on the order total vs. per line item give different numbers, and refunds/proration amplify the difference. Write it down and unit-test it.
  • Concurrency is the failure mode you can't see. Two sessions consuming a single-use code looks fine in testing and becomes a reconciliation problem in production. The conditional-update pattern above is the minimum; a unique constraint on redemptions is better.
  • Filament v4 is not supported. Pin Filament in your lockfile alongside this package so a routine composer update doesn't force a decision under pressure.
  • Debugging "resource doesn't appear": check the plugin is registered on the correct panel, the policy allows viewAny, and run php artisan filament:optimize-clear. Version mismatch between Filament and the plugin usually surfaces as a class-not-found on a form component, not a clean error.
  • Custom strategy is the safest extension point; forking is the last resort. If you fork, you own upgrades forever. If the package's calculator can't express per-customer limits or atomic counters, add your own tables and a service layer — don't fork the internals.
  • Maintenance reality: single maintainer, 1.0.x, low star count. Assign a named code owner. Set a calendar trigger — six months of no releases → evaluate vendoring or replacing with a ~200-line internal resource. It is not worth being permanently blocked on a dependency you can't upgrade.
  • Licensing is MIT — no legal blocker to vendoring if that day comes.
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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager