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 Discount Laravel Package

binafy/laravel-discount

Laravel Discount lets you apply percentage, fixed amount, buy X get Y, tiered, and free shipping discounts with codes, expiry dates, usage limits, and stackable rules. Integrates with Laravel Cart.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit. binafy/laravel-discount is a domain service, not a commerce platform. It ships plain Eloquent models over three tables (discounts, discount_usages, discountables), a facade with a deliberately small surface (apply, applyCode, applyMany, isValid, findByCode, generateCode(s), matchingTier), and a DiscountResult value object (originalAmount, discountAmount, payableAmount(), hasFreeShipping(), payableShipping(), payableTotal()). That is the right shape for a Laravel monolith whose checkout owns orders and payments: the package owns promotion definitions and evaluation, not the order lifecycle. It is a poor fit if checkout is a separate service — you would either re-expose the library over an API or reimplement the rule set elsewhere.

Integration feasibility. High. Auto-registered service provider, migrations run out of the box, optional publish of config (table names, user model, code alphabet) and migrations. The README is unusually complete for a 58-star package: per-type examples, documented exception types, an explicit upgrade note for the type enum widening, and edge-case illustrations (fixed discounts clamped at zero, tier boundaries, BXGY with mixed baskets). The result object is a clean seam for an internal adapter, which is where I would put the package behind our own interface on day one.

Technical risk.

  • Money representation. Examples return floats (60.0, 100.0) against decimal(10,2) storage. Rounding policy (half-up vs banker's), accumulation across stacked discounts, and penny-level reconciliation against the PSP/invoice are undocumented. Highest-severity unknown.
  • Redemption atomicity. Usage limits and redeem are described functionally, not transactionally. A popular code under concurrent checkout is a classic oversell; assume we must add locking or a unique redemption key until proven otherwise.
  • Refund / void semantics. No documented release of a usage count on cancellation, refund, or failed payment. Either the package handles it (verify in source) or we own a reconciliation job.
  • Schema still moving. The recent release added buy_x_get_y, tiered, and free_shipping types, requiring existing installs to widen an enum and make value optional. Schema-affecting feature additions in a young package imply upgrade toil and migration rehearsal.
  • Stacking semantics. The feature list says "stackable"; the free-shipping section says free shipping applies "on top of whichever monetary discount wins," implying a single-monetary-winner model. That distinction materially changes promotion design and must be confirmed empirically, not from the README.
  • Line-item allocation. Results are order-level only. Tax-inclusive pricing, per-line invoice discounts, and partial refunds all require proportional allocation the package does not appear to provide.
  • Adoption/bus factor. 58 stars, 0 dependents, MIT, no commercial backing. There is no third-party validation; the CI test badge is a positive signal but coverage depth is unknown. We are the QA team.
  • Currency and tax neutrality. No currency field and no guidance on whether discounts apply pre- or post-tax. Fine for single-currency, but a constraint to document now.

Key questions.

  1. Are amounts integer minor units internally, and what is the documented rounding rule per type and across stacking?
  2. Is redeem/usage-limit enforcement atomic (row lock, unique constraint, or idempotency key)? Can double-submit burn two usages?
  3. How is a usage released on refund, cancellation, or failed payment — automatic, event-driven, or manual?
  4. Can two monetary discounts stack, or is it strictly one winner plus free shipping? What is the tie-break order?
  5. Is apply() the single validation path, or can constraints (min_order_value, windows, limits) be bypassed by constructing a Discount directly?
  6. Are starts_at/expires_at evaluated in app timezone or UTC, and is the boundary inclusive?
  7. What does discountables scope — global product eligibility, or per-user/segment? How are guest redemptions keyed (session, IP, email)?
  8. Test coverage percentage and whether driver-specific behaviour (MySQL enum, Postgres check constraint, SQLite in tests) is exercised.
  9. Maintainer response profile on issues and PRs over the last 6–12 months.
  10. Does isValid() share code with apply(), or are there two divergent evaluators?

Integration Approach

Stack fit. Laravel 9+ / PHP 8.1+; confirm the tested matrix against our actual Laravel minor and DB driver before committing. The package is happiest alongside binafy/laravel-cart (same vendor). If we run a different cart, integration is still viable — we pass an amount and optional quantity, and consume DiscountResult — but we absorb any line-level allocation work ourselves. Free shipping is the one integration that is not self-contained: the flag is advisory, so the shipping calculator must be changed to honour it, with a test that fails if it doesn't.

Migration path. Pin an exact version in composer.json. Publish config and migrations into the application so the schema is versioned in our repo rather than implicitly by the vendor — this also fixes the "published migration stops loading the package copy" behaviour into something we control. Rehearse on an ephemeral database in CI, then staging, then production. If any discounts table already exists, ship the enum-widening migration first and add doctrine/dbal if we are on Laravel 10 or below. Backout is clean: drop three tables, remove the dependency, no unrecoverable data — provided promotions are not the sole source of truth for historical order pricing (they must not be; snapshot the applied discount onto the order record).

Compatibility. Verify ->change() behaviour per driver (MySQL 8 vs Postgres check-constraint representation of enum(), SQLite in tests). Store and evaluate timestamps in UTC with an explicit conversion at the display layer.

Sequencing.

  • Phase 0 — spike (1–2 weeks). Harness with property tests for rounding, a concurrency test hammering a single-use code, and a refund/cancel probe. This is the go/no-go gate.
  • Phase 1 — adapter. Wrap the facade behind our own DiscountPort plus our own DTO, map package exceptions (DiscountNotFoundException, DiscountExpiredException, DiscountNotStartedException) to domain errors at that boundary, and enforce an architecture rule that nothing outside the adapter references the facade.
  • Phase 2 — codes and admin CRUD. Coupon issuance, bulk generation as a queued job (not a request), export, and a disable switch.
  • Phase 3 — windows and events. Expiry notifications and metrics off DiscountExpired.
  • Phase 4 — tiered and buy-X-get-Y. Highest cart-math risk; ship behind a flag and with margin reporting.
  • Phase 5 — free shipping wiring into the shipping calculator, plus observability and a promotional-incident runbook.

Gate the decision on the spike proving three things: deterministic rounding, atomic redemption, and a workable refund release path (built by us if necessary).

Operational Impact

Maintenance. Expect schema and enum churn given the recent type additions; budget one to two days per upgrade for review, migration rehearsal, and regression on promotion math. Single-maintainer risk is real but partly mitigated by MIT licensing — a fork is a viable contingency if the project stalls, which is precisely why everything goes through our adapter. Add the package to the dependency-review list with a named owner.

Support. Community GitHub issues only; no SLA, no commercial escalation. We own L2/L3. That means one backend engineer as named owner, a written runbook (how to disable a live promotion, how to release a stuck usage count, how to reconcile a disputed order), and a "bad coupon" playbook that assumes minutes, not days.

Scaling. Evaluation is in-process — no network hop, a handful of queries, cheap at checkout volumes. The scaling hazards are writes, not reads: discount_usages inserts plus usage-count checks on a hot code create row contention, so index code, add a unique constraint keyed on (discount_id, order_id) for idempotent redemption, and cache read-only discount definitions with explicit invalidation on admin writes. Bulk code generation of tens of thousands should be a background job with a unique index and retry, never an HTTP request.

Failure modes.

  • Package exceptions thrown mid-checkout surfacing as 500s → map at the adapter, always.
  • Silent 0.0 when thresholds aren't met (below a tier, short of a full BXGY set) → no reason codes are returned, so the UX must explain why; build our own messaging.
  • Free-shipping flag not honoured downstream → systematic undercharge (revenue leak) or overcharge (support load). This is the single most likely silent defect; cover it with an integration test.
  • Float drift against PSP and invoice totals → reconciliation breaks at month-end.
  • Double redemption on client retry or back-button → idempotency key required at the checkout boundary.
  • Expiry boundary/timezone bugs → promotions live an hour too long or die early; store UTC, test the boundary.
  • Coupon guessing → the default 8-character unambiguous alphabet is sound entropy, but rate-limit applyCode and log failures.

Ramp-up. The README is sufficient for a mid-level Laravel developer to ship a percentage coupon in about a day; allow three to five days to be genuinely safe across tiered, BXGY, and free shipping, plus the adapter pattern. Codify a review checklist — money type and rounding, redemption idempotency, exception mapping, free-shipping propagation, timezone handling — so promotion work doesn't depend on one person's memory.

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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel