Framework support
Lunar 1.5 requires PHP 8.3 or later and Laravel 12 or 13. PHP 8.2 and Laravel 11 have reached end of life and are no longer supported. The admin panel is built on Filament v4. Projects with custom Filament resources, pages, widgets, or actions need to follow the Filament v4 upgrade guide alongside the Lunar upgrade. Thelunarphp/filament3-2fa package is no longer required, as Filament v4 provides its own multi-factor authentication primitives.
See the installation requirements for the full list.
High impact changes
Nested set package replaced
Collections build their tree using a nested set implementation, and Lunar now ships its own maintained fork.kalnoy/nestedset is replaced by lunarphp/nestedset, kept current for Laravel 12 and 13.
The API is unchanged, so tree queries continue to work as before, but the namespaces have moved from Kalnoy\Nestedset to Lunar\Nestedset. Application code that imports NodeTrait, QueryBuilder, or NestedSetServiceProvider directly needs updating. See Collections.
Order line purchasable morph is now nullable
Shipping order lines previously stored a placeholder morph pointing atLunar\DataTypes\ShippingOption, a data transfer object rather than a model. Reading $orderLine->purchasable on a shipping line, or eager loading lines.purchasable across an order, raised a fatal error as a result.
The purchasable_type and purchasable_id columns on order_lines are now nullable, and shipping lines are stored with no morph at all. A migration nulls the placeholder on existing shipping lines, scoped to that exact class so custom morphs are left untouched. $orderLine->purchasable now returns null for shipping lines instead of failing, so storefront code iterating $order->lines should guard for null or iterate $order->productLines instead.
Discount conditions honored by BuyXGetY
Lunar\DiscountTypes\BuyXGetY never checked the discount’s own conditions, so minimum spend, customer restrictions, and max_uses_per_user were all ignored and only min_qty gated the reward. These are now enforced, matching AmountOff.
Live BuyXGetY discounts configured with a minimum spend or customer restriction will start applying it, so review them before upgrading to confirm they reflect what the store intends to offer.
Stripe amount conversion
StripeManager passed the stored cart total straight through to Stripe for all but three currency codes, assuming the currency’s configured decimal_places matched the sub-unit Stripe expects. A currency configured with four decimal places was overcharged by a factor of 100.
Amounts are now converted back to the major unit using the currency’s decimal_places, then re-scaled to the sub-unit Stripe requires for that currency code. Stores using a currency whose decimal_places is not 2 should verify the amounts sent to Stripe after upgrading. See Stripe.
Discounts
A focused round of work on discount correctness:- Coupon codes now apply immediately on stores that also have an always-on discount. The memoized discount set was reused across carts within a request, which excluded coupon discounts and left the total unchanged until the page was reloaded.
- Re-creating a draft order for the same cart no longer consumes another use of its discounts. A card decline followed by a retry previously exhausted the coupon and re-priced the same order without it. Carts now track their own consumed discounts through
consumedDiscountIds(). BuyXGetYsupports a specific product variant as an automatically added reward. This previously raised a fatal error during cart calculation, taking down the basket rather than just checkout.- A multi-quantity
BuyXGetYreward is added as one line with that quantity, rather than one line per item. - Automatically added reward lines are stored using Lunar’s morph map, so their
purchasable_typematches every other line holding the same purchasable. - Automatic rewards only select items that can be fulfilled, so an out-of-stock reward is skipped rather than raising an error.
- Collection breadcrumbs are shown in the admin panel’s discount selection lists, making collections with the same name distinguishable.
- Discount usage lookups are memoized per cart instance, cutting repeated draft-order queries on every calculate.
Carts and orders
- Switching the session currency now reprices the cart.
CartSession::setCurrency()updated the cart’scurrency_idbut left the loaded relationships in place, so the next calculate priced every line in the previous currency. See Carts. - Shipping modifiers can safely calculate the cart. A modifier that did so previously re-entered the modifier pipeline and exhausted the stack, which surfaced only once a shipping address with a selected option was present.
- Order creation no longer duplicates addresses of the same type. Existing addresses are matched on type alone, so rebuilding a draft order with a changed address updates it in place.
Cart::lines()andOrder::lines()order byid, andProductVariant::values()orders byposition. These relationships previously left row order to the database engine, which is stable on MySQL but not on PostgreSQL, where it could change a cart’s fingerprint after an unrelated update.- Order lines for digital products appear on the generated invoice PDF. The template iterated physical lines only, silently omitting non-shippable products and services.
Tax
- State tax zones are scoped to the address country. A state zone previously matched any address with a matching state code or name in any country, so a shopper in Western Australia could be charged Washington State sales tax. See Taxation.
SystemTaxDriverresolves the default tax zone throughTaxZone::getDefault(), removing duplicate queries during cart calculation.
Shipping
- Weight tiers on the
ship-bydriver are evaluated in the shipping method’s configuredweight_unit, defaulting tokg. Each cart line’s weight is converted from its own unit first, so a cart mixing grams and kilograms totals correctly. Methods that set a unit other thankgneed their tier thresholds re-entered in that unit. See Table Rate Shipping. - The admin tier form shows the configured unit and rejects decimal weight tiers, instead of silently truncating them.
- Shipping Zone create and update no longer fail validation with “The selected countries is invalid” when limiting a zone to specific countries or states.
- The Shipping permission is translated in the admin panel.
Admin panel
- A new
Lunar\Admin\Events\ProductVariantInventoryUpdatedevent is dispatched when stock is saved on the product or variant inventory pages, bringing inventory changes in line with the panel’s other resource events. See Extending the Admin Panel. - Attribute
default_valueis exposed in the admin panel and applied when creating records. - Media custom properties are no longer dropped on create or edit. Keys contributed through a
RelationManagerExtensionare persisted, and keys the edit form does not render survive a save. - Customers can be deleted individually, rather than only through the bulk action.
- Product option mapping no longer shows the first variant’s SKU against unmatched option permutations, and newly added permutations save correctly, inheriting their tax class and pricing from the oldest sibling variant.
- The Unit Quantity field on product variants enforces a minimum of
1, preventing a division by zero when the item is added to a cart. - Boolean order metadata is displayed as Yes or No rather than a raw value.
- Product variant widget labels are translated.
- Whitespace is trimmed from panel form inputs.
Developer experience
- A Laravel Boost skill ships with the core package, installable with
php artisan boost:add-skill lunarphp/lunar. It orients an AI assistant in a Lunar project and points it at current documentation. See AI Development. - Date parameters on the channel and customer group scoping and scheduling traits accept
DateTimeInterface, so applications usingDate::use(CarbonImmutable::class)no longer hit aTypeErrorduring cart and discount calculation. HasDefaultRecord::getDefault()is annotated as nullable, matching what it returns when no record is flagged as the default.- The
$formatterStyleparameter onDefaultPriceFormatteris typedint, matching theNumberFormatterconstants passed to it. - Route facade imports are fixed in the Opayo and PayPal route files.
Upgrading
1.5.0-beta.6 requires the migration in this release, which makes the order line purchasable morph nullable and clears the placeholder morph from existing shipping lines.