---
title: "Placing orders — Shops for Laravel | Roundly"
description: "Turn a cart into an order in one transaction — lines, stock, coupon, shipping, addresses and buyer — and fill addresses from the address book."
url: https://roundly-consulting.com/open-source/docs/shops-for-laravel/placing-orders
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Shops for Laravel](https://roundly-consulting.com/open-source/docs/shops-for-laravel.md)

# Placing orders

Shops::cart($cart)->checkout() turns a cart into an order in one transaction (PlaceOrderAction). An oversell rolls everything back and leaves the cart untouched:

```php
use RoundlyConsulting\Shops\Facades\Shops;
use RoundlyConsulting\Shops\Orders\DataTransferObjects\Address;
use RoundlyConsulting\Shops\Orders\DataTransferObjects\PlaceOrderData;

$address = new Address(
    name: 'Jane Doe',
    street: 'Main Street 1',
    city: 'Berlin',
    postalCode: '10115',
    countryIso: 'DE',
    email: 'jane@example.com',
);

$order = Shops::cart($cart)->checkout(new PlaceOrderData(
    billing: $address,
    shipping: $address,          // the shipping country drives per-country tax
    couponCode: 'WELCOME10',     // optional — defaults to the cart's coupon_code
    note: 'Leave at the door',   // optional
    customer: $user,             // optional — defaults to the cart owner
));

$order = Shops::cart($cart)->checkout();   // no addresses, the cart's own coupon_code

// The same as an action:
$order = app(\RoundlyConsulting\Shops\Actions\Orders\PlaceOrderAction::class)->execute($cart, $data);
```

## What happens

- 1\. The cart row is locked for the whole checkout. An empty cart, or a line whose variant was deleted or soft-deleted since it was added, is refused with CheckoutRefusedException before anything is written.
- 2\. An Order is created with status New, the cart’s currency, the addresses and the note.
- 3\. The buyer is linked — the given customer, else the cart owner (guest orders have none) — and the cart’s shop\_id is copied; order items inherit the order’s shop.
- 4\. Every cart line is copied through the internal AddOrderItemAction at the name, sku, price and tax class the cart snapshotted — what the customer saw, even if the catalog was repriced since — plus the tax rate.
- 5\. Stock is reserved — an oversell throws InsufficientStockException and rolls everything back.
- 6\. The coupon (the given code, else the cart’s coupon\_code) is redeemed and its discount snapshotted. A missing or non-redeemable code is silently skipped and never blocks checkout.
- 7\. Shipping is quoted and snapshotted onto the order (see Shipping below).
- 8\. The cart is cleared, the order refreshed and returned; OrderPlaced fires after the transaction commits.

## Refused checkouts

Because the cart stays locked, a double-submitted “Place order” places one order: the second request waits, finds the cart emptied and is refused with Orders\\Exceptions\\CheckoutRefusedException — catch it and show the order the first request placed. The same exception refuses an empty cart and a line whose variant is gone, naming the line in $cartItem:

```php
use RoundlyConsulting\Shops\Facades\Shops;
use RoundlyConsulting\Shops\Orders\Exceptions\CheckoutRefusedException;

try {
    $order = Shops::cart($cart)->checkout($data);
} catch (CheckoutRefusedException $e) {
    $e->cartItem;   // ?CartItem — the line whose variant was removed; null for an empty cart
    // A double-submitted "Place order" lands here too: show the order the first request placed.
}
```

## Shipping

With a shipping address, checkout asks the bound ShippingMethod (shops.shipping.method) to quote it — after the lines are added, so the method can price them — and snapshots the result onto the order as shipping\_cost, in the order currency. Pass the option the customer chose with shippingCost instead. The order’s final price and the charge include it, and a free-shipping coupon takes it off. No address and no chosen cost means no shipping charge; a cart has no destination, so Shops::cart($cart)->price() never includes shipping:

```php
use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Shops\Facades\Shops;
use RoundlyConsulting\Shops\Orders\DataTransferObjects\PlaceOrderData;

// Quoted through shops.shipping.method and snapshotted onto the order:
$order = Shops::cart($cart)->checkout(new PlaceOrderData(shipping: $address));

// Or the option the customer chose:
$order = Shops::cart($cart)->checkout(new PlaceOrderData(
    shipping: $address,
    shippingCost: Money::ofMinor(490, 'EUR'),
));

$order->shipping_cost;           // ?Money, in the order currency
$order->price->shippingCost();   // the shipping charged — zero with a free-shipping coupon
```

## PlaceOrderData

Every field is optional:

```php
new PlaceOrderData(
    ?Address $billing = null,
    ?Address $shipping = null,
    ?string $couponCode = null,
    ?string $note = null,
    ?Model $customer = null,
    ?Money $shippingCost = null,   // the shipping option the customer chose — else quoted
);
```

## Orders built by hand

For an admin or phone order, go through a cart too — the line snapshot, the stock reservation and the coupon redemption then run together:

```php
use RoundlyConsulting\Shops\Cart\Cart;
use RoundlyConsulting\Shops\Facades\Shops;
use RoundlyConsulting\Shops\Orders\DataTransferObjects\PlaceOrderData;

// An admin or phone order: go through a cart so the snapshot,
// the reservation and the coupon redemption run together.
$cart = Cart::create(['currency' => 'EUR', 'shop_id' => $shop->id]);
Shops::cart($cart)->add($variant, 2);
$order = Shops::cart($cart)->checkout(new PlaceOrderData(customer: $customer));
```

## Addresses

Billing and shipping addresses are stored as JSON and cast to an immutable Address DTO with name, street, city, postalCode, countryIso and optional company, phone and email:

```php
use RoundlyConsulting\Shops\Orders\DataTransferObjects\Address;

$order->shipping_address;               // ?Address — an immutable DTO (JSON column)
$order->shipping_address?->countryIso;  // "DE"

$address = Address::fromArray([
    'name' => 'Jane Doe', 'street' => 'Main Street 1', 'city' => 'Berlin',
    'postal_code' => '10115', 'country_iso' => 'DE', 'phone' => '+49 30 000000',
]);
$address->toArray();                    // snake_case keys

// Reuse the cast on your own models:
protected function casts(): array
{
    return ['delivery_address' => \RoundlyConsulting\Shops\Support\Casts\AddressCast::class];
}
```

## From the customer’s address book

With addresses-for-laravel, build the order’s addresses from the customer’s saved ones. Shipping comes from the primary shipping address, billing from the primary billing address; when there is no billing address, shipping is reused (toggle with shops.addresses.billing\_same\_as\_shipping or the billingSameAsShipping argument). Shops::addresses() returns the AddressBook behind it. The customer model must implement Addressable:

```php
use Illuminate\Foundation\Auth\User as Authenticatable;
use RoundlyConsulting\Addresses\Contracts\Addressable;
use RoundlyConsulting\Addresses\Traits\HasAddresses;

class User extends Authenticatable implements Addressable
{
    use HasAddresses;
}

$order = Shops::cart($cart)->checkout(PlaceOrderData::fromAddressBook($user, couponCode: 'WELCOME10'));

// Or read the defaults yourself:
$defaults = Shops::addresses()->defaults($user);   // OrderAddresses
$defaults->shipping;                               // ?Address
$defaults->billing;                                // ?Address
```

Addresses are snapshotted onto the order — there is no foreign key into the address book, so editing a saved address never alters a past order.

## The order customer

An order carries an optional polymorphic customer. It powers the address book, store credit and verified-purchase reviews; it is nullable, so guest orders are fully supported:

```php
$order->customer;        // ?Model — the buyer, copied from the cart owner at place-order
$order->hasCustomer();   // false for guest orders
```

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/shops-for-laravel.md)
