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.
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:
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.Coupon model — casts, fillable, any scopes for "active" / "not expired".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.
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.
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.coupons directly. Expose reads through your repository/service. Cheapest insurance against a schema change you don't control.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.composer update doesn't force a decision under pressure.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.How can I help you explore Laravel packages today?