Skip to content

Commit c751875

Browse files
Merge remote-tracking branch '39690/fix-for-issue-39169' into comprs_june
2 parents 36b2198 + bd6ef72 commit c751875

File tree

8 files changed

+184
-10
lines changed

8 files changed

+184
-10
lines changed

app/code/Magento/Bundle/Model/Product/Price.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Magento\Framework\Pricing\PriceCurrencyInterface;
1010
use Magento\Framework\App\ObjectManager;
1111
use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory;
12+
use Magento\Catalog\Model\Pricing\SpecialPriceService;
1213

1314
/**
1415
* Bundle product type price model
@@ -66,6 +67,7 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price
6667
* @param \Magento\Catalog\Helper\Data $catalogData
6768
* @param \Magento\Framework\Serialize\Serializer\Json|null $serializer
6869
* @param ProductTierPriceExtensionFactory|null $tierPriceExtensionFactory
70+
* @param SpecialPriceService|null $specialPriceService
6971
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
7072
*/
7173
public function __construct(
@@ -80,7 +82,8 @@ public function __construct(
8082
\Magento\Framework\App\Config\ScopeConfigInterface $config,
8183
\Magento\Catalog\Helper\Data $catalogData,
8284
?\Magento\Framework\Serialize\Serializer\Json $serializer = null,
83-
?ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null
85+
?ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null,
86+
?SpecialPriceService $specialPriceService = null
8487
) {
8588
$this->_catalogData = $catalogData;
8689
$this->serializer = $serializer ?: ObjectManager::getInstance()
@@ -95,7 +98,8 @@ public function __construct(
9598
$groupManagement,
9699
$tierPriceFactory,
97100
$config,
98-
$tierPriceExtensionFactory
101+
$tierPriceExtensionFactory,
102+
$specialPriceService
99103
);
100104
}
101105

@@ -629,6 +633,9 @@ public function calculateSpecialPrice(
629633
$store = null
630634
) {
631635
if ($specialPrice !== null && $specialPrice != false) {
636+
637+
$specialPriceTo = $this->getSpecialPriceService()->execute($specialPriceTo);
638+
632639
if ($this->_localeDate->isScopeDateInInterval($store, $specialPriceFrom, $specialPriceTo)) {
633640
$specialPrice = $finalPrice * ($specialPrice / 100);
634641
$finalPrice = min($finalPrice, $specialPrice);

app/code/Magento/Bundle/Test/Unit/Model/Product/PriceTest.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory;
1515
use Magento\Catalog\Helper\Data;
1616
use Magento\Catalog\Model\Product;
17+
use Magento\Catalog\Model\Pricing\SpecialPriceService;
1718
use Magento\CatalogRule\Model\ResourceModel\RuleFactory;
1819
use Magento\Customer\Api\GroupManagementInterface;
1920
use Magento\Customer\Model\Session;
@@ -135,6 +136,17 @@ function ($value) {
135136
->onlyMethods(['create'])
136137
->disableOriginalConstructor()
137138
->getMock();
139+
140+
$specialPriceService = $this->getMockBuilder(SpecialPriceService::class)
141+
->disableOriginalConstructor()
142+
->getMock();
143+
144+
$specialPriceService->expects($this->any())
145+
->method('execute')
146+
->willReturnCallback(function ($value) {
147+
return $value;
148+
});
149+
138150
$objectManagerHelper = new ObjectManagerHelper($this);
139151
$this->model = $objectManagerHelper->getObject(
140152
Price::class,
@@ -150,7 +162,8 @@ function ($value) {
150162
'config' => $scopeConfig,
151163
'catalogData' => $this->catalogHelperMock,
152164
'serializer' => $this->serializer,
153-
'tierPriceExtensionFactory' => $tierPriceExtensionFactoryMock
165+
'tierPriceExtensionFactory' => $tierPriceExtensionFactoryMock,
166+
'specialPriceService' => $specialPriceService
154167
]
155168
);
156169
}

app/code/Magento/Bundle/Test/Unit/Pricing/Price/SpecialPriceTest.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Magento\Bundle\Pricing\Price\SpecialPrice;
1111
use Magento\Catalog\Model\Product;
1212
use Magento\Catalog\Pricing\Price\RegularPrice;
13+
use Magento\Catalog\Model\Pricing\SpecialPriceService;
1314
use Magento\Framework\Pricing\Price\PriceInterface;
1415
use Magento\Framework\Pricing\PriceCurrencyInterface;
1516
use Magento\Framework\Pricing\PriceInfo\Base;
@@ -47,6 +48,11 @@ class SpecialPriceTest extends TestCase
4748
*/
4849
protected $priceCurrencyMock;
4950

51+
/**
52+
* @var SpecialPriceService|MockObject
53+
*/
54+
private $specialPriceService;
55+
5056
protected function setUp(): void
5157
{
5258
$this->saleable = $this->getMockBuilder(Product::class)
@@ -62,13 +68,18 @@ protected function setUp(): void
6268

6369
$this->priceCurrencyMock = $this->getMockForAbstractClass(PriceCurrencyInterface::class);
6470

71+
$this->specialPriceService = $this->getMockBuilder(SpecialPriceService::class)
72+
->disableOriginalConstructor()
73+
->getMock();
74+
6575
$objectHelper = new ObjectManager($this);
6676
$this->model = $objectHelper->getObject(
6777
SpecialPrice::class,
6878
[
6979
'saleableItem' => $this->saleable,
7080
'localeDate' => $this->localeDate,
71-
'priceCurrency' => $this->priceCurrencyMock
81+
'priceCurrency' => $this->priceCurrencyMock,
82+
'specialPriceService' => $this->specialPriceService
7283
]
7384
);
7485
}
@@ -102,6 +113,11 @@ public function testGetValue($regularPrice, $specialPrice, $isScopeDateInInterva
102113
->with(WebsiteInterface::ADMIN_CODE, $specialFromDate, $specialToDate)
103114
->willReturn($isScopeDateInInterval);
104115

116+
$this->specialPriceService->expects($this->once())
117+
->method('execute')
118+
->with($specialToDate)
119+
->willReturn($specialToDate);
120+
105121
$this->priceCurrencyMock->expects($this->never())
106122
->method('convertAndRound');
107123

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\Model\Pricing;
9+
10+
/**
11+
* This class provides functionality to normalize the end date/time of special prices
12+
*/
13+
class SpecialPriceService
14+
{
15+
/**
16+
* This class subtracts one day from $dateTo if it contains a specific time (hours, minutes, seconds)
17+
* because \Magento\Framework\Stdlib\DateTime\Timezone::isScopeDateInInterval adds one day.
18+
* This ensures that the special price expires exactly at the specified time
19+
*
20+
* For example,
21+
* - If $dateTo is "2025-05-12 17:00:00", it will be converted to "2025-05-11 17:00:00"
22+
* - If $dateTo is "2024-05-12 00:00:00", it will remain unchanged
23+
*
24+
* @param mixed $dateTo
25+
* @return mixed|string
26+
*/
27+
public function execute(mixed $dateTo): mixed
28+
{
29+
if ($dateTo
30+
&& strtotime($dateTo) !== false
31+
&& date('H:i:s', strtotime($dateTo)) !== '00:00:00') {
32+
$dateToTimestamp = strtotime($dateTo);
33+
$dateTo = date('Y-m-d H:i:s', $dateToTimestamp - 86400);
34+
}
35+
36+
return $dateTo;
37+
}
38+
}

app/code/Magento/Catalog/Model/Product/Type/Price.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory;
1616
use Magento\Framework\App\ObjectManager;
1717
use Magento\Store\Api\Data\WebsiteInterface;
18+
use Magento\Catalog\Model\Pricing\SpecialPriceService;
1819

1920
/**
2021
* Product type price model
@@ -88,6 +89,11 @@ class Price implements ResetAfterRequestInterface
8889
*/
8990
private $tierPriceExtensionFactory;
9091

92+
/**
93+
* @var SpecialPriceService|null
94+
*/
95+
private ?SpecialPriceService $specialPriceService;
96+
9197
/**
9298
* Constructor
9399
*
@@ -101,6 +107,7 @@ class Price implements ResetAfterRequestInterface
101107
* @param \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory
102108
* @param \Magento\Framework\App\Config\ScopeConfigInterface $config
103109
* @param ProductTierPriceExtensionFactory|null $tierPriceExtensionFactory
110+
* @param SpecialPriceService|null $specialPriceService
104111
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
105112
*/
106113
public function __construct(
@@ -113,7 +120,8 @@ public function __construct(
113120
GroupManagementInterface $groupManagement,
114121
\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory,
115122
\Magento\Framework\App\Config\ScopeConfigInterface $config,
116-
?ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null
123+
?ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null,
124+
?SpecialPriceService $specialPriceService = null
117125
) {
118126
$this->_ruleFactory = $ruleFactory;
119127
$this->_storeManager = $storeManager;
@@ -126,6 +134,18 @@ public function __construct(
126134
$this->config = $config;
127135
$this->tierPriceExtensionFactory = $tierPriceExtensionFactory ?: ObjectManager::getInstance()
128136
->get(ProductTierPriceExtensionFactory::class);
137+
$this->specialPriceService = $specialPriceService ?: ObjectManager::getInstance()
138+
->get(SpecialPriceService::class);
139+
}
140+
141+
/**
142+
* Returns the SpecialPriceService instance
143+
*
144+
* @return SpecialPriceService|null
145+
*/
146+
protected function getSpecialPriceService(): ?SpecialPriceService
147+
{
148+
return $this->specialPriceService;
129149
}
130150

131151
/**
@@ -642,6 +662,9 @@ public function calculateSpecialPrice(
642662
$store = null
643663
) {
644664
if ($specialPrice !== null && $specialPrice != false) {
665+
666+
$specialPriceTo = $this->specialPriceService->execute($specialPriceTo);
667+
645668
if ($this->_localeDate->isScopeDateInInterval($store, $specialPriceFrom, $specialPriceTo)) {
646669
$finalPrice = min($finalPrice, (float) $specialPrice);
647670
}

app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
namespace Magento\Catalog\Pricing\Price;
88

99
use Magento\Catalog\Model\Product;
10+
use Magento\Framework\App\ObjectManager;
1011
use Magento\Framework\Pricing\Adjustment\CalculatorInterface;
1112
use Magento\Framework\Pricing\Price\AbstractPrice;
1213
use Magento\Framework\Pricing\Price\BasePriceProviderInterface;
1314
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
1415
use Magento\Store\Api\Data\WebsiteInterface;
16+
use Magento\Catalog\Model\Pricing\SpecialPriceService;
1517

1618
/**
1719
* Special price model
@@ -21,29 +23,38 @@ class SpecialPrice extends AbstractPrice implements SpecialPriceInterface, BaseP
2123
/**
2224
* Price type special
2325
*/
24-
const PRICE_CODE = 'special_price';
26+
public const PRICE_CODE = 'special_price';
2527

2628
/**
2729
* @var TimezoneInterface
2830
*/
2931
protected $localeDate;
3032

33+
/**
34+
* @var SpecialPriceService
35+
*/
36+
private SpecialPriceService $specialPriceService;
37+
3138
/**
3239
* @param Product $saleableItem
3340
* @param float $quantity
3441
* @param CalculatorInterface $calculator
3542
* @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
3643
* @param TimezoneInterface $localeDate
44+
* @param SpecialPriceService|null $specialPriceService
3745
*/
3846
public function __construct(
3947
Product $saleableItem,
4048
$quantity,
4149
CalculatorInterface $calculator,
4250
\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
43-
TimezoneInterface $localeDate
51+
TimezoneInterface $localeDate,
52+
?SpecialPriceService $specialPriceService = null
4453
) {
4554
parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency);
4655
$this->localeDate = $localeDate;
56+
$this->specialPriceService = $specialPriceService ?: ObjectManager::getInstance()
57+
->get(SpecialPriceService::class);
4758
}
4859

4960
/**
@@ -103,10 +114,12 @@ public function getSpecialToDate()
103114
*/
104115
public function isScopeDateInInterval()
105116
{
117+
$dateTo = $this->specialPriceService->execute($this->getSpecialToDate());
118+
106119
return $this->localeDate->isScopeDateInInterval(
107120
WebsiteInterface::ADMIN_CODE,
108121
$this->getSpecialFromDate(),
109-
$this->getSpecialToDate()
122+
$dateTo
110123
);
111124
}
112125

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\Test\Unit\Service;
9+
10+
use Magento\Catalog\Model\Pricing\SpecialPriceService;
11+
use PHPUnit\Framework\TestCase;
12+
13+
/**
14+
* Test for SpecialPriceService
15+
*/
16+
class SpecialPriceServiceTest extends TestCase
17+
{
18+
/**
19+
* @var SpecialPriceService
20+
*/
21+
private SpecialPriceService $specialPriceService;
22+
23+
/**
24+
* Set up a test environment
25+
*/
26+
protected function setUp(): void
27+
{
28+
$this->specialPriceService = new SpecialPriceService();
29+
}
30+
31+
/**
32+
* Data provider for execute method test
33+
*
34+
* @return array
35+
*/
36+
public static function executeDataProvider(): array
37+
{
38+
return [
39+
'invalid_date' => [
40+
'dateTo' => 'some date to',
41+
'expected' => 'some date to'
42+
],
43+
'date_without_time' => [
44+
'dateTo' => '2025-05-12 00:00:00',
45+
'expected' => '2025-05-12 00:00:00'
46+
],
47+
'date_with_specific_time' => [
48+
'dateTo' => '2025-05-12 17:00:00',
49+
'expected' => '2025-05-11 17:00:00'
50+
]
51+
];
52+
}
53+
54+
/**
55+
* @dataProvider executeDataProvider
56+
* @param mixed $dateTo
57+
* @param mixed $expected
58+
*/
59+
public function testExecute(mixed $dateTo, mixed $expected): void
60+
{
61+
$result = $this->specialPriceService->execute($dateTo);
62+
$this->assertEquals($expected, $result);
63+
}
64+
}

lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2015 Adobe
4+
* All Rights Reserved.
55
*/
66

77
namespace Magento\Framework\Stdlib\DateTime;

0 commit comments

Comments
 (0)