From 7274ed19c2520ce1746cbbbc7043ffcf2bcf4c61 Mon Sep 17 00:00:00 2001 From: KrasnoshchokBohdan Date: Tue, 17 Jun 2025 19:25:32 +0300 Subject: [PATCH 1/4] magento/magento2#39959 catalog_product_save_before observer throws date-related error when using REST API without store-level values (getFinalPrice() issue) Adjusted the processing of `SpecialFromDate` to ensure proper formatting when the date is provided as a `DateTimeInterface` instance. This prevents errors arising during `getFinalPrice()` execution under certain scenarios. --- app/code/Magento/Catalog/Model/Product/Type/Price.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index 091e623e7bc29..29a6457650d12 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -451,10 +451,16 @@ protected function _getCustomerGroupId($product) */ protected function _applySpecialPrice($product, $finalPrice) { + + $specialPriceFrom = $product->getSpecialFromDate(); + if ($specialPriceFrom instanceof \DateTimeInterface) { + $specialPriceFrom = $specialPriceFrom->format('Y-m-d H:i:s'); + } + return $this->calculateSpecialPrice( $finalPrice, $product->getSpecialPrice(), - $product->getSpecialFromDate(), + $specialPriceFrom, $product->getSpecialToDate(), WebsiteInterface::ADMIN_CODE ); From 7cac36eb5fc0a719231ec9dfaef7ef35abc496bf Mon Sep 17 00:00:00 2001 From: KrasnoshchokBohdan Date: Thu, 19 Jun 2025 17:13:13 +0300 Subject: [PATCH 2/4] magento/magento2#39959 catalog_product_save_before observer throws date-related error when using REST API without store-level values (getFinalPrice() issue) Updated special price logic to ensure date formats are handled consistently when calculating and setting the "special from" date. This improves compatibility and prevents potential type issues with date-related operations. --- app/code/Magento/Catalog/Model/Product/Type/Price.php | 8 +------- .../Magento/Catalog/Observer/SetSpecialPriceStartDate.php | 3 ++- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index f420687dd6052..d9d5d748ce4cc 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -451,16 +451,10 @@ protected function _getCustomerGroupId($product) */ protected function _applySpecialPrice($product, $finalPrice) { - - $specialPriceFrom = $product->getSpecialFromDate(); - if ($specialPriceFrom instanceof \DateTimeInterface) { - $specialPriceFrom = $specialPriceFrom->format('Y-m-d H:i:s'); - } - return $this->calculateSpecialPrice( $finalPrice, $product->getSpecialPrice(), - $specialPriceFrom, + $product->getSpecialFromDate(), $product->getSpecialToDate(), WebsiteInterface::ADMIN_CODE ); diff --git a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php index 280357069f967..20e0d76b967ac 100644 --- a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php +++ b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php @@ -39,7 +39,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var $product \Magento\Catalog\Model\Product */ $product = $observer->getEvent()->getProduct(); if ($product->getSpecialPrice() && $product->getSpecialFromDate() === null) { - $product->setData('special_from_date', $this->localeDate->date()->setTime(0, 0)); + $product->setData('special_from_date', + $this->localeDate->date()->setTime(0, 0)->format('Y-m-d H:i:s')); } return $this; } From c4210d544a6cf6352a48d97cf877fc2be0327495 Mon Sep 17 00:00:00 2001 From: KrasnoshchokBohdan Date: Mon, 23 Jun 2025 09:34:57 +0300 Subject: [PATCH 3/4] magento/magento2#39959 catalog_product_save_before observer throws date-related error when using REST API without store-level values (getFinalPrice() issue) - fix static test errors --- .../Magento/Catalog/Observer/SetSpecialPriceStartDate.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php index 20e0d76b967ac..1a553c0508895 100644 --- a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php +++ b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php @@ -39,8 +39,10 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var $product \Magento\Catalog\Model\Product */ $product = $observer->getEvent()->getProduct(); if ($product->getSpecialPrice() && $product->getSpecialFromDate() === null) { - $product->setData('special_from_date', - $this->localeDate->date()->setTime(0, 0)->format('Y-m-d H:i:s')); + $product->setData( + 'special_from_date', + $this->localeDate->date()->setTime(0, 0)->format('Y-m-d H:i:s') + ); } return $this; } From 2b3d323bfbdac63d60a9ad525a03648ff490e644 Mon Sep 17 00:00:00 2001 From: KrasnoshchokBohdan Date: Sat, 19 Jul 2025 15:08:45 +0300 Subject: [PATCH 4/4] magento/magento2#39959 catalog_product_save_before observer throws date-related error when using REST API without store-level values (getFinalPrice() issue) Add tests and logic for handling special_from_date updates Introduced integration and unit tests to validate behavior of the special price start date logic. Ensures special_from_date is set automatically if not provided and remains unchanged when already set. Also added comments and adjustments to observer logic to handle special price updates consistently. --- .../Observer/SetSpecialPriceStartDate.php | 4 + .../Observer/SetSpecialPriceStartDateTest.php | 88 ++++++++++++- .../Model/Product/SpecialPriceTest.php | 116 ++++++++++++++++++ 3 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/Product/SpecialPriceTest.php diff --git a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php index 1a553c0508895..e52913775692c 100644 --- a/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php +++ b/app/code/Magento/Catalog/Observer/SetSpecialPriceStartDate.php @@ -39,6 +39,10 @@ public function execute(\Magento\Framework\Event\Observer $observer) /** @var $product \Magento\Catalog\Model\Product */ $product = $observer->getEvent()->getProduct(); if ($product->getSpecialPrice() && $product->getSpecialFromDate() === null) { + // Set the special_from_date to the current date with time 00:00:00 when a special price is defined + // but no start date is specified. This ensures the special price takes effect immediately + // and is consistent with how the special price validation works in Magento. + // The time is explicitly set to midnight to ensure the special price is active for the entire day. $product->setData( 'special_from_date', $this->localeDate->date()->setTime(0, 0)->format('Y-m-d H:i:s') diff --git a/app/code/Magento/Catalog/Test/Unit/Observer/SetSpecialPriceStartDateTest.php b/app/code/Magento/Catalog/Test/Unit/Observer/SetSpecialPriceStartDateTest.php index b5c9c637f4311..bdd4867c57e9b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Observer/SetSpecialPriceStartDateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Observer/SetSpecialPriceStartDateTest.php @@ -88,13 +88,13 @@ protected function setUp(): void } /** - * Test observer execute method + * Test observer execute method when special_from_date is null */ public function testExecuteModifySpecialFromDate(): void { $specialPrice = 15; $specialFromDate = null; - $localeDateMock = ['special_from_date' => $this->returnValue($this->dateObject)]; + $formattedDate = '2023-01-01 00:00:00'; $this->observerMock ->expects($this->once()) @@ -106,10 +106,18 @@ public function testExecuteModifySpecialFromDate(): void ->method('getProduct') ->willReturn($this->productMock); - $this->dateObject->expects($this->any()) + $this->dateObject + ->expects($this->once()) ->method('setTime') + ->with(0, 0) ->willReturnSelf(); + $this->dateObject + ->expects($this->once()) + ->method('format') + ->with('Y-m-d H:i:s') + ->willReturn($formattedDate); + $this->timezone ->expects($this->once()) ->method('date') @@ -128,7 +136,79 @@ public function testExecuteModifySpecialFromDate(): void $this->productMock ->expects($this->once()) ->method('setData') - ->willReturn($localeDateMock); + ->with('special_from_date', $formattedDate); + + $this->observer->execute($this->observerMock); + } + + /** + * Test observer doesn't modify special_from_date when it's already set + */ + public function testExecuteDoesNotModifyExistingSpecialFromDate(): void + { + $specialPrice = 15; + $existingSpecialFromDate = '2023-01-01 00:00:00'; + + $this->observerMock + ->expects($this->once()) + ->method('getEvent') + ->willReturn($this->eventMock); + + $this->eventMock + ->expects($this->once()) + ->method('getProduct') + ->willReturn($this->productMock); + + $this->productMock + ->expects($this->once()) + ->method('getSpecialPrice') + ->willReturn($specialPrice); + + $this->productMock + ->expects($this->once()) + ->method('getSpecialFromDate') + ->willReturn($existingSpecialFromDate); + + $this->productMock + ->expects($this->never()) + ->method('setData'); + + $this->timezone + ->expects($this->never()) + ->method('date'); + + $this->observer->execute($this->observerMock); + } + + /** + * Test observer doesn't set special_from_date when special price is not set + */ + public function testExecuteDoesNotSetSpecialFromDateWithoutSpecialPrice(): void + { + $specialPrice = null; + + $this->observerMock + ->expects($this->once()) + ->method('getEvent') + ->willReturn($this->eventMock); + + $this->eventMock + ->expects($this->once()) + ->method('getProduct') + ->willReturn($this->productMock); + + $this->productMock + ->expects($this->once()) + ->method('getSpecialPrice') + ->willReturn($specialPrice); + + $this->productMock + ->expects($this->never()) + ->method('getSpecialFromDate'); + + $this->productMock + ->expects($this->never()) + ->method('setData'); $this->observer->execute($this->observerMock); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/SpecialPriceTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/SpecialPriceTest.php new file mode 100644 index 0000000000000..e28bdca5d233e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/SpecialPriceTest.php @@ -0,0 +1,116 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->localeDate = $this->objectManager->get(TimezoneInterface::class); + } + + /** + * Test that special_from_date is automatically set when special price is set via API + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testSpecialFromDateSetWhenSpecialPriceSet(): void + { + $product = $this->productRepository->get('simple'); + $product->setSpecialPrice(5.99); + $product->setSpecialFromDate(null); + + $this->productRepository->save($product); + $updatedProduct = $this->productRepository->get('simple', false, null, true); + $this->assertNotNull($updatedProduct->getSpecialFromDate()); + $expectedDate = $this->localeDate->date()->setTime(0, 0, 0)->format('Y-m-d'); + $actualDate = substr($updatedProduct->getSpecialFromDate(), 0, 10); + + // Assert special_from_date is set to current date with time 00:00:00 + $this->assertEquals($expectedDate, $actualDate); + } + + /** + * Test that existing special_from_date is not changed when product is saved + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testExistingSpecialFromDateNotChanged(): void + { + $product = $this->productRepository->get('simple'); + + $specificDate = '2023-01-01 00:00:00'; + $product->setSpecialPrice(5.99); + $product->setSpecialFromDate($specificDate); + + $this->productRepository->save($product); + $updatedProduct = $this->productRepository->get('simple', false, null, true); + + // Assert special_from_date remains unchanged + $this->assertEquals($specificDate, $updatedProduct->getSpecialFromDate()); + } + + /** + * Test that special price is correctly applied when special_from_date is today + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testSpecialPriceAppliedWithTodayDate(): void + { + $product = $this->productRepository->get('simple'); + $regularPrice = 10.00; + $specialPrice = 5.99; + + $product->setPrice($regularPrice); + $today = $this->localeDate->date()->format('Y-m-d 00:00:00'); + $product->setSpecialPrice($specialPrice); + $product->setSpecialFromDate($today); + + $this->productRepository->save($product); + $updatedProduct = $this->productRepository->get('simple', false, null, true); + + // Assert special price is applied + $this->assertEquals($specialPrice, $updatedProduct->getFinalPrice()); + } +}