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

Adjustments Laravel Package

vanilo/adjustments

Vanilo Adjustments provides a flexible adjustment system for Laravel e-commerce apps, letting you apply promotions, discounts, taxes, and other price modifications to orders and line items through a consistent, extensible API.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and wire up

    composer require vanilo/adjustments
    php artisan migrate
    

    Laravel's package discovery registers the service provider automatically. The migration creates an adjustments table (polymorphic adjustable, a nullable adjuster morph, type, name, amount, and a data JSON column). Run php artisan vendor:publish first if the package exposes migrations/config you need to customize.

  2. Read these files first — the module is small and mostly contracts:

    • src/Contracts/Adjustable.php — the interface your models must implement.
    • src/Contracts/Adjustment.php — the adjustment contract.
    • src/Traits/AdjustableTrait.php — what adjustments(), total(), and the sum helpers actually do.
    • src/Models/Adjustment.php — fillable fields and casts.
    • src/AdjustmentType.php (or equivalent) — the type constants (promotion, shipping, tax, fee, …).
    • database/migrations/*_create_adjustments_table.php — column types and indexes.
  3. Make a model adjustable

    use Vanilo\Adjustments\Contracts\Adjustable;
    use Vanilo\Adjustments\Traits\AdjustableTrait;
    
    class OrderItem extends Model implements Adjustable
    {
        use AdjustableTrait;
    }
    
  4. First use case — attach a discount to a line item and read the reconciled price:

    $item->adjustments()->create([
        'type'      => AdjustmentType::PROMOTION,
        'name'      => 'Spring sale 10%',
        'amount'    => -1990,          // minor units, negative = discount
        'adjuster_type' => Promotion::class,
        'adjuster_id'   => $promotion->id,
        'data'      => ['rule' => 'pct_10', 'base' => 19900],
    ]);
    
    $item->total();            // base price + all adjustments
    $item->adjustmentTotal();  // sum of adjustments only
    

Implementation Patterns

Persist adjustments, don't recompute silently. Adjustments are database rows, not virtual attributes. When a coupon is applied, insert a row; when the cart changes or the coupon is removed, delete and recreate. This gives you an audit trail and lets you explain "why is the total €42?" to a customer.

Use the polymorphic adjuster for provenance. Link each adjustment back to the entity that produced it (Promotion, Coupon, TaxRate, ShippingMethod). Then deleting a promotion can cascade or nullify its adjustments, and reporting can group by source without parsing name strings.

Signed amounts, one convention. Decide once: discounts negative, fees/taxes/shipping positive (or all positive with the type carrying the sign). Whatever you choose, enforce it in a single factory/service (AdjustmentService::discount(), ::fee(), ::tax()) rather than at every call site — mixed conventions are the #1 source of doubled or inverted totals.

Layer order-level and item-level adjustments. Compute line totals from item adjustments first, then apply order-level adjustments (shipping, order-wide coupons) on the subtotal. Do this in one calculator class so grandTotal() never drifts between the cart page, checkout, and the invoice.

Reconcile on cart mutations. A typical flow:

DB::transaction(function () use ($cart) {
    $cart->items->each->adjustments()->delete();
    $cart->adjustments()->delete();
    $this->promotionEngine->apply($cart); // creates fresh rows
});

Store the "why" in data. Percentages, matched rule IDs, original prices, and rounding remainders belong in the JSON column. It survives schema changes and makes support tickets tractable.

Test with direct row creation. You rarely need the full promotion engine to test totals — build adjustments in factories and assert on total() / adjustmentTotal().

Gotchas and Tips

  • Totals are read-time, not cached. If the underlying price changes after an adjustment is created, stale percent-based amounts won't self-correct. Store the computed amount (not just the percentage) or explicitly recalc on price change.
  • total() vs adjustmentTotal(). First is base + adjustments; second is the delta only. Mixing them up produces totals that look plausible but are off by the base price. Assert both in tests.
  • Money precision. Check the amount column type. Prefer integer minor units or decimal(15,4) with bcmath; floats will bite you on multi-item sums and tax rounding.
  • N+1 on order lists. adjustments() is a lazy morphMany. Eager-load with('adjustments') anywhere you display totals in a table, or you'll fire a query per row.
  • Orphaned adjustments. No automatic cleanup when the adjustable is soft-deleted. Add a deleting observer or a scheduled cleanup, and consider a FK/cascade on adjustable_id if your schema allows it.
  • Nullable adjuster. Manual adjustments (gift cards, corrections) legitimately have no adjuster — always null-check before calling $adjustment->adjuster->name.
  • data is cast to array. Mutating nested keys requires reassignment ($a->data = [...] then save()); in-place $a->data['x'] = 1 may not persist depending on the cast.
  • Migration ordering. The adjustments table references your models polymorphically but the migration may run before your own tables. Publish and rename the timestamp if your test suite uses RefreshDatabase against a fresh schema.
  • Model swapping. Vanilo/Concorde modules often let you bind a custom Adjustment implementation via concord()->registerModel(...). If you need extra columns or behavior, extend rather than editing vendor files — but verify the binding exists in this version's service provider.
  • Debugging tip. When a total looks wrong, dump $model->adjustments->groupBy('type')->map->sum('amount') — it isolates which adjustment category is misbehaving far faster than stepping through the promotion engine.
  • Pin the version. At 5 stars and 0 dependents this is a low-traffic subpackage; lock the minor version and skim the changelog on upgrade, since API churn here cascades into every pricing calculation in your app.
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