Skip to content

magento/magento2#39169: Special Price To Date is wrongly validated on applySpecialPrice #39690

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: 2.4-develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9111182
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan Mar 1, 2025
364b4e5
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan Mar 1, 2025
2a9dec5
Merge branch '2.4-develop' into fix-for-issue-39169
engcom-Hotel Mar 28, 2025
131815c
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan Apr 2, 2025
0d8b6b0
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan Apr 10, 2025
c8c8b35
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan Apr 10, 2025
e79e0fa
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan Apr 14, 2025
0e95cd4
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan Apr 14, 2025
1ce9b74
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan Apr 14, 2025
a3fe48c
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan Apr 15, 2025
7f6699e
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan Apr 16, 2025
3a45985
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan Apr 21, 2025
b42fab8
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan Apr 22, 2025
8ace126
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan Apr 28, 2025
58b3d98
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan Apr 28, 2025
5cbfdb6
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan Apr 28, 2025
cf359ba
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan Apr 28, 2025
2cea5b6
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan May 2, 2025
ed831bd
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan May 2, 2025
93c0caa
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan May 3, 2025
331dc74
magento/magento2#39146: Image resize queue error due to .tmp file han…
KrasnoshchokBohdan May 12, 2025
9b27e4d
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan May 12, 2025
fe976d7
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan May 17, 2025
188ece2
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan May 17, 2025
0a5d7f1
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan May 19, 2025
157c99a
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan May 19, 2025
706ee4c
Merge branch '2.4-develop' into fix-for-issue-39169
engcom-Hotel May 21, 2025
db809f1
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan May 22, 2025
cff0ee8
Merge branch '2.4-develop' into fix-for-issue-39169
engcom-Hotel May 28, 2025
18260e7
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan May 29, 2025
015fd43
magento/magento2#39169: Special Price To Date is wrongly validated on…
KrasnoshchokBohdan May 30, 2025
8442ee0
Merge branch '2.4-develop' into fix-for-issue-39169
KrasnoshchokBohdan May 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions app/code/Magento/Bundle/Model/Product/Price.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory;
use Magento\Catalog\Model\Pricing\SpecialPriceService;

/**
* Bundle product type price model
Expand Down Expand Up @@ -66,6 +67,7 @@ class Price extends \Magento\Catalog\Model\Product\Type\Price
* @param \Magento\Catalog\Helper\Data $catalogData
* @param \Magento\Framework\Serialize\Serializer\Json|null $serializer
* @param ProductTierPriceExtensionFactory|null $tierPriceExtensionFactory
* @param SpecialPriceService|null $specialPriceService
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
Expand All @@ -80,7 +82,8 @@ public function __construct(
\Magento\Framework\App\Config\ScopeConfigInterface $config,
\Magento\Catalog\Helper\Data $catalogData,
?\Magento\Framework\Serialize\Serializer\Json $serializer = null,
?ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null
?ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null,
?SpecialPriceService $specialPriceService = null
) {
$this->_catalogData = $catalogData;
$this->serializer = $serializer ?: ObjectManager::getInstance()
Expand All @@ -95,7 +98,8 @@ public function __construct(
$groupManagement,
$tierPriceFactory,
$config,
$tierPriceExtensionFactory
$tierPriceExtensionFactory,
$specialPriceService
);
}

Expand Down Expand Up @@ -629,6 +633,9 @@ public function calculateSpecialPrice(
$store = null
) {
if ($specialPrice !== null && $specialPrice != false) {

$specialPriceTo = $this->getSpecialPriceService()->execute($specialPriceTo);

if ($this->_localeDate->isScopeDateInInterval($store, $specialPriceFrom, $specialPriceTo)) {
$specialPrice = $finalPrice * ($specialPrice / 100);
$finalPrice = min($finalPrice, $specialPrice);
Expand Down
15 changes: 14 additions & 1 deletion app/code/Magento/Bundle/Test/Unit/Model/Product/PriceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory;
use Magento\Catalog\Helper\Data;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Pricing\SpecialPriceService;
use Magento\CatalogRule\Model\ResourceModel\RuleFactory;
use Magento\Customer\Api\GroupManagementInterface;
use Magento\Customer\Model\Session;
Expand Down Expand Up @@ -135,6 +136,17 @@ function ($value) {
->onlyMethods(['create'])
->disableOriginalConstructor()
->getMock();

$specialPriceService = $this->getMockBuilder(SpecialPriceService::class)
->disableOriginalConstructor()
->getMock();

$specialPriceService->expects($this->any())
->method('execute')
->willReturnCallback(function ($value) {
return $value;
});

$objectManagerHelper = new ObjectManagerHelper($this);
$this->model = $objectManagerHelper->getObject(
Price::class,
Expand All @@ -150,7 +162,8 @@ function ($value) {
'config' => $scopeConfig,
'catalogData' => $this->catalogHelperMock,
'serializer' => $this->serializer,
'tierPriceExtensionFactory' => $tierPriceExtensionFactoryMock
'tierPriceExtensionFactory' => $tierPriceExtensionFactoryMock,
'specialPriceService' => $specialPriceService
]
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Magento\Bundle\Pricing\Price\SpecialPrice;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Pricing\Price\RegularPrice;
use Magento\Catalog\Model\Pricing\SpecialPriceService;
use Magento\Framework\Pricing\Price\PriceInterface;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Framework\Pricing\PriceInfo\Base;
Expand Down Expand Up @@ -47,6 +48,11 @@ class SpecialPriceTest extends TestCase
*/
protected $priceCurrencyMock;

/**
* @var SpecialPriceService|MockObject
*/
private $specialPriceService;

protected function setUp(): void
{
$this->saleable = $this->getMockBuilder(Product::class)
Expand All @@ -62,13 +68,18 @@ protected function setUp(): void

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

$this->specialPriceService = $this->getMockBuilder(SpecialPriceService::class)
->disableOriginalConstructor()
->getMock();

$objectHelper = new ObjectManager($this);
$this->model = $objectHelper->getObject(
SpecialPrice::class,
[
'saleableItem' => $this->saleable,
'localeDate' => $this->localeDate,
'priceCurrency' => $this->priceCurrencyMock
'priceCurrency' => $this->priceCurrencyMock,
'specialPriceService' => $this->specialPriceService
]
);
}
Expand Down Expand Up @@ -102,6 +113,11 @@ public function testGetValue($regularPrice, $specialPrice, $isScopeDateInInterva
->with(WebsiteInterface::ADMIN_CODE, $specialFromDate, $specialToDate)
->willReturn($isScopeDateInInterval);

$this->specialPriceService->expects($this->once())
->method('execute')
->with($specialToDate)
->willReturn($specialToDate);

$this->priceCurrencyMock->expects($this->never())
->method('convertAndRound');

Expand Down
38 changes: 38 additions & 0 deletions app/code/Magento/Catalog/Model/Pricing/SpecialPriceService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Catalog\Model\Pricing;

/**
* This class provides functionality to normalize the end date/time of special prices
*/
class SpecialPriceService
{
/**
* This class subtracts one day from $dateTo if it contains a specific time (hours, minutes, seconds)
* because \Magento\Framework\Stdlib\DateTime\Timezone::isScopeDateInInterval adds one day.
* This ensures that the special price expires exactly at the specified time
*
* For example,
* - If $dateTo is "2025-05-12 17:00:00", it will be converted to "2025-05-11 17:00:00"
* - If $dateTo is "2024-05-12 00:00:00", it will remain unchanged
*
* @param mixed $dateTo
* @return mixed|string
*/
public function execute(mixed $dateTo): mixed
{
if ($dateTo
&& strtotime($dateTo) !== false
&& date('H:i:s', strtotime($dateTo)) !== '00:00:00') {
$dateToTimestamp = strtotime($dateTo);
$dateTo = date('Y-m-d H:i:s', $dateToTimestamp - 86400);
}

return $dateTo;
}
}
25 changes: 24 additions & 1 deletion app/code/Magento/Catalog/Model/Product/Type/Price.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory;
use Magento\Framework\App\ObjectManager;
use Magento\Store\Api\Data\WebsiteInterface;
use Magento\Catalog\Model\Pricing\SpecialPriceService;

/**
* Product type price model
Expand Down Expand Up @@ -88,6 +89,11 @@ class Price implements ResetAfterRequestInterface
*/
private $tierPriceExtensionFactory;

/**
* @var SpecialPriceService|null
*/
private ?SpecialPriceService $specialPriceService;

/**
* Constructor
*
Expand All @@ -101,6 +107,7 @@ class Price implements ResetAfterRequestInterface
* @param \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory
* @param \Magento\Framework\App\Config\ScopeConfigInterface $config
* @param ProductTierPriceExtensionFactory|null $tierPriceExtensionFactory
* @param SpecialPriceService|null $specialPriceService
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
Expand All @@ -113,7 +120,8 @@ public function __construct(
GroupManagementInterface $groupManagement,
\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory,
\Magento\Framework\App\Config\ScopeConfigInterface $config,
?ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null
?ProductTierPriceExtensionFactory $tierPriceExtensionFactory = null,
?SpecialPriceService $specialPriceService = null
) {
$this->_ruleFactory = $ruleFactory;
$this->_storeManager = $storeManager;
Expand All @@ -126,6 +134,18 @@ public function __construct(
$this->config = $config;
$this->tierPriceExtensionFactory = $tierPriceExtensionFactory ?: ObjectManager::getInstance()
->get(ProductTierPriceExtensionFactory::class);
$this->specialPriceService = $specialPriceService ?: ObjectManager::getInstance()
->get(SpecialPriceService::class);
}

/**
* Returns the SpecialPriceService instance
*
* @return SpecialPriceService|null
*/
protected function getSpecialPriceService(): ?SpecialPriceService
{
return $this->specialPriceService;
}

/**
Expand Down Expand Up @@ -642,6 +662,9 @@ public function calculateSpecialPrice(
$store = null
) {
if ($specialPrice !== null && $specialPrice != false) {

$specialPriceTo = $this->specialPriceService->execute($specialPriceTo);

if ($this->_localeDate->isScopeDateInInterval($store, $specialPriceFrom, $specialPriceTo)) {
$finalPrice = min($finalPrice, (float) $specialPrice);
}
Expand Down
19 changes: 16 additions & 3 deletions app/code/Magento/Catalog/Pricing/Price/SpecialPrice.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
namespace Magento\Catalog\Pricing\Price;

use Magento\Catalog\Model\Product;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Pricing\Adjustment\CalculatorInterface;
use Magento\Framework\Pricing\Price\AbstractPrice;
use Magento\Framework\Pricing\Price\BasePriceProviderInterface;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Store\Api\Data\WebsiteInterface;
use Magento\Catalog\Model\Pricing\SpecialPriceService;

/**
* Special price model
Expand All @@ -21,29 +23,38 @@ class SpecialPrice extends AbstractPrice implements SpecialPriceInterface, BaseP
/**
* Price type special
*/
const PRICE_CODE = 'special_price';
public const string PRICE_CODE = 'special_price';

/**
* @var TimezoneInterface
*/
protected $localeDate;

/**
* @var SpecialPriceService
*/
private SpecialPriceService $specialPriceService;

/**
* @param Product $saleableItem
* @param float $quantity
* @param CalculatorInterface $calculator
* @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
* @param TimezoneInterface $localeDate
* @param SpecialPriceService|null $specialPriceService
*/
public function __construct(
Product $saleableItem,
$quantity,
CalculatorInterface $calculator,
\Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
TimezoneInterface $localeDate
TimezoneInterface $localeDate,
?SpecialPriceService $specialPriceService = null
) {
parent::__construct($saleableItem, $quantity, $calculator, $priceCurrency);
$this->localeDate = $localeDate;
$this->specialPriceService = $specialPriceService ?: ObjectManager::getInstance()
->get(SpecialPriceService::class);
}

/**
Expand Down Expand Up @@ -103,10 +114,12 @@ public function getSpecialToDate()
*/
public function isScopeDateInInterval()
{
$dateTo = $this->specialPriceService->execute($this->getSpecialToDate());

return $this->localeDate->isScopeDateInInterval(
WebsiteInterface::ADMIN_CODE,
$this->getSpecialFromDate(),
$this->getSpecialToDate()
$dateTo
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php
/**
* Copyright 2025 Adobe
* All Rights Reserved.
*/
declare(strict_types=1);

namespace Magento\Catalog\Test\Unit\Service;

use Magento\Catalog\Model\Pricing\SpecialPriceService;
use PHPUnit\Framework\TestCase;

/**
* Test for SpecialPriceService
*/
class SpecialPriceServiceTest extends TestCase
{
/**
* @var SpecialPriceService
*/
private SpecialPriceService $specialPriceService;

/**
* Set up a test environment
*/
protected function setUp(): void
{
$this->specialPriceService = new SpecialPriceService();
}

/**
* Data provider for execute method test
*
* @return array
*/
public static function executeDataProvider(): array
{
return [
'invalid_date' => [
'dateTo' => 'some date to',
'expected' => 'some date to'
],
'date_without_time' => [
'dateTo' => '2025-05-12 00:00:00',
'expected' => '2025-05-12 00:00:00'
],
'date_with_specific_time' => [
'dateTo' => '2025-05-12 17:00:00',
'expected' => '2025-05-11 17:00:00'
]
];
}

/**
* @dataProvider executeDataProvider
* @param mixed $dateTo
* @param mixed $expected
*/
public function testExecute(mixed $dateTo, mixed $expected): void
{
$result = $this->specialPriceService->execute($dateTo);
$this->assertEquals($expected, $result);
}
}
4 changes: 2 additions & 2 deletions lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
* Copyright 2015 Adobe
* All Rights Reserved.
*/

namespace Magento\Framework\Stdlib\DateTime;
Expand Down