> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lunarphp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Orders

> Order creation, lifecycle, lines, transactions, cancellation, and refunds in Lunar.

An order is a purchase created from a cart, tracked through payment and fulfilment until it is archived.

## Overview

An order is created from a [cart](/2.x/reference/carts) at checkout. It snapshots the cart's lines, addresses, and totals so the record stays accurate even if products, prices, or addresses change later. An order's lifecycle is read from two independently derived rollups, `payment_status` and `fulfilment_status`, plus an open/closed archive flag — there is no single headline "order status" column.

<Info>
  All monetary values (such as `sub_total`, `total`, `tax_total`) are stored as integers in the currency's minor unit and cast through `Lunar\Core\Models\Concerns\FormatsPrices`, which exposes formatted/decimal accessors.
</Info>

Fulfilment (shipping, collection, digital provisioning, tracking, carriers) is covered in full on the [Fulfilments](/2.x/reference/fulfilments) page. Sending emails to the customer is covered on the [Notifications](/2.x/reference/notifications) page. Capturing and refunding money through a payment provider is covered on the [Payments](/2.x/reference/payments) page — this page documents the order-side records those operations write to.

## Order model

```php theme={null}
Lunar\Core\Models\Order
```

### Fields

| Field | Type | Description |
| :- | :- | :- |
| id | `bigIncrements` | Primary key |
| public\_id | `ulid` | Stable external identifier for APIs and integrations |
| user\_id | `foreignId` `nullable` | The authenticated user who placed the order |
| channel\_id | `foreignId` | The channel the order was placed through |
| payment\_status | `string` | Derived rollup of the transaction ledger, see [Payment status](#payment-status) |
| fulfilment\_status | `string` | Derived rollup of the order's fulfilments, see [Fulfilment status](#fulfilment-status) |
| reference | `string` `nullable` | The generated order reference |
| customer\_reference | `string` `nullable` | A reference provided by the customer |
| sub\_total | `unsignedBigInteger` | The subtotal minus any discounts, excluding tax |
| discount\_total | `unsignedBigInteger` | The discount amount, excluding tax |
| shipping\_total | `unsignedBigInteger` | The shipping total including tax |
| tax\_breakdown | `jsonb` | Breakdown of applied taxes |
| tax\_total | `unsignedBigInteger` | The total amount of tax applied |
| total | `unsignedBigInteger` | The grand total including tax |
| notes | `text` `nullable` | Additional order notes |
| currency\_code | `string` | The currency code the order was placed in |
| compare\_currency\_code | `string` `nullable` | The default currency code at the time of the order |
| exchange\_rate | `decimal` | The exchange rate between `currency_code` and `compare_currency_code` |
| placed\_at | `dateTime` `nullable` | The datetime the order was considered placed. Null means the order is still a draft |
| closed\_at | `dateTime` `nullable` | The datetime the order was archived, see [Open and closed orders](#open-and-closed-orders) |
| cancelled\_at | `dateTime` `nullable` | The datetime the order was cancelled, see [Cancelling an order](#cancelling-an-order) |
| cancel\_reason | `string` `nullable` | The cancellation reason key |
| cancel\_note | `text` `nullable` | A staff note recorded with the cancellation |
| meta | `jsonb` `nullable` | Custom metadata |
| created\_at | `timestamp` | |
| updated\_at | `timestamp` | |
| customer\_id | `foreignId` `nullable` | |
| new\_customer | `boolean` | Whether the customer is a first-time buyer |
| discount\_breakdown | `jsonb` `nullable` | Breakdown of applied discounts |
| shipping\_breakdown | `jsonb` `nullable` | Breakdown of shipping charges |
| cart\_id | `unsignedBigInteger` `nullable` | The cart used to create the order |
| fingerprint | `string` `nullable` | A hash used to detect cart changes since the order was created |
| region\_id | `foreignId` `nullable` | The region resolved for the order |

### Relationships

| Relationship | Type | Related Model | Description |
| :- | :- | :- | :- |
| `channel` | BelongsTo | `Lunar\Core\Models\Channel` | |
| `region` | BelongsTo | `Lunar\Core\Models\Region` | |
| `cart` | BelongsTo | `Lunar\Core\Models\Cart` | |
| `lines` | HasMany | `Lunar\Core\Models\OrderLine` | Ordered by `id` |
| `fulfilments` | HasMany | `Lunar\Core\Models\Fulfilment` | See [Fulfilments](/2.x/reference/fulfilments) |
| `physicalLines` | HasMany | `Lunar\Core\Models\OrderLine` | Lines where `type` is `physical` |
| `fulfillableLines` | HasMany | `Lunar\Core\Models\OrderLine` | Lines where `requires_fulfilment` is `true` — the set fulfilments are created against |
| `digitalLines` | HasMany | `Lunar\Core\Models\OrderLine` | Lines where `type` is `digital` |
| `shippingLines` | HasMany | `Lunar\Core\Models\OrderLine` | Lines where `type` is `shipping` |
| `productLines` | HasMany | `Lunar\Core\Models\OrderLine` | All lines excluding shipping |
| `currency` | BelongsTo | `Lunar\Core\Models\Currency` | Matched on `currency_code` |
| `addresses` | HasMany | `Lunar\Core\Models\OrderAddress` | |
| `shippingAddress` | HasOne | `Lunar\Core\Models\OrderAddress` | Address where `type` is `shipping` |
| `billingAddress` | HasOne | `Lunar\Core\Models\OrderAddress` | Address where `type` is `billing` |
| `transactions` | HasMany | `Lunar\Core\Models\Transaction` | Ordered by `created_at` descending |
| `captures` | HasMany | `Lunar\Core\Models\Transaction` | Transactions where `type` is `capture` |
| `intents` | HasMany | `Lunar\Core\Models\Transaction` | Transactions where `type` is `intent` |
| `refunds` | HasMany | `Lunar\Core\Models\Transaction` | Transactions where `type` is `refund` |
| `customer` | BelongsTo | `Lunar\Core\Models\Customer` | |
| `user` | BelongsTo | User | The authenticatable model from the auth config |

### Scopes

| Scope | Description |
| :- | :- |
| `open` | Orders where `closed_at` is null |
| `closed` | Orders where `closed_at` is set |
| `cancelled` | Orders where `cancelled_at` is set |

### Other useful methods

| Method | Description |
| :- | :- |
| `isDraft(): bool` | `true` when `placed_at` is empty |
| `isPlaced(): bool` | `true` when `placed_at` is set |
| `isOpen(): bool` / `isClosed(): bool` | Whether the order has been archived, see [Open and closed orders](#open-and-closed-orders) |
| `isCancelled(): bool` | Whether the order has been cancelled |
| `lifecycleStatus(): string` | Returns `'cancelled'`, `'closed'`, or `'open'` — cancelled takes precedence. Used for badges and search |
| `cancelReasonLabel(): ?string` | The human-readable label for `cancel_reason` |

## Creating an order from a cart

An order is created from a cart via the `createOrder()` verb:

```php theme={null}
use Lunar\Core\Models\Cart;

$cart = Cart::find(1);

$order = $cart->createOrder(
    allowMultipleOrders: false,
    orderIdToUpdate: null,
);
```

* `allowMultipleOrders` — a cart generally has one draft order associated with it. Pass `true` to allow multiple orders per cart (for example, a declined payment followed by a new checkout attempt with a different order).
* `orderIdToUpdate` — optionally pass the ID of an existing draft order (one with a null `placed_at`) belonging to the cart, to update it instead of creating a new one.

Before creating, the cart is validated against `config('lunar.cart.validators.order_create')` (defaulting to `Lunar\Core\Validation\Cart\ValidateCartForOrderCreation`), which throws a validation exception with a helpful message when the cart is not ready. Check readiness without throwing:

```php theme={null}
$cart->canCreateOrder(); // bool
```

`createOrder()` delegates to `Lunar\Core\Contracts\Actions\Carts\CreatesOrder` (default `Lunar\Core\Actions\Carts\CreateOrder`), which fills the order's `cart_id` and `fingerprint`, then runs it through the pipeline configured under `lunar.orders.pipelines.creation` in `config/lunar/orders.php`:

```php theme={null}
'pipelines' => [
    'creation' => [
        FillOrderFromCart::class,
        CreateOrderLines::class,
        CreateOrderAddresses::class,
        CreateShippingLine::class,
        CleanUpOrderLines::class,
        MapDiscountBreakdown::class,
    ],
],
```

Each stage runs in order: `FillOrderFromCart` copies totals, currency, and customer data across and generates the reference; `CreateOrderLines` snapshots the cart's lines onto `OrderLine` rows; `CreateOrderAddresses` copies the cart's addresses; `CreateShippingLine` adds a `shipping` line when a shipping option was chosen; `CleanUpOrderLines` and `MapDiscountBreakdown` tidy up lines and rewrite the discount breakdown once discounts are marked as used. Add a custom stage by extending the array, or replace `CreatesOrder`'s binding to change the flow entirely.

`payment_status` and `fulfilment_status` are not set explicitly during creation — they take the migration column defaults (`pending` and `unfulfilled`) until the first recompute runs (for example, when a transaction or fulfilment is recorded against the order).

### Order reference generation

The order reference is generated from `config/lunar/orders.php`:

```php theme={null}
'reference_format' => [
    'prefix' => null,
    'padding_direction' => STR_PAD_LEFT,
    'padding_character' => '0',
    'length' => 8,
],

'reference_generator' => Lunar\Core\Orders\ReferenceGenerator::class,
```

The default generator pads the order's ID to `length` characters using `padding_character`/`padding_direction`, then prepends `prefix`. Set `reference_generator` to `null` to disable reference generation. A custom generator implements `Lunar\Core\Contracts\OrderReferenceGenerator`:

```php theme={null}
namespace App\Orders;

use Lunar\Core\Contracts\OrderReferenceGenerator;
use Lunar\Core\Models\Order;

class CustomReferenceGenerator implements OrderReferenceGenerator
{
    public function generate(Order $order): string
    {
        return 'ORD-'.$order->id;
    }
}
```

## Order lifecycle

An order's lifecycle is read from three independent signals rather than one status column: whether it is a draft or placed (`placed_at`), its two derived rollups (`payment_status`, `fulfilment_status`), and whether it has been archived (`closed_at`) or cancelled (`cancelled_at`).

<Warning>
  `payment_status` and `fulfilment_status` are **derived, unguarded rollups**, not a hand-driven state machine — there is no fixed transition graph and nothing to call `transitionTo()` on. They are recomputed automatically from the transaction ledger and the order's fulfilments whenever either changes, and any value can follow any other as the underlying records change.
</Warning>

### Payment status

`payment_status` is cast to a `Lunar\Core\States\Order\Payment\PaymentStatus` instance, rolled up from the order's `transactions`:

| State | `$name` | Meaning |
| :- | :- | :- |
| `Pending` | `pending` | No transactions recorded |
| `Voided` | `voided` | Transactions exist but none succeeded |
| `Authorized` | `authorized` | A successful payment intent exists, nothing captured yet |
| `PartiallyPaid` | `partially-paid` | Some, but not all, of the order total has been captured |
| `Paid` | `paid` | The full order total has been captured (or the order total is zero) |
| `PartiallyRefunded` | `partially-refunded` | The order was paid in full and part of it has since been refunded |
| `Refunded` | `refunded` | Everything captured has been refunded |

### Fulfilment status

`fulfilment_status` is cast to a `Lunar\Core\States\Order\Fulfilment\FulfilmentStatus` instance, rolled up from the order's fulfillable lines and its `fulfilments` (see [Fulfilments](/2.x/reference/fulfilments) for the per-fulfilment lifecycle):

| State | `$name` | Meaning |
| :- | :- | :- |
| `Unfulfilled` | `unfulfilled` | None of the fulfillable quantity has been dispatched |
| `PartiallyFulfilled` | `partially-fulfilled` | Some, but not all, of the fulfillable quantity has been dispatched |
| `Fulfilled` | `fulfilled` | All fulfillable quantity has been dispatched (an order with nothing to fulfil resolves here immediately) |
| `PartiallyReturned` | `partially-returned` | Some dispatched quantity has been returned |
| `Returned` | `returned` | All dispatched quantity has been returned |

Both rollups recompute automatically whenever a `Transaction` or a `Fulfilment`/`FulfilmentLine` changes, dispatching `Lunar\Core\Events\Orders\OrderPaymentStatusUpdated` / `OrderFulfilmentStatusUpdated` when the value actually changes.

### Open and closed orders

Independently of payment and fulfilment, an order is either open (in the active work queue) or closed (archived):

```php theme={null}
$order->close();   // Lunar\Core\Contracts\Actions\Orders\ClosesOrder
$order->reopen();  // Lunar\Core\Contracts\Actions\Orders\ReopensOrder
```

Both are idempotent — closing a closed order, or reopening an open one, is a no-op. A cancelled order cannot be reopened.

<Tip>
  An order can be closed automatically the moment it becomes fully paid (`Paid`) and fully fulfilled (`Fulfilled`). It is off by default — bind a custom `Lunar\Core\Contracts\OrderSettings` implementation and return `true` from `autoClosesSettledOrders()` to opt in:

  ```php theme={null}
  use Lunar\Core\Contracts\OrderSettings;
  use Lunar\Core\Models\Order;

  class AutoCloseOrderSettings implements OrderSettings
  {
      public function autoClosesSettledOrders(Order $order): bool
      {
          return true;
      }
  }
  ```

  Bind it in a service provider: `$this->app->bind(OrderSettings::class, AutoCloseOrderSettings::class);`. Auto-close never reopens an order that later regresses (a return or a partial refund) — that stays a deliberate `reopen()`.
</Tip>

### Cancelling an order

```php theme={null}
$order->cancel(
    reason: 'customer',
    note: 'Customer changed their mind',
    notify: true,
);
```

Cancellation is one-way and covers status only — it does not issue a refund or restock inventory. An order can be cancelled as long as it is not already cancelled and nothing on it has shipped or returned. Cancelling voids the order's un-shipped fulfilments, stamps `cancelled_at`/`cancel_reason`/`cancel_note`, and also closes the order.

Reasons come from the `CancelReasons` manifest rather than config, so they can be adjusted per application:

```php theme={null}
use Lunar\Core\Facades\CancelReasons;

CancelReasons::all();                          // array<string, string>
CancelReasons::add('warehouse-error', 'Warehouse error');
CancelReasons::forget('fraud');
```

## Order lines

```php theme={null}
Lunar\Core\Models\OrderLine
```

<Info>
  There is no generic "fee" or "extra charge" line primitive. A line's `type` is whatever the purchasable's `getType()` returns (core's `ProductVariant` returns `physical` or `digital`); an extra charge is modelled as an order line tied to its own `Purchasable`, and incoming money is tracked separately as `Transaction` rows (see [Transactions](#transactions)) rather than as a line item.
</Info>

### Fields

| Field | Type | Description |
| :- | :- | :- |
| id | `bigIncrements` | Primary key |
| public\_id | `ulid` | Stable external identifier |
| order\_id | `foreignId` | |
| purchasable\_type | `string` `nullable` | Polymorphic type for the purchasable item. Empty on shipping lines |
| purchasable\_id | `unsignedBigInteger` `nullable` | Polymorphic ID for the purchasable item. Empty on shipping lines |
| type | `string` | The line type, from the purchasable's `getType()` (e.g. `physical`, `digital`); always `shipping` for the shipping line |
| requires\_shipping | `boolean` | Needs physical delivery, stamped from the purchasable's `isShippable()` |
| requires\_fulfilment | `boolean` | Needs fulfilling at all — a superset of `requires_shipping` that drives the fulfilment rollup |
| description | `string` | A description of the line item |
| option | `string` `nullable` | Option information if the item is a variant |
| identifier | `string` | An identifier for the purchasable item, typically a SKU |
| unit\_price | `unsignedBigInteger` | The unit price of the line |
| unit\_quantity | `smallInteger` | The unit quantity, typically `1` |
| quantity | `unsignedInteger` | The quantity purchased |
| sub\_total | `unsignedBigInteger` | The subtotal minus any discounts, excluding tax |
| discount\_total | `unsignedBigInteger` | The discount amount, excluding tax |
| tax\_breakdown | `jsonb` | Breakdown of applied taxes |
| tax\_total | `unsignedBigInteger` | The total amount of tax applied |
| total | `unsignedBigInteger` | The grand total including tax |
| refunded\_quantity | `unsignedInteger` | Rollup of `refund_lines.quantity` for this line, see [Refunds](#refunds) |
| notes | `text` `nullable` | Additional line notes |
| meta | `jsonb` `nullable` | Custom metadata |
| created\_at | `timestamp` | |
| updated\_at | `timestamp` | |

### Relationships

| Relationship | Type | Related Model | Description |
| :- | :- | :- | :- |
| `order` | BelongsTo | `Lunar\Core\Models\Order` | |
| `purchasable` | MorphTo | — | The polymorphic purchasable item. `null` on shipping lines |
| `fulfilmentLines` | HasMany | `Lunar\Core\Models\FulfilmentLine` | See [Fulfilments](/2.x/reference/fulfilments) |
| `refundLines` | HasMany | `Lunar\Core\Models\RefundLine` | See [Refunds](#refunds) |
| `currency` | HasOneThrough | `Lunar\Core\Models\Currency` | Resolved through the order |

### Scopes

| Scope | Description |
| :- | :- |
| `withoutFulfilment` | Lines not yet allocated to any fulfilment |

### Other useful methods

```php theme={null}
$line->refundableQuantity(): int; // quantity minus refunded_quantity, floored at 0
```

<Info>
  Shipping lines have no `purchasable` — a shipping option is a data transfer object rather than a model, so shipping lines are stored with no morph. Everything needed to display the line (`description`, `unit_price`, `total`, `meta`) is snapshotted onto the line itself. Guard for `null` when iterating `$order->lines`, or use `$order->productLines` to exclude shipping lines.
</Info>

## Order addresses

An order has many addresses, typically one for billing and one for shipping. They are created automatically by the `createOrder()` pipeline.

```php theme={null}
Lunar\Core\Models\OrderAddress
```

### Fields

| Field | Type | Description |
| :- | :- | :- |
| id | `bigIncrements` | Primary key |
| public\_id | `ulid` | Stable external identifier |
| order\_id | `foreignId` | |
| country\_id | `foreignId` `nullable` | |
| title | `string` `nullable` | |
| first\_name | `string` `nullable` | |
| last\_name | `string` `nullable` | |
| company\_name | `string` `nullable` | |
| line\_one | `string` `nullable` | |
| line\_two | `string` `nullable` | |
| line\_three | `string` `nullable` | |
| city | `string` `nullable` | |
| state | `string` `nullable` | |
| postcode | `string` `nullable` | |
| delivery\_instructions | `string` `nullable` | |
| contact\_email | `string` `nullable` | |
| contact\_phone | `string` `nullable` | |
| type | `string` | The address type: `billing` or `shipping` |
| shipping\_option | `string` `nullable` | A unique identifier for the selected shipping option |
| meta | `jsonb` `nullable` | Custom metadata |
| created\_at | `timestamp` | |
| updated\_at | `timestamp` | |
| tax\_identifier | `string` `nullable` | A tax identification number |

### Relationships

| Relationship | Type | Related Model | Description |
| :- | :- | :- | :- |
| `order` | BelongsTo | `Lunar\Core\Models\Order` | |
| `country` | BelongsTo | `Lunar\Core\Models\Country` | |

The shipping and billing addresses are accessed directly from the order:

```php theme={null}
$order->shippingAddress;
$order->billingAddress;
```

## Transactions

Money movement against an order — authorizations, captures, and refunds — is recorded on `Transaction` rows. There is no separate "tender" model: a successful `capture` transaction *is* the record of money received, and a `refund` transaction is money paid back out.

```php theme={null}
Lunar\Core\Models\Transaction
```

### Fields

| Field | Type | Description |
| :- | :- | :- |
| id | `bigIncrements` | Primary key |
| public\_id | `ulid` | Stable external identifier |
| order\_id | `foreignId` | |
| success | `boolean` | Whether the transaction was successful |
| driver | `string` | The payment driver used, e.g. `stripe` |
| amount | `integer` | The transaction amount, in the order currency's minor unit |
| reference | `string` | The reference returned from the payment provider |
| status | `string` | The transaction status, e.g. `settled` |
| notes | `string` `nullable` | Any relevant notes |
| card\_type | `string` `nullable` | The card type, e.g. `visa` |
| last\_four | `string` `nullable` | The last four digits of the card |
| meta | `jsonb` `nullable` | Custom metadata |
| created\_at | `timestamp` | |
| updated\_at | `timestamp` | |
| parent\_transaction\_id | `foreignId` `nullable` | A reference to a parent transaction |
| captured\_at | `dateTime` `nullable` | When the transaction was captured |
| type | `enum` | The transaction type: `capture`, `intent`, or `refund` |

### Relationships

| Relationship | Type | Related Model | Description |
| :- | :- | :- | :- |
| `order` | BelongsTo | `Lunar\Core\Models\Order` | |
| `currency` | HasOneThrough | `Lunar\Core\Models\Currency` | Resolved through the order |
| `refundLines` | HasMany | `Lunar\Core\Models\RefundLine` | Line-level allocations for a refund transaction, see [Refunds](#refunds) |

<Info>
  See the [Payments](/2.x/reference/payments) reference for how a payment driver authorizes and creates these transactions in the first place.
</Info>

### Capturing a payment

Capture an amount against a successful payment intent transaction:

```php theme={null}
$capture = $order->capture(
    transactionId: $transaction->id,
    amount: 49.99, // major units, converted to the currency's minor unit internally
);

$capture->success; // bool
$capture->message; // string
```

`$order->capture()` delegates to `Lunar\Core\Contracts\Actions\Orders\CapturesOrder`, guards that the amount does not exceed the intent, and dispatches the driver's `capture()` — which creates the `capture` `Transaction`. Recording the capture triggers a `payment_status` recompute automatically.

## Refunds

A refund targets a specific capture `Transaction` and can cover order lines, shipping, and a manual adjustment in one request:

```php theme={null}
use Lunar\Core\DataObjects\RefundRequest;

$refund = $order->refund(new RefundRequest(
    transactionId: $transaction->id,
    lines: [
        ['order_line_id' => $line->id, 'quantity' => 1],
    ],
    shipping: 0,
    adjustment: 0,
    notes: 'Damaged on arrival',
    notify: true,
));

$refund->success;     // bool
$refund->message;     // ?string
$refund->transaction; // ?Lunar\Core\Models\Transaction — the refund transaction, when the driver records one
```

Each requested line is validated against its own `refundableQuantity()`, and the total requested amount is validated against the order's available-to-refund balance (captured minus already refunded). An amount-only refund — no line allocation — is expressed with an empty `lines` array and the whole amount on `adjustment`.

When the driver's refund succeeds and returns a `Transaction`, each line allocation is recorded as a `RefundLine` and the corresponding `OrderLine.refunded_quantity` is incremented:

```php theme={null}
Lunar\Core\Models\RefundLine
```

| Field | Type | Description |
| :- | :- | :- |
| id | `bigIncrements` | Primary key |
| public\_id | `ulid` | Stable external identifier |
| transaction\_id | `foreignId` | The refund transaction this allocation belongs to |
| order\_line\_id | `foreignId` | The order line this allocation refunds |
| quantity | `unsignedInteger` | The quantity refunded |
| amount | `unsignedBigInteger` | The refunded amount, in the order currency's minor unit |
| created\_at | `timestamp` | |
| updated\_at | `timestamp` | |

| Relationship | Type | Related Model | Description |
| :- | :- | :- | :- |
| `transaction` | BelongsTo | `Lunar\Core\Models\Transaction` | |
| `orderLine` | BelongsTo | `Lunar\Core\Models\OrderLine` | |

<Info>
  Not every payment driver hands back the `Transaction` it created for a refund — `$refund->transaction` can be `null`. In that case the money still refunds correctly through the provider, it just cannot be attributed to specific lines in the ledger.
</Info>

## Notifying the customer

```php theme={null}
$order->notifyCustomer(
    notification: 'order-update',
    message: 'Your order has been delayed by a day.',
    recipients: [], // defaults to the order's billing + shipping contacts
);
```

See the [Notifications](/2.x/reference/notifications) reference for the notification manifest, mail routing, and the notifications Lunar sends automatically.

## Fulfilments

An order's physical, digital, or click-and-collect fulfilment is tracked through `Fulfilment` records, created against the order's `fulfillableLines`:

```php theme={null}
$fulfilment = $order->createFulfilment(
    lines: [$line->id => 1],
);
```

See the [Fulfilments](/2.x/reference/fulfilments) reference for the full fulfilment lifecycle, methods, carriers, and tracking.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.