NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Shops for Laravel

The Shops facade

RoundlyConsulting\Shops\Facades\Shops is the whole API in one place. Each area is a scoped handle — one cart, one order, one variant’s stock — or a sub-accessor. The package also registers it as the global Shops alias:

use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Shops\Facades\Shops;
use RoundlyConsulting\Shops\Inventory\Enums\StockReason;
use RoundlyConsulting\Shops\Orders\DataTransferObjects\PlaceOrderData;
use RoundlyConsulting\Shops\Orders\Enums\Status;

// Cart — lines of another cart are refused (ForeignItemException)
Shops::cart($cart)->add($variant, 2);            // CartItem; the same variant merges into its line
Shops::cart($cart)->update($item, 3);            // ?CartItem — 0 removes the line
Shops::cart($cart)->remove($item);
Shops::cart($cart)->clear();                     // the "empty cart" button
Shops::cart($cart)->price('SUMMER');             // Price DTO, optionally with a coupon code
Shops::cart($cart)->subtotal();                  // Money
$order = Shops::cart($cart)->checkout(new PlaceOrderData(couponCode: 'SUMMER'));

// Order
Shops::order($order)->transition(Status::InProgress);
Shops::order($order)->charge();                  // PaymentResult — Paid on success; New/InProgress only
Shops::order($order)->fulfil();                  // reservation becomes a sale
Shops::order($order)->cancel();                  // reservation released, store credit returned
Shops::order($order)->refund();                  // an unfulfilled order's reservation released too
Shops::order($order)->quoteShipping($address);   // Money, through the bound ShippingMethod

// Inventory — every change is a StockAdjustment ledger row
Shops::inventory($variant)->receive(10, ref: $purchaseOrder, note: 'PO-17');
Shops::inventory($variant)->returned(1, $order); // the variant must be on that order
Shops::inventory($variant)->adjust(-2, StockReason::Manual, note: 'Damaged');
Shops::inventory($variant)->available();         // stock - reserved

// Coupons, tenancy, addresses
Shops::coupons()->preview('WELCOME10', Money::ofMinor(1000, 'EUR')); // DiscountResult, no redemption
Shops::current()->set($shop);                    // CurrentShop: set / get / id / forget / run
Shops::addresses()->defaults($customer);         // OrderAddresses (billing, shipping)

Entry points

CallReturnsWhat it is
Shops::cart(Cart $cart)Handles\CartHandleOne cart: lines, price, checkout.
Shops::order(Order $order)Handles\OrderHandleOne order: transitions, charge, shipping quote.
Shops::inventory(ProductVariant $variant)Handles\InventoryHandleOne variant’s stock.
Shops::coupons()Handles\CouponsHandleCoupon previews, no redemption.
Shops::current()Shops\CurrentShopThe bound tenant (see Shops & tenancy).
Shops::addresses()Orders\AddressBookA customer’s default order addresses.
Shops::fake()Testing\ShopsFakeThe recording manager (see Testing).

Handle methods

MethodReturnsDoes
cart($cart)->add(ProductVariant $variant, int $quantity = 1)CartItemSnapshots the variant; the same variant merges into its line.
cart($cart)->update(CartItem $item, int $quantity)?CartItemSets a line’s quantity; 0 removes it and returns null.
cart($cart)->remove(CartItem $item)voidRemoves a line.
cart($cart)->clear()CartRemoves every line; the cart stays.
cart($cart)->price(?string $couponCode = null)PricePrices the cart, with the stored or given coupon code. Never redeems.
cart($cart)->subtotal()MoneyGoods subtotal before any discount.
cart($cart)->checkout(PlaceOrderData $data = new PlaceOrderData)OrderPlaces the order in one transaction, holding the cart’s row lock — a double submit places one order.
order($order)->transition(Status $to)OrderAny allowed status move.
order($order)->cancel() / refund() / fulfil()OrderShortcuts for Canceled (store credit returned), Refunded and Fulfilled.
order($order)->charge()PaymentResultNew or InProgress orders only, decided under the order’s row lock. Store credit first when enabled, the rest through the gateway; Paid on success.
order($order)->quoteShipping(Address $to)MoneyQuote through the bound ShippingMethod.
inventory($variant)->receive(int $quantity, ?Model $ref = null, ?string $note = null)StockAdjustmentA delivery: on-hand stock goes up.
inventory($variant)->returned(int $quantity, ?Model $ref = null, ?string $note = null)StockAdjustmentA customer return; with an Order ref the variant must be on it.
inventory($variant)->adjust(int $delta, StockReason $reason = StockReason::Manual, ?Model $ref = null, ?string $note = null)StockAdjustmentAny signed adjustment; a manual correction by default.
inventory($variant)->available()intStock minus reserved.
inventory($variant)->inStock(int $quantity = 1)boolWhether the quantity can be sold now.
coupons()->preview(string $code, Money $goods)DiscountResultWhat a code would take off, via the bound DiscountResolver.
addresses()->defaults(Addressable $customer, ?bool $billingSameAsShipping = null)OrderAddressesPrimary billing and shipping addresses as order snapshots.
addresses()->map(?AddressModel $address)?AddressOne saved address as an order Address.
  • A line of another cart is refused with ForeignItemException before anything is written — scope carts to the current customer and let the handle guard the line ids that arrive in a request.
  • returned() with an Order reference refuses a variant that was never on that order, also with ForeignItemException.
  • Illegal status moves throw IllegalStatusTransitionException and change nothing; an oversell at checkout throws InsufficientStockException, and an empty cart or a line whose variant was removed throws CheckoutRefusedException — both write nothing.
  • Shops::current() methods (set, get, id, forget, run) are listed under Shops & tenancy.

Model shorthand

The model convenience methods go through the same manager, so they behave identically and Shops::fake() sees them:

use RoundlyConsulting\Shops\Orders\Enums\Status;
use RoundlyConsulting\Shops\Shops\Shop;

$cart->add($variant, 2);            // Shops::cart($cart)->add($variant, 2)
$order->transitionTo(Status::Paid); // Shops::order($order)->transition(Status::Paid)
$order->markInProgress();           // → InProgress
$order->markPaid();                 // → Paid
$order->markFulfilled();            // → Fulfilled
$order->cancel();                   // Shops::order($order)->cancel()
$order->refund();                   // Shops::order($order)->refund()
Shop::current();                    // Shops::current()->get(), typed to the package's Shop model

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.