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.Order model
Fields
Relationships
Scopes
Other useful methods
Creating an order from a cart
An order is created from a cart via thecreateOrder() verb:
allowMultipleOrders— a cart generally has one draft order associated with it. Passtrueto 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 nullplaced_at) belonging to the cart, to update it instead of creating a new one.
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:
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 fromconfig/lunar/orders.php:
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
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):Cancelling an order
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 thecreateOrder() 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 onTransaction 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 captureTransaction and can cover order lines, shipping, and a manual adjustment in one request:
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
Fulfilments
An order’s physical, digital, or click-and-collect fulfilment is tracked throughFulfilment records, created against the order’s fulfillableLines: