Highlights
Admin permission audit closes authorization gaps across 80 components
A full audit of all 80 admin Livewire components found that a user holding a coarser gate could trigger destructive or write operations without the specific fine-grained permission the operation requires. All gaps are now closed.
Patched paths: AddVariant was gated on add_products instead of add_product_variants; BrandForm, CategoryForm, and SupplierForm had no gate on mount() at all (the supplier form leaked PII on openPanel); brand table reorder was ungated; variant and attribute bulk-delete actions relied on the parent table gate rather than their own. Explicit ->authorize() calls are now present on every mutating Filament action and Livewire method.
IDOR closed on pricing and variant slide-overs
ManagePricing exposed a public $model property and UpdateVariant exposed public $variant and $product properties without #[Locked]. Any admin could swap the serialized Eloquent ID from the browser and overwrite pricing or variant data for a product the flow never opened. All three properties are now locked.
Per-user discount limit enforced on guest carts
Coupons with usage_limit_per_user were only checked when cart->customer_id was present, so a guest could apply a once-per-customer code indefinitely by checking out without an account. Both DiscountValidator and CreateOrderFromCartAction now reject these coupons for anonymous carts, returning the requires_login error.
Atomic stock reservation prevents oversell under concurrency
Stock was previously reserved by a queued ReserveOrderItemStockListener that fired after the order transaction committed with no row lock. Two concurrent checkouts reading the same last unit would both succeed and oversell.
Stock is now reserved synchronously inside CreateOrderFromCartAction under a lockForUpdate() row lock. If the available quantity falls short, InsufficientStockException is thrown and the entire order rolls back. A new StockReserver contract is introduced with LockingStockReserver as the default implementation, bound in CoreServiceProvider. Virtual and external products are skipped via a new tracksInventory() method on the Stockable contract.
Tax calculated on the discounted line total
Two independent bugs caused taxes to be applied to the pre-discount subtotal. DiscountCalculator persisted line adjustments with a raw insert but never reloaded the in-memory adjustments relation, so CalculateTax read an empty collection and taxed the full line amount. The tax adapters then divided the taxable total by quantity, applied the rate per unit, and multiplied back, overcharging by a cent on non-divisible totals. The TaxableItem contract now exposes a single getTaxableTotal(): int and the adapters tax the exact total in one pass.
Bug Fixes
fix(security): enforce explicit permissions on every Filament action and Livewire mutation by @mckenziearts in #573fix(security): enforce per-user discount limit on guest carts by @mckenziearts in #588fix(tax): tax the discounted line total exactly by @mckenziearts in #589fix(security): lock model properties on pricing and variant slide-overs by @mckenziearts in #590fix(stock): reserve inventory atomically inside the checkout transaction by @mckenziearts in #591
Performance
perf(admin): trim collection and attribute deck queries by @mckenziearts in #592
Upgrading
This release changes three contracts. Update custom implementations before upgrading.
1. TaxableItem implementers replace two methods with one:
// Remove getTaxableAmount() and getQuantity(), add:
public function getTaxableTotal(): int
{
return $this->amount * $this->quantity;
}2. Custom Stockable / Product / ProductVariant models add:
public function tracksInventory(): bool
{
return $this->isStandard() || $this->isVariant();
}3. ReserveOrderItemStockListener is removed. Drop it from your EventServiceProvider. Stock reservation now runs inline during checkout. For a custom strategy, bind the new contract:
$this->app->bind(StockReserver::class, CustomStockReserver::class);4. CreateOrderFromCartAction gained a StockReserver constructor dependency. Resolve it from the container, never instantiate with new.
Contributors
Full Changelog: v2.9.2...v2.10.0