tastyigniter/ti-ext-coupons
TastyIgniter Coupons extension for adding fixed amount or percentage discounts, advanced usage restrictions, location-based and time-sensitive promotions to boost restaurant sales.
Requires a working TastyIgniter install (v4.x; v4.2.0 of the extension pins tastyigniter/core to ^v4.4 — check your lockfile before upgrading). This is not a general-purpose Laravel package: it registers models, admin UI, cart/checkout hooks, and API resources through TastyIgniter's extension system. Do not try to install it into a plain Laravel app.
composer require tastyigniter/ti-ext-coupons
php artisan igniter:up # runs extension migrations + publishes assets
php artisan migrate
Then enable it in Admin → System → Extensions (or php artisan igniter:extension:enable TastyIgniter.Coupons). The menu entry appears under Marketing / Promotions → Coupons.
Read these files first, in this order:
database/migrations/*coupons* and *coupon_history* — the real schema is the source of truth for fields like validity, order_restriction, is_limited_to_cart_item, auto_apply, and the redemption counters. Faster than guessing from the README.src/Models/Coupon.php — especially isValid() / the validation scope methods and the menus / categories / locations relations. This is the entire business logic surface.src/Controllers/Coupons.php + models/coupon/fields.yaml or equivalent) — it shows which restrictions are actually wired vs. merely documented.src/CartConditions/ (or wherever the coupon registers as a cart condition) — how the discount lands on the order total.routes/ or src/Api/ — confirm the coupons:read / coupons:write abilities before you promise an integration.First use case to build end-to-end: a percentage coupon with a minimum order total, limited to one collection location, with usage_limit_per_customer = 1. Create it in the admin, place a test order through the storefront, then verify the row in coupon_history. That single loop exercises validation, cart condition application, and the redemption ledger — the three things that will bite you later.
Seeding for dev: you can create coupons programmatically, but the model has many relation-based attributes; build a small factory in database/seeders using the model's create() with locations, menus, categories synced afterwards rather than mass-assigning arrays. Keep the seeder in your app, not in the vendor directory.
Programmatic coupon creation. Treat the admin form fields as the contract:
$coupon = Coupon::create([
'name' => 'First Order 10%',
'code' => 'WELCOME10',
'type' => 'P', // fixed 'F' / percentage 'P' — verify constants on the model
'discount' => 10,
'minimum_total' => 25,
'validity' => 'period',
'valid_from' => now(),
'valid_to' => now()->addMonth(),
'order_restriction' => 'delivery', // or 'collection'
'auto_apply' => false,
'status' => true,
]);
$coupon->locations()->sync([$locationId]);
Confirm type, order_restriction, and validity literals against the model constants/migration enum before shipping a seeder — the README does not document them.
Validation as a single gate. Route every UI and API check through one place — Coupon::isValid() (or the service that wraps it) — rather than reimplementing minimum-total / time-window logic in your controller. When support reports "the code doesn't work," you want exactly one function to add logging to. Wrap it:
$result = $coupon->isValid($cart); // returns bool or a reason depending on version
if (! $result) {
Log::info('coupon.rejected', ['code' => $coupon->code, 'cart' => $cart->id]);
}
Redemption lifecycle — respect the two-phase flow. Since v1.10.3/1.10.4 the extension logs the coupon before payment and marks it redeemed after payment succeeds. If you add a custom payment gateway or an off-site checkout, replicate that ordering; otherwise abandoned payments burn through usage_limit_per_customer. Any custom code that writes coupon_history must be idempotent per order.
Auto-apply as a checkout condition. With auto_apply = true, the coupon attaches without a code. That is the "discount already applied" UX. Note that auto-apply and manual codes can collide — decide in your own code whether a manual code overrides the auto-applied one, and clear the previous condition explicitly when it does.
Location scoping for multi-site. validity is per-coupon, but locations is a pivot. For franchise rollouts, create one coupon per location (or a shared coupon synced to a set of locations) and name them with a predictable prefix (LAUNCH-LONDON-10) so marketing can filter in the admin list. The v4.1.3 change moved this to the Location class directly — if you have an older fork/override reading a serialized blob, adapt it.
Back-office/API integration. Use the JSON:API endpoints for a marketing dashboard only after issuing a token scoped to coupons:read / coupons:write. Do the read-only dashboard first; write access from an external tool is where you'll discover the validation gaps. Beware created_at showing as -0001-11-30 00:00:00 in documented examples — normalize/guard timestamps in your transformers rather than surfacing them to users.
Reporting. Build finance/support views on the coupon_history table, not on the coupon's counter column. The history carries coupon, order, customer, amount, and timestamp — that's your audit trail. Join to orders and sum discounts to reconcile against till totals.
Testing. Write contract tests around the validation path with a stubbed cart: valid window, expired window, below minimum, wrong day/time, delivery vs collection, restricted customer, exhausted usage. These are cheap to write and are the only thing protecting you on a core upgrade, since upstream triage routes through the TastyIgniter core tracker.
redemptions: 0 almost certainly means "unlimited," but verify it against the validation code before you build any reporting or enforcement on top. Same for null date columns — the docs show sentinel dates (-0001-11-30), which are usually the zero-value of the date type, not real data.config('app.timezone') is consumed in isValid() before trusting it.order_restriction once evaluated incorrectly, add a regression test for both delivery and collection against a collection-only cart, not just a happy path.Coupon::saved) for syncing to a CDN-cached promo banner, a custom cart condition that wraps the coupon for extra eligibility rules, and overriding the admin form fields in your own extension to add metadata columns. Editing vendor/tastyigniter/ti-ext-coupons directly is a dead end on the next composer update.How can I help you explore Laravel packages today?