Skip to main content
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 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.
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.
Fulfilment (shipping, collection, digital provisioning, tracking, carriers) is covered in full on the Fulfilments page. Sending emails to the customer is covered on the Notifications page. Capturing and refunding money through a payment provider is covered on the Payments page — this page documents the order-side records those operations write to.

Order model

Fields

Relationships

Scopes

Other useful methods

Creating an order from a cart

An order is created from a cart via the createOrder() verb:
  • 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:
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:
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:
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:

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

Payment status

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

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 for the per-fulfilment lifecycle): 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):
Both are idempotent — closing a closed order, or reopening an open one, is a no-op. A cancelled order cannot be reopened.
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:
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().

Cancelling an order

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:

Order lines

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) rather than as a line item.

Fields

Relationships

Scopes

Other useful methods

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.

Order addresses

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

Fields

Relationships

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

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.

Fields

Relationships

See the Payments reference for how a payment driver authorizes and creates these transactions in the first place.

Capturing a payment

Capture an amount against a successful payment intent transaction:
$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:
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:
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.

Notifying the customer

See the 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:
See the Fulfilments reference for the full fulfilment lifecycle, methods, carriers, and tracking.