diff --git a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php index 167fcb1569cbf..58ce33305da85 100644 --- a/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php +++ b/app/code/Magento/Braintree/Gateway/Validator/ErrorCodeProvider.php @@ -38,6 +38,14 @@ public function getErrorCodes($response): array $result[] = $error->code; } + if (isset($response->transaction) && $response->transaction->status === 'gateway_rejected') { + $result[] = $response->transaction->gatewayRejectionReason; + } + + if (isset($response->transaction) && $response->transaction->status === 'processor_declined') { + $result[] = $response->transaction->processorResponseCode; + } + return $result; } } diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php new file mode 100644 index 0000000000000..cddb4852da0e3 --- /dev/null +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/ErrorCodeProviderTest.php @@ -0,0 +1,94 @@ + ['errors' => $errors], + 'transaction' => $transaction, + ] + ); + $this->model = new ErrorCodeProvider(); + $actual = $this->model->getErrorCodes($response); + + $this->assertSame($expectedResult, $actual); + } + + /** + * Gets list of errors variations. + * + * @return array + */ + public function getErrorCodeDataProvider(): array + { + return [ + [ + 'errors' => [ + ['code' => 91734], + ['code' => 91504] + ], + 'transaction' => [ + 'status' => 'success', + ], + 'expectedResult' => ['91734', '91504'] + ], + [ + 'errors' => [], + 'transaction' => [ + 'status' => 'processor_declined', + 'processorResponseCode' => '1000' + ], + 'expectedResult' => ['1000'] + ], + [ + 'errors' => [], + 'transaction' => [ + 'status' => 'processor_declined', + 'processorResponseCode' => '1000' + ], + 'expectedResult' => ['1000'] + ], + [ + 'errors' => [ + ['code' => 91734], + ['code' => 91504] + ], + 'transaction' => [ + 'status' => 'processor_declined', + 'processorResponseCode' => '1000' + ], + 'expectedResult' => ['91734', '91504', '1000'] + ], + ]; + } +} diff --git a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php index 4741a3ea38c6f..d966e4e3f10ec 100644 --- a/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php +++ b/app/code/Magento/Braintree/Test/Unit/Gateway/Validator/GeneralResponseValidatorTest.php @@ -14,6 +14,9 @@ use Magento\Payment\Gateway\Validator\ResultInterfaceFactory; use PHPUnit_Framework_MockObject_MockObject as MockObject; +/** + * Class GeneralResponseValidatorTest + */ class GeneralResponseValidatorTest extends \PHPUnit\Framework\TestCase { /** @@ -61,11 +64,13 @@ public function testValidate(array $validationSubject, bool $isValid, $messages, $result = new Result($isValid, $messages); $this->resultInterfaceFactory->method('create') - ->with([ - 'isValid' => $isValid, - 'failsDescription' => $messages, - 'errorCodes' => $errorCodes - ]) + ->with( + [ + 'isValid' => $isValid, + 'failsDescription' => $messages, + 'errorCodes' => $errorCodes + ] + ) ->willReturn($result); $actual = $this->responseValidator->validate($validationSubject); @@ -82,9 +87,11 @@ public function dataProviderTestValidate() { $successTransaction = new \stdClass(); $successTransaction->success = true; + $successTransaction->status = 'authorized'; $failureTransaction = new \stdClass(); $failureTransaction->success = false; + $failureTransaction->status = 'declined'; $failureTransaction->message = 'Transaction was failed.'; $errors = [ @@ -93,10 +100,10 @@ public function dataProviderTestValidate() 'code' => 81804, 'attribute' => 'base', 'message' => 'Cannot process transaction.' - ] + ], ] ]; - $errorTransaction = new Error(['errors' => $errors]); + $errorTransaction = new Error(['errors' => $errors, 'transaction' => ['status' => 'declined']]); return [ [ diff --git a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml index 81da0a252e567..7155264b4e6ad 100644 --- a/app/code/Magento/Braintree/etc/braintree_error_mapping.xml +++ b/app/code/Magento/Braintree/etc/braintree_error_mapping.xml @@ -20,6 +20,7 @@ Credit card number must be 12-19 digits. Cardholder name is too long. CVV verification failed. + CVV verification failed. Postal code verification failed. Credit card number is prohibited. Addresses must have at least one field filled in. diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/cc-form.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/cc-form.js index dc816c035a23d..868fe174ae482 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/cc-form.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/cc-form.js @@ -12,7 +12,7 @@ define([ 'Magento_Ui/js/model/messageList', 'mage/translate', 'Magento_Checkout/js/model/full-screen-loader', - 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/action/set-payment-information-extended', 'Magento_Checkout/js/model/payment/additional-validators', 'Magento_Braintree/js/view/payment/validator-handler' ], function ( @@ -22,7 +22,7 @@ define([ messageList, $t, fullScreenLoader, - setPaymentInformationAction, + setPaymentInformationExtended, additionalValidators, validatorManager ) { @@ -51,9 +51,10 @@ define([ if (additionalValidators.validate()) { fullScreenLoader.startLoader(); $.when( - setPaymentInformationAction( + setPaymentInformationExtended( this.messageContainer, - this.getData() + this.getData(), + true ) ).done(this.done.bind(this)) .fail(this.fail.bind(this)); diff --git a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js index 0a9ec4fb6c6ee..b3837103148cc 100644 --- a/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js +++ b/app/code/Magento/Braintree/view/frontend/web/js/view/payment/method-renderer/multishipping/paypal.js @@ -8,14 +8,14 @@ define([ 'jquery', 'underscore', 'Magento_Braintree/js/view/payment/method-renderer/paypal', - 'Magento_Checkout/js/action/set-payment-information', + 'Magento_Checkout/js/action/set-payment-information-extended', 'Magento_Checkout/js/model/payment/additional-validators', 'Magento_Checkout/js/model/full-screen-loader' ], function ( $, _, Component, - setPaymentInformationAction, + setPaymentInformationExtended, additionalValidators, fullScreenLoader ) { @@ -131,9 +131,10 @@ define([ placeOrder: function () { fullScreenLoader.startLoader(); $.when( - setPaymentInformationAction( + setPaymentInformationExtended( this.messageContainer, - this.getData() + this.getData(), + true ) ).done(this.done.bind(this)) .fail(this.fail.bind(this)); diff --git a/app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php b/app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php new file mode 100644 index 0000000000000..dd5689a396ebd --- /dev/null +++ b/app/code/Magento/CatalogInventory/Observer/ParentItemProcessorInterface.php @@ -0,0 +1,24 @@ +stockConfiguration = $stockConfiguration; $this->stockRegistry = $stockRegistry; $this->stockItemValidator = $stockItemValidator ?: ObjectManager::getInstance()->get(StockItemValidator::class); + $this->parentItemProcessorPool = $parentItemProcessorPool; } /** @@ -96,10 +106,15 @@ public function __construct( * * @param EventObserver $observer * @return void + * @throws LocalizedException + * @throws NoSuchEntityException */ public function execute(EventObserver $observer) { + /** @var Product $product */ $product = $observer->getEvent()->getProduct(); + + /** @var Item $stockItem */ $stockItem = $this->getStockItemToBeUpdated($product); if ($product->getStockData() !== null) { @@ -108,6 +123,7 @@ public function execute(EventObserver $observer) } $this->stockItemValidator->validate($product, $stockItem); $this->stockRegistry->updateStockItemBySku($product->getSku(), $stockItem); + $this->processParents($product); } /** @@ -156,4 +172,17 @@ private function getStockData(Product $product) } return $stockData; } + + /** + * Process stock data for parent products + * + * @param Product $product + * @return void + */ + private function processParents(Product $product) + { + foreach ($this->parentItemProcessorPool as $processor) { + $processor->process($product); + } + } } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js new file mode 100644 index 0000000000000..4085da82f4151 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @api + */ +define([ + 'Magento_Checkout/js/model/quote', + 'Magento_Checkout/js/model/url-builder', + 'mage/storage', + 'Magento_Checkout/js/model/error-processor', + 'Magento_Customer/js/model/customer', + 'Magento_Checkout/js/action/get-totals', + 'Magento_Checkout/js/model/full-screen-loader' +], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader) { + 'use strict'; + + return function (messageContainer, paymentData, skipBilling) { + var serviceUrl, + payload; + + skipBilling = skipBilling || false; + payload = { + cartId: quote.getQuoteId(), + paymentMethod: paymentData + }; + + /** + * Checkout for guest and registered customer. + */ + if (!customer.isLoggedIn()) { + serviceUrl = urlBuilder.createUrl('/guest-carts/:cartId/set-payment-information', { + cartId: quote.getQuoteId() + }); + payload.email = quote.guestEmail; + } else { + serviceUrl = urlBuilder.createUrl('/carts/mine/set-payment-information', {}); + } + + if (skipBilling === false) { + payload.billingAddress = quote.billingAddress(); + } + + fullScreenLoader.startLoader(); + + return storage.post( + serviceUrl, JSON.stringify(payload) + ).fail( + function (response) { + errorProcessor.process(response, messageContainer); + } + ).always( + function () { + fullScreenLoader.stopLoader(); + } + ); + }; +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js index 997b60503a2b3..d5261c976a725 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information.js @@ -7,54 +7,13 @@ * @api */ define([ - 'Magento_Checkout/js/model/quote', - 'Magento_Checkout/js/model/url-builder', - 'mage/storage', - 'Magento_Checkout/js/model/error-processor', - 'Magento_Customer/js/model/customer', - 'Magento_Checkout/js/action/get-totals', - 'Magento_Checkout/js/model/full-screen-loader' -], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader) { + 'Magento_Checkout/js/action/set-payment-information-extended' + +], function (setPaymentInformationExtended) { 'use strict'; return function (messageContainer, paymentData) { - var serviceUrl, - payload; - - /** - * Checkout for guest and registered customer. - */ - if (!customer.isLoggedIn()) { - serviceUrl = urlBuilder.createUrl('/guest-carts/:cartId/set-payment-information', { - cartId: quote.getQuoteId() - }); - payload = { - cartId: quote.getQuoteId(), - email: quote.guestEmail, - paymentMethod: paymentData, - billingAddress: quote.billingAddress() - }; - } else { - serviceUrl = urlBuilder.createUrl('/carts/mine/set-payment-information', {}); - payload = { - cartId: quote.getQuoteId(), - paymentMethod: paymentData, - billingAddress: quote.billingAddress() - }; - } - - fullScreenLoader.startLoader(); - return storage.post( - serviceUrl, JSON.stringify(payload) - ).fail( - function (response) { - errorProcessor.process(response, messageContainer); - } - ).always( - function () { - fullScreenLoader.stopLoader(); - } - ); + return setPaymentInformationExtended(messageContainer, paymentData, false); }; }); diff --git a/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php new file mode 100644 index 0000000000000..f1567f2b196de --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Inventory/ParentItemProcessor.php @@ -0,0 +1,129 @@ +configurableType = $configurableType; + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + $this->stockItemRepository = $stockItemRepository; + $this->stockConfiguration = $stockConfiguration; + } + + /** + * Process parent products + * + * @param Product $product + * @return void + */ + public function process(Product $product) + { + $parentIds = $this->configurableType->getParentIdsByChild($product->getId()); + foreach ($parentIds as $productId) { + $this->processStockForParent((int)$productId); + } + } + + /** + * Change stock item for parent product depending on children stock items + * + * @param int $productId + * @return void + */ + private function processStockForParent(int $productId) + { + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); + + $criteria->setProductsFilter($productId); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + if (empty($allItems)) { + return; + } + $parentStockItem = array_shift($allItems); + + $childrenIds = $this->configurableType->getChildrenIds($productId); + $criteria->setProductsFilter($childrenIds); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + + $childrenIsInStock = false; + + foreach ($allItems as $childItem) { + if ($childItem->getIsInStock() === true) { + $childrenIsInStock = true; + break; + } + } + + if ($this->isNeedToUpdateParent($parentStockItem, $childrenIsInStock)) { + $parentStockItem->setIsInStock($childrenIsInStock); + $parentStockItem->setStockStatusChangedAuto(1); + $this->stockItemRepository->save($parentStockItem); + } + } + + /** + * Check is parent item should be updated + * + * @param StockItemInterface $parentStockItem + * @param bool $childrenIsInStock + * @return bool + */ + private function isNeedToUpdateParent( + StockItemInterface $parentStockItem, + bool $childrenIsInStock + ): bool { + return $parentStockItem->getIsInStock() !== $childrenIsInStock && + ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php index 2f07f8b90ce7e..890564fdb303c 100644 --- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php +++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php @@ -132,7 +132,7 @@ public function addChild($sku, $childSku) throw new StateException(__("The parent product doesn't have configurable product options.")); } - $attributeIds = []; + $attributeData = []; foreach ($configurableProductOptions as $configurableProductOption) { $attributeCode = $configurableProductOption->getProductAttribute()->getAttributeCode(); if (!$child->getData($attributeCode)) { @@ -143,9 +143,11 @@ public function addChild($sku, $childSku) ) ); } - $attributeIds[] = $configurableProductOption->getAttributeId(); + $attributeData[$configurableProductOption->getAttributeId()] = [ + 'position' => $configurableProductOption->getPosition() + ]; } - $configurableOptionData = $this->getConfigurableAttributesData($attributeIds); + $configurableOptionData = $this->getConfigurableAttributesData($attributeData); /** @var \Magento\ConfigurableProduct\Helper\Product\Options\Factory $optionFactory */ $optionFactory = $this->getOptionsFactory(); @@ -211,16 +213,16 @@ private function getOptionsFactory() /** * Get Configurable Attribute Data * - * @param int[] $attributeIds + * @param int[] $attributeData * @return array */ - private function getConfigurableAttributesData($attributeIds) + private function getConfigurableAttributesData($attributeData) { $configurableAttributesData = []; $attributeValues = []; $attributes = $this->attributeFactory->create() ->getCollection() - ->addFieldToFilter('attribute_id', $attributeIds) + ->addFieldToFilter('attribute_id', array_keys($attributeData)) ->getItems(); foreach ($attributes as $attribute) { foreach ($attribute->getOptions() as $option) { @@ -237,6 +239,7 @@ private function getConfigurableAttributesData($attributeIds) 'attribute_id' => $attribute->getId(), 'code' => $attribute->getAttributeCode(), 'label' => $attribute->getStoreLabel(), + 'position' => $attributeData[$attribute->getId()]['position'], 'values' => $attributeValues, ]; } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php index ad2fcd1e59360..c385934352ab8 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/LinkManagementTest.php @@ -149,16 +149,19 @@ public function testAddChild() ->disableOriginalConstructor() ->setMethods(['getId', 'getData']) ->getMock(); - $extensionAttributesMock = $this->getMockBuilder(\Magento\Catalog\Api\Data\ProductExtension::class) ->disableOriginalConstructor() - ->setMethods([ - 'getConfigurableProductOptions', 'setConfigurableProductOptions', 'setConfigurableProductLinks' - ]) + ->setMethods( + [ + 'getConfigurableProductOptions', + 'setConfigurableProductOptions', + 'setConfigurableProductLinks' + ] + ) ->getMock(); $optionMock = $this->getMockBuilder(\Magento\ConfigurableProduct\Api\Data\Option::class) ->disableOriginalConstructor() - ->setMethods(['getProductAttribute', 'getAttributeId']) + ->setMethods(['getProductAttribute', 'getPosition', 'getAttributeId']) ->getMock(); $productAttributeMock = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class) ->disableOriginalConstructor() @@ -189,7 +192,6 @@ public function testAddChild() ->disableOriginalConstructor() ->setMethods(['getValue', 'getLabel']) ->getMock(); - $attributeCollectionMock = $this->getMockBuilder( \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection::class ) @@ -216,20 +218,18 @@ public function testAddChild() $productAttributeMock->expects($this->any())->method('getAttributeCode')->willReturn('color'); $simple->expects($this->any())->method('getData')->willReturn('color'); $optionMock->expects($this->any())->method('getAttributeId')->willReturn('1'); + $optionMock->expects($this->any())->method('getPosition')->willReturn('0'); $optionsFactoryMock->expects($this->any())->method('create')->willReturn([$optionMock]); $attributeFactoryMock->expects($this->any())->method('create')->willReturn($attributeMock); $attributeMock->expects($this->any())->method('getCollection')->willReturn($attributeCollectionMock); $attributeCollectionMock->expects($this->any())->method('addFieldToFilter')->willReturnSelf(); $attributeCollectionMock->expects($this->any())->method('getItems')->willReturn([$attributeMock]); - + $attributeMock->expects($this->any())->method('getId')->willReturn(1); $attributeMock->expects($this->any())->method('getOptions')->willReturn([$attributeOptionMock]); - $extensionAttributesMock->expects($this->any())->method('setConfigurableProductOptions'); $extensionAttributesMock->expects($this->any())->method('setConfigurableProductLinks'); - $this->productRepository->expects($this->once())->method('save'); - $this->assertTrue(true, $this->object->addChild($productSku, $childSku)); } diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index c3ffe988b00d7..8cec84abc4fea 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -255,4 +255,11 @@ + + + + Magento\ConfigurableProduct\Model\Inventory\ParentItemProcessor + + + diff --git a/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js b/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js index 1e352e4297131..089dabac00b0f 100644 --- a/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js +++ b/app/code/Magento/Payment/view/frontend/web/js/view/payment/iframe.js @@ -125,12 +125,7 @@ define([ this.isPlaceOrderActionAllowed(false); $.when( - setPaymentInformationAction( - this.messageContainer, - { - method: this.getCode() - } - ) + this.setPaymentInformation() ).done( this.done.bind(this) ).fail( @@ -145,6 +140,18 @@ define([ } }, + /** + * {Function} + */ + setPaymentInformation: function () { + setPaymentInformationAction( + this.messageContainer, + { + method: this.getCode() + } + ); + }, + /** * {Function} */ diff --git a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php index 45eee0a4001d1..1ac1547eb8d0a 100644 --- a/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php +++ b/app/code/Magento/Search/Model/ResourceModel/SynonymReader.php @@ -106,7 +106,7 @@ private function queryByPhrase($phrase) */ private function escapePhrase(string $phrase): string { - return preg_replace('/@+|[@+-]+$/', '', $phrase); + return preg_replace('/@+|[@+-]+$|[<>]/', '', $phrase); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php index d899839d43d40..53c1bf08bb796 100644 --- a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/LinkManagementTest.php @@ -6,11 +6,19 @@ */ namespace Magento\ConfigurableProduct\Api; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Eav\Model\AttributeRepository; +use Magento\Eav\Model\Entity\Attribute\Option; +use Magento\Framework\Webapi\Rest\Request; +use Magento\TestFramework\TestCase\WebapiAbstract; -class LinkManagementTest extends \Magento\TestFramework\TestCase\WebapiAbstract +/** + * Class LinkManagementTest for testing ConfigurableProduct to SimpleProduct link functionality + */ +class LinkManagementTest extends WebapiAbstract { const SERVICE_NAME = 'configurableProductLinkManagementV1'; + const OPTION_SERVICE_NAME = 'configurableProductOptionRepositoryV1'; const SERVICE_VERSION = 'V1'; const RESOURCE_PATH = '/V1/configurable-products'; @@ -85,9 +93,27 @@ public function testAddChildFullRestCreation() $this->createConfigurableProduct($productSku); $attribute = $this->attributeRepository->get('catalog_product', 'test_configurable'); - $attributeValue = $attribute->getOptions()[1]->getValue(); - $this->addOptionToConfigurableProduct($productSku, $attribute->getAttributeId(), $attributeValue); - $this->createSimpleProduct($childSku, $attributeValue); + + $this->addOptionToConfigurableProduct( + $productSku, + $attribute->getAttributeId(), + [ + [ + 'value_index' => $attribute->getOptions()[1]->getValue() + ] + ] + ); + + $this->createSimpleProduct( + $childSku, + [ + [ + 'attribute_code' => 'test_configurable', + 'value' => $attribute->getOptions()[1]->getValue() + ] + ] + ); + $res = $this->addChild($productSku, $childSku); $this->assertTrue($res); @@ -103,10 +129,129 @@ public function testAddChildFullRestCreation() $this->assertTrue($added); // clean up products + + $this->deleteProduct($productSku); + $this->deleteProduct($childSku); + } + + /** + * Test if configurable option attribute positions are being preserved after simple products were assigned to a + * configurable product. + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test.php + */ + public function testConfigurableOptionPositionPreservation() + { + $productSku = 'configurable-product-sku'; + $childProductSkus = [ + 'simple-product-sku-1', + 'simple-product-sku-2' + ]; + $attributesToAdd = [ + 'custom_attr_1', + 'custom_attr_2', + ]; + + $this->createConfigurableProduct($productSku); + + $position = 0; + $attributeOptions = []; + foreach ($attributesToAdd as $attributeToAdd) { + /** @var Attribute $attribute */ + $attribute = $this->attributeRepository->get('catalog_product', $attributeToAdd); + + /** @var Option $options[] */ + $options = $attribute->getOptions(); + array_shift($options); + + $attributeOptions[$attributeToAdd] = $options; + + $valueIndexesData = []; + foreach ($options as $option) { + $valueIndexesData []['value_index']= $option->getValue(); + } + $this->addOptionToConfigurableProduct( + $productSku, + $attribute->getAttributeId(), + $valueIndexesData, + $position + ); + $position++; + } + + $this->assertArrayHasKey($attributesToAdd[0], $attributeOptions); + $this->assertArrayHasKey($attributesToAdd[1], $attributeOptions); + $this->assertCount(4, $attributeOptions[$attributesToAdd[0]]); + $this->assertCount(4, $attributeOptions[$attributesToAdd[1]]); + + $attributesBeforeAssign = $this->getConfigurableAttribute($productSku); + + $simpleProdsAttributeData = []; + foreach ($attributeOptions as $attributeCode => $options) { + $simpleProdsAttributeData [0][] = [ + 'attribute_code' => $attributeCode, + 'value' => $options[0]->getValue(), + ]; + $simpleProdsAttributeData [0][] = [ + 'attribute_code' => $attributeCode, + 'value' => $options[1]->getValue(), + ]; + $simpleProdsAttributeData [1][] = [ + 'attribute_code' => $attributeCode, + 'value' => $options[2]->getValue(), + ]; + $simpleProdsAttributeData [1][] = [ + 'attribute_code' => $attributeCode, + 'value' => $options[3]->getValue(), + ]; + } + + foreach ($childProductSkus as $childNum => $childSku) { + $this->createSimpleProduct($childSku, $simpleProdsAttributeData[$childNum]); + $res = $this->addChild($productSku, $childSku); + $this->assertTrue($res); + } + + $childProductsDiff = array_diff( + $childProductSkus, + array_column( + $this->getChildren($productSku), + 'sku' + ) + ); + $this->assertCount(0, $childProductsDiff, 'Added child product count mismatch expected result'); + + $attributesAfterAssign = $this->getConfigurableAttribute($productSku); + + $this->assertEquals( + $attributesBeforeAssign[0]['position'], + $attributesAfterAssign[0]['position'], + 'Product 1 attribute option position mismatch' + ); + $this->assertEquals( + $attributesBeforeAssign[1]['position'], + $attributesAfterAssign[1]['position'], + 'Product 2 attribute option position mismatch' + ); + + foreach ($childProductSkus as $childSku) { + $this->deleteProduct($childSku); + } + $this->deleteProduct($productSku); + } + + /** + * Delete product by SKU + * + * @param string $sku + * @return bool + */ + private function deleteProduct(string $sku): bool + { $serviceInfo = [ 'rest' => [ - 'resourcePath' => '/V1/products/' . $productSku, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE + 'resourcePath' => '/V1/products/' . $sku, + 'httpMethod' => Request::HTTP_METHOD_DELETE ], 'soap' => [ 'service' => 'catalogProductRepositoryV1', @@ -114,19 +259,29 @@ public function testAddChildFullRestCreation() 'operation' => 'catalogProductRepositoryV1DeleteById', ], ]; - $this->_webApiCall($serviceInfo, ['sku' => $productSku]); + return $this->_webApiCall($serviceInfo, ['sku' => $sku]); + } + + /** + * Get configurable product attributes + * + * @param string $productSku + * @return array + */ + protected function getConfigurableAttribute(string $productSku): array + { $serviceInfo = [ 'rest' => [ - 'resourcePath' => '/V1/products/' . $childSku, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE + 'resourcePath' => self::RESOURCE_PATH . '/' . $productSku . '/options/all', + 'httpMethod' => Request::HTTP_METHOD_GET ], 'soap' => [ - 'service' => 'catalogProductRepositoryV1', + 'service' => self::OPTION_SERVICE_NAME, 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => 'catalogProductRepositoryV1DeleteById', - ], + 'operation' => self::OPTION_SERVICE_NAME . 'GetList' + ] ]; - $this->_webApiCall($serviceInfo, ['sku' => $childSku]); + return $this->_webApiCall($serviceInfo, ['sku' => $productSku]); } private function addChild($productSku, $childSku) @@ -134,7 +289,7 @@ private function addChild($productSku, $childSku) $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $productSku . '/child', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + 'httpMethod' => Request::HTTP_METHOD_POST ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -159,7 +314,7 @@ protected function createConfigurableProduct($productSku) $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST + 'httpMethod' => Request::HTTP_METHOD_POST ], 'soap' => [ 'service' => 'catalogProductRepositoryV1', @@ -170,24 +325,22 @@ protected function createConfigurableProduct($productSku) return $this->_webApiCall($serviceInfo, $requestData); } - protected function addOptionToConfigurableProduct($productSku, $attributeId, $attributeValue) + protected function addOptionToConfigurableProduct($productSku, $attributeId, $attributeValues, $position = 0) { $requestData = [ 'sku' => $productSku, 'option' => [ 'attribute_id' => $attributeId, 'label' => 'test_configurable', - 'position' => 0, + 'position' => $position, 'is_use_default' => true, - 'values' => [ - ['value_index' => $attributeValue], - ] + 'values' => $attributeValues ] ]; $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/configurable-products/'. $productSku .'/options', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => 'configurableProductOptionRepositoryV1', @@ -198,7 +351,7 @@ protected function addOptionToConfigurableProduct($productSku, $attributeId, $at return $this->_webApiCall($serviceInfo, $requestData); } - protected function createSimpleProduct($sku, $attributeValue) + protected function createSimpleProduct($sku, $customAttributes) { $requestData = [ 'product' => [ @@ -209,15 +362,13 @@ protected function createSimpleProduct($sku, $attributeValue) 'price' => 3.62, 'status' => 1, 'visibility' => 4, - 'custom_attributes' => [ - ['attribute_code' => 'test_configurable', 'value' => $attributeValue], - ] + 'custom_attributes' => $customAttributes ] ]; $serviceInfo = [ 'rest' => [ 'resourcePath' => '/V1/products', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => 'catalogProductRepositoryV1', @@ -244,7 +395,7 @@ protected function removeChild($productSku, $childSku) $serviceInfo = [ 'rest' => [ 'resourcePath' => sprintf($resourcePath, $productSku, $childSku), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE + 'httpMethod' => Request::HTTP_METHOD_DELETE ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -265,7 +416,7 @@ protected function getChildren($productSku) $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $productSku . '/children', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET + 'httpMethod' => Request::HTTP_METHOD_GET ], 'soap' => [ 'service' => self::SERVICE_NAME, diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php new file mode 100644 index 0000000000000..a50a7b096fe13 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/Observer/SaveInventoryDataObserverTest.php @@ -0,0 +1,86 @@ +productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); + $this->stockItemRepository = Bootstrap::getObjectManager() + ->get(StockItemRepositoryInterface::class); + } + + /** + * Check that parent product will be out of stock + * + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDataFixture Magento/CatalogInventory/_files/configurable_options_with_low_stock.php + * @throws NoSuchEntityException + * @throws InputException + * @throws StateException + * @throws CouldNotSaveException + * @return void + */ + public function testAutoChangingIsInStockForParent() + { + /** @var ProductInterface $product */ + $product = $this->productRepository->get('simple_10'); + + /** @var ProductExtensionInterface $attributes*/ + $attributes = $product->getExtensionAttributes(); + + /** @var StockItemInterface $stockItem */ + $stockItem = $attributes->getStockItem(); + $stockItem->setQty(0); + $stockItem->setIsInStock(false); + $attributes->setStockItem($stockItem); + $product->setExtensionAttributes($attributes); + $this->productRepository->save($product); + + /** @var ProductInterface $product */ + $parentProduct = $this->productRepository->get('configurable'); + + $parentProductStockItem = $this->stockItemRepository->get( + $parentProduct->getExtensionAttributes()->getStockItem()->getItemId() + ); + $this->assertSame(false, $parentProductStockItem->getIsInStock()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php b/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php new file mode 100644 index 0000000000000..f6013b2e4b939 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogInventory/_files/configurable_options_with_low_stock.php @@ -0,0 +1,38 @@ +create(ProductRepositoryInterface::class); + +/** @var StockItemRepositoryInterface $stockItemRepository */ +$stockItemRepository = $objectManager->get(StockItemRepositoryInterface::class); + +/** @var ProductInterface $product */ +$product = $productRepository->get('simple_10'); + +/** @var StockItemInterface $stockItem */ +$stockItem = $product->getExtensionAttributes()->getStockItem(); +$stockItem->setIsInStock(true) + ->setQty(1); +$stockItemRepository->save($stockItem); + +/** @var ProductInterface $product */ +$product = $productRepository->get('simple_20'); + +/** @var StockItemInterface $stockItem */ +$stockItem = $product->getExtensionAttributes()->getStockItem(); +$stockItem->setIsInStock(false) + ->setQty(0); +$stockItemRepository->save($stockItem); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Api/PaymentInformationManagementTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Api/PaymentInformationManagementTest.php index f6338c1ee1664..1266dc7bb8843 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Api/PaymentInformationManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Api/PaymentInformationManagementTest.php @@ -96,7 +96,7 @@ public function testSavePaymentInformationAndPlaceOrderWithErrors( array_push($errors['errors'], ['code' => $testErrorCode]); } - $response = new Error(['errors' => $errors]); + $response = new Error(['errors' => $errors, 'transaction' => ['status' => 'declined']]); $this->client->method('placeRequest') ->willReturn(['object' => $response]); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test.php new file mode 100644 index 0000000000000..3f907c0ad0cb6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test.php @@ -0,0 +1,101 @@ +reinitialize(); + +/** @var $eavConfig Config */ +$eavConfig = Bootstrap::getObjectManager()->get(Config::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +$attributesData = [ + [ + 'code' => 'custom_attr_1', + 'label' => 'custom_attr_1', + ], + [ + 'code' => 'custom_attr_2', + 'label' => 'custom_attr_2', + ], +]; + +foreach ($attributesData as $attributeData) { + $attribute = $eavConfig->getAttribute('catalog_product', $attributeData['code']); + + $eavConfig->clear(); + + + if (!$attribute->getId()) { + + /** @var $attribute Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => $attributeData['code'], + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => $attributeData['label'], + 'backend_type' => 'int', + 'option' => [ + 'value' => [ + 'option_0' => [ + $attributeData['label'] . ' Option 1' + ], + 'option_1' => [ + $attributeData['label'] . ' Option 2' + ], + 'option_2' => [ + $attributeData['label'] . ' Option 3' + ], + 'option_3' => [ + $attributeData['label'] . ' Option 4' + ] + ], + 'order' => [ + 'option_0' => 1, + 'option_1' => 2, + 'option_2' => 3, + 'option_3' => 4 + ], + ], + ] + ); + + $attributeRepository->save($attribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); + } +} + +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test_rollback.php new file mode 100644 index 0000000000000..c947eaafda393 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attributes_for_position_test_rollback.php @@ -0,0 +1,41 @@ +get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = Bootstrap::getObjectManager() + ->get(Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +$attributesToDelete = [ + 'custom_attr_1', + 'custom_attr_2', +]; + +foreach ($attributesToDelete as $attributeToDelete) { + $eavConfig = Bootstrap::getObjectManager()->get(Config::class); + $attribute = $eavConfig->getAttribute('catalog_product', $attributeToDelete); + if ($attribute instanceof AbstractAttribute + && $attribute->getId() + ) { + $attribute->delete(); + } + $eavConfig->clear(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php index 2d0020ba22680..90540a1d637d5 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymReaderTest.php @@ -25,7 +25,7 @@ protected function setUp() /** * @return array */ - public static function loadByPhraseDataProvider() + public function loadByPhraseDataProvider(): array { return [ [ @@ -64,6 +64,15 @@ public static function loadByPhraseDataProvider() [ 'query_value+@', [] ], + [ + '<', [] + ], + [ + '>', [] + ], + [ + '', [['synonyms' => 'british,english', 'store_id' => 1, 'website_id' => 0]] + ], ]; } @@ -72,7 +81,7 @@ public static function loadByPhraseDataProvider() * @param array $expectedResult * @dataProvider loadByPhraseDataProvider */ - public function testLoadByPhrase($phrase, $expectedResult) + public function testLoadByPhrase(string $phrase, array $expectedResult) { $data = $this->model->loadByPhrase($phrase)->getData(); diff --git a/lib/web/mage/common.js b/lib/web/mage/common.js index 01f696ec1b7fc..53f5b74872192 100644 --- a/lib/web/mage/common.js +++ b/lib/web/mage/common.js @@ -18,10 +18,21 @@ define([ 'form', function (e) { var formKeyElement, + existingFormKeyElement, + isKeyPresentInForm, form = $(e.target), formKey = $('input[name="form_key"]').val(); - if (formKey && !form.find('input[name="form_key"]').length && form[0].method !== 'get') { + existingFormKeyElement = form.find('input[name="form_key"]'); + isKeyPresentInForm = existingFormKeyElement.length; + + /* Verifies that existing auto-added form key is a direct form child element, + protection from a case when one form contains another form. */ + if (isKeyPresentInForm && existingFormKeyElement.attr('auto-added-form-key') === '1') { + isKeyPresentInForm = form.find('> input[name="form_key"]').length; + } + + if (formKey && !isKeyPresentInForm && form[0].method !== 'get') { formKeyElement = document.createElement('input'); formKeyElement.setAttribute('type', 'hidden'); formKeyElement.setAttribute('name', 'form_key');