Skip to main content

Overview

The order history page allows authenticated customers to view their past orders, check order statuses, and review the details of individual orders. This guide walks through resolving the current customer, querying their orders, displaying a paginated order list, and building an order detail page. The examples below use standard Laravel controllers and Blade templates. The same concepts apply whether the storefront is built with Livewire, Inertia, or a headless API.

Resolving the Current Customer

Lunar separates the concept of a Customer from Laravel’s User model. An authenticated user can be linked to one or more customers through the LunarUser trait. The StorefrontSession facade provides the current customer for the session.
use Lunar\Facades\StorefrontSession;

$customer = StorefrontSession::getCustomer();
If no customer is associated with the current session, getCustomer() returns null. Protect account pages with middleware that checks for an authenticated user and a valid customer:
use Lunar\Facades\StorefrontSession;

class AccountController extends Controller
{
    public function orders()
    {
        $customer = StorefrontSession::getCustomer();

        if (! $customer) {
            abort(404);
        }

        // ...
    }
}
The StorefrontSession automatically resolves the customer from the authenticated user when the LunarUser trait is applied to the User model. See the Storefront Session reference for details on how customer resolution works.

Listing Orders

Query the customer’s orders using the orders relationship. Only orders with a placed_at value should be shown, as draft orders (where placed_at is null) have not been completed.
use Lunar\Facades\StorefrontSession;

class AccountController extends Controller
{
    public function orders()
    {
        $customer = StorefrontSession::getCustomer();

        if (! $customer) {
            abort(404);
        }

        $orders = $customer->orders()
            ->whereNotNull('placed_at')
            ->orderBy('placed_at', 'desc')
            ->paginate(10);

        return view('account.orders', compact('orders'));
    }
}

Displaying the Order List

Each order has its financial totals cast to Lunar\DataTypes\Price objects, so formatted() can be called directly.
<h1>Order History</h1>

@if($orders->isEmpty())
    <p>No orders have been placed yet.</p>
@else
    <table>
        <thead>
            <tr>
                <th>Order</th>
                <th>Date</th>
                <th>Status</th>
                <th>Total</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach($orders as $order)
                <tr>
                    <td>{{ $order->reference }}</td>
                    <td>{{ $order->placed_at->format('M d, Y') }}</td>
                    <td>{{ $order->status }}</td>
                    <td>{{ $order->total->formatted() }}</td>
                    <td>
                        <a href="{{ route('account.orders.show', $order) }}">
                            View
                        </a>
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>

    {{ $orders->links() }}
@endif

Order Financial Properties

After retrieval, each Lunar\Models\Order has the following price properties:
PropertyTypeDescription
sub_totalLunar\DataTypes\PriceSubtotal before discounts and tax
discount_totalLunar\DataTypes\PriceTotal discounts applied
shipping_totalLunar\DataTypes\PriceShipping cost including tax
tax_totalLunar\DataTypes\PriceTotal tax amount
totalLunar\DataTypes\PriceFinal order total
All values are Lunar\DataTypes\Price objects with access to value (integer in minor units), decimal() (float), and formatted() (currency string).

Order Detail Page

The order detail page shows the full breakdown of a specific order, including line items, addresses, and totals.
use Lunar\Facades\StorefrontSession;
use Lunar\Models\Order;

class AccountController extends Controller
{
    public function showOrder(Order $order)
    {
        $customer = StorefrontSession::getCustomer();

        if (! $customer || $order->customer_id !== $customer->id) {
            abort(404);
        }

        if ($order->isDraft()) {
            abort(404);
        }

        $order->load([
            'productLines.purchasable.product',
            'shippingAddress.country',
            'billingAddress.country',
            'shippingLines',
        ]);

        return view('account.orders.show', compact('order'));
    }
}
Always verify that the order belongs to the current customer. Without this check, a customer could view another customer’s order by guessing the URL.

Displaying Order Lines

Order lines hold a snapshot of each purchased item. The description field contains the product name at the time of purchase, and option holds any variant options.
<h1>Order {{ $order->reference }}</h1>
<p>Placed on {{ $order->placed_at->format('M d, Y \a\t g:i A') }}</p>
<p>Status: {{ $order->status }}</p>

<h2>Items</h2>
<table>
    <thead>
        <tr>
            <th>Product</th>
            <th>Price</th>
            <th>Quantity</th>
            <th>Total</th>
        </tr>
    </thead>
    <tbody>
        @foreach($order->productLines as $line)
            <tr>
                <td>
                    <p>{{ $line->description }}</p>
                    @if($line->option)
                        <p>{{ $line->option }}</p>
                    @endif
                    <p>{{ $line->identifier }}</p>
                </td>
                <td>{{ $line->unit_price->formatted() }}</td>
                <td>{{ $line->quantity }}</td>
                <td>{{ $line->total->formatted() }}</td>
            </tr>
        @endforeach
    </tbody>
</table>

Displaying Addresses

Each order stores a billing address and (for shippable orders) a shipping address. These are snapshots taken at the time of order creation and are separate from the customer’s saved addresses.
<div>
    <h2>Shipping Address</h2>
    @if($order->shippingAddress)
        <p>{{ $order->shippingAddress->first_name }} {{ $order->shippingAddress->last_name }}</p>
        @if($order->shippingAddress->company_name)
            <p>{{ $order->shippingAddress->company_name }}</p>
        @endif
        <p>{{ $order->shippingAddress->line_one }}</p>
        @if($order->shippingAddress->line_two)
            <p>{{ $order->shippingAddress->line_two }}</p>
        @endif
        <p>{{ $order->shippingAddress->city }}, {{ $order->shippingAddress->state }} {{ $order->shippingAddress->postcode }}</p>
        <p>{{ $order->shippingAddress->country?->name }}</p>
    @endif
</div>

<div>
    <h2>Billing Address</h2>
    @if($order->billingAddress)
        <p>{{ $order->billingAddress->first_name }} {{ $order->billingAddress->last_name }}</p>
        @if($order->billingAddress->company_name)
            <p>{{ $order->billingAddress->company_name }}</p>
        @endif
        <p>{{ $order->billingAddress->line_one }}</p>
        @if($order->billingAddress->line_two)
            <p>{{ $order->billingAddress->line_two }}</p>
        @endif
        <p>{{ $order->billingAddress->city }}, {{ $order->billingAddress->state }} {{ $order->billingAddress->postcode }}</p>
        <p>{{ $order->billingAddress->country?->name }}</p>
    @endif
</div>

Displaying Order Totals

<h2>Order Summary</h2>
<dl>
    <dt>Subtotal</dt>
    <dd>{{ $order->sub_total->formatted() }}</dd>

    @if($order->discount_total->value > 0)
        <dt>Discount</dt>
        <dd>-{{ $order->discount_total->formatted() }}</dd>
    @endif

    @if($order->shippingLines->isNotEmpty())
        <dt>Shipping</dt>
        <dd>{{ $order->shipping_total->formatted() }}</dd>
    @endif

    <dt>Tax</dt>
    <dd>{{ $order->tax_total->formatted() }}</dd>

    <dt>Total</dt>
    <dd>{{ $order->total->formatted() }}</dd>
</dl>

Tax Breakdown

To display a detailed tax breakdown:
@foreach($order->tax_breakdown->amounts as $tax)
    <p>{{ $tax->description }} ({{ $tax->percentage }}%): {{ $tax->price->formatted() }}</p>
@endforeach

Eager Loading for Performance

When displaying an order list, eager load the relationships needed for the list view to avoid N+1 queries:
$orders = $customer->orders()
    ->whereNotNull('placed_at')
    ->orderBy('placed_at', 'desc')
    ->with('currency')
    ->paginate(10);
For the detail page, load everything needed in a single query:
$order->load([
    'productLines.purchasable.product',
    'shippingAddress.country',
    'billingAddress.country',
    'shippingLines',
]);

Routes

use App\Http\Controllers\AccountController;

Route::middleware('auth')->group(function () {
    Route::get('/account/orders', [AccountController::class, 'orders'])->name('account.orders');
    Route::get('/account/orders/{order}', [AccountController::class, 'showOrder'])->name('account.orders.show');
});

Putting It All Together

Here is a complete controller for the order history pages:
<?php

namespace App\Http\Controllers;

use Lunar\Facades\StorefrontSession;
use Lunar\Models\Order;

class AccountController extends Controller
{
    public function orders()
    {
        $customer = StorefrontSession::getCustomer();

        if (! $customer) {
            abort(404);
        }

        $orders = $customer->orders()
            ->whereNotNull('placed_at')
            ->orderBy('placed_at', 'desc')
            ->paginate(10);

        return view('account.orders', compact('orders'));
    }

    public function showOrder(Order $order)
    {
        $customer = StorefrontSession::getCustomer();

        if (! $customer || $order->customer_id !== $customer->id) {
            abort(404);
        }

        if ($order->isDraft()) {
            abort(404);
        }

        $order->load([
            'productLines.purchasable.product',
            'shippingAddress.country',
            'billingAddress.country',
            'shippingLines',
        ]);

        return view('account.orders.show', compact('order'));
    }
}

Next Steps