Caution
This is a beta release of the framework. Breaking changes may be introduced to v3 releases during the beta period.
This second 3.0 beta is a hardening pass over the checkout and order lifecycle. It makes stock reservation race-safe, turns provider webhooks into real order reconciliation, guards order status with a state machine, closes a set of admin and storefront security gaps, and fixes a tax rounding drift. The official 3.0 release is coming this September.
Installation
"minimum-stability": "beta",
"prefer-stable": truecomposer require shopper/framework:^3.0.0-betaHighlights
Race-safe stock reservation
Stock is now reserved inside the checkout transaction under a row lock on the stockable, instead of on a queued listener after the order committed. Two concurrent checkouts can no longer both claim the last unit, and a shortfall aborts the whole order. Virtual and external products are never reserved.
Payment webhook ingestion
Provider webhooks are now consumed end to end. A new POST /store/webhooks/{driver} endpoint verifies the signature, deduplicates the event by id, and reconciles the order: captured to paid, refunded to refunded, a failed or cancelled payment cancels the order and restores the reserved stock. Previously a paid order could stay pending forever.
Order status state machine
Order status changes go through a guarded transition map, so an invalid move such as cancelled to completed is rejected at the domain level rather than only in the admin UI.
Security hardening
Closes an admin IDOR on the variant and pricing slide-overs, a guest coupon reuse bypass on once-per-customer codes, and a mass-assignment hole on the cart payment session.
New Features
- feat(payment): ingest provider webhooks and reconcile order status by @mckenziearts in #582
- feat(orders): guard order status changes with a state machine by @mckenziearts in #583
Improvements
- refactor(orders): auto-fill the order number on create by @mckenziearts in #586
- perf(admin): pluck collection product ids on the query and fold attribute deck queries by @mckenziearts in #585
Bug Fixes
- fix(stock): reserve inventory atomically inside the checkout transaction by @mckenziearts in #581
- fix(security): close admin IDOR, guest coupon bypass, and payment_session injection by @mckenziearts in #584
- fix(admin): stop the product header clipping the delete confirmation modal by @mckenziearts in #585
- fix(tax): tax the exact line total and rate-limit cart creation by @mckenziearts in #587
- fix(sdk): independent beta version and working types publish by @mckenziearts in #580
Breaking Changes
- [BREAKING]
Stockablecontract gainstracksInventory(), andReserveOrderItemStockListeneris removed. A customStockablemust implement the method; stock is reserved during checkout, not on theOrderItemCreatedevent. (#581) - [BREAKING]
Ordercontract gainscanTransitionTo()andtransitionTo(), and invalid status transitions now throwInvalidOrderStatusTransitionException. A customOrdermodel can use theHasOrderStatusTransitionstrait. (#583) - [BREAKING]
TaxableItemcontract replacesgetTaxableAmount()andgetQuantity()withgetTaxableTotal(). A custom implementation must return the line total. (#587) - [BREAKING]
Cart::$payment_sessionis guarded from mass assignment and is written only throughCartManager::setPaymentSession(). (#584)
Upgrading
These breaks only affect projects that implement Shopper contracts directly or rely on the removed stock listener. Projects using the default models inherit the new behavior automatically.
Custom Stockable
public function tracksInventory(): bool
{
return $this->isStandard() || $this->isVariant();
}Custom Order
use Shopper\Core\Traits\HasOrderStatusTransitions;
class Order extends Model implements OrderContract
{
use HasOrderStatusTransitions;
}Custom TaxableItem
public function getTaxableTotal(): int
{
return $this->unitPrice * $this->quantity;
}Writing a cart payment session
resolve(CartManager::class)->setPaymentSession($cart, $session);Contributors
Full Changelog: v3.0.0-beta.1...v3.0.0-beta.2