diff --git a/app/code/Magento/CatalogGraphQl/Plugin/ProductAttributeSortInput.php b/app/code/Magento/CatalogGraphQl/Plugin/ProductAttributeSortInput.php new file mode 100644 index 000000000000..ac1a2279b771 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Plugin/ProductAttributeSortInput.php @@ -0,0 +1,95 @@ +getSortFieldsOrder($info, $args['sort']); + } + return [$field, $context, $info, $value, $args]; + } + + /** + * Get sort fields in the original order + * + * @param ResolveInfo $info + * @param array $sortFields + * @return array + * @throws \Exception + */ + private function getSortFieldsOrder(ResolveInfo $info, array $sortFields) + { + $sortFieldsOriginal = []; + Visitor::visit( + $info->operation, + [ + 'enter' => [ + NodeKind::ARGUMENT => function (Node $node) use (&$sortFieldsOriginal, $sortFields) { + if ($node->name->value === 'sort') { + Visitor::visit( + $node->value, + [ + 'enter' => [ + NodeKind::OBJECT_FIELD => + function (Node $node) use (&$sortFieldsOriginal, $sortFields) { + if (isset($sortFields[$node->name->value])) { + $sortFieldsOriginal[$node->name->value] = + $sortFields[$node->name->value]; + } + } + ] + ] + ); + return Visitor::stop(); + } + } + ] + ] + ); + return $sortFieldsOriginal; + } +} diff --git a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml index a6fbced9b42c..bdae2e16ff84 100644 --- a/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/graphql/di.xml @@ -283,4 +283,12 @@ + + + + + + + + diff --git a/app/code/Magento/CustomerImportExport/Model/Import/AbstractCustomer.php b/app/code/Magento/CustomerImportExport/Model/Import/AbstractCustomer.php index 8fb08cecc54c..1b5674b85fe4 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/AbstractCustomer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/AbstractCustomer.php @@ -6,6 +6,8 @@ namespace Magento\CustomerImportExport\Model\Import; +use Magento\Customer\Model\Config\Share; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Validator\EmailAddress; use Magento\Framework\Validator\ValidateException; use Magento\Framework\Validator\ValidatorChain; @@ -87,6 +89,11 @@ abstract class AbstractCustomer extends \Magento\ImportExport\Model\Import\Entit */ protected $masterAttributeCode = '_email'; + /** + * @var Share + */ + private $configShare; + /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -99,6 +106,7 @@ abstract class AbstractCustomer extends \Magento\ImportExport\Model\Import\Entit * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\CustomerImportExport\Model\ResourceModel\Import\Customer\StorageFactory $storageFactory * @param array $data + * @param Share|null $configShare * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -112,7 +120,8 @@ public function __construct( \Magento\ImportExport\Model\Export\Factory $collectionFactory, \Magento\Eav\Model\Config $eavConfig, \Magento\CustomerImportExport\Model\ResourceModel\Import\Customer\StorageFactory $storageFactory, - array $data = [] + array $data = [], + ?Share $configShare = null ) { $this->_storageFactory = $storageFactory; parent::__construct( @@ -127,7 +136,7 @@ public function __construct( $eavConfig, $data ); - + $this->configShare = $configShare ?? ObjectManager::getInstance()->get(Share::class); $this->addMessageTemplate(self::ERROR_WEBSITE_IS_EMPTY, __('Please specify a website.')); $this->addMessageTemplate( self::ERROR_EMAIL_IS_EMPTY, @@ -174,6 +183,11 @@ protected function _initCustomers(array $data) protected function _getCustomerId($email, $websiteCode) { $email = strtolower(trim($email)); + + if ($this->configShare->isGlobalScope()) { + return $this->_customerStorage->getCustomerIdByEmail($email); + } + if (isset($this->_websiteCodeToId[$websiteCode])) { $websiteId = $this->_websiteCodeToId[$websiteCode]; return $this->_customerStorage->getCustomerId($email, $websiteId); diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index 17a2b3678d9c..c8d33ff14dd2 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -6,6 +6,7 @@ namespace Magento\CustomerImportExport\Model\Import; +use Magento\Customer\Model\Config\Share; use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites as CountryWithWebsitesSource; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\App\ObjectManager; @@ -272,7 +273,8 @@ class Address extends AbstractCustomer * @param array $data * @param CountryWithWebsitesSource|null $countryWithWebsites * @param AddressStorage|null $addressStorage - * @param Processor $indexerProcessor + * @param Processor|null $indexerProcessor + * @param Share|null $configShare * * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -297,7 +299,8 @@ public function __construct( array $data = [], ?CountryWithWebsitesSource $countryWithWebsites = null, ?AddressStorage $addressStorage = null, - ?Processor $indexerProcessor = null + ?Processor $indexerProcessor = null, + ?Share $configShare = null ) { $this->_customerFactory = $customerFactory; $this->_addressFactory = $addressFactory; @@ -325,7 +328,8 @@ public function __construct( $collectionFactory, $eavConfig, $storageFactory, - $data + $data, + $configShare ); $this->_entityTable = isset( diff --git a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php index 21a2252257f7..0c16e2010fe5 100644 --- a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php +++ b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php @@ -5,6 +5,7 @@ */ namespace Magento\CustomerImportExport\Model\ResourceModel\Import\Customer; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\ResourceModel\Customer\Collection as CustomerCollection; use Magento\Customer\Model\ResourceModel\Customer\CollectionFactory as CustomerCollectionFactory; use Magento\Framework\DataObject; @@ -29,6 +30,11 @@ class Storage */ protected $_customerIds = []; + /** + * @var array + */ + private $customerIdsByEmail = []; + /** * Number of items to fetch from db in one query * @@ -60,12 +66,19 @@ class Storage */ private $customerStoreIds = []; + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + /** * @param CustomerCollectionFactory $collectionFactory + * @param CustomerRepositoryInterface $customerRepository * @param array $data */ public function __construct( CustomerCollectionFactory $collectionFactory, + CustomerRepositoryInterface $customerRepository, array $data = [] ) { $this->_customerCollection = isset( @@ -73,6 +86,7 @@ public function __construct( ) ? $data['customer_collection'] : $collectionFactory->create(); $this->_pageSize = isset($data['page_size']) ? (int) $data['page_size'] : 0; $this->customerCollectionFactory = $collectionFactory; + $this->customerRepository = $customerRepository; } /** @@ -130,7 +144,8 @@ public function addCustomerByArray(array $customer): Storage /** * Add customer to array * - * @deprecated 100.3.0 @see addCustomerByArray + * @deprecated 100.3.0 + * @see addCustomerByArray * @param DataObject $customer * @return $this */ @@ -164,6 +179,25 @@ public function getCustomerId(string $email, int $websiteId) return false; } + /** + * Find customer ID by email. + * + * @param string $email + * @return bool|int + */ + public function getCustomerIdByEmail(string $email) + { + if (!isset($this->customerIdsByEmail[$email])) { + try { + $this->customerIdsByEmail[$email] = $this->customerRepository->get($email)->getId(); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + $this->customerIdsByEmail[$email] = false; + } + } + + return $this->customerIdsByEmail[$email]; + } + /** * Get previously loaded customer id. * diff --git a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php index c86ba7966113..be66976ac7ca 100644 --- a/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php +++ b/app/code/Magento/CustomerImportExport/Test/Unit/Model/Import/AddressTest.php @@ -9,6 +9,7 @@ use Magento\Customer\Model\Address\Validator\Postcode; use Magento\Customer\Model\AddressFactory; +use Magento\Customer\Model\Config\Share; use Magento\Customer\Model\CustomerFactory; use Magento\Customer\Model\Indexer\Processor; use Magento\Customer\Model\ResourceModel\Address\Attribute as AddressAttribute; @@ -149,6 +150,16 @@ class AddressTest extends TestCase */ private $countryWithWebsites; + /** + * @var Share|MockObject + */ + private $configShare; + + /** + * @var Storage + */ + private $customerStorage; + /** * Init entity adapter model */ @@ -171,6 +182,7 @@ protected function setUp(): void ->method('getAllOptions') ->willReturn([]); + $this->configShare = $this->createMock(Share::class); $this->_model = $this->_getModelMock(); $this->errorAggregator = $this->createPartialMock( ProcessingErrorAggregator::class, @@ -198,7 +210,7 @@ protected function _getModelDependencies() ->getMock(); $connection = $this->createMock(\stdClass::class); $attributeCollection = $this->_createAttrCollectionMock(); - $customerStorage = $this->_createCustomerStorageMock(); + $this->customerStorage = $this->_createCustomerStorageMock(); $customerEntity = $this->_createCustomerEntityMock(); $addressCollection = new Collection( $this->createMock(EntityFactory::class) @@ -222,7 +234,7 @@ protected function _getModelDependencies() 'bunch_size' => 1, 'attribute_collection' => $attributeCollection, 'entity_type_id' => 1, - 'customer_storage' => $customerStorage, + 'customer_storage' => $this->customerStorage, 'customer_entity' => $customerEntity, 'address_collection' => $addressCollection, 'entity_table' => 'not_used', @@ -388,7 +400,8 @@ protected function _getModelMock() $this->_getModelDependencies(), $this->countryWithWebsites, $this->createMock(\Magento\CustomerImportExport\Model\ResourceModel\Import\Address\Storage::class), - $this->createMock(Processor::class) + $this->createMock(Processor::class), + $this->configShare ); $property = new \ReflectionProperty($modelMock, '_availableBehaviors'); @@ -447,6 +460,37 @@ public function testValidateRowForUpdate(array $rowData, array $errors, $isValid { $this->_model->setParameters(['behavior' => Import::BEHAVIOR_ADD_UPDATE]); + $this->configShare->expects($this->once()) + ->method('isGlobalScope') + ->willReturn(false); + + if ($isValid) { + $this->assertTrue($this->_model->validateRow($rowData, 0)); + } else { + $this->assertFalse($this->_model->validateRow($rowData, 0)); + } + } + + /** + * @dataProvider validateRowForUpdateDataProvider + * + * @param array $rowData + * @param array $errors + * @param boolean $isValid + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function testValidateRowForUpdateGlobalCustomer(array $rowData, array $errors, $isValid = false) + { + $this->_model->setParameters(['behavior' => Import::BEHAVIOR_ADD_UPDATE]); + + $this->configShare->expects($this->once()) + ->method('isGlobalScope') + ->willReturn(true); + + $this->customerStorage->expects($this->once()) + ->method('getCustomerIdByEmail') + ->willReturn(1); + if ($isValid) { $this->assertTrue($this->_model->validateRow($rowData, 0)); } else { diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Range.php b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Range.php index a2cab32ea4e6..5cad2317ce9b 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Range.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Filter/Builder/Range.php @@ -26,6 +26,8 @@ public function __construct( } /** + * Add the range filters + * * @param RequestFilterInterface|RangeFilterRequest $filter * @return array */ @@ -33,10 +35,10 @@ public function buildFilter(RequestFilterInterface $filter) { $filterQuery = []; $fieldName = $this->fieldMapper->getFieldName($filter->getField()); - if ($filter->getFrom()) { + if ($filter->getFrom() !== null) { $filterQuery['range'][$fieldName]['gte'] = $filter->getFrom(); } - if ($filter->getTo()) { + if ($filter->getTo() !== null) { $filterQuery['range'][$fieldName]['lte'] = $filter->getTo(); } return [$filterQuery]; diff --git a/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php b/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php index c3da61f0bd87..471b895aa94f 100644 --- a/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php +++ b/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php @@ -157,7 +157,8 @@ private function addItemToCart(Quote $cart, Data\CartItem $cartItem, int $cartIt $cartItemPosition ); } else { - $product = $this->productReader->getProductBySku($sku); + $productBySku = $this->productReader->getProductBySku($sku); + $product = isset($productBySku) ? clone $productBySku : null; if (!$product || !$product->isSaleable() || !$product->isAvailable()) { $errors[] = $this->error->create( __('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(), diff --git a/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php index f2a1cff4c5b9..236006bc521b 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php +++ b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php @@ -72,5 +72,6 @@ public function execute(CartInterface $quote, bool $increment): void $this->couponUsagePublisher->publish($updateInfo); $this->processor->updateCustomerRulesUsages($updateInfo); + $this->processor->updateCouponUsages($updateInfo); } } diff --git a/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php index 3ae4ec80f537..7255b455c90a 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php +++ b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php @@ -12,6 +12,7 @@ use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; +use Magento\SalesRule\Model\Service\CouponUsagePublisher; /** * Updates the coupon usages @@ -28,16 +29,25 @@ class UpdateCouponUsages */ private $updateInfoFactory; + /** + * @var CouponUsagePublisher + */ + private $couponUsagePublisher; + /** * @param CouponUsageProcessor $couponUsageProcessor * @param UpdateInfoFactory $updateInfoFactory + * @param ?CouponUsagePublisher $couponUsagePublisher */ public function __construct( CouponUsageProcessor $couponUsageProcessor, - UpdateInfoFactory $updateInfoFactory + UpdateInfoFactory $updateInfoFactory, + ?CouponUsagePublisher $couponUsagePublisher = null ) { $this->couponUsageProcessor = $couponUsageProcessor; $this->updateInfoFactory = $updateInfoFactory; + $this->couponUsagePublisher = $couponUsagePublisher + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CouponUsagePublisher::class); } /** @@ -66,7 +76,9 @@ public function execute(OrderInterface $subject, bool $increment): OrderInterfac $updateInfo->setCouponAlreadyApplied(true); } - $this->couponUsageProcessor->process($updateInfo); + $this->couponUsagePublisher->publish($updateInfo); + $this->couponUsageProcessor->updateCustomerRulesUsages($updateInfo); + $this->couponUsageProcessor->updateCouponUsages($updateInfo); return $subject; } diff --git a/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php b/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php index e6dae81cf6eb..2a1de27876f7 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php +++ b/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php @@ -89,7 +89,7 @@ public function updateRuleUsages(UpdateInfo $updateInfo): void } $rule->loadCouponCode(); - if ($isIncrement || $rule->getTimesUsed() > 0) { + if ((!$updateInfo->isCouponAlreadyApplied() && $isIncrement) || !$isIncrement) { $rule->setTimesUsed($rule->getTimesUsed() + ($isIncrement ? 1 : -1)); $rule->save(); } diff --git a/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php b/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php index a3224f52ea53..266e9ddf97cc 100644 --- a/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php +++ b/app/code/Magento/SalesRule/Model/CouponUsageConsumer.php @@ -80,7 +80,6 @@ public function process(OperationInterface $operation): void $data = $this->serializer->unserialize($serializedData); $updateInfo = $this->updateInfoFactory->create(); $updateInfo->setData($data); - $this->processor->updateCouponUsages($updateInfo); $this->processor->updateRuleUsages($updateInfo); } catch (NotFoundException $e) { $this->logger->critical($e->getMessage()); diff --git a/app/code/Magento/SalesRule/etc/db_schema.xml b/app/code/Magento/SalesRule/etc/db_schema.xml index 3912ba3642ba..8c33870de493 100644 --- a/app/code/Magento/SalesRule/etc/db_schema.xml +++ b/app/code/Magento/SalesRule/etc/db_schema.xml @@ -36,7 +36,7 @@ default="0" comment="Discount Step"/> - 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 cbf2aa7fe83f..b28bde114cda 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -364,6 +364,54 @@ public function testFilterLn(): void ); } + /** + * Verify that products returned in a correct order + * + * @magentoApiDataFixture Magento/Catalog/_files/products_for_search.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSortMultipleFields(): void + { + $query = <<graphQlQuery($query); + $this->assertEquals(5, $response['products']['total_count']); + $prod1 = $this->productRepository->get('search_product_5'); + $prod2 = $this->productRepository->get('search_product_4'); + $prod3 = $this->productRepository->get('search_product_3'); + $prod4 = $this->productRepository->get('search_product_1'); + $prod5 = $this->productRepository->get('search_product_2'); + + $filteredProducts = [$prod1, $prod2, $prod3, $prod4, $prod5]; + $productItemsInResponse = array_map(null, $response['products']['items'], $filteredProducts); + foreach ($productItemsInResponse as $itemIndex => $itemArray) { + $this->assertNotEmpty($itemArray); + $this->assertResponseFields( + $productItemsInResponse[$itemIndex][0], + [ + 'name' => $filteredProducts[$itemIndex]->getName(), + ] + ); + } + } + /** * Compare arrays by value in 'name' field. * @@ -2158,9 +2206,9 @@ public function testProductBasicFullTextSearchQuery(): void } } QUERY; - $prod1 = $this->productRepository->get('blue_briefs'); + $prod1 = $this->productRepository->get('navy-striped-shoes'); $prod2 = $this->productRepository->get('grey_shorts'); - $prod3 = $this->productRepository->get('navy-striped-shoes'); + $prod3 = $this->productRepository->get('blue_briefs'); $response = $this->graphQlQuery($query); $this->assertEquals(3, $response['products']['total_count']); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php index 950b26e3b9f5..5c95e99cf4a2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogGraphQl/ProductSearchTest.php @@ -371,4 +371,90 @@ private function getProductSearchQueryWithMatchType( } QUERY; } + + #[ + DataFixture(CategoryFixture::class, as: 'category'), + DataFixture( + ProductFixture::class, + [ + 'price' => 0, + 'category_ids' => ['$category.id$'], + ], + 'product1' + ), + DataFixture( + ProductFixture::class, + [ + 'price' => 0.5, + 'category_ids' => ['$category.id$'], + ], + 'product2' + ), + DataFixture( + ProductFixture::class, + [ + 'price' => 1, + 'category_ids' => ['$category.id$'], + ], + 'product3' + ), + DataFixture( + ProductFixture::class, + [ + 'price' => 1.5, + 'category_ids' => ['$category.id$'], + ], + 'product4' + ), + ] + public function testProductSearchWithZeroPriceProducts() + { + + $response = $this->graphQlQuery($this->getSearchQueryBasedOnPriceRange(0, null)); + $this->assertEquals(4, $response['products']['totalResult']); + + $response = $this->graphQlQuery($this->getSearchQueryBasedOnPriceRange(0, 0)); + $this->assertEquals(1, $response['products']['totalResult']); + + $response = $this->graphQlQuery($this->getSearchQueryBasedOnPriceRange(0.5, null)); + $this->assertEquals(3, $response['products']['totalResult']); + + $response = $this->graphQlQuery($this->getSearchQueryBasedOnPriceRange(0, 1)); + $this->assertEquals(3, $response['products']['totalResult']); + } + + /** + * Prepare search query for products with price range + * + * @param float $priceFrom + * @param float|null $priceTo + * @return string + */ + private function getSearchQueryBasedOnPriceRange(float $priceFrom, null|float $priceTo): string + { + $priceToFilter = $priceTo !== null ? ',to:"' . $priceTo . '"' : ''; + return <<quoteFactory = $objectManager->get(QuoteFactory::class); $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->fixtures = DataFixtureStorageManager::getStorage(); + $this->quoteIdMaskedFactory = $objectManager->get(\Magento\Quote\Model\QuoteIdMaskFactory::class); + $this->quoteIdMaskedResource = $objectManager->get(\Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask::class); } protected function tearDown(): void @@ -101,6 +131,75 @@ public function testMergeGuestWithCustomerCart() self::assertEquals(1, $item2['quantity']); } + #[ + DataFixture(ProductFixture::class, ['sku' => 'simple1', 'price' => 10], as:'p1'), + DataFixture(ProductFixture::class, ['sku' => 'simple2', 'price' => 20], as:'p2'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$p1.sku$', 'price' => 10, 'price_type' => 0], as:'link1'), + DataFixture(BundleSelectionFixture::class, ['sku' => '$p2.sku$', 'price' => 25, 'price_type' => 0], as:'link2'), + DataFixture(BundleOptionFixture::class, ['title' => 'Checkbox Options', 'type' => 'checkbox', + 'required' => 1,'product_links' => ['$link1$', '$link2$']], 'opt1'), + DataFixture(BundleOptionFixture::class, ['title' => 'Checkbox Options', 'type' => 'checkbox', + 'required' => 1,'product_links' => ['$link1$', '$link2$']], 'opt2'), + DataFixture( + BundleProductFixture::class, + ['sku' => 'bundle-product-multiselect-checkbox-options','price' => 50,'price_type' => 1, + '_options' => ['$opt1$', '$opt2$']], + as:'bp1' + ), + DataFixture(Customer::class, ['email' => 'me@example.com'], as: 'customer'), + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], as: 'customerCart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$customerCart.id$', + 'product_id' => '$bp1.id$', + 'selections' => [['$p1.id$'], ['$p2.id$']], + 'qty' => 1 + ] + ), + DataFixture(GuestCartFixture::class, as: 'guestCart'), + DataFixture( + AddBundleProductToCart::class, + [ + 'cart_id' => '$guestCart.id$', + 'product_id' => '$bp1.id$', + 'selections' => [['$p1.id$'], ['$p2.id$']], + 'qty' => 2 + ] + ) + ] + public function testMergeGuestWithCustomerCartBundleProduct() + { + $guestCart = $this->fixtures->get('guestCart'); + $guestQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$guestCart->getId()); + + $customerCart = $this->fixtures->get('customerCart'); + $customerCartId = (int)$customerCart->getId(); + $customerQuoteMaskedId = $this->quoteIdToMaskedId->execute($customerCartId); + if (!$customerQuoteMaskedId) { + $quoteIdMask = $this->quoteIdMaskedFactory->create()->setQuoteId($customerCartId); + $this->quoteIdMaskedResource->save($quoteIdMask); + $customerQuoteMaskedId = $this->quoteIdToMaskedId->execute($customerCartId); + } + + $queryHeader = $this->getHeaderMap('me@example.com', 'password'); + $cartMergeQuery = $this->getCartMergeMutation($guestQuoteMaskedId, $customerQuoteMaskedId); + $mergeResponse = $this->graphQlMutation($cartMergeQuery, [], '', $queryHeader); + self::assertArrayHasKey('mergeCarts', $mergeResponse); + + $cartResponse = $mergeResponse['mergeCarts']; + self::assertArrayHasKey('items', $cartResponse); + self::assertCount(1, $cartResponse['items']); + $cartResponse = $this->graphQlMutation($this->getCartQuery($customerQuoteMaskedId), [], '', $queryHeader); + + self::assertArrayHasKey('cart', $cartResponse); + self::assertArrayHasKey('items', $cartResponse['cart']); + self::assertCount(1, $cartResponse['cart']['items']); + $item1 = $cartResponse['cart']['items'][0]; + self::assertArrayHasKey('quantity', $item1); + self::assertEquals(3, $item1['quantity']); + } + /** * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php * @magentoApiDataFixture Magento/Customer/_files/customer.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddProductWithOptionsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddProductWithOptionsToCartTest.php index 868232288ed5..ad13d7202000 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddProductWithOptionsToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/AddProductWithOptionsToCartTest.php @@ -105,6 +105,64 @@ public function testAddProductWithOptionsResponse() ); } + #[ + DataFixture(GuestCart::class, as: 'quote'), + DataFixture(QuoteIdMask::class, ['cart_id' => '$quote.id$'], 'quoteIdMask'), + DataFixture( + Product::class, + [ + 'sku' => 'simple1', + 'options' => [ + [ + 'title' => 'option1', + 'type' => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + ] + ] + ], + 'product1' + ), + DataFixture(Indexer::class, as: 'indexer') + ] + public function testAddSameProductWithDifferentOptionValues() + { + $uidEncoder = Bootstrap::getObjectManager()->create(Uid::class); + + $cartId = DataFixtureStorageManager::getStorage()->get('quoteIdMask')->getMaskedId(); + $product = DataFixtureStorageManager::getStorage()->get('product1'); + /* @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $sku = $product->getSku(); + $option = $product->getOptions(); + $optionUid = $uidEncoder->encode( + 'custom-option' . '/' . $option[0]->getData()['option_id'] + ); + + // Assert if product options for item added to the cart + // are present in mutation response after product with selected option was added + $mutation = $this->getAddProductWithDifferentOptionValuesMutation( + $cartId, + $sku, + $optionUid + ); + $response = $this->graphQlMutation($mutation); + + $this->assertArrayHasKey('items', $response['addProductsToCart']['cart']); + $this->assertCount(2, $response['addProductsToCart']['cart']['items']); + $this->assertArrayHasKey('customizable_options', $response['addProductsToCart']['cart']['items'][0]); + + $this->assertEquals( + $response['addProductsToCart']['cart']['items'], + $this->getExpectedResponseForDifferentOptionValues($optionUid, $sku) + ); + + // Assert if product options for item in the cart are present in the response + $query = $this->getCartQueryForDifferentOptionValues($cartId); + $response = $this->graphQlQuery($query); + $this->assertEquals( + $this->getExpectedResponseForDifferentOptionValues($optionUid, $sku), + $response['cart']['items'] + ); + } + #[ DataFixture(GuestCart::class, as: 'quote'), DataFixture(QuoteIdMask::class, ['cart_id' => '$quote.id$'], 'quoteIdMask'), @@ -268,7 +326,67 @@ private function getAddProductWithOptionMutation(string $cartId, string $sku, st } } } -} +} +QRY; + } + + /** + * Returns mutation for the test with different option values + * + * @param string $cartId + * @param string $sku + * @param string $optionUid + * @return string + */ + private function getAddProductWithDifferentOptionValuesMutation( + string $cartId, + string $sku, + string $optionUid + ): string { + return << [ + "quantity" => 1, + "product" => ["sku" => "{$sku}"], + "customizable_options" => [ + 0 => [ + "customizable_option_uid" => "{$optionUid}", + "label" => "option1", + "values" => [ + 0 => [ + "customizable_option_value_uid" => "{$optionUid}", + "value" => "value1" + ] + ] + ] + ] + ], + 1 => [ + "quantity" => 1, + "product" => ["sku" => "{$sku}"], + "customizable_options" => [ + 0 => [ + "customizable_option_uid" => "{$optionUid}", + "label" => "option1", + "values" => [ + 0 => [ + "customizable_option_value_uid" => "{$optionUid}", + "value" => "value2" + ] + ] + ] + ] + ], + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/UpdateCouponUsagesTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/UpdateCouponUsagesTest.php index 777959d2df8c..1f0c81f21412 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/UpdateCouponUsagesTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Coupon/UpdateCouponUsagesTest.php @@ -124,4 +124,44 @@ public function testCancelOrderBeforeUsageConsumerExecution(): void $this->cartManagement->placeOrder($cart->getId()); $consumer->process(1); } + + #[ + DataFixture(ProductFixture::class, as: 'p1'), + DataFixture( + SalesRuleFixture::class, + ['coupon_code' => 'once', 'uses_per_coupon' => 1, 'discount_amount' => 10] + ), + DataFixture(Customer::class, as: 'customer'), + + DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], 'cart1'), + DataFixture(AddProductToCart::class, ['cart_id' => '$cart1.id$', 'product_id' => '$p1.id$']), + DataFixture(SetBillingAddress::class, ['cart_id' => '$cart1.id$']), + DataFixture(SetShippingAddress::class, ['cart_id' => '$cart1.id$']), + DataFixture(SetDeliveryMethod::class, ['cart_id' => '$cart1.id$']), + DataFixture(SetPaymentMethod::class, ['cart_id' => '$cart1.id$']), + + DataFixture(GuestCart::class, as: 'cart2'), + DataFixture(AddProductToCart::class, ['cart_id' => '$cart2.id$', 'product_id' => '$p1.id$']), + DataFixture(SetBillingAddress::class, ['cart_id' => '$cart2.id$']), + DataFixture(SetShippingAddress::class, ['cart_id' => '$cart2.id$']), + DataFixture(SetDeliveryMethod::class, ['cart_id' => '$cart2.id$']), + DataFixture(SetPaymentMethod::class, ['cart_id' => '$cart2.id$']), + ] + public function testCancelOrderBeforeConsumerAndRuleTimesUsed(): void + { + $cart = $this->fixtures->get('cart1'); + $this->couponManagement->set($cart->getId(), 'once'); + $orderId = $this->cartManagement->placeOrder($cart->getId()); + $this->orderManagement->cancel($orderId); + $consumer = $this->consumerFactory->get('sales.rule.update.coupon.usage'); + $consumer->process(2); + + $cart = $this->fixtures->get('cart2'); + $customer = $this->fixtures->get('customer'); + $this->cartManagement->assignCustomer($cart->getId(), $customer->getId(), 1); + $cart = $this->cartRepository->get($cart->getId()); + $this->couponManagement->set($cart->getId(), 'once'); + $this->cartManagement->placeOrder($cart->getId()); + $consumer->process(1); + } }