diff --git a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php index 18dfb87adeb..1bc9141bd08 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/RuleProductsSelectBuilder.php @@ -168,9 +168,34 @@ public function buildSelect( sprintf($joinCondition, $tableAlias, $storeId), [] ); + + $tierPriceTable = $this->resource->getTableName('catalog_product_entity_tier_price'); + + $select->joinLeft( + ['price_tier' => $tierPriceTable], + '(price_tier.' . $linkField . ' = e.' . $linkField . ')' + . ' AND (price_tier.website_id = ' . $websiteId . ')' + . ' AND (price_tier.customer_group_id = rp.customer_group_id OR price_tier.all_groups = 1)' + . ' AND (price_tier.qty = 1)', + [] + ); + + $select->joinLeft( + ['price_tier0' => $tierPriceTable], + '(price_tier0.' . $linkField . ' = e.' . $linkField . ')' + . ' AND (price_tier0.website_id = 0)' + . ' AND (price_tier0.customer_group_id = rp.customer_group_id OR price_tier0.all_groups = 1)' + . ' AND (price_tier0.qty = 1)', + [] + ); + $select->columns( [ - 'default_price' => $connection->getIfNullSql($tableAlias . '.value', 'pp_default.value'), + 'default_price' => 'LEAST(' + . $connection->getIfNullSql('price_tier0.value', 'pp_default.value') . ',' + . $connection->getIfNullSql('price_tier.value', 'pp_default.value') . ',' + . $connection->getIfNullSql($tableAlias . '.value', 'pp_default.value') + . ')', ] ); diff --git a/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php new file mode 100644 index 00000000000..5486809bcee --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Unit/Model/Indexer/RuleProductsSelectBuilderTest.php @@ -0,0 +1,170 @@ +resource = $this->createMock(ResourceConnection::class); + $this->eavConfig = $this->createMock(Config::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->metadataPool = $this->createMock(MetadataPool::class); + $this->activeTableSwitcher = $this->createMock(ActiveTableSwitcher::class); + $this->tableSwapper = $this->createMock(IndexerTableSwapperInterface::class); + $this->connection = $this->createMock(AdapterInterface::class); + $this->select = $this->createMock(Select::class); + + $this->resource->method('getConnection')->willReturn($this->connection); + $this->resource->method('getTableName')->willReturnCallback(fn($t) => $t); + + $this->connection->method('select')->willReturn($this->select); + $this->connection->method('query')->willReturn( + $this->createMock(\Zend_Db_Statement_Interface::class) + ); + $this->connection->method('getIfNullSql')->willReturnCallback( + fn($a, $b) => "IFNULL($a,$b)" + ); + + $backendMock = $this->createMock(AbstractBackend::class); + $backendMock->method('getTable')->willReturn('catalog_product_entity_decimal'); + + $priceMock = $this->createMock(AbstractAttribute::class); + $priceMock->method('getBackend')->willReturn($backendMock); + $priceMock->method('getId')->willReturn(77); + + $this->eavConfig->method('getAttribute') + ->with(Product::ENTITY, 'price') + ->willReturn($priceMock); + + $metadataMock = $this->createMock(EntityMetadataInterface::class); + $metadataMock->method('getLinkField')->willReturn('entity_id'); + $this->metadataPool->method('getMetadata')->willReturn($metadataMock); + + $websiteMock = $this->createMock(\Magento\Store\Model\Website::class); + $websiteMock->method('getDefaultGroup')->willReturn(null); + $this->storeManager->method('getWebsite')->willReturn($websiteMock); + + $this->select->method('from')->willReturnSelf(); + $this->select->method('order')->willReturnSelf(); + $this->select->method('where')->willReturnSelf(); + $this->select->method('join')->willReturnSelf(); + $this->select->method('joinInner')->willReturnSelf(); + $this->select->method('joinLeft')->willReturnSelf(); + $this->select->method('columns')->willReturnSelf(); + } + + public function testBuildSelectJoinsTierPricesForWebsite(): void + { + $joinedTables = []; + $this->select->method('joinLeft') + ->willReturnCallback(function ($table) use (&$joinedTables) { + $joinedTables[] = array_key_first((array)$table); + return $this->select; + }); + + $this->buildModel()->buildSelect(1, []); + + $this->assertContains('price_tier', $joinedTables, + 'buildSelect must LEFT JOIN catalog_product_entity_tier_price for website-specific tier prices'); + $this->assertContains('price_tier0', $joinedTables, + 'buildSelect must LEFT JOIN catalog_product_entity_tier_price for global (website_id=0) tier prices'); + } + + public function testBuildSelectIncludesTierPricesInLeastExpression(): void + { + $columnsArg = null; + $this->select->method('columns') + ->willReturnCallback(function ($cols) use (&$columnsArg) { + $columnsArg = $cols; + return $this->select; + }); + + $this->buildModel()->buildSelect(1, []); + + $this->assertNotNull($columnsArg); + $this->assertArrayHasKey('default_price', $columnsArg); + $this->assertStringContainsString('LEAST(', $columnsArg['default_price'], + 'default_price must use LEAST() to ensure the rule price never exceeds the tier price'); + $this->assertStringContainsString('price_tier', $columnsArg['default_price'], + 'LEAST() expression must include tier price values'); + } + + public function testBuildSelectTierJoinIncludesAllGroupsCondition(): void + { + $tierJoinConditions = []; + $this->select->method('joinLeft') + ->willReturnCallback(function ($table, $condition) use (&$tierJoinConditions) { + $alias = array_key_first((array)$table); + if (in_array($alias, ['price_tier', 'price_tier0'])) { + $tierJoinConditions[$alias] = $condition; + } + return $this->select; + }); + + $this->buildModel()->buildSelect(1, []); + + foreach (['price_tier', 'price_tier0'] as $alias) { + $this->assertArrayHasKey($alias, $tierJoinConditions); + $this->assertStringContainsString('all_groups', $tierJoinConditions[$alias], + "$alias JOIN condition must handle all_groups=1 tier prices so every customer group benefits"); + } + } + + private function buildModel(): RuleProductsSelectBuilder + { + return new RuleProductsSelectBuilder( + $this->resource, + $this->eavConfig, + $this->storeManager, + $this->metadataPool, + $this->activeTableSwitcher, + $this->tableSwapper + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php index 4d2e40b21ba..03fca6a0f9d 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/Model/Indexer/IndexerBuilderTest.php @@ -240,6 +240,285 @@ public function testReindexFullForSecondStore(): void $this->assertEquals(25, $rulePrice); } + /** + * Regression test: catalog rule indexer must consider tier prices so the + * rule never produces a price higher than an existing tier price. + * + * Setup: + * - Product regular price: $100 + * - All-groups global tier price: $30 (qty=1) + * - Catalog rule: 50% off → $50 without tier consideration + * + * Without the fix the indexed rule price is $50, which is higher than the + * $30 tier price — customers who qualify for the tier see a higher price. + * + * With the fix the indexer uses LEAST(tier, regular) = $30 as the base, + * so 50% → $15, and the rule price ≤ tier price for all customer groups. + * + * @magentoDataFixture Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule.php + */ + public function testReindexFullRulePriceNeverExceedsTierPrice(): void + { + $this->indexerBuilder->reindexFull(); + + $product = $this->productRepository->get('simple-tier-price-rule'); + $productId = (int)$product->getId(); + $websiteId = (int)$this->storeManager->getDefaultStoreView()->getWebsiteId(); + $tierPrice = 30.0; + $regularPrice = 100.0; + $ruleDiscount = 50; // 50% + + $rulePriceOnRegular = $regularPrice * (1 - $ruleDiscount / 100); // $50 + + // Rule price must be ≤ tier price for every customer group in the rule + foreach ([0, 1, 2, 3] as $customerGroupId) { + $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, $customerGroupId, $productId); + + // Without fix: $50 > $30 tier — assertion would fail + $this->assertNotFalse( + $rulePrice, + "No rule price indexed for customer group $customerGroupId" + ); + $this->assertLessThanOrEqual( + $tierPrice, + (float)$rulePrice, + "Customer group $customerGroupId: rule price \$$rulePrice must not exceed tier price \$$tierPrice. " + . "Without the fix the indexer ignores tier prices and returns \$$rulePriceOnRegular." + ); + } + } + + /** + * Verify that when the catalog rule discount produces a price LOWER than + * the tier price, the lower rule price is used (rule wins). + * + * Setup: + * - Product regular price: $100 + * - All-groups global tier price: $80 (qty=1) + * - Catalog rule: 50% off → $50 + * + * Expected: rule price = $40 (50% of LEAST($80,$100)=$80), which is < $80 tier ✓ + * + * @magentoDataFixture Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule.php + */ + public function testReindexFullRulePriceWinsWhenLowerThanTierPrice(): void + { + // Adjust the tier price to $80 so the 50% rule ($50) would win + $product = $this->productRepository->get('simple-tier-price-rule', true, null, true); + + /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierFactory */ + $tierFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); + /** @var \Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory $extFactory */ + $extFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory::class); + + $tier = $tierFactory->create(); + $tier->setCustomerGroupId(\Magento\Customer\Model\Group::CUST_GROUP_ALL); + $tier->setQty(1); + $tier->setValue(80.00); + $tier->setWebsiteId(0); + $tier->setExtensionAttributes($extFactory->create()); + + $product->setTierPrices([$tier]); + $this->productRepository->save($product); + + $this->indexerBuilder->reindexFull(); + + $productId = (int)$product->getId(); + $websiteId = (int)$this->storeManager->getDefaultStoreView()->getWebsiteId(); + + foreach ([0, 1, 2, 3] as $customerGroupId) { + $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, $customerGroupId, $productId); + $this->assertNotFalse($rulePrice, "No rule price indexed for group $customerGroupId"); + $this->assertLessThanOrEqual( + 80.0, + (float)$rulePrice, + "Group $customerGroupId: rule price must not exceed tier price \$80" + ); + } + } + + /** + * Specific customer group tier: only group 1 has a tier price. + * Group 1 rule price must be ≤ tier price; other groups get the regular rule price. + * + * Regular: $100 · Group-1 global tier: $20 · 50% rule for all groups + * Group 1 expected: rule price ≤ $20 (LEAST($20,$100)=$20 → 50%=$10) + * Groups 0,2,3 expected: rule price = $50 (no tier, LEAST=$100 → 50%=$50) + * + * @magentoDataFixture Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule.php + */ + public function testReindexFullSpecificGroupTierOnlyAppliesToThatGroup(): void + { + $om = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + /** @var \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierFactory */ + $tierFactory = $om->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); + $extFactory = $om->get(\Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory::class); + + // Replace the all-groups tier with a group-1-only tier at $20 + $product = $this->productRepository->get('simple-tier-price-rule', true, null, true); + + $tier = $tierFactory->create(); + $tier->setCustomerGroupId(1); // group 1 only, not all groups + $tier->setQty(1); + $tier->setValue(20.00); + $tier->setWebsiteId(0); + $tier->setExtensionAttributes($extFactory->create()); + + $product->setTierPrices([$tier]); + $this->productRepository->save($product); + + $this->indexerBuilder->reindexFull(); + + $productId = (int)$product->getId(); + $websiteId = (int)$this->storeManager->getDefaultStoreView()->getWebsiteId(); + + // Group 1: tier caps the rule + $rulePrice1 = (float)$this->resourceRule->getRulePrice(new \DateTime(), $websiteId, 1, $productId); + $this->assertLessThanOrEqual(20.0, $rulePrice1, 'Group 1 rule price must not exceed its $20 tier price'); + + // Other groups: no tier → rule applies to regular price → $50 + foreach ([0, 2, 3] as $g) { + $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, $g, $productId); + if ($rulePrice !== false) { + $this->assertEqualsWithDelta( + 50.0, + (float)$rulePrice, + 0.01, + "Group $g has no tier price — rule price should be \$50 (50% of \$100)" + ); + } + } + } + + /** + * Website-specific tier price: tier is scoped to website_id=1 (not global). + * Tests the `price_tier` JOIN (website-specific path). + * + * Regular: $100 · Website-1 tier: $25 (all groups, qty=1) · 50% rule + * Expected: LEAST($25 website-tier, null global-tier, $100 regular) = $25 → 50%=$12.50 + * + * @magentoDataFixture Magento/CatalogRule/_files/product_with_website_tier_price_and_50_percent_rule.php + */ + public function testReindexFullWebsiteScopedTierPriceIsRespected(): void + { + $this->indexerBuilder->reindexFull(); + + $product = $this->productRepository->get('simple-website-tier-price-rule'); + $productId = (int)$product->getId(); + $websiteId = (int)$this->storeManager->getDefaultStoreView()->getWebsiteId(); + $tierPrice = 25.0; + + foreach ([0, 1, 2, 3] as $g) { + $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, $g, $productId); + $this->assertNotFalse($rulePrice, "No rule price for group $g"); + $this->assertLessThanOrEqual( + $tierPrice, + (float)$rulePrice, + "Group $g: website-scoped tier \$$tierPrice must cap the indexed rule price" + ); + } + } + + /** + * When both a global tier ($40) and a website-specific tier ($20) exist, + * LEAST must pick the lower one (website-specific $20 wins). + * + * Regular: $100 · Global tier: $40 · Website-1 tier: $20 · 50% rule + * Expected: LEAST($40, $20, $100) = $20 → 50% = $10 ≤ $20 for all groups. + * + * Tier prices are inserted via the DB connection to avoid Magento's model-level + * uniqueness check, which rejects two all-groups tiers for different websites + * even though the DB schema correctly allows it (different website_id). + * + * @magentoDataFixture Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule.php + */ + public function testReindexFullLowestTierWinsWhenBothGlobalAndWebsiteExist(): void + { + $product = $this->productRepository->get('simple-tier-price-rule'); + $productId = (int)$product->getId(); + + // Insert both tiers directly — Magento's model validation rejects two + // all-groups tiers for different websites even though the schema allows it. + $conn = $this->connection->getConnection(); + $tierTable = $this->connection->getTableName('catalog_product_entity_tier_price'); + $conn->delete($tierTable, ['entity_id = ?' => $productId]); + $conn->insert($tierTable, [ + 'entity_id' => $productId, 'all_groups' => 1, 'customer_group_id' => 0, + 'qty' => 1, 'value' => 40.0, 'website_id' => 0, + ]); + $conn->insert($tierTable, [ + 'entity_id' => $productId, 'all_groups' => 1, 'customer_group_id' => 0, + 'qty' => 1, 'value' => 20.0, 'website_id' => 1, + ]); + + $this->indexerBuilder->reindexFull(); + + $websiteId = (int)$this->storeManager->getDefaultStoreView()->getWebsiteId(); + + foreach ([0, 1, 2, 3] as $g) { + $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, $g, $productId); + $this->assertNotFalse($rulePrice, "No rule price for group $g"); + $this->assertLessThanOrEqual( + 20.0, + (float)$rulePrice, + "Group $g: website tier \$20 must be the effective base — LEAST picks \$20 over \$40 global" + ); + } + } + + /** + * Volume discount: tier prices with qty > 1 are intentionally NOT considered + * by the indexer (only qty=1 is used as the base price for single-unit display). + * + * When a product has only qty≥5 tier prices, the catalog rule applies to the + * regular price unchanged. This documents the known limitation. + * + * Regular: $100 · Tier only for qty=5: $20 · 50% rule + * Expected: rule price = $50 (50% of $100 — qty=1 tier not found, regular used) + * + * @magentoDataFixture Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule.php + */ + public function testReindexFullTierPriceForQtyAboveOneIsNotConsideredByIndexer(): void + { + $om = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $tierFactory = $om->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); + $extFactory = $om->get(\Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory::class); + + $product = $this->productRepository->get('simple-tier-price-rule', true, null, true); + + // Only a qty=5 tier price — no qty=1 tier + $tier = $tierFactory->create(); + $tier->setCustomerGroupId(\Magento\Customer\Model\Group::CUST_GROUP_ALL); + $tier->setQty(5); // qty > 1 + $tier->setValue(20.0); + $tier->setWebsiteId(0); + $tier->setExtensionAttributes($extFactory->create()); + + $product->setTierPrices([$tier]); + $this->productRepository->save($product); + + $this->indexerBuilder->reindexFull(); + + $productId = (int)$product->getId(); + $websiteId = (int)$this->storeManager->getDefaultStoreView()->getWebsiteId(); + + foreach ([0, 1, 2, 3] as $g) { + $rulePrice = $this->resourceRule->getRulePrice(new \DateTime(), $websiteId, $g, $productId); + if ($rulePrice !== false) { + // No qty=1 tier — indexer uses regular price $100 → 50% = $50 + $this->assertEqualsWithDelta( + 50.0, + (float)$rulePrice, + 0.01, + "Group $g: qty>1 tier is not considered by the indexer; " + . "rule applies to regular price \$100 → expected \$50" + ); + } + } + } + /** * Returns triggers count. * diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule.php new file mode 100644 index 00000000000..83cd280a973 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule.php @@ -0,0 +1,86 @@ +get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductTierPriceInterfaceFactory $tierPriceFactory */ +$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class); +/** @var ProductTierPriceExtensionFactory $tierExtFactory */ +$tierExtFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +/** @var CatalogRuleRepositoryInterface $catalogRuleRepository */ +$catalogRuleRepository = $objectManager->get(CatalogRuleRepositoryInterface::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); + +// Create product with regular price $100 +$product = $productFactory->create(); +$product->setTypeId('simple') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product With Tier Price') + ->setSku('simple-tier-price-rule') + ->setPrice(100.00) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_in_stock' => 1]); + +// Add all-groups global tier price: $30 for qty=1 +$tierPrice = $tierPriceFactory->create(); +$tierPrice->setCustomerGroupId(Group::CUST_GROUP_ALL); +$tierPrice->setQty(1); +$tierPrice->setValue(30.00); +$tierPrice->setWebsiteId(0); +$tierPrice->setExtensionAttributes($tierExtFactory->create()); + +$product->setTierPrices([$tierPrice]); +$productRepository->save($product); + +// Create 50% off catalog rule for all customer groups on website 1 +$rule = $ruleFactory->create(); +$rule->loadPost([ + 'name' => 'Test Catalog Rule 50% off (tier price test)', + 'is_active' => '1', + 'stop_rules_processing' => 0, + 'website_ids' => [1], + 'customer_group_ids' => [Group::NOT_LOGGED_IN_ID, 1, 2, 3], + 'discount_amount' => 50, + 'simple_action' => 'by_percent', + 'from_date' => '', + 'to_date' => '', + 'sort_order' => 0, + 'sub_is_enable' => 0, + 'sub_discount_amount' => 0, + 'conditions' => [], +]); +$catalogRuleRepository->save($rule); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule_rollback.php new file mode 100644 index 00000000000..5f908bfc793 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_tier_price_and_50_percent_rule_rollback.php @@ -0,0 +1,35 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +// Remove product +try { + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + $productRepository->deleteById('simple-tier-price-rule'); +} catch (\Exception $e) { + // already gone +} + +// Remove catalog rules created by fixture +$ruleCollection = $objectManager->create(RuleCollection::class); +$ruleCollection->addFieldToFilter('name', ['like' => '%tier price test%']); +foreach ($ruleCollection as $rule) { + $rule->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_website_tier_price_and_50_percent_rule.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_website_tier_price_and_50_percent_rule.php new file mode 100644 index 00000000000..e73bb5035c5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_website_tier_price_and_50_percent_rule.php @@ -0,0 +1,74 @@ +get(ProductInterfaceFactory::class); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class); +$tierExtFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +$catalogRuleRepository = $objectManager->get(CatalogRuleRepositoryInterface::class); +$ruleFactory = $objectManager->get(RuleFactory::class); + +$product = $productFactory->create(); +$product->setTypeId('simple') + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Product With Website-Scoped Tier Price') + ->setSku('simple-website-tier-price-rule') + ->setPrice(100.00) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_in_stock' => 1]); + +// Website-scoped tier price: $25 for website_id=1, all groups, qty=1 +$tierPrice = $tierPriceFactory->create(); +$tierPrice->setCustomerGroupId(Group::CUST_GROUP_ALL); +$tierPrice->setQty(1); +$tierPrice->setValue(25.00); +$tierPrice->setWebsiteId(1); // website-specific, not global (0) +$tierPrice->setExtensionAttributes($tierExtFactory->create()); + +$product->setTierPrices([$tierPrice]); +$productRepository->save($product); + +$rule = $ruleFactory->create(); +$rule->loadPost([ + 'name' => 'Test Catalog Rule 50% off (website tier test)', + 'is_active' => '1', + 'stop_rules_processing' => 0, + 'website_ids' => [1], + 'customer_group_ids' => [Group::NOT_LOGGED_IN_ID, 1, 2, 3], + 'discount_amount' => 50, + 'simple_action' => 'by_percent', + 'from_date' => '', + 'to_date' => '', + 'sort_order' => 0, + 'sub_is_enable' => 0, + 'sub_discount_amount' => 0, + 'conditions' => [], +]); +$catalogRuleRepository->save($rule); diff --git a/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_website_tier_price_and_50_percent_rule_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_website_tier_price_and_50_percent_rule_rollback.php new file mode 100644 index 00000000000..25d470058f8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogRule/_files/product_with_website_tier_price_and_50_percent_rule_rollback.php @@ -0,0 +1,30 @@ +get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $objectManager->get(ProductRepositoryInterface::class)->deleteById('simple-website-tier-price-rule'); +} catch (\Exception $e) { +} + +$ruleCollection = $objectManager->create(RuleCollection::class); +$ruleCollection->addFieldToFilter('name', ['like' => '%website tier test%']); +foreach ($ruleCollection as $rule) { + $rule->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false);