A feature-by-feature pass across Shopify, WooCommerce, and PrestaShop's checkout
layer — sourced, not recalled from memory — checked against what
Lunar's Cart::createOrder() / order-creation pipeline
actually supports today. Companion to the Cart survey: this starts where that one
left off — address and shipping-option capture through to a placed order. For
deciding what to design next, not a build order.
Who's allowed to check out, and in how many steps.
Order.user_id and customer_id are both nullable, and ValidateCartForOrderCreation never checks for either — it only requires a billing address and, if shippable, a shipping address + option. A cart with no user_id creates an order fine.setShippingAddress()/setBillingAddress()/setShippingOption() as independent calls that a UI can sequence however it likes. WooCommerce and PrestaShop both ship one-page as a plugin/theme layer, not core, so this isn't a Lunar gap so much as storefront work still to do.setShippingAddress() call.Payments facade is driver-based (Payments::driver('card')) so a wallet driver is architecturally pluggable, but none ships, and there's no one-tap "skip the address form" path since address capture still runs through the standard cart-address flow first.Order.meta and Cart.meta are both free-form JSON columns carried straight through FillOrderFromCart ('meta' => $cart->meta) — technically able to record a timestamp/version of accepted terms today, but no dedicated field, checkbox validation, or admin display exists.What actually happens inside createOrder(), verified from source.
Cart::draftOrder() matches on fingerprint() + total, so re-running createOrder() on an unchanged cart reuses the same draft order instead of duplicating it (CreateOrder::execute()); once an order is placed, hasCompletedOrders() throws DisallowMultipleCartOrdersException unless allowMultipleOrders is explicitly passed.Order::isDraft()/isPlaced() gate on placed_at; orders.draft_status config (default awaiting-payment) sets the initial status. The order exists — and can be re-run through the pipeline idempotently via the fingerprint match above — before a payment driver ever authorizes anything.orders.pipelines.creation chain does this explicitly — FillOrderFromCart, CreateOrderLines, CreateOrderAddresses, CreateShippingLine, CleanUpOrderLines, MapDiscountBreakdown — each copying cart state into immutable order rows rather than referencing the cart live.ValidateCartForOrderCreation requires country_id, first_name, line_one, city, postcode on billing always, and on shipping too unless the chosen ShippingOption->collect is true (in-store pickup skips a shipping address).FillOrderFromCart copies currency_code and exchange_rate from the cart's currency onto the order at creation — later currency-config changes don't retroactively alter placed orders.What tells the customer (and staff) an order happened.
config/lunar/orders.php carries a mailers and notifications array, but grep across core turns up exactly one reader of that config (Order::getStatusLabelAttribute(), and it only reads label). Nothing in core ever dispatches a mailer or notification from a status change — those keys are unwired placeholders, not a working feature.src/Events/ in core contains only PaymentAttemptEvent. No OrderCreated, no OrderStatusUpdated. Confirmation email, staff Slack ping, or customer SMS on status change all have to be built from scratch on plain Eloquent model events (Order::updated()), same pattern as the cart-event gap.Order.reference, status, OrderAddress.contact_email) but no lookup mechanism — a guest with no account has no route back to their order without the confirmation email that also doesn't exist yet.CreateOrder::execute() dispatches MarkAsNewCustomer::dispatch($order->id) as a queued job after every order creation — genuinely wired, unlike the mail/notification config above.Distinct from abandoned cart recovery (covered in the Cart survey) — this is someone who reached address/email capture and still left.
Order::isDraft() plus the address already captured on it — but per the Cart survey's finding, there's no Filament resource for Cart and (unverified here, likely the same gap) no dedicated "abandoned checkout" view distinguishing a draft order with a captured address from one that never got that far.OrderAddress.contact_email, where an abandoned guest cart usually has none — but nothing sends on it.What the customer sees the moment money is on screen.
TaxZone.price_display is a first-class enum (tax_inclusive/tax_exclusive), and Price::priceExTax()/priceIncTax() both exist on the model — more complete than PrestaShop, where dual-price display is a separately-sold addon module, not core.Cart.taxBreakdown and OrderLine.tax_breakdown are both populated structured objects (iterate .amounts), not just a lump-sum total — the data supports a itemized tax display, a storefront just has to render it.Currency.exchange_rate plus sync_prices per non-default currency, and the rate is snapshotted onto the order at creation (see 02) — the same mechanics PrestaShop needs an addon for.attribute_data + Language, but checkout itself — form labels, validation errors, status labels — is storefront-owned Laravel localization, not something Lunar's order pipeline touches either way.ShippingOption.collect is a real boolean the validator checks directly — when true, ValidateCartForOrderCreation skips the shipping-address requirement entirely. Modeled at the same level as the collection driver in the Table Rate Shipping add-on.