github shopperlabs/shopper v3.0.0-beta.2

pre-release5 hours ago

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": true
composer require shopper/framework:^3.0.0-beta

Highlights

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] Stockable contract gains tracksInventory(), and ReserveOrderItemStockListener is removed. A custom Stockable must implement the method; stock is reserved during checkout, not on the OrderItemCreated event. (#581)
  • [BREAKING] Order contract gains canTransitionTo() and transitionTo(), and invalid status transitions now throw InvalidOrderStatusTransitionException. A custom Order model can use the HasOrderStatusTransitions trait. (#583)
  • [BREAKING] TaxableItem contract replaces getTaxableAmount() and getQuantity() with getTaxableTotal(). A custom implementation must return the line total. (#587)
  • [BREAKING] Cart::$payment_session is guarded from mass assignment and is written only through CartManager::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

Don't miss a new shopper release

NewReleases is sending notifications on new releases.