beyondcode/laravel-vouchers
Beyondcode's Laravel Vouchers package lets you generate, manage, and redeem voucher or coupon codes in Laravel apps. Set expiry dates, usage limits, and custom redemption rules, then attach vouchers to users or models through a simple, flexible API.
Architecture fit
vouchers table, a Voucher model, a HasVouchers trait applied to issuing models, and a redeem() state transition. Vouchers attach to any Eloquent model via a polymorphic relation, so one table serves promo codes, referral invites, license keys, partner activations, and beta access without per-feature schema work. From a roadmap standpoint this is an enabler that unblocks a family of features rather than a feature itself.data column is the only extension point. Business meaning (value, plan, SKU, currency, campaign) lives in our application code — flexible, but with no schema-level guarantees, no indexing, and no validation. Malformed payloads are possible and should be validated at the boundary we build.Integration feasibility
composer require, publish migrations, add the trait, create and redeem. Half a day to a working spike. One to two weeks to production-grade once tests, an admin surface, authorization, idempotency, and observability are included.Technical risk
redeem() is a read-then-write on redeemed_at, two concurrent requests can both succeed on a single-use code. This must be verified in source, not assumed. If the guarantee is absent, wrap redemption in a transaction with lockForUpdate() or a conditional update backed by a unique index, and budget it as work rather than a caveat. Flash sales, launches, and partner distributions are exactly where this surfaces.illuminate/* constraint gap against our pinned Laravel version is the single most likely reason a spike fails, and it is the cheapest thing to check first. Confirm the PHP constraint at the same time.Key questions to resolve before commitment
redeem() transactional and safe under concurrent redemption? Show the mechanism, or we add locking.Voucher factory ship, and do the published migrations behave under RefreshDatabase?Recommendation: adopt behind an isolation layer, or reimplement in a day and own it outright — the core is plausibly 150–250 lines. Adopting without an isolation boundary is the only indefensible option, because it converts a reversible decision into a migration project.
Stack fit
Migration path
code; composite index on the polymorphic owner columns plus redeemed_at; an explicit decision on soft deletes.vouchers with data preserving original semantics, feature-flag the old read path, cut writes over first, then reads. Validate redeemed/expired state during backfill rather than importing blindly.VoucherIssuer / VoucherRedeemer boundary on day one. No code outside that layer imports BeyondCode\Vouchers\*. This is what makes replacement a contained refactor, and it is the single place to enforce locking, authorization, and event dispatch.Compatibility
illuminate/* constraint against our pinned Laravel version before scoping anything else — cheapest possible disqualifier.data for reporting.RefreshDatabase, and whether a Voucher factory ships. If not, we write it before the first feature test.Sequencing
VoucherIssuer, VoucherRedeemer, domain events, and policy classes for who may issue and redeem. Nothing ships before this exists.Maintenance
illuminate/* constraint or fork), and dependency-drift reviews across the small tree.Support
Scaling
code and the polymorphic owner columns carry the load.Failure modes
data payloads causing runtime exceptions during redemption. Mitigation: validate and version the payload shape on write; treat data as an internal contract.Ramp-up
How can I help you explore Laravel packages today?