michael-rubel/laravel-couponables
Laravel Couponables adds coupon and voucher support to any Eloquent model. Create redeemable codes, attach them polymorphically to users or orders, and enforce usage limits, expiry dates, and per-model redemption rules through a clean, extensible API.
Architecture fit
laravel-couponables is a thin, single-purpose domain library, not a merchandising or promotion-rules engine. Its design centers on two concepts: a Coupon entity (code, type/value discount, expiry, usage limits) and a polymorphic redemption relationship, so any Eloquent model — User, Order, Cart, GuestSession — can have coupons attached and redemption history recorded via a morph map. It ships a Couponable trait, a Coupon model, a redemption/pivot model, config-driven model swap-outs, migrations, and guard exceptions for expiry, limits, and invalid redemptions.
For a TPM, the fit question is scope, not technology. It fits cleanly where the business needs promo codes that discount an amount and track who redeemed what. It is a poor fit where the business needs promotions: stacking, priority/conflict resolution, product/category targeting, multi-currency, BOGO, tiered or conditional discounts, or campaign orchestration. Those must be built on top of, or instead of, this package. The polymorphic redemption model is genuinely useful — it decouples "who consumed the coupon" from "what the coupon is", which matches how most teams model redemption on user and order simultaneously.
Integration feasibility
Low friction. composer require, publish config and migrations, add the trait to one or two models, and redemption is available. No external services, no queues, no scheduled jobs, no SaaS dependency. The package integrates at two natural seams in a typical checkout: a validation/apply step (read-only check that a code is valid for this model right now) and a consumption step (write the redemption, ideally inside the same transaction that persists the order). Exceptions provide a clean branch point for user-facing error messaging.
Technical risk
lockForUpdate or equivalent at the call site.expires_at, coupon-code case sensitivity/collation, and refund/cancellation reversal (there is no obvious first-class "un-redeem") are the classic sources of finance-visible defects.Key questions to answer in a one-week spike
Stack fit
A near-perfect fit for a conventional Laravel 8–12 / PHP 8.2–8.4 application using Eloquent on MySQL or Postgres. Version 9.0.2 explicitly adds Laravel 12 support and adds PHP 8.4 to the CI matrix, so the package now covers the current Laravel release rather than trailing it — which removes the most likely reason to fork or patch in the near term. It slots into a monolith or modular monolith without ceremony. Two stack caveats to verify: UUID/ULID primary keys (common in modern Laravel apps) and multi-tenancy (e.g. stancl/tenancy) — the latter needs either per-tenant migrations or a tenant-scoped coupon table plus a global scope, since the package has no native tenancy awareness.
Migration path
This is an additive capability, so "migration" is mostly data, not code:
discounts table), build a one-time backfill mapping legacy codes and historical redemptions into coupons + redemptions, and run a shadow period where the new path computes discounts alongside the old one and diffs results before cutover.PromotionService (or CouponManager) interface immediately. Nothing in application code should call package methods directly. This is the single highest-leverage decision: it caps upgrade/abandonment cost to one class.Compatibility
Verify Composer constraints against your framework version, and confirm the migrations run on your DB driver. 9.0.2 is a patch release (9.0.1 → 9.0.2), so no BC breaks are signalled under semver, but confirm from composer.json that Laravel 12 support was added rather than swapped in for an older branch — if you are on Laravel 8–10, prove the constraint still resolves before scheduling the bump. If you already have a pricing/discount engine, define precedence explicitly (package coupon applied before or after tax, shipping, and existing discounts) — the package will not arbitrate that for you. Confirm exception classes are caught at the boundary so a package upgrade cannot leak a 500 into checkout.
Sequencing
code and morph columns.If you are migrating to Laravel 12 this year, slot the 9.0.2 bump into that same upgrade train rather than running it as a separate change — it is now a tested-supported combination, and bundling keeps the regression surface to one deploy.
Maintenance
Low ongoing code churn: the package is small and stable, and releases are infrequent. The real cost is ownership, not upkeep — your team owns the integration, the adapter, the tests, and the upgrade decision. Pin the version in composer.json (e.g. ^9.0), review release notes before bumping, and keep the adapter's test suite green so a bump is a one-PR event. The 9.0.2 release is exactly the shape of change we want to see from a low-bus-factor dependency: framework-matrix upkeep with no behavioural changes. It also confirms the maintainer is still responsive to ecosystem shifts, which meaningfully lowers the fork-and-vendor probability over a 12–18 month horizon. Keep the fork option documented regardless; the adapter makes it a contained operation.
Support
MIT, community-only, no SLA and no vendor to escalate to. Documentation is README-level; expect engineers to read the source for edge-case behavior. Budget for internal documentation of your integration conventions (precedence, rounding, reversal policy) — that is where institutional knowledge will otherwise evaporate.
Scaling
The workload is read-heavy on validation and write-light on redemption, which scales comfortably. Watch two things: (1) hot-code contention — a launch-day code redeemed by thousands of users concurrently will serialize on the coupon row if limits are enforced with locks; (2) index hygiene — ensure code is uniquely indexed with the correct collation and the polymorphic columns are indexed for reporting queries. Caching is safe for validation previews but must never be the source of truth for consumption; always re-check inside the transaction. Redemption history grows monotonically — plan for archival if coupon volume is high.
Failure modes
expires_at handling at the boundary.Ramp-up
Small surface area. A mid-level Laravel engineer can complete the foundation and checkout integration in roughly one to two weeks, with the spike and adapter/test work adding another week. The TPM-facing effort is front-loaded: pin down discount semantics, precedence, rounding, and reversal policy before engineering starts, because those are business decisions the package deliberately does not make. The modest star count and zero dependents should raise the bar on internal test coverage rather than block adoption — this is a utility library, and the correct posture is "adopt with an adapter, a pin, and a fork option." The 9.0.2 release mildly strengthens that posture: current-framework support is now maintained, so the primary residual risks are concurrency semantics and your own integration discipline, not abandonment.
How can I help you explore Laravel packages today?