- = /* @noEscape */ $secureRenderer->renderStyleAsTag(
- "width:" . $block->escapeHtmlAttr($rating) . "%",
- 'div.rating-summary div.rating-result>span:first-child'
+ = /* @noEscape */
+ $secureRenderer->renderStyleAsTag(
+ 'width:' . $block->escapeHtmlAttr($rating) . '%',
+ '#rating-result_' . $block->getProduct()->getId() . ' span'
) ?>
diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
index 288f648b50ee9..e6eb8efa294ef 100644
--- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls
+++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls
@@ -69,7 +69,7 @@ type OrderAddress @doc(description: "Contains detailed information about an orde
country_code: CountryCodeEnum @doc(description: "The customer's country.")
street: [String!]! @doc(description: "An array of strings that define the street number and name.")
company: String @doc(description: "The customer's company.")
- telephone: String! @doc(description: "The telephone number.")
+ telephone: String @doc(description: "The telephone number.")
fax: String @doc(description: "The fax number.")
postcode: String @doc(description: "The customer's ZIP or postal code.")
city: String! @doc(description: "The city or town.")
diff --git a/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php
index 025f2c7215964..e3ad412a8e783 100644
--- a/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php
+++ b/app/code/Magento/SalesRule/Helper/CartFixedDiscount.php
@@ -51,6 +51,8 @@ public function calculateShippingAmountWhenAppliedToShipping(
): float {
$shippingAmount = (float) $address->getShippingAmount();
if ($shippingAmount == 0.0) {
+ $addressQty = $this->getAddressQty($address);
+ $address->setItemQty($addressQty);
$address->setCollectShippingRates(true);
$address->collectShippingRates();
$shippingRates = $address->getAllShippingRates();
@@ -82,7 +84,7 @@ public function getDiscountAmount(
float $baseRuleTotals,
string $discountType
): float {
- $ratio = $baseItemPrice * $qty / $baseRuleTotals;
+ $ratio = $baseRuleTotals != 0 ? $baseItemPrice * $qty / $baseRuleTotals : 0;
return $this->deltaPriceRound->round(
$ruleDiscount * $ratio,
$discountType
@@ -109,7 +111,7 @@ public function getDiscountedAmountProportionally(
string $discountType
): float {
$baseItemPriceTotal = $baseItemPrice * $qty - $baseItemDiscountAmount;
- $ratio = $baseItemPriceTotal / $baseRuleTotalsDiscount;
+ $ratio = $baseRuleTotalsDiscount != 0 ? $baseItemPriceTotal / $baseRuleTotalsDiscount : 0;
$discountAmount = $this->deltaPriceRound->round($ruleDiscount * $ratio, $discountType);
return $discountAmount;
}
@@ -127,7 +129,7 @@ public function getShippingDiscountAmount(
float $shippingAmount,
float $quoteBaseSubtotal
): float {
- $ratio = $shippingAmount / $quoteBaseSubtotal;
+ $ratio = $quoteBaseSubtotal != 0 ? $shippingAmount / $quoteBaseSubtotal : 0;
return $this->priceCurrency
->roundPrice(
$rule->getDiscountAmount() * $ratio
@@ -241,4 +243,35 @@ public function getAvailableDiscountAmount(
}
return $availableDiscountAmount;
}
+
+ /**
+ * Get address quantity.
+ *
+ * @param AddressInterface $address
+ * @return float
+ */
+ private function getAddressQty(AddressInterface $address): float
+ {
+ $addressQty = 0;
+ $items = array_filter(
+ $address->getAllItems(),
+ function ($item) {
+ return !$item->getProduct()->isVirtual() && !$item->getParentItem();
+ }
+ );
+ foreach ($items as $item) {
+ if ($item->getHasChildren() && $item->isShipSeparately()) {
+ foreach ($item->getChildren() as $child) {
+ if ($child->getProduct()->isVirtual()) {
+ continue;
+ }
+ $addressQty += $child->getTotalQty();
+ }
+ } else {
+ $addressQty += (float)$item->getQty();
+ }
+ }
+
+ return (float)$addressQty;
+ }
}
diff --git a/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule/Createdat.php b/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule/Createdat.php
index 4ebf01d145fe9..342fa8363da09 100644
--- a/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule/Createdat.php
+++ b/app/code/Magento/SalesRule/Model/ResourceModel/Report/Rule/Createdat.php
@@ -89,10 +89,13 @@ protected function _aggregateByOrder($aggregationField, $from, $to)
0
),
'total_amount' => $connection->getIfNullSql(
- 'SUM((base_subtotal - ' . $connection->getIfNullSql(
+ 'SUM(((base_subtotal - ' . $connection->getIfNullSql(
'base_subtotal_canceled',
0
- ) . ' - ' . $connection->getIfNullSql(
+ ) . ' + ' . $connection->getIfNullSql(
+ 'base_shipping_amount - ' . $connection->getIfNullSql('base_shipping_canceled', 0),
+ 0
+ ) . ') - ' . $connection->getIfNullSql(
'ABS(base_discount_amount) - ABS('
. $connection->getIfNullSql('base_discount_canceled', 0) . ')',
0
@@ -119,17 +122,20 @@ protected function _aggregateByOrder($aggregationField, $from, $to)
0
),
'total_amount_actual' => $connection->getIfNullSql(
- 'SUM((base_subtotal_invoiced - ' . $connection->getIfNullSql(
+ 'SUM(((base_subtotal_invoiced - ' . $connection->getIfNullSql(
'base_subtotal_refunded',
0
- ) . ' - ' . $connection->getIfNullSql(
+ ) . ' + ' . $connection->getIfNullSql(
+ 'base_shipping_invoiced - ' . $connection->getIfNullSql('base_shipping_refunded', 0),
+ 0
+ ) . ') - ' . $connection->getIfNullSql(
'ABS(base_discount_invoiced) - ABS('
. $connection->getIfNullSql('base_discount_refunded', 0) . ')',
0
) . ' + ' . $connection->getIfNullSql(
'base_tax_invoiced - ' . $connection->getIfNullSql('base_tax_refunded', 0),
0
- ) . ') * base_to_global_rate)',
+ ) . ') * base_to_global_rate)',
0
),
];
diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml
new file mode 100644
index 0000000000000..9bcefdcfc3144
--- /dev/null
+++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StoreFrontAddZeroPriceProductToCardWithFixedAmountPriceRule.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/Theme/Controller/Result/MessagePlugin.php b/app/code/Magento/Theme/Controller/Result/MessagePlugin.php
index 20d0165e84740..056f1b21a9be8 100644
--- a/app/code/Magento/Theme/Controller/Result/MessagePlugin.php
+++ b/app/code/Magento/Theme/Controller/Result/MessagePlugin.php
@@ -7,7 +7,7 @@
use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Controller\ResultInterface;
-use Magento\Framework\Message\MessageInterface;
+use Magento\Framework\Stdlib\Cookie\CookieSizeLimitReachedException;
use Magento\Framework\Translate\Inline\ParserInterface;
use Magento\Framework\Translate\InlineInterface;
use Magento\Framework\Session\Config\ConfigInterface;
@@ -22,7 +22,7 @@ class MessagePlugin
/**
* Cookies name for messages
*/
- const MESSAGES_COOKIES_NAME = 'mage-messages';
+ public const MESSAGES_COOKIES_NAME = 'mage-messages';
/**
* @var \Magento\Framework\Stdlib\CookieManagerInterface
@@ -101,11 +101,44 @@ public function afterRenderResult(
ResultInterface $result
) {
if (!($subject instanceof Json)) {
- $this->setCookie($this->getMessages());
+ $newMessages = [];
+ foreach ($this->messageManager->getMessages(true)->getItems() as $message) {
+ $newMessages[] = [
+ 'type' => $message->getType(),
+ 'text' => $this->interpretationStrategy->interpret($message),
+ ];
+ }
+ if (!empty($newMessages)) {
+ $this->setMessages($this->getCookiesMessages(), $newMessages);
+ }
}
return $result;
}
+ /**
+ * Add new messages to already existing ones.
+ *
+ * In case if there are too many messages clear old messages.
+ *
+ * @param array $oldMessages
+ * @param array $newMessages
+ * @throws CookieSizeLimitReachedException
+ */
+ private function setMessages(array $oldMessages, array $newMessages): void
+ {
+ $messages = array_merge($oldMessages, $newMessages);
+ try {
+ $this->setCookie($messages);
+ } catch (CookieSizeLimitReachedException $e) {
+ if (empty($oldMessages)) {
+ throw $e;
+ }
+
+ array_shift($oldMessages);
+ $this->setMessages($oldMessages, $newMessages);
+ }
+ }
+
/**
* Set 'mage-messages' cookie with 'messages' array
*
@@ -166,24 +199,6 @@ private function convertMessageText(string $text): string
return $text;
}
- /**
- * Return messages array and clean message manager messages
- *
- * @return array
- */
- protected function getMessages()
- {
- $messages = $this->getCookiesMessages();
- /** @var MessageInterface $message */
- foreach ($this->messageManager->getMessages(true)->getItems() as $message) {
- $messages[] = [
- 'type' => $message->getType(),
- 'text' => $this->interpretationStrategy->interpret($message),
- ];
- }
- return $messages;
- }
-
/**
* Return messages stored in cookies
*
diff --git a/app/code/Magento/Theme/CustomerData/Messages.php b/app/code/Magento/Theme/CustomerData/Messages.php
index adb3f7df27395..15575b1c1c7e0 100644
--- a/app/code/Magento/Theme/CustomerData/Messages.php
+++ b/app/code/Magento/Theme/CustomerData/Messages.php
@@ -7,6 +7,7 @@
namespace Magento\Theme\CustomerData;
use Magento\Customer\CustomerData\SectionSourceInterface;
+use Magento\Framework\App\ObjectManager;
use Magento\Framework\Message\ManagerInterface as MessageManager;
use Magento\Framework\Message\MessageInterface;
use Magento\Framework\View\Element\Message\InterpretationStrategyInterface;
@@ -28,18 +29,27 @@ class Messages implements SectionSourceInterface
*/
private $interpretationStrategy;
+ /**
+ * @var MessagesProviderInterface
+ */
+ private $messageProvider;
+
/**
* Constructor
*
* @param MessageManager $messageManager
* @param InterpretationStrategyInterface $interpretationStrategy
+ * @param MessagesProviderInterface|null $messageProvider
*/
public function __construct(
MessageManager $messageManager,
- InterpretationStrategyInterface $interpretationStrategy
+ InterpretationStrategyInterface $interpretationStrategy,
+ ?MessagesProviderInterface $messageProvider = null
) {
$this->messageManager = $messageManager;
$this->interpretationStrategy = $interpretationStrategy;
+ $this->messageProvider = $messageProvider
+ ?? ObjectManager::getInstance()->get(MessagesProviderInterface::class);
}
/**
@@ -47,19 +57,20 @@ public function __construct(
*/
public function getSectionData()
{
- $messages = $this->messageManager->getMessages(true);
+ $messages = $this->messageProvider->getMessages();
+ $messageResponse = array_reduce(
+ $messages->getItems(),
+ function (array $result, MessageInterface $message) {
+ $result[] = [
+ 'type' => $message->getType(),
+ 'text' => $this->interpretationStrategy->interpret($message)
+ ];
+ return $result;
+ },
+ []
+ );
return [
- 'messages' => array_reduce(
- $messages->getItems(),
- function (array $result, MessageInterface $message) {
- $result[] = [
- 'type' => $message->getType(),
- 'text' => $this->interpretationStrategy->interpret($message)
- ];
- return $result;
- },
- []
- ),
+ 'messages' => $messageResponse
];
}
}
diff --git a/app/code/Magento/Theme/CustomerData/MessagesProvider.php b/app/code/Magento/Theme/CustomerData/MessagesProvider.php
new file mode 100644
index 0000000000000..360f348cb2a45
--- /dev/null
+++ b/app/code/Magento/Theme/CustomerData/MessagesProvider.php
@@ -0,0 +1,42 @@
+messageManager = $messageManager;
+ }
+
+ /**
+ * Return collection object of messages from session
+ *
+ * @return Collection
+ */
+ public function getMessages() : Collection
+ {
+ return $this->messageManager->getMessages(true);
+ }
+}
diff --git a/app/code/Magento/Theme/CustomerData/MessagesProviderInterface.php b/app/code/Magento/Theme/CustomerData/MessagesProviderInterface.php
new file mode 100644
index 0000000000000..cfa281e8aab42
--- /dev/null
+++ b/app/code/Magento/Theme/CustomerData/MessagesProviderInterface.php
@@ -0,0 +1,20 @@
+cookieManagerMock = $this->getMockBuilder(CookieManagerInterface::class)
- ->getMockForAbstractClass();
- $this->cookieMetadataFactoryMock = $this->getMockBuilder(CookieMetadataFactory::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->managerMock = $this->getMockBuilder(ManagerInterface::class)
- ->getMockForAbstractClass();
- $this->interpretationStrategyMock = $this->getMockBuilder(InterpretationStrategyInterface::class)
- ->getMockForAbstractClass();
- $this->serializerMock = $this->getMockBuilder(\Magento\Framework\Serialize\Serializer\Json::class)
- ->getMock();
- $this->inlineTranslateMock = $this->getMockBuilder(InlineInterface::class)
- ->getMockForAbstractClass();
- $this->sessionConfigMock = $this->getMockBuilder(ConfigInterface::class)
- ->getMockForAbstractClass();
+ $this->cookieManagerMock = $this->createMock(CookieManagerInterface::class);
+ $this->cookieMetadataFactoryMock = $this->createMock(CookieMetadataFactory::class);
+ $this->managerMock = $this->createMock(ManagerInterface::class);
+ $this->interpretationStrategyMock = $this->createMock(InterpretationStrategyInterface::class);
+ $this->serializerMock = $this->createMock(JsonSerializer::class);
+ $this->inlineTranslateMock = $this->createMock(InlineInterface::class);
+ $this->sessionConfigMock = $this->createMock(ConfigInterface::class);
$this->model = new MessagePlugin(
$this->cookieManagerMock,
@@ -85,9 +78,7 @@ protected function setUp(): void
public function testAfterRenderResultJson()
{
/** @var Json|MockObject $resultMock */
- $resultMock = $this->getMockBuilder(Json::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $resultMock = $this->createMock(Json::class);
$this->cookieManagerMock->expects($this->never())
->method('setPublicCookie');
@@ -114,19 +105,12 @@ public function testAfterRenderResult()
$messages = array_merge($existingMessages, $messages);
/** @var Redirect|MockObject $resultMock */
- $resultMock = $this->getMockBuilder(Redirect::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $resultMock = $this->createMock(Redirect::class);
/** @var PublicCookieMetadata|MockObject $cookieMetadataMock */
- $cookieMetadataMock = $this->getMockBuilder(PublicCookieMetadata::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $cookieMetadataMock = $this->createMock(PublicCookieMetadata::class);
$this->cookieMetadataFactoryMock->expects($this->once())
->method('createPublicCookieMetadata')
->willReturn($cookieMetadataMock);
-
$this->cookieManagerMock->expects($this->once())
->method('setPublicCookie')
->with(
@@ -157,25 +141,20 @@ function ($data) {
);
/** @var MessageInterface|MockObject $messageMock */
- $messageMock = $this->getMockBuilder(MessageInterface::class)
- ->getMock();
+ $messageMock = $this->createMock(MessageInterface::class);
$messageMock->expects($this->once())
->method('getType')
->willReturn($messageType);
-
$this->interpretationStrategyMock->expects($this->once())
->method('interpret')
->with($messageMock)
->willReturn($messageText);
/** @var Collection|MockObject $collectionMock */
- $collectionMock = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $collectionMock = $this->createMock(Collection::class);
$collectionMock->expects($this->once())
->method('getItems')
->willReturn([$messageMock]);
-
$this->managerMock->expects($this->once())
->method('getMessages')
->with(true, null)
@@ -187,43 +166,27 @@ function ($data) {
public function testAfterRenderResultWithNoMessages()
{
/** @var Redirect|MockObject $resultMock */
- $resultMock = $this->getMockBuilder(Redirect::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->cookieManagerMock->expects($this->once())
- ->method('getCookie')
- ->with(
- MessagePlugin::MESSAGES_COOKIES_NAME
- )
- ->willReturn(json_encode([]));
+ $resultMock = $this->createMock(Redirect::class);
- $this->serializerMock->expects($this->once())
- ->method('unserialize')
- ->willReturnCallback(
- function ($data) {
- return json_decode($data, true);
- }
- );
+ $this->cookieManagerMock->expects($this->never())
+ ->method('getCookie');
+ $this->serializerMock->expects($this->never())
+ ->method('unserialize');
$this->serializerMock->expects($this->never())
->method('serialize');
/** @var Collection|MockObject $collectionMock */
- $collectionMock = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $collectionMock = $this->createMock(Collection::class);
$collectionMock->expects($this->once())
->method('getItems')
->willReturn([]);
-
$this->managerMock->expects($this->once())
->method('getMessages')
->with(true, null)
->willReturn($collectionMock);
$this->cookieMetadataFactoryMock->expects($this->never())
- ->method('createPublicCookieMetadata')
- ->willReturn(null);
+ ->method('createPublicCookieMetadata');
$this->assertEquals($resultMock, $this->model->afterRenderResult($resultMock, $resultMock));
}
@@ -240,19 +203,12 @@ public function testAfterRenderResultWithoutExisting()
];
/** @var Redirect|MockObject $resultMock */
- $resultMock = $this->getMockBuilder(Redirect::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $resultMock = $this->createMock(Redirect::class);
/** @var PublicCookieMetadata|MockObject $cookieMetadataMock */
- $cookieMetadataMock = $this->getMockBuilder(PublicCookieMetadata::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $cookieMetadataMock = $this->createMock(PublicCookieMetadata::class);
$this->cookieMetadataFactoryMock->expects($this->once())
->method('createPublicCookieMetadata')
->willReturn($cookieMetadataMock);
-
$this->cookieManagerMock->expects($this->once())
->method('setPublicCookie')
->with(
@@ -283,25 +239,20 @@ function ($data) {
);
/** @var MessageInterface|MockObject $messageMock */
- $messageMock = $this->getMockBuilder(MessageInterface::class)
- ->getMock();
+ $messageMock = $this->createMock(MessageInterface::class);
$messageMock->expects($this->once())
->method('getType')
->willReturn($messageType);
-
$this->interpretationStrategyMock->expects($this->once())
->method('interpret')
->with($messageMock)
->willReturn($messageText);
/** @var Collection|MockObject $collectionMock */
- $collectionMock = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $collectionMock = $this->createMock(Collection::class);
$collectionMock->expects($this->once())
->method('getItems')
->willReturn([$messageMock]);
-
$this->managerMock->expects($this->once())
->method('getMessages')
->with(true, null)
@@ -322,19 +273,12 @@ public function testAfterRenderResultWithWrongJson()
];
/** @var Redirect|MockObject $resultMock */
- $resultMock = $this->getMockBuilder(Redirect::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $resultMock = $this->createMock(Redirect::class);
/** @var PublicCookieMetadata|MockObject $cookieMetadataMock */
- $cookieMetadataMock = $this->getMockBuilder(PublicCookieMetadata::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $cookieMetadataMock = $this->createMock(PublicCookieMetadata::class);
$this->cookieMetadataFactoryMock->expects($this->once())
->method('createPublicCookieMetadata')
->willReturn($cookieMetadataMock);
-
$this->cookieManagerMock->expects($this->once())
->method('setPublicCookie')
->with(
@@ -351,7 +295,6 @@ public function testAfterRenderResultWithWrongJson()
$this->serializerMock->expects($this->never())
->method('unserialize');
-
$this->serializerMock->expects($this->once())
->method('serialize')
->willReturnCallback(
@@ -361,25 +304,20 @@ function ($data) {
);
/** @var MessageInterface|MockObject $messageMock */
- $messageMock = $this->getMockBuilder(MessageInterface::class)
- ->getMock();
+ $messageMock = $this->createMock(MessageInterface::class);
$messageMock->expects($this->once())
->method('getType')
->willReturn($messageType);
-
$this->interpretationStrategyMock->expects($this->once())
->method('interpret')
->with($messageMock)
->willReturn($messageText);
/** @var Collection|MockObject $collectionMock */
- $collectionMock = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $collectionMock = $this->createMock(Collection::class);
$collectionMock->expects($this->once())
->method('getItems')
->willReturn([$messageMock]);
-
$this->managerMock->expects($this->once())
->method('getMessages')
->with(true, null)
@@ -400,15 +338,9 @@ public function testAfterRenderResultWithWrongArray()
];
/** @var Redirect|MockObject $resultMock */
- $resultMock = $this->getMockBuilder(Redirect::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $resultMock = $this->createMock(Redirect::class);
/** @var PublicCookieMetadata|MockObject $cookieMetadataMock */
- $cookieMetadataMock = $this->getMockBuilder(PublicCookieMetadata::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $cookieMetadataMock = $this->createMock(PublicCookieMetadata::class);
$this->cookieMetadataFactoryMock->expects($this->once())
->method('createPublicCookieMetadata')
->willReturn($cookieMetadataMock);
@@ -443,25 +375,20 @@ function ($data) {
);
/** @var MessageInterface|MockObject $messageMock */
- $messageMock = $this->getMockBuilder(MessageInterface::class)
- ->getMock();
+ $messageMock = $this->createMock(MessageInterface::class);
$messageMock->expects($this->once())
->method('getType')
->willReturn($messageType);
-
$this->interpretationStrategyMock->expects($this->once())
->method('interpret')
->with($messageMock)
->willReturn($messageText);
/** @var Collection|MockObject $collectionMock */
- $collectionMock = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $collectionMock = $this->createMock(Collection::class);
$collectionMock->expects($this->once())
->method('getItems')
->willReturn([$messageMock]);
-
$this->managerMock->expects($this->once())
->method('getMessages')
->with(true, null)
@@ -485,19 +412,12 @@ public function testAfterRenderResultWithAllowedInlineTranslate(): void
];
/** @var Redirect|MockObject $resultMock */
- $resultMock = $this->getMockBuilder(Redirect::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $resultMock = $this->createMock(Redirect::class);
/** @var PublicCookieMetadata|MockObject $cookieMetadataMock */
- $cookieMetadataMock = $this->getMockBuilder(PublicCookieMetadata::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $cookieMetadataMock = $this->createMock(PublicCookieMetadata::class);
$this->cookieMetadataFactoryMock->expects($this->once())
->method('createPublicCookieMetadata')
->willReturn($cookieMetadataMock);
-
$this->cookieManagerMock->expects($this->once())
->method('setPublicCookie')
->with(
@@ -528,12 +448,10 @@ function ($data) {
);
/** @var MessageInterface|MockObject $messageMock */
- $messageMock = $this->getMockBuilder(MessageInterface::class)
- ->getMock();
+ $messageMock = $this->createMock(MessageInterface::class);
$messageMock->expects($this->once())
->method('getType')
->willReturn($messageType);
-
$this->interpretationStrategyMock->expects($this->once())
->method('interpret')
->with($messageMock)
@@ -544,13 +462,10 @@ function ($data) {
->willReturn(true);
/** @var Collection|MockObject $collectionMock */
- $collectionMock = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $collectionMock = $this->createMock(Collection::class);
$collectionMock->expects($this->once())
->method('getItems')
->willReturn([$messageMock]);
-
$this->managerMock->expects($this->once())
->method('getMessages')
->with(true, null)
@@ -562,27 +477,17 @@ function ($data) {
public function testSetCookieWithCookiePath()
{
/** @var Redirect|MockObject $resultMock */
- $resultMock = $this->getMockBuilder(Redirect::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $resultMock = $this->createMock(Redirect::class);
/** @var PublicCookieMetadata|MockObject $cookieMetadataMock */
- $cookieMetadataMock = $this->getMockBuilder(PublicCookieMetadata::class)
- ->disableOriginalConstructor()
- ->getMock();
-
+ $cookieMetadataMock = $this->createMock(PublicCookieMetadata::class);
$this->cookieMetadataFactoryMock->expects($this->once())
->method('createPublicCookieMetadata')
->willReturn($cookieMetadataMock);
/** @var MessageInterface|MockObject $messageMock */
- $messageMock = $this->getMockBuilder(MessageInterface::class)
- ->getMock();
-
+ $messageMock = $this->createMock(MessageInterface::class);
/** @var Collection|MockObject $collectionMock */
- $collectionMock = $this->getMockBuilder(Collection::class)
- ->disableOriginalConstructor()
- ->getMock();
+ $collectionMock = $this->createMock(Collection::class);
$collectionMock->expects($this->once())
->method('getItems')
->willReturn([$messageMock]);
diff --git a/app/code/Magento/Theme/Test/Unit/CustomerData/MessagesTest.php b/app/code/Magento/Theme/Test/Unit/CustomerData/MessagesTest.php
index cb07730c9d8d1..44ccb2b8b3dfb 100644
--- a/app/code/Magento/Theme/Test/Unit/CustomerData/MessagesTest.php
+++ b/app/code/Magento/Theme/Test/Unit/CustomerData/MessagesTest.php
@@ -12,6 +12,7 @@
use Magento\Framework\Message\MessageInterface;
use Magento\Framework\View\Element\Message\InterpretationStrategyInterface;
use Magento\Theme\CustomerData\Messages;
+use Magento\Theme\CustomerData\MessagesProviderInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -22,6 +23,11 @@ class MessagesTest extends TestCase
*/
protected $messageManager;
+ /**
+ * @var MessagesProviderInterface|MockObject
+ */
+ private $messageProvider;
+
/**
* @var InterpretationStrategyInterface|MockObject
*/
@@ -36,10 +42,16 @@ protected function setUp(): void
{
$this->messageManager = $this->getMockBuilder(ManagerInterface::class)
->getMock();
+ $this->messageProvider = $this->getMockBuilder(MessagesProviderInterface::class)
+ ->getMock();
$this->messageInterpretationStrategy = $this->createMock(
InterpretationStrategyInterface::class
);
- $this->object = new Messages($this->messageManager, $this->messageInterpretationStrategy);
+ $this->object = new Messages(
+ $this->messageManager,
+ $this->messageInterpretationStrategy,
+ $this->messageProvider
+ );
}
public function testGetSectionData()
@@ -59,9 +71,8 @@ public function testGetSectionData()
->method('interpret')
->with($msg)
->willReturn($msgText);
- $this->messageManager->expects($this->once())
+ $this->messageProvider->expects($this->once())
->method('getMessages')
- ->with(true, null)
->willReturn($msgCollection);
$msgCollection->expects($this->once())
->method('getItems')
diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml
index d6fe3f8fef355..6ea495e2702ae 100644
--- a/app/code/Magento/Theme/etc/di.xml
+++ b/app/code/Magento/Theme/etc/di.xml
@@ -19,6 +19,7 @@
+
Magento\Framework\App\Cache\Type\Config
diff --git a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js
index 5f7571aa7246d..b9a147e2609de 100644
--- a/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js
+++ b/app/code/Magento/Ui/view/base/web/js/lib/validation/rules.js
@@ -1101,6 +1101,22 @@ define([
return moment.utc(value, params.dateFormat).isSameOrBefore(moment.utc());
},
$.mage.__('The Date of Birth should not be greater than today.')
+ ],
+ 'validate-no-utf8mb4-characters': [
+ function (value) {
+ var validator = this,
+ message = $.mage.__('Please remove invalid characters: {0}.'),
+ matches = value.match(/(?:[\uD800-\uDBFF][\uDC00-\uDFFF])/g),
+ result = matches === null;
+
+ if (!result) {
+ validator.charErrorMessage = message.replace('{0}', matches.join());
+ }
+
+ return result;
+ }, function () {
+ return this.charErrorMessage;
+ }
]
}, function (data) {
return {
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductWithDisabledProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductWithDisabledProductTest.php
new file mode 100644
index 0000000000000..da16dd9ba139a
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/BundleProductWithDisabledProductTest.php
@@ -0,0 +1,156 @@
+compareArraysRecursively = $objectManager->create(CompareArraysRecursively::class);
+ }
+
+ /**
+ * Test Bundle product with disabled product test.
+ *
+ * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_with_disabled_product_options.php
+ *
+ * @throws Exception
+ */
+ public function testBundleProductWithMultipleOptionsWithDisabledProduct(): void
+ {
+ $categorySku = 'c1';
+ $query
+ = <<graphQlQuery($query);
+ $this->assertBundleProduct($response);
+ }
+
+ /**
+ * Assert bundle product response.
+ *
+ * @param array $response
+ */
+ private function assertBundleProduct(array $response): void
+ {
+ $this->assertNotEmpty(
+ $response['categoryList'][0]['products']['items'],
+ 'Precondition failed: "items" must not be empty'
+ );
+ $productItems = end($response['categoryList'][0]['products']['items'])['items'];
+ $this->assertEquals(3, count($productItems[0]['options']));
+ $this->assertEquals('virtual1', $productItems[0]['options'][0]['product']['sku']);
+ $this->assertEquals('virtual2', $productItems[0]['options'][1]['product']['sku']);
+ $this->assertEquals('virtual3', $productItems[0]['options'][2]['product']['sku']);
+ $this->assertEquals(3, count($productItems[1]['options']));
+ $this->assertEquals('virtual1', $productItems[1]['options'][0]['product']['sku']);
+ $this->assertEquals('virtual2', $productItems[1]['options'][1]['product']['sku']);
+ $this->assertEquals('virtual3', $productItems[1]['options'][2]['product']['sku']);
+ }
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php
index 0386d414b8682..28021304ce029 100644
--- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php
@@ -64,15 +64,19 @@ public function __construct(
*
* @param array $customerLogin
* @param array $productData
+ * @param array|null $addressData
* @return array
*/
- public function placeOrderWithBundleProduct(array $customerLogin, array $productData): array
- {
+ public function placeOrderWithBundleProduct(
+ array $customerLogin,
+ array $productData,
+ ?array $addressData = null
+ ): array {
$this->customerLogin = $customerLogin;
$this->createCustomerCart();
$this->addBundleProduct($productData);
- $this->setBillingAddress();
- $shippingMethod = $this->setShippingAddress();
+ $this->setBillingAddress($addressData);
+ $shippingMethod = $this->setShippingAddress($addressData);
$paymentMethod = $this->setShippingMethod($shippingMethod);
$this->setPaymentMethod($paymentMethod);
return $this->doPlaceOrder();
@@ -198,11 +202,13 @@ private function addBundleProduct(array $productData)
/**
* Set the billing address on the cart
*
+ * @param array $addressData
* @return array
*/
- private function setBillingAddress(): array
+ private function setBillingAddress(?array $addressData = null): array
{
- $setBillingAddress = <<assertEquals($expectedBundleOptions, $bundleOptionsFromResponse);
}
+ /**
+ * Test customer order with bundle product and no telephone in address
+ *
+ * @magentoApiDataFixture Magento/Customer/_files/attribute_telephone_not_required_address.php
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php
+ */
+ public function testOrderBundleProductWithNoTelephoneInAddress()
+ {
+ //Place order with bundled product
+ $qty = 1;
+ $bundleSku = 'bundle-product-two-dropdown-options';
+ /** @var CustomerPlaceOrder $bundleProductOrderFixture */
+ $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class);
+ $orderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct(
+ ['email' => 'customer@example.com', 'password' => 'password'],
+ ['sku' => $bundleSku, 'quantity' => $qty],
+ ['telephone' => '']
+ );
+ $orderNumber = $orderResponse['placeOrder']['order']['order_number'];
+ $customerOrderResponse = $this->getCustomerOrderQueryBundleProduct($orderNumber);
+ $customerOrderItems = $customerOrderResponse[0];
+ $this->assertEquals("Pending", $customerOrderItems['status']);
+ $billingAddress = $customerOrderItems['billing_address'];
+ $shippingAddress = $customerOrderItems['shipping_address'];
+ $this->assertNull($billingAddress['telephone']);
+ $this->assertNull($shippingAddress['telephone']);
+ }
+
/**
* Test customer order details with bundle products
* @magentoApiDataFixture Magento/Customer/_files/customer.php
@@ -220,59 +249,81 @@ private function getCustomerOrderQueryBundleProduct($orderNumber)
$query =
<<objectManager->get(ProductRepositoryInterface::class);
+ /** @var ProductInterface $product */
+ $product = $productRepository->get($sku, true, null, true);
+ $extension = $product->getExtensionAttributes();
+ $options = $extension->getBundleProductOptions();
+ $options[0]->setRequired($isOption1Required);
+ $options[1]->setRequired($isOption2Required);
+ $extension->setBundleProductOptions($options);
+ $stockItem = $extension->getStockItem();
+ $stockItem->setUseConfigManageStock(1);
+ $product->setExtensionAttributes($extension);
+ $productRepository->save($product);
+
+ $stockItem = $this->getStockItem((int) $product->getId());
+ $this->assertNotNull($stockItem);
+ $this->assertTrue($stockItem->getIsInStock());
+ foreach ($outOfStockConfig as $childSku => $stockData) {
+ $this->updateStockItem($childSku, $stockData);
+ }
+
+ $stockItem = $this->getStockItem((int) $product->getId());
+ $this->assertNotNull($stockItem);
+ $this->assertFalse($stockItem->getIsInStock());
+ foreach ($inStockConfig as $childSku => $stockData) {
+ $this->updateStockItem($childSku, $stockData);
+ }
+
+ $stockItem = $this->getStockItem((int) $product->getId());
+ $this->assertNotNull($stockItem);
+ $this->assertTrue($stockItem->getIsInStock());
+ }
+
+ /**
+ * @return array
+ */
+ public function shouldUpdateBundleStockStatusIfChildProductsStockStatusChangedDataProvider(): array
+ {
+ return [
+ 'all options are required' => [
+ true,
+ true,
+ 'out-of-stock' => [
+ 'simple1' => [
+ 'is_in_stock' => false
+ ],
+ ],
+ 'in-stock' => [
+ 'simple1' => [
+ 'is_in_stock' => true
+ ]
+ ]
+ ],
+ 'all options are optional' => [
+ false,
+ false,
+ 'out-of-stock' => [
+ 'simple1' => [
+ 'is_in_stock' => false
+ ],
+ 'simple2' => [
+ 'is_in_stock' => false
+ ],
+ ],
+ 'in-stock' => [
+ 'simple1' => [
+ 'is_in_stock' => true
+ ]
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * @param string $sku
+ * @param array $data
+ * @throws NoSuchEntityException
+ */
+ private function updateStockItem(string $sku, array $data): void
+ {
+ /** @var ProductRepositoryInterface $productRepository */
+ $productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
+ $product = $productRepository->get($sku, true, null, true);
+ $extendedAttributes = $product->getExtensionAttributes();
+ $stockItem = $extendedAttributes->getStockItem();
+ $stockItem->setIsInStock($data['is_in_stock']);
+ $extendedAttributes->setStockItem($stockItem);
+ $product->setExtensionAttributes($extendedAttributes);
+ $productRepository->save($product);
+ }
+
+ /**
+ * @param int $productId
+ * @return StockItemInterface|null
+ */
+ private function getStockItem(int $productId): ?StockItemInterface
+ {
+ $criteriaFactory = $this->objectManager->create(StockItemCriteriaInterfaceFactory::class);
+ $stockItemRepository = $this->objectManager->create(StockItemRepositoryInterface::class);
+ $stockConfiguration = $this->objectManager->create(StockConfigurationInterface::class);
+ $criteria = $criteriaFactory->create();
+ $criteria->setScopeFilter($stockConfiguration->getDefaultScopeId());
+ $criteria->setProductsFilter($productId);
+ $stockItemCollection = $stockItemRepository->getList($criteria);
+ $stockItems = $stockItemCollection->getItems();
+ return reset($stockItems);
+ }
+
/**
* @param float $selectionQty
* @param float $qty
diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_disabled_product_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_disabled_product_options.php
new file mode 100644
index 0000000000000..5661c508efbae
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_disabled_product_options.php
@@ -0,0 +1,174 @@
+requireDataFixture(
+ 'Magento/Catalog/_files/multiple_products_with_disabled_virtual_product.php'
+);
+
+$objectManager = Bootstrap::getObjectManager();
+
+$productIds = range(101, 104, 1);
+
+foreach ($productIds as $productId) {
+ /** @var Item $stockItem */
+ $stockItem = $objectManager->create(Item::class);
+ $stockItem->load($productId, 'product_id');
+
+ if (!$stockItem->getProductId()) {
+ $stockItem->setProductId($productId);
+ }
+ $stockItem->setUseConfigManageStock(1);
+ $stockItem->setIsQtyDecimal(0);
+ if ($productId == 104) {
+ $stockItem->setQty(0);
+ $stockItem->setIsInStock(0);
+ } else {
+ $stockItem->setQty(1000);
+ $stockItem->setIsInStock(1);
+ }
+ $stockItem->save();
+}
+
+/** @var $product Product */
+$product = $objectManager->create(Product::class);
+$product->setTypeId(Type::TYPE_BUNDLE)
+ ->setId(3)
+ ->setCategoryIds([565])
+ ->setAttributeSetId(4)
+ ->setWebsiteIds([1])
+ ->setName('Bundle Product')
+ ->setSku('bundle-product')
+ ->setVisibility(Visibility::VISIBILITY_BOTH)
+ ->setStatus(Status::STATUS_ENABLED)
+ ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1])
+ ->setPriceView(1)
+ ->setPriceType(1)
+ ->setPrice(10.0)
+ ->setShipmentType(0)
+ ->setBundleOptionsData(
+ [
+ // Required "Drop-down" option 1
+ [
+ 'title' => 'Option 1',
+ 'default_title' => 'Option 1',
+ 'type' => 'select',
+ 'required' => 1,
+ 'position' => 1,
+ 'delete' => '',
+ ],
+ // Required "Drop-down" option 2
+ [
+ 'title' => 'Option 2',
+ 'default_title' => 'Option 2',
+ 'type' => 'select',
+ 'required' => 1,
+ 'position' => 2,
+ 'delete' => '',
+ ]
+ ]
+ )->setBundleSelectionsData(
+ [
+ [
+ [
+ 'product_id' => 101,
+ 'selection_qty' => 1,
+ 'selection_can_change_qty' => 1,
+ 'delete' => '',
+ 'option_id' => 1
+ ],
+ [
+ 'product_id' => 102,
+ 'selection_qty' => 1,
+ 'selection_can_change_qty' => 1,
+ 'delete' => '',
+ 'option_id' => 1
+ ],
+ [
+ 'product_id' => 103,
+ 'selection_qty' => 1,
+ 'selection_can_change_qty' => 1,
+ 'delete' => '',
+ 'option_id' => 1
+ ],
+ [
+ 'product_id' => 104,
+ 'selection_qty' => 1,
+ 'selection_can_change_qty' => 1,
+ 'delete' => '',
+ 'option_id' => 1
+ ]
+ ],
+ [
+ [
+ 'product_id' => 101,
+ 'selection_qty' => 1,
+ 'selection_can_change_qty' => 1,
+ 'delete' => '',
+ 'option_id' => 2
+ ],
+ [
+ 'product_id' => 102,
+ 'selection_qty' => 1,
+ 'selection_can_change_qty' => 1,
+ 'delete' => '',
+ 'option_id' => 2
+ ],
+ [
+ 'product_id' => 103,
+ 'selection_qty' => 1,
+ 'selection_can_change_qty' => 1,
+ 'delete' => '',
+ 'option_id' => 2
+ ]
+ ]
+ ]
+ );
+$productRepository = $objectManager->create(ProductRepositoryInterface::class);
+
+if ($product->getBundleOptionsData()) {
+ $options = [];
+ foreach ($product->getBundleOptionsData() as $key => $optionData) {
+ if (!(bool)$optionData['delete']) {
+ $option = $objectManager->create(OptionInterfaceFactory::class)
+ ->create(['data' => $optionData]);
+ $option->setSku($product->getSku());
+ $option->setOptionId(null);
+
+ $links = [];
+ $bundleLinks = $product->getBundleSelectionsData();
+ if (!empty($bundleLinks[$key])) {
+ foreach ($bundleLinks[$key] as $linkData) {
+ if (!(bool)$linkData['delete']) {
+ $link = $objectManager->create(LinkInterfaceFactory::class)
+ ->create(['data' => $linkData]);
+ $linkProduct = $productRepository->getById($linkData['product_id']);
+ $link->setSku($linkProduct->getSku());
+ $link->setQty($linkData['selection_qty']);
+ $links[] = $link;
+ }
+ }
+ $option->setProductLinks($links);
+ $options[] = $option;
+ }
+ }
+ }
+ $extension = $product->getExtensionAttributes();
+ $extension->setBundleProductOptions($options);
+ $product->setExtensionAttributes($extension);
+}
+$product->save();
diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_disabled_product_options_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_disabled_product_options_rollback.php
new file mode 100644
index 0000000000000..3d542ebbb0df4
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_disabled_product_options_rollback.php
@@ -0,0 +1,36 @@
+requireDataFixture(
+ 'Magento/Catalog/_files/multiple_products_with_disabled_virtual_product_rollback.php'
+);
+
+$objectManager = Bootstrap::getObjectManager();
+/** @var Registry $registry */
+$registry = Bootstrap::getObjectManager()->get(Registry::class);
+/** @var ProductRepositoryInterface $productRepository */
+$productRepository = $objectManager->create(ProductRepositoryInterface::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+//delete bundle product
+try {
+ $product = $productRepository->get('bundle-product');
+ $productRepository->delete($product);
+} catch (NoSuchEntityException $exception) {
+ //Product already removed
+}
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options.php
index 46a88b1ec8b13..d1d4ccacace7f 100644
--- a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options.php
+++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options.php
@@ -9,9 +9,16 @@
Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php');
$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
+$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
$productIds = range(10, 12, 1);
foreach ($productIds as $productId) {
+ $product = $productRepository->getById($productId, true, null, true);
+ if ((int) $product->getStatus() === \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) {
+ $product->unlockAttribute('status')
+ ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED);
+ $product->save();
+ }
/** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */
$stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class);
$stockItem->load($productId, 'product_id');
@@ -167,7 +174,6 @@
]
]
);
-$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
if ($product->getBundleOptionsData()) {
$options = [];
diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php
index 7228e21f0a026..883b41387a2b0 100644
--- a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php
+++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/Product/Type/BundleTest.php
@@ -5,18 +5,26 @@
*/
namespace Magento\BundleImportExport\Model\Import\Product\Type;
+use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product as ProductResource;
+use Magento\CatalogInventory\Api\Data\StockItemInterface;
+use Magento\CatalogInventory\Api\StockConfigurationInterface;
+use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory;
+use Magento\CatalogInventory\Api\StockItemRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Filesystem;
use Magento\ImportExport\Model\Import;
+use Magento\ImportExport\Model\Import\Adapter as ImportAdapter;
+use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface;
use Magento\ImportExport\Model\Import\Source\Csv;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\Framework\App\Filesystem\DirectoryList;
/**
* @magentoAppArea adminhtml
+ * @magentoAppIsolation enabled
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class BundleTest extends \Magento\TestFramework\Indexer\TestCase
@@ -24,12 +32,12 @@ class BundleTest extends \Magento\TestFramework\Indexer\TestCase
/**
* Bundle product test Name
*/
- const TEST_PRODUCT_NAME = 'Bundle 1';
+ private const TEST_PRODUCT_NAME = 'Bundle 1';
/**
* Bundle product test Type
*/
- const TEST_PRODUCT_TYPE = 'bundle';
+ private const TEST_PRODUCT_TYPE = 'bundle';
/**
* @var \Magento\CatalogImportExport\Model\Import\Product
@@ -81,29 +89,8 @@ public function testBundleImport()
{
// import data from CSV file
$pathToFile = __DIR__ . '/../../_files/import_bundle.csv';
- $filesystem = $this->objectManager->create(
- Filesystem::class
- );
-
- $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT);
- $source = $this->objectManager->create(
- Csv::class,
- [
- 'file' => $pathToFile,
- 'directory' => $directory
- ]
- );
- $errors = $this->model->setSource(
- $source
- )->setParameters(
- [
- 'behavior' => Import::BEHAVIOR_APPEND,
- 'entity' => 'catalog_product'
- ]
- )->validateData();
-
- $this->assertTrue($errors->getErrorsCount() == 0);
- $this->model->importData();
+ $errors = $this->doImport($pathToFile, Import::BEHAVIOR_APPEND);
+ $this->assertEquals(0, $errors->getErrorsCount());
$resource = $this->objectManager->get(ProductResource::class);
$productId = $resource->getIdBySku(self::TEST_PRODUCT_NAME);
@@ -161,50 +148,13 @@ public function testBundleImportUpdateValues(array $expectedValues): void
{
// import data from CSV file
$pathToFile = __DIR__ . '/../../_files/import_bundle.csv';
- $filesystem = $this->objectManager->create(
- Filesystem::class
- );
-
- $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT);
- $source = $this->objectManager->create(
- Csv::class,
- [
- 'file' => $pathToFile,
- 'directory' => $directory
- ]
- );
- $errors = $this->model->setSource(
- $source
- )->setParameters(
- [
- 'behavior' => Import::BEHAVIOR_APPEND,
- 'entity' => 'catalog_product'
- ]
- )->validateData();
-
- $this->assertTrue($errors->getErrorsCount() == 0);
- $this->model->importData();
+ $errors = $this->doImport($pathToFile, Import::BEHAVIOR_APPEND);
+ $this->assertEquals(0, $errors->getErrorsCount());
// import data from CSV file to update values
$pathToFile2 = __DIR__ . '/../../_files/import_bundle_update_values.csv';
- $source2 = $this->objectManager->create(
- Csv::class,
- [
- 'file' => $pathToFile2,
- 'directory' => $directory
- ]
- );
- $errors2 = $this->model->setSource(
- $source2
- )->setParameters(
- [
- 'behavior' => Import::BEHAVIOR_APPEND,
- 'entity' => 'catalog_product'
- ]
- )->validateData();
-
- $this->assertTrue($errors2->getErrorsCount() == 0);
- $this->model->importData();
+ $errors = $this->doImport($pathToFile2, Import::BEHAVIOR_APPEND);
+ $this->assertEquals(0, $errors->getErrorsCount());
$resource = $this->objectManager->get(ProductResource::class);
$productId = $resource->getIdBySku(self::TEST_PRODUCT_NAME);
@@ -244,24 +194,8 @@ public function testBundleImportWithMultipleStoreViews(): void
{
// import data from CSV file
$pathToFile = __DIR__ . '/../../_files/import_bundle_multiple_store_views.csv';
- $filesystem = $this->objectManager->create(Filesystem::class);
- $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT);
- $source = $this->objectManager->create(
- Csv::class,
- [
- 'file' => $pathToFile,
- 'directory' => $directory,
- ]
- );
- $errors = $this->model->setSource($source)
- ->setParameters(
- [
- 'behavior' => Import::BEHAVIOR_APPEND,
- 'entity' => 'catalog_product',
- ]
- )->validateData();
- $this->assertTrue($errors->getErrorsCount() == 0);
- $this->model->importData();
+ $errors = $this->doImport($pathToFile, Import::BEHAVIOR_APPEND);
+ $this->assertEquals(0, $errors->getErrorsCount());
$resource = $this->objectManager->get(ProductResource::class);
$productId = $resource->getIdBySku(self::TEST_PRODUCT_NAME);
$this->assertIsNumeric($productId);
@@ -323,6 +257,121 @@ public function valuesDataProvider(): array
];
}
+ /**
+ * @magentoDbIsolation enabled
+ * @dataProvider shouldUpdateBundleStockStatusIfChildProductsStockStatusChangedDataProvider
+ * @param bool $isOption1Required
+ * @param bool $isOption2Required
+ * @param string $outOfStockImportFile
+ * @param string $inStockImportFile
+ * @throws NoSuchEntityException
+ */
+ public function testShouldUpdateBundleStockStatusIfChildProductsStockStatusChanged(
+ bool $isOption1Required,
+ bool $isOption2Required,
+ string $outOfStockImportFile,
+ string $inStockImportFile
+ ): void {
+ // import data from CSV file
+ $pathToFile = __DIR__ . '/../../_files/import_bundle.csv';
+ $errors = $this->doImport($pathToFile, Import::BEHAVIOR_APPEND);
+ $this->assertEquals(0, $errors->getErrorsCount());
+ $this->importedProductSkus = ['Simple 1', 'Simple 2', 'Simple 3', 'Bundle 1'];
+ $sku = 'Bundle 1';
+ /** @var ProductRepositoryInterface $productRepository */
+ $productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
+ /** @var ProductInterface $product */
+ $product = $productRepository->get($sku, true, null, true);
+ $options = $product->getExtensionAttributes()->getBundleProductOptions();
+ $options[0]->setRequired($isOption1Required);
+ $options[1]->setRequired($isOption2Required);
+ $extension = $product->getExtensionAttributes();
+ $extension->setBundleProductOptions($options);
+ $product->setExtensionAttributes($extension);
+ $productRepository->save($product);
+
+ $stockItem = $this->getStockItem((int) $product->getId());
+ $this->assertNotNull($stockItem);
+ $this->assertTrue($stockItem->getIsInStock());
+
+ $errors = $this->doImport(__DIR__ . '/../../_files/' . $outOfStockImportFile);
+ $this->assertEquals(0, $errors->getErrorsCount());
+
+ $stockItem = $this->getStockItem((int) $product->getId());
+ $this->assertNotNull($stockItem);
+ $this->assertFalse($stockItem->getIsInStock());
+
+ $errors = $this->doImport(__DIR__ . '/../../_files/' . $inStockImportFile);
+ $this->assertEquals(0, $errors->getErrorsCount());
+
+ $stockItem = $this->getStockItem((int) $product->getId());
+ $this->assertNotNull($stockItem);
+ $this->assertTrue($stockItem->getIsInStock());
+ }
+
+ /**
+ * @return array
+ */
+ public function shouldUpdateBundleStockStatusIfChildProductsStockStatusChangedDataProvider(): array
+ {
+ return [
+ 'all options are required' => [
+ true,
+ true,
+ 'out-of-stock' => 'import_bundle_set_option1_products_out_of_stock.csv',
+ 'in-stock' => 'import_bundle_set_option1_products_in_stock.csv'
+ ],
+ 'all options are optional' => [
+ false,
+ false,
+ 'out-of-stock' => 'import_bundle_set_all_products_out_of_stock.csv',
+ 'in-stock' => 'import_bundle_set_option1_products_in_stock.csv'
+ ]
+ ];
+ }
+
+ /**
+ * @param int $productId
+ * @return StockItemInterface|null
+ */
+ private function getStockItem(int $productId): ?StockItemInterface
+ {
+ $criteriaFactory = $this->objectManager->create(StockItemCriteriaInterfaceFactory::class);
+ $stockItemRepository = $this->objectManager->create(StockItemRepositoryInterface::class);
+ $stockConfiguration = $this->objectManager->create(StockConfigurationInterface::class);
+ $criteria = $criteriaFactory->create();
+ $criteria->setScopeFilter($stockConfiguration->getDefaultScopeId());
+ $criteria->setProductsFilter($productId);
+ $stockItemCollection = $stockItemRepository->getList($criteria);
+ $stockItems = $stockItemCollection->getItems();
+ return reset($stockItems);
+ }
+
+ /**
+ * @param string $file
+ * @param string $behavior
+ * @param bool $validateOnly
+ * @return ProcessingErrorAggregatorInterface
+ */
+ private function doImport(
+ string $file,
+ string $behavior = Import::BEHAVIOR_ADD_UPDATE,
+ bool $validateOnly = false
+ ): ProcessingErrorAggregatorInterface {
+ /** @var Filesystem $filesystem */
+ $filesystem =$this->objectManager->create(Filesystem::class);
+ $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT);
+ $source = ImportAdapter::findAdapterFor($file, $directoryWrite);
+ $errors = $this->model
+ ->setParameters(['behavior' => $behavior, 'entity' => 'catalog_product'])
+ ->setSource($source)
+ ->validateData();
+ if (!$validateOnly && !$errors->getAllErrors()) {
+ $this->model->importData();
+ }
+ return $errors;
+ }
+
/**
* teardown
*/
diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_set_all_products_out_of_stock.csv b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_set_all_products_out_of_stock.csv
new file mode 100644
index 0000000000000..36e8e87196ac1
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_set_all_products_out_of_stock.csv
@@ -0,0 +1,4 @@
+"sku","qty","is_in_stock"
+"Simple 1","0","0"
+"Simple 2","0","0"
+"Simple 3","0","0"
diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_set_option1_products_in_stock.csv b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_set_option1_products_in_stock.csv
new file mode 100644
index 0000000000000..ac888ecbc7079
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_set_option1_products_in_stock.csv
@@ -0,0 +1,2 @@
+"sku","qty","is_in_stock"
+"Simple 1","100","1"
diff --git a/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_set_option1_products_out_of_stock.csv b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_set_option1_products_out_of_stock.csv
new file mode 100644
index 0000000000000..64830cdf3619c
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/BundleImportExport/Model/Import/_files/import_bundle_set_option1_products_out_of_stock.csv
@@ -0,0 +1,2 @@
+"sku","qty","is_in_stock"
+"Simple 1","0","0"
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products_with_disabled_virtual_product.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products_with_disabled_virtual_product.php
new file mode 100644
index 0000000000000..5cb63985311df
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products_with_disabled_virtual_product.php
@@ -0,0 +1,114 @@
+create(Category::class);
+$category->isObjectNew(true);
+$category->setId('565')
+ ->setName('c1')
+ ->setAttributeSetId('3')
+ ->setParentId(2)
+ ->setPath('1/2/565')
+ ->setLevel('2')
+ ->setDefaultSortBy('name')
+ ->setIsActive(true)
+ ->save();
+
+//virtual product 1
+/** @var $product Product */
+$product = Bootstrap::getObjectManager()->create(Product::class);
+$product->isObjectNew(true);
+$product->setTypeId(Product\Type::TYPE_VIRTUAL)
+ ->setId(101)
+ ->setAttributeSetId(4)
+ ->setName('Virtual Product1')
+ ->setSku('virtual1')
+ ->setTaxClassId('none')
+ ->setDescription('description unique word')
+ ->setShortDescription('short description')
+ ->setOptionsContainer('container1')
+ ->setPrice(10)
+ ->setWeight(1)
+ ->setMetaTitle('meta title')
+ ->setMetaKeyword('meta keyword')
+ ->setMetaDescription('meta description')
+ ->setVisibility(Visibility::VISIBILITY_BOTH)
+ ->setStatus(Status::STATUS_ENABLED)
+ ->setWebsiteIds([1])
+ ->setCategoryIds([565])
+ ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1])
+ ->save();
+
+//virtual product 2
+$product = Bootstrap::getObjectManager()->create(Product::class);
+$product->isObjectNew(true);
+$product->setTypeId(Product\Type::TYPE_VIRTUAL)
+ ->setId(102)
+ ->setAttributeSetId(4)
+ ->setName('Virtual Product2')
+ ->setSku('virtual2')
+ ->setTaxClassId('none')
+ ->setDescription('description')
+ ->setShortDescription('short description')
+ ->setOptionsContainer('container1')
+ ->setPrice(20)
+ ->setWeight(1)
+ ->setMetaTitle('meta title')
+ ->setMetaKeyword('meta keyword')
+ ->setMetaDescription('meta description')
+ ->setVisibility(Visibility::VISIBILITY_IN_CATALOG)
+ ->setStatus(Status::STATUS_ENABLED)
+ ->setWebsiteIds([1])
+ ->setCategoryIds([565])
+ ->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1])
+ ->save();
+
+//virtual product 3
+$product = Bootstrap::getObjectManager()->create(Product::class);
+$product->isObjectNew(true);
+$product->setTypeId(Product\Type::TYPE_VIRTUAL)
+ ->setId(103)
+ ->setAttributeSetId(4)
+ ->setName('Virtual Product3')
+ ->setSku('virtual3')
+ ->setTaxClassId('none')
+ ->setDescription('description')
+ ->setShortDescription('short description')
+ ->setPrice(30)
+ ->setWeight(1)
+ ->setVisibility(Visibility::VISIBILITY_IN_CATALOG)
+ ->setStatus(Status::STATUS_ENABLED)
+ ->setWebsiteIds([1])
+ ->setCategoryIds([565])
+ ->setStockData(['use_config_manage_stock' => 1, 'qty' => 140, 'is_qty_decimal' => 0, 'is_in_stock' => 1])
+ ->save();
+
+//virtual product 4
+$product = Bootstrap::getObjectManager()->create(Product::class);
+$product->isObjectNew(true);
+$product->setTypeId(Product\Type::TYPE_VIRTUAL)
+ ->setId(104)
+ ->setAttributeSetId(4)
+ ->setName('Virtual Product4')
+ ->setSku('virtual4')
+ ->setTaxClassId('none')
+ ->setDescription('description')
+ ->setShortDescription('short description')
+ ->setPrice(40)
+ ->setWeight(1)
+ ->setVisibility(Visibility::VISIBILITY_IN_CATALOG)
+ ->setStatus(Status::STATUS_DISABLED)
+ ->setWebsiteIds([1])
+ ->setCategoryIds([565])
+ ->setStockData(['use_config_manage_stock' => 1, 'qty' => 0, 'is_qty_decimal' => 0, 'is_in_stock' => 0])
+ ->save();
diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products_with_disabled_virtual_product_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products_with_disabled_virtual_product_rollback.php
new file mode 100644
index 0000000000000..0a7ee384e5428
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products_with_disabled_virtual_product_rollback.php
@@ -0,0 +1,44 @@
+get(Registry::class);
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', true);
+
+//delete category
+/** @var CategoryCollection $collection */
+$categoryCollection = $objectManager->create(CategoryCollection::class);
+$categoryCollection
+ ->addAttributeToFilter('level', 2)
+ ->load()
+ ->delete();
+
+//delete product
+/** @var ProductRepositoryInterface $productRepository */
+$productRepository = $objectManager->create(ProductRepositoryInterface::class);
+
+foreach (['virtual1', 'virtual2', 'virtual3', 'virtual4'] as $sku) {
+ try {
+ $product = $productRepository->get($sku, false, null, true);
+ $productRepository->delete($product);
+ } catch (NoSuchEntityException $exception) {
+ //Product already removed
+ }
+}
+
+$registry->unregister('isSecureArea');
+$registry->register('isSecureArea', false);
diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php
index 0bef038b5f7e5..b06de8109ecbd 100644
--- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php
+++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/ResourceModel/Product/ConditionsToCollectionApplierTest.php
@@ -247,7 +247,8 @@ private function conditionProvider()
'simple-product-9',
'simple-product-10',
'simple-product-11',
- 'simple-product-12'
+ 'simple-product-12',
+ 'simple-product-13',
]
],
@@ -272,7 +273,8 @@ private function conditionProvider()
'simple-product-9',
'simple-product-10',
'simple-product-11',
- 'simple-product-12'
+ 'simple-product-12',
+ 'simple-product-13',
]
],
@@ -386,7 +388,8 @@ private function conditionProvider()
'simple-product-9',
'simple-product-10',
'simple-product-11',
- 'simple-product-12'
+ 'simple-product-12',
+ 'simple-product-13',
]
],
@@ -416,7 +419,8 @@ private function conditionProvider()
'simple-product-9',
'simple-product-10',
'simple-product-11',
- 'simple-product-12'
+ 'simple-product-12',
+ 'simple-product-13',
]
],
@@ -424,12 +428,9 @@ private function conditionProvider()
'variation 22' => [
'condition' => $this->getConditionsForVariation22(),
'expected-sku' => [
- 'simple-product-1',
- 'simple-product-2',
- 'simple-product-3',
- 'simple-product-4',
'simple-product-7',
- 'simple-product-8'
+ 'simple-product-8',
+ 'simple-product-13',
]
],
];
diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php
index db44a3d10ac9c..71c3d2369506e 100644
--- a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php
+++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/conditions_to_collection/products.php
@@ -173,6 +173,19 @@
'qty' => 42,
'categories' => ['Category 1.1.1'],
],
+ [
+ 'type-id' => 'simple',
+ 'attribute-set-id' => $attributeSetGuardians->getId(),
+ 'website-ids' => [1],
+ 'name' => 'Simple Product 13',
+ 'sku' => 'simple-product-13',
+ 'price' => 10,
+ 'visibility' => \Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH,
+ 'status' => \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED,
+ 'stock-data' => ['use_config_manage_stock' => 1, 'qty' => 22, 'is_in_stock' => 1],
+ 'qty' => 42,
+ 'categories' => [],
+ ],
];
foreach ($productsData as $productData) {
diff --git a/dev/tests/integration/testsuite/Magento/CatalogRuleConfigurable/Model/Indexer/Product/ProductRuleIndexerTest.php b/dev/tests/integration/testsuite/Magento/CatalogRuleConfigurable/Model/Indexer/Product/ProductRuleIndexerTest.php
index 7463b0967833f..d040cad330aff 100644
--- a/dev/tests/integration/testsuite/Magento/CatalogRuleConfigurable/Model/Indexer/Product/ProductRuleIndexerTest.php
+++ b/dev/tests/integration/testsuite/Magento/CatalogRuleConfigurable/Model/Indexer/Product/ProductRuleIndexerTest.php
@@ -49,51 +49,109 @@ protected function setUp(): void
}
/**
+ * @dataProvider productsDataProvider
+ * @param string $reindexSku
+ * @param array $expectedPrices
* @return void
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
- public function testExecute(): void
+ public function testExecute(string $reindexSku, array $expectedPrices): void
{
- $product = $this->productRepository->get('configurable');
+ $product = $this->productRepository->get($reindexSku);
$this->productRuleIndexer->execute([$product->getId()]);
- $product = $this->productRepository->get('simple_10');
- $price = $this->getCatalogRulePrice($product);
- $this->assertEquals(5, $price);
+ $this->assertEquals($expectedPrices, $this->getCatalogRulePrices(array_keys($expectedPrices)));
}
/**
+ * @dataProvider productsDataProvider
+ * @param string $reindexSku
+ * @param array $expectedPrices
* @return void
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
- public function testExecuteRow(): void
+ public function testExecuteRow(string $reindexSku, array $expectedPrices): void
{
- $product = $this->productRepository->get('configurable');
+ $product = $this->productRepository->get($reindexSku);
$this->productRuleIndexer->executeRow($product->getId());
- $product = $this->productRepository->get('simple_10');
- $price = $this->getCatalogRulePrice($product);
- $this->assertEquals(5, $price);
+ $this->assertEquals($expectedPrices, $this->getCatalogRulePrices(array_keys($expectedPrices)));
}
/**
+ * @dataProvider productsDataProvider
+ * @param string $reindexSku
+ * @param array $expectedPrices
* @return void
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
*/
- public function testExecuteList(): void
+ public function testExecuteList(string $reindexSku, array $expectedPrices): void
{
- $product = $this->productRepository->get('configurable');
+ $product = $this->productRepository->get($reindexSku);
$this->productRuleIndexer->executeList([$product->getId()]);
- $product = $this->productRepository->get('simple_10');
- $price = $this->getCatalogRulePrice($product);
- $this->assertEquals(5, $price);
+ $this->assertEquals($expectedPrices, $this->getCatalogRulePrices(array_keys($expectedPrices)));
}
+ /**
+ * @return void
+ */
public function testExecuteFull(): void
{
$this->productRuleIndexer->executeFull();
- $product = $this->productRepository->get('simple_10');
- $price = $this->getCatalogRulePrice($product);
- $this->assertEquals(5, $price);
+ $expectedPrices = [
+ 'simple_10' => 5,
+ 'simple_20' => 10,
+ ];
+ $this->assertEquals($expectedPrices, $this->getCatalogRulePrices(array_keys($expectedPrices)));
+ }
+
+ /**
+ * @return array
+ */
+ public function productsDataProvider(): array
+ {
+ return [
+ [
+ 'configurable',
+ [
+ 'simple_10' => 5,
+ 'simple_20' => 10,
+ ]
+ ],
+ [
+ 'simple_10',
+ [
+ 'simple_10' => 5,
+ 'simple_20' => 10,
+ ]
+ ],
+ [
+ 'simple_20',
+ [
+ 'simple_10' => 5,
+ 'simple_20' => 10,
+ ]
+ ],
+ ];
+ }
+
+ /**
+ * @param array $skus
+ * @return array
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function getCatalogRulePrices(array $skus): array
+ {
+ $actualPrices = [];
+ foreach ($skus as $sku) {
+ $product = $this->productRepository->get($sku);
+ $actualPrices[$sku] = $this->getCatalogRulePrice($product);
+ }
+ return $actualPrices;
}
/**
diff --git a/dev/tests/integration/testsuite/Magento/Cron/Model/Config/Backend/SitemapTest.php b/dev/tests/integration/testsuite/Magento/Cron/Model/Config/Backend/SitemapTest.php
new file mode 100644
index 0000000000000..6e40366eee04d
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/Cron/Model/Config/Backend/SitemapTest.php
@@ -0,0 +1,98 @@
+objectManager = Bootstrap::getObjectManager();
+ }
+
+ /**
+ * @dataProvider frequencyDataProvider
+ * @param string $frequency
+ * @param string $expectedCronExpr
+ */
+ public function testDirectSave(string $frequency, string $expectedCronExpr): void
+ {
+ $preparedValueFactory = $this->objectManager->get(PreparedValueFactory::class);
+ /** @var Sitemap $sitemapValue */
+ $sitemapValue = $preparedValueFactory->create('sitemap/generate/frequency', $frequency, 'default', 0);
+ $sitemapValue->save();
+
+ self::assertEquals($expectedCronExpr, $this->getCronExpression());
+ }
+
+ /**
+ * @dataProvider frequencyDataProvider
+ * @param string $frequency
+ * @param string $expectedCronExpr
+ */
+ public function testSaveFromAdmin(string $frequency, string $expectedCronExpr): void
+ {
+ $config = $this->objectManager->create(ConfigModel::class);
+ $config->setSection('sitemap');
+ $config->setGroups(
+ [
+ 'generate' => [
+ 'fields' => [
+ 'time' => ['value' => ['00', '00', '00']],
+ 'frequency' => ['value' => $frequency],
+ ],
+ ],
+ ]
+ );
+ $config->save();
+
+ self::assertEquals($expectedCronExpr, $this->getCronExpression());
+ }
+
+ /**
+ * @return array
+ */
+ public function frequencyDataProvider(): array
+ {
+ return [
+ 'daily' => [Frequency::CRON_DAILY, '0 0 * * *'],
+ 'weekly' => [Frequency::CRON_WEEKLY, '0 0 * * 1'],
+ 'monthly' => [Frequency::CRON_MONTHLY, '0 0 1 * *'],
+ ];
+ }
+
+ /**
+ * @return string
+ */
+ private function getCronExpression(): string
+ {
+ $valueFactory = $this->objectManager->get(ValueFactory::class);
+ $cronExprValue = $valueFactory->create()
+ ->load(Sitemap::CRON_STRING_PATH, 'path');
+
+ return $cronExprValue->getValue();
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Report/Rule/CreatedatTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Report/Rule/CreatedatTest.php
index 393f9f1cc925b..6f50ce57309e5 100644
--- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Report/Rule/CreatedatTest.php
+++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/ResourceModel/Report/Rule/CreatedatTest.php
@@ -58,7 +58,8 @@ public function testTotals($orderParams)
private function getTotalAmount(\Magento\Sales\Model\Order $order)
{
return (
- $order->getBaseSubtotal() - $order->getBaseSubtotalCanceled()
+ ($order->getBaseSubtotal() - $order->getBaseSubtotalCanceled()
+ + ($order->getBaseShippingAmount() - $order->getBaseShippingCanceled()))
- (abs((float) $order->getBaseDiscountAmount()) - abs((float) $order->getBaseDiscountCanceled()))
+ ($order->getBaseTaxAmount() - $order->getBaseTaxCanceled())
) * $order->getBaseToGlobalRate();
@@ -73,7 +74,8 @@ private function getTotalAmount(\Magento\Sales\Model\Order $order)
private function getTotalAmountActual(\Magento\Sales\Model\Order $order)
{
return (
- $order->getBaseSubtotalInvoiced() - $order->getSubtotalRefunded()
+ ($order->getBaseSubtotalInvoiced() - $order->getSubtotalRefunded()
+ + ($order->getBaseShippingInvoiced() - $order->getBaseShippingRefunded()))
- abs((float) $order->getBaseDiscountInvoiced()) - abs((float) $order->getBaseDiscountRefunded())
+ $order->getBaseTaxInvoiced() - $order->getBaseTaxRefunded()
) * $order->getBaseToGlobalRate();
diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js
index 4a9d0c65e19ea..0b0687e7d329c 100644
--- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js
+++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/plugins/magentowidget/editor_plugin.js
@@ -142,7 +142,7 @@ define([
* @return {*}
*/
encodeWidgets: function (content) {
- return content.gsub(/\{\{widget(.*?)\}\}/i, function (match) {
+ return content.gsub(/\{\{widget([\S\s]*?)\}\}/i, function (match) {
var attributes = wysiwyg.parseAttributesString(match[1]),
imageSrc,
imageHtml = '';