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

Tomato Coupons Laravel Package

tomatophp/tomato-coupons

A Laravel package for managing coupons and discount codes, letting you create, apply, and track promotional offers with flexible rules, expiration dates, and usage limits. Integrates smoothly with TomatoPHP and Filament admin panels or custom storefronts.

View on GitHub
Deep Wiki
Context7

Getting Started

Pin the version on install, in the same commit as the config publish. This package's last release was May 2024 and the TomatoPHP ecosystem uses loose Composer constraints; an unpinned composer update months later can pull an untested Filament major and silently break the admin resources.

composer require tomatophp/tomato-coupons:1.0.4
php artisan vendor:publish --tag=tomato-coupons-config
php artisan vendor:publish --tag=tomato-coupons-migrations
php artisan migrate

If you are inside a full TomatoPHP app, prefer the installer hook (php artisan tomatophp:install coupons) so the plugin, config, and migrations land together and the Filament panel provider registers the plugin for you.

Read these files before writing any integration code, in this order. Ninety minutes here saves a week of surprises:

  1. config/tomato-coupons.php — model bindings and table names. This is your sanctioned extension surface; swap classes here rather than editing vendor/.
  2. database/migrations/* — the actual coupon/gift-card schema. Look specifically for code, discount type/amount, usage counters, active_from/active_to, and whether there is a redemption table at all or only an integer column.
  3. src/Models/Coupon.php and GiftCard.php — casts, scopes, relationships. Confirm what "usage limit" means in code.
  4. src/Filament/ — the resources are the most honest documentation of which fields and validation rules the package actually enforces.
  5. The service provider — what gets bound into the container and how migrations are loaded.

First use case: admin-only coupon CRUD, before any checkout wiring. Open the Filament panel, create one fixed-amount and one percentage coupon, then confirm the round-trip from tinker:

$coupon = \TomatoPHP\Coupons\Models\Coupon::create([
    'code'      => 'LAUNCH10',
    'type'      => 'percentage',
    'amount'    => 10,
    'is_active' => true,
]);

\TomatoPHP\Coupons\Models\Coupon::where('code', 'LAUNCH10')->first()?->toArray();

If that persists cleanly, the package is doing the one thing it reliably does. Everything from here is your code — plan accordingly.

Implementation Patterns

Treat the package as a back-office CRUD layer, not a pricing engine. It stores coupon definitions and gives you Filament screens. It does not give you applyCouponToCart(), stacking rules, minimum-spend evaluation, or tax handling. Put it behind your own interface so a fork, replacement, or upstream breakage touches exactly one class:

namespace App\Discounts;

interface CouponValidator
{
    public function validate(string $code, Cart $cart): ValidatedCoupon;
}

EloquentCouponValidator wraps the vendor model. Checkout depends on the interface, never on the vendor model or the Filament resource class.

Own the redemption record. Create your own coupon_redemptions table with a unique index on (coupon_id, customer_id, order_id) and write inside the order transaction. The package's counter column is a denormalized convenience; when it disagrees with the ledger, trust the ledger.

DB::transaction(function () use ($couponId, $order) {
    $coupon = Coupon::whereKey($couponId)->lockForUpdate()->firstOrFail();

    abort_unless($coupon->is_active, 422, 'Coupon inactive');
    abort_unless(
        now()->betweenIncluded(
            $coupon->active_from ?? now()->subYear(),
            $coupon->active_to ?? now()->addYear()
        ),
        422, 'Coupon expired'
    );

    $order->update(['coupon_id' => $coupon->id]);
    CouponRedemption::create([
        'coupon_id'   => $coupon->id,
        'customer_id' => $order->user_id,
        'order_id'    => $order->id,
    ]);
    $coupon->increment('uses');
});

The unique index is the real enforcement of per-user limits; lockForUpdate() plus the counter is the convenience layer.

Gift card balances need an append-only ledger. If you use GiftCard at all, treat balance as a cache and write every debit/credit to a gift_card_transactions table. Debit with a conditional update and assert the affected row count:

$affected = DB::table('gift_cards')
    ->where('id', $giftCardId)
    ->where('balance', '>=', $amount)
    ->update(['balance' => DB::raw('balance - ' . (int) $amount)]);

abort_if($affected !== 1, 422, 'Insufficient gift card balance');

Put reusable eligibility into a scope, not into your controller. If you find yourself repeating is_active + date-window checks, wrap them once (Coupon::currentlyValid()) and call it from the validator and from the admin list. That keeps the two views from drifting.

Use the polymorphic relation for attachment, not raw joins. If the package exposes a morph relation to orders or users, keep application code on that relation. Relations survive column renames and forks; hand-written joins against tomato_coupons tables do not.

Keep checkout out of the Filament layer entirely. Resource classes only make sense inside a panel. Your API, Inertia front end, or queue worker should consume models through your validator. That is what leaves you a headless path later without a rewrite.

Add reporting indexes before volume arrives. At minimum: unique on code, a composite on (is_active, active_from, active_to) for the "currently valid" lookup, and timestamp indexes on redemptions. Redemption tables become a reporting hotspot faster than expected.

CI the resolved stack. A small matrix job installing against your target Laravel and Filament versions turns a Filament major from a production incident into a red build. Combined with the pin, this is your entire maintenance strategy.

Gotchas and Tips

There is no discount math and no eligibility engine. No stacking logic, no minimum-spend check, no per-customer limit enforcement at the database level, no tax handling. Budget two to three weeks of real work for production-grade coupons plus gift cards. The package saves you the schema and admin screens, not the semantics.

Concurrency is the sharpest edge. Two simultaneous checkouts redeeming the same single-use coupon — or two parallel gift card debits — will both succeed silently without lockForUpdate() or a conditional update. Write a parallel-process test against a real database before you enable redemption in production.

Timezone and expiry. Store and compare in UTC. Never evaluate active_from/active_to against a model cached earlier in the request, and never evaluate them client-side. Both reliably honor expired coupons.

Currency is likely unvalidated. Verify whether a gift card's currency is compared to the cart currency. Assume it is not, and check it in your own path.

Cascade deletes corrupt usage counts. A onDelete('cascade') from redemptions back to orders means deleting an order silently frees a redemption. Prefer restrict, or soft-delete and derive counters from the ledger.

Config class swaps are the first extension point. When you need a behavior fix, override the model binding in config/tomato-coupons.php before forking. Fork only when the fix lives inside a resource or a migration — and keep the fork on a single branch with an upstream remote so you can rebase or drop it.

Debugging checklist:

  • Filament resource not showing? Confirm the panel provider registers the plugin and that the coupon plugin class is in the enabled list.
  • Publish failing? The tag must match config('tomato-coupons.migrations_tag') exactly.
  • Coupon "not applying"? Log $coupon->toArray() and your validator's rejection reason separately. Vendor models emit no validation feedback, so a silent rejection almost always lives in your wrapper.

Authorization is yours. Review the Filament resources for policy usage. If none is wired, register policies yourself before this reaches a shared admin panel.

Support reality. Community-only, 2 stars, 0 dependents, no SLA. Write an internal runbook covering schema ownership, deviations from upstream, and the redemption invariants your tests enforce. That document is what protects you when a Filament major lands.

When to skip it. If you are not already on a TomatoPHP/Filament panel, the schema is easy to replicate and the value is thin — you can write the coupon and gift-card tables in a day. This package earns its place only when the Filament admin is already your world; otherwise build in-house and own the money math from day one.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console