Order History

Customers with (and without!) accounts often want to see evidence of their purchases, on-site.

# Post-Checkout

After a customer has paid for or otherwise completed an order, Commerce redirects them to the returnUrl memoized on the order. This is often set with the standard redirectInput() function when making a payment, but may not take effect until the customer returns after completing off-site authentication.

To send them to a dedicated order summary page, you can send a redirect param like this:

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/payments/pay') }}
  {{ redirectInput('orders/{number}') }}

  {# ... #}

This object template is evaluated using the order object itself—in this example, we’re using the number attribute, which is guaranteed to be unique. Commerce (nor Craft) doesn’t know how to handle a request to that URL, though, so it’s up to us to define a route.

# Routing

Routes are a native Craft feature, and can be configured via the control panel via

  1. Settings
  2. Routes
or a config/routes.php file.

Commerce doesn’t automatically give Orders front-end URLs (like entries or categories), so it’s up to each project to define an access pattern that makes sense.

# Guest Orders

To support the post-checkout redirect we set up, let’s add a URL rule that points to a template. Open up (or create) config/routes.php and add a new key to the array:

return [
    // ...
    'orders/<orderNumber:[a-f0-9]{32}>' => ['template' => '_orders/receipt'],

Our object template included the order’s number, which is always 32 “hexadecimal” characters long (letters a through f and numbers 0 through 9). This route matches that pattern, and passes the captured value to our template under a variable named orderNumber.

Keep in mind that your order number is different than its reference. Commerce does not allow customization of the number, as it must be random and globally unique. However, many stores do incorporate the order number (or part of it) in their reference format.

Create the _orders/receipt template in your templates directory, and add a simple output statement to prove that things are connected:

{{ orderNumber }}

Find a valid order number in the control panel, and try loading the URL (`https://my-shop.ddev.site/orders/)

To load an order with that number, we’ll use an order query. We only want to show completed orders, so we’ll set an additional parameters on the query:


{% set order = craft.orders()
  .one() %}

{% if not order %}
  {% exit 404 %}
{% endif %}

{{ order.dateOrdered|date }}

The substance of this “receipt” view is entirely up to you—orders contain a huge amount of data, including line items, adjustments, transactions, status history, notes, custom fields, and more.

If you elect to use a more relaxed pattern for your routes, take care to not inadvertently disclose personal information. Using predictable identifiers like sequential IDs (order.id) or reference numbers (order.reference) leaves your store open to enumeration attacks.

# Obfuscation

Customers will want to confirm some amount of information about their orders immediately after placing them—but it can be unsafe to leave personal information like emails, addresses, or even order contents out on the semi-public web, indefinitely.

Consider setting up a policy with your team about how long order data is accessible, and consider redacting sensitive information after a period of time:

{% set elapsed = order.dateOrdered.diff(now) %}

{% if elapsed.days < 60 %}
  {# Fresh orders can display everything! #}
{% else %}
  {# Output anonymous or redacted info... #}
{% endif %}

# Credentialed Customers

If your store supports registration, routing and order lookup might look a bit different. It also means that we can display an index of past orders so the customer doesn’t have to look up individual receipt URLs.

Here’s an example of some routes that comprise a lightweight “account” area:

return [
    'account' => ['template' => '_account/dashboard'],
    'account/orders' => ['template' => '_account/order-history'],
    'account/orders/<orderNumber:[a-f0-9]{32}>' => ['template' => '_account/order'],

We won’t implement the root /account route, but let’s assume it contains some links to other functions like updating addresses and security settings.

The last route should look familiar—it’s essentially the same as the guest customer’s version, but nested within the /account path. The location here doesn’t make much of a difference, but its template will contain a couple more access checks.

# Protecting Access

All the templates in this account area should contain the {% requireLogin %} tag, and narrow the scope of any order queries using the currentUser global variable.

# Listing Orders

Create a new folder in your templates/ directory named _account, and a file within it named order-history.twig.

Orders are always connected to a Craft user. To fetch the current user’s orders, we’ll use the .customer() query param:

{% requireLogin %}

{% set orders = craft.orders()
  .orderBy('dateOrdered DESC')
  .all() %}

  {% for order in orders %}
    <li><a href="{{ siteUrl("account/orders/#{order.number}") }}">Order {{ order.reference }}</a> ({{ order.total|commerceCurrency }})</li>
  {% endfor %}

For multi-store installations, you can display orders for a single store at a time with the .storeId() query param. If a customer’s orders are apt to contain multiple currencies, pass the appropriate currency to the commerceCurrency filter.

# Single Orders

The links in our order list point to single orders, with a parameterized path like /account/orders/cc93d4b51f20299672d936cbafce9f4d. Let’s mock out this template—it will look a lot like the guest template, but with a couple of additional constraints:

{% requireLogin %}

{% set order = craft.orders()
  .one() %}

  <h2>Order {{ order.reference }}</h2>

  {# ... #}

# Order Data

# Line Items

Line items can be handled exactly the same way as they are in a cart—minus the editing components!

      <th>Unit Price</th>
    {% for item in order.lineItems %}
        <td>{{ item.qty }}</td>
        <td>{{ item.description }}</td>
        <td>{{ item.price }}</td>
        <td>{{ item.subtotal }}</td>
    {% endfor %}

# Adjustments

Adjustments like tax and shipping (and others, provided by plugins) can apply to line items, or directly to the order. Commerce makes these available as an array under the adjustments attribute of line items and orders. For more information about how these affect totals and subtotals, see the orders documentation.

👷 Stay tuned! We’re consolidating examples from around the Commerce docs here, for easier reference.