Skip to content

Commit 2c09a86

Browse files
MC-30809: Indexer price calculation for configurable product
1 parent 6b8f542 commit 2c09a86

File tree

9 files changed

+526
-92
lines changed

9 files changed

+526
-92
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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\TestFramework\Catalog\Model\Product\Price;
9+
10+
use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer;
11+
use Magento\Catalog\Model\ResourceModel\Product as ProductResource;
12+
13+
/**
14+
* Search and return price data from price index table.
15+
*/
16+
class GetDataFromIndexTable
17+
{
18+
/**
19+
* @var ProductResource
20+
*/
21+
private $productResource;
22+
23+
/**
24+
* @param ProductResource $productResource
25+
*/
26+
public function __construct(
27+
ProductResource $productResource
28+
) {
29+
$this->productResource = $productResource;
30+
}
31+
32+
/**
33+
* Returns price data by product id.
34+
*
35+
* @param int $productId
36+
* @param int|null $groupId
37+
* @param int|null $websiteId
38+
* @return array
39+
*/
40+
public function execute(int $productId, ?int $groupId = null, ?int $websiteId = null): array
41+
{
42+
$select = $this->productResource->getConnection()->select()
43+
->from($this->productResource->getTable(TableMaintainer::MAIN_INDEX_TABLE))
44+
->where('entity_id = ?', $productId);
45+
if (isset($groupId)) {
46+
$select->where('customer_group_id = ?', $groupId);
47+
}
48+
if (isset($websiteId)) {
49+
$select->where('website_id = ?', $websiteId);
50+
}
51+
52+
return $this->productResource->getConnection()->fetchAll($select);
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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\CatalogRuleConfigurable\Model\Product\Type\Configurable;
9+
10+
use Magento\Catalog\Api\Data\ProductInterface;
11+
use Magento\Catalog\Api\ProductRepositoryInterface;
12+
use Magento\Catalog\Model\Product\Type\AbstractType;
13+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Price;
14+
use Magento\Customer\Model\Group;
15+
use Magento\Framework\ObjectManagerInterface;
16+
use Magento\TestFramework\Catalog\Model\Product\Price\GetDataFromIndexTable;
17+
use Magento\TestFramework\Helper\Bootstrap;
18+
use PHPUnit\Framework\TestCase;
19+
20+
/**
21+
* Provides tests for configurable product pricing with catalog rules.
22+
*
23+
* @magentoDbIsolation disabled
24+
* @magentoAppArea frontend
25+
*/
26+
class PriceTest extends TestCase
27+
{
28+
/**
29+
* @var ObjectManagerInterface
30+
*/
31+
private $objectManager;
32+
33+
/**
34+
* @var ProductRepositoryInterface
35+
*/
36+
private $productRepository;
37+
38+
/**
39+
* @var Price
40+
*/
41+
private $priceModel;
42+
43+
/**
44+
* @var GetDataFromIndexTable
45+
*/
46+
private $getDataFromIndexTable;
47+
48+
/**
49+
* @inheritdoc
50+
*/
51+
protected function setUp()
52+
{
53+
$this->objectManager = Bootstrap::getObjectManager();
54+
$this->priceModel = $this->objectManager->create(Price::class);
55+
$this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class);
56+
$this->productRepository->cleanCache();
57+
$this->getDataFromIndexTable = $this->objectManager->get(GetDataFromIndexTable::class);
58+
}
59+
60+
/**
61+
* @magentoDataFixture Magento/CatalogRuleConfigurable/_files/configurable_product_with_percent_rule.php
62+
* @return void
63+
*/
64+
public function testGetFinalPriceWithCustomOptionAndCatalogRule(): void
65+
{
66+
$this->assertConfigurableProductPrice(
67+
20,
68+
25,
69+
['price' => 10, 'final_price' => 9, 'min_price' => 9, 'max_price' => 9, 'tier_price' => null],
70+
['price' => 20, 'final_price' => 15, 'min_price' => 15, 'max_price' => 15, 'tier_price' => 15]
71+
);
72+
}
73+
74+
/**
75+
* @magentoDataFixture Magento/CatalogRuleConfigurable/_files/configurable_product_with_percent_rules_for_children.php
76+
* @return void
77+
*/
78+
public function testGetFinalPriceWithCustomOptionAndCatalogRulesForChildren(): void
79+
{
80+
$this->assertConfigurableProductPrice(
81+
19.5,
82+
23,
83+
['price' => 10, 'final_price' => 4.5, 'min_price' => 4.5, 'max_price' => 9, 'tier_price' => null],
84+
['price' => 20, 'final_price' => 8, 'min_price' => 8, 'max_price' => 15, 'tier_price' => 15]
85+
);
86+
}
87+
88+
/**
89+
* Asserts configurable product prices.
90+
*
91+
* @param float $priceWithFirstSimple
92+
* @param float $priceWithSecondSimple
93+
* @param array $firstSimplePrices
94+
* @param array $secondSimplePrices
95+
* @return void
96+
*/
97+
private function assertConfigurableProductPrice(
98+
float $priceWithFirstSimple,
99+
float $priceWithSecondSimple,
100+
array $firstSimplePrices,
101+
array $secondSimplePrices
102+
): void {
103+
$configurable = $this->productRepository->get('configurable');
104+
$this->assertIndexTableData('simple_10', $firstSimplePrices);
105+
$this->assertIndexTableData('simple_20', $secondSimplePrices);
106+
//Add tier price option
107+
$optionId = $configurable->getOptions()[0]->getId();
108+
$configurable->addCustomOption(AbstractType::OPTION_PREFIX . $optionId, 'text');
109+
$configurable->addCustomOption('option_ids', $optionId);
110+
//First simple rule price + Option price
111+
$this->assertFinalPrice($priceWithFirstSimple, $configurable);
112+
$configurable->addCustomOption('simple_product', 20, $this->productRepository->get('simple_20'));
113+
//Second simple rule price + Option price
114+
$this->assertFinalPrice($priceWithSecondSimple, $configurable);
115+
}
116+
117+
/**
118+
* Asserts product final price.
119+
*
120+
* @param float $expectedPrice
121+
* @param ProductInterface $product
122+
* @return void
123+
*/
124+
private function assertFinalPrice(float $expectedPrice, ProductInterface $product): void
125+
{
126+
$this->assertEquals(
127+
round($expectedPrice, 2),
128+
round($this->priceModel->getFinalPrice(1, $product), 2)
129+
);
130+
}
131+
132+
/**
133+
* Asserts price data in index table.
134+
*
135+
* @param string $sku
136+
* @param array $expectedPrices
137+
* @return void
138+
*/
139+
private function assertIndexTableData(string $sku, array $expectedPrices): void
140+
{
141+
$data = $this->getDataFromIndexTable->execute(
142+
(int)$this->productRepository->get($sku)->getId(),
143+
Group::NOT_LOGGED_IN_ID
144+
);
145+
$data = reset($data);
146+
foreach ($expectedPrices as $column => $price) {
147+
$this->assertEquals($price, $data[$column], $column);
148+
}
149+
}
150+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
use Magento\CatalogRule\Api\CatalogRuleRepositoryInterface;
9+
use Magento\CatalogRule\Model\Rule;
10+
use Magento\CatalogRule\Model\Rule\Condition\Combine;
11+
use Magento\CatalogRule\Model\Rule\Condition\Product;
12+
use Magento\CatalogRule\Model\RuleFactory;
13+
use Magento\Customer\Model\Group;
14+
use Magento\Store\Api\WebsiteRepositoryInterface;
15+
use Magento\Store\Model\StoreManagerInterface;
16+
use Magento\TestFramework\Helper\Bootstrap;
17+
18+
require __DIR__ . '/../../ConfigurableProduct/_files/configurable_product_with_custom_option_and_simple_tier_price.php';
19+
Bootstrap::getInstance()->loadArea('adminhtml');
20+
21+
/** @var StoreManagerInterface $storeManager */
22+
$storeManager = $objectManager->get(StoreManagerInterface::class);
23+
/** @var WebsiteRepositoryInterface $websiteRepository */
24+
$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class);
25+
/** @var CatalogRuleRepositoryInterface $ruleRepository */
26+
$ruleRepository = $objectManager->get(CatalogRuleRepositoryInterface::class);
27+
/** @var Rule $rule */
28+
$rule = $objectManager->get(RuleFactory::class)->create();
29+
$rule->loadPost(
30+
[
31+
'name' => 'Percent rule for configurable product',
32+
'is_active' => '1',
33+
'stop_rules_processing' => 0,
34+
'website_ids' => [$websiteRepository->get('base')->getId()],
35+
'customer_group_ids' => Group::NOT_LOGGED_IN_ID,
36+
'discount_amount' => 50,
37+
'simple_action' => 'by_percent',
38+
'from_date' => '',
39+
'to_date' => '',
40+
'sort_order' => 0,
41+
'sub_is_enable' => 0,
42+
'sub_discount_amount' => 0,
43+
'conditions' => [
44+
'1' => ['type' => Combine::class, 'aggregator' => 'all', 'value' => '1', 'new_child' => ''],
45+
'1--1' => ['type' => Product::class, 'attribute' => 'sku', 'operator' => '==', 'value' => 'configurable'],
46+
],
47+
]
48+
);
49+
$ruleRepository->save($rule);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
use Magento\CatalogRule\Api\CatalogRuleRepositoryInterface;
9+
use Magento\CatalogRule\Model\Indexer\IndexBuilder;
10+
use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory;
11+
use Magento\CatalogRule\Model\Rule;
12+
use Magento\TestFramework\Helper\Bootstrap;
13+
14+
$objectManager = Bootstrap::getObjectManager();
15+
/** @var IndexBuilder $indexBuilder */
16+
$indexBuilder = $objectManager->get(IndexBuilder::class);
17+
/** @var CatalogRuleRepositoryInterface $ruleRepository */
18+
$ruleRepository = $objectManager->create(CatalogRuleRepositoryInterface::class);
19+
/** @var CollectionFactory $ruleCollectionFactory */
20+
$ruleCollectionFactory = $objectManager->get(CollectionFactory::class);
21+
$ruleCollection = $ruleCollectionFactory->create();
22+
$ruleCollection->addFieldToFilter('name', ['eq' => 'Percent rule for configurable product']);
23+
$ruleCollection->setPageSize(1);
24+
/** @var Rule $rule */
25+
$rule = $ruleCollection->getFirstItem();
26+
if ($rule->getId()) {
27+
$ruleRepository->delete($rule);
28+
}
29+
$indexBuilder->reindexFull();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
use Magento\CatalogRule\Api\CatalogRuleRepositoryInterface;
9+
use Magento\CatalogRule\Model\Rule;
10+
use Magento\CatalogRule\Model\Rule\Condition\Combine;
11+
use Magento\CatalogRule\Model\Rule\Condition\Product;
12+
use Magento\CatalogRule\Model\RuleFactory;
13+
use Magento\Customer\Model\Group;
14+
15+
require __DIR__ . '/configurable_product_with_percent_rule.php';
16+
17+
/** @var CatalogRuleRepositoryInterface $ruleRepository */
18+
$ruleRepository = $objectManager->get(CatalogRuleRepositoryInterface::class);
19+
/** @var Rule $firstRule */
20+
$ruleFactory = $objectManager->get(RuleFactory::class);
21+
22+
$firstRule = $ruleFactory->create();
23+
$firstRule->loadPost(
24+
[
25+
'name' => 'Percent rule for first simple product',
26+
'is_active' => '1',
27+
'stop_rules_processing' => 0,
28+
'website_ids' => [$websiteRepository->get('base')->getId()],
29+
'customer_group_ids' => Group::NOT_LOGGED_IN_ID,
30+
'discount_amount' => 10,
31+
'simple_action' => 'by_percent',
32+
'from_date' => '',
33+
'to_date' => '',
34+
'sort_order' => 0,
35+
'sub_is_enable' => 0,
36+
'sub_discount_amount' => 0,
37+
'conditions' => [
38+
'1' => ['type' => Combine::class, 'aggregator' => 'all', 'value' => '1', 'new_child' => ''],
39+
'1--1' => ['type' => Product::class, 'attribute' => 'sku', 'operator' => '==', 'value' => 'simple_10'],
40+
],
41+
]
42+
);
43+
$ruleRepository->save($firstRule);
44+
/** @var Rule $secondRule */
45+
$secondRule = $ruleFactory->create();
46+
$secondRule->loadPost(
47+
[
48+
'name' => 'Percent rule for second simple product',
49+
'is_active' => '1',
50+
'stop_rules_processing' => 0,
51+
'website_ids' => [$websiteRepository->get('base')->getId()],
52+
'customer_group_ids' => Group::NOT_LOGGED_IN_ID,
53+
'discount_amount' => 20,
54+
'simple_action' => 'by_percent',
55+
'from_date' => '',
56+
'to_date' => '',
57+
'sort_order' => 0,
58+
'sub_is_enable' => 0,
59+
'sub_discount_amount' => 0,
60+
'conditions' => [
61+
'1' => ['type' => Combine::class, 'aggregator' => 'all', 'value' => '1', 'new_child' => ''],
62+
'1--1' => ['type' => Product::class, 'attribute' => 'sku', 'operator' => '==', 'value' => 'simple_20'],
63+
],
64+
]
65+
);
66+
$ruleRepository->save($secondRule);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
use Magento\CatalogRule\Api\CatalogRuleRepositoryInterface;
9+
use Magento\CatalogRule\Model\Indexer\IndexBuilder;
10+
use Magento\CatalogRule\Model\ResourceModel\Rule\CollectionFactory;
11+
use Magento\TestFramework\Helper\Bootstrap;
12+
13+
$objectManager = Bootstrap::getObjectManager();
14+
/** @var IndexBuilder $indexBuilder */
15+
$indexBuilder = $objectManager->get(IndexBuilder::class);
16+
/** @var CatalogRuleRepositoryInterface $ruleRepository */
17+
$ruleRepository = $objectManager->create(CatalogRuleRepositoryInterface::class);
18+
/** @var CollectionFactory $ruleCollectionFactory */
19+
$ruleCollectionFactory = $objectManager->get(CollectionFactory::class);
20+
$ruleCollection = $ruleCollectionFactory->create();
21+
$ruleCollection->addFieldToFilter(
22+
'name',
23+
[
24+
'in' => [
25+
'Percent rule for configurable product',
26+
'Percent rule for first simple product',
27+
'Percent rule for second simple product',
28+
]
29+
]
30+
);
31+
foreach ($ruleCollection as $rule) {
32+
$ruleRepository->delete($rule);
33+
}
34+
$indexBuilder->reindexFull();

0 commit comments

Comments
 (0)