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

Coupon Bundle Laravel Package

elcodi/coupon-bundle

Coupon Bundle is part of the Elcodi e-commerce suite of Symfony components and bundles, providing coupon functionality for online stores. Note that this package is read-only; issues, questions, and pull requests belong in the main elcodi/elcodi repository.

View on GitHub
Deep Wiki
Context7

Getting Started

Elcodi's coupon-bundle is a read-only subtree split of elcodi/elcodi. Install it for dev-tracking convenience but treat the monorepo as your source of truth:

composer require elcodi/coupon-bundle

Then enable it in AppKernel.php (order matters — it depends on CartBundle, ProductBundle, and RuleBundle):

new Elcodi\Bundle\CartBundle\ElcodiCartBundle(),
new Elcodi\Bundle\RuleBundle\ElcodiRuleBundle(),
new Elcodi\Bundle\CouponBundle\ElcodiCouponBundle(),

Where to look first, in order:

  1. Resources/config/services.yml — the DI wiring tells you the real service IDs (elcodi.coupon_manager, elcodi.coupon_factory, elcodi.coupon_repository, elcodi.coupon_transformer, etc.).
  2. Entity/Coupon.php + Entity/Interfaces/CouponInterface.php — the shape of a coupon (code, type, value, valid dates, usage counters, rules collection).
  3. Entity/CouponRule.php (and the rule classes under Rule/) — Elcodi leans on a rule/expression engine rather than hard-coded conditions, so most customisation happens here.
  4. Manager/CouponManager.php — the actual apply/validate façade you will inject into controllers and services.

Your first use case is almost always "apply a coupon code typed by a shopper into a cart, and return a usable discount or a reason it failed." That flow is:

$coupon = $this->get('elcodi.coupon_repository')->findOneByCode($code);

if (null === $coupon) {
    // unknown code — do not leak existence
}

$this->get('elcodi.coupon_manager')->applyCoupon($coupon, $cart);

Do not reach for CouponRepository::findOneByCode() and start mutating the cart yourself — the manager is what coordinates rule evaluation, date validity checks, usage counters and cart totals. Skipping it is the fastest way to produce a coupon that "works" in admin but silently fails in checkout.

Implementation Patterns

Pattern 1 — Programmatic coupon creation (fixtures, imports, admin imports). Use the factory, not new Coupon(); the factory wires the ID, enabled flag, timestamps and empty rules collection:

$coupon = $this->get('elcodi.coupon_factory')->create();
$coupon
    ->setCode('SUMMER10')
    ->setType('percentage')     // or 'fixed_amount'
    ->setValue(10)
    ->setValidFrom(new \DateTime())
    ->setValidTo(new \DateTime('+30 days'))
    ->setCount(0)
    ->setEnabled(true);

$this->get('elcodi.coupon_repository')->save($coupon);

Pattern 2 — Rule-driven eligibility. Rather than branching on coupon attributes in your controller, attach CouponRule entities. Each rule has a name, an expression, and a target rule_type. This is how the CouponBundle composes things like "minimum basket subtotal", "only on first order", "restricted to product X". Keep the logic in rules so the same coupon validates identically at cart-time and order-time.

Pattern 3 — Apply at cart, persist at order. The cart holds coupons in-memory via CartCouponManager; the order persists coupon references through order listeners. Two useful integration points:

  • A cart listener (elcodi.cart_coupon_applier or your own CartEvent subscriber) that re-validates coupons on every cart mutation — coupons must survive quantity changes, not just the moment they were entered.
  • An order listener (OrderOnCreatedEvent) that calls couponManager->consumeCoupon($coupon, $order) so the usage counter only increments on successful checkout, not on add-to-cart.

Pattern 4 — Expose failures, not booleans. The manager's coupon-apply path throws on hard failures and returns enriched coupons on success. Wrap in a try/catch and map exceptions to user-facing flash messages ("This coupon has expired", "Coupon not valid for this basket"). Your template then renders the discount line item from Cart::getCoupons() rather than recomputing it.

Pattern 5 — Admin CRUD. The bundle ships no admin UI (Elcodi's admin lives above the bundles). Wire a form for CouponType (the bundle exposes the entity, you build the form type) and always validate code uniqueness via the Coupon entity's unique constraint — otherwise checkout will 500 on the first duplicate insert rather than show a friendly error.

Gotchas and Tips

  • Read-only repository. All issues, PRs and version tags must go through elcodi/elcodi. The split has no composer.json branch aliasing beyond dev-master. If you pin a tag here, you inherit the monorepo's release cadence; if you track dev-master here, you are testing the whole suite. Prefer the elcodi/elcodi requirement in production.
  • Package age. Elcodi is not actively developed. Confirm the Symfony version constraint against your app before install — modern Symfony will need symfony/* overrides or you will fight deprecations in the event-dispatcher and doctrine-bridge wiring.
  • Type values are strings, not constants. setType() takes percentage / fixed_amount (and the coupon types in CouponTypes). Misspellings fail silently — nothing validates the type against the enum. Wrap this in a constant or your own value object.
  • Money is integer minor units. Coupons and cart totals use Elcodi's currency-aware money handling. Passing 10 as a fixed-amount coupon means 10 minor units (cents) unless your currency config differs. Percentage coupons are evaluated against the subtotal, not the post-discount total. Print $coupon->getValue() and the resulting Cart::getAmount() side-by-side when a 10% discount "looks wrong" — 95% of these tickets are unit confusion.
  • Timezone in validFrom/validTo. The database stores naive datetimes; the comparison uses PHP's default timezone. Set date_default_timezone_set() or php.ini consistently between dev and prod, or expiry boundaries will drift.
  • Usage counters are not atomic. count / count_used are incremented in PHP. Under concurrent checkouts you can exceed a limited-use coupon. If you sell limited coupons, add a SELECT ... FOR UPDATE in your own repository method or a DB CHECK (or unique "coupon_usage" row per order).
  • Debugging rule failures. The expression engine swallows context. Log $coupon->getRules() and each rule's getName()/getExpression() on invalid results, and run the expression against the cart manually. The RuleBundle's compiler is where the actual failure occurs, not in CouponManager.
  • Extending. Custom coupon behaviour is best added as a new CouponRule subclass plus a Doctrine entity, registered via a compiler pass in your own bundle — do not edit the vendor tree (subtree splits make that unrecoverable). For new coupon types, override the elcodi.coupon_applier service definition with your own class implementing the same interface.
  • Do not delete consumed coupons when an order is voided without also decrementing the counter via a refund listener; otherwise your coupon is either over- or under-used for the rest of its life.
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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log