boboko / checkout · competitive survey

What checkout elsewhere can do that boboko can't yet

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.

● have ◐ partial ○ missing
01

Getting to checkout

Who's allowed to check out, and in how many steps.

Guest checkout (no account required)
have
Structural, not bolted-on: 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.
One-page vs. multi-step checkout
missing
Pure storefront-UI concern — Lunar has no opinion here, it just exposes 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.
Address autocomplete (type-ahead, from Google Places / Loqate)
missing
Research: cuts address-entry keystrokes by 70%+ and is a proven abandonment-reduction tactic (Google Maps Platform, Loqate). No Lunar hook for it either way — it's a storefront form concern layered on top of the same setShippingAddress() call.
Express/accelerated checkout (Shop Pay, Apple Pay, Google Pay equivalents)
missing
Research: Shopify reports Shop Pay can lift conversion up to 50% over guest checkout, mobile especially. Lunar's 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.
Terms & conditions acceptance at checkout
partial
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.
02

Order creation mechanics

What actually happens inside createOrder(), verified from source.

Duplicate-order prevention on repeat submits
have
Two layers, both real: 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.
Draft order created before payment, finalized after
have
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.
Order address, line, and shipping-line snapshotting from cart
have
The whole 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.
Address validation before order creation
have
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).
Exchange rate and currency locked at order time
have
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.
03

Confirmation & communication

What tells the customer (and staff) an order happened.

Order confirmation email on placement
missing
Surprising given how close it looks to shipping: every status in 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.
Order-status-changed events
missing
Same gap as Cart's event survey found — 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 tracking / status lookup for guests
missing
Research: PrestaShop's order-tracking extensions explicitly cover "non-logged-in customers track their orders." Lunar has the data (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.
New-customer detection on first order
have
CreateOrder::execute() dispatches MarkAsNewCustomer::dispatch($order->id) as a queued job after every order creation — genuinely wired, unlike the mail/notification config above.
04

Abandoned checkout recovery

Distinct from abandoned cart recovery (covered in the Cart survey) — this is someone who reached address/email capture and still left.

Draft orders are queryable and staff-visible
partial
The data exists — 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.
Automated recovery email (post-address-capture)
missing
Research: Shopify's built-in template fires after a shopper enters details and leaves, with editable wait time and an optional discount. boboko has strictly better raw material for this than the cart-abandonment case — a draft order after address capture always has OrderAddress.contact_email, where an abandoned guest cart usually has none — but nothing sends on it.
Abandoned-checkout stage tracking (email captured vs. shipping selected vs. payment started)
missing
No event dispatch anywhere in the checkout pipeline (see 03) means no timestamped record of which step a checkout got to — only the current state of the draft order, not its history.
05

Pricing, tax & locale at checkout

What the customer sees the moment money is on screen.

Tax-inclusive vs. tax-exclusive price display
have
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.
Full tax breakdown shown at checkout (per-line, per-rate)
have
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.
Multi-currency checkout (pay in shopper's own currency)
have
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.
Multi-language checkout copy
partial
Product/collection/attribute copy is fully translatable via 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.
Click-and-collect / in-store pickup as a checkout option
have
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.