Skip to content

Commit 519e018

Browse files
authored
Merge branch '2.4-develop' into 2.4-develop-pr126
2 parents 0c6a51b + 4b0ef30 commit 519e018

File tree

59 files changed

+2262
-114
lines changed

Some content is hidden

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

59 files changed

+2262
-114
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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\Quote\Item;
9+
10+
use Magento\Bundle\Model\Product\Price;
11+
use Magento\Bundle\Model\Product\Type;
12+
use Magento\Catalog\Model\Product;
13+
use Magento\Framework\Serialize\Serializer\Json;
14+
15+
/**
16+
* Bundle product options model
17+
*/
18+
class Option
19+
{
20+
/**
21+
* @var Json
22+
*/
23+
private $serializer;
24+
25+
/**
26+
* @param Json $serializer
27+
*/
28+
public function __construct(
29+
Json $serializer
30+
) {
31+
$this->serializer = $serializer;
32+
}
33+
34+
/**
35+
* Get selection options for provided bundle product
36+
*
37+
* @param Product $product
38+
* @return array
39+
*/
40+
public function getSelectionOptions(Product $product): array
41+
{
42+
$options = [];
43+
$bundleOptionIds = $this->getOptionValueAsArray($product, 'bundle_option_ids');
44+
if ($bundleOptionIds) {
45+
/** @var Type $typeInstance */
46+
$typeInstance = $product->getTypeInstance();
47+
$optionsCollection = $typeInstance->getOptionsByIds($bundleOptionIds, $product);
48+
$selectionIds = $this->getOptionValueAsArray($product, 'bundle_selection_ids');
49+
50+
if ($selectionIds) {
51+
$selectionsCollection = $typeInstance->getSelectionsByIds($selectionIds, $product);
52+
$optionsCollection->appendSelections($selectionsCollection, true);
53+
54+
foreach ($selectionsCollection as $selection) {
55+
$selectionId = $selection->getSelectionId();
56+
$options[$selectionId][] = $this->getBundleSelectionAttributes($product, $selection);
57+
}
58+
}
59+
}
60+
61+
return $options;
62+
}
63+
64+
/**
65+
* Get selection attributes for provided selection
66+
*
67+
* @param Product $product
68+
* @param Product $selection
69+
* @return array
70+
*/
71+
private function getBundleSelectionAttributes(Product $product, Product $selection): array
72+
{
73+
$selectionId = $selection->getSelectionId();
74+
/** @var \Magento\Bundle\Model\Option $bundleOption */
75+
$bundleOption = $selection->getOption();
76+
/** @var Price $priceModel */
77+
$priceModel = $product->getPriceModel();
78+
$price = $priceModel->getSelectionFinalTotalPrice($product, $selection, 0, 1);
79+
$customOption = $product->getCustomOption('selection_qty_' . $selectionId);
80+
$qty = (float)($customOption ? $customOption->getValue() : 0);
81+
82+
return [
83+
'code' => 'bundle_selection_attributes',
84+
'value'=> $this->serializer->serialize(
85+
[
86+
'price' => $price,
87+
'qty' => $qty,
88+
'option_label' => $bundleOption->getTitle(),
89+
'option_id' => $bundleOption->getId(),
90+
]
91+
)
92+
];
93+
}
94+
95+
/**
96+
* Get unserialized value of custom option
97+
*
98+
* @param Product $product
99+
* @param string $code
100+
* @return array
101+
*/
102+
private function getOptionValueAsArray(Product $product, string $code): array
103+
{
104+
$option = $product->getCustomOption($code);
105+
return $option && $option->getValue()
106+
? $this->serializer->unserialize($option->getValue())
107+
: [];
108+
}
109+
}
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+
declare(strict_types=1);
7+
8+
namespace Magento\Bundle\Model\Quote\Item\Option;
9+
10+
use Magento\Framework\DataObject;
11+
use Magento\Framework\Serialize\Serializer\Json;
12+
use Magento\Quote\Model\Quote\Item\Option\ComparatorInterface;
13+
14+
/**
15+
* Bundle quote item option comparator
16+
*/
17+
class BundleSelectionAttributesComparator implements ComparatorInterface
18+
{
19+
/**
20+
* @var Json
21+
*/
22+
private $serializer;
23+
24+
/**
25+
* @param Json $serializer
26+
*/
27+
public function __construct(
28+
Json $serializer
29+
) {
30+
$this->serializer = $serializer;
31+
}
32+
33+
/**
34+
* @inheritdoc
35+
*/
36+
public function compare(DataObject $option1, DataObject $option2): bool
37+
{
38+
$value1 = $option1->getValue() ? $this->serializer->unserialize($option1->getValue()) : [];
39+
$value2 = $option2->getValue() ? $this->serializer->unserialize($option2->getValue()) : [];
40+
$option1Id = isset($value1['option_id']) ? (int) $value1['option_id'] : null;
41+
$option2Id = isset($value2['option_id']) ? (int) $value2['option_id'] : null;
42+
43+
return $option1Id === $option2Id;
44+
}
45+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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\Quote;
9+
10+
use Magento\Bundle\Model\Product\Type;
11+
use Magento\Bundle\Model\Quote\Item\Option;
12+
use Magento\Quote\Model\Quote;
13+
use Magento\Quote\Model\Quote\Item;
14+
use Magento\Quote\Model\QuoteManagement;
15+
16+
/**
17+
* Update bundle selection custom options
18+
*/
19+
class UpdateBundleQuoteItemOptions
20+
{
21+
/**
22+
* @var Option
23+
*/
24+
private $option;
25+
26+
/**
27+
* @param Option $option
28+
*/
29+
public function __construct(
30+
Option $option
31+
) {
32+
$this->option = $option;
33+
}
34+
35+
/**
36+
* Update bundle selection custom options before order is placed
37+
*
38+
* @param QuoteManagement $subject
39+
* @param Quote $quote
40+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
41+
*/
42+
public function beforeSubmit(
43+
QuoteManagement $subject,
44+
Quote $quote,
45+
array $orderData = []
46+
): void {
47+
foreach ($quote->getAllVisibleItems() as $quoteItem) {
48+
if ($quoteItem->getProductType() === Type::TYPE_CODE) {
49+
$options = $this->option->getSelectionOptions($quoteItem->getProduct());
50+
foreach ($quoteItem->getChildren() as $childItem) {
51+
/** @var Item $childItem */
52+
$customOption = $childItem->getOptionByCode('selection_id');
53+
$selectionId = $customOption ? $customOption->getValue() : null;
54+
if ($selectionId && isset($options[$selectionId])) {
55+
$childItem->setOptions($options[$selectionId]);
56+
}
57+
}
58+
}
59+
}
60+
}
61+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
11+
<section name="AdminOrderItemsOrderedSection">
12+
<element name="orderItemOptionLabel" type="text" selector=".edit-order-table tr:nth-of-type({{row}}) .col-product .option-label" parameterized="true"/>
13+
<element name="orderItemOptionValue" type="text" selector=".edit-order-table tr:nth-of-type({{row}}) .col-product .option-value" parameterized="true"/>
14+
<element name="orderItemOptionPrice" type="text" selector=".edit-order-table tr:nth-of-type({{row}}) .col-product .option-value .price" parameterized="true"/>
15+
</section>
16+
</sections>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
10+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd">
11+
<section name="StorefrontCustomerOrderSection">
12+
<element name="orderItemOptionLabel" type="text" selector=".table-order-items tr:nth-of-type({{row}}) td.label" parameterized="true"/>
13+
<element name="orderItemOptionValue" type="text" selector=".table-order-items tr:nth-of-type({{row}}) td.value" parameterized="true"/>
14+
</section>
15+
</sections>
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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+
<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
9+
xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd">
10+
<test name="StorefrontPlaceOrderBundleProductFixedPriceWithUpdatedPriceTest">
11+
<annotations>
12+
<features value="Bundle"/>
13+
<stories value="Placing order with bundle product"/>
14+
<title value="Order details with bundle product fixed price should show the correct price for bundle items"/>
15+
<description value="Order details with bundle product fixed price should show the correct price for bundle items"/>
16+
<severity value="MAJOR"/>
17+
<useCaseId value="MC-40603"/>
18+
<testCaseId value="MC-40744"/>
19+
<group value="bundle"/>
20+
<group value="catalog"/>
21+
</annotations>
22+
23+
<before>
24+
<createData entity="CustomerEntityOne" stepKey="createCustomer"/>
25+
<createData entity="SimpleProduct2" stepKey="createFirstProduct"/>
26+
<createData entity="SimpleProduct2" stepKey="createSecondProduct"/>
27+
<createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct">
28+
<field key="price">11.00</field>
29+
</createData>
30+
<createData entity="RadioButtonsOption" stepKey="createFirstBundleOption">
31+
<field key="position">1</field>
32+
<requiredEntity createDataKey="createFixedBundleProduct"/>
33+
</createData>
34+
<createData entity="RadioButtonsOption" stepKey="createSecondBundleOption">
35+
<field key="position">2</field>
36+
<requiredEntity createDataKey="createFixedBundleProduct"/>
37+
</createData>
38+
<createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct">
39+
<requiredEntity createDataKey="createFixedBundleProduct"/>
40+
<requiredEntity createDataKey="createFirstBundleOption"/>
41+
<requiredEntity createDataKey="createFirstProduct"/>
42+
<field key="price_type">0</field>
43+
<field key="price">7.00</field>
44+
</createData>
45+
<createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct">
46+
<requiredEntity createDataKey="createFixedBundleProduct"/>
47+
<requiredEntity createDataKey="createSecondBundleOption"/>
48+
<requiredEntity createDataKey="createSecondProduct"/>
49+
<field key="price_type">0</field>
50+
<field key="price">5.00</field>
51+
</createData>
52+
<actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/>
53+
<actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage">
54+
<argument name="productId" value="$createFixedBundleProduct.id$"/>
55+
</actionGroup>
56+
<actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/>
57+
</before>
58+
<after>
59+
<deleteData createDataKey="createFirstProduct" stepKey="deleteSimpleProductForBundleItem"/>
60+
<deleteData createDataKey="createSecondProduct" stepKey="deleteVirtualProductForBundleItem"/>
61+
<deleteData createDataKey="createFixedBundleProduct" stepKey="deleteBundleProduct"/>
62+
<deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/>
63+
<actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/>
64+
<waitForPageLoad stepKey="waitForClearProductsGridFilters"/>
65+
</after>
66+
<!--Login customer on storefront-->
67+
<actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer">
68+
<argument name="Customer" value="$$createCustomer$$" />
69+
</actionGroup>
70+
<!--Open Product Page-->
71+
<actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openBundleProductPage">
72+
<argument name="product" value="$createFixedBundleProduct$"/>
73+
</actionGroup>
74+
<!--Add bundle to cart-->
75+
<actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"/>
76+
<actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart">
77+
<argument name="quantity" value="1"/>
78+
</actionGroup>
79+
<!--Open bundle product in admin-->
80+
<actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage">
81+
<argument name="productId" value="$createFixedBundleProduct.id$"/>
82+
</actionGroup>
83+
<!--Change price of the first option-->
84+
<fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYPrice('0', '0')}}" userInput="9" stepKey="fillBundleOption1Price"/>
85+
<!--Save the bundle product-->
86+
<actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/>
87+
<!--Open Product Page-->
88+
<actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openBundleProductPage2">
89+
<argument name="product" value="$createFixedBundleProduct$"/>
90+
</actionGroup>
91+
<!--Add bundle to cart-->
92+
<actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart2"/>
93+
<actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart2">
94+
<argument name="quantity" value="1"/>
95+
</actionGroup>
96+
<!--Verify bundle product details-->
97+
<actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCart"/>
98+
<see selector="{{StorefrontBundledSection.nthItemOptionsTitle('1')}}" userInput="$$createFirstBundleOption.title$$" stepKey="seeOptionLabelInShoppingCart"/>
99+
<see selector="{{StorefrontBundledSection.nthItemOptionsValue('1')}}" userInput="1 x $$createFirstProduct.name$$ $9.00" stepKey="seeOptionValueInShoppingCart"/>
100+
<see selector="{{StorefrontBundledSection.nthItemOptionsTitle('2')}}" userInput="$$createSecondBundleOption.title$$" stepKey="seeOption2LabelInShoppingCart"/>
101+
<see selector="{{StorefrontBundledSection.nthItemOptionsValue('2')}}" userInput="1 x $$createSecondProduct.name$$ $5.00" stepKey="seeOption2ValueInShoppingCart"/>
102+
<!--Verify total-->
103+
<grabTextFrom selector="{{CheckoutCartSummarySection.total}}" stepKey="grabShoppingCartTotal"/>
104+
<assertEquals stepKey="verifyGrandTotalOnShoppingCartPage">
105+
<actualResult type="variable">grabShoppingCartTotal</actualResult>
106+
<expectedResult type="string">$60.00</expectedResult>
107+
</assertEquals>
108+
109+
<!--Navigate to checkout-->
110+
<actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage"/>
111+
<!--Click next button to open payment section-->
112+
<actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/>
113+
<!--Click place order-->
114+
<actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/>
115+
<grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/>
116+
<!--Navigate to order details page in custom account-->
117+
<amOnPage url="{{StorefrontCustomerOrderViewPage.url({$grabOrderNumber})}}" stepKey="amOnOrderPage"/>
118+
<!--Verify bundle order items details-->
119+
<see selector="{{StorefrontCustomerOrderSection.orderItemOptionLabel('2')}}" userInput="$$createFirstBundleOption.title$$" stepKey="seeOptionLabelInCustomerOrderItems"/>
120+
<see selector="{{StorefrontCustomerOrderSection.orderItemOptionValue('3')}}" userInput="1 x $$createFirstProduct.name$$ $9.00" stepKey="seeOptionValueInCustomerOrderItems"/>
121+
<see selector="{{StorefrontCustomerOrderSection.orderItemOptionLabel('4')}}" userInput="$$createSecondBundleOption.title$$" stepKey="seeOption2LabelInCustomerOrderItems"/>
122+
<see selector="{{StorefrontCustomerOrderSection.orderItemOptionValue('5')}}" userInput="1 x $$createSecondProduct.name$$ $5.00" stepKey="seeOption2ValueInCustomerOrderItems"/>
123+
<!--Navigate to order details page on admin-->
124+
<actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrdersGridById">
125+
<argument name="orderId" value="{$grabOrderNumber}"/>
126+
</actionGroup>
127+
<!--Verify bundle order items details-->
128+
<see selector="{{AdminOrderItemsOrderedSection.orderItemOptionLabel('2')}}" userInput="$$createFirstBundleOption.title$$" stepKey="seeOptionLabelInAdminOrderItems"/>
129+
<see selector="{{AdminOrderItemsOrderedSection.orderItemOptionValue('3')}}" userInput="1 x $$createFirstProduct.name$$" stepKey="seeOptionValueInAdminOrderItems"/>
130+
<see selector="{{AdminOrderItemsOrderedSection.orderItemOptionPrice('3')}}" userInput="$9.00" stepKey="seeOptionPriceInAdminOrderItems"/>
131+
<see selector="{{AdminOrderItemsOrderedSection.orderItemOptionLabel('4')}}" userInput="$$createSecondBundleOption.title$$" stepKey="seeOption2LabelInAdminOrderItems"/>
132+
<see selector="{{AdminOrderItemsOrderedSection.orderItemOptionValue('5')}}" userInput="1 x $$createSecondProduct.name$$" stepKey="seeOption2ValueInAdminOrderItems"/>
133+
<see selector="{{AdminOrderItemsOrderedSection.orderItemOptionPrice('5')}}" userInput="$5.00" stepKey="seeOption2PriceInAdminOrderItems"/>
134+
<!--Verify total-->
135+
<grabTextFrom selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="grabAdminOrderTotal"/>
136+
<assertEquals stepKey="verifyGrandTotalOnAdminOrderPage">
137+
<actualResult type="variable">grabAdminOrderTotal</actualResult>
138+
<expectedResult type="string">$60.00</expectedResult>
139+
</assertEquals>
140+
</test>
141+
</tests>

0 commit comments

Comments
 (0)