> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lunarphp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Upgrade Guide

> How to move a Lunar v1.x application to v2.x using the lunarphp/upgrade package.

This page covers the tooling and steps for moving a Lunar v1.x application to v2.x.

## Overview

Lunar v2 is a wholesale breaking release. The core package moves to the `Lunar\Core\…` namespace, the admin panel splits into a `lunarphp/filament` bridge package, migrations are flattened into a single v2 baseline, and a long list of renamed columns, renamed classes, and reshaped data (pricing, selling policy, order status, regions, fulfilment) ship alongside it. See [What's new in Lunar v2](/2.x/getting-started/overview/introduction) for the full picture of what changed and why.

A dedicated package, [`lunarphp/upgrade`](https://github.com/lunarphp/lunar), automates as much of the v1 to v2 transition as possible:

* **Rector rules** rewrite class references, renamed methods, renamed properties, and money-attribute access patterns across application code.
* **Data migrations** transform the existing v1 database in place (column renames, enum reconciliation, backfilling new v2 concepts such as regions and fulfilments) rather than requiring a fresh install.
* **A guided CLI**, `php artisan lunar:upgrade`, runs the composer, data-migration, and migrations-ledger steps in order, with a dry run mode and a manual-actions report at the end.

`lunarphp/upgrade` is a dev dependency of the application being upgraded. It is not a runtime dependency of Lunar v2 itself, and it is not published as part of a fresh v2 install.

<Warning>
  The upgrade package's data migrations are **one-way**. They have no `down()` method, so there is no automated rollback. Take a full database backup before running `php artisan lunar:upgrade`, and treat "restore from backup" as the only way back if something goes wrong partway through.
</Warning>

## Before upgrading

* Back up the database. This is the only recovery path if the upgrade needs to be undone.
* Commit or stash any uncommitted application changes.
* Make sure the application is fully up to date on the latest Lunar v1.x release and all its migrations have run. The upgrade command refuses to proceed otherwise.
* Run the upgrade against a staging copy of the database first, given the size of the breaking surface between v1 and v2.

<Info>
  Coming from GetCandy (pre-v1.x)? The `lunar:migrate:getcandy` command that handled that transition exists only on Lunar v1.x and has been removed in v2. Reach v1.x first, run that command there, then continue with this guide.
</Info>

## Requirements

Lunar v2 raises the minimum platform requirements:

* PHP 8.4 or later.
* Laravel 12 or 13.
* Filament v5, if using the Filament-based admin panel (`lunarphp/filament` / `lunarphp/admin`).

Upgrade the application's PHP and Laravel versions before starting the Lunar upgrade if it is not already on these versions.

## Step-by-step

### 1. Require the upgrade package

```sh theme={null}
composer require lunarphp/upgrade --dev
```

### 2. Run the upgrade command

```sh theme={null}
php artisan lunar:upgrade
```

The command runs four steps in order, each tied to the spec that introduced it:

1. **`composer-require-rewrite`** — rewrites the application's `composer.json`, swapping `lunarphp/lunar` for `lunarphp/admin` in `require` / `require-dev`, and adding an explicit `lunarphp/core` requirement if it was previously only pulled in transitively. Run `composer update` afterwards to refresh the lockfile.
2. **`rector`** — reports how many class references the bundled Rector rules would rewrite across the configured paths.
3. **`data-migrations`** — runs the package's data migrations against the existing v1 schema: class-string rewrites, column renames, enum value reconciliation, and backfilling new v2 concepts (regions, fulfilments, stock, `public_id`).
4. **`ledger-rewrite`** — rewrites the application's `migrations` table so v1 Lunar migration rows are removed and the v2 flat baseline is recorded as already run, so future v2.x migrations layer on cleanly.

Useful options:

```sh theme={null}
php artisan lunar:upgrade --dry-run              # print what each step would do, write nothing
php artisan lunar:upgrade --only=data-migrations  # run a single step by name
php artisan lunar:upgrade --skip=rector           # skip a step by name
php artisan lunar:upgrade --paths=app --paths=src # override the code paths Rector considers
php artisan lunar:upgrade --connection=upgrade    # target a non-default database connection
```

The command aborts before making any changes if the configured connection has no v1.x migration rows (nothing to upgrade) or already has v2 baseline rows recorded (already upgraded).

<Info>
  The `rector` step in `lunar:upgrade` currently reports the rewrite plan rather than invoking Rector directly. Run Rector against the application separately, using the rule set the upgrade package ships:

  ```sh theme={null}
  vendor/bin/rector process --config=vendor/lunarphp/upgrade/config/rector.php
  ```

  This covers the class, method, property, and money-attribute rewrites described below. Run it before or after the `lunar:upgrade` data migrations — the two operate on code and data respectively and do not depend on each other's order.
</Info>

### 3. Review the manual-actions report

After the data migrations run, `lunar:upgrade` prints a report of what happened per step, followed by any manual actions that need attention. Currently this includes:

* **`Lunar\DiscountTypes\AmountOff` has been split** into `Lunar\Core\DiscountTypes\PercentageOff` and `Lunar\Core\DiscountTypes\FixedAmountOff`. Stored discount records are converted automatically (based on their `data.fixed_value` flag), but source code referencing `AmountOff` needs to be updated by hand to whichever type it meant, since a rename rule cannot infer that. The discount's `data.fixed_values` key is renamed to `data.amounts`, and the `data.fixed_value` flag is removed.

### 4. Publish config and clear caches

```sh theme={null}
php artisan vendor:publish --tag=lunar --force
php artisan optimize:clear
```

Review the application's `lunar.upgrade` config (published from `lunarphp/upgrade`) before running the data migrations if the store uses non-default order statuses — see [Order status mapping](#order-status-mapping) below.

## What Rector automates

The bundled Rector configuration (`vendor/lunarphp/upgrade/config/rector.php`) applies several categories of rewrite across `app/`, `config/`, and `database/` by default:

* **Namespace moves.** Every class shipped by `lunarphp/core` moves from `Lunar\…` to `Lunar\Core\…` (for example `Lunar\Models\Product` becomes `Lunar\Core\Models\Product`, `Lunar\Facades\CartSession` becomes `Lunar\Core\Facades\CartSession`). Filament support classes extracted into the new `lunarphp/filament` bridge package move from `Lunar\Admin\Support\…` to `Lunar\Filament\…`.
* **Renamed methods and properties**, including `ProductType::mappedAttributes()` → `attributeMapping()`, `Price::$compare_price` → `$list_price`, and `ProductVariant::$purchasable` → `$selling_policy` (class-scoped, so the unrelated `CartLine`/`OrderLine` `purchasable` morph relation and the `customer_group_product.purchasable` pivot boolean are left untouched).
* **Money-attribute access.** v1's per-attribute `PriceDataType` value object is gone; casts now return raw integers with formatting on the model. Rector rewrites `$order->total->value` to `$order->total`, `$order->total->formatted()` to `$order->format('total')`, and `$price->price->unitDecimal()` / `unitFormatted()` on the catalogue `Price` model to `$price->unitDecimal('price')` / `$price->unitFormat('price')`.
* **`Model::modelClass()` calls** rewrite to `Model::class`, since model class substitution has been removed (see [Model extending](#model-extending-is-removed) below).
* **`Order::refund()` call sites** rewrite from three positional arguments to a `Lunar\Core\DataObjects\RefundRequest`: `$order->refund($transactionId, $amount, $notes)` becomes `$order->refund(new RefundRequest(transactionId: $transactionId, adjustment: $amount, notes: $notes))`.
* **Action call sites.** `SomeAction::run(...$args)` rewrites to `app(SomeContract::class)->execute(...$args)`, following the move to constructor-injected action classes bound to a contract.
* **Translated catalogue field access.** `$product->translateAttribute('name')` / `$product->attr('name')` rewrite to `$product->translate('name')` for `name` and `description`, now dedicated translatable columns instead of `attribute_data` entries.
* **Custom price formatter signatures.** A consumer's own `PriceFormatterInterface` implementation has its `$formatterStyle` parameter retyped from `string` to `int` on `formatted()`, `unitFormatted()`, and `formatValue()`, matching the `NumberFormatter::*` constants it receives.

## What the data migrations do

The package ships one data migration per v1 to v2 schema change, run in dependency order by the `data-migrations` step:

* **Class-string rewrite** — updates persisted class strings (discount conditions, purchasable morphs, and similar) from `Lunar\…` to `Lunar\Core\…`.
* **`compare_price` → `list_price`** — renames the column on `prices`.
* **Catalogue `name` / `description` columns** — adds dedicated translatable columns and migrates the equivalent `attribute_data` values onto them.
* **Attribute data keys to IDs** — converts the attribute system's handle-keyed storage to id-keyed storage.
* **Order line `requires_fulfilment` backfill.**
* **Stock backfill** — populates the new per-location `StockLevel` / `StockMovement` records from the v1 flat `stock` column.
* **Default region seed and backfill** — v1 has no region concept; this seeds a catch-all default `Region` from the v1 default channel, currency, and language, and backfills `region_id` on existing carts and orders.
* **Shipping line purchasable morph nulling** — shipping order lines no longer store a placeholder morph.
* **`purchasable` → `selling_policy`** — renames the column on `product_variants`, reconciles stored values onto the `SellingPolicy` enum's canonical set (correcting the `in_stock_or_backorder` typo some v1 test fixtures used), and maps any unrecognised value to `always` so the enum cast cannot throw after upgrade.
* **`public_id` backfill** — mints a ULID onto every addressable model.
* **Brand handle and status columns.**
* **Product type columns.**
* **Product variant `enabled` column.**
* **Collection and channel status columns.**
* **Soft-delete reconciliation** — v2 drops soft deletes from several models; this migration reconciles previously soft-deleted rows.
* **Order status and fulfilment backfill** — replaces the hand-set v1 `orders.status` headline with the derived `payment_status` / `fulfilment_status` rollups and the `closed_at` / `cancelled_at` archive columns, and materializes a whole-order `Fulfilment` record for each historically shipped order. See [Order status mapping](#order-status-mapping).
* **`AmountOff` discount type split** — converts stored `AmountOff` discount rows into `PercentageOff` or `FixedAmountOff` based on the discount's `data.fixed_value` flag (see the manual action above).
* **Product association `sort` column backfill.**

### Order status mapping

v1's `orders.status` is a free-form string set by hand per store. v2 replaces it with two rollups derived from the order's transactions and fulfilments. The order-status data migration needs to know which of a store's v1 statuses map to which v2 concept, configured in `config/lunar/upgrade.php`:

```php theme={null}
'orders' => [
    'fulfilled_statuses' => ['dispatched', 'complete'],
    'closed_statuses' => ['complete', 'cancelled', 'refunded'],
    'cancelled_statuses' => ['cancelled'],
],
```

The defaults cover the stock v1 statuses. Add any custom status strings the store uses to the appropriate list before running `php artisan lunar:upgrade`.

## Manual checklist

Rector and the data migrations cover most of the mechanical work. The following need a manual pass because they cannot be inferred automatically or depend on how the application used the feature.

<Warning>
  **Model class substitution is removed.** v1 let an application swap a Lunar model for its own subclass via `ModelManifest::replace()` and have Lunar hand back instances of that subclass everywhere. v2 removes `HasModelExtending`, the `ModelManifest` substitution API, `Model::modelClass()`, and the `Models\Contracts\*` model interfaces entirely. Lunar models are now a single concrete class.

  Rector rewrites `Model::modelClass()` calls to `Model::class`, but a subclass relying on substitution needs re-implementing through native Laravel mechanisms:

  | Previously used substitution for…     | Replace with                                                                       |
  | :------------------------------------ | :--------------------------------------------------------------------------------- |
  | An added relationship                 | `Model::resolveRelationUsing('name', fn ($model) => …)`                            |
  | An added method                       | A model macro via `Model::macro(...)`                                              |
  | An added column                       | Nothing extra — a migrated column is already a readable/writable attribute         |
  | A cast or accessor on an added column | `Model::addCasts(['column' => CastClass::class])`                                  |
  | A global query constraint             | `Model::addGlobalScope(...)`                                                       |
  | A lifecycle hook                      | `Model::observe(...)` or `Event::listen('eloquent.*: Class', ...)`                 |
  | Overridden behaviour (not just data)  | Bind the relevant action contract to a custom implementation in a service provider |
</Warning>

* **Untyped `->purchasable` accesses.** The `RenamePropertyRector` rule that renames `ProductVariant::$purchasable` to `$selling_policy` is class-scoped, so it only rewrites expressions Rector can type as `ProductVariant`. A fully untyped `$model->purchasable` is not rewritten and needs manual review — though most untyped occurrences are the unrelated `CartLine`/`OrderLine` purchasable morph or the `customer_group_product.purchasable` pivot boolean, which must not be renamed.
* **`AmountOff` discount type references.** As noted above, source code referencing `Lunar\DiscountTypes\AmountOff` needs a manual decision between `PercentageOff` and `FixedAmountOff`.
* **`ProductVariant::getTotalInventory()` for the `Always` selling policy.** v1 added the `backorder` allowance on top of `stock_available` for variants set to always sell. v2 returns the honest physical `stock_available` figure instead (the `Always` policy already short-circuits availability checks, so the old inflated number was never load-bearing for sell/no-sell decisions, but any code displaying or exporting the figure directly will see a lower number).
* **`Order::refund()` call sites Rector could not rewrite.** The refund rewrite only fires for exactly two or three positional, unnamed arguments on a receiver typed as `Order`. Calls already using named arguments, a different argument count, or an untyped receiver need a manual check against the new `RefundRequest` signature.
* **Removed config keys and Filament v3/v4 customizations.** Any published `config/lunar/*` files, Filament resources, or Blade views customized against v1 need reviewing against the v2 shape of the equivalent file — the upgrade package does not rewrite published assets. The `lunar.admin.order_count_statuses` key is removed with no replacement (the order navigation badge now counts open orders directly); override `OrderResource::getNavigationBadge()` for a different count.
* **Replaced extension hooks.** Two v1 admin extension hooks are removed rather than renamed, so Rector cannot rewrite them. A resource extension's `extendTable()` is replaced by a `configureTable(Table $table): Table` hook targeting the bridge table class (for example `Lunar\Filament\Tables\Currency\CurrencyTable`) instead of the resource. A list-page extension's `relationManagers()` is replaced by `getRelations()` on an extension targeting the resource itself. `extendTable()` still works when targeting a relation manager or relation page.
* **`lunar:migrate:getcandy`.** Removed in v2 with no replacement. Applications still on GetCandy-era data must run it on v1.x before upgrading further.

## Reference

* [Lunar upgrade package on GitHub](https://github.com/lunarphp/lunar/tree/2.x/packages/upgrade)
* [What's new in Lunar v2](/2.x/getting-started/overview/introduction)
