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');