diff --git a/CHANGELOG-WIP.md b/CHANGELOG-WIP.md index 279b9566ca..ed503ea2b5 100644 --- a/CHANGELOG-WIP.md +++ b/CHANGELOG-WIP.md @@ -1,9 +1,30 @@ # Release Notes for Craft Commerce (WIP) -## Administration - +### Store Management +- It is now possible to design card views for Products and Variants. ([#3809](https://github.com/craftcms/commerce/pull/3809)) +- Order conditions can now have a “Coupon Code” rule. ([#3776](https://github.com/craftcms/commerce/discussions/3776)) +- Order conditions can now have a “Payment Gateway” rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722)) +- Variant conditions can now have a “Product” rule. - Tax rates now have statuses. ([#3790](https://github.com/craftcms/commerce/discussions/3790)) -- Added support for `to`, `bcc`, and `cc` email fields to support environment variables. ([#3738](https://github.com/craftcms/commerce/issues/3738)) + +### Administration +- The `to`, `bcc`, and `cc` email fields now support environment variables. ([#3738](https://github.com/craftcms/commerce/issues/3738)) + +### Development +- Added the `couponCode` order query param. - Added an `originalCart` value to the `commerce/update-cart` failed ajax response. ([#430](https://github.com/craftcms/commerce/issues/430)) - Added a new "Payment Gateway" order condition rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722)) + +### Extensibility +- Added `craft\commerce\base\InventoryItemTrait`. +- Added `craft\commerce\base\InventoryLocationTrait`. +- Added `craft\commerce\elements\conditions\orders\CouponCodeConditionRule`. +- Added `craft\commerce\elements\conditions\variants\ProductConditionRule`. +- Added `craft\commerce\elements\db\OrderQuery::$couponCode`. +- Added `craft\commerce\elements\db\OrderQuery::couponCode()`. +- Added `craft\commerce\services\Inventory::updateInventoryLevel()`. +- Added `craft\commerce\services\Inventory::updatePurchasableInventoryLevel()`. - Added `craft\commerce\services\TaxRates::getAllEnabledTaxRates()`. + +### System +- Craft Commerce now requires Craft CMS 5.5 or later. diff --git a/CHANGELOG.md b/CHANGELOG.md index 089c31dd94..db68d6a985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Release Notes for Craft Commerce -## 5.2.7 - 2024-11 +## Unreleased + +- Fixed a bug where a product’s default price was showing incorrectly on the Products index page. ([#3807](https://github.com/craftcms/commerce/issues/3807)) +- Fixed a bug where inline-editable Matrix fields weren’t saving content on product variants. ([#3805](https://github.com/craftcms/commerce/issues/3805)) +- Fixed a bug where order errors weren't showing on the Edit Order page. + +## 5.2.8 - 2024-12-04 + +- Fixed a bug where line items weren’t getting hyperlinked within Edit Order pages. ([#3792](https://github.com/craftcms/commerce/issues/3792)) +- Fixed a bug where Inventory pages were showing draft purchasables. +- Fixed a PHP error that could occur when creating inventory transfers. ([#3696](https://github.com/craftcms/commerce/issues/3696)) +- Fixed a bug where prices weren’t getting formatted per the user’s formatting locale, in payment models on Edit Order pages. ([#3789](https://github.com/craftcms/commerce/issues/3789)) +- Fixed a bug where store settings weren’t respecting environment variables. ([#3786](https://github.com/craftcms/commerce/issues/3786)) + +## 5.2.7 - 2024-12-02 - Fixed an error that occurred on the Orders index page when running Craft CMS 5.5.4 or later. ([#3793](https://github.com/craftcms/commerce/issues/3793)) - Fixed a bug where a structured product type’s “Max Levels” setting wasn’t being respected. ([#3785](https://github.com/craftcms/commerce/issues/3785)) diff --git a/composer.json b/composer.json index b3b4d39ca0..871b90968a 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "prefer-stable": true, "require": { "php": "^8.2", - "craftcms/cms": "^5.2.0", + "craftcms/cms": "^5.5.0", "dompdf/dompdf": "^2.0.2", "ibericode/vat": "^1.2.2", "iio/libmergepdf": "^4.0", diff --git a/composer.lock b/composer.lock index 1b85b101b2..a56e134c3b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2f3ba664672e378629a5fd88f697019c", + "content-hash": "733b4b868ad8dbf52e6ea739a7a83494", "packages": [ { "name": "bacon/bacon-qr-code", @@ -331,16 +331,16 @@ }, { "name": "craftcms/cms", - "version": "5.5.3", + "version": "5.5.1.1", "source": { "type": "git", "url": "https://github.com/craftcms/cms.git", - "reference": "20199c5340825146f2354d0a29fb694b7d86e400" + "reference": "2233b27fd7e80cccc3aab927ad073f5916167dba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/craftcms/cms/zipball/20199c5340825146f2354d0a29fb694b7d86e400", - "reference": "20199c5340825146f2354d0a29fb694b7d86e400", + "url": "https://api.github.com/repos/craftcms/cms/zipball/2233b27fd7e80cccc3aab927ad073f5916167dba", + "reference": "2233b27fd7e80cccc3aab927ad073f5916167dba", "shasum": "" }, "require": { @@ -454,7 +454,7 @@ "rss": "https://github.com/craftcms/cms/releases.atom", "source": "https://github.com/craftcms/cms" }, - "time": "2024-11-22T23:29:45+00:00" + "time": "2024-11-19T02:11:31+00:00" }, { "name": "craftcms/plugin-installer", @@ -1636,16 +1636,16 @@ }, { "name": "illuminate/collections", - "version": "v10.48.25", + "version": "v10.48.23", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "48de3d6bc6aa779112ddcb608a3a96fc975d89d8" + "reference": "37c863cffb345869dd134eff8e646bc82a19cc96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/48de3d6bc6aa779112ddcb608a3a96fc975d89d8", - "reference": "48de3d6bc6aa779112ddcb608a3a96fc975d89d8", + "url": "https://api.github.com/repos/illuminate/collections/zipball/37c863cffb345869dd134eff8e646bc82a19cc96", + "reference": "37c863cffb345869dd134eff8e646bc82a19cc96", "shasum": "" }, "require": { @@ -1687,20 +1687,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-11-21T14:02:44+00:00" + "time": "2024-06-19T14:25:05+00:00" }, { "name": "illuminate/conditionable", - "version": "v10.48.25", + "version": "v10.48.23", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", - "reference": "3ee34ac306fafc2a6f19cd7cd68c9af389e432a5" + "reference": "d0958e4741fc9d6f516a552060fd1b829a85e009" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/conditionable/zipball/3ee34ac306fafc2a6f19cd7cd68c9af389e432a5", - "reference": "3ee34ac306fafc2a6f19cd7cd68c9af389e432a5", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/d0958e4741fc9d6f516a552060fd1b829a85e009", + "reference": "d0958e4741fc9d6f516a552060fd1b829a85e009", "shasum": "" }, "require": { @@ -1733,20 +1733,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-11-21T14:02:44+00:00" + "time": "2023-02-03T08:06:17+00:00" }, { "name": "illuminate/contracts", - "version": "v10.48.25", + "version": "v10.48.23", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "f90663a69f926105a70b78060a31f3c64e2d1c74" + "reference": "8d7152c4a1f5d9cf7da3e8b71f23e4556f6138ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/f90663a69f926105a70b78060a31f3c64e2d1c74", - "reference": "f90663a69f926105a70b78060a31f3c64e2d1c74", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/8d7152c4a1f5d9cf7da3e8b71f23e4556f6138ac", + "reference": "8d7152c4a1f5d9cf7da3e8b71f23e4556f6138ac", "shasum": "" }, "require": { @@ -1781,11 +1781,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-11-21T14:02:44+00:00" + "time": "2024-01-15T18:52:32+00:00" }, { "name": "illuminate/macroable", - "version": "v10.48.25", + "version": "v10.48.23", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -2182,23 +2182,23 @@ }, { "name": "moneyphp/money", - "version": "v4.6.0", + "version": "v4.5.1", "source": { "type": "git", "url": "https://github.com/moneyphp/money.git", - "reference": "ddf6a86b574808f8844777ed4e8c4f92a10dac9b" + "reference": "142107bec4870ac2586057dc2fe917d25c92a91e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/moneyphp/money/zipball/ddf6a86b574808f8844777ed4e8c4f92a10dac9b", - "reference": "ddf6a86b574808f8844777ed4e8c4f92a10dac9b", + "url": "https://api.github.com/repos/moneyphp/money/zipball/142107bec4870ac2586057dc2fe917d25c92a91e", + "reference": "142107bec4870ac2586057dc2fe917d25c92a91e", "shasum": "" }, "require": { "ext-bcmath": "*", "ext-filter": "*", "ext-json": "*", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" }, "require-dev": { "cache/taggable-cache": "^1.1.0", @@ -2264,9 +2264,9 @@ ], "support": { "issues": "https://github.com/moneyphp/money/issues", - "source": "https://github.com/moneyphp/money/tree/v4.6.0" + "source": "https://github.com/moneyphp/money/tree/v4.5.1" }, - "time": "2024-11-22T10:59:03+00:00" + "time": "2024-09-27T12:04:27+00:00" }, { "name": "monolog/monolog", @@ -3954,16 +3954,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", "shasum": "" }, "require": { @@ -4001,7 +4001,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -4017,20 +4017,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.2.0", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + "reference": "87254c78dd50721cfd015b62277a8281c5589702" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87254c78dd50721cfd015b62277a8281c5589702", + "reference": "87254c78dd50721cfd015b62277a8281c5589702", "shasum": "" }, "require": { @@ -4081,7 +4081,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.6" }, "funding": [ { @@ -4097,20 +4097,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", "shasum": "" }, "require": { @@ -4157,7 +4157,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" }, "funding": [ { @@ -4173,7 +4173,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/filesystem", @@ -4243,23 +4243,23 @@ }, { "name": "symfony/http-client", - "version": "v6.4.16", + "version": "v6.4.15", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "60a113666fa67e598abace38e5f46a0954d8833d" + "reference": "cb4073c905cd12b8496d24ac428a9228c1750670" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/60a113666fa67e598abace38e5f46a0954d8833d", - "reference": "60a113666fa67e598abace38e5f46a0954d8833d", + "url": "https://api.github.com/repos/symfony/http-client/zipball/cb4073c905cd12b8496d24ac428a9228c1750670", + "reference": "cb4073c905cd12b8496d24ac428a9228c1750670", "shasum": "" }, "require": { "php": ">=8.1", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "~3.4.3|^3.5.1", + "symfony/http-client-contracts": "^3.4.1", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -4316,7 +4316,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.16" + "source": "https://github.com/symfony/http-client/tree/v6.4.15" }, "funding": [ { @@ -4332,20 +4332,20 @@ "type": "tidelift" } ], - "time": "2024-11-27T11:52:33+00:00" + "time": "2024-11-13T13:40:18+00:00" }, { "name": "symfony/http-client-contracts", - "version": "v3.5.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "c2f3ad828596624ca39ea40f83617ef51ca8bbf9" + "reference": "20414d96f391677bf80078aa55baece78b82647d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/c2f3ad828596624ca39ea40f83617ef51ca8bbf9", - "reference": "c2f3ad828596624ca39ea40f83617ef51ca8bbf9", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", "shasum": "" }, "require": { @@ -4394,7 +4394,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" }, "funding": [ { @@ -4410,20 +4410,20 @@ "type": "tidelift" } ], - "time": "2024-11-25T12:02:18+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/mailer", - "version": "v7.2.0", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc" + "reference": "69c9948451fb3a6a4d47dc8261d1794734e76cdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/e4d358702fb66e4c8a2af08e90e7271a62de39cc", - "reference": "e4d358702fb66e4c8a2af08e90e7271a62de39cc", + "url": "https://api.github.com/repos/symfony/mailer/zipball/69c9948451fb3a6a4d47dc8261d1794734e76cdd", + "reference": "69c9948451fb3a6a4d47dc8261d1794734e76cdd", "shasum": "" }, "require": { @@ -4432,7 +4432,7 @@ "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/mime": "^7.2", + "symfony/mime": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { @@ -4474,7 +4474,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.2.0" + "source": "https://github.com/symfony/mailer/tree/v7.1.6" }, "funding": [ { @@ -4490,20 +4490,20 @@ "type": "tidelift" } ], - "time": "2024-11-25T15:21:05+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/mime", - "version": "v7.2.0", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d" + "reference": "caa1e521edb2650b8470918dfe51708c237f0598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/cc84a4b81f62158c3846ac7ff10f696aae2b524d", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d", + "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598", + "reference": "caa1e521edb2650b8470918dfe51708c237f0598", "shasum": "" }, "require": { @@ -4558,7 +4558,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.0" + "source": "https://github.com/symfony/mime/tree/v7.1.6" }, "funding": [ { @@ -4574,7 +4574,7 @@ "type": "tidelift" } ], - "time": "2024-11-23T09:19:39+00:00" + "time": "2024-10-25T15:11:02+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5279,16 +5279,16 @@ }, { "name": "symfony/process", - "version": "v7.2.0", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/42783370fda6e538771f7c7a36e9fa2ee3a84892", + "reference": "42783370fda6e538771f7c7a36e9fa2ee3a84892", "shasum": "" }, "require": { @@ -5320,7 +5320,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.0" + "source": "https://github.com/symfony/process/tree/v7.1.8" }, "funding": [ { @@ -5336,20 +5336,20 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2024-11-06T14:23:19+00:00" }, { "name": "symfony/property-access", - "version": "v7.2.0", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "3ae42efba01e45aaedecf5c93c8d6a3ab3a82276" + "reference": "975d7f7fd8fcb952364c6badc46d01a580532bf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/3ae42efba01e45aaedecf5c93c8d6a3ab3a82276", - "reference": "3ae42efba01e45aaedecf5c93c8d6a3ab3a82276", + "url": "https://api.github.com/repos/symfony/property-access/zipball/975d7f7fd8fcb952364c6badc46d01a580532bf9", + "reference": "975d7f7fd8fcb952364c6badc46d01a580532bf9", "shasum": "" }, "require": { @@ -5396,7 +5396,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v7.2.0" + "source": "https://github.com/symfony/property-access/tree/v7.1.6" }, "funding": [ { @@ -5412,20 +5412,20 @@ "type": "tidelift" } ], - "time": "2024-09-26T12:28:35+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/property-info", - "version": "v7.2.0", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "b00580d9d7c9654e1df95df85105d0da67418b3f" + "reference": "3748f85f64351d282fd028e44309856f1d79142e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/b00580d9d7c9654e1df95df85105d0da67418b3f", - "reference": "b00580d9d7c9654e1df95df85105d0da67418b3f", + "url": "https://api.github.com/repos/symfony/property-info/zipball/3748f85f64351d282fd028e44309856f1d79142e", + "reference": "3748f85f64351d282fd028e44309856f1d79142e", "shasum": "" }, "require": { @@ -5436,7 +5436,8 @@ "conflict": { "phpdocumentor/reflection-docblock": "<5.2", "phpdocumentor/type-resolver": "<1.5.1", - "symfony/dependency-injection": "<6.4" + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" }, "require-dev": { "phpdocumentor/reflection-docblock": "^5.2", @@ -5479,7 +5480,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v7.2.0" + "source": "https://github.com/symfony/property-info/tree/v7.1.8" }, "funding": [ { @@ -5495,7 +5496,7 @@ "type": "tidelift" } ], - "time": "2024-11-27T09:50:52+00:00" + "time": "2024-11-09T07:07:11+00:00" }, { "name": "symfony/serializer", @@ -5597,16 +5598,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { @@ -5660,7 +5661,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -5676,20 +5677,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "591ebd41565f356fcd8b090fe64dbb5878f50281" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/591ebd41565f356fcd8b090fe64dbb5878f50281", + "reference": "591ebd41565f356fcd8b090fe64dbb5878f50281", "shasum": "" }, "require": { @@ -5747,7 +5748,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.1.8" }, "funding": [ { @@ -5763,20 +5764,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2024-11-13T13:31:21+00:00" }, { "name": "symfony/type-info", - "version": "v7.2.0", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "e0bfd95bceb3886c59487828537691aecb7d9c6b" + "reference": "51535dde21c7abf65c9d000a30bb15f6478195e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/e0bfd95bceb3886c59487828537691aecb7d9c6b", - "reference": "e0bfd95bceb3886c59487828537691aecb7d9c6b", + "url": "https://api.github.com/repos/symfony/type-info/zipball/51535dde21c7abf65c9d000a30bb15f6478195e6", + "reference": "51535dde21c7abf65c9d000a30bb15f6478195e6", "shasum": "" }, "require": { @@ -5785,11 +5786,13 @@ }, "conflict": { "phpstan/phpdoc-parser": "<1.0", - "symfony/dependency-injection": "<6.4" + "symfony/dependency-injection": "<6.4", + "symfony/property-info": "<6.4" }, "require-dev": { "phpstan/phpdoc-parser": "^1.0|^2.0", - "symfony/dependency-injection": "^6.4|^7.0" + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -5827,7 +5830,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.2.0" + "source": "https://github.com/symfony/type-info/tree/v7.1.8" }, "funding": [ { @@ -5843,20 +5846,20 @@ "type": "tidelift" } ], - "time": "2024-11-18T09:51:31+00:00" + "time": "2024-11-07T15:49:33+00:00" }, { "name": "symfony/uid", - "version": "v7.2.0", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426" + "reference": "65befb3bb2d503bbffbd08c815aa38b472999917" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2d294d0c48df244c71c105a169d0190bfb080426", - "reference": "2d294d0c48df244c71c105a169d0190bfb080426", + "url": "https://api.github.com/repos/symfony/uid/zipball/65befb3bb2d503bbffbd08c815aa38b472999917", + "reference": "65befb3bb2d503bbffbd08c815aa38b472999917", "shasum": "" }, "require": { @@ -5901,7 +5904,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.2.0" + "source": "https://github.com/symfony/uid/tree/v7.1.6" }, "funding": [ { @@ -5917,7 +5920,7 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/var-dumper", @@ -6522,16 +6525,16 @@ }, { "name": "voku/portable-ascii", - "version": "2.0.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/voku/portable-ascii.git", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + "reference": "b56450eed252f6801410d810c8e1727224ae0743" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b56450eed252f6801410d810c8e1727224ae0743", + "reference": "b56450eed252f6801410d810c8e1727224ae0743", "shasum": "" }, "require": { @@ -6556,7 +6559,7 @@ "authors": [ { "name": "Lars Moelleken", - "homepage": "https://www.moelleken.org/" + "homepage": "http://www.moelleken.org/" } ], "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", @@ -6568,7 +6571,7 @@ ], "support": { "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + "source": "https://github.com/voku/portable-ascii/tree/2.0.1" }, "funding": [ { @@ -6592,7 +6595,7 @@ "type": "tidelift" } ], - "time": "2024-11-21T01:49:47+00:00" + "time": "2022-03-08T17:03:00+00:00" }, { "name": "voku/portable-utf8", @@ -8708,16 +8711,16 @@ }, { "name": "fakerphp/faker", - "version": "v1.24.1", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + "reference": "a136842a532bac9ecd8a1c723852b09915d7db50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/a136842a532bac9ecd8a1c723852b09915d7db50", + "reference": "a136842a532bac9ecd8a1c723852b09915d7db50", "shasum": "" }, "require": { @@ -8765,9 +8768,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.0" }, - "time": "2024-11-21T13:46:39+00:00" + "time": "2024-11-07T15:11:20+00:00" }, { "name": "graham-campbell/result-type", @@ -9420,16 +9423,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.12", + "version": "1.12.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0" + "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", + "reference": "0d1fc20a962a91be578bcfe7cf939e6e1a2ff733", "shasum": "" }, "require": { @@ -9474,7 +9477,7 @@ "type": "github" } ], - "time": "2024-11-28T22:13:23+00:00" + "time": "2024-11-17T14:08:01+00:00" }, { "name": "phpunit/php-code-coverage", @@ -9801,16 +9804,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.4.4", + "version": "11.4.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f9ba7bd3c9f3ff54ec379d7a1c2e3f13fe0bbde4" + "reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f9ba7bd3c9f3ff54ec379d7a1c2e3f13fe0bbde4", - "reference": "f9ba7bd3c9f3ff54ec379d7a1c2e3f13fe0bbde4", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e8e8ed1854de5d36c088ec1833beae40d2dedd76", + "reference": "e8e8ed1854de5d36c088ec1833beae40d2dedd76", "shasum": "" }, "require": { @@ -9820,7 +9823,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.12.0", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", @@ -9831,7 +9834,7 @@ "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", "sebastian/code-unit": "^3.0.1", - "sebastian/comparator": "^6.2.1", + "sebastian/comparator": "^6.1.1", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.0", "sebastian/exporter": "^6.1.3", @@ -9881,7 +9884,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.4" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.3" }, "funding": [ { @@ -9897,20 +9900,20 @@ "type": "tidelift" } ], - "time": "2024-11-27T10:44:52+00:00" + "time": "2024-10-28T13:07:50+00:00" }, { "name": "psy/psysh", - "version": "v0.12.5", + "version": "v0.12.4", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "36a03ff27986682c22985e56aabaf840dd173cb5" + "reference": "2fd717afa05341b4f8152547f142cd2f130f6818" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/36a03ff27986682c22985e56aabaf840dd173cb5", - "reference": "36a03ff27986682c22985e56aabaf840dd173cb5", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/2fd717afa05341b4f8152547f142cd2f130f6818", + "reference": "2fd717afa05341b4f8152547f142cd2f130f6818", "shasum": "" }, "require": { @@ -9937,12 +9940,12 @@ ], "type": "library", "extra": { + "branch-alias": { + "dev-main": "0.12.x-dev" + }, "bamarni-bin": { "bin-links": false, "forward-command": false - }, - "branch-alias": { - "dev-main": "0.12.x-dev" } }, "autoload": { @@ -9974,9 +9977,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.5" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.4" }, - "time": "2024-11-29T06:14:30+00:00" + "time": "2024-06-10T01:18:23+00:00" }, { "name": "rector/rector", @@ -11031,16 +11034,16 @@ }, { "name": "symfony/browser-kit", - "version": "v7.2.0", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "8d64d17e198082f8f198d023a6b634e7b5fdda94" + "reference": "714becc9ba9b20115ffededc58f6b7172dc394cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/8d64d17e198082f8f198d023a6b634e7b5fdda94", - "reference": "8d64d17e198082f8f198d023a6b634e7b5fdda94", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/714becc9ba9b20115ffededc58f6b7172dc394cf", + "reference": "714becc9ba9b20115ffededc58f6b7172dc394cf", "shasum": "" }, "require": { @@ -11079,7 +11082,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v7.2.0" + "source": "https://github.com/symfony/browser-kit/tree/v7.1.6" }, "funding": [ { @@ -11095,20 +11098,20 @@ "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2024-10-25T15:11:02+00:00" }, { "name": "symfony/console", - "version": "v7.2.0", + "version": "v7.1.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf" + "reference": "ff04e5b5ba043d2badfb308197b9e6b42883fcd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", + "url": "https://api.github.com/repos/symfony/console/zipball/ff04e5b5ba043d2badfb308197b9e6b42883fcd5", + "reference": "ff04e5b5ba043d2badfb308197b9e6b42883fcd5", "shasum": "" }, "require": { @@ -11172,7 +11175,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.0" + "source": "https://github.com/symfony/console/tree/v7.1.8" }, "funding": [ { @@ -11188,20 +11191,20 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2024-11-06T14:23:19+00:00" }, { "name": "symfony/css-selector", - "version": "v7.2.0", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + "reference": "4aa4f6b3d6749c14d3aa815eef8226632e7bbc66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", - "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/4aa4f6b3d6749c14d3aa815eef8226632e7bbc66", + "reference": "4aa4f6b3d6749c14d3aa815eef8226632e7bbc66", "shasum": "" }, "require": { @@ -11237,7 +11240,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.2.0" + "source": "https://github.com/symfony/css-selector/tree/v7.1.6" }, "funding": [ { @@ -11253,20 +11256,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/dom-crawler", - "version": "v7.2.0", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "b176e1f1f550ef44c94eb971bf92488de08f7c6b" + "reference": "794ddd5481ba15d8a04132c95e211cd5656e09fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b176e1f1f550ef44c94eb971bf92488de08f7c6b", - "reference": "b176e1f1f550ef44c94eb971bf92488de08f7c6b", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/794ddd5481ba15d8a04132c95e211cd5656e09fb", + "reference": "794ddd5481ba15d8a04132c95e211cd5656e09fb", "shasum": "" }, "require": { @@ -11304,7 +11307,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.2.0" + "source": "https://github.com/symfony/dom-crawler/tree/v7.1.6" }, "funding": [ { @@ -11320,20 +11323,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T16:15:23+00:00" + "time": "2024-10-25T15:11:02+00:00" }, { "name": "symfony/finder", - "version": "v7.2.0", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "6de263e5868b9a137602dd1e33e4d48bfae99c49" + "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/6de263e5868b9a137602dd1e33e4d48bfae99c49", - "reference": "6de263e5868b9a137602dd1e33e4d48bfae99c49", + "url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8", + "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8", "shasum": "" }, "require": { @@ -11368,7 +11371,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.2.0" + "source": "https://github.com/symfony/finder/tree/v7.1.6" }, "funding": [ { @@ -11384,7 +11387,7 @@ "type": "tidelift" } ], - "time": "2024-10-23T06:56:12+00:00" + "time": "2024-10-01T08:31:23+00:00" }, { "name": "symfony/polyfill-php80", @@ -11668,6 +11671,6 @@ "platform": { "php": "^8.2" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/src/Plugin.php b/src/Plugin.php index 6d12e66d84..30bbb5ada0 100755 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -257,7 +257,7 @@ public static function editions(): array /** * @inheritDoc */ - public string $schemaVersion = '5.2.7.0'; + public string $schemaVersion = '5.2.7.1'; /** * @inheritdoc diff --git a/src/base/InventoryItemTrait.php b/src/base/InventoryItemTrait.php new file mode 100644 index 0000000000..1bfc485d20 --- /dev/null +++ b/src/base/InventoryItemTrait.php @@ -0,0 +1,61 @@ + + * @since 5.3.0 + */ +trait InventoryItemTrait +{ + /** + * @var int|null The inventory item ID + */ + public ?int $inventoryItemId = null; + + /** + * @var InventoryItem|null The inventory item + * @see getInventoryItem() + * @see setInventoryItem() + */ + private ?InventoryItem $_inventoryItem = null; + + /** + * @param InventoryItem|null $inventoryItem + * @return void + */ + public function setInventoryItem(?InventoryItem $inventoryItem): void + { + $this->_inventoryItem = $inventoryItem; + $this->inventoryItemId = $inventoryItem?->id ?? null; + } + + /** + * @return InventoryItem|null + * @throws \yii\base\InvalidConfigException + */ + public function getInventoryItem(): ?InventoryItem + { + if (isset($this->_inventoryItem)) { + return $this->_inventoryItem; + } + + if ($this->inventoryItemId) { + $this->_inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($this->inventoryItemId); + + return $this->_inventoryItem; + } + + return null; + } +} diff --git a/src/base/InventoryLocationTrait.php b/src/base/InventoryLocationTrait.php new file mode 100644 index 0000000000..02888bc927 --- /dev/null +++ b/src/base/InventoryLocationTrait.php @@ -0,0 +1,61 @@ + + * @since 5.3.0 + */ +trait InventoryLocationTrait +{ + /** + * @var int|null The inventory item ID + */ + public ?int $inventoryLocationId = null; + + /** + * @var InventoryLocation|null The inventory item + * @see getInventoryLocation() + * @see setInventoryLocation() + */ + private ?InventoryLocation $_inventoryLocation = null; + + /** + * @param InventoryLocation|null $inventoryLocation + * @return void + */ + public function setInventoryLocation(?InventoryLocation $inventoryLocation): void + { + $this->_inventoryLocation = $inventoryLocation; + $this->inventoryLocationId = $inventoryLocation?->id ?? null; + } + + /** + * @return InventoryLocation|null + * @throws \yii\base\InvalidConfigException + */ + public function getInventoryLocation(): ?InventoryLocation + { + if (isset($this->_inventoryLocation)) { + return $this->_inventoryLocation; + } + + if ($this->inventoryLocationId) { + $this->_inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($this->inventoryLocationId); + + return $this->_inventoryLocation; + } + + return null; + } +} diff --git a/src/base/InventoryMovement.php b/src/base/InventoryMovement.php index 6675a1ff48..6d8ea1b6d3 100644 --- a/src/base/InventoryMovement.php +++ b/src/base/InventoryMovement.php @@ -8,7 +8,6 @@ namespace craft\commerce\base; use craft\commerce\enums\InventoryTransactionType; -use craft\commerce\models\InventoryItem; use craft\commerce\models\InventoryLocation; /** @@ -18,10 +17,7 @@ */ abstract class InventoryMovement extends Model implements InventoryMovementInterface { - /** - * @var InventoryItem The inventory item - */ - public InventoryItem $inventoryItem; + use InventoryItemTrait; /** * @var InventoryLocation @@ -90,27 +86,31 @@ public function init(): void } /** - * @inheritDoc + * @return array */ - public function isValid(): bool + protected function defineRules(): array { - return $this->validate(); + $rules = parent::defineRules(); + + $rules[] = [['inventoryItemId'], 'safe']; + + return $rules; } /** * @inheritDoc */ - public function getInventoryMovementHash(): string + public function isValid(): bool { - return $this->_inventoryMovementHash; + return $this->validate(); } /** * @inheritDoc */ - public function getInventoryItem(): InventoryItem + public function getInventoryMovementHash(): string { - return $this->inventoryItem; + return $this->_inventoryMovementHash; } /** diff --git a/src/base/InventoryMovementInterface.php b/src/base/InventoryMovementInterface.php index 2ea87f0c0a..7e29090bad 100644 --- a/src/base/InventoryMovementInterface.php +++ b/src/base/InventoryMovementInterface.php @@ -21,7 +21,7 @@ interface InventoryMovementInterface /** * @return InventoryItem */ - public function getInventoryItem(): InventoryItem; + public function getInventoryItem(): ?InventoryItem; /** * @return InventoryLocation diff --git a/src/base/Purchasable.php b/src/base/Purchasable.php index ce7906982c..7ac8cf2de2 100644 --- a/src/base/Purchasable.php +++ b/src/base/Purchasable.php @@ -383,6 +383,20 @@ private function _getTeller(): Teller return Plugin::getInstance()->getCurrencies()->getTeller($this->getStore()->getCurrency()); } + /** + * @inheritdoc + */ + public function __unset($name) + { + // Allow clearing of specific memoized properties + if (in_array($name, ['stock', 'shippingCategory', 'taxCategory'])) { + $this->{'_' . $name} = null; + return; + } + + parent::__unset($name); + } + /** * @inheritdoc */ @@ -949,7 +963,6 @@ public function getStock(): int return $this->_stock; } - /** * Returns the total stock across all locations this purchasable is tracked in. * @return Collection @@ -1119,6 +1132,8 @@ public function afterSave(bool $isNew): void */ public function afterPropagate(bool $isNew): void { + parent::afterPropagate($isNew); + Plugin::getInstance()->getCatalogPricing()->createCatalogPricingJob([ 'purchasableIds' => [$this->getCanonicalId()], 'storeId' => $this->getStoreId(), @@ -1274,6 +1289,24 @@ protected function attributeHtml(string $attribute): string } } + $dimensions = []; + if ($attribute === 'dimensions') { + $dimensions = array_filter([ + $this->length, + $this->width, + $this->height, + ]); + } + + if ($attribute === 'priceView') { + $price = $this->basePriceAsCurrency; + if ($this->getBasePromotionalPrice() && $this->getBasePromotionalPrice() < $this->getBasePrice()) { + $price = Html::tag('del', $price, ['style' => 'opacity: .5']) . ' ' . $this->basePromotionalPriceAsCurrency; + } + + return $price; + } + return match ($attribute) { 'sku' => (string)Html::encode($this->getSkuAsText()), 'price' => $this->basePriceAsCurrency, @@ -1285,6 +1318,7 @@ protected function attributeHtml(string $attribute): string 'minQty' => (string)$this->minQty, 'maxQty' => (string)$this->maxQty, 'stock' => $stock, + 'dimensions' => !empty($dimensions) ? implode(' x ', $dimensions) . ' ' . Plugin::getInstance()->getSettings()->dimensionUnits : '', default => parent::attributeHtml($attribute), }; } @@ -1322,6 +1356,82 @@ protected static function defineDefaultTableAttributes(string $source): array ]; } + /** + * @inheritdoc + */ + public static function attributePreviewHtml(array $attribute): mixed + { + return match ($attribute['value']) { + 'sku', 'priceView', 'dimensions', 'weight' => $attribute['placeholder'], + 'availableForPurchase', 'promotable' => Html::tag('span', '', [ + 'class' => 'checkbox-icon', + 'role' => 'img', + 'title' => $attribute['label'], + 'aria' => [ + 'label' => $attribute['label'], + ], + ]) . + Html::tag('span', $attribute['label'], [ + 'class' => 'checkbox-preview-label', + ]), + default => parent::attributePreviewHtml($attribute) + }; + } + + /** + * @inheritdoc + */ + protected static function defineDefaultCardAttributes(): array + { + return array_merge(parent::defineDefaultCardAttributes(), [ + 'sku', + 'priceView', + ]); + } + + /** + * @inheritdoc + */ + protected static function defineCardAttributes(): array + { + return array_merge(Element::defineCardAttributes(), [ + 'availableForPurchase' => [ + 'label' => Craft::t('commerce', 'Available for purchase'), + ], + 'basePrice' => [ + 'label' => Craft::t('commerce', 'Base Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'basePromotionalPrice' => [ + 'label' => Craft::t('commerce', 'Base Promotional Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'dimensions' => [ + 'label' => Craft::t('commerce', 'Dimensions'), + 'placeholder' => '1 x 2 x 3 ' . Plugin::getInstance()->getSettings()->dimensionUnits, + ], + 'priceView' => [ + 'label' => Craft::t('commerce', 'Price'), + 'placeholder' => Html::tag('del', '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(199.99), ['style' => 'opacity: .5']) . ' ¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'promotable' => [ + 'label' => Craft::t('commerce', 'Promotable'), + ], + 'sku' => [ + 'label' => Craft::t('commerce', 'SKU'), + 'placeholder' => Html::tag('code', 'SKU123'), + ], + 'stock' => [ + 'label' => Craft::t('commerce', 'Stock'), + 'placeholder' => 10, + ], + 'weight' => [ + 'label' => Craft::t('commerce', 'Weight'), + 'placeholder' => 123 . Plugin::getInstance()->getSettings()->weightUnits, + ], + ]); + } + /** * @inheritdoc */ diff --git a/src/base/StoreTrait.php b/src/base/StoreTrait.php index 8cec683c28..cc92f91216 100644 --- a/src/base/StoreTrait.php +++ b/src/base/StoreTrait.php @@ -1,4 +1,9 @@ map(function(UpdateInventoryLevel $updateInventoryLevel) { - return $updateInventoryLevel->inventoryItem->getPurchasable(); + return $this->map(function(UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel) { + return $updateInventoryLevel->getInventoryItem()->getPurchasable(); })->all(); } } diff --git a/src/controllers/InventoryController.php b/src/controllers/InventoryController.php index 13e2653aea..8ebfb7d3af 100644 --- a/src/controllers/InventoryController.php +++ b/src/controllers/InventoryController.php @@ -242,6 +242,8 @@ public function actionInventoryLevelsTableData(): Response $inventoryQuery->leftJoin(['purchasables' => Table::PURCHASABLES], '[[ii.purchasableId]] = [[purchasables.id]]'); $inventoryQuery->addGroupBy(['[[purchasables.description]]', '[[purchasables.sku]]']); + $inventoryQuery->andWhere(['not', ['elements.id' => null]]); + if ($search) { $inventoryQuery->andWhere(['or', ['like', 'purchasables.description', $search], ['like', 'purchasables.sku', $search]]); } @@ -501,7 +503,6 @@ public function actionUpdateLevels(): Response $note = Craft::$app->getRequest()->getRequiredParam('note'); $inventoryLocationId = (int)Craft::$app->getRequest()->getRequiredParam('inventoryLocationId'); $inventoryItemIds = Craft::$app->getRequest()->getRequiredParam('ids'); - $inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($inventoryLocationId); $type = Craft::$app->getRequest()->getRequiredParam('type'); // We don't add zero amounts as transactions movements @@ -512,17 +513,16 @@ public function actionUpdateLevels(): Response $errors = []; $updateInventoryLevels = UpdateInventoryLevelCollection::make(); foreach ($inventoryItemIds as $inventoryItemId) { - $inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId); - - $updateInventoryLevels->push(new UpdateInventoryLevel([ - 'type' => $type, - 'updateAction' => $updateAction, - 'inventoryItem' => $inventoryItem, - 'inventoryLocation' => $inventoryLocation, - 'quantity' => $quantity, - 'note' => $note, - ]) - ); + // Verbosely set property to show usages + $updateInventoryLevel = new UpdateInventoryLevel(); + $updateInventoryLevel->type = $type; + $updateInventoryLevel->updateAction = $updateAction; + $updateInventoryLevel->inventoryItemId = $inventoryItemId; + $updateInventoryLevel->inventoryLocationId = $inventoryLocationId; + $updateInventoryLevel->quantity = $quantity; + $updateInventoryLevel->note = $note; + + $updateInventoryLevels->push($updateInventoryLevel); } @@ -538,7 +538,8 @@ public function actionUpdateLevels(): Response $resultingInventoryLevels = []; foreach ($updateInventoryLevels as $updateInventoryLevel) { - $resultingInventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel($updateInventoryLevel->inventoryItem, $updateInventoryLevel->inventoryLocation); + /** @var UpdateInventoryLevel $updateInventoryLevel */ + $resultingInventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel($updateInventoryLevel->inventoryItemId, $updateInventoryLevel->inventoryLocationId); } return $this->asSuccess(Craft::t('commerce', 'Inventory updated.'), [ @@ -563,12 +564,9 @@ public function actionEditUpdateLevelsModal(): Response $quantity = (int)$this->request->getParam('quantity', 0); $type = $this->request->getRequiredParam('type'); - $inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($inventoryLocationId); - $inventoryLevels = []; foreach ($inventoryItemIds as $inventoryItemId) { - $item = Plugin::getInstance()->getInventory()->getInventoryItemById((int)$inventoryItemId); - $inventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel($item, $inventoryLocation); + $inventoryLevels[] = Plugin::getInstance()->getInventory()->getInventoryLevel((int)$inventoryItemId, $inventoryLocationId); } $params = [ @@ -612,17 +610,14 @@ public function actionSaveInventoryMovement(): Response return $this->asSuccess(Craft::t('commerce', 'No inventory movements made.')); } - $inventoryMovement = new InventoryManualMovement( - [ - 'inventoryItem' => Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId), - 'fromInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId), - 'toInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId), - 'fromInventoryTransactionType' => InventoryTransactionType::from($fromInventoryTransactionType), - 'toInventoryTransactionType' => InventoryTransactionType::from($toInventoryTransactionType), - 'quantity' => $quantity, - 'note' => $note, - ] - ); + $inventoryMovement = new InventoryManualMovement(); + $inventoryMovement->inventoryItemId = $inventoryItemId; + $inventoryMovement->fromInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId); + $inventoryMovement->toInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId); + $inventoryMovement->fromInventoryTransactionType = InventoryTransactionType::from($fromInventoryTransactionType); + $inventoryMovement->toInventoryTransactionType = InventoryTransactionType::from($toInventoryTransactionType); + $inventoryMovement->quantity = $quantity; + $inventoryMovement->note = $note; if ($inventoryMovement->validate()) { /** @var InventoryMovementCollection $inventoryMovementCollection */ @@ -663,19 +658,16 @@ public function actionEditMovementModal(): Response $toInventoryTransactionType = $toInventoryTransactionType->value; } - $inventoryMovement = new InventoryManualMovement( - [ - 'inventoryItem' => Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId), - 'fromInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId), - 'toInventoryLocation' => Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId), - 'fromInventoryTransactionType' => InventoryTransactionType::from($fromInventoryTransactionType), - 'toInventoryTransactionType' => InventoryTransactionType::from($toInventoryTransactionType), - 'quantity' => $quantity, - 'note' => $note, - ] - ); - - $fromLevel = Plugin::getInstance()->getInventory()->getInventoryLevel($inventoryMovement->inventoryItem, $inventoryMovement->fromInventoryLocation); + $inventoryMovement = new InventoryManualMovement(); + $inventoryMovement->inventoryItemId = $inventoryItemId; + $inventoryMovement->fromInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fromInventoryLocationId); + $inventoryMovement->toInventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($toInventoryLocationId); + $inventoryMovement->fromInventoryTransactionType = InventoryTransactionType::from($fromInventoryTransactionType); + $inventoryMovement->toInventoryTransactionType = InventoryTransactionType::from($toInventoryTransactionType); + $inventoryMovement->quantity = $quantity; + $inventoryMovement->note = $note; + + $fromLevel = Plugin::getInstance()->getInventory()->getInventoryLevel($inventoryMovement->inventoryItemId, $inventoryMovement->fromInventoryLocation); $fromTotal = $fromLevel->{$fromInventoryTransactionType . 'Total'}; $movableTo = $movableTo->toArray(); @@ -703,10 +695,7 @@ public function actionUnfulfilledOrders(): Response $inventoryLocationId = Craft::$app->getRequest()->getParam('inventoryLocationId'); $inventoryItemId = Craft::$app->getRequest()->getParam('inventoryItemId'); - $inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($inventoryLocationId); - $inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($inventoryItemId); - - $orders = Plugin::getInstance()->getInventory()->getUnfulfilledOrders($inventoryItem, $inventoryLocation); + $orders = Plugin::getInstance()->getInventory()->getUnfulfilledOrders($inventoryItemId, $inventoryLocationId); $title = Craft::t('commerce', '{count} Unfulfilled Orders', [ 'count' => count($orders), diff --git a/src/controllers/OrdersController.php b/src/controllers/OrdersController.php index 52c382a06e..32edfcf0ea 100644 --- a/src/controllers/OrdersController.php +++ b/src/controllers/OrdersController.php @@ -259,10 +259,10 @@ public function actionFulfill(): Response $qty = (int)$fulfillment['quantity']; if ($qty != 0) { $inventoryLocation = Plugin::getInstance()->getInventoryLocations()->getInventoryLocationById($fulfillment['inventoryLocationId']); - $inventoryItem = Plugin::getInstance()->getInventory()->getInventoryItemById($fulfillment['inventoryItemId']); + $movement = new InventoryFulfillMovement(); $movement->fromInventoryLocation = $inventoryLocation; - $movement->inventoryItem = $inventoryItem; + $movement->inventoryItemId = $fulfillment['inventoryItemId']; $movement->toInventoryLocation = $inventoryLocation; $movement->fromInventoryTransactionType = InventoryTransactionType::COMMITTED; $movement->toInventoryTransactionType = InventoryTransactionType::FULFILLED; @@ -1236,12 +1236,16 @@ public function actionPaymentAmountData(): Response $paymentCurrencies = Plugin::getInstance()->getPaymentCurrencies(); $paymentCurrency = $this->request->getRequiredParam('paymentCurrency'); $paymentAmount = $this->request->getRequiredParam('paymentAmount'); + $locale = $this->request->getRequiredParam('locale'); $orderId = $this->request->getRequiredParam('orderId'); /** @var Order $order */ $order = Order::find()->id($orderId)->one(); $baseCurrency = $order->currency; - $baseCurrencyPaymentAmount = $paymentCurrencies->convertCurrency($paymentAmount, $paymentCurrency, $baseCurrency); + $paymentAmount = MoneyHelper::toMoney(['value' => $paymentAmount, 'currency' => $baseCurrency, 'locale' => $locale]); + $paymentAmount = MoneyHelper::toDecimal($paymentAmount); + + $baseCurrencyPaymentAmount = $paymentCurrencies->convertCurrency((float)$paymentAmount, $paymentCurrency, $baseCurrency); $baseCurrencyPaymentAmountAsCurrency = Craft::t('commerce', 'Pay {amount} of {currency} on the order.', ['amount' => Currency::formatAsCurrency($baseCurrencyPaymentAmount, $baseCurrency), 'currency' => $baseCurrency]); $outstandingBalance = $order->outstandingBalance; @@ -1443,6 +1447,7 @@ private function _registerJavascript(array $variables): void if ($order->hasErrors()) { $response['order']['errors'] = $order->getErrors(); + $response['errors'] = $order->getErrors(); $response['error'] = Craft::t('commerce', 'The order is not valid.'); } diff --git a/src/controllers/TransfersController.php b/src/controllers/TransfersController.php index a6fcf304ba..0c278fdb69 100644 --- a/src/controllers/TransfersController.php +++ b/src/controllers/TransfersController.php @@ -197,7 +197,7 @@ public function actionReceiveTransfer(): Response $inventoryAcceptedMovement = new InventoryTransferMovement(); $inventoryAcceptedMovement->quantity = $acceptedAmount; $inventoryAcceptedMovement->transferId = $transfer->id; - $inventoryAcceptedMovement->inventoryItem = $detail->getInventoryItem(); + $inventoryAcceptedMovement->setInventoryItem($detail->getInventoryItem()); $inventoryAcceptedMovement->toInventoryLocation = $transfer->getDestinationLocation(); $inventoryAcceptedMovement->fromInventoryLocation = $transfer->getDestinationLocation(); // we are moving from incoming to available $inventoryAcceptedMovement->toInventoryTransactionType = InventoryTransactionType::AVAILABLE; @@ -213,9 +213,9 @@ public function actionReceiveTransfer(): Response $inventoryRejectedMovement = new UpdateInventoryLevel(); $inventoryRejectedMovement->quantity = $rejectedAmount * -1; $inventoryRejectedMovement->updateAction = InventoryUpdateQuantityType::ADJUST; - $inventoryRejectedMovement->inventoryItem = $detail->getInventoryItem(); + $inventoryRejectedMovement->inventoryItemId = $detail->inventoryItemId; $inventoryRejectedMovement->transferId = $transfer->id; - $inventoryRejectedMovement->inventoryLocation = $transfer->getDestinationLocation(); + $inventoryRejectedMovement->setInventoryLocation($transfer->getDestinationLocation()); $inventoryRejectedMovement->type = InventoryTransactionType::INCOMING->value; $inventoryUpdateCollection->push($inventoryRejectedMovement); diff --git a/src/elements/Product.php b/src/elements/Product.php index 20526d32c8..8261a18620 100644 --- a/src/elements/Product.php +++ b/src/elements/Product.php @@ -565,6 +565,45 @@ protected static function defineDefaultTableAttributes(string $source): array return $attributes; } + /** + * @inheritdoc + */ + public static function attributePreviewHtml(array $attribute): mixed + { + return match ($attribute['value']) { + 'defaultSku' => $attribute['placeholder'], + default => parent::attributePreviewHtml($attribute) + }; + } + + /** + * @inheritdoc + */ + protected static function defineCardAttributes(): array + { + return array_merge(parent::defineCardAttributes(), [ + 'defaultPrice' => [ + 'label' => Craft::t('commerce', 'Price'), + 'placeholder' => '¤' . Craft::$app->getFormattingLocale()->getFormatter()->asDecimal(123.99), + ], + 'defaultSku' => [ + 'label' => Craft::t('commerce', 'SKU'), + 'placeholder' => Html::tag('code', 'SKU123'), + ], + ]); + } + + /** + * @inheritdoc + */ + protected static function defineDefaultCardAttributes(): array + { + return array_merge(parent::defineDefaultCardAttributes(), [ + 'defaultSku', + 'defaultPrice', + ]); + } + /** * @inheritdoc */ @@ -1065,6 +1104,18 @@ public function getVariants(bool $includeDisabled = false): VariantCollection return $this->_variants->filter(fn(Variant $variant) => $includeDisabled || ($variant->getStatus() === self::STATUS_ENABLED)); } + /** + * @return VariantCollection + * @throws InvalidConfigException + * @internal Do not use. Temporary method until we get a nested element manager provider in core. + * + * TODO: Remove this once we have a nested element manager provider interface in core. + */ + public function getAllVariants(): VariantCollection + { + return $this->getVariants(true); + } + /** * @inheritdoc */ @@ -1187,9 +1238,9 @@ public function getVariantManager(): NestedElementManager /** @phpstan-ignore-next-line */ fn(Product $product) => self::createVariantQuery($product), [ - 'attribute' => 'variants', + 'attribute' => 'allVariants', // TODO: can change this back to 'variants' once we have a nested element manager provider in core. 'propagationMethod' => $this->getType()->propagationMethod, - 'valueGetter' => fn(Product $product) => $product->getVariants(true), + 'valueSetter' => fn($variants) => $this->setVariants($variants), // TODO: can change this back to 'variants' once we have a nested element manager provider in core. ], ); } diff --git a/src/elements/Transfer.php b/src/elements/Transfer.php index f59e49e689..df1b16120e 100644 --- a/src/elements/Transfer.php +++ b/src/elements/Transfer.php @@ -82,11 +82,17 @@ public function __toString(): string ]); } + /** + * @inheritdoc + */ public static function hasDrafts(): bool { return false; } + /** + * @inheritdoc + */ protected function metadata(): array { $additionalMeta = []; @@ -191,7 +197,7 @@ public function setTransferStatus(TransferStatusType|string $status): void } /** - * @inheritDoc + * @inheritdoc */ public static function displayName(): string { @@ -199,7 +205,7 @@ public static function displayName(): string } /** - * @inheritDoc + * @inheritdoc */ public static function lowerDisplayName(): string { @@ -207,7 +213,7 @@ public static function lowerDisplayName(): string } /** - * @inheritDoc + * @inheritdoc */ public static function pluralDisplayName(): string { @@ -215,7 +221,7 @@ public static function pluralDisplayName(): string } /** - * @inheritDoc + * @inheritdoc */ public static function pluralLowerDisplayName(): string { @@ -223,7 +229,7 @@ public static function pluralLowerDisplayName(): string } /** - * @inheritDoc + * @inheritdoc */ public static function refHandle(): ?string { @@ -231,7 +237,7 @@ public static function refHandle(): ?string } /** - * @inheritDoc + * @inheritdoc */ public static function trackChanges(): bool { @@ -239,7 +245,7 @@ public static function trackChanges(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function hasTitles(): bool { @@ -247,7 +253,7 @@ public static function hasTitles(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function hasContent(): bool { @@ -255,7 +261,7 @@ public static function hasContent(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function hasUris(): bool { @@ -263,7 +269,7 @@ public static function hasUris(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function isLocalized(): bool { @@ -271,7 +277,7 @@ public static function isLocalized(): bool } /** - * @inheritDoc + * @inheritdoc */ public static function hasStatuses(): bool { @@ -280,7 +286,7 @@ public static function hasStatuses(): bool /** * @return TransferQuery - * @inheritDoc + * @inheritdoc */ public static function find(): ElementQueryInterface { @@ -288,7 +294,7 @@ public static function find(): ElementQueryInterface } /** - * @inheritDoc + * @inheritdoc */ public static function createCondition(): ElementConditionInterface { @@ -296,7 +302,7 @@ public static function createCondition(): ElementConditionInterface } /** - * @inheritDoc + * @inheritdoc */ protected static function includeSetStatusAction(): bool { @@ -331,7 +337,7 @@ protected static function defineSortOptions(): array } /** - * @inheritDoc + * @inheritdoc */ protected static function defineTableAttributes(): array { @@ -347,7 +353,7 @@ protected static function defineTableAttributes(): array } /** - * @inheritDoc + * @inheritdoc */ protected static function defineDefaultTableAttributes(string $source): array { @@ -359,7 +365,7 @@ protected static function defineDefaultTableAttributes(string $source): array } /** - * @inheritDoc + * @inheritdoc */ protected function attributeHtml(string $attribute): string { @@ -384,7 +390,7 @@ protected function attributeHtml(string $attribute): string } /** - * @inheritDoc + * @inheritdoc */ protected function defineRules(): array { @@ -436,7 +442,7 @@ public function validateLocations($attribute, $params, $validator) } /** - * @inheritDoc + * @inheritdoc */ public function getUriFormat(): ?string { @@ -480,7 +486,7 @@ protected static function defineSources(string $context = null): array /** * - * @inheritDoc + * @inheritdoc */ protected function previewTargets(): array { @@ -497,6 +503,9 @@ protected function previewTargets(): array return $previewTargets; } + /** + * @inheritdoc + */ protected function safeActionMenuItems(): array { $safeActions = parent::safeActionMenuItems(); @@ -516,9 +525,8 @@ protected function safeActionMenuItems(): array return $safeActions; } - /** - * @inheritDoc + * @inheritdoc */ protected function route(): array|string|null { @@ -533,7 +541,7 @@ protected function route(): array|string|null } /** - * @inheritDoc + * @inheritdoc */ public function canView(User $user): bool { @@ -545,7 +553,7 @@ public function canView(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ public function canSave(User $user): bool { @@ -557,7 +565,7 @@ public function canSave(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ public function canDuplicate(User $user): bool { @@ -565,7 +573,7 @@ public function canDuplicate(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ public function canDelete(User $user): bool { @@ -583,7 +591,7 @@ public function canDelete(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ public function canCreateDrafts(User $user): bool { @@ -591,7 +599,7 @@ public function canCreateDrafts(User $user): bool } /** - * @inheritDoc + * @inheritdoc */ protected function cpEditUrl(): ?string { @@ -599,7 +607,7 @@ protected function cpEditUrl(): ?string } /** - * @inheritDoc + * @inheritdoc */ public function getPostEditUrl(): ?string { @@ -607,7 +615,7 @@ public function getPostEditUrl(): ?string } /** - * @inheritDoc + * @inheritdoc */ public function prepareEditScreen(Response $response, string $containerId): void { @@ -735,7 +743,7 @@ public function addDetail(TransferDetail $detail): void } /** - * @inheritDoc + * @inheritdoc */ public function getFieldLayout(): ?FieldLayout { @@ -743,7 +751,7 @@ public function getFieldLayout(): ?FieldLayout } /** - * @inheritDoc + * @inheritdoc */ public function beforeValidate() { @@ -755,7 +763,7 @@ public function beforeValidate() } /** - * @inheritDoc + * @inheritdoc */ public function afterSave(bool $isNew): void { @@ -779,26 +787,26 @@ public function afterSave(bool $isNew): void if ($this->getTransferStatus() === TransferStatusType::PENDING && $originalTransferStatus == TransferStatusType::DRAFT->value) { $inventoryUpdateCollection = new UpdateInventoryLevelCollection(); foreach ($this->getDetails() as $detail) { - $inventoryUpdate1 = new UpdateInventoryLevelInTransfer([ - 'type' => InventoryTransactionType::INCOMING->value, - 'updateAction' => InventoryUpdateQuantityType::ADJUST, - 'inventoryItem' => $detail->getInventoryItem(), - 'transferId' => $this->id, - 'inventoryLocation' => $this->getDestinationLocation(), - 'quantity' => $detail->quantity, - 'note' => Craft::t('commerce', 'Incoming transfer from Transfer ID: ') . $this->id, - ]); + $inventoryUpdate1 = new UpdateInventoryLevelInTransfer(); + $inventoryUpdate1->type = InventoryTransactionType::INCOMING->value; + $inventoryUpdate1->updateAction = InventoryUpdateQuantityType::ADJUST; + $inventoryUpdate1->inventoryItemId = $detail->inventoryItemId; + $inventoryUpdate1->transferId = $this->id; + $inventoryUpdate1->inventoryLocationId = $this->destinationLocationId; + $inventoryUpdate1->quantity = $detail->quantity; + $inventoryUpdate1->note = Craft::t('commerce', 'Incoming transfer from Transfer ID: ') . $this->id; + $inventoryUpdateCollection->push($inventoryUpdate1); - $inventoryUpdate2 = new UpdateInventoryLevelInTransfer([ - 'type' => 'onHand', - 'updateAction' => InventoryUpdateQuantityType::ADJUST, - 'inventoryItem' => $detail->getInventoryItem(), - 'transferId' => $this->id, - 'inventoryLocation' => $this->getOriginLocation(), - 'quantity' => $detail->quantity * -1, - 'note' => Craft::t('commerce', 'Outgoing transfer from Transfer ID: ') . $this->id, - ]); + $inventoryUpdate2 = new UpdateInventoryLevelInTransfer(); + $inventoryUpdate2->type = 'onHand'; + $inventoryUpdate2->updateAction = InventoryUpdateQuantityType::ADJUST; + $inventoryUpdate2->inventoryItemId = $detail->inventoryItemId; + $inventoryUpdate2->transferId = $this->id; + $inventoryUpdate2->inventoryLocationId = $this->originLocationId; + $inventoryUpdate2->quantity = $detail->quantity * -1; + $inventoryUpdate2->note = Craft::t('commerce', 'Outgoing transfer from Transfer ID: ') . $this->id; + $inventoryUpdateCollection->push($inventoryUpdate2); } diff --git a/src/elements/Variant.php b/src/elements/Variant.php index f0ddc22b28..7ef4f42117 100755 --- a/src/elements/Variant.php +++ b/src/elements/Variant.php @@ -740,6 +740,14 @@ public function canView(User $user): bool return $product->canView($user); } + /** + * @inheritdoc + */ + public function getCpEditUrl(): ?string + { + return $this->getOwner() ? $this->getOwner()->getCpEditUrl() : null; + } + /** * @inheritdoc */ @@ -1060,6 +1068,7 @@ public function afterSave(bool $isNew): void parent::afterSave($isNew); if (!$this->propagating && $this->isDefault && $ownerId && $this->duplicateOf === null) { + // @TODO - this data is now joined in on the product query so can be removed at the next breaking change $defaultData = [ 'defaultVariantId' => $this->id, 'defaultSku' => $this->getSkuAsText(), @@ -1363,6 +1372,18 @@ protected static function defineSearchableAttributes(): array return [...parent::defineSearchableAttributes(), ...['productTitle']]; } + /** + * @inheritdoc + */ + protected static function defineCardAttributes(): array + { + return array_merge(parent::defineCardAttributes(), [ + 'product' => [ + 'label' => Craft::t('commerce', 'Product'), + ], + ]); + } + /** * @inheritdoc */ diff --git a/src/elements/conditions/orders/CouponCodeConditionRule.php b/src/elements/conditions/orders/CouponCodeConditionRule.php new file mode 100644 index 0000000000..a6c231899f --- /dev/null +++ b/src/elements/conditions/orders/CouponCodeConditionRule.php @@ -0,0 +1,57 @@ + + * @since 5.3.0 + */ +class CouponCodeConditionRule extends OrderTextValuesAttributeConditionRule +{ + public string $orderAttribute = 'couponCode'; + + /** + * @inheritdoc + */ + public function getLabel(): string + { + return Craft::t('commerce', 'Coupon Code'); + } + + /** + * @inheritdoc + */ + protected function matchValue(mixed $value): bool + { + switch ($this->operator) { + case self::OPERATOR_EMPTY: + return !$value; + case self::OPERATOR_NOT_EMPTY: + return (bool)$value; + } + + if ($this->value === '') { + return true; + } + + return match ($this->operator) { + self::OPERATOR_EQ => strcasecmp($value, $this->value) === 0, + self::OPERATOR_NE => strcasecmp($value, $this->value) !== 0, + self::OPERATOR_BEGINS_WITH => is_string($value) && StringHelper::startsWith($value, $this->value, false), + self::OPERATOR_ENDS_WITH => is_string($value) && StringHelper::endsWith($value, $this->value, false), + self::OPERATOR_CONTAINS => is_string($value) && StringHelper::contains($value, $this->value, false), + default => throw new InvalidConfigException("Invalid operator: $this->operator"), + }; + } +} diff --git a/src/elements/conditions/orders/DiscountOrderCondition.php b/src/elements/conditions/orders/DiscountOrderCondition.php index 14c7a730ad..4b35288076 100644 --- a/src/elements/conditions/orders/DiscountOrderCondition.php +++ b/src/elements/conditions/orders/DiscountOrderCondition.php @@ -5,6 +5,7 @@ use craft\commerce\base\HasStoreInterface; use craft\commerce\base\StoreTrait; use craft\elements\db\ElementQueryInterface; +use craft\helpers\ArrayHelper; use yii\base\NotSupportedException; /** @@ -41,7 +42,12 @@ protected function config(): array */ protected function selectableConditionRules(): array { - return array_merge(parent::selectableConditionRules(), []); + $rules = array_merge(parent::selectableConditionRules(), []); + + // We don't need the condition to have the coupon code rule + ArrayHelper::removeValue($rules, CouponCodeConditionRule::class); + + return $rules; } /** diff --git a/src/elements/conditions/orders/OrderCondition.php b/src/elements/conditions/orders/OrderCondition.php index dd9e894c1c..15330cc8d0 100644 --- a/src/elements/conditions/orders/OrderCondition.php +++ b/src/elements/conditions/orders/OrderCondition.php @@ -30,8 +30,9 @@ protected function selectableConditionRules(): array { return array_merge(parent::selectableConditionRules(), [ DateOrderedConditionRule::class, - CustomerConditionRule::class, CompletedConditionRule::class, + CouponCodeConditionRule::class, + CustomerConditionRule::class, PaidConditionRule::class, HasPurchasableConditionRule::class, ItemSubtotalConditionRule::class, diff --git a/src/elements/conditions/variants/ProductConditionRule.php b/src/elements/conditions/variants/ProductConditionRule.php new file mode 100644 index 0000000000..c2464ab969 --- /dev/null +++ b/src/elements/conditions/variants/ProductConditionRule.php @@ -0,0 +1,68 @@ + + * @since 5.3.0 + */ +class ProductConditionRule extends BaseElementSelectConditionRule implements ElementConditionRuleInterface +{ + /** + * @inheritdoc + */ + protected function elementType(): string + { + return Product::class; + } + + /** + * @inheritdoc + */ + public function getLabel(): string + { + return Craft::t('commerce', 'Product'); + } + + /** + * @inheritdoc + */ + public function getExclusiveQueryParams(): array + { + return ['product', 'productId', 'primaryOwnerId', 'primaryOwner', 'owner', 'ownerId']; + } + + /** + * @inheritdoc + */ + public function modifyQuery(ElementQueryInterface $query): void + { + /** @var VariantQuery $query */ + $query->ownerId($this->getElementId()); + } + + /** + * @inheritdoc + */ + public function matchElement(ElementInterface $element): bool + { + /** @var Variant $element */ + return $element->getOwnerId() == $this->getElementId(); + } +} diff --git a/src/elements/conditions/variants/VariantCondition.php b/src/elements/conditions/variants/VariantCondition.php index 311b7baffd..49c3530b53 100644 --- a/src/elements/conditions/variants/VariantCondition.php +++ b/src/elements/conditions/variants/VariantCondition.php @@ -25,6 +25,7 @@ class VariantCondition extends ElementCondition protected function selectableConditionRules(): array { return array_merge(parent::selectableConditionRules(), [ + ProductConditionRule::class, SkuConditionRule::class, ]); } diff --git a/src/elements/db/OrderQuery.php b/src/elements/db/OrderQuery.php index fe483afcce..e91c433b43 100644 --- a/src/elements/db/OrderQuery.php +++ b/src/elements/db/OrderQuery.php @@ -59,6 +59,12 @@ class OrderQuery extends ElementQuery */ public mixed $reference = null; + /** + * @var mixed The order reference of the resulting order. + * @used-by couponCode() + */ + public mixed $couponCode = null; + /** * @var mixed The email address the resulting orders must have. */ @@ -372,6 +378,48 @@ public function reference(mixed $value): OrderQuery return $this; } + /** + * Narrows the query results based on the order's coupon code. + * + * Possible values include: + * + * | Value | Fetches {elements}… + * | - | - + * | `':empty:'` | that don’t have a coupon code. + * | `':notempty:'` | that have a coupon code. + * | `'Foo'` | with a coupon code of `Foo`. + * | `'Foo*'` | with a coupon code that begins with `Foo`. + * | `'*Foo'` | with a coupon code that ends with `Foo`. + * | `'*Foo*'` | with a coupon code that contains `Foo`. + * | `'not *Foo*'` | with a coupon code that doesn’t contain `Foo`. + * | `['*Foo*', '*Bar*']` | with a coupon code that contains `Foo` or `Bar`. + * | `['not', '*Foo*', '*Bar*']` | with a coupon code that doesn’t contain `Foo` or `Bar`. + * + * --- + * + * ```twig + * {# Fetch the requested {element} #} + * {% set {element-var} = {twig-method} + * .reference('foo') + * .one() %} + * ``` + * + * ```php + * // Fetch the requested {element} + * ${element-var} = {php-method} + * ->reference('foo') + * ->one(); + * ``` + * + * @param string|null $value The property value + * @return static self reference + */ + public function couponCode(mixed $value): OrderQuery + { + $this->couponCode = $value; + return $this; + } + /** * Narrows the query results based on the customers’ email addresses. * @@ -1602,6 +1650,11 @@ protected function beforePrepare(): bool $this->subQuery->andWhere(Db::parseParam('commerce_orders.reference', $this->reference)); } + if (isset($this->couponCode)) { + // Coupon code criteria is case-insensitive like in the adjuster + $this->subQuery->andWhere(Db::parseParam('commerce_orders.couponCode', $this->couponCode, caseInsensitive: true)); + } + if (isset($this->email) && $this->email) { // Join and search the users table for email address $this->subQuery->leftJoin(CraftTable::USERS . ' users', '[[users.id]] = [[commerce_orders.customerId]]'); diff --git a/src/elements/db/ProductQuery.php b/src/elements/db/ProductQuery.php index 28be43d670..745a542cf7 100644 --- a/src/elements/db/ProductQuery.php +++ b/src/elements/db/ProductQuery.php @@ -768,18 +768,20 @@ protected function beforePrepare(): bool 'commerce_products.postDate', 'commerce_products.expiryDate', 'subquery.price as defaultPrice', - 'commerce_products.defaultPrice as defaultBasePrice', + 'purchasablesstores.basePrice as defaultBasePrice', 'commerce_products.defaultVariantId', - 'commerce_products.defaultSku', - 'commerce_products.defaultWeight', - 'commerce_products.defaultLength', - 'commerce_products.defaultWidth', - 'commerce_products.defaultHeight', + 'purchasables.sku as defaultSku', + 'purchasables.weight as defaultWeight', + 'purchasables.length as defaultLength', + 'purchasables.width as defaultWidth', + 'purchasables.height as defaultHeight', 'sitestores.storeId', ]); // Join in sites stores to get product's store for current request $this->query->leftJoin(['sitestores' => Table::SITESTORES], '[[elements_sites.siteId]] = [[sitestores.siteId]]'); + $this->query->leftJoin(['purchasables' => Table::PURCHASABLES], '[[purchasables.id]] = [[commerce_products.defaultVariantId]]'); + $this->query->leftJoin(['purchasablesstores' => Table::PURCHASABLES_STORES], '[[purchasablesstores.purchasableId]] = [[commerce_products.defaultVariantId]] and [[sitestores.storeId]] = [[purchasablesstores.storeId]]'); $this->subQuery->addSelect(['catalogprices.price']); diff --git a/src/migrations/Install.php b/src/migrations/Install.php index defc1bbf0c..756520a8ec 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -857,17 +857,17 @@ public function createTables(): void 'handle' => $this->string()->notNull(), 'primary' => $this->boolean()->notNull(), 'currency' => $this->string()->notNull()->defaultValue('USD'), - 'autoSetCartShippingMethodOption' => $this->boolean()->notNull()->defaultValue(false), - 'autoSetNewCartAddresses' => $this->boolean()->notNull()->defaultValue(false), - 'autoSetPaymentSource' => $this->boolean()->notNull()->defaultValue(false), - 'allowEmptyCartOnCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'allowCheckoutWithoutPayment' => $this->boolean()->notNull()->defaultValue(false), - 'allowPartialPaymentOnCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'requireShippingAddressAtCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'requireBillingAddressAtCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'requireShippingMethodSelectionAtCheckout' => $this->boolean()->notNull()->defaultValue(false), - 'useBillingAddressForTax' => $this->boolean()->notNull()->defaultValue(false), - 'validateOrganizationTaxIdAsVatId' => $this->boolean()->notNull()->defaultValue(false), + 'autoSetCartShippingMethodOption' => $this->string()->notNull()->defaultValue('false'), + 'autoSetNewCartAddresses' => $this->string()->notNull()->defaultValue('false'), + 'autoSetPaymentSource' => $this->string()->notNull()->defaultValue('false'), + 'allowEmptyCartOnCheckout' => $this->string()->notNull()->defaultValue('false'), + 'allowCheckoutWithoutPayment' => $this->string()->notNull()->defaultValue('false'), + 'allowPartialPaymentOnCheckout' => $this->string()->notNull()->defaultValue('false'), + 'requireShippingAddressAtCheckout' => $this->string()->notNull()->defaultValue('false'), + 'requireBillingAddressAtCheckout' => $this->string()->notNull()->defaultValue('false'), + 'requireShippingMethodSelectionAtCheckout' => $this->string()->notNull()->defaultValue('false'), + 'useBillingAddressForTax' => $this->string()->notNull()->defaultValue('false'), + 'validateOrganizationTaxIdAsVatId' => $this->string()->notNull()->defaultValue('false'), 'orderReferenceFormat' => $this->string(), 'freeOrderPaymentStrategy' => $this->string()->defaultValue('complete'), 'minimumTotalPriceStrategy' => $this->string()->defaultValue('default'), diff --git a/src/migrations/m241204_091901_fix_store_environment_variables.php b/src/migrations/m241204_091901_fix_store_environment_variables.php new file mode 100644 index 0000000000..279ac79e65 --- /dev/null +++ b/src/migrations/m241204_091901_fix_store_environment_variables.php @@ -0,0 +1,92 @@ +from(Table::STORES) + ->all(); + + // Get the store settings for each store from the project config + $storeSettings = \Craft::$app->getProjectConfig()->get('commerce.stores'); + + + // Store properties to update + $storeProperties = [ + 'autoSetNewCartAddresses', + 'autoSetCartShippingMethodOption', + 'autoSetPaymentSource', + 'allowEmptyCartOnCheckout', + 'allowCheckoutWithoutPayment', + 'allowPartialPaymentOnCheckout', + 'requireShippingAddressAtCheckout', + 'requireBillingAddressAtCheckout', + 'requireShippingMethodSelectionAtCheckout', + 'useBillingAddressForTax', + 'validateOrganizationTaxIdAsVatId', + ]; + + // Update stores env var DB columns + foreach ($storeProperties as $storeProperty) { + $this->alterColumn(Table::STORES, $storeProperty, $this->string()->notNull()->defaultValue('false')); + } + + // Loop through each store and update values in the DB to match the PC values + foreach ($stores as $store) { + $storeSettingsForStore = $storeSettings[$store['uid']] ?? null; + + // If there isn't data in the PC for this store, skip it + if (!$storeSettingsForStore) { + continue; + } + + $updateData = []; + foreach ($storeProperties as $storeProperty) { + // If there isn't data in the PC for this store property, skip it + if (!isset($storeSettingsForStore[$storeProperty])) { + continue; + } + + // Parse the value from the PC + $envVarValue = App::parseBooleanEnv($storeSettingsForStore[$storeProperty]); + if ($envVarValue === null) { + continue; + } + + $updateData[$storeProperty] = $storeSettingsForStore[$storeProperty]; + } + + if (empty($updateData)) { + continue; + } + + $this->update(Table::STORES, $updateData, ['id' => $store['id']]); + } + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m241204_091901_fix_store_environment_variables cannot be reverted.\n"; + return false; + } +} diff --git a/src/models/inventory/InventoryManualMovement.php b/src/models/inventory/InventoryManualMovement.php index e3e98f5b3d..7d0ed0a7ec 100644 --- a/src/models/inventory/InventoryManualMovement.php +++ b/src/models/inventory/InventoryManualMovement.php @@ -83,7 +83,7 @@ public function fromLocationAfterQuantity(): int ->from(Table::INVENTORYTRANSACTIONS) ->where([ 'type' => $this->fromInventoryTransactionType->value, - 'inventoryItemId' => $this->inventoryItem->id, + 'inventoryItemId' => $this->inventoryItemId, 'inventoryLocationId' => $this->fromInventoryLocation->id, ]) ->params([':quantity' => $this->quantity]) @@ -112,7 +112,7 @@ public function toLocationAfterQuantity(): int ->from(Table::INVENTORYTRANSACTIONS) ->where([ 'type' => $this->toInventoryTransactionType->value, - 'inventoryItemId' => $this->inventoryItem->id, + 'inventoryItemId' => $this->inventoryItemId, 'inventoryLocationId' => $this->toInventoryLocation->id, ]) ->params([':quantity' => $this->quantity]) diff --git a/src/models/inventory/InventoryTransferMovement.php b/src/models/inventory/InventoryTransferMovement.php index 558c52cc13..cbe7d8506d 100644 --- a/src/models/inventory/InventoryTransferMovement.php +++ b/src/models/inventory/InventoryTransferMovement.php @@ -9,12 +9,4 @@ */ class InventoryTransferMovement extends InventoryMovement { - /** - * @return array - */ - public function defineRules(): array - { - $rules = parent::defineRules(); - return $rules; - } } diff --git a/src/models/inventory/UpdateInventoryLevel.php b/src/models/inventory/UpdateInventoryLevel.php index 57f8574059..38ac79cd63 100644 --- a/src/models/inventory/UpdateInventoryLevel.php +++ b/src/models/inventory/UpdateInventoryLevel.php @@ -3,10 +3,10 @@ namespace craft\commerce\models\inventory; use craft\base\Model; +use craft\commerce\base\InventoryItemTrait; +use craft\commerce\base\InventoryLocationTrait; use craft\commerce\enums\InventoryTransactionType; use craft\commerce\enums\InventoryUpdateQuantityType; -use craft\commerce\models\InventoryItem; -use craft\commerce\models\InventoryLocation; /** * Update (Set and Adjust) Inventory Quantity model @@ -15,6 +15,8 @@ */ class UpdateInventoryLevel extends Model { + use InventoryItemTrait, InventoryLocationTrait; + /** * The type is the set of InventoryTransactionType values, plus the `onHand` type. * @var string The inventory update type. @@ -32,16 +34,6 @@ class UpdateInventoryLevel extends Model */ public InventoryUpdateQuantityType $updateAction; - /** - * @var InventoryItem The inventory item - */ - public InventoryItem $inventoryItem; - - /** - * @var InventoryLocation The inventory location. - */ - public InventoryLocation $inventoryLocation; - /** * @var int The quantity to update. */ diff --git a/src/models/inventory/UpdateInventoryLevelInTransfer.php b/src/models/inventory/UpdateInventoryLevelInTransfer.php index 675e25da1e..e85aee3cd9 100644 --- a/src/models/inventory/UpdateInventoryLevelInTransfer.php +++ b/src/models/inventory/UpdateInventoryLevelInTransfer.php @@ -3,10 +3,10 @@ namespace craft\commerce\models\inventory; use craft\base\Model; +use craft\commerce\base\InventoryItemTrait; +use craft\commerce\base\InventoryLocationTrait; use craft\commerce\enums\InventoryTransactionType; use craft\commerce\enums\InventoryUpdateQuantityType; -use craft\commerce\models\InventoryItem; -use craft\commerce\models\InventoryLocation; /** * Update (Set and Adjust) Inventory Quantity model @@ -15,6 +15,8 @@ */ class UpdateInventoryLevelInTransfer extends Model { + use InventoryItemTrait, InventoryLocationTrait; + /** * The type is the set of InventoryTransactionType values, plus the `onHand` type. * @var string The inventory update type. @@ -32,16 +34,6 @@ class UpdateInventoryLevelInTransfer extends Model */ public InventoryUpdateQuantityType $updateAction; - /** - * @var InventoryItem The inventory item - */ - public InventoryItem $inventoryItem; - - /** - * @var InventoryLocation The inventory location. - */ - public InventoryLocation $inventoryLocation; - /** * @var int The quantity to update. */ diff --git a/src/services/Inventory.php b/src/services/Inventory.php index 93107c4fba..9150a2f392 100644 --- a/src/services/Inventory.php +++ b/src/services/Inventory.php @@ -61,7 +61,7 @@ public function getInventoryLevelsForPurchasable(Purchasable $purchasable): Coll $storeInventoryLocations = Plugin::getInstance()->getInventoryLocations()->getInventoryLocations($storeId); foreach ($storeInventoryLocations as $inventoryLocation) { - $inventoryLevel = $this->getInventoryLevel($purchasable->getInventoryItem(), $inventoryLocation); + $inventoryLevel = $this->getInventoryLevel($purchasable->inventoryItemId, $inventoryLocation->id); if (!$inventoryLevel) { continue; @@ -115,17 +115,20 @@ public function getInventoryItemsByIds(array $ids): Collection /** * Returns an inventory level model which is the sum of all inventory movements types for an item in a location. * - * @param InventoryItem $inventoryItem - * @param InventoryLocation $inventoryLocation + * @param InventoryItem|int $inventoryItem + * @param InventoryLocation|int $inventoryLocation * @param bool $withTrashed * @return ?InventoryLevel */ - public function getInventoryLevel(InventoryItem $inventoryItem, InventoryLocation $inventoryLocation, bool $withTrashed = false): ?InventoryLevel + public function getInventoryLevel(InventoryItem|int $inventoryItem, InventoryLocation|int $inventoryLocation, bool $withTrashed = false): ?InventoryLevel { + $inventoryItemId = $inventoryItem instanceof InventoryItem ? $inventoryItem->id : $inventoryItem; + $inventoryLocationId = $inventoryLocation instanceof InventoryLocation ? $inventoryLocation->id : $inventoryLocation; + $result = $this->getInventoryLevelQuery(withTrashed: $withTrashed) ->andWhere([ - 'inventoryLocationId' => $inventoryLocation->id, - 'inventoryItemId' => $inventoryItem->id, + 'inventoryLocationId' => $inventoryLocationId, + 'inventoryItemId' => $inventoryItemId, ])->one(); if (!$result) { @@ -207,6 +210,7 @@ public function getInventoryLocationLevels(InventoryLocation $inventoryLocation, { $levels = $this->getInventoryLevelQuery(withTrashed: $withTrashed) ->andWhere(['inventoryLocationId' => $inventoryLocation->id]) + ->andWhere(['not', ['elements.id' => null]]) ->collect(); $inventoryItems = Plugin::getInstance()->getInventory()->getInventoryItemsByIds($levels->pluck('inventoryItemId')->unique()->toArray()); @@ -262,8 +266,12 @@ public function getInventoryLevelQuery(?int $limit = null, ?int $offset = null, ->limit($limit) ->offset($offset); + $query->leftJoin( + ['elements' => CraftTable::ELEMENTS], + '[[ii.purchasableId]] = [[elements.id]] AND [[elements.draftId]] IS NULL AND [[elements.revisionId]] IS NULL' + ); + if (!$withTrashed) { - $query->leftJoin(['elements' => CraftTable::ELEMENTS], '[[ii.purchasableId]] = [[elements.id]]'); $query->andWhere(['elements.dateDeleted' => null]); } @@ -317,6 +325,59 @@ public function executeUpdateInventoryLevels(UpdateInventoryLevelCollection $upd } } + /** + * @param int $inventoryItemId + * @param int $quantity + * @param array $updateInventoryLevelAttributes + * @return void + * @throws Exception + * @throws InvalidConfigException + * @since 5.3.0 + */ + public function updateInventoryLevel(int $inventoryItemId, int $quantity, array $updateInventoryLevelAttributes = []) + { + $updateInventoryLevelAttributes += [ + 'quantity' => $quantity, + 'updateAction' => InventoryUpdateQuantityType::SET, + 'inventoryLocationId' => Plugin::getInstance()->getInventoryLocations()->getAllInventoryLocations()->first()->id, + 'type' => InventoryTransactionType::AVAILABLE->value, + ]; + + $updateInventoryLevel = new UpdateInventoryLevel($updateInventoryLevelAttributes); + $updateInventoryLevel->inventoryItemId = $inventoryItemId; + + $updateInventoryLevels = UpdateInventoryLevelCollection::make(); + $updateInventoryLevels->push($updateInventoryLevel); + + Plugin::getInstance()->getInventory()->executeUpdateInventoryLevels($updateInventoryLevels); + } + + /** + * @param Purchasable $purchasable + * @param int $quantity + * @param array $updateInventoryLevelAttributes + * @return void + * @throws Exception + * @throws InvalidConfigException + * @throws \craft\errors\DeprecationException + * @since 5.3.0 + */ + public function updatePurchasableInventoryLevel(Purchasable $purchasable, int $quantity, array $updateInventoryLevelAttributes = []) + { + $updateInventoryLevelAttributes += [ + 'quantity' => $quantity, + 'updateAction' => InventoryUpdateQuantityType::SET, + 'inventoryItemId' => $purchasable->inventoryItemId, + 'inventoryLocationId' => $purchasable->getStore()->getInventoryLocations()->first()->id, + 'type' => InventoryTransactionType::AVAILABLE->value, + ]; + + $this->updateInventoryLevel($purchasable->inventoryItemId, $quantity, $updateInventoryLevelAttributes); + + // Clear the stock cache for the class instance + unset($purchasable->stock); + } + /** * @param UpdateInventoryLevel|UpdateInventoryLevelInTransfer $updateInventoryLevel * @return bool @@ -335,8 +396,8 @@ private function _setInventoryLevel(UpdateInventoryLevel|UpdateInventoryLevelInT ->from($tableName) ->where([ 'type' => $types, - 'inventoryItemId' => $updateInventoryLevel->inventoryItem->id, - 'inventoryLocationId' => $updateInventoryLevel->inventoryLocation->id, + 'inventoryItemId' => $updateInventoryLevel->inventoryItemId, + 'inventoryLocationId' => $updateInventoryLevel->inventoryLocationId, ]) ->params([':quantity' => $updateInventoryLevel->quantity]) ->scalar(); @@ -349,8 +410,8 @@ private function _setInventoryLevel(UpdateInventoryLevel|UpdateInventoryLevelInT $data = [ 'quantity' => $quantityQuery, 'type' => $type, - 'inventoryItemId' => $updateInventoryLevel->inventoryItem->id, - 'inventoryLocationId' => $updateInventoryLevel->inventoryLocation->id, + 'inventoryItemId' => $updateInventoryLevel->inventoryItemId, + 'inventoryLocationId' => $updateInventoryLevel->inventoryLocationId, 'note' => $updateInventoryLevel->note, 'movementHash' => $this->getMovementHash(), 'dateCreated' => Db::prepareDateForDb(new \DateTime()), @@ -384,8 +445,8 @@ private function _adjustInventoryLevel(UpdateInventoryLevel|UpdateInventoryLevel ->insert($tableName, [ 'quantity' => $updateInventoryLevel->quantity, 'type' => $type, - 'inventoryItemId' => $updateInventoryLevel->inventoryItem->id, - 'inventoryLocationId' => $updateInventoryLevel->inventoryLocation->id, + 'inventoryItemId' => $updateInventoryLevel->inventoryItemId, + 'inventoryLocationId' => $updateInventoryLevel->inventoryLocationId, 'movementHash' => $this->getMovementHash(), 'dateCreated' => Db::prepareDateForDb(new \DateTime()), 'note' => $updateInventoryLevel->note, @@ -483,13 +544,16 @@ public function getMovementHash(): string } /** - * @param InventoryItem $inventoryItem - * @param InventoryLocation $inventoryLocation + * @param InventoryItem|int $inventoryItem + * @param InventoryLocation|int $inventoryLocation * @return array */ - public function getUnfulfilledOrders(InventoryItem $inventoryItem, InventoryLocation $inventoryLocation): array + public function getUnfulfilledOrders(InventoryItem|int $inventoryItem, InventoryLocation|int $inventoryLocation): array { - $inventoryLevel = $this->getInventoryLevel($inventoryItem, $inventoryLocation); + $inventoryItemId = $inventoryItem instanceof InventoryItem ? $inventoryItem->id : $inventoryItem; + $inventoryLocationId = $inventoryLocation instanceof InventoryLocation ? $inventoryLocation->id : $inventoryLocation; + + $inventoryLevel = $this->getInventoryLevel($inventoryItemId, $inventoryLocationId); if ($inventoryLevel->committedTotal <= 0) { return []; @@ -502,8 +566,8 @@ public function getUnfulfilledOrders(InventoryItem $inventoryItem, InventoryLoca ->leftJoin(['orders' => Table::ORDERS], '[[lineItems.orderId]] = [[orders.id]]') ->leftJoin(['it' => Table::INVENTORYTRANSACTIONS], '[[it.lineItemId]] = [[lineItems.id]]') ->where(['orders.isCompleted' => true]) - ->andWhere(['it.inventoryItemId' => $inventoryItem->id]) - ->andWhere(['it.inventoryLocationId' => $inventoryLocation->id]) + ->andWhere(['it.inventoryItemId' => $inventoryItemId]) + ->andWhere(['it.inventoryLocationId' => $inventoryLocationId]) ->andWhere(['it.type' => InventoryTransactionType::COMMITTED->value]) ->groupBy(['lineItems.orderId', 'lineItems.id']) ->having(['>=', 'SUM(it.quantity)', 'lineItems.qty']) @@ -615,6 +679,7 @@ public function getInventoryFulfillmentLevels(Order $order): Collection */ public function orderCompleteHandler(Order $order) { + /** @var Collection[] $allInventoryLevels */ $allInventoryLevels = []; $qtyLineItem = []; foreach ($order->getLineItems() as $lineItem) { @@ -637,7 +702,7 @@ public function orderCompleteHandler(Order $order) $selectedInventoryLevelForItem = []; /** * @var int $purchasableId - * @var InventoryLevel $inventoryLevels + * @var Collection $inventoryLevels */ foreach ($allInventoryLevels as $purchasableId => $inventoryLevels) { foreach ($inventoryLevels as $level) { @@ -680,15 +745,16 @@ public function orderCompleteHandler(Order $order) $availableTotalByPurchasableIdAndLocationId[$lineItem->purchasableId . '-' . $level->inventoryLocationId] -= $lineItem->qty; } - $movements->push(new InventoryCommittedMovement([ - 'inventoryItem' => $level->getInventoryItem(), - 'fromInventoryLocation' => $level->getInventoryLocation(), - 'toInventoryLocation' => $level->getInventoryLocation(), - 'fromInventoryTransactionType' => InventoryTransactionType::AVAILABLE, - 'toInventoryTransactionType' => InventoryTransactionType::COMMITTED, - 'quantity' => $lineItem->qty, - 'lineItemId' => $lineItem->id, - ])); + $inventoryCommittedMovement = new InventoryCommittedMovement(); + $inventoryCommittedMovement->inventoryItemId = $level->inventoryItemId; + $inventoryCommittedMovement->fromInventoryLocation = $level->getInventoryLocation(); + $inventoryCommittedMovement->toInventoryLocation = $level->getInventoryLocation(); + $inventoryCommittedMovement->fromInventoryTransactionType = InventoryTransactionType::AVAILABLE; + $inventoryCommittedMovement->toInventoryTransactionType = InventoryTransactionType::COMMITTED; + $inventoryCommittedMovement->quantity = $lineItem->qty; + $inventoryCommittedMovement->lineItemId = $lineItem->id; + + $movements->push($inventoryCommittedMovement); } } @@ -713,15 +779,16 @@ public function orderCompleteHandler(Order $order) $availableTotalByPurchasableIdAndLocationId[$purchasableId . '-' . $level->inventoryLocationId] -= $qtyToReserve; - $movements->push(new InventoryManualMovement([ - 'inventoryItem' => $level->getInventoryItem(), - 'fromInventoryLocation' => $level->getInventoryLocation(), - 'toInventoryLocation' => $level->getInventoryLocation(), - 'fromInventoryTransactionType' => InventoryTransactionType::AVAILABLE, - 'toInventoryTransactionType' => InventoryTransactionType::RESERVED, - 'quantity' => $qtyToReserve, - 'lineItemId' => $lineItemId, - ])); + $inventoryManualMovement = new InventoryManualMovement(); + $inventoryManualMovement->inventoryItemId = $level->inventoryItemId; + $inventoryManualMovement->fromInventoryLocation = $level->getInventoryLocation(); + $inventoryManualMovement->toInventoryLocation = $level->getInventoryLocation(); + $inventoryManualMovement->fromInventoryTransactionType = InventoryTransactionType::AVAILABLE; + $inventoryManualMovement->toInventoryTransactionType = InventoryTransactionType::RESERVED; + $inventoryManualMovement->quantity = $qtyToReserve; + $inventoryManualMovement->lineItemId = $lineItemId; + + $movements->push($inventoryManualMovement); $qty -= $qtyToReserve; if ($qty <= 0) { diff --git a/src/services/InventoryLocations.php b/src/services/InventoryLocations.php index c92e806797..c5d4375c81 100644 --- a/src/services/InventoryLocations.php +++ b/src/services/InventoryLocations.php @@ -180,7 +180,7 @@ public function executeDeactivateInventoryLocation(DeactivateInventoryLocation $ $inventoryMovement = new InventoryLocationDeactivatedMovement(); $inventoryMovement->fromInventoryLocation = $deactivateInventoryLocation->inventoryLocation; $inventoryMovement->toInventoryLocation = $deactivateInventoryLocation->destinationInventoryLocation; - $inventoryMovement->inventoryItem = $inventoryLevel->getInventoryItem(); + $inventoryMovement->inventoryItemId = $inventoryLevel->inventoryItemId; $inventoryMovement->quantity = $inventoryLevel->getTotal($type); $inventoryMovement->fromInventoryTransactionType = $type; $inventoryMovement->toInventoryTransactionType = $type; @@ -283,6 +283,7 @@ private function _createInventoryLocationsQuery(bool $withTrashed = false): Quer 'dateCreated', 'dateUpdated', ]) + ->orderBy(['name' => SORT_ASC]) ->from([Table::INVENTORYLOCATIONS]); if (!$withTrashed) { diff --git a/src/services/LineItems.php b/src/services/LineItems.php index e7627fde24..812c1994cd 100644 --- a/src/services/LineItems.php +++ b/src/services/LineItems.php @@ -444,6 +444,7 @@ public function create(Order $order, array $params = [], LineItemType $type = Li } $params['class'] = LineItem::class; + /** @var LineItem $lineItem */ $lineItem = Craft::createObject($params); if ($lineItem->type === LineItemType::Purchasable) { diff --git a/src/templates/_components/gateways/_modalWrapper.twig b/src/templates/_components/gateways/_modalWrapper.twig index 06ab826d5a..948ff011b4 100644 --- a/src/templates/_components/gateways/_modalWrapper.twig +++ b/src/templates/_components/gateways/_modalWrapper.twig @@ -20,9 +20,15 @@ {{ formHtml|raw }}
- Payment Amount + {{ "Payment Amount"|t('commerce') }}
- + + {% set currencies = craft.commerce.paymentCurrencies.getAllPaymentCurrencies() %} {% set primaryCurrency = craft.commerce.paymentCurrencies.getPrimaryPaymentCurrency() %} @@ -77,7 +83,8 @@ function updatePrice(form) { - var price = form.find("input.paymentAmount").val(); + var price = form.find("input[name='paymentAmount[value]']").val(); + var locale = form.find("input[name='paymentAmount[locale]']").val(); $.ajax({ type: "POST", @@ -89,6 +96,7 @@ data: { 'action' : 'commerce/orders/payment-amount-data', 'paymentAmount': price, + 'locale': locale, 'paymentCurrency': form.find(".paymentCurrency").val(), 'orderId' : orderId }, diff --git a/src/templates/settings/producttypes/_edit.twig b/src/templates/settings/producttypes/_edit.twig index a290e42450..d607cb77d1 100644 --- a/src/templates/settings/producttypes/_edit.twig +++ b/src/templates/settings/producttypes/_edit.twig @@ -408,6 +408,7 @@ {{ forms.fieldLayoutDesignerField({ fieldLayout: productType.getProductFieldLayout(), + withCardViewDesigner: true, }) }}
@@ -417,6 +418,7 @@ {% namespace "variant-layout" %} {{ forms.fieldLayoutDesignerField({ fieldLayout: productType.getVariantFieldLayout(), + withCardViewDesigner: true, }) }} {% endnamespace %} diff --git a/tests/unit/elements/order/OrderQueryTest.php b/tests/unit/elements/order/OrderQueryTest.php index 2c06a75e6c..9c6720864e 100644 --- a/tests/unit/elements/order/OrderQueryTest.php +++ b/tests/unit/elements/order/OrderQueryTest.php @@ -63,6 +63,45 @@ public function emailDataProvider(): array ]; } + /** + * @param string $couponCode + * @param int $count + * @return void + * @dataProvider couponCodeDataProvider + */ + public function testCouponCode(?string $couponCode, int $count): void + { + $ordersFixture = $this->tester->grabFixture('orders'); + /** @var Order $order */ + $order = $ordersFixture->getElement('completed-new'); + + // Temporarily add a coupon code to an order + \craft\commerce\records\Order::updateAll(['couponCode' => 'foo'], ['id' => $order->id]); + + $orderQuery = Order::find(); + $orderQuery->couponCode($couponCode); + + self::assertCount($count, $orderQuery->all()); + + // Remove temporary coupon code + \craft\commerce\records\Order::updateAll(['couponCode' => null], ['id' => $order->id]); + } + + /** + * @return array[] + */ + public function couponCodeDataProvider(): array + { + return [ + 'normal' => ['foo', 1], + 'case-insensitive' => ['fOo', 1], + 'using-null' => [null, 3], + 'empty-code' => [':empty:', 2], + 'not-empty-code' => [':notempty:', 1], + 'no-results' => ['nope', 0], + ]; + } + /** * @param mixed $handle * @param int $count diff --git a/tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php b/tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php new file mode 100644 index 0000000000..1905ef5bb6 --- /dev/null +++ b/tests/unit/elements/order/conditions/CouponCodeConditionRuleTest.php @@ -0,0 +1,162 @@ + + * @since 5.3.0 + */ +class CouponCodeConditionRuleTest extends Unit +{ + /** + * @return array + */ + public function _fixtures(): array + { + return [ + 'orders' => [ + 'class' => OrdersFixture::class, + ], + ]; + } + + /** + * @group Order + * @dataProvider matchElementDataProvider + */ + public function testMatchElement(?string $coupon, string $operator = '=', ?string $orderCoupon = null, bool $expectedMatch = true): void + { + $condition = $this->_createCondition($coupon, $operator); + + $ordersFixture = $this->tester->grabFixture('orders'); + /** @var Order $order */ + $order = $ordersFixture->getElement('completed-new'); + + if ($orderCoupon) { + $order->couponCode = $orderCoupon; + } + + $match = $condition->matchElement($order); + + if ($expectedMatch) { + self::assertTrue($match); + } else { + self::assertFalse($match); + } + } + + /** + * @return array[] + */ + public function matchElementDataProvider(): array + { + return [ + 'match-equals' => ['coupon1', '=', 'coupon1', true], + 'match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN1', true], + 'no-match-equals' => ['coupon1', '=', 'coupon2', false], + 'no-match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN2', false], + 'no-match-equals-null' => ['coupon1', '=', null, false], + 'match-contains' => ['coupon1', '**', 'coupon1', true], + 'match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN1', true], + 'no-match-contains' => ['coupon1', '**', 'coupon2', false], + 'no-match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN2', false], + 'match-begins-with' => ['coupon', 'bw', 'coupon1', true], + 'match-begins-with-case-insensitive' => ['coupon', 'bw', 'cOuPoN1', true], + 'no-match-begins-with' => ['coupon', 'bw', 'foocoupon2', false], + 'no-match-begins-with-case-insensitive' => ['coupon', 'bw', 'foocOuPoN2', false], + 'match-ends-with' => ['pon1', 'ew', 'coupon1', true], + 'match-ends-with-case-insensitive' => ['pon1', 'ew', 'cOuPoN1', true], + 'no-match-ends-with' => ['pon2', 'ew', 'coupon2foo', false], + 'no-match-ends-with-case-insensitive' => ['pon2', 'ew', 'cOuPoN2foo', false], + ]; + } + + /** + * @group Order + * @dataProvider modifyQueryDataProvider + */ + public function testModifyQuery(?string $coupon, string $operator = '=', ?string $orderCoupon = null, int $expectedResults = 0): void + { + $condition = $this->_createCondition($coupon, $operator); + $orderFixture = $this->tester->grabFixture('orders'); + /** @var Order $order */ + $order = $orderFixture->getElement('completed-new'); + + // Temporarily add a coupon code to an order + \craft\commerce\records\Order::updateAll(['couponCode' => $orderCoupon], ['id' => $order->id]); + + $query = Order::find(); + $condition->modifyQuery($query); + + self::assertCount($expectedResults, $query->ids()); + + if ($expectedResults > 0) { + self::assertContainsEquals($order->id, $query->ids()); + } else { + self::assertEmpty($query->ids()); + } + + // Remove temporary coupon code + \craft\commerce\records\Order::updateAll(['couponCode' => null], ['id' => $order->id]); + } + + /** + * @return array[] + */ + public function modifyQueryDataProvider(): array + { + return [ + 'match-equals' => ['coupon1', '=', 'coupon1', 1], + 'match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN1', 1], + 'no-match-equals' => ['coupon1', '=', 'coupon2', 0], + 'no-match-equals-case-insensitive' => ['coupon1', '=', 'cOuPoN2', 0], + 'no-match-equals-null' => ['coupon1', '=', null, 0], + 'match-contains' => ['coupon1', '**', 'coupon1', 1], + 'match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN1', 1], + 'no-match-contains' => ['coupon1', '**', 'coupon2', 0], + 'no-match-contains-case-insensitive' => ['coupon1', '**', 'cOuPoN2', 0], + 'match-begins-with' => ['coupon', 'bw', 'coupon1', 1], + 'match-begins-with-case-insensitive' => ['coupon', 'bw', 'cOuPoN1', 1], + 'no-match-begins-with' => ['coupon', 'bw', 'foocoupon2', 0], + 'no-match-begins-with-case-insensitive' => ['coupon', 'bw', 'foocOuPoN2', 0], + 'match-ends-with' => ['pon1', 'ew', 'coupon1', 1], + 'match-ends-with-case-insensitive' => ['pon1', 'ew', 'cOuPoN1', 1], + 'no-match-ends-with' => ['pon2', 'ew', 'coupon2foo', 0], + 'no-match-ends-with-case-insensitive' => ['pon2', 'ew', 'cOuPoN2foo', 0], + ]; + } + + /** + * @param string|null $value + * @param string|null $operator + * @return OrderCondition + */ + private function _createCondition(?string $value, ?string $operator = null): OrderCondition + { + $condition = Order::createCondition(); + /** @var CouponCodeConditionRule $rule */ + $rule = \Craft::$app->getConditions()->createConditionRule(CouponCodeConditionRule::class); + $rule->value = $value; + + if ($operator) { + $rule->operator = $operator; + } + + $condition->addConditionRule($rule); + + return $condition; + } +} diff --git a/tests/unit/elements/order/conditions/OrderConditionTest.php b/tests/unit/elements/order/conditions/OrderConditionTest.php index b040a3cc97..fd5cdec51f 100644 --- a/tests/unit/elements/order/conditions/OrderConditionTest.php +++ b/tests/unit/elements/order/conditions/OrderConditionTest.php @@ -9,6 +9,7 @@ use Codeception\Test\Unit; use craft\commerce\elements\conditions\orders\CompletedConditionRule; +use craft\commerce\elements\conditions\orders\CouponCodeConditionRule; use craft\commerce\elements\conditions\orders\CustomerConditionRule; use craft\commerce\elements\conditions\orders\DateOrderedConditionRule; use craft\commerce\elements\conditions\orders\HasPurchasableConditionRule; @@ -66,8 +67,9 @@ public function testConditionRuleTypes(): void $rules = array_keys($rules); self::assertContains(DateOrderedConditionRule::class, $rules); - self::assertContains(CustomerConditionRule::class, $rules); self::assertContains(CompletedConditionRule::class, $rules); + self::assertContains(CouponCodeConditionRule::class, $rules); + self::assertContains(CustomerConditionRule::class, $rules); self::assertContains(PaidConditionRule::class, $rules); self::assertContains(HasPurchasableConditionRule::class, $rules); self::assertContains(ItemSubtotalConditionRule::class, $rules); diff --git a/tests/unit/services/InventoryTest.php b/tests/unit/services/InventoryTest.php new file mode 100644 index 0000000000..78ae95fb76 --- /dev/null +++ b/tests/unit/services/InventoryTest.php @@ -0,0 +1,99 @@ + + * @since 5.3.0 + */ +class InventoryTest extends Unit +{ + /** + * @return array + */ + public function _fixtures(): array + { + return [ + 'products' => [ + 'class' => ProductFixture::class, + ], + ]; + } + + /** + * @param array $updateConfigs + * @param int $expected + * @return void + * @throws DeprecationException + * @throws InvalidConfigException + * @throws Exception + * @dataProvider setStockLevelDataProvider + */ + public function testUpdatePurchasableInventoryLevel(array $updateConfigs, int $expected): void + { + $variant = Variant::find()->sku('rad-hood')->one(); + $originalStock = $variant->getStock(); + + foreach ($updateConfigs as $updateConfig) { + $qty = $updateConfig['quantity']; + unset($updateConfig['quantity']); + + Plugin::getInstance()->getInventory()->updatePurchasableInventoryLevel($variant, $qty, $updateConfig); + } + + self::assertEquals($expected, $variant->getStock()); + + Plugin::getInstance()->getInventory()->updatePurchasableInventoryLevel($variant, $originalStock); + } + + /** + * @return array[] + */ + public function setStockLevelDataProvider(): array + { + return [ + 'simple-single-arg' => [ + [ + ['quantity' => 10], + ], + 'expected' => 10, + ], + 'set-and-adjust' => [ + [ + ['quantity' => 10], + ['quantity' => 2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], + ], + 'expected' => 12, + ], + 'just-adjust' => [ + [ + ['quantity' => 2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], + ], + 'expected' => 2, + ], + 'set-and-adjust-negative' => [ + [ + ['quantity' => 10], + ['quantity' => -2, 'updateAction' => InventoryUpdateQuantityType::ADJUST], + ], + 'expected' => 8, + ], + ]; + } +}