Skip to content

6.x - #4124

Draft
lukeholder wants to merge 193 commits into
5.xfrom
6.x
Draft

6.x#4124
lukeholder wants to merge 193 commits into
5.xfrom
6.x

Conversation

@lukeholder

@lukeholder lukeholder commented Sep 23, 2025

Copy link
Copy Markdown
Member

Remaining: src-yii2/src/ Migration

High-level checklist of what's left to finish migrating Commerce from the legacy Yii2 codebase (src-yii2/, craft\commerce\*) to the new Laravel codebase (src/, CraftCms\Commerce\*).

Most of src-yii2/ is already migrated (thin class_alias()/delegation/extends wrappers). What's below is what's still a full legacy implementation with no src/ equivalent, grouped by domain. Templates and JS/Vue/SCSS assets are each tracked as a single task, not itemized.

TODO

Plugin.php bootstrap migration

  • (element/widget/link-type registries, method overrides, event listeners, permissions, CP nav, resave commands, behaviors → macros, GQL/GC/exports/twig registrations) — all done
  • Follow-up bug: Stores::afterDeleteCraftSiteHandler() null-pointer on single-store installs when reassigning primary store
  • Follow-up bug: ProductTypes::getViewableProductTypeIds() unguarded $user->can(...) call when no authenticated user
  • Design gap (needs a cms-6 core change, not just Commerce): no extension point for defineFields()/defineRules() equivalent on User/Address now that behaviors are gone — primaryBillingAddressId etc. can't be validated or serialized. @rias mentioned looking into ValidationRulesResolving

Condition-rule / query-builder system (biggest remaining chunk)

Craft's condition-builder system has no src/ equivalent yet beyond CatalogPricing's own rules. Blocks several other items below (Discounts, Sales, Zones, Gateways, GQL resolvers, the corresponding Pest tests).

  • Design + build the new-system condition/condition-rule base (mirroring whatever cms-6 provides for its own elements)
  • Migrate order conditions/rules (coupon code, customer, status, totals, paid, shipping, etc. — elements/conditions/orders/*)
  • Migrate product conditions/rules (elements/conditions/products/*)
  • Migrate variant conditions/rules (elements/conditions/variants/*)
  • Migrate purchasable conditions/rules (elements/conditions/purchasables/*, excluding the already-migrated CatalogPricing ones)
  • Migrate address conditions/rules (discount/gateway/zone/postal-code — elements/conditions/addresses/*)
  • Migrate customer conditions/rules (elements/conditions/customers/*)
  • Migrate user conditions/rules (discount group — elements/conditions/users/*)
  • Migrate transfer condition (elements/conditions/transfers/TransferCondition.php)

Gateways

  • Port Dummy/Manual/MissingGateway gateway driver implementations to src/Payment/Gateway/ (only Responses/, Records/, Contracts/ exist so far)
  • Port Base/Gateway.php + Base/GatewayTrait.php base classes
  • Port helpers/PaymentForm.php

GraphQL

  • Port Product/Variant GQL types, interfaces, resolvers, input types (gql/types/, gql/interfaces/, gql/resolvers/, gql/types/input/, gql/types/generators/)
  • Port Product/Variant GQL queries (gql/queries/*)
  • Port gql/arguments/elements/{Product,Variant}.php (currently still legacy-only)
  • Port helpers/Gql.php
  • Wire up _registerGqlInterfaces()/_registerGqlQueries()/_registerRelatedToArguments() (schema-registration half) once the above lands

Transfers

  • Migrate the whole Transfers domain to src/services/Transfers.php, elements/Transfer.php, elements/db/TransferQuery.php, fieldlayoutelements/TransferManagementField.php (currently only TransferDetail model is migrated)

Element actions & field-layout elements

  • Port element actions: CopyLoadCartUrl, CreateDiscount, CreateSale, DownloadOrderPdfAction, SetDefaultVariant, UpdateOrderStatus
  • Port field-layout elements: ProductTitleField, VariantTitleField, VariantsField, UserAddressSettings, TransferManagementField, and the Purchasable*Field classes (SKU, price, stock, weight, dimensions, allowed qty, available-for-purchase, free-shipping, promotable)
  • Port fields/Products.php and fields/Variants.php (custom field types)
  • Confirm linktypes/Product.php can be deleted now that src/Catalog/LinkTypes/ProductLinkType.php covers it

Behaviors (mostly dead code — needs cleanup, not porting)

Per Group 7 findings: Site/User/Address no longer support attachBehavior(), so these are already non-functional except where Yii2 classes are genuinely still Yii2 (CraftVariable).

  • CustomerBehavior/CustomerAddressBehavior/StoreBehavior — confirm nothing external still references them, then delete (functionality already replaced by macros in Group 7)
  • CurrencyAttributeBehavior — assess whether still needed / has a Laravel-native equivalent (casts?)
  • StoreLocationBehavior — assess and migrate or delete
  • ValidateOrganizationTaxIdBehavior — migrate to the new validation/Ruleset system

Console

  • Port console/controllers/{ExampleTemplatesController,GatewaysController,PricingCatalogController,ResetDataController,TransferCustomerDataController}.php to Laravel Artisan commands (src/Console/Commands/, following the ResaveCommand pattern already used for Groups 6)
  • Delete console/Controller.php base once all controllers above are ported

Helpers

  • helpers/Cp.php
  • helpers/Currency.php
  • helpers/Locale.php / helpers/Localization.php
  • helpers/Order.php
  • helpers/ProductQuery.php
  • helpers/ProjectConfigData.php
  • helpers/Purchasable.php
  • (Gql/PaymentForm helpers listed under GraphQL/Gateways above)

Base classes / traits

  • Base/InventoryItemTrait.php, Base/InventoryLocationTrait.php
  • Base/Model.php
  • Base/Stat.php, Base/StatTrait.php, Base/StatWidgetTrait.php
  • Base/StoreTrait.php
  • Base/TaxEngineInterface.php, Base/TaxIdValidatorInterface.php, Base/ZoneInterface.php

Twig / web

  • web/twig/Extension.php — port to src/, then simplify Plugin::boot()'s Twig::registerExtension() call site
  • web/twig/CraftVariableBehavior.php — stays legacy intentionally (real Yii2 CraftVariable, not aliased); revisit only if core changes that
  • Migrate all CP asset bundles (JS/Vue/SCSS, e.g. commercecp, commerceui, inventory, catalogpricing, coupons, transfers, etc.) to the Craft 6 asset pipeline (see docs/6.x/extend/assets.md)
  • Migrate all CP Twig templates under src-yii2/templates/ to the src/ template structure/rendering approach

Data layer

  • Port the remaining loose Yii2 migrations to database/migrations/ (Laravel migration format) alongside Install.php
  • validators/CouponsValidator.php — migrate to the new Ruleset/validation system

Translations

  • Port all locale translation files to the new bulk-per-locale export format (per docs/6.x/extend/translation.md), updating Craft::t('commerce', ...) call sites to t('...', category: 'commerce') as each domain migrates

Plugin routing/variables cleanup

  • plugin/LegacyRoutingModule.php, plugin/Routes.php (remaining rules deliberately staying — re-check only if core routing changes), plugin/Variables.php

Queue jobs

  • Migrate queue/jobs/{SendEmail,ResaveProductVariants,CatalogPricing}.php to native Laravel ShouldQueue jobs, then drop the LegacyJobWrapper shim and switch craft\helpers\Queue::push() call sites to the Illuminate\Support\Facades\Queue facade

Test infrastructure — port tests-yii2/ → Pest (tests/Feature, tests/Unit)

Suggested order per the migration plan, now that Plugin::boot()/register() fire correctly under Testbench:

  • Helpers — done (Currency, Locale, Localization)
  • Stats (AverageOrderTotal, NewCustomers, RepeatCustomers, TopCustomers, TopProducts, TopProductTypes, TopPurchasables, TotalOrders, TotalOrdersByCountry, TotalRevenue, base Stat) — low coupling, do next
  • Models (Sale, TaxRate, StoreSettings, LineItem minus its mock dependency) + Adjusters (Discount, Tax)
  • Services (bulk of the list — Carts, CatalogPricing, Coupons, Customers, Discounts, Inventory, LineItems, Orders, PaymentCurrencies, Sales, ShippingCategories/Methods/Rules, Stores, TaxCategories) — port alongside whatever src/ service work is active
  • Elements (Order, Product, Variant, Donation) + Controllers (Cart, Orders, EmailPreview, ShippingRules) — widest surface, do after services
  • Blocked until the condition-rule system lands in src/: DiscountTest, VariantQueryTest, the order/product condition-rule tests, ProductResolverTest
  • Blocked until a gateway driver exists in src/: GatewaysTest
  • GQL feature test (GqlCest.phptests/Feature/Gql/) — lowest priority, needs the full GQL stack wired up
  • tests-yii2/test/{fixtures,mockclasses}/* — port ProductFixture/mock Purchasable as needed by the ports above
  • Drop: DebugPanelHelperTest (feature removed, no replacement)

Then

  • Once src-yii2/ only contains class_alias()/thin-wrapper files, collapse remaining legacy namespace shims and deprecate craft\commerce\* per the plan
  • Sweep CHANGELOG-WIP.md for completeness against the final state before release

@lukeholder
lukeholder requested a review from a team as a code owner September 23, 2025 07:42
@lukeholder
lukeholder marked this pull request as draft September 23, 2025 07:42
….5 requirements

- Rename src/ to src-yii2/ for the legacy Yii2 codebase. The new
  src/ directory will contain Laravel-based CraftCms\Commerce code,
  introduced progressively in later commits.
- Bump composer requirements to Craft 6 (craftcms/cms 6.0.0-alpha.1)
  and PHP 8.5+.
- Update phpstan, rector, and .gitignore for the new layout.

This is a structural change only — no behaviour changes.
Element and base-class signature compatibility for Craft 6's new
abstract method signatures:
- Order::getRecalculationMode() — return null safely before init() runs
- Order::getLink() — return type ?\Illuminate\Support\HtmlString
- Product/Variant/Subscription::setEagerLoadedElements() —
  use \CraftCms\Cms\Element\Data\EagerLoadPlan
- Transfer::prepareEditScreen() — return
  \CraftCms\Cms\Http\Responses\CpScreenResponse|Response
- VariantCollection::make() — variadic, matching Illuminate\Collection
- Purchasable::__unset() — add string type hint and void return
- PaymentCurrency::safeAttributes() — declare array return type

Rector pass: remove redundant /** @inheritdoc */ docblocks across
src-yii2/; add #[\Override] in src/gql/ handlers; drop deprecated
setAccessible(true) calls from tests (redundant since PHP 8.1).
The Commerce debug panel relied on craft\debug\Module (Yii2 debug
module) which no longer exists in Craft 6. Removed entirely:
- src-yii2/debug/CommercePanel.php
- src-yii2/helpers/DebugPanel.php
- src-yii2/events/CommerceDebugPanelDataEvent.php
- src-yii2/views/debug/commerce/ (detail, model, summary views)
- _registerDebugPanels() and its onInit hook from Plugin.php
- All DebugPanel::prependOrAppendModelTab() calls from 19 controllers

Also wires up craft.commerce as a macro on CraftCms\Cms\Twig\Variables\
CraftVariable so it works with the Laravel-based Twig variable layer,
and migrates Plugin to the new CraftCms\Cms\Support\Facades\Updates
facade.
Introduce Pest as the test runner for new src/ code, alongside the
existing Codeception suite (which stays in place while src-yii2/ is
still active). New tests will live under tests/Unit and tests/Feature
following Pest conventions; src-yii2/ tests stay Codeception until
their corresponding classes are migrated.

Adds:
- tests/Pest.php — Pest bootstrap
- tests/TestCase.php, tests/UnitTestCase.php — base classes
- tests/Support/DatabaseLock.php — concurrency helper for parallel runs
- testbench.yaml — Orchestra Testbench config
- phpunit.xml.dist — Pest/PHPUnit config

(Composer dependency bumps for Pest/Testbench are folded into the
bootstrap commit.)
Move the dependency-free constants/enums to the new src/ tree first;
every later stage can then import from CraftCms\Commerce\.

New locations:
- craft\commerce\db\Table → CraftCms\Commerce\Database\Table
- craft\commerce\enums\InventoryTransactionType → CraftCms\Commerce\Inventory\Enums\InventoryTransactionType
- craft\commerce\enums\InventoryUpdateQuantityType → CraftCms\Commerce\Inventory\Enums\InventoryUpdateQuantityType
- craft\commerce\enums\LineItemType → CraftCms\Commerce\Order\LineItem\Enums\LineItemType
- craft\commerce\enums\TransferStatusType → CraftCms\Commerce\Transfer\Enums\TransferStatusType

Legacy classes become class_alias stubs that point at the new
locations, preserving backwards compatibility for existing imports.
Move the 11 plugin contracts to domain-organized Contracts/ namespaces.
With Stage 1 enums and these interfaces in place, the rest of the
migration can implement against the new types without touching the
old craft\commerce\base\* paths.

New locations:
- craft\commerce\base\AdjusterInterface → CraftCms\Commerce\Order\Adjuster\Contracts\AdjusterInterface
- craft\commerce\base\CatalogPricingConditionRuleInterface → CraftCms\Commerce\CatalogPricing\Contracts\CatalogPricingConditionRuleInterface
- craft\commerce\base\GatewayInterface → CraftCms\Commerce\Payment\Gateway\Contracts\GatewayInterface
- craft\commerce\base\HasStoreInterface → CraftCms\Commerce\Store\Contracts\HasStoreInterface
- craft\commerce\base\InventoryMovementInterface → CraftCms\Commerce\Inventory\Contracts\InventoryMovementInterface
- craft\commerce\base\PlanInterface → CraftCms\Commerce\Subscription\Contracts\PlanInterface
- craft\commerce\base\PurchasableInterface → CraftCms\Commerce\Purchasable\Contracts\PurchasableInterface
- craft\commerce\base\RequestResponseInterface → CraftCms\Commerce\Payment\Gateway\Contracts\RequestResponseInterface
- craft\commerce\base\ShippingMethodInterface → CraftCms\Commerce\Shipping\Contracts\ShippingMethodInterface
- craft\commerce\base\ShippingRuleInterface → CraftCms\Commerce\Shipping\Contracts\ShippingRuleInterface
- craft\commerce\base\StatInterface → CraftCms\Commerce\Stats\Contracts\StatInterface

Legacy interfaces become class_alias stubs.
Move all 56 event classes from craft\commerce\events into the new
domain-organized CraftCms\Commerce\*\Events namespaces. Event classes
adopt PHP 8 constructor property promotion for clean, typed initialization.

New locations group events by domain:
- CraftCms\Commerce\Catalog\Events
- CraftCms\Commerce\Email\Events
- CraftCms\Commerce\Inventory\Events
- CraftCms\Commerce\Order\Events
- CraftCms\Commerce\Payment\Events
- CraftCms\Commerce\Pdf\Events
- CraftCms\Commerce\Promotion\Events
- CraftCms\Commerce\Purchasable\Events
- CraftCms\Commerce\Report\Events
- CraftCms\Commerce\Shipping\Events
- CraftCms\Commerce\Store\Events
- CraftCms\Commerce\Subscription\Events
- CraftCms\Commerce\Tax\Events

Cancelable events (previously extending craft\events\CancelableEvent)
now use the CraftCms\Cms\Shared\Concerns\ValidatableEvent trait.

Legacy craft\commerce\events\* classes are replaced with class_alias
stubs pointing at the new classes.
Move the 11 helper classes from craft\commerce\helpers to
CraftCms\Commerce\Helpers, swapping internal Craft/Yii static helper
calls for CraftCms\Cms\* and Laravel equivalents (Url, Cp, Json,
StringHelper, etc.).

New locations:
- CraftCms\Commerce\Helpers\Cp
- CraftCms\Commerce\Helpers\Currency
- CraftCms\Commerce\Helpers\Gql
- CraftCms\Commerce\Helpers\LineItem
- CraftCms\Commerce\Helpers\Locale
- CraftCms\Commerce\Helpers\Localization
- CraftCms\Commerce\Helpers\Order
- CraftCms\Commerce\Helpers\PaymentForm
- CraftCms\Commerce\Helpers\ProductQuery
- CraftCms\Commerce\Helpers\ProjectConfigData
- CraftCms\Commerce\Helpers\Purchasable

class_alias stubs in src-yii2/helpers/ are deferred until the
services that depend on these helpers are migrated; the old
craft\commerce\helpers\* paths still work via the Yii2 autoloader.
Migrate the simplest models (scalar properties, no element ties)
from craft\commerce\models to domain-organized classes under src/.
New classes extend CraftCms\Cms\Component\Component and use
getRules() with Laravel validation syntax instead of Yii2's
defineRules().

Models:
- craft\commerce\models\Coupon → CraftCms\Commerce\Promotion\Models\Coupon
- craft\commerce\models\LineItemStatus → CraftCms\Commerce\Order\Models\LineItemStatus
- craft\commerce\models\PaymentCurrency → CraftCms\Commerce\Payment\Models\PaymentCurrency
- craft\commerce\models\PurchasableStore → CraftCms\Commerce\Purchasable\Models\PurchasableStore
- craft\commerce\models\Settings → CraftCms\Commerce\Settings
- craft\commerce\models\ShippingCategory → CraftCms\Commerce\Shipping\Models\ShippingCategory
- craft\commerce\models\TaxCategory → CraftCms\Commerce\Tax\Models\TaxCategory

Subscription/payment forms and gateway response models:
- craft\commerce\models\subscriptions\CancelSubscriptionForm → CraftCms\Commerce\Subscription\Forms\CancelSubscriptionForm
- craft\commerce\models\subscriptions\SubscriptionForm → CraftCms\Commerce\Subscription\Forms\SubscriptionForm
- craft\commerce\models\subscriptions\SwitchPlansForm → CraftCms\Commerce\Subscription\Forms\SwitchPlansForm
- craft\commerce\models\subscriptions\SubscriptionPayment → CraftCms\Commerce\Subscription\Models\SubscriptionPayment
- craft\commerce\models\responses\Dummy → CraftCms\Commerce\Payment\Gateway\Responses\Dummy
- craft\commerce\models\responses\Manual → CraftCms\Commerce\Payment\Gateway\Responses\Manual
- craft\commerce\models\responses\DummySubscriptionResponse → CraftCms\Commerce\Subscription\Responses\DummySubscriptionResponse

Legacy classes become class_alias stubs.
Models with lazy-loaded relations (typically via getter methods that
call into a service to fetch related models) move into domain-organized
classes under src/.

Models:
- craft\commerce\models\OrderNotice → CraftCms\Commerce\Order\Models\OrderNotice
- craft\commerce\models\OrderHistory → CraftCms\Commerce\Order\Models\OrderHistory
- craft\commerce\models\SiteStore → CraftCms\Commerce\Store\Models\SiteStore
- craft\commerce\models\ShippingRuleCategory → CraftCms\Commerce\Shipping\Models\ShippingRuleCategory

Payment forms:
- craft\commerce\models\payments\BasePaymentForm → CraftCms\Commerce\Payment\Forms\BasePaymentForm
- craft\commerce\models\payments\OffsitePaymentForm → CraftCms\Commerce\Payment\Forms\OffsitePaymentForm
- craft\commerce\models\payments\CreditCardPaymentForm → CraftCms\Commerce\Payment\Forms\CreditCardPaymentForm
- craft\commerce\models\payments\DummyPaymentForm → CraftCms\Commerce\Payment\Forms\DummyPaymentForm

CreditCardPaymentForm's Luhn check is converted from a Yii2 method
validator to a Laravel closure rule in getRules(); setAttributes() now
overrides the Validates trait's method for expiry parsing.

Legacy classes become class_alias stubs.
adjustments, and inventory movement models

Stage 5c — inventory items, catalog/product type sites, transfer
details:
- ProductTypeSite → CraftCms\Commerce\Catalog\Models\ProductTypeSite
- InventoryItem → CraftCms\Commerce\Inventory\Models\InventoryItem
- InventoryFulfillmentLevel → CraftCms\Commerce\Inventory\Models\InventoryFulfillmentLevel
- InventoryLevel → CraftCms\Commerce\Inventory\Models\InventoryLevel
- InventoryTransaction → CraftCms\Commerce\Inventory\Models\InventoryTransaction
- UpdateInventoryLevel → CraftCms\Commerce\Inventory\Models\UpdateInventoryLevel
- UpdateInventoryLevelInTransfer → CraftCms\Commerce\Inventory\Models\UpdateInventoryLevelInTransfer
- TransferDetail → CraftCms\Commerce\Transfer\Models\TransferDetail

Stage 5d — email, PDF, zones, adjustments, inventory movements,
plus shared infrastructure:
- Email → CraftCms\Commerce\Email\Models\Email
- Pdf → CraftCms\Commerce\Pdf\Models\Pdf
- OrderAdjustment → CraftCms\Commerce\Order\Models\OrderAdjustment
- TaxRate → CraftCms\Commerce\Tax\Models\TaxRate
- ShippingAddressZone → CraftCms\Commerce\Shipping\Models\ShippingAddressZone
- TaxAddressZone → CraftCms\Commerce\Tax\Models\TaxAddressZone
- base\Zone (abstract) → CraftCms\Commerce\Base\Zone
- base\InventoryMovement (abstract) + 6 InventoryMovement subclasses + DeactivateInventoryLocation → CraftCms\Commerce\Inventory\Models\

Adds CraftCms\Commerce\Store\Concerns\StoreTrait — shared storeId
helper for store-aware models. Updates InventoryItemTrait,
InventoryLocationTrait, and InventoryMovementInterface to reference
new namespaces. Old src-yii2/Base/StoreTrait marked @deprecated.

Legacy classes become class_alias stubs.
…logPricing

Four models from craft\commerce\models move to domain-organized classes
under src/:
- OrderStatus → CraftCms\Commerce\Order\Models\OrderStatus
- PaymentSource → CraftCms\Commerce\Payment\Models\PaymentSource
- InventoryLocation → CraftCms\Commerce\Inventory\Models\InventoryLocation
- CatalogPricing → CraftCms\Commerce\Catalog\Models\CatalogPricing

Key swaps:
- Cp::statusLabelHtml() → app(CraftCms\Cms\Cp\Html\StatusHtml::class)
- Html::encode() → htmlspecialchars(..., ENT_QUOTES | ENT_SUBSTITUTE)
- Db::uidsByIds() → DB::table(...)->uidsByIds() (Laravel query builder macro)
- craft\elements\Address → CraftCms\Cms\Address\Elements\Address
- craft\base\* contracts → CraftCms\Cms\Component\Contracts\*
- Craft::$app->getUser()->getIdentity()?->can() → request()->craftUser()?->can()
- HandleValidator → inline regex + reserved-word closure
- CurrencyAttributeBehavior dropped (Yii2-only)
- Craft::$app->getDeprecator() → CraftCms\Cms\Support\Facades\Deprecator

Legacy classes become class_alias stubs.
4 supporting interfaces

Three models:
- Sale → CraftCms\Commerce\Promotion\Models\Sale
- StoreSettings → CraftCms\Commerce\Store\Models\StoreSettings
- Transaction → CraftCms\Commerce\Payment\Models\Transaction

Four interfaces previously left at the legacy base/ path:
- base\TaxIdValidatorInterface → CraftCms\Commerce\Tax\Contracts\TaxIdValidatorInterface
- base\TaxEngineInterface → CraftCms\Commerce\Tax\Contracts\TaxEngineInterface
- base\ZoneInterface → CraftCms\Commerce\Base\ZoneInterface
- base\SubscriptionResponseInterface → CraftCms\Commerce\Subscription\Contracts\SubscriptionResponseInterface

Key swaps:
- new Query()->select()->from()->leftJoin()->where()->column()
  → DB::table()->leftJoin()->where()->pluck()->all() (Sale)
- Craft::$app->getFormatter()->asPercent() → I18N::getFormatter()->asPercent()
- Craft::$app->getAddresses()->getCountryRepository()->getList(language)
  → Addresses::getCountryRepository()->getList(app()->getLocale())
- Address::findOne($id) → Elements::getElementById($id, Address::class)
- Craft::$app->getElements()->saveElement() → Elements::saveElement()
- Conditions::createCondition() facade
- Transaction's hash generation moved from init() to __construct
- CurrencyAttributeBehavior dropped (Yii2-only)

Legacy classes become class_alias stubs.
The full shipping method class hierarchy moves to src/:
- craft\commerce\base\ShippingMethod (abstract) → CraftCms\Commerce\Shipping\Models\BaseShippingMethod
- craft\commerce\models\ShippingMethod → CraftCms\Commerce\Shipping\Models\ShippingMethod
- craft\commerce\models\ShippingMethodOption → CraftCms\Commerce\Shipping\Models\ShippingMethodOption

Key swaps:
- craft\base\Chippable/Colorable/Iconic/Statusable → CraftCms\Cms\Component\Contracts\*
- craft\enums\Color → CraftCms\Cms\Shared\Enums\Color
- NotImplementedException → \BadMethodCallException (inline)
- UniqueValidator → Rule::unique() (Laravel validation)
- AttributeTypecastBehavior dropped (Yii2-only)
- CurrencyAttributeBehavior / currencyAttributes() / getCurrency()
  dropped from ShippingMethodOption (Yii2-only)
- Json::decodeIfJson() → CraftCms\Cms\Support\Json::decodeIfJson()
- Conditions::createCondition() facade

ShippingMethodOrderCondition and ShippingMethodCustomerCondition
remain on the old craft\commerce\elements\conditions\* paths until
their dependencies are migrated.

Legacy classes become class_alias stubs.
craft\commerce\models\ShippingRule → CraftCms\Commerce\Shipping\Models\ShippingRule.

Key swaps:
- Json::decodeIfJson() → CraftCms\Cms\Support\Json::decodeIfJson()
- Conditions::createCondition() facade
- Yii2 attribute-based closure validators (addError()) → Laravel
  closures with $fail() pattern
- validateShippingRuleCategories method validator → inline closure in
  getRules() using the Validates trait's addModelErrors() helper
- $this->getAttributes() in getOptions() → $this->toArray()

ShippingRuleOrderCondition and ShippingRuleCustomerCondition stay on
the old craft\commerce\elements\conditions\* paths until their
dependencies are migrated. Order and ShippingRuleCategory record
references also stay on the old paths.

Legacy class becomes a class_alias stub.
CatalogPricingRule moves to src/:
- craft\commerce\models\CatalogPricingRule → CraftCms\Commerce\Catalog\Models\CatalogPricingRule

Key swaps:
- craft\base\Model → CraftCms\Cms\Component\Component
- Yii2 defineRules() → Laravel getRules() with Rule::in() for 'apply'
- I18N::getFormatter()->asPercent() / Conditions::createCondition() facades
- CraftCms\Cms\Support\Json::decodeIfJson()

Post-Stage 5 fixes:
- Fix infinite recursion in ShippingMethodOrderCondition,
  ShippingRuleOrderCondition, DiscountOrderCondition config() methods.
  $this->toArray(['storeId']) was calling getObjectVars() which triggers
  the PHP 8.4 $config property hook getter, recursing into config().
  Replaced with explicit ['storeId' => $this->storeId].

Also adds CraftCms\Commerce\Base\EnumHelpersTrait (companion to the
Stage 1 enums, missed at the time) and the WIP changelog covering
stages 1–5.

Legacy CatalogPricingRule becomes a class_alias stub.
- Switch ConditionRule::modifyQuery() param types from
  craft\elements\db\ElementQueryInterface | yii\db\QueryInterface to
  Illuminate\Contracts\Database\Query\Builder, matching the Laravel
  condition rule signature in 6.x.
- Affected: DiscountedItemSubtotalConditionRule,
  OrderCurrencyValuesAttributeConditionRule, OrderSiteConditionRule,
  ShippingMethodConditionRule.
- src-yii2/services/Taxes.php: minor adjustment alongside the above.
- Templates: guard discounts/sales _edit.twig against a crash when the
  shippingrulecategories table doesn't exist yet (Stage 1/2 setups).
Settings already lived at CraftCms\Commerce\Settings from Stage 5a, but
the legacy src-yii2/models/Settings.php still held the full Yii2
implementation. Now:
- src-yii2/models/Settings.php replaced with a class_alias stub
- src/Settings.php gains the setAttributes() override from the legacy
  class so deprecated Commerce-4 settings keys are silently stripped
  (preserves backward compatibility for project configs that still
  reference orderPdfFilenameFormat, autoSetNewCartAddresses, etc.).

DummyPlan moves to CraftCms\Commerce\Subscription\Models\DummyPlan.
Still extends the unmigrated craft\commerce\base\Plan; switches to the
new CraftCms\Commerce\Subscription\Contracts\PlanInterface argument
type.

Legacy classes become class_alias stubs.
craft\commerce\models\Store → CraftCms\Commerce\Store\Models\Store.

Key swaps:
- craft\base\Model → CraftCms\Cms\Component\Component
- craft\helpers\App::parseEnv() → CraftCms\Cms\Support\Env::parse()
- craft\helpers\App::parseBooleanEnv() → CraftCms\Cms\Support\Env::parseBoolean()
- craft\helpers\UrlHelper::cpUrl() → CraftCms\Cms\Support\Url::cpUrl()
- craft\models\Site → CraftCms\Cms\Site\Data\Site
- UniqueValidator → Rule::unique() on the stores table, ignoring the
  current record id
- Yii2 attribute-based closure validator for currency-change-when-
  orders-exist → Laravel closure rule with $fail() pattern
- Craft::$app->getDeprecator() → CraftCms\Cms\Support\Facades\Deprecator
- Craft::t('commerce', ...) → global t() with category
- Yii2 attributes() override (added name/settings) → fields() override
  (same purpose under the new serialization layer)
- Dropped EnvAttributeParserBehavior — the existing getXxx(bool $parse)
  pattern already handles env parsing on every accessor

ZoneAddressCondition, Order element, and the Store record stay on the
legacy craft\commerce\* paths until those are migrated.

Legacy class becomes a class_alias stub.
…iscount

Migrated craft\commerce\models\Discount → CraftCms\Commerce\Promotion\Models\Discount.

Key swaps:
- craft\base\Model → CraftCms\Cms\Component\Component
- Yii2 Query builder (relation loaders) → DB::table()->leftJoin()->pluck()->all()
- Conditions::createCondition() facade
- I18N::getFormatter()->asPercent()
- CraftCms\Cms\Support\Json::decodeIfJson()
- Yii2 defineRules() → Laravel getRules(); closure validators rewritten
  with the $fail() pattern; Rule::in() for categoryRelationshipType/appliedTo
- craft\elements\conditions\ElementConditionInterface →
  CraftCms\Cms\Element\Conditions\Contracts\ElementConditionInterface
- DiscountOrderCondition / DiscountCustomerCondition / DiscountAddressCondition,
  Order element, DiscountRecord, Coupons service retained as legacy refs

Removed 5.x-deprecated API while migrating (per CLAUDE.md guidance):
- Discount::setExcludeOnSale() / getExcludeOnSale() (use $excludeOnPromotion)
- Settings::VIEW_URI_CUSTOMERS / VIEW_URI_PROMOTIONS / VIEW_URI_SHIPPING /
  VIEW_URI_TAX constants
- Store::setCountries() / getCountries() / getCountriesList() /
  getAdministrativeAreasListByCountryCode() / getMarketAddressCondition()
  (use the equivalents on Store::getSettings())

Legacy Discount becomes a class_alias stub.
craft\commerce\services\Currencies → CraftCms\Commerce\Services\Currencies.

This is the first service migrated under the new Craft 6 pattern (see
docs/6.x/extend/services.md): services are plain auto-loadable PHP
classes marked with #[\Illuminate\Container\Attributes\Singleton] so
Laravel's container reuses the instance. No Yii2 Component inheritance
on the new class. Preferred access:

    app(\CraftCms\Commerce\Services\Currencies::class)->getTeller(...)

Legacy access stays working via the existing
`Plugin::getInstance()->getCurrencies()` route — the old service in
src-yii2/ is reduced to a thin Yii2 Component that delegates every
method to the new singleton via app(). Once all callers move to app(),
the legacy wrapper can be deleted.

Behaviour is unchanged. init() moved to __construct(). Tellers are still
cached per-iso on the singleton.
…e\Services

craft\commerce\services\PaymentCurrencies → CraftCms\Commerce\Services\PaymentCurrencies.
#[Singleton] on the new class, plain PHP. Yii2 component declaration
in Plugin.php stays — the legacy wrapper at src-yii2/services/
PaymentCurrencies.php now delegates every method to the new singleton
via app().

Key swaps inside the new service:
- Yii2 craft\db\Query → Laravel DB::table()->select()->where()->get()
- Db::update(...) → DB::table()->where(...)->update(...)
- Craft::createObject(['class' => ..., 'attributes' => ...])
  → new PaymentCurrency((array) $row)
- craft\commerce\errors\CurrencyException → \RuntimeException (the
  Yii2 base exception isn't visible to phpstan / no longer relevant
  in the Laravel layer)

Removed convertCurrency() from the new service — deprecated in 5.0.0.
Kept on the legacy wrapper only, so the two unmigrated src-yii2/
callers (Order element, OrdersController) keep working; they'll move
to convert()/convertAmount() when their classes migrate.
…rvices

craft\commerce\services\TaxCategories → CraftCms\Commerce\Services\TaxCategories.

Key swaps:
- Yii2 Query → Laravel DB::table() + Schema facade for the icon/color
  column-exists check (replaces $db->getSchema()->getTableSchema()->getColumn())
- ArrayHelper::firstWhere/firstValue/map/getColumn → collect()->firstWhere/
  first/mapWithKeys/pluck()
- Craft::$app->getDb()->createCommand()->delete()/insert() → DB::table()->
  where()->delete() / DB::table()->insert()
- Craft::$app->getQueue()->push(new ResaveElements([...])) (Yii2 array-config
  job) → dispatch(new ResaveElements(elementType: ..., criteria: ...))
  (CraftCms\Cms\Element\Jobs\ResaveElements)
- Yii2 InvalidConfigException for "must have one default" → \RuntimeException

TaxCategoryRecord and softDelete() retained — the Yii2 record stays
until the records layer migrates.

Legacy class becomes a delegating Yii2 Component wrapper.
…ce\Services

craft\commerce\services\ShippingCategories → CraftCms\Commerce\Services\ShippingCategories.

Same patterns as TaxCategories:
- #[Singleton] on the new class, Plugin's Yii2 component declaration
  delegates via the wrapper at src-yii2/services/
- Yii2 Query → Laravel DB::table()/Schema facade
- ArrayHelper utilities → Collection / native array_diff
- Craft::$app->getQueue()->push(new ResaveElements([...])) →
  dispatch(new ResaveElements(elementType: ..., criteria: ...,
  updateSearchIndex: false))
- InvalidConfigException for "must have one default" → \RuntimeException

ShippingCategoryRecord, softDelete(), and the legacy Variant element
are retained. The purchasable-store fallback logic on product-type
removal (assigns affected purchasables to the default shipping
category) is preserved exactly.

Legacy class becomes a delegating Yii2 Component wrapper.
craft\commerce\services\TaxZones → CraftCms\Commerce\Services\TaxZones.

Same pattern as the previous 6a services: #[Singleton] new class,
delegating Yii2 Component wrapper at src-yii2/services/.

Swaps:
- Yii2 Query → Laravel DB::table()->select()->orderBy()
- Craft::createObject(['class' => TaxAddressZone, 'attributes' => $row])
  → new TaxAddressZone((array) $row)
- yii\base\Exception ("zone not found") → \RuntimeException

TaxZoneRecord, ZoneAddressCondition, and the legacy Plugin::getInstance()
->getStores()->getCurrentStore() lookup retained.
…Services

craft\commerce\services\ShippingZones → CraftCms\Commerce\Services\ShippingZones.

Mirrors the TaxZones migration: Yii2 Query → Laravel DB::table(),
Craft::createObject → new ShippingAddressZone((array) $row),
yii\base\Exception → \RuntimeException. Legacy class becomes a
delegating Yii2 Component wrapper.

This finishes Stage 6a (Store config services): Currencies,
PaymentCurrencies, TaxCategories, ShippingCategories, TaxZones,
ShippingZones — all behind app(CraftCms\Commerce\Services\* ::class).
Adds a "Stage 6a" section covering Currencies, PaymentCurrencies,
TaxCategories, ShippingCategories, TaxZones, ShippingZones — all six
now under CraftCms\Commerce\Services and accessed via app(). Captures
the cross-cutting swaps applied (Yii2 Query → DB::table(), createObject
→ new, ArrayHelper → Collection, etc.).

Also records that PaymentCurrencies::convertCurrency() (deprecated in
5.0.0) was dropped from the new service but kept on the legacy wrapper
for the two unmigrated src-yii2/ callers.
…block conflict

The macro closures in registerCustomerMacros()/registerCustomerAddressMacros()
call other macros registered on the same class (e.g. getPrimaryBillingAddress()
calling getPrimaryBillingAddressId()) - PHPStan can't trace Macroable
dispatch even within its own registration site.

The three orderBy() "arguments.count" errors are a genuine cms-6 issue:
ElementQuery's class docblock has `@method static orderBy($column)` (1 param)
which conflicts with its own real `orderBy($column, $direction = 'asc')`
method (2 params) - PHPStan prioritizes the stale docblock tag. Not
something to fix here since ElementQuery is owned by cms-6; suppressed
with an explanation instead.
…me() false-path suppressions

Real fix: $number = str_replace(...) then $number-- decremented a plain
string rather than a numeric type - cast to (int) first.

Every DateTimeHelper::toDateTime()/strtotime() result flowing through this
file only fails for an unparseable input, and every input here is either a
hardcoded relative-date string ('first day of this month', etc.) or an
already-validated Y-m-d string - the DateTimeInterface|false union is
real per the signature but practically unreachable here. Suppressed rather
than adding defensive checks for a case that can't happen.

groupByRaw()/orderByRaw() SQL fragments are built entirely from server-side
driver/timezone detection in getChartQueryOptionsByInterval(), never from
user input, so PHPStan's literal-string requirement (an anti-SQL-injection
check) doesn't apply here either - suppressed with the same reasoning.
…ro/facade suppressions

Real bug: `use Illuminate\Http\Response as JsonResponseAlias;` was importing
the wrong class - JsonResponse does NOT extend Response (they're siblings,
both extending different Symfony HttpFoundation base classes separately),
so filter()/prices() were declared to return the aliased Response while
actually returning response()->json(...) (a real JsonResponse). Fixed the
import and both return types to the real Illuminate\Http\JsonResponse.

Everything else is the established Site::getStore() macro suppression
pattern, plus a @var CatalogPricingCondition assertion where
Conditions::createCondition() loses its narrow type through the facade's
__callStatic dispatch (same category as the Elements::duplicateElement()
facade-narrowing issue fixed earlier in Order.php).
…er-input date bug plus established patterns

Real bug: ProductsController::create() assigned
DateTimeHelper::toDateTime($request->input('postDate'/'expiryDate')) directly
to Product's ?DateTime properties - unlike the Stat.php/Order.php cases fixed
earlier, these inputs are genuinely user-controllable (CP form fields), so a
malformed date string would return false and, under strict_types, throw a
TypeError. Added a real fallback (now() / null) and converted to a concrete
DateTime instance.

Donation.php had the same $validator->addError($lineItem, ...) bug already
found and fixed in Purchasable.php (LineItem has no addError() method) -
switched to $lineItem->errors()->add(). Also added the missing
Donation model datetime casts (same gap as Order's model, fixed earlier)
and converted afterSave()'s date assignments through Carbon::instance().

LineItem was missing @Property float $price (writable, has get/setPrice())
entirely - added.

Everything else is abort_unless() truthy-to-explicit-null-check cleanup and
the established Gateway class_alias-chain suppressions.
…suppressions

Real bug (severe): Helpers/Cp.php, Currency.php, and Purchasable.php all
imported CraftCms\Cms\Cp\Cp - a tiny unrelated class with only config()/
vite() static methods. It has nothing to do with form-field HTML. Every
call in these three files (fieldHtml, textHtml, moneyInputHtml,
lightswitchHtml, renderTemplate) would fatal with "undefined static method"
- this is the entire Commerce CP UI for tax zones, tax categories, shipping
categories, inventory locations, money inputs, and price tables. Fixed the
imports to the legacy craft\helpers\Cp bridge, which has all of these
methods under the same names.

Real bug: request()->craftUser()?->id / currentUser()?->id read a
nonexistent property on the CraftUser contract (only getCraftUserId()
exists) in Inventory.php, InventoryLocations.php, and
OrderStatusesController.php - same category of bug already found and fixed
in OrdersController.php, silently writing null instead of the real user ID.

Real fix: LineItemStatuses::handleArchivedLineItemStatus() formatted a date
to a DB string via CraftDb::prepareDateForDb() and assigned it to a
Carbon-cast property - simplified to Carbon::now() directly, avoiding an
unnecessary format/reparse round-trip.

Real fix: Inventory::reduceInventoryForOrder() (executeInventoryMovements
loop) called hasInventory()/getInventoryLevels(), both specific to the
concrete Purchasable class, on a PurchasableInterface-typed variable with
no instanceof guard - same category of bug already fixed in
Helpers/Order.php and Donation.php.

Everything else is the established Site::getStore() macro suppression,
TODO-marked trigger() event-bridging suppressions, OrderAdjustment missing
its $sourceSnapshot docblock, and two provably-dead null checks
(Discount::$baseDiscount is a non-nullable float, InventoryMovementCollection
already yields the concrete InventoryMovement type without needing a
downcast @var).
Real bug: CatalogPricingRulesController's edit screen called ->tabs([...])
with a plain numeric-indexed list of tab configs, but CpScreenResponse::tabs()
requires string keys matching each tab's container id (confirmed against
every other controller's ->tabs() call in this codebase, e.g.
ProductTypesController) - this would have rendered broken/non-functional
tabs in the CP. Fixed to use 'rule'/'conditions'/'actions' keys.

Same Carbon::instance()/DateTime::createFromInterface() conversions as the
Order/Donation fixes earlier, now for CatalogPricingRule, Discount, Sale,
and Gateway records/models, both at the record-save sites (domain model's
plain ?DateTime -> Carbon-cast record property) and the controller
user-input sites (DateTimeHelper::toDateTime() can genuinely return false
for a malformed date).

Two LineItemStatuses/Gateways-style CraftDb::prepareDateForDb(new DateTime())
"archive now" assignments simplified to Carbon::now() directly.

Added the missing CatalogPricingRule::$applyAmountAsPercent/$applyAmountAsFlat
docblock. Simplified one more redundant elseif branch in Discounts.php
(same provably-redundant-given-preceding-if pattern fixed in Order.php
earlier), and removed one incorrect Gateway suppression that turned out to
be unnecessary once the return type was already narrow enough.
…other real bugs

Real bug (severe): inventoryLevelsTableData() built its query entirely with
Yii2 query-builder syntax (andWhere() with nested condition arrays like
['not', [...]] and ['or', [...], [...]], [[bracket]]-quoted columns,
addGroupBy(), addOrderBy()) against a genuine Laravel Illuminate\Database\
Query\Builder returned by Inventory::getInventoryLevelQuery(). None of
that syntax exists on Laravel's builder - this is the main data-table
endpoint for the Inventory CP screen, so it would have fatally errored on
every page load. Converted to real Laravel builder calls (where(), a
where() closure for the OR/LIKE search, whereNotNull(), groupBy(), orderBy(),
a proper leftJoin() with column args), replaced ->all() with ->get() and the
limit(null)/offset(null) reset trick with getCountForPagination(), and fixed
the resulting Collection-vs-array usage (array_column -> pluck/filter/unique).

Also fixed two CpModalResponse-returning modal actions
(editUpdateLevelsModal/editMovementModal) that had an early "live preview"
return path returning a real JsonResponse but were typed to return only
CpModalResponse/Response - widened to CpModalResponse|JsonResponse.

InventoryLocationsController: setAttributes() called with a second `false`
"safeOnly" argument that no longer exists on the new Validatable contract -
confirmed the new setAttributes() always behaves like the old safeOnly=false
mode, so dropping the arg preserves behavior. Also fixed a facade-narrowing
Address lookup to handle a genuinely-possible null (stale/deleted address id)
instead of asserting non-null.

abort_unless()/abort_if() truthy-check cleanup across CartController,
DownloadsController, ProductTypesController, and TransfersController
(array|string|null / object|null params made explicit), plus two provably-dead
checks in CartController (LineItem::$qty is a non-nullable int;
returnCart() never actually returns null).
…ro, and legacy-property errors

Catalog/CatalogPricing:
- CatalogPricing model: reference real CatalogPricingRule class instead of
  legacy craft\commerce\models\CatalogPricingRule
- Products/Variants: assert narrow Product/Variant type after Elements
  facade's getElementById() call, which loses generic type via __callStatic
- VariantQuery: suppress owner()/primaryOwner() param-type mismatch against
  NestedElementQueryInterface's stricter signature
- CatalogPricing::getCatalogPricesPageInfo(): use getCountForPagination()
  instead of get()->count() to avoid fetching unnecessary rows
- Add @var assertions for Conditions::createCondition() facade calls

Controllers:
- ProductQuery::cleanseQueryCriteria(): drop unnecessary nullsafe on
  request(), which always resolves the bound Request singleton
- PaymentsController: suppress legacy GatewayTrait getIsFrontendEnabled()/
  $handle access via the class_alias chain, drop stray setAttributes()
  2nd arg, initialize $error before conditional assignment, fix
  abort_unless() bool coercion
- EmailsController: use '' instead of null as an array key (PHP already
  coerces null keys to ''), call Email's setTo()/setBcc()/setCc() setters
  instead of nonexistent magic properties
- StoreManagementController: resolve Site::getStore() macro result once
  into a typed local instead of repeating nullable/macro type assertions
- TaxRatesController: call TaxRate's real hasTaxIdValidators()/
  getIsEverywhere()/getTaxZone()/getTaxCategory() methods instead of
  nonexistent magic properties
- UserOrdersController/WebhooksController: suppress Macroable macro and
  class_alias chain false positives
- Order/Carts: drop invalid `false` default passed to Request::cookie()
  (expects array|string|null), compare against null instead
…— 0 errors project-wide

- LineItemStatus::get(): widen return type from ?static to ?self to match
  what LineItemStatuses::getLineItemStatusById() actually returns
- OrderQuery: remove dead is_array() check (containsPurchasables shape is
  already typed as always-array), suppress afterHydrate()'s intentional
  Collection<Order> narrowing of the interface's Collection<ElementInterface>
- Payment/Currencies::getAllCurrencies(): assert Collection<int, Currency>
  since collect() can't infer element type from ISOCurrencies' plain
  IteratorAggregate implementation
- PaymentSource/PaymentSources: suppress legacy craft\commerce\base\Gateway
  class_alias chain false positives (assign.propertyType, method.notFound)
- Purchasables::updateStoreStockCache(): guard with instanceof Purchasable
  before calling Inventory::getInventoryLevelsForPurchasable(), which only
  accepts the concrete inventory-trackable element class
- Stats/RepeatCustomers: use getCountForPagination() instead of
  get()->count() to avoid fetching unnecessary rows

Full project phpstan analyse now reports 0 errors (362 files).
Migrates all 62 legacy condition and condition-rule classes under
src-yii2/elements/conditions/{purchasables,products,variants,orders,
addresses,customers,users}/ to the new src/ codebase, mirroring cms-6's
own condition-builder framework (BaseCondition, BaseConditionRule, and
its typed base rules) exactly as CatalogPricingCondition already does.
No new base framework was needed.

- src/Purchasable/Conditions/: PurchasableConditionRule,
  PurchasableTypeConditionRule, SkuConditionRule,
  CatalogPricingRulePurchasableCategoryConditionRule,
  CatalogPricingRulePurchasableCondition. SkuConditionRule now calls
  PurchasableQuery::sku() directly instead of the Yii2 version's manual
  join-and-alias workaround.
- src/Catalog/Conditions/: ProductCondition, VariantCondition, and their
  rules (product-type, variant sku/stock/price/search/inventory-tracked,
  product/variant element-select). The variant-attribute rules now filter
  via whereIn('elements.id', ...) against VariantQuery instead of Yii2's
  array-condition subquery syntax. ProductConditionRule (variants/) is
  renamed VariantProductConditionRule to stay unambiguous now that
  Product's and Variant's rules share one flat namespace.
  ProductVariantHasUnlimitedStockConditionRule removed outright
  (deprecated since 5.0.0, already unregistered).
- src/Order/Conditions/: OrderCondition and its full rule set, including
  the shared OrderTextValuesAttributeConditionRule/
  OrderValuesAttributeConditionRule/OrderCurrencyValuesAttributeConditionRule
  bases. The currency base no longer mocks a Money field to reuse the
  money-input widget (the old craft\fields\Money hack) — it extends
  BaseNumberConditionRule directly and renders the currency-aware input
  itself. Discount/Gateway/ShippingMethod/ShippingRule order-condition
  subclasses included.
- src/Address/Conditions/: DiscountAddressCondition, ZoneAddressCondition,
  GatewayAddressCondition, PostalCodeFormulaConditionRule, extending
  cms-6's own Address\Conditions\AddressCondition.
- src/Customer/Conditions/: covers both legacy customers/ and users/
  folders (DiscountCustomerCondition, HasOrdersConditionRule,
  SignedInConditionRule, DiscountGroupConditionRule, and the
  Shipping/CatalogPricingRule customer condition variants), extending
  cms-6's User\Conditions\UserCondition/GroupConditionRule.

Every legacy src-yii2/elements/conditions/** file is replaced with a
class_alias stub pointing at its new src/ equivalent, so existing stored
condition configs (project config, DB JSON columns) keep resolving.

Transfer's condition was left untouched — its element hasn't been
migrated to src/ yet.
Swaps every remaining craft\commerce\elements\conditions\* import for its
CraftCms\Commerce\*\Conditions equivalent from the previous commit:

- Discount, ShippingRule, BaseShippingMethod: order/customer conditions
- Base/Zone, Base/ZoneInterface, StoreSettings, StoreManagementController:
  ZoneAddressCondition (shared by tax and shipping zones)
- Catalog/Models/CatalogPricingRule, CatalogPricingRulesController:
  customer/product/variant/purchasable rule-scoping conditions
- Order/Product/Variant elements: their own condition() factory methods
- src-yii2/Base/Gateway.php: order/address conditions, plus a stray
  docblock cast fixed from DiscountOrderCondition to the correct
  GatewayOrderCondition

Also drops a stale "TODO: migrate condition classes once in src/" comment
in CatalogPricingRule::getPurchasableIds(), left over from before this
migration.
Targets the handful of rules with real custom matching logic (not the
mechanical 1:1 base-rule ports), all testable without a database:

- CouponCodeConditionRule: case-insensitive EQ/NE matching
- TotalDiscountConditionRule: negated-value comparison (discounts are
  stored as negative amounts, but the rule's configured value is entered
  as positive)
- PaymentGatewayConditionRule: legacy single-value <-> values[] B/C shim
- DiscountGroupConditionRule: the custom "is in all of" operator

No condition tests existed anywhere in this codebase before this.
Added/Deprecated entries for the new CraftCms\Commerce\*\Conditions
classes across Catalog, Catalog Pricing, Customers, Orders, Payments,
Promotions, Purchasables, Shipping, Stores, and Tax, plus behavioral
notes for the consumers now wired to them (Discount, ShippingRule,
BaseShippingMethod, Zone/ZoneInterface, CatalogPricingRule, legacy
Gateway).
Ports craft\commerce\elements\Transfer, elements\db\TransferQuery,
elements\conditions\transfers\TransferCondition, services\Transfers, and
fieldlayoutelements\TransferManagementField to CraftCms\Commerce\Transfer\*,
following the same base-condition/element/query conventions established for
the rest of the condition-system migration. Legacy classes become class_alias
stubs; Transfer moves into src/Plugin.php's $elementTypes, replacing the
legacy Elements::EVENT_REGISTER_ELEMENT_TYPES bridge in src-yii2/Plugin.php.
Rewires TransfersController, SettingsController, and HasServices to reference
the new classes directly, and adds Pest unit tests for the element's
pure-logic methods (detail totals, status transitions, location validation).
…s to Laravel in src/

Ports craft\commerce\elements\actions\{CopyLoadCartUrl,CreateDiscount,CreateSale,
DownloadOrderPdfAction,SetDefaultVariant,UpdateOrderStatus}, the remaining
fieldlayoutelements\* classes (ProductTitleField, VariantTitleField, VariantsField,
UserAddressSettings, and the 8 Purchasable*Field classes), and the fields\Products/
Variants custom field types to CraftCms\Commerce\*, following the established
element-action/field-layout-element/relation-field base classes. Legacy classes
become class_alias stubs; Product/Variant/UpdateOrderStatus/SetDefaultVariant move
into src/Plugin.php's $elementTypes/$fieldTypes arrays, replacing the legacy
Fields::EVENT_REGISTER_FIELD_TYPES bridge in src-yii2/Plugin.php.

Also deletes src-yii2/linktypes/Product.php, which was fully superseded by the
already-migrated and already-registered CraftCms\Commerce\Catalog\LinkTypes\ProductLinkType
and had no remaining references anywhere in the codebase.
Replaces the manual per-driver SQL string building in Stat::getChartQueryOptionsByInterval()
(MySQL CONVERT_TZ/PostgreSQL AT TIME ZONE/SQLite passthrough for timezone conversion, then
EXTRACT/strftime/DATE() for day and month grouping keys) and the selectRaw()/groupByRaw()/
orderByRaw()/DB::raw() calls scattered across the individual Stats classes (SUM, COUNT, IFNULL
vs COALESCE, CASE WHEN) with typed, composable query builder expressions.

Adds four small expression classes under src/Support/Expressions/ (LocalTimestamp, DateOnly,
MonthKey, Round) following the same driver-detection pattern as tpetry/laravel-query-expressions,
for the SQL constructs the package doesn't provide (calendar month/day truncation with timezone
conversion) — used together with the package's own Sum, Count, Coalesce, CaseGroup/CaseRule,
Alias, and Value expressions everywhere else.

Behavior-preserving: all Stats feature tests pass except the one pre-existing, unrelated
TotalOrdersTest failure that predates this change.
…ling cms-6 checkout

phpstan.neon pointed scanFiles/stubFiles at ../cms-6/yii2-adapter/..., which only resolves
when cms-6 is checked out as a sibling directory (true in the local ddev dev setup, where
/tmp/packages/cms-6 and /tmp/packages/commerce-6 are siblings, but not in CI or any standalone
checkout of this repo). Point them at vendor/craftcms/yii2-adapter/... instead, which composer
already installs identically everywhere and matches how every other CI job in this repo already
resolves craftcms/cms and craftcms/yii2-adapter.

Also pin parallel.maximumNumberOfProcesses to 1. Discovered while testing the above: PHPStan's
worker processes each independently reflect on classes reached through class_alias() chains
(the legacy src-yii2/ -> src/ stubs), and depending on which worker resolves a given alias
target first, whether it can trace the chain is non-deterministic between otherwise-identical
runs — surfacing as argument.type/method.notFound errors, or stale @phpstan-ignore-next-line
suppressions, that flip between two or three call sites in Payment/Gateway/Gateways.php and
Payment/Transactions.php from run to run. Single-process analysis is slower but removes the
race entirely; confirmed 3 consecutive clean runs locally after pinning it.
Completes the Gateways domain migration: Gateway.php (inlining GatewayTrait),
the three concrete gateway types, and the helpers/PaymentForm.php stub, with
all consumers rewired to the new namespace. Fixes the CI phpstan failures
caused by craft\commerce\base\Gateway still being a real, non-aliased class
with legacy type-hints that mismatched GatewayInterface, and removes ~19
now-stale @phpstan-ignore-next-line suppressions this made provable.
2.4.5's PHPStanContainerMemento reflected into a private $container property
on PHPStan\Parser\RichParser that no longer exists as of PHPStan 2.2.x,
crashing rector process with MissingPrivatePropertyException. 2.6.3 requires
phpstan/phpstan ^2.2.6, which includes the matching container-compatibility
fix (rectorphp/rector#8208).
PR #4124 (head 6.x, base 5.x) stays open for the whole migration, so
every push already triggers a pull_request run. The push: branches: [6.x]
trigger was firing a second, redundant full CI run for the same commit.
Stores::afterDeleteCraftSiteHandler(): on a single-store install, reassigning
the primary store to "another" store after the last site is deleted found no
other store (firstWhere() returned null) and then dereferenced it. Skip the
reassignment when there's nothing to promote.

ProductTypes::getViewableProductTypeIds()/getCreatableProductTypeIds(): both
called $user->can(...) without checking that request()->craftUser() returned
a user, fataling in console/queue contexts. Mirrors the console/null-user
guard already used by the sibling getViewableProductTypes().
…namespace

composer.json maps craft\commerce\ to src-yii2/, and every file in these
directories declares (or aliases into) namespace craft\commerce\base or
craft\commerce\enums (lowercase). The directories were Base/ and Enums/
(capitalized), which macOS's case-insensitive filesystem silently tolerates
but a case-sensitive Linux filesystem does not. This broke autoloading for
every class under craft\commerce\base\* (StoreTrait, GatewayTrait, Model,
Stat, etc.) and craft\commerce\enums\* on GitHub Actions CI runners, which
never surfaced locally because ddev mounts the Mac host filesystem into the
Linux container. It also explains why the Tests/Feature and Tests/Arch CI
jobs have effectively never run to completion until now — they were gated
behind Rector, which was itself broken until the previous commit.
… live autoload root

Two arch rules, both scoped to src/ and verified to actually catch violations
(Pest's toUse()/toBeUsedIn() resolve targets as classes/namespaces via its
ObjectsRepository, so plain function names outside its small hardcoded
core-language-construct list, and Class::method static-call strings, are
silently never matched — confirmed empirically before relying on either):

- No debug functions (die/dd/dump/env) in src/.
- src/ must not reference legacy Craft core classes, excluding craft\commerce
  (allowed during the migration per this repo's CLAUDE.md).

Getting any arch() rule to run at all required moving
src-yii2/test/{fixtures/elements/ProductFixture.php,mockclasses/Purchasable.php}
to tests-yii2/, matching where every other pre-Pest reference/porting fixture
already lives. Pest's arch plugin enumerates every PSR-4 namespace declared in
composer.json to build its analysis graph, not just the expect() target, so
these two forgotten files sitting inside the live craft\commerce\ (src-yii2/)
autoload root — referencing craft\base\ElementInterface, which isn't even
aliased anymore — fataled the whole test run before either rule could
evaluate. Updated their two internal namespace declarations and the three
call sites that imported them (craft\commerce\test\* -> craftcommercetests\*)
to match their new location.
…rce\Gql\...)

Same class of bug as the earlier src-yii2/Base and src-yii2/Enums fix, this
time in the new src/ tree: namespace CraftCms\Commerce\Gql\Handlers (etc.)
declared throughout, but the directory was src/gql/handlers (lowercase).

Unlike the earlier StoreTrait case, this didn't fatal — Plugin::boot()
registers CraftCms\Commerce\Gql\Handlers\HasProduct as a GQL argument handler
via is_a($handler, ArgumentHandlerInterface::class, true), and is_a() with
$allow_string swallows a failed autoload and just returns false. That surfaced
as "Argument handler [...] must implement [ArgumentHandlerInterface]" on
Tests/Feature CI, which was misleading: the class autoloads fine on macOS
(case-insensitive host filesystem mounted into the ddev container) and so
genuinely does implement the interface — it just couldn't be found by name on
GitHub's case-sensitive Linux runners.

Renamed src/gql -> src/Gql, handlers -> Handlers, types -> Types,
types/input -> Types/Input, types/input/criteria -> Types/Input/Criteria.
Re-scanned both src/ and src-yii2/ in full for any other namespace/directory
casing mismatches; none remain.
…ueries, arguments)

Completes the GraphQL migration checklist: Arguments/Elements/{Product,Variant},
Interfaces/Elements/{Product,Variant}, Types/Elements/{Product,Variant},
Types/Generators/{ProductType,VariantType}, Types/Input/{IntFalse,Product,Variant},
Types/SaleType, Resolvers/Elements/{Product,Variant}, and Queries/{Product,Variant},
all under CraftCms\Commerce\Gql\. helpers/Gql.php was already fully ported in an
earlier commit; its legacy src-yii2 counterpart is now a thin deprecated subclass
matching the established pattern.

The "wire up schema-registration" checklist item turned out to already be mostly
done — Plugin.php already registered the GqlArguments handlers and the
GqlSchemaComponentsResolving/GqlEagerLoadableFieldsResolving listeners. The only
missing piece was populating the new $gqlTypes/$gqlQueries properties the base
Plugin class's HasGql concern reads automatically, replacing the legacy
Event::on(Gql::EVENT_REGISTER_GQL_TYPES/QUERIES) wiring entirely.

Also fixes 3 stray legacy craft\gql\* imports in already-migrated files
(Catalog/Variants.php, Gql/Types/Input/Criteria/{Product,Variant}Relation.php)
that the new Arch tests would otherwise have flagged, and points those two
Criteria classes at the new Arguments classes instead of the legacy ones.

Verified beyond phpstan/check-cs: manually executed a GraphQL query through
products -> variants -> sales against a seeded schema, and prebuilt/validated
the full schema (introspection path), both via a throwaway test since this
repo has no GQL feature-test harness yet.
…on on SQLite

Two compounding bugs, both in date/timezone handling around the "today" stat:

1. LocalTimestamp's SQLite branch returned the raw (UTC) column unconverted,
   on the assumption "SQLite is only used by the test suite, which always
   runs in UTC" — false: tests/TestCase.php pins the app's timezone to
   America/Los_Angeles for determinism. Day/month grouping (TotalOrders and
   every other stat using getChartQueryOptionsByInterval) would bucket orders
   under their UTC calendar date instead of the app's configured one,
   splitting a single day's data across two chart entries whenever UTC and
   LA disagreed on the current date (i.e. for most of each 24-hour period).
   Fixed by resolving the configured timezone's current UTC offset in PHP
   (DateTimeZone::getOffset(), which accounts for DST) and applying it via
   SQLite's datetime(column, '+/-N minutes') modifier, since SQLite has no
   named-timezone SQL functions to call directly.

2. Separately, TotalOrdersTest's "today" dataset computed its expected
   start/end dates via new DateTime('now') inside the ->with() array. Pest
   resolves dataset closures before beforeEach()/app boot, i.e. before
   TestCase::setUp() pins the timezone — so the dataset's "now" could be
   read under a different default timezone than the one Stat itself later
   uses when it independently recomputes "today" inside TotalOrders's own
   constructor. Moved the date computation into the test body, after the
   fixture (and app boot) has already run, so both sides agree by construction.

Verified with 5 repeated runs of the previously-100%-reproducible failure,
the full Stats suite, and the full test suite (109/109 passing, first fully
clean run this session).
Ports the five remaining Yii2 console controllers to CraftCms\Commerce\Console\Commands\*,
following the Illuminate\Console\Command + CraftCommand pattern already established by
the resave commands. GatewaysController's two real actions (list, webhook-url) split into
separate command classes, matching how cms-6 splits its own multi-action controllers
(e.g. project-config:get/set/apply). Legacy `commerce/*` CLI routes are preserved as
command aliases via $aliases, so existing scripts/muscle-memory keep working.

Destructive/interactive flows were translated to their Laravel-idiomatic equivalents where
that's a strict improvement with no behavior change for the required inputs: ResetData now
uses ConfirmableTrait (a --force bypass plus a components->task()-driven, single
DB::transaction()-wrapped delete, rather than the ad-hoc yes/no string prompt + manual
begin/commit/rollBack); ExampleTemplates and TransferCustomerData use Laravel's own
ask()/confirm() and only prompt for options not already supplied on the command line.

console/Controller.php (an empty pass-through base with nothing else extending it once the
above landed) is deleted outright, not stubbed — console controllers are CLI entry points
invoked by route string, not classes anything else in the codebase instantiates or
type-hints against, so there's no back-compat surface to preserve the way there is for
services/models.

Verified against a real dev install (not just phpstan/tests), which surfaced two real,
unrelated pre-existing bugs the migrated code paths hadn't exercised before:
- Gateway::set{Billing,Shipping}AddressCondition() didn't accept null, despite handling it
  in the method body (setOrderCondition already did) - throws the moment any real gateway
  config has a null condition, i.e. immediately for `commerce:gateways:list`.
- CatalogPricing::setQueueProgress() called method_exists(null, ...), which throws a
  TypeError under PHP 8's stricter argument types - hits every no-queue call, i.e.
  immediately for `commerce:pricing-catalog:generate`.
Both fixed. commerce:reset-data was verified by code review (DB::transaction wrapping,
correct table/column names) rather than executed live, since the harness's destructive-
action guard correctly declined to run a bulk-delete command even against a dev database
confirmed to have zero rows in every affected table.
Follow-up to 0a53428: a bad pathspec in that commit's `git add` (src-yii2/console,
already handled by a separate git rm) silently aborted staging every other file passed
in the same invocation, so Plugin.php's $commands registration for the 5 new console
commands, the Gateway.php/CatalogPricing.php bugfixes those commands surfaced, and the
CHANGELOG-WIP.md entry never actually made it into that commit — only the new command
classes and the src-yii2 deletions did. The commands existed but were never wired up.

Re-verified with phpstan and the full test suite now that Plugin.php's registration is
actually included.
…ectConfigData,Purchasable}.php

All 8 already had complete src/Helpers/ counterparts from earlier migration work,
so this is mostly the usual legacy-stub conversion + consumer rewiring — but
verifying each one caught two real, latent bugs:

- src/Helpers/{Cp,Currency,Purchasable}.php still imported the legacy core
  craft\helpers\Cp instead of CraftCms\Cms\Cp\FormFields (fieldHtml/moneyInputHtml/
  textHtml) or the CraftCms\Cms template()/TemplateMode helpers (renderTemplate)
  or FormFields::lightswitchFromConfig()->toHtml() (lightswitchHtml, itself
  deprecated in cms-6's own yii2-adapter). This is a real gap in the "src/ should
  not reference legacy Craft core classes" Arch rule added earlier this session:
  Pest-arch's dependency-layer resolution excludes vendor-directory namespaces
  entirely, so any craft\* class living under vendor/craftcms/cms (as opposed to
  this repo's own src-yii2/) is invisible to it and can slip through undetected.
  That's a separate, wider-reaching finding worth its own follow-up pass.

- CraftCms\Commerce\Helpers\Localization no longer `extends \craft\helpers\
  Localization` (it only needs normalizePercentage(), and normalizeNumber() is
  called via CraftCms\Cms\Support\Facades\I18N internally), but
  PaymentsController.php called the commerce subclass's *inherited*
  normalizeNumber() directly - undefined once the extends was dropped. Pointed
  it at the I18N facade instead, matching Localization's own internal usage.

Rewired the 15 already-migrated src/ consumers that still imported the legacy
craft\commerce\helpers\* classes to use CraftCms\Commerce\Helpers\* directly.

Verified beyond phpstan/tests: called the FormFields-backed methods
(Cp::taxZoneFieldHtml, Currency::moneyInputHtml, Purchasable::skuInputHtml,
Purchasable::availableForPurchaseInputHtml) against a real dev install via
tinker, since none of the existing test suite exercises CP-rendering helpers.
…idgetTrait,StoreTrait,TaxEngineInterface,TaxIdValidatorInterface,ZoneInterface}

All 9 already had complete src/ counterparts from earlier migration work
(most already carried @deprecated docblocks pointing at them, and the
CHANGELOG-WIP.md entries for their replacements already existed - this was
purely the stub-conversion + verification pass). StatTrait is deliberately
left as a real, untouched legacy trait, matching the GatewayTrait precedent
from the Gateway migration: its properties were merged directly onto the new
Stat class rather than ported to a dedicated trait, so there's no clean
class_alias target, and it costs nothing to leave as free-standing legacy
code for any third-party code still using it directly.

Model.php is a genuine dead end: an empty pass-through subclass of
craft\base\Model with zero consumers anywhere in the codebase. Pointed its
stub at CraftCms\Cms\Component\Component directly (the documented modern
base for what used to extend craft\base\Model - see docs/6.x/extend/models.md
and every already-migrated Commerce model) rather than a nonexistent "new
commerce Model" class.

Comparing old vs new caught one real fidelity gap: legacy Stat implements
both StatInterface and HasStoreInterface, but the new class only implemented
StatInterface even though StoreTrait already satisfies HasStoreInterface's
contract - just a dropped `implements` clause. Added it back.

Rewired the 2 already-migrated src/ consumers (Catalog/Elements/Product.php,
Store/Models/SiteStore.php) still importing the legacy craft\commerce\base\StoreTrait.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant