diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/PriceRange.php b/app/code/Magento/BundleGraphQl/Model/Resolver/PriceRange.php
new file mode 100644
index 0000000000000..0129c7ae65138
--- /dev/null
+++ b/app/code/Magento/BundleGraphQl/Model/Resolver/PriceRange.php
@@ -0,0 +1,81 @@
+priceProviderPool = $priceProviderPool;
+ $this->discount = $discount;
+ $this->productDataProvider = $productDataProvider
+ ?? ObjectManager::getInstance()->get(ProductDataProvider::class);
+ $this->priceRangeDataProvider = $priceRangeDataProvider
+ ?? ObjectManager::getInstance()->get(PriceRangeDataProvider::class);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ $this->productDataProvider->addProductSku($value['sku']);
+ $productData = $this->productDataProvider->getProductBySku($value['sku']);
+ $value['model'] = $productData['model'];
+
+ return $this->priceRangeDataProvider->prepare($context, $info, $value);
+ }
+}
diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls
index 9f447d7fc7118..9820bf108f325 100644
--- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls
@@ -55,6 +55,7 @@ type BundleItem @doc(description: "Defines an individual item within a bundle pr
type: String @doc(description: "The input type that the customer uses to select the item. Examples include radio button and checkbox.")
position: Int @doc(description: "A number indicating the sequence order of this item compared to the other bundle items.")
sku: String @doc(description: "The SKU of the bundle product.")
+ price_range: PriceRange! @doc(description: "The range of prices for the product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\PriceRange")
options: [BundleItemOption] @doc(description: "An array of additional options for this bundle item.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\BundleItemLinks")
}
diff --git a/app/code/Magento/Catalog/view/base/web/images/category/placeholder/image.jpg b/app/code/Magento/Catalog/view/base/web/images/category/placeholder/image.jpg
new file mode 100644
index 0000000000000..3526efa91f55a
Binary files /dev/null and b/app/code/Magento/Catalog/view/base/web/images/category/placeholder/image.jpg differ
diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php
index 4f4d9355cff13..e8c43b437f0c5 100644
--- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php
+++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Category.php
@@ -8,16 +8,15 @@
namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder;
use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
-use Magento\CatalogGraphQl\DataProvider\CategoryAttributesMapper;
use Magento\CatalogGraphQl\DataProvider\Category\Query\CategoryAttributeQuery;
+use Magento\CatalogGraphQl\DataProvider\CategoryAttributesMapper;
+use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter;
use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilderInterface;
use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\RootCategoryProvider;
use Magento\Framework\Api\Search\AggregationInterface;
use Magento\Framework\Api\Search\AggregationValueInterface;
use Magento\Framework\Api\Search\BucketInterface;
use Magento\Framework\App\ResourceConnection;
-use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Formatter\LayerFormatter;
-use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\Builder\Aggregations;
/**
* Category layer builder
@@ -36,7 +35,7 @@ class Category implements LayerBuilderInterface
*/
private static $bucketMap = [
self::CATEGORY_BUCKET => [
- 'request_name' => 'category_id',
+ 'request_name' => 'category_uid',
'label' => 'Category'
],
];
@@ -44,37 +43,37 @@ class Category implements LayerBuilderInterface
/**
* @var CategoryAttributeQuery
*/
- private $categoryAttributeQuery;
+ private CategoryAttributeQuery $categoryAttributeQuery;
/**
* @var CategoryAttributesMapper
*/
- private $attributesMapper;
+ private CategoryAttributesMapper $attributesMapper;
/**
* @var ResourceConnection
*/
- private $resourceConnection;
+ private ResourceConnection $resourceConnection;
/**
* @var RootCategoryProvider
*/
- private $rootCategoryProvider;
+ private RootCategoryProvider $rootCategoryProvider;
/**
* @var LayerFormatter
*/
- private $layerFormatter;
+ private LayerFormatter $layerFormatter;
/**
* @var CollectionFactory
*/
- private $categoryCollectionFactory;
+ private CollectionFactory $categoryCollectionFactory;
/**
* @var Aggregations\Category\IncludeDirectChildrenOnly
*/
- private $includeDirectChildrenOnly;
+ private Aggregations\Category\IncludeDirectChildrenOnly $includeDirectChildrenOnly;
/**
* @param CategoryAttributeQuery $categoryAttributeQuery
@@ -152,7 +151,7 @@ function (AggregationValueInterface $value) {
foreach ($bucket->getValues() as $value) {
$categoryId = $value->getValue();
if (!\in_array($categoryId, $categoryIds, true)) {
- continue ;
+ continue;
}
$result['options'][] = $this->layerFormatter->buildItem(
$categoryLabels[$categoryId] ?? $categoryId,
diff --git a/app/code/Magento/CatalogGraphQl/Model/PriceRangeDataProvider.php b/app/code/Magento/CatalogGraphQl/Model/PriceRangeDataProvider.php
new file mode 100644
index 0000000000000..8139481729e1e
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Model/PriceRangeDataProvider.php
@@ -0,0 +1,185 @@
+priceProviderPool = $priceProviderPool;
+ $this->discount = $discount;
+ }
+
+ /**
+ * Prepare Query object based on search text
+ *
+ * @param ContextInterface $context
+ * @param ResolveInfo $info
+ * @param array $value
+ * @throws Exception
+ * @return mixed|Value
+ */
+ public function prepare(ContextInterface $context, ResolveInfo $info, array $value): array
+ {
+ if (!isset($value['model'])) {
+ throw new LocalizedException(__('"model" value should be specified'));
+ }
+ /** @var StoreInterface $store */
+ $store = $context->getExtensionAttributes()->getStore();
+
+ /** @var Product $product */
+ $product = $value['model'];
+ $product->unsetData('minimal_price');
+ // add store filter for the product
+ $product->setData(self::STORE_FILTER_CACHE_KEY, $store);
+
+ if ($context) {
+ $customerGroupId = $context->getExtensionAttributes()->getCustomerGroupId();
+ if ($customerGroupId !== null) {
+ $product->setCustomerGroupId($customerGroupId);
+ }
+ }
+
+ $requestedFields = $info->getFieldSelection(10);
+ $returnArray = [];
+
+ $returnArray['minimum_price'] = ($requestedFields['minimum_price'] ?? 0) ? ($this->canShowPrice($product) ?
+ $this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
+ $returnArray['maximum_price'] = ($requestedFields['maximum_price'] ?? 0) ? ($this->canShowPrice($product) ?
+ $this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
+
+ return $returnArray;
+ }
+
+ /**
+ * Get formatted minimum product price
+ *
+ * @param SaleableInterface $product
+ * @param StoreInterface $store
+ * @return array
+ */
+ private function getMinimumProductPrice(SaleableInterface $product, StoreInterface $store): array
+ {
+ $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId());
+ $minPriceArray = $this->formatPrice(
+ (float)$priceProvider->getMinimalRegularPrice($product)->getValue(),
+ (float)$priceProvider->getMinimalFinalPrice($product)->getValue(),
+ $store
+ );
+ $minPriceArray['model'] = $product;
+
+ return $minPriceArray;
+ }
+
+ /**
+ * Get formatted maximum product price
+ *
+ * @param SaleableInterface $product
+ * @param StoreInterface $store
+ * @return array
+ */
+ private function getMaximumProductPrice(SaleableInterface $product, StoreInterface $store): array
+ {
+ $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId());
+ $maxPriceArray = $this->formatPrice(
+ (float)$priceProvider->getMaximalRegularPrice($product)->getValue(),
+ (float)$priceProvider->getMaximalFinalPrice($product)->getValue(),
+ $store
+ );
+ $maxPriceArray['model'] = $product;
+
+ return $maxPriceArray;
+ }
+
+ /**
+ * Format price for GraphQl output
+ *
+ * @param float $regularPrice
+ * @param float $finalPrice
+ * @param StoreInterface $store
+ * @return array
+ */
+ private function formatPrice(float $regularPrice, float $finalPrice, StoreInterface $store): array
+ {
+ return [
+ 'regular_price' => [
+ 'value' => $regularPrice,
+ 'currency' => $store->getCurrentCurrencyCode(),
+ ],
+ 'final_price' => [
+ 'value' => $finalPrice,
+ 'currency' => $store->getCurrentCurrencyCode(),
+ ],
+ 'discount' => $this->discount->getDiscountByDifference($regularPrice, $finalPrice),
+ ];
+ }
+
+ /**
+ * Check if the product is allowed to show price
+ *
+ * @param ProductInterface $product
+ * @return bool
+ */
+ private function canShowPrice(ProductInterface $product): bool
+ {
+ return $product->hasData('can_show_price') ? $product->getData('can_show_price') : true;
+ }
+
+ /**
+ * Format empty result
+ *
+ * @return array
+ */
+ private function formatEmptyResult(): array
+ {
+ return [
+ 'regular_price' => [
+ 'value' => null,
+ 'currency' => null,
+ ],
+ 'final_price' => [
+ 'value' => null,
+ 'currency' => null,
+ ],
+ 'discount' => null,
+ ];
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php
index 549b1311000ec..34b9fff3d9026 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php
@@ -9,6 +9,8 @@
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\Category\FileInfo;
+use Magento\Framework\App\Area;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem\DirectoryList;
use Magento\Framework\GraphQl\Config\Element\Field;
@@ -16,7 +18,9 @@
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\UrlInterface;
+use Magento\Framework\View\Asset\Repository;
use Magento\Store\Api\Data\StoreInterface;
+use Psr\Log\LoggerInterface;
/**
* Resolve category image to a fully qualified URL
@@ -29,16 +33,32 @@ class Image implements ResolverInterface
/** @var FileInfo */
private $fileInfo;
+ /**
+ * @var Repository
+ */
+ private Repository $assetRepo;
+
+ /**
+ * @var LoggerInterface
+ */
+ private LoggerInterface $logger;
+
/**
* @param DirectoryList $directoryList
* @param FileInfo $fileInfo
+ * @param Repository|null $assetRepo
+ * @param LoggerInterface|null $logger
*/
public function __construct(
DirectoryList $directoryList,
- FileInfo $fileInfo
+ FileInfo $fileInfo,
+ Repository $assetRepo = null,
+ LoggerInterface $logger = null
) {
$this->directoryList = $directoryList;
$this->fileInfo = $fileInfo;
+ $this->assetRepo = $assetRepo ?? ObjectManager::getInstance()->get(Repository::class);
+ $this->logger = $logger ?? ObjectManager::getInstance()->get(LoggerInterface::class);
}
/**
@@ -64,11 +84,14 @@ public function resolve(
$store = $context->getExtensionAttributes()->getStore();
$baseUrl = $store->getBaseUrl(UrlInterface::URL_TYPE_WEB);
- $filenameWithMedia = $this->fileInfo->isBeginsWithMediaDirectoryPath($imagePath)
+ $filenameWithMedia = $this->fileInfo->isBeginsWithMediaDirectoryPath($imagePath)
? $imagePath : $this->formatFileNameWithMediaCategoryFolder($imagePath);
if (!$this->fileInfo->isExist($filenameWithMedia)) {
- throw new GraphQlInputException(__('Category image not found.'));
+ $this->logger->error(__('Category image not found'));
+ return $this->assetRepo
+ ->createAsset('Magento_Catalog::images/category/placeholder/image.jpg', ['area' => Area::AREA_FRONTEND])
+ ->getUrl();
}
// return full url
diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php
index cdea4a35ca6d2..25db5207af285 100644
--- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php
+++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php
@@ -7,42 +7,48 @@
namespace Magento\CatalogGraphQl\Model\Resolver\Product;
-use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\CatalogGraphQl\Model\PriceRangeDataProvider;
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount;
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
-use Magento\Catalog\Model\Product;
-use Magento\Framework\Exception\LocalizedException;
-use Magento\Framework\Pricing\SaleableInterface;
-use Magento\Store\Api\Data\StoreInterface;
/**
* Format product's pricing information for price_range field
*/
class PriceRange implements ResolverInterface
{
- private const STORE_FILTER_CACHE_KEY = '_cache_instance_store_filter';
-
/**
* @var Discount
*/
- private $discount;
+ private Discount $discount;
/**
* @var PriceProviderPool
*/
- private $priceProviderPool;
+ private PriceProviderPool $priceProviderPool;
+
+ /**
+ * @var PriceRangeDataProvider
+ */
+ private PriceRangeDataProvider $priceRangeDataProvider;
/**
* @param PriceProviderPool $priceProviderPool
* @param Discount $discount
+ * @param PriceRangeDataProvider|null $priceRangeDataProvider
*/
- public function __construct(PriceProviderPool $priceProviderPool, Discount $discount)
- {
+ public function __construct(
+ PriceProviderPool $priceProviderPool,
+ Discount $discount,
+ PriceRangeDataProvider $priceRangeDataProvider = null
+ ) {
$this->priceProviderPool = $priceProviderPool;
$this->discount = $discount;
+ $this->priceRangeDataProvider = $priceRangeDataProvider
+ ?? ObjectManager::getInstance()->get(PriceRangeDataProvider::class);
}
/**
@@ -55,128 +61,6 @@ public function resolve(
array $value = null,
array $args = null
) {
- if (!isset($value['model'])) {
- throw new LocalizedException(__('"model" value should be specified'));
- }
- /** @var StoreInterface $store */
- $store = $context->getExtensionAttributes()->getStore();
-
- /** @var Product $product */
- $product = $value['model'];
- $product->unsetData('minimal_price');
- // add store filter for the product
- $product->setData(self::STORE_FILTER_CACHE_KEY, $store);
-
- if ($context) {
- $customerGroupId = $context->getExtensionAttributes()->getCustomerGroupId();
- if ($customerGroupId !== null) {
- $product->setCustomerGroupId($customerGroupId);
- }
- }
-
- $requestedFields = $info->getFieldSelection(10);
- $returnArray = [];
-
- if (isset($requestedFields['minimum_price'])) {
- $returnArray['minimum_price'] = $this->canShowPrice($product) ?
- $this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult();
- }
- if (isset($requestedFields['maximum_price'])) {
- $returnArray['maximum_price'] = $this->canShowPrice($product) ?
- $this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult();
- }
- return $returnArray;
- }
-
- /**
- * Get formatted minimum product price
- *
- * @param SaleableInterface $product
- * @param StoreInterface $store
- * @return array
- */
- private function getMinimumProductPrice(SaleableInterface $product, StoreInterface $store): array
- {
- $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId());
- $regularPrice = $priceProvider->getMinimalRegularPrice($product)->getValue();
- $finalPrice = $priceProvider->getMinimalFinalPrice($product)->getValue();
- $minPriceArray = $this->formatPrice((float) $regularPrice, (float) $finalPrice, $store);
- $minPriceArray['model'] = $product;
- return $minPriceArray;
- }
-
- /**
- * Get formatted maximum product price
- *
- * @param SaleableInterface $product
- * @param StoreInterface $store
- * @return array
- */
- private function getMaximumProductPrice(SaleableInterface $product, StoreInterface $store): array
- {
- $priceProvider = $this->priceProviderPool->getProviderByProductType($product->getTypeId());
- $regularPrice = $priceProvider->getMaximalRegularPrice($product)->getValue();
- $finalPrice = $priceProvider->getMaximalFinalPrice($product)->getValue();
- $maxPriceArray = $this->formatPrice((float) $regularPrice, (float) $finalPrice, $store);
- $maxPriceArray['model'] = $product;
- return $maxPriceArray;
- }
-
- /**
- * Format price for GraphQl output
- *
- * @param float $regularPrice
- * @param float $finalPrice
- * @param StoreInterface $store
- * @return array
- */
- private function formatPrice(float $regularPrice, float $finalPrice, StoreInterface $store): array
- {
- return [
- 'regular_price' => [
- 'value' => $regularPrice,
- 'currency' => $store->getCurrentCurrencyCode()
- ],
- 'final_price' => [
- 'value' => $finalPrice,
- 'currency' => $store->getCurrentCurrencyCode()
- ],
- 'discount' => $this->discount->getDiscountByDifference($regularPrice, $finalPrice),
- ];
- }
-
- /**
- * Check if the product is allowed to show price
- *
- * @param ProductInterface $product
- * @return bool
- */
- private function canShowPrice($product): bool
- {
- if ($product->hasData('can_show_price') && $product->getData('can_show_price') === false) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Format empty result
- *
- * @return array
- */
- private function formatEmptyResult(): array
- {
- return [
- 'regular_price' => [
- 'value' => null,
- 'currency' => null
- ],
- 'final_price' => [
- 'value' => null,
- 'currency' => null
- ],
- 'discount' => null
- ];
+ return $this->priceRangeDataProvider->prepare($context, $info, $value);
}
}
diff --git a/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/ImageTest.php b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/ImageTest.php
new file mode 100644
index 0000000000000..4aac601a3386e
--- /dev/null
+++ b/app/code/Magento/CatalogGraphQl/Test/Unit/Model/Resolver/Category/ImageTest.php
@@ -0,0 +1,205 @@
+fileInfoMock = $this->createMock(FileInfo::class);
+ $this->directoryListMock = $this->createMock(DirectoryList::class);
+ $this->fieldMock = $this->createMock(Field::class);
+ $this->resolveInfoMock = $this->createMock(ResolveInfo::class);
+ $this->contextMock = $this->createMock(Context::class);
+ $this->categoryMock = $this->createMock(Category::class);
+ $this->assetRepoMock = $this->createMock(Repository::class);
+ $this->loggerMock = $this->createMock(LoggerInterface::class);
+ $this->image = new Image(
+ $this->directoryListMock,
+ $this->fileInfoMock,
+ $this->assetRepoMock,
+ $this->loggerMock
+ );
+ }
+
+ public function testResolve(): void
+ {
+ $this->valueMock = ['model' => $this->categoryMock];
+ $contextExtensionInterfaceMock = $this->getMockBuilder(ContextExtensionInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getStore'])
+ ->getMockForAbstractClass();
+ $storeMock = $this->createMock(Store::class);
+ $this->categoryMock
+ ->expects($this->once())
+ ->method('getData')
+ ->with('image')
+ ->willReturn('/media/catalog/category/test.jpg');
+ $this->contextMock
+ ->expects($this->once())
+ ->method('getExtensionAttributes')
+ ->willReturn($contextExtensionInterfaceMock);
+ $contextExtensionInterfaceMock
+ ->expects($this->once())
+ ->method('getStore')
+ ->willReturn($storeMock);
+ $storeMock
+ ->expects($this->once())
+ ->method('getBaseUrl')
+ ->willReturn('https://magento.url');
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('isBeginsWithMediaDirectoryPath')
+ ->willReturn('fileName');
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('isExist')
+ ->willReturn(true);
+
+ $this->assertEquals(
+ 'https://magento.url/media/catalog/category/test.jpg',
+ $this->image->resolve(
+ $this->fieldMock,
+ $this->contextMock,
+ $this->resolveInfoMock,
+ $this->valueMock
+ )
+ );
+ }
+
+ public function testResolveWithoutModelInValueParameter(): void
+ {
+ $this->expectException(LocalizedException::class);
+ $this->expectExceptionMessage('"model" value should be specified');
+ $this->image->resolve($this->fieldMock, $this->contextMock, $this->resolveInfoMock, $this->valueMock);
+ }
+
+ public function testResolveWhenImageFileDoesntExist(): void
+ {
+ $this->valueMock = ['model' => $this->categoryMock];
+ $contextExtensionInterfaceMock = $this->getMockBuilder(ContextExtensionInterface::class)
+ ->disableOriginalConstructor()
+ ->setMethods(['getStore'])
+ ->getMockForAbstractClass();
+ $storeMock = $this->createMock(Store::class);
+ $this->categoryMock
+ ->expects($this->once())
+ ->method('getData')
+ ->with('image')
+ ->willReturn('/media/catalog/category/test.jpg');
+ $this->contextMock
+ ->expects($this->once())
+ ->method('getExtensionAttributes')
+ ->willReturn($contextExtensionInterfaceMock);
+ $contextExtensionInterfaceMock
+ ->expects($this->once())
+ ->method('getStore')
+ ->willReturn($storeMock);
+ $storeMock
+ ->expects($this->once())
+ ->method('getBaseUrl')
+ ->willReturn('https://magento.url');
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('isBeginsWithMediaDirectoryPath')
+ ->willReturn('fileName');
+ $this->fileInfoMock
+ ->expects($this->once())
+ ->method('isExist')
+ ->willReturn(false);
+ $assetFileMock = $this->createMock(File::class);
+ $assetFileMock
+ ->expects($this->once())
+ ->method('getUrl')
+ ->willReturn('https://magento.url/Magento_Catalog/images/category/placeholder/image.jpg');
+ $this->assetRepoMock
+ ->expects($this->once())
+ ->method('createAsset')
+ ->with('Magento_Catalog::images/category/placeholder/image.jpg', ['area' => 'frontend'])
+ ->willReturn($assetFileMock);
+ $this->assertEquals(
+ 'https://magento.url/Magento_Catalog/images/category/placeholder/image.jpg',
+ $this->image->resolve(
+ $this->fieldMock,
+ $this->contextMock,
+ $this->resolveInfoMock,
+ $this->valueMock
+ )
+ );
+ }
+}
diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
index 0817987a383da..98d895a10c264 100644
--- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls
@@ -105,8 +105,8 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\
image: ProductImage @doc(description: "The relative path to the main image on the product page.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage")
small_image: ProductImage @doc(description: "The relative path to the small image, which is used on catalog pages.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage")
thumbnail: ProductImage @doc(description: "The relative path to the product's thumbnail image.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductImage")
- new_from_date: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo")
- new_to_date: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo")
+ new_from_date: String @doc(description: "The beginning date for new product listings, and determines if the product is featured as a new product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo")
+ new_to_date: String @doc(description: "The end date for new product listings.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\NewFromTo")
tier_price: Float @deprecated(reason: "Use `price_tiers` for product tier price information.") @doc(description: "The price when tier pricing is in effect and the items purchased threshold has been reached.")
options_container: String @doc(description: "If the product has multiple options, determines where they appear on the product page.")
created_at: String @deprecated(reason: "The field should not be used on the storefront.") @doc(description: "Timestamp indicating when the product was created.")
diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/HomePageUrlLocator.php b/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/HomePageUrlLocator.php
index 6cc669e46d080..2cb31d3dadb1b 100644
--- a/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/HomePageUrlLocator.php
+++ b/app/code/Magento/CmsUrlRewriteGraphQl/Model/Resolver/UrlRewrite/HomePageUrlLocator.php
@@ -7,26 +7,25 @@
namespace Magento\CmsUrlRewriteGraphQl\Model\Resolver\UrlRewrite;
-use Magento\UrlRewriteGraphQl\Model\Resolver\UrlRewrite\CustomUrlLocatorInterface;
-use Magento\Store\Model\ScopeInterface;
use Magento\Cms\Helper\Page;
+use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Store\Model\ScopeInterface;
+use Magento\UrlRewriteGraphQl\Model\Resolver\UrlRewrite\CustomUrlLocatorInterface;
-/**
- * Home page URL locator.
- */
class HomePageUrlLocator implements CustomUrlLocatorInterface
{
+ public const HOME_PAGE_URL_DELIMITER = '|';
+
/**
- * @var \Magento\Framework\App\Config\ScopeConfigInterface
+ * @var ScopeConfigInterface
*/
- private $scopeConfig;
+ private ScopeConfigInterface $scopeConfig;
/**
- * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
+ * @param ScopeConfigInterface $scopeConfig
*/
- public function __construct(
- \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
- ) {
+ public function __construct(ScopeConfigInterface $scopeConfig)
+ {
$this->scopeConfig = $scopeConfig;
}
@@ -40,8 +39,10 @@ public function locateUrl($urlKey): ?string
Page::XML_PATH_HOME_PAGE,
ScopeInterface::SCOPE_STORE
);
- return $homePageUrl;
+
+ return strtok($homePageUrl, self::HOME_PAGE_URL_DELIMITER);
}
+
return null;
}
}
diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/Test/Unit/Model/Resolver/UrlRewrite/HomePageUrlLocatorTest.php b/app/code/Magento/CmsUrlRewriteGraphQl/Test/Unit/Model/Resolver/UrlRewrite/HomePageUrlLocatorTest.php
new file mode 100644
index 0000000000000..5624f9fa365f8
--- /dev/null
+++ b/app/code/Magento/CmsUrlRewriteGraphQl/Test/Unit/Model/Resolver/UrlRewrite/HomePageUrlLocatorTest.php
@@ -0,0 +1,61 @@
+scopeConfigInterfaceMock = $this->createMock(ScopeConfigInterface::class);
+ $this->homePageUrlLocator = new HomePageUrlLocator($this->scopeConfigInterfaceMock);
+ }
+
+ public function testLocateUrl(): void
+ {
+ $this->scopeConfigInterfaceMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with(Page::XML_PATH_HOME_PAGE, ScopeInterface::SCOPE_STORE)
+ ->willReturn('home');
+ $this->assertEquals('home', $this->homePageUrlLocator->locateUrl($this->homePageUrlKey));
+ }
+
+ public function testLocateUrlWhenMultipleStoresHaveSameHomePageUrl(): void
+ {
+ $this->scopeConfigInterfaceMock
+ ->expects($this->once())
+ ->method('getValue')
+ ->with(Page::XML_PATH_HOME_PAGE, ScopeInterface::SCOPE_STORE)
+ ->willReturn('home|8');
+ $this->assertEquals('home', $this->homePageUrlLocator->locateUrl($this->homePageUrlKey));
+ }
+}
diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls
index 5dc05f43af183..126fd44e024fc 100644
--- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls
@@ -62,7 +62,7 @@ input ConfigurableProductCartItemInput {
}
type ConfigurableCartItem implements CartItemInterface @doc(description: "An implementation for configurable product cart items.") {
- customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") @doc(description: "An array containing the customizable options the shopper selected.")
+ customizable_options: [SelectedCustomizableOption]! @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") @doc(description: "An array containing the customizable options the shopper selected.")
configurable_options: [SelectedConfigurableOption!]! @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableCartItemOptions") @doc(description: "An array containing the configuranle options the shopper selected.")
configured_variant: ProductInterface! @doc(description: "Product details of the cart item.") @resolver(class: "\\Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ProductResolver")
}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AvailablePaymentMethods.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AvailablePaymentMethods.php
index 907d778550593..ff1228b4a2ff9 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/AvailablePaymentMethods.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AvailablePaymentMethods.php
@@ -10,26 +10,41 @@
use Magento\Checkout\Api\PaymentInformationManagementInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
+use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Quote\Api\Data\CartInterface;
+use Magento\Quote\Api\ShippingMethodManagementInterface;
/**
* Get list of active payment methods resolver.
*/
class AvailablePaymentMethods implements ResolverInterface
{
+ public const FREE_SHIPPING_METHOD = 'freeshipping';
+
+ public const FREE_PAYMENT_METHOD_CODE = 'free';
+
/**
* @var PaymentInformationManagementInterface
*/
- private $informationManagement;
+ private PaymentInformationManagementInterface $informationManagement;
+
+ /**
+ * @var ShippingMethodManagementInterface
+ */
+ private ShippingMethodManagementInterface $informationShipping;
/**
* @param PaymentInformationManagementInterface $informationManagement
+ * @param ShippingMethodManagementInterface $informationShipping
*/
- public function __construct(PaymentInformationManagementInterface $informationManagement)
- {
+ public function __construct(
+ PaymentInformationManagementInterface $informationManagement,
+ ShippingMethodManagementInterface $informationShipping
+ ) {
$this->informationManagement = $informationManagement;
+ $this->informationShipping = $informationShipping;
}
/**
@@ -50,19 +65,55 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
*
* @param CartInterface $cart
* @return array
+ * @throws GraphQlInputException
*/
private function getPaymentMethodsData(CartInterface $cart): array
{
$paymentInformation = $this->informationManagement->getPaymentInformation($cart->getId());
$paymentMethods = $paymentInformation->getPaymentMethods();
-
+ $shippingData = $this->getShippingData($cart->getId());
+ $carrierCode = $shippingData['carrier_code'] ?? null;
+ $grandTotal = $shippingData['grand_total'] ?? 0;
$paymentMethodsData = [];
foreach ($paymentMethods as $paymentMethod) {
- $paymentMethodsData[] = [
- 'title' => $paymentMethod->getTitle(),
- 'code' => $paymentMethod->getCode(),
- ];
+ /**
+ * Checking payment method and shipping method for zero price product
+ */
+ if ((int)$grandTotal === 0 && $carrierCode === self::FREE_SHIPPING_METHOD
+ && $paymentMethod->getCode() === self::FREE_PAYMENT_METHOD_CODE) {
+ $paymentMethodsData[] = [
+ 'title' => $paymentMethod->getTitle(),
+ 'code' => $paymentMethod->getCode(),
+ ];
+ } elseif ((int)$grandTotal >= 0
+ && $carrierCode !== self::FREE_SHIPPING_METHOD) {
+ $paymentMethodsData[] = [
+ 'title' => $paymentMethod->getTitle(),
+ 'code' => $paymentMethod->getCode(),
+ ];
+ }
}
return $paymentMethodsData;
}
+
+ /**
+ * Retrieve selected shipping method
+ *
+ * @param string $cartId
+ * @return array
+ */
+ private function getShippingData(string $cartId): array
+ {
+ $shippingData = [];
+ try {
+ $shippingMethod = $this->informationShipping->get($cartId);
+ if ($shippingMethod) {
+ $shippingData['carrier_code'] = $shippingMethod->getCarrierCode();
+ $shippingData['grand_total'] = $shippingMethod->getBaseAmount();
+ }
+ } catch (LocalizedException $exception) {
+ $shippingData = [];
+ }
+ return $shippingData;
+ }
}
diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php
index d992db1e3b53e..dfbc20bf7abd4 100644
--- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php
+++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartItemPrices.php
@@ -63,6 +63,10 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value
'currency' => $currencyCode,
'value' => $cartItem->getCalculationPrice(),
],
+ 'price_including_tax' => [
+ 'currency' => $currencyCode,
+ 'value' => $cartItem->getPriceInclTax(),
+ ],
'row_total' => [
'currency' => $currencyCode,
'value' => $cartItem->getRowTotal(),
diff --git a/app/code/Magento/QuoteGraphQl/etc/di.xml b/app/code/Magento/QuoteGraphQl/etc/di.xml
index 8dd35ab7f300b..73724e03bcf57 100644
--- a/app/code/Magento/QuoteGraphQl/etc/di.xml
+++ b/app/code/Magento/QuoteGraphQl/etc/di.xml
@@ -41,4 +41,9 @@
+
+
+ Magento\Quote\Api\ShippingMethodManagementInterface
+
+
diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
index 29afff169e637..1dc66531fbac6 100644
--- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls
@@ -360,6 +360,7 @@ type Discount @doc(description:"Defines an individual discount. A discount can b
type CartItemPrices @doc(description: "Contains details about the price of the item, including taxes and discounts.") {
price: Money! @doc(description: "The price of the item before any discounts were applied. The price that might include tax, depending on the configured display settings for cart.")
+ price_including_tax: Money! @doc(description: "The price of the item before any discounts were applied. The price that might include tax, depending on the configured display settings for cart.")
row_total: Money! @doc(description: "The value of the price multiplied by the quantity of the item.")
row_total_including_tax: Money! @doc(description: "The value of `row_total` plus the tax applied to the item.")
discounts: [Discount] @doc(description: "An array of discounts to be applied to the cart item.")
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php
index 5c2fecc6d98e1..bc492fc0cef8b 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductViewTest.php
@@ -21,8 +21,8 @@
*/
class BundleProductViewTest extends GraphQlAbstract
{
- const KEY_PRICE_TYPE_FIXED = 'FIXED';
- const KEY_PRICE_TYPE_DYNAMIC = 'DYNAMIC';
+ private const KEY_PRICE_TYPE_FIXED = 'FIXED';
+ private const KEY_PRICE_TYPE_DYNAMIC = 'DYNAMIC';
/**
* @magentoApiDataFixture Magento/Bundle/_files/product_1.php
@@ -56,6 +56,20 @@ public function testAllFieldsBundleProducts()
type
position
sku
+ price_range{
+ maximum_price {
+ final_price {
+ currency
+ value
+ }
+ }
+ minimum_price {
+ final_price {
+ currency
+ value
+ }
+ }
+ }
options {
id
quantity
@@ -95,6 +109,9 @@ public function testAllFieldsBundleProducts()
$this->assertEquals('PRICE_RANGE', $response['products']['items'][0]['price_view']);
}
$this->assertBundleBaseFields($bundleProduct, $response['products']['items'][0]);
+ $product = $response['products']['items'][0]['items'][0];
+ $this->assertEquals(10, $product['price_range']['maximum_price']['final_price']['value']);
+ $this->assertEquals(10, $product['price_range']['minimum_price']['final_price']['value']);
$this->assertBundleProductOptions($bundleProduct, $response['products']['items'][0]);
$this->assertNotEmpty(
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php
index bccc3cc312d43..23f71ecc8a2b5 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php
@@ -347,6 +347,8 @@ public function testCategoryProducts()
}
}
name
+ new_from_date
+ new_to_date
options_container
price {
minimalPrice {
@@ -621,6 +623,48 @@ public function testCategoryImage(?string $imagePrefix)
$this->assertEquals($expectedImageUrl, $childCategory['image']);
}
+ /**
+ * Test categories query when category image is not found or missing.
+ *
+ * @magentoApiDataFixture Magento/Catalog/_files/catalog_category_with_missing_image.php
+ */
+ public function testCategoriesQueryWhenCategoryImageIsMissing(): void
+ {
+ /** @var CategoryCollection $categoryCollection */
+ $categoryCollection = $this->objectManager->get(CategoryCollection::class);
+ $categoryModel = $categoryCollection
+ ->addAttributeToSelect('image')
+ ->addAttributeToFilter('name', ['eq' => 'Parent Image Category'])
+ ->getFirstItem();
+ $categoryId = $categoryModel->getId();
+ $query = <<graphQlQuery($query);
+ $this->assertArrayNotHasKey('errors', $response);
+ $this->assertNotEmpty($response['categories']);
+ $categories = current($response['categories']['items']);
+ $this->assertEquals($categoryId, $categories['id']);
+ $this->assertEquals('Parent Image Category', $categories['name']);
+ $this->assertStringEndsWith('Magento_Catalog/images/category/placeholder/image.jpg', $categories['image']);
+ }
+
/**
* @magentoApiDataFixture Magento/Catalog/_files/categories.php
*/
@@ -725,6 +769,8 @@ private function assertAttributes($actualResponse)
'short_description',
'country_of_manufacture',
'gift_message_available',
+ 'new_from_date',
+ 'new_to_date',
'options_container',
'special_price',
'special_to_date',
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php
index 81f27dfafd332..5a1e547f7050a 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php
@@ -411,6 +411,8 @@ public function testCategoryProducts()
}
}
name
+ new_from_date
+ new_to_date
options_container
price {
minimalPrice {
@@ -846,6 +848,8 @@ private function assertAttributes($actualResponse)
'short_description',
'country_of_manufacture',
'gift_message_available',
+ 'new_from_date',
+ 'new_to_date',
'options_container',
'special_price'
];
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchCategoryAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchCategoryAggregationsTest.php
index 90dd8e343b2ce..33b22909a92ab 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchCategoryAggregationsTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchCategoryAggregationsTest.php
@@ -57,7 +57,7 @@ private function getCategoryAggregation(array $result) : ?array
return array_filter(
$result['products']['aggregations'],
function ($a) {
- return $a['attribute_code'] == 'category_id';
+ return $a['attribute_code'] == 'category_uid';
}
);
}
@@ -89,7 +89,7 @@ private function aggregationCategoryTesting(string $filterValue, string $include
$categoryAggregation = array_filter(
$result['products']['aggregations'],
function ($a) {
- return $a['attribute_code'] == 'category_id';
+ return $a['attribute_code'] == 'category_uid';
}
);
$this->assertNotEmpty($categoryAggregation);
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php
index 38214c2b6bd8e..57394503b4ad4 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php
@@ -7,28 +7,26 @@
namespace Magento\GraphQl\Catalog;
+use Magento\Catalog\Api\CategoryLinkManagementInterface;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Api\Data\CategoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Category;
-use Magento\Catalog\Model\CategoryLinkManagement;
use Magento\Catalog\Model\Indexer\Product\Category\Processor;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Category\Collection;
+use Magento\Config\Model\ResourceModel\Config;
use Magento\Eav\Api\Data\AttributeOptionInterface;
use Magento\Eav\Model\Config as eavConfig;
+use Magento\Framework\App\Cache;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\DataObject;
+use Magento\Framework\ObjectManagerInterface;
use Magento\TestFramework\Catalog\Model\GetCategoryByName;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\Helper\CacheCleaner;
-use Magento\TestFramework\ObjectManager;
use Magento\TestFramework\TestCase\GraphQlAbstract;
-use Magento\Catalog\Api\CategoryLinkManagementInterface;
-use Magento\Config\Model\ResourceModel\Config;
-use Magento\Framework\App\Cache;
-use Magento\Framework\ObjectManagerInterface;
/**
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
@@ -235,8 +233,9 @@ private function compareFilterNames(array $a, array $b)
}
/**
- * Layered navigation for Configurable products with out of stock options
- * Two configurable products each having two variations and one of the child products of one Configurable set to OOS
+ * Layered navigation for Configurable products with out of stock options
+ * Two configurable products each having two variations and one of the child products
+ * of one Configurable set to OOS
*
* @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php
* @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php
@@ -257,7 +256,11 @@ public function testLayeredNavigationForConfigurableProducts()
$this->assertEquals(2, $response['products']['total_count']);
$this->assertNotEmpty($response['products']['aggregations']);
$this->assertNotEmpty($response['products']['filters'], 'Filters is empty');
- $this->assertCount(2, $response['products']['aggregations'], 'Aggregation count does not match');
+ $this->assertCount(
+ 2,
+ $response['products']['aggregations'],
+ 'Aggregation count does not match'
+ );
// Custom attribute filter layer data
$this->assertResponseFields(
@@ -402,9 +405,20 @@ public function testFilterProductsByDropDownCustomAttribute()
$filteredProducts = [$product3, $product2, $product1];
$countOfFilteredProducts = count($filteredProducts);
$response = $this->graphQlQuery($query);
- $this->assertEquals(3, $response['products']['total_count'], 'Number of products returned is incorrect');
- $this->assertTrue(count($response['products']['filters']) > 0, 'Product filters is not empty');
- $this->assertCount(3, $response['products']['aggregations'], 'Incorrect count of aggregations');
+ $this->assertEquals(
+ 3,
+ $response['products']['total_count'],
+ 'Number of products returned is incorrect'
+ );
+ $this->assertTrue(
+ count($response['products']['filters']) > 0,
+ 'Product filters is not empty'
+ );
+ $this->assertCount(
+ 3,
+ $response['products']['aggregations'],
+ 'Incorrect count of aggregations'
+ );
$productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts);
for ($itemIndex = 0; $itemIndex < $countOfFilteredProducts; $itemIndex++) {
@@ -670,7 +684,7 @@ public function testSearchAndFilterByCustomAttribute()
$this->assertResponseFields(
$response['products']['aggregations'][1],
[
- 'attribute_code' => 'category_id',
+ 'attribute_code' => 'category_uid',
'count' => 7,
'label' => 'Category'
]
@@ -803,7 +817,11 @@ public function testFilterByCategoryIdAndCustomAttribute()
// presort expected and actual results as different search engines have different orders
usort($expectedCategoryInAggregations, [$this, 'compareLabels']);
usort($actualCategoriesFromResponse, [$this, 'compareLabels']);
- $categoryInAggregations = array_map(null, $expectedCategoryInAggregations, $actualCategoriesFromResponse);
+ $categoryInAggregations = array_map(
+ null,
+ $expectedCategoryInAggregations,
+ $actualCategoriesFromResponse
+ );
//Validate the categories and sub-categories data in the filter layer
foreach ($categoryInAggregations as $index => $categoryAggregationsData) {
@@ -1184,7 +1202,10 @@ public function testFilterWithinSpecificPriceRangeSortedByNameDesc()
public function testSortByPosition()
{
// Get category ID for filtering
- $category = $this->categoryCollection->addFieldToFilter('name', 'Category 999')->getFirstItem();
+ $category = $this->categoryCollection->addFieldToFilter(
+ 'name',
+ 'Category 999'
+ )->getFirstItem();
$categoryId = $category->getId();
$queryAsc = <<categoryCollection->addFieldToFilter('name', 'Category 999')->getFirstItem();
+ $category = $this->categoryCollection->addFieldToFilter(
+ 'name',
+ 'Category 999'
+ )->getFirstItem();
$categoryId = (int) $category->getId();
$expectedProductsAsc = ['simple1000', 'simple1001', 'simple1002'];
@@ -1533,7 +1557,7 @@ public function testFilterProductsForExactMatchingName()
]
],
[
- 'attribute_code' => 'category_id',
+ 'attribute_code' => 'category_uid',
'count' => 1,
'label' => 'Category',
'options' => [
@@ -1642,7 +1666,11 @@ public function testFilterProductsBySingleCategoryId(string $fieldName, string $
QUERY;
$response = $this->graphQlQuery($query);
- $this->assertEquals(2, $response['products']['total_count'], 'Incorrect count of products returned');
+ $this->assertEquals(
+ 2,
+ $response['products']['total_count'],
+ 'Incorrect count of products returned'
+ );
$links = $this->categoryLinkManagement->getAssignedProducts(
is_numeric($queryCategoryId) ? $queryCategoryId : base64_decode($queryCategoryId)
);
@@ -1749,12 +1777,18 @@ public function testSearchAndSortByRelevance()
$responseAsc = $this->graphQlQuery(sprintf($query, 'ASC'));
$this->assertEquals(3, $responseDesc['products']['total_count']);
$this->assertNotEmpty($responseDesc['products']['filters'], 'Filters should have the Category layer');
- $this->assertEquals('Colorful Category', $responseDesc['products']['filters'][0]['filter_items'][0]['label']);
+ $this->assertEquals(
+ 'Colorful Category',
+ $responseDesc['products']['filters'][0]['filter_items'][0]['label']
+ );
$this->assertCount(2, $responseDesc['products']['aggregations']);
$expectedProductsInResponse = ['Blue briefs', 'Navy Blue Striped Shoes', 'Grey shorts'];
$namesDesc = array_column($responseDesc['products']['items'], 'name');
$this->assertEqualsCanonicalizing($expectedProductsInResponse, $namesDesc);
- $this->assertEquals($namesDesc, array_reverse(array_column($responseAsc['products']['items'], 'name')));
+ $this->assertEquals(
+ $namesDesc,
+ array_reverse(array_column($responseAsc['products']['items'], 'name'))
+ );
}
/**
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php
index d573e2893e8f3..9f75e54612510 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php
@@ -86,6 +86,8 @@ public function testQueryAllFieldsSimpleProduct()
}
}
name
+ new_from_date
+ new_to_date
options_container
... on CustomizableProductInterface {
options {
@@ -337,6 +339,8 @@ public function testQueryMediaGalleryEntryFieldsSimpleProduct()
}
}
name
+ new_from_date
+ new_to_date
options_container
... on CustomizableProductInterface {
field_options: options {
@@ -893,9 +897,16 @@ private function assertEavAttributes($product, $actualResponse)
* @param string $eavAttributeCode
* @return string
*/
- private function eavAttributesToGraphQlSchemaFieldTranslator(string $eavAttributeCode)
+ private function eavAttributesToGraphQlSchemaFieldTranslator(string $eavAttributeCode): string
{
- return $eavAttributeCode;
+ switch ($eavAttributeCode) {
+ case 'news_from_date':
+ return 'new_from_date';
+ case 'news_to_date':
+ return 'new_to_date';
+ default:
+ return $eavAttributeCode;
+ }
}
/**
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/CategoryTest.php
new file mode 100644
index 0000000000000..3b106ab48be20
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/CategoryTest.php
@@ -0,0 +1,51 @@
+getSearchQueryWithSCategoryUID();
+ $response = $this->graphQlQuery($query);
+ $this->assertNotEmpty($response['products']);
+ $this->assertEquals(1, count($response['products']['aggregations']));
+ $this->assertNotEmpty($response['products']['aggregations']);
+ $this->assertEquals('price', $response['products']['aggregations'][0]['attribute_code']);
+ }
+
+ /**
+ * Prepare search query with suggestions
+ *
+ * @return string
+ */
+ private function getSearchQueryWithSCategoryUID() : string
+ {
+ return <<getMaskedQuoteIdByReservedOrderId->execute('test_quote');
+ $query = $this->getQuery($maskedQuoteId);
+ $response = $this->graphQlQuery($query);
+
+ self::assertArrayHasKey('cart', $response);
+ self::assertArrayHasKey('available_payment_methods', $response['cart']);
+ self::assertEquals('free', current($response['cart']['available_payment_methods'])['code']);
+ self::assertEquals(
+ 'No Payment Information Required',
+ current($response['cart']['available_payment_methods'])['title']
+ );
+ }
+
/**
* _security
* @magentoApiDataFixture Magento/Customer/_files/customer.php
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php
index f2228900d1085..efbe163a12766 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/CartTotalsTest.php
@@ -62,6 +62,7 @@ public function testGetCartTotalsWithTaxApplied()
$cartItem = $response['cart']['items'][0];
self::assertEquals(10, $cartItem['prices']['price']['value']);
+ self::assertEquals(10.75, $cartItem['prices']['price_including_tax']['value']);
self::assertEquals(20, $cartItem['prices']['row_total']['value']);
self::assertEquals(21.5, $cartItem['prices']['row_total_including_tax']['value']);
@@ -129,6 +130,7 @@ public function testGetCartTotalsWithCatalogRuleApplied()
$cartItem = $response['cart']['items'][0];
self::assertEquals(9, $cartItem['prices']['price']['value']);
+ self::assertEquals(9, $cartItem['prices']['price_including_tax']['value']);
self::assertEquals(18, $cartItem['prices']['row_total']['value']);
self::assertEquals(18, $cartItem['prices']['row_total_including_tax']['value']);
@@ -157,6 +159,7 @@ public function testGetCartTotalsWithCatalogRuleAndTaxApplied()
$cartItem = $response['cart']['items'][0];
self::assertEquals(9, $cartItem['prices']['price']['value']);
+ self::assertEquals(9.68, $cartItem['prices']['price_including_tax']['value']);
self::assertEquals(18, $cartItem['prices']['row_total']['value']);
self::assertEquals(19.35, $cartItem['prices']['row_total_including_tax']['value']);
@@ -185,6 +188,7 @@ public function testGetCartTotalsWithCatalogRuleAndCartRuleApplied()
$cartItem = $response['cart']['items'][0];
self::assertEquals(9, $cartItem['prices']['price']['value']);
+ self::assertEquals(9, $cartItem['prices']['price_including_tax']['value']);
self::assertEquals(18, $cartItem['prices']['row_total']['value']);
self::assertEquals(18, $cartItem['prices']['row_total_including_tax']['value']);
self::assertEquals(9, $cartItem['prices']['total_item_discount']['value']);
@@ -244,6 +248,7 @@ public function testGetTotalsWithNoTaxApplied()
$cartItem = $response['cart']['items'][0];
self::assertEquals(10, $cartItem['prices']['price']['value']);
+ self::assertEquals(10, $cartItem['prices']['price_including_tax']['value']);
self::assertEquals(20, $cartItem['prices']['row_total']['value']);
self::assertEquals(20, $cartItem['prices']['row_total_including_tax']['value']);
@@ -271,6 +276,7 @@ public function testGetCartTotalsWithNoAddressSet()
$cartItem = $response['cart']['items'][0];
self::assertEquals(10, $cartItem['prices']['price']['value']);
+ self::assertEquals(10, $cartItem['prices']['price_including_tax']['value']);
self::assertEquals(20, $cartItem['prices']['row_total']['value']);
self::assertEquals(20, $cartItem['prices']['row_total_including_tax']['value']);
@@ -321,6 +327,10 @@ private function getQuery(string $maskedQuoteId): string
value
currency
}
+ price_including_tax {
+ value
+ currency
+ }
row_total {
value
currency
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php
index 5cd977aee6981..6a6e93ad515b7 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/GetAvailablePaymentMethodsTest.php
@@ -49,6 +49,30 @@ public function testGetAvailablePaymentMethods()
self::assertEquals('Check / Money order', $response['cart']['available_payment_methods'][0]['title']);
}
+ /**
+ * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product_with_zero_price.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/enable_offline_shipping_methods.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product_with_zero_price.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_freeshipping_shipping_method.php
+ */
+ public function testGetAvailablePaymentMethodsForZeroSubTotalCheckout():void
+ {
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote');
+ $query = $this->getQuery($maskedQuoteId);
+ $response = $this->graphQlQuery($query);
+
+ self::assertArrayHasKey('cart', $response);
+ self::assertArrayHasKey('available_payment_methods', $response['cart']);
+ self::assertEquals('free', current($response['cart']['available_payment_methods'])['code']);
+ self::assertEquals(
+ 'No Payment Information Required',
+ current($response['cart']['available_payment_methods'])['title']
+ );
+ }
+
/**
* _security
* @magentoApiDataFixture Magento/Customer/_files/customer.php
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/CartItemPricesWithFPTTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/CartItemPricesWithFPTTest.php
index 1d53bd03be3d4..f1fbe94aec947 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/CartItemPricesWithFPTTest.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Weee/CartItemPricesWithFPTTest.php
@@ -326,6 +326,10 @@ private function getQuery(string $maskedQuoteId): string
value
currency
}
+ price_including_tax {
+ value
+ currency
+ }
row_total {
value
currency
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_missing_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_missing_image.php
new file mode 100644
index 0000000000000..5d2496f93d243
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_missing_image.php
@@ -0,0 +1,37 @@
+get(\Magento\Framework\Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA);
+$fileName = 'magento_small_image.jpg';
+$filePath = 'catalog/category/' . $fileName;
+$mediaDirectory->create('catalog/category');
+$shortImageContent = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileName);
+$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($filePath), $shortImageContent);
+
+
+$filePath = 'catalog/category/magento_small_image.jpg';
+/** @var Category $category */
+$categoryParent = $objectManager->create(Category::class);
+$categoryParent->setName('Parent Image Category')
+ ->setPath('1/2')
+ ->setLevel(2)
+ ->setImage($filePath)
+ ->setAvailableSortBy('name')
+ ->setDefaultSortBy('name')
+ ->setIsActive(true)
+ ->setPosition(1)
+ ->save();
+
+$mediaDirectory->getDriver()->deleteFile($mediaDirectory->getAbsolutePath($filePath));
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_missing_image_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_missing_image_rollback.php
new file mode 100644
index 0000000000000..4800d78e220ba
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_with_missing_image_rollback.php
@@ -0,0 +1,27 @@
+get(Registry::class);
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+/** @var Collection $collection */
+$collection = $objectManager->create(Collection::class);
+$collection
+ ->addAttributeToFilter('name', ['in' => ['Parent Image Category']])
+ ->load()
+ ->delete();
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_with_zero_price.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_with_zero_price.php
new file mode 100644
index 0000000000000..e2b125fa15573
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_with_zero_price.php
@@ -0,0 +1,45 @@
+get(ProductInterfaceFactory::class);
+/** @var DataObjectHelper $dataObjectHelper */
+$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class);
+/** @var ProductRepositoryInterface $productRepository */
+$productRepository = $objectManager->get(ProductRepositoryInterface::class);
+
+$product = $productFactory->create();
+$productData = [
+ ProductInterface::TYPE_ID => Type::TYPE_SIMPLE,
+ ProductInterface::ATTRIBUTE_SET_ID => 4,
+ ProductInterface::SKU => 'simple_product_with_zero_price',
+ ProductInterface::NAME => 'Simple Product With Zero Price',
+ ProductInterface::PRICE => 0,
+ ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH,
+ ProductInterface::STATUS => Status::STATUS_ENABLED,
+];
+$dataObjectHelper->populateWithArray($product, $productData, ProductInterface::class);
+/** Out of interface */
+$product
+ ->setWebsiteIds([1])
+ ->setStockData([
+ 'qty' => 85,
+ 'is_in_stock' => true,
+ 'manage_stock' => true,
+ 'is_qty_decimal' => true,
+ ]);
+$productRepository->save($product);
diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_with_zero_price_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_with_zero_price_rollback.php
new file mode 100644
index 0000000000000..17d2811f33e29
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/GraphQl/Catalog/_files/simple_product_with_zero_price_rollback.php
@@ -0,0 +1,32 @@
+get(ProductRepositoryInterface::class);
+/** @var Registry $registry */
+$registry = $objectManager->get(Registry::class);
+
+$currentArea = $registry->registry('isSecureArea');
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+try {
+ $productRepository->deleteById('simple_product_with_zero_price');
+} catch (NoSuchEntityException $e) {
+ /**
+ * Tests which are wrapped with MySQL transaction clear all data by transaction rollback.
+ */
+}
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', $currentArea);
diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product_with_zero_price.php b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product_with_zero_price.php
new file mode 100644
index 0000000000000..41e5d6e726295
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/GraphQl/Quote/_files/add_simple_product_with_zero_price.php
@@ -0,0 +1,28 @@
+get(ProductRepositoryInterface::class);
+/** @var QuoteFactory $quoteFactory */
+$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class);
+/** @var QuoteResource $quoteResource */
+$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class);
+/** @var CartRepositoryInterface $cartRepository */
+$cartRepository = Bootstrap::getObjectManager()->get(CartRepositoryInterface::class);
+
+$product = $productRepository->get('simple_product_with_zero_price');
+
+$quote = $quoteFactory->create();
+$quoteResource->load($quote, 'test_quote', 'reserved_order_id');
+$quote->addProduct($product, 2);
+$cartRepository->save($quote);
diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/BackOrderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/BackOrderTest.php
new file mode 100644
index 0000000000000..985c49dcdbba0
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/BackOrderTest.php
@@ -0,0 +1,76 @@
+checkQuoteItemQty call
+ * returns a quantity to be backordered, that this value is added to the QuoteItem
+ * and saved into the OrderItem
+ */
+class BackOrderTest extends \PHPUnit\Framework\TestCase
+{
+ /**
+ * @var ObjectManagerInterface
+ */
+ private $objectManager;
+
+ /**
+ * @var QuoteIdMaskFactory
+ */
+ private $quoteIdMaskFactory;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->objectManager = Bootstrap::getObjectManager();
+ $this->quoteIdMaskFactory = $this->objectManager->get(QuoteIdMaskFactory::class);
+ }
+
+ /**
+ * @magentoDataFixture Magento/Sales/_files/quote_with_backorder.php
+ * @return void
+ */
+ public function testCreateOrderWithBackorders()
+ {
+ /** @var Quote $quote */
+ $quote = $this->objectManager->create(Quote::class);
+ $quote->load('test01', 'reserved_order_id');
+
+ /** @var CheckoutSession $checkoutSession */
+ $checkoutSession = $this->objectManager->get(CheckoutSession::class);
+ $checkoutSession->setQuoteId($quote->getId());
+
+ /** @var QuoteIdMask $quoteIdMask */
+ $quoteIdMask = $this->quoteIdMaskFactory->create();
+ $quoteIdMask->load($quote->getId(), 'quote_id');
+ $cartId = $quoteIdMask->getMaskedId();
+
+ /** @var GuestCartManagementInterface $cartManagement */
+ $cartManagement = $this->objectManager->get(GuestCartManagementInterface::class);
+ $orderId = $cartManagement->placeOrder($cartId);
+
+ //The order should have 10 backordered items
+ /** @var Order $order */
+ $order = $this->objectManager->get(OrderRepository::class)->get($orderId);
+ $this->assertNotNull($order);
+ $orderitem = $order->getAllItems()[0];
+ $this->assertEquals($orderitem->getQtyBackordered(), 10);
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_backorder.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_backorder.php
new file mode 100644
index 0000000000000..d5de9af85df80
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_backorder.php
@@ -0,0 +1,74 @@
+loadArea('frontend');
+
+$storeManager = Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\Store\Model\StoreManagerInterface::class);
+
+$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class);
+$product->setTypeId('simple')
+ ->setId(1)
+ ->setAttributeSetId(4)
+ ->setName('Simple Product')
+ ->setSku('simple')
+ ->setPrice(10)
+ ->setTaxClassId(0)
+ ->setMetaTitle('meta title')
+ ->setMetaKeyword('meta keyword')
+ ->setMetaDescription('meta description')
+ ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH)
+ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED)
+ ->setStockData(
+ [
+ 'qty' => 0,
+ 'is_in_stock' => 1,
+ 'manage_stock' => 1,
+ 'backorders' => 2
+ ]
+ )
+ ->setWebsiteIds([$storeManager->getStore()->getWebsiteId()])
+ ->save();
+
+$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+$product = $productRepository->get('simple');
+
+$addressData = include __DIR__ . '/address_data.php';
+$billingAddress = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(
+ \Magento\Quote\Model\Quote\Address::class,
+ ['data' => $addressData]
+);
+$billingAddress->setAddressType('billing');
+$shippingAddress = clone $billingAddress;
+$shippingAddress->setId(null)->setAddressType('shipping');
+$store = $storeManager->getStore();
+
+/** @var \Magento\Quote\Model\Quote $quote */
+$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class);
+$quote->setCustomerIsGuest(true)
+ ->setStoreId($store->getId())
+ ->setReservedOrderId('test01')
+ ->setBillingAddress($billingAddress)
+ ->setShippingAddress($shippingAddress)
+ ->addProduct($product, 10);
+$quote->getPayment()->setMethod('checkmo');
+$quote->setIsMultiShipping('0');
+$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate')->setCollectShippingRates(true);
+$quote->collectTotals();
+
+$quoteRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\Quote\Api\CartRepositoryInterface::class);
+$quoteRepository->save($quote);
+
+/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */
+$quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class)
+ ->create();
+$quoteIdMask->setQuoteId($quote->getId());
+$quoteIdMask->setDataChanges(true);
+$quoteIdMask->save();
diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_backorder_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_backorder_rollback.php
new file mode 100644
index 0000000000000..94d2e8c3c1e33
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_backorder_rollback.php
@@ -0,0 +1,37 @@
+get(\Magento\Framework\Registry::class);
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+/** @var $quote \Magento\Quote\Model\Quote */
+$quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class);
+$quote->load('test01', 'reserved_order_id');
+if ($quote->getId()) {
+ $quote->delete();
+}
+
+/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */
+$productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
+
+try {
+ $product = $productRepository->get('simple', false, null, true);
+ $productRepository->delete($product);
+} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) {
+ //Product already removed
+}
+
+/** @var \Magento\CatalogInventory\Model\StockRegistryStorage $stockRegistryStorage */
+$stockRegistryStorage = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()
+ ->get(\Magento\CatalogInventory\Model\StockRegistryStorage::class);
+$stockRegistryStorage->removeStockItem(1);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/lib/internal/Magento/Framework/Data/Argument/Interpreter/ArrayType.php b/lib/internal/Magento/Framework/Data/Argument/Interpreter/ArrayType.php
index 9a881ccf515ab..1a914a8f9b0d1 100644
--- a/lib/internal/Magento/Framework/Data/Argument/Interpreter/ArrayType.php
+++ b/lib/internal/Magento/Framework/Data/Argument/Interpreter/ArrayType.php
@@ -5,7 +5,9 @@
*/
namespace Magento\Framework\Data\Argument\Interpreter;
+use InvalidArgumentException;
use Magento\Framework\Data\Argument\InterpreterInterface;
+use Magento\Framework\ObjectManager\Helper\SortItems as SortItemsHelper;
/**
* Interpreter of array data type that supports arrays of unlimited depth
@@ -17,107 +19,39 @@ class ArrayType implements InterpreterInterface
*
* @var InterpreterInterface
*/
- private $itemInterpreter;
+ private InterpreterInterface $itemInterpreter;
+
+ /**
+ * @var SortItemsHelper
+ */
+ private SortItemsHelper $sortItemsHelper;
/**
* @param InterpreterInterface $itemInterpreter
+ * @param SortItemsHelper|null $sortItemsHelper
*/
- public function __construct(InterpreterInterface $itemInterpreter)
+ public function __construct(InterpreterInterface $itemInterpreter, SortItemsHelper $sortItemsHelper = null)
{
$this->itemInterpreter = $itemInterpreter;
+ $this->sortItemsHelper = $sortItemsHelper ?: new \Magento\Framework\ObjectManager\Helper\SortItems();
}
/**
* @inheritdoc
* @return array
- * @throws \InvalidArgumentException
+ * @throws InvalidArgumentException
*/
- public function evaluate(array $data)
+ public function evaluate(array $data): array
{
- $items = isset($data['item']) ? $data['item'] : [];
+ $items = $data['item'] ?? [];
if (!is_array($items)) {
- throw new \InvalidArgumentException('Array items are expected.');
+ throw new InvalidArgumentException('Array items are expected.');
}
$result = [];
- $items = $this->sortItems($items);
+ $items = $this->sortItemsHelper->sortItems($items);
foreach ($items as $itemKey => $itemData) {
$result[$itemKey] = $this->itemInterpreter->evaluate($itemData);
}
return $result;
}
-
- /**
- * Sort items by sort order attribute.
- *
- * @param array $items
- * @return array
- */
- private function sortItems($items)
- {
- $sortOrderDefined = $this->isSortOrderDefined($items);
- if ($sortOrderDefined) {
- $indexedItems = [];
- foreach ($items as $key => $item) {
- $indexedItems[] = ['key' => $key, 'item' => $item];
- }
- uksort(
- $indexedItems,
- function ($firstItemKey, $secondItemKey) use ($indexedItems) {
- return $this->compareItems($firstItemKey, $secondItemKey, $indexedItems);
- }
- );
- // Convert array of sorted items back to initial format
- $items = [];
- foreach ($indexedItems as $indexedItem) {
- $items[$indexedItem['key']] = $indexedItem['item'];
- }
- }
- return $items;
- }
-
- /**
- * Compare sortOrder of item
- *
- * @param mixed $firstItemKey
- * @param mixed $secondItemKey
- * @param array $indexedItems
- * @return int
- */
- private function compareItems($firstItemKey, $secondItemKey, $indexedItems)
- {
- $firstItem = $indexedItems[$firstItemKey]['item'];
- $secondItem = $indexedItems[$secondItemKey]['item'];
- $firstValue = 0;
- $secondValue = 0;
- if (isset($firstItem['sortOrder'])) {
- $firstValue = (int)$firstItem['sortOrder'];
- }
-
- if (isset($secondItem['sortOrder'])) {
- $secondValue = (int)$secondItem['sortOrder'];
- }
-
- if ($firstValue == $secondValue) {
- // These keys reflect initial relative position of items.
- // Allows stable sort for items with equal 'sortOrder'
- return $firstItemKey < $secondItemKey ? -1 : 1;
- }
- return $firstValue < $secondValue ? -1 : 1;
- }
-
- /**
- * Determine if a sort order exists for any of the items.
- *
- * @param array $items
- * @return bool
- */
- private function isSortOrderDefined($items)
- {
- foreach ($items as $itemData) {
- if (isset($itemData['sortOrder'])) {
- return true;
- }
- }
- return false;
- }
}
diff --git a/lib/internal/Magento/Framework/Data/Argument/Interpreter/DataObject.php b/lib/internal/Magento/Framework/Data/Argument/Interpreter/DataObject.php
index 3ff27f6aa89eb..25af622482abd 100644
--- a/lib/internal/Magento/Framework/Data/Argument/Interpreter/DataObject.php
+++ b/lib/internal/Magento/Framework/Data/Argument/Interpreter/DataObject.php
@@ -28,13 +28,16 @@ public function __construct(BooleanUtils $booleanUtils)
* Compute and return effective value of an argument
*
* @param array $data
- * @return mixed
+ * @return array
* @throws \InvalidArgumentException
* @throws \UnexpectedValueException
*/
- public function evaluate(array $data)
+ public function evaluate(array $data): array
{
$result = ['instance' => $data['value']];
+ if (array_key_exists('sortOrder', $data)) {
+ $result['sortOrder'] = $data['sortOrder'];
+ }
if (isset($data['shared'])) {
$result['shared'] = $this->booleanUtils->toBoolean($data['shared']);
}
diff --git a/lib/internal/Magento/Framework/ObjectManager/Config/Config.php b/lib/internal/Magento/Framework/ObjectManager/Config/Config.php
index df74eedf463a4..861a6de2c0995 100644
--- a/lib/internal/Magento/Framework/ObjectManager/Config/Config.php
+++ b/lib/internal/Magento/Framework/ObjectManager/Config/Config.php
@@ -5,12 +5,14 @@
*/
namespace Magento\Framework\ObjectManager\Config;
-use Magento\Framework\Serialize\SerializerInterface;
use Magento\Framework\ObjectManager\ConfigCacheInterface;
+use Magento\Framework\ObjectManager\ConfigInterface;
use Magento\Framework\ObjectManager\DefinitionInterface;
+use Magento\Framework\ObjectManager\Helper\SortItems as SortItemsHelper;
use Magento\Framework\ObjectManager\RelationsInterface;
+use Magento\Framework\Serialize\SerializerInterface;
-class Config implements \Magento\Framework\ObjectManager\ConfigInterface
+class Config implements ConfigInterface
{
/**
* Config cache
@@ -22,12 +24,11 @@ class Config implements \Magento\Framework\ObjectManager\ConfigInterface
/**
* Class definitions
*
- * @var \Magento\Framework\ObjectManager\DefinitionInterface
+ * @var DefinitionInterface
*/
protected $_definitions;
/**
- * Current cache key
*
* @var string
*/
@@ -41,7 +42,6 @@ class Config implements \Magento\Framework\ObjectManager\ConfigInterface
protected $_preferences = [];
/**
- * Virtual types
*
* @var array
*/
@@ -76,18 +76,28 @@ class Config implements \Magento\Framework\ObjectManager\ConfigInterface
protected $_mergedArguments;
/**
- * @var \Magento\Framework\Serialize\SerializerInterface
+ * @var SerializerInterface
*/
private $serializer;
/**
- * @param RelationsInterface $relations
- * @param DefinitionInterface $definitions
+ * @var SortItemsHelper
*/
- public function __construct(RelationsInterface $relations = null, DefinitionInterface $definitions = null)
- {
+ private SortItemsHelper $sortItemsHelper;
+
+ /**
+ * @param RelationsInterface|null $relations
+ * @param DefinitionInterface|null $definitions
+ * @param SortItemsHelper|null $sortItemsHelper
+ */
+ public function __construct(
+ RelationsInterface $relations = null,
+ DefinitionInterface $definitions = null,
+ SortItemsHelper $sortItemsHelper = null
+ ) {
$this->_relations = $relations ?: new \Magento\Framework\ObjectManager\Relations\Runtime();
$this->_definitions = $definitions ?: new \Magento\Framework\ObjectManager\Definition\Runtime();
+ $this->sortItemsHelper = $sortItemsHelper ?: new \Magento\Framework\ObjectManager\Helper\SortItems();
}
/**
@@ -185,11 +195,12 @@ public function getPreference($type)
* @return array
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
- protected function _collectConfiguration($type)
+ protected function _collectConfiguration($type): array
{
if (!isset($this->_mergedArguments[$type])) {
if (isset($this->_virtualTypes[$type])) {
$arguments = $this->_collectConfiguration($this->_virtualTypes[$type]);
+ $arguments = $this->sortItemsHelper->sortItems($arguments);
} elseif ($this->_relations->has($type)) {
$relations = $this->_relations->getParents($type);
$arguments = [];
@@ -198,6 +209,7 @@ protected function _collectConfiguration($type)
$relationArguments = $this->_collectConfiguration($relation);
if ($relationArguments) {
$arguments = array_replace($arguments, $relationArguments);
+ $arguments = $this->sortItemsHelper->sortItems($arguments);
}
}
}
@@ -208,6 +220,7 @@ protected function _collectConfiguration($type)
if (isset($this->_arguments[$type])) {
if ($arguments && count($arguments)) {
$arguments = array_replace_recursive($arguments, $this->_arguments[$type]);
+ $arguments = $this->sortItemsHelper->sortItems($arguments);
} else {
$arguments = $this->_arguments[$type];
}
@@ -339,7 +352,7 @@ public function getPreferences()
/**
* Get serializer
*
- * @return \Magento\Framework\Serialize\SerializerInterface
+ * @return SerializerInterface
* @deprecated 101.0.0
*/
private function getSerializer()
diff --git a/lib/internal/Magento/Framework/ObjectManager/Helper/SortItems.php b/lib/internal/Magento/Framework/ObjectManager/Helper/SortItems.php
new file mode 100644
index 0000000000000..c2f45e340b44f
--- /dev/null
+++ b/lib/internal/Magento/Framework/ObjectManager/Helper/SortItems.php
@@ -0,0 +1,141 @@
+isSortOrderDefined($items);
+ if ($sortOrderDefined) {
+ if (!$this->isMultiSortOrder($items)) {
+ $indexedItems = [];
+ foreach ($items as $key => $item) {
+ $indexedItems[] = ['key' => $key, 'item' => $item];
+ }
+ uksort(
+ $indexedItems,
+ function ($firstItemKey, $secondItemKey) use ($indexedItems) {
+ return $this->compareItems($firstItemKey, $secondItemKey, $indexedItems);
+ }
+ );
+ // Convert array of sorted items back to initial format
+ $items = [];
+ foreach ($indexedItems as $indexedItem) {
+ $items[$indexedItem['key']] = $indexedItem['item'];
+ }
+ } else {
+ $indexedItem = [];
+ foreach ($items as $key => $itemData) {
+ foreach ($itemData as $itemKey => $item) {
+ $indexedItem[] = ['parent'=>$key, 'key' => $itemKey, 'item' => $item];
+ }
+ }
+
+ uksort(
+ $indexedItem,
+ function ($firstItemKey, $secondItemKey) use ($indexedItem) {
+ return $this->compareItems($firstItemKey, $secondItemKey, $indexedItem);
+ }
+ );
+ $items = [];
+ foreach ($indexedItem as $iItem) {
+ $items[$iItem['parent']][$iItem['key']] = $iItem['item'];
+ }
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * Compare sortOrder of item
+ *
+ * @param mixed $firstItemKey
+ * @param mixed $secondItemKey
+ * @param array $indexedItems
+ * @return int
+ */
+ private function compareItems($firstItemKey, $secondItemKey, array $indexedItems): int
+ {
+ $firstItem = $indexedItems[$firstItemKey]['item'];
+ $secondItem = $indexedItems[$secondItemKey]['item'];
+ $firstValue = 0;
+ $secondValue = 0;
+ if (isset($firstItem['sortOrder'])) {
+ $firstValue = (int)$firstItem['sortOrder'];
+ }
+
+ if (isset($secondItem['sortOrder'])) {
+ $secondValue = (int)$secondItem['sortOrder'];
+ }
+
+ if ($firstValue == $secondValue) {
+ // These keys reflect initial relative position of items.
+ // Allows stable sort for items with equal 'sortOrder'
+ return $firstValue <=> $secondValue;
+ }
+ return $firstValue <=> $secondValue;
+ }
+
+ /**
+ * Determine if a sort order exists for any of the items.
+ *
+ * @param array $items
+ * @return bool
+ */
+ private function isSortOrderDefined(array $items): bool
+ {
+ $isSortOrder = false;
+
+ array_walk($items, function ($value) use (&$isSortOrder) {
+ if (!!is_array($value)) {
+ if (isset($value['sortOrder'])) {
+ $isSortOrder = true;
+ } else {
+ array_walk($value, function ($valueData) use (&$isSortOrder) {
+ if (isset($valueData['sortOrder'])) {
+ $isSortOrder = true;
+ }
+ });
+ }
+ }
+ });
+
+ return $isSortOrder;
+ }
+ /**
+ * Determine if a sort order exists for any of the items.
+ *
+ * @param array $items
+ * @return bool
+ */
+ private function isMultiSortOrder(array $items): bool
+ {
+ $isMultiSortOrder = false;
+
+ array_walk($items, function ($value) use (&$isMultiSortOrder) {
+ if (!!is_array($value)) {
+ array_walk($value, function ($valueData) use (&$isMultiSortOrder) {
+ $isMultiSortOrder = isset($valueData['sortOrder']);
+ });
+ }
+ });
+
+ return $isMultiSortOrder;
+ }
+}
diff --git a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/ConfigTest.php b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/ConfigTest.php
index 419fd482d89ed..0852a3017a549 100644
--- a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/ConfigTest.php
+++ b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Config/ConfigTest.php
@@ -10,6 +10,7 @@
use Magento\Framework\ObjectManager\Config\Config;
use Magento\Framework\ObjectManager\ConfigCacheInterface;
use Magento\Framework\ObjectManager\DefinitionInterface;
+use Magento\Framework\ObjectManager\Helper\SortItems;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use PHPUnit\Framework\TestCase;
@@ -60,7 +61,16 @@ public function testExtendWithCacheMock()
$serializerMock->expects($this->atLeast(2))
->method('serialize')
->willReturn('[[],[],[],[]]');
- $config = new Config(null, $definitions, $serializerMock);
+
+ $sortItemsMock = $this->getMockForAbstractClass(SortItems::class);
+ $config =$this->objectManagerHelper->getObject(
+ Config::class,
+ [
+ 'relations' => null,
+ 'definitions' => $definitions,
+ 'sortItemsHelper' => $sortItemsMock,
+ ]
+ );
$this->objectManagerHelper->setBackwardCompatibleProperty(
$config,
'serializer',
diff --git a/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Helper/SortItemsTest.php b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Helper/SortItemsTest.php
new file mode 100644
index 0000000000000..44646946bafb7
--- /dev/null
+++ b/lib/internal/Magento/Framework/ObjectManager/Test/Unit/Helper/SortItemsTest.php
@@ -0,0 +1,143 @@
+_model = new SortItemsHelper();
+ }
+
+ /**
+ * @param array $input
+ * @param array $expected
+ *
+ * @dataProvider evaluateDataProvider
+ */
+ public function testSortItems(array $input, array $expected): void
+ {
+ $actual = $this->_model->sortItems($input);
+ $this->assertSame($expected, $actual);
+ }
+
+ /**
+ * @return array
+ */
+ public function evaluateDataProvider(): array
+ {
+ return [
+ 'empty array items' => [
+ [],
+ [],
+ ],
+ 'absent array items' => [
+ [],
+ [],
+ ],
+ 'present array items' => [
+ [
+ 'key1' => ['value' => 'value 1'],
+ 'key2' => ['value' => 'value 2'],
+ 'key3' => ['value' => 'value 3'],
+ ],
+ [
+ 'key1' => ['value' => 'value 1'],
+ 'key2' => ['value' => 'value 2'],
+ 'key3' => ['value' => 'value 3'],
+ ],
+ ],
+ 'sorted array items' => [
+ [
+ 'key1' => ['value' => 'value 1', 'sortOrder' => 50],
+ 'key2' => ['value' => 'value 2'],
+ 'key3' => ['value' => 'value 3', 'sortOrder' => 10],
+ 'key4' => ['value' => 'value 4'],
+ ],
+ [
+ 'key2' => ['value' => 'value 2'],
+ 'key4' => ['value' => 'value 4'],
+ 'key3' => ['value' => 'value 3', 'sortOrder' => 10],
+ 'key1' => ['value' => 'value 1', 'sortOrder' => 50],
+ ],
+ ],
+ 'multi sorted array items' => [
+ [
+ 'key1' => ['value' => 'value 1'],
+ 'key2' => ['value' => 'value 2'],
+ 'item1' => [
+ 'key3'=>['value' => 'value 3', 'sortOrder' => 30],
+ 'key4'=>['value' => 'value 4', 'sortOrder' => 10],
+ 'key5'=>['value' => 'value 5', 'sortOrder' => 20],
+ ],
+ ],
+ [
+ 'key1' => ['value' => 'value 1'],
+ 'key2' => ['value' => 'value 2'],
+ 'item1' => [
+ 'key4'=>['value' => 'value 4', 'sortOrder' => 10],
+ 'key5'=>['value' => 'value 5', 'sortOrder' => 20],
+ 'key3'=>['value' => 'value 3', 'sortOrder' => 30],
+ ],
+
+ ],
+ ],
+ 'pre-sorted array items' => [
+ [
+ 'item' => [
+ 'key1' => ['value' => 'value 1'],
+ 'key4' => ['value' => 'value 4'],
+ 'key3' => ['value' => 'value 3'],
+ 'key2' => ['value' => 'value 2', 'sortOrder' => 10],
+ ]
+ ],
+ [
+ 'item' => [
+ 'key1' => ['value' => 'value 1'],
+ 'key4' => ['value' => 'value 4'],
+ 'key3' => ['value' => 'value 3'],
+ 'key2' => ['value' => 'value 2', 'sortOrder' => 10],
+ ]
+ ],
+ ],
+ 'sort order edge case values' => [
+ [
+ 'item' => [
+ 'key1' => ['value' => 'value 1', 'sortOrder' => 101],
+ 'key4' => ['value' => 'value 4'],
+ 'key2' => ['value' => 'value 2', 'sortOrder' => -10],
+ 'key3' => ['value' => 'value 3'],
+ 'key5' => ['value' => 'value 5', 'sortOrder' => 20],
+ ],
+ ],
+ [
+ 'item' => [
+ 'key2' => ['value' => 'value 2', 'sortOrder' => -10],
+ 'key4' => ['value' => 'value 4'],
+ 'key3' => ['value' => 'value 3'],
+ 'key5' => ['value' => 'value 5', 'sortOrder' => 20],
+ 'key1' => ['value' => 'value 1', 'sortOrder' => 101],
+ ]
+ ],
+ ],
+ ];
+ }
+}