Skip to content

Commit 9d1fe17

Browse files
authored
Merge pull request #6781 from magento-l3/PR-L3-20210405
PR-L3-20210405
2 parents c640062 + 9717890 commit 9d1fe17

File tree

47 files changed

+1616
-38
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1616
-38
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Bundle\Model\Product;
9+
10+
use Magento\Catalog\Model\Product;
11+
use Magento\Catalog\Model\Product\Type as BundleType;
12+
13+
/**
14+
* Service to check is bundle product has single choice (no customization possible)
15+
*/
16+
class SingleChoiceProvider
17+
{
18+
/**
19+
* Single choice availability
20+
*
21+
* @param Product $product
22+
* @return bool
23+
*/
24+
public function isSingleChoiceAvailable(Product $product) : bool
25+
{
26+
$result = false;
27+
if ($product->getTypeId() === BundleType::TYPE_BUNDLE) {
28+
$typeInstance = $product->getTypeInstance();
29+
$typeInstance->setStoreFilter($product->getStoreId(), $product);
30+
31+
if ($typeInstance->hasRequiredOptions($product)) {
32+
$options = $typeInstance->getOptions($product);
33+
$isNoCustomizations = true;
34+
foreach ($options as $option) {
35+
$optionId = $option->getId();
36+
$required = $option->getRequired();
37+
if ($isNoCustomizations && (int) $required === 1) {
38+
$selectionsCollection = $typeInstance->getSelectionsCollection(
39+
[$optionId],
40+
$product
41+
);
42+
$selections = $selectionsCollection->exportToArray();
43+
if (count($selections) > 1) {
44+
foreach ($selections as $selection) {
45+
if ($isNoCustomizations) {
46+
$isNoCustomizations = (int)$selection['is_default'] === 1
47+
&& (int)$selection['selection_can_change_qty'] === 0;
48+
} else {
49+
break;
50+
}
51+
}
52+
}
53+
} else {
54+
$isNoCustomizations = false;
55+
break;
56+
}
57+
}
58+
59+
$result = $isNoCustomizations;
60+
}
61+
}
62+
return $result;
63+
}
64+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Bundle\Plugin\Catalog\Model\Product\Type;
9+
10+
use Magento\Catalog\Model\Product\Type\AbstractType as Subject;
11+
use Magento\Catalog\Model\Product;
12+
use Magento\Catalog\Model\Product\Type;
13+
use Magento\Bundle\Model\Product\SingleChoiceProvider;
14+
15+
/**
16+
* Plugin to add possibility to add bundle product with single option from list
17+
*/
18+
class AbstractType
19+
{
20+
/**
21+
* @var SingleChoiceProvider
22+
*/
23+
private $singleChoiceProvider;
24+
25+
/**
26+
* @param SingleChoiceProvider $singleChoiceProvider
27+
*/
28+
public function __construct(
29+
SingleChoiceProvider $singleChoiceProvider
30+
) {
31+
$this->singleChoiceProvider = $singleChoiceProvider;
32+
}
33+
34+
/**
35+
* Add possibility to add to cart from the list in case of one required option
36+
*
37+
* @param Subject $subject
38+
* @param bool $result
39+
* @param Product $product
40+
* @return bool
41+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
42+
*/
43+
public function afterIsPossibleBuyFromList(Subject $subject, $result, $product)
44+
{
45+
if ($product->getTypeId() === Type::TYPE_BUNDLE) {
46+
$isSingleChoice = $this->singleChoiceProvider->isSingleChoiceAvailable($product);
47+
if ($isSingleChoice === true) {
48+
$result = $isSingleChoice;
49+
}
50+
}
51+
return $result;
52+
}
53+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Bundle\Plugin\Catalog\ViewModel\Product;
9+
10+
use Magento\Catalog\Model\Product;
11+
use Magento\Catalog\ViewModel\Product\OptionsData as Subject;
12+
use Magento\Catalog\Model\Product\Type;
13+
use Magento\Bundle\Model\Product\SingleChoiceProvider;
14+
15+
/**
16+
* Plugin to add bundle options data
17+
*/
18+
class AddBundleOptionsData
19+
{
20+
/**
21+
* @var SingleChoiceProvider
22+
*/
23+
private $singleChoiceProvider;
24+
25+
/**
26+
* @param SingleChoiceProvider $singleChoiceProvider
27+
*/
28+
public function __construct(
29+
SingleChoiceProvider $singleChoiceProvider
30+
) {
31+
$this->singleChoiceProvider = $singleChoiceProvider;
32+
}
33+
34+
public function afterGetOptionsData(Subject $subject, array $result, Product $product) : array
35+
{
36+
if ($product->getTypeId() === Type::TYPE_BUNDLE) {
37+
if ($this->singleChoiceProvider->isSingleChoiceAvailable($product) === true) {
38+
$typeInstance = $product->getTypeInstance();
39+
$typeInstance->setStoreFilter($product->getStoreId(), $product);
40+
$options = $typeInstance->getOptions($product);
41+
foreach ($options as $option) {
42+
$optionId = $option->getId();
43+
$selectionsCollection = $typeInstance->getSelectionsCollection(
44+
[$optionId],
45+
$product
46+
);
47+
$selections = $selectionsCollection->exportToArray();
48+
$countSelections = count($selections);
49+
foreach ($selections as $selection) {
50+
$name = 'bundle_option[' . $optionId . ']';
51+
if ($countSelections > 1) {
52+
$name .= '[]';
53+
}
54+
$result[] = [
55+
'name' => $name,
56+
'value' => $selection['selection_id']
57+
];
58+
}
59+
}
60+
}
61+
}
62+
return $result;
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
11+
<actionGroup name="StorefrontAddCategoryBundleProductWithSingleChoiceToCartActionGroup">
12+
<annotations>
13+
<description>Adds a Bundled Product with the single choice to the Cart from the Category page.</description>
14+
</annotations>
15+
<arguments>
16+
<argument name="product"/>
17+
<argument name="quantity" defaultValue="1" type="string"/>
18+
</arguments>
19+
20+
<moveMouseOver selector="{{StorefrontCategoryProductSection.ProductInfoByName(product.name)}}" stepKey="moveMouseOverProduct"/>
21+
<click selector="{{StorefrontCategoryProductSection.ProductAddToCartByName(product.name)}}" stepKey="clickAddToCart"/>
22+
<waitForPageLoad stepKey="waitForPageLoad1"/>
23+
<waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/>
24+
<see selector="{{StorefrontMessagesSection.success}}" userInput="You added {{product.name}} to your shopping cart." stepKey="seeAddToCartSuccessMessage"/>
25+
26+
<!--Open minicart and change Qty-->
27+
<scrollToTopOfPage stepKey="scrollToTheTopOfThePage"/>
28+
<waitForElementVisible selector="{{StorefrontMinicartSection.showCart}}" stepKey="waitForElementToBeVisible"/>
29+
<click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCart"/>
30+
<waitForPageLoad stepKey="waitForPageToLoad2"/>
31+
<waitForElementVisible selector="{{StorefrontMinicartSection.quantity}}" stepKey="waitForElementQty"/>
32+
<pressKey selector="{{StorefrontMinicartSection.itemQuantity(product.name)}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::BACKSPACE]" stepKey="deleteFiled"/>
33+
<fillField selector="{{StorefrontMinicartSection.itemQuantity(product.name)}}" userInput="{{quantity}}" stepKey="changeQty"/>
34+
<conditionalClick selector="{{StorefrontMinicartSection.itemQuantityUpdate(product.name)}}" dependentSelector="{{StorefrontMinicartSection.itemQuantityUpdate(product.name)}}" visible="true" stepKey="updateQty"/>
35+
<waitForAjaxLoad stepKey="waitForAjaxLoad"/>
36+
<waitForText userInput="{{quantity}}" selector="{{StorefrontMinicartSection.productCount}}" time="30" stepKey="assertProductCount"/>
37+
38+
<!-- Close minicart -->
39+
<click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickOnMiniCartToClose"/>
40+
<waitForPageLoad stepKey="waitForPageToLoad3"/>
41+
</actionGroup>
42+
</actionGroups>

app/code/Magento/Bundle/etc/frontend/di.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@
1616
<type name="Magento\Catalog\Model\Product">
1717
<plugin name="add_bundle_child_identities" type="Magento\Bundle\Model\Plugin\Frontend\ProductIdentitiesExtender" sortOrder="100"/>
1818
</type>
19+
<type name="Magento\Catalog\Model\Product\Type\AbstractType">
20+
<plugin name="add_to_cart_single_option" type="Magento\Bundle\Plugin\Catalog\Model\Product\Type\AbstractType" />
21+
</type>
22+
<type name="Magento\Catalog\ViewModel\Product\OptionsData">
23+
<plugin name="add_bundle_options_data" type="Magento\Bundle\Plugin\Catalog\ViewModel\Product\AddBundleOptionsData" />
24+
</type>
1925
</config>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
namespace Magento\Captcha\Plugin;
7+
8+
use Magento\Captcha\Helper\Data as HelperCaptcha;
9+
use Magento\Captcha\Model\ResourceModel\LogFactory;
10+
use Magento\Sales\Api\Data\OrderInterface;
11+
use Magento\Sales\Api\OrderManagementInterface;
12+
13+
/**
14+
* Reset attempts for frontend checkout
15+
*/
16+
class ResetPaymentAttemptsAfterOrderIsPlacedPlugin
17+
{
18+
/**
19+
* Form ID
20+
*/
21+
private const FORM_ID = 'payment_processing_request';
22+
23+
/**
24+
* @var HelperCaptcha
25+
*/
26+
private $helper;
27+
28+
/**
29+
* @var LogFactory
30+
*/
31+
private $resLogFactory;
32+
33+
/**
34+
* ResetPaymentAttemptsAfterOrderIsPlacedPlugin constructor
35+
*
36+
* @param HelperCaptcha $helper
37+
* @param LogFactory $resLogFactory
38+
*/
39+
public function __construct(
40+
HelperCaptcha $helper,
41+
LogFactory $resLogFactory
42+
) {
43+
$this->helper = $helper;
44+
$this->resLogFactory = $resLogFactory;
45+
}
46+
47+
/**
48+
* Reset attempts for frontend checkout
49+
*
50+
* @param OrderManagementInterface $subject
51+
* @param OrderInterface $result
52+
* @param OrderInterface $order
53+
* @return OrderInterface
54+
*/
55+
public function afterPlace(
56+
OrderManagementInterface $subject,
57+
OrderInterface $result,
58+
OrderInterface $order
59+
): OrderInterface {
60+
$captchaModel = $this->helper->getCaptcha(self::FORM_ID);
61+
$captchaModel->setShowCaptchaInSession(false);
62+
$this->resLogFactory->create()->deleteUserAttempts($order->getCustomerEmail());
63+
return $result;
64+
}
65+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
/**
4+
* Copyright © Magento, Inc. All rights reserved.
5+
* See COPYING.txt for license details.
6+
*/
7+
-->
8+
9+
<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd">
11+
<actionGroup name="StorefrontCheckoutPaymentsWithCaptchaActionGroup">
12+
<arguments>
13+
<argument name="captcha" type="string"/>
14+
</arguments>
15+
16+
<fillField selector="{{StorefrontCaptchaOnOnepageCheckoutPyamentSection.captchaField}}" userInput="{{captcha}}" stepKey="fillCaptchaField" />
17+
</actionGroup>
18+
</actionGroups>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Captcha\Test\Unit\Plugin;
10+
11+
use Magento\Captcha\Model\ResourceModel\Log;
12+
use Magento\Captcha\Model\ResourceModel\LogFactory;
13+
use Magento\Captcha\Plugin\ResetPaymentAttemptsAfterOrderIsPlacedPlugin;
14+
use Magento\Captcha\Helper\Data as HelperCaptcha;
15+
use Magento\Captcha\Model\DefaultModel;
16+
use Magento\Sales\Api\Data\OrderInterface;
17+
use Magento\Sales\Api\OrderManagementInterface;
18+
use PHPUnit\Framework\TestCase;
19+
20+
/**
21+
* Unit test for ResetPaymentAttemptsAfterOrderIsPlacedPluginTest
22+
*/
23+
class ResetPaymentAttemptsAfterOrderIsPlacedPluginTest extends TestCase
24+
{
25+
/**
26+
* Test that the method resets attempts for frontend checkout
27+
*/
28+
public function testExecuteExpectsDeleteUserAttemptsCalled()
29+
{
30+
$orderManagementInterfaceMock = $this->getMockForAbstractClass(OrderManagementInterface::class);
31+
$resultOrderMock = $this->createMock(OrderInterface::class);
32+
$orderMock = $this->createMock(OrderInterface::class);
33+
$orderMock->expects($this->once())->method('getCustomerEmail')->willReturn('[email protected]');
34+
$captchaModelMock = $this->createMock(DefaultModel::class);
35+
$captchaModelMock->expects($this->once())->method('setShowCaptchaInSession')->with(false)->willReturnSelf();
36+
$helperCaptchaMock = $this->createMock(HelperCaptcha::class);
37+
$helperCaptchaMock->expects($this->once())->method('getCaptcha')->willReturn($captchaModelMock);
38+
$logMock = $this->createMock(Log::class);
39+
$logMock->expects($this->once())->method('deleteUserAttempts')->willReturnSelf();
40+
$resLogFactoryMock = $this->createMock(LogFactory::class);
41+
$resLogFactoryMock->expects($this->once())->method('create')->willReturn($logMock);
42+
$observer = new ResetPaymentAttemptsAfterOrderIsPlacedPlugin($helperCaptchaMock, $resLogFactoryMock);
43+
$observer->afterPlace($orderManagementInterfaceMock, $resultOrderMock, $orderMock);
44+
}
45+
}

app/code/Magento/Captcha/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"magento/module-backend": "*",
1111
"magento/module-checkout": "*",
1212
"magento/module-customer": "*",
13+
"magento/module-sales": "*",
1314
"magento/module-store": "*",
1415
"magento/module-authorization": "*",
1516
"laminas/laminas-captcha": "^2.10",

app/code/Magento/Captcha/etc/frontend/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@
3434
</argument>
3535
</arguments>
3636
</type>
37+
<type name="Magento\Sales\Api\OrderManagementInterface">
38+
<plugin name="reset_payment_attempts_after_order_is_placed_plugin" type="Magento\Captcha\Plugin\ResetPaymentAttemptsAfterOrderIsPlacedPlugin"/>
39+
</type>
3740
</config>

app/code/Magento/Captcha/etc/frontend/sections.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@
1010
<action name="customer/ajax/login">
1111
<section name="captcha"/>
1212
</action>
13+
<action name="rest/*/V1/carts/*/payment-information">
14+
<section name="captcha"/>
15+
</action>
16+
<action name="rest/*/V1/guest-carts/*/payment-information">
17+
<section name="captcha"/>
18+
</action>
1319
</config>

0 commit comments

Comments
 (0)