A feature-by-feature pass across Shopify, WooCommerce, and PrestaShop's shipping
layer — sourced, not recalled from memory — checked against what
Lunar core's ShippingManifest and the
lunarphp/table-rate-shipping add-on actually support
today, and what's actually wired up in boboko-core and 3dealer right now.
For deciding what to design next, not a build order.
The mechanism Lunar core provides for offering and applying a shipping charge — everything else in this survey is built on top of it.
Lunar\Base\ShippingModifier abstract class + ShippingManifest::addOption() — any package can register options onto the manifest via a pipeline of modifiers (ShippingModifiers::getModifiers()).Lunar\Pipelines\Cart\ApplyShipping — reads ShippingManifest::getShippingOption($cart) or a manual shippingOptionOverride, writes a ShippingBreakdown and shippingSubTotal onto the cart before CalculateTax runs.Cart::isShippable() — true if any line's purchasable (e.g. ProductVariant::isShippable()) is shippable; a digital-only cart skips the shipping-address requirement entirely.Cart::setShippingOption() → SetShippingOption action, validated by ShippingOptionValidator, triggers a recalculate. Nothing in 3dealer's storefront calls it yet — no shipping step exists in the UI.Lunar\Pipelines\Order\Creation\CreateShippingLine writes an immutable shipping-type order line from the cart's shipping breakdown at checkout — survives later rate changes.lunarphp/table-rate-shipping is installed (composer.json, pinned ^1.3) and its ShippingPlugin is registered in CorePlugin::boot() — so 3dealer inherits it automatically, it doesn't need its own registration.
ShippingZone model, type unrestricted|countries|states|postcodes; ShippingZoneResolver::get() matches a cart's address against zone scope, falling back to any unrestricted zone.Drivers\ShippingMethods\FlatRate::resolve() — one price per cart subtotal via Pricing::for($shippingRate).Drivers\ShippingMethods\ShipBy::resolve() — data['charge_by'] is cart_total or weight, tiered via priceBreaks, with customer-group price overrides taking priority.Drivers\ShippingMethods\FreeShipping::resolve() — data['minimum_spend'] (per-currency array supported), optional use_discount_amount to check against post-discount subtotal.Drivers\ShippingMethods\Collection::resolve() — zero-price option, flagged collect: true on the ShippingOption. Single implicit "store" — no concept of which location, no per-location stock or hours.ShippingExclusionList + ShippingZone::shippingExclusions() — every driver checks it before resolving and returns null if any cart line's product is excluded from that zone.ShippingMethod::customerGroups() pivot carries visible, enabled, starts_at, ends_at — scheduling and audience-gating a rate is already modeled.ShippingZoneResource, ShippingMethodResource, ShippingExclusionListResource ship with the add-on — usable as soon as the Filament plugin is registered, which it is via CorePlugin.resources/views beyond a passing mention in components/footer.blade.php — the whole backend above is unwired to any customer-facing UI.Real carriers quoting and printing on Lunar's behalf, rather than merchant-defined flat/tiered rates.
table-rate-shipping calls an external carrier API — all four shipped drivers (FlatRate, ShipBy, FreeShipping, Collection) compute from local data. Shopify's CarrierService API is the model for this: shop sends weight/dims/destination, carrier returns live rates at checkout.ProductVariant has weight_value/weight_unit (referenced in ShipBy's weight tier and docs/lunar.md) but no length/width/height fields exist in core migrations — enough for weight-tier rating, not enough for carrier-grade dimensional/volumetric quotes.OrderShippingZone pivot table records which zone an order matched, but no field anywhere stores a carrier tracking number or shipment status.Where an order physically ships from, and whether it can ship from more than one place.
vendor/lunarphp/core or lunar — stock is a flat quantity on the variant. WooCommerce needs Calcurates or WooCommerce Warehouses add-ons for this; it's genuinely not a Lunar concept at all.CreateShippingLine writes exactly one shipping line per order.Collection driver models pickup as a single yes/no rate per zone — no location entity to pick from, no per-location hours/capacity.ShipBy/FlatRate (carrier-agnostic priced shipping) and Collection (pickup) exist as concepts.ShippingOption, CartAddress, or the order shipping line. WooCommerce needs a dedicated delivery-date-picker plugin for this too — not a gap unique to Lunar, but still open here.What happens when a shipment crosses a border, or something goes wrong in transit.
Product/ProductVariant migrations. Every international shipment needs one per line item to clear customs — researched requirement, not yet modeled anywhere in Lunar.CalculateTax pipeline step handles sales tax/VAT on the cart itself, not import duty estimation for cross-border orders. DDP vs. DDU is the standard framing (seller-collects-upfront vs. customer-pays-on-delivery) — neither is modeled.ShippingZone type countries/states/postcodes already scopes which rates apply where — the building block international shipping would sit on top of.