diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Filter/DateTimeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Filter/DateTimeTest.php index 629500ca91cdc..053d7d6e97826 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Filter/DateTimeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Filter/DateTimeTest.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\Product\Filter\DateTime; use Magento\Framework\Locale\Resolver; use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; use Magento\Framework\Stdlib\DateTime\Timezone; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; @@ -43,7 +44,7 @@ function () { ); $timezone = $objectManager->getObject( Timezone::class, - ['localeResolver' => $localeResolver] + ['localeResolver' => $localeResolver, 'dateFormatterFactory' => new DateFormatterFactory()] ); $stdlibDateTimeFilter = $objectManager->getObject( \Magento\Framework\Stdlib\DateTime\Filter\DateTime::class, diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml index 264c55ba43390..bebf6ce5302d6 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -35,7 +35,7 @@ - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml new file mode 100644 index 0000000000000..c4c70cef81b0b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + <description value="Checkout display default country per store view"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37707"/> + <useCaseId value="MC-36884"/> + <group value="checkout"/> + </annotations> + <before> + <!-- Create simple product --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <!-- Create store view --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <!-- Set Germany as default country for created store view --> + <magentoCLI command="config:set --scope=stores --scope-code={{customStore.code}} general/country/default {{DE_Address_Berlin_Not_Default_Address.country_id}}" stepKey="changeDefaultCountry"/> + </before> + <after> + <!--Delete product and store view--> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + </after> + <!-- Open product and add product to cart--> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <!-- Go to cart --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <!-- Switch store view --> + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStoreViewActionGroup"> + <argument name="storeView" value="customStore"/> + </actionGroup> + <!-- Go to checkout page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="openCheckoutShippingPage"/> + <!-- Grab country code from checkout page and assert value with default country for created store view --> + <grabValueFrom selector="{{CheckoutShippingSection.country}}" stepKey="grabCountry"/> + <assertEquals stepKey="assertCountryValue"> + <actualResult type="const">$grabCountry</actualResult> + <expectedResult type="string">{{DE_Address_Berlin_Not_Default_Address.country_id}}</expectedResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js index a857d89a72b14..d1adb27353e1c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/cart/shipping-estimation.js @@ -79,7 +79,13 @@ define( if (!quote.isVirtual()) { checkoutProvider.on('shippingAddress', function (shippingAddressData) { - checkoutData.setShippingAddressFromData(shippingAddressData); + //jscs:disable requireCamelCaseOrUpperCaseIdentifiers + if (quote.shippingAddress().countryId !== shippingAddressData.country_id || + (shippingAddressData.postcode || shippingAddressData.region_id) + ) { + checkoutData.setShippingAddressFromData(shippingAddressData); + } + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers }); } else { checkoutProvider.on('shippingAddress', function (shippingAddressData) { diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 39071f25ea18c..70232e955a86d 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -19,6 +19,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Locale\Resolver; use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; use Magento\Framework\Stdlib\DateTime\Timezone; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Element\Html\Date; @@ -50,7 +51,7 @@ class DobTest extends TestCase const YEAR = '2014'; // Value of date('Y', strtotime(self::DATE)) - const DATE_FORMAT = 'M/d/Y'; + const DATE_FORMAT = 'M/d/y'; /** Constants used by Dob::setDateInput($code, $html) */ const DAY_HTML = @@ -119,7 +120,7 @@ function () { ); $timezone = $objectManager->getObject( Timezone::class, - ['localeResolver' => $localeResolver] + ['localeResolver' => $localeResolver, 'dateFormatterFactory' => new DateFormatterFactory()] ); $this->_locale = Resolver::DEFAULT_LOCALE; @@ -357,7 +358,8 @@ public function getDateFormatDataProvider(): array preg_replace( '/[^MmDdYy\/\.\-]/', '', - (new \IntlDateFormatter('ar_SA', \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE)) + (new DateFormatterFactory()) + ->create('ar_SA', \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE) ->getPattern() ) ], diff --git a/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php new file mode 100644 index 0000000000000..0bb102f34dd2d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Inventory; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\Catalog\Api\Data\ProductInterface as Product; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\GroupedProduct\Model\ResourceModel\Product\Link; +use Magento\Framework\App\ResourceConnection; + +/** + * Process parent stock item for grouped product + */ +class ParentItemProcessor implements ParentItemProcessorInterface +{ + /** + * @var Grouped + */ + private $groupedType; + + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $criteriaInterfaceFactory; + + /** + * Product metadata pool + * + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @param Grouped $groupedType + * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockConfigurationInterface $stockConfiguration + * @param ResourceConnection $resource + * @param MetadataPool $metadataPool + */ + public function __construct( + Grouped $groupedType, + StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, + StockItemRepositoryInterface $stockItemRepository, + StockConfigurationInterface $stockConfiguration, + ResourceConnection $resource, + MetadataPool $metadataPool + ) { + $this->groupedType = $groupedType; + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + $this->stockConfiguration = $stockConfiguration; + $this->stockItemRepository = $stockItemRepository; + $this->resource = $resource; + $this->metadataPool = $metadataPool; + } + + /** + * Process parent products + * + * @param Product $product + * @return void + */ + public function process(Product $product) + { + $parentIds = $this->getParentEntityIdsByChild($product->getId()); + foreach ($parentIds as $productId) { + $this->processStockForParent((int)$productId); + } + } + + /** + * Change stock item for parent product depending on children stock items + * + * @param int $productId + * @return void + */ + private function processStockForParent(int $productId) + { + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); + $criteria->setProductsFilter($productId); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + if (empty($allItems)) { + return; + } + $parentStockItem = array_shift($allItems); + $groupedChildrenIds = $this->groupedType->getChildrenIds($productId); + $criteria->setProductsFilter($groupedChildrenIds); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + + $groupedChildrenIsInStock = false; + + foreach ($allItems as $childItem) { + if ($childItem->getIsInStock() === true) { + $groupedChildrenIsInStock = true; + break; + } + } + + if ($this->isNeedToUpdateParent($parentStockItem, $groupedChildrenIsInStock)) { + $parentStockItem->setIsInStock($groupedChildrenIsInStock); + $parentStockItem->setStockStatusChangedAuto(1); + $this->stockItemRepository->save($parentStockItem); + } + } + + /** + * Check is parent item should be updated + * + * @param StockItemInterface $parentStockItem + * @param bool $childrenIsInStock + * @return bool + */ + private function isNeedToUpdateParent(StockItemInterface $parentStockItem, bool $childrenIsInStock): bool + { + return $parentStockItem->getIsInStock() !== $childrenIsInStock && + ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); + } + + /** + * Retrieve parent ids array by child id + * + * @param int $childId + * @return string[] + */ + private function getParentEntityIdsByChild($childId) + { + $select = $this->resource->getConnection() + ->select() + ->from(['l' => $this->resource->getTableName('catalog_product_link')], []) + ->join( + ['e' => $this->resource->getTableName('catalog_product_entity')], + 'e.' . + $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() . ' = l.product_id', + ['e.entity_id'] + ) + ->where('l.linked_product_id = ?', $childId) + ->where( + 'link_type_id = ?', + Link::LINK_TYPE_GROUPED + ); + + return $this->resource->getConnection()->fetchCol($select); + } +} diff --git a/app/code/Magento/GroupedProduct/etc/di.xml b/app/code/Magento/GroupedProduct/etc/di.xml index 43678d0ad7a82..d9534c6d3fe7d 100644 --- a/app/code/Magento/GroupedProduct/etc/di.xml +++ b/app/code/Magento/GroupedProduct/etc/di.xml @@ -105,4 +105,11 @@ </argument> </arguments> </type> + <type name="Magento\CatalogInventory\Observer\SaveInventoryDataObserver"> + <arguments> + <argument name="parentItemProcessorPool" xsi:type="array"> + <item name="grouped" xsi:type="object"> Magento\GroupedProduct\Model\Inventory\ParentItemProcessor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php index 55992c92226af..87cd4cf346288 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Edit/Form.php @@ -226,6 +226,7 @@ protected function _prepareForm() 'title' => __('Select File to Import'), 'required' => true, 'class' => 'input-file', + 'onchange' => 'varienImport.refreshLoadedFileLastModified(this);', 'note' => __( 'File must be saved in UTF-8 encoding for proper import' ), @@ -282,7 +283,7 @@ protected function getDownloadSampleFileHtml() private function getImportBehaviorTooltip() { $html = '<div class="admin__field-tooltip tooltip"> - <a class="admin__field-tooltip-action action-help" target="_blank" title="What is this?" + <a class="admin__field-tooltip-action action-help" target="_blank" title="What is this?" href="https://docs.magento.com/m2/ce/user_guide/system/data-import.html"><span>' . __('What is this?') . '</span></a></div>'; diff --git a/app/code/Magento/ImportExport/i18n/en_US.csv b/app/code/Magento/ImportExport/i18n/en_US.csv index a4943fe72826f..a91a76612fd9f 100644 --- a/app/code/Magento/ImportExport/i18n/en_US.csv +++ b/app/code/Magento/ImportExport/i18n/en_US.csv @@ -127,3 +127,4 @@ Summary,Summary "File %1 deleted","File %1 deleted" "Please provide valid export file name","Please provide valid export file name" "%1 is not a valid file","%1 is not a valid file" +"Content of uploaded file was changed, please re-upload the file","Content of uploaded file was changed, please re-upload the file" diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml index 69779baba381d..d512ce8182ede 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml @@ -7,6 +7,10 @@ <?php /** @var $block \Magento\ImportExport\Block\Adminhtml\Import\Edit\Before */ /** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +$fieldNameSourceFile = \Magento\ImportExport\Model\Import::FIELD_NAME_SOURCE_FILE; +$uploaderErrorMessage = $block->escapeHtml( + __('Content of uploaded file was changed, please re-upload the file') +); ?> <?php $scriptString = <<<script @@ -49,6 +53,12 @@ require([ */ sampleFilesBaseUrl: '{$block->escapeJs($block->getUrl('*/*/download/', ['filename' => 'entity-name']))}', + /** + * Loaded file last modified + * @type {int|null} + */ + loadedFileLastModified: null, + /** * Reset selected index * @param {string} elementId @@ -162,11 +172,50 @@ require([ } }, + /** + * Refresh loaded file last modified + */ + refreshLoadedFileLastModified: function(e) { + if (jQuery(e)[0].files.length > 0) { + this.loadedFileLastModified = jQuery(e)[0].files[0].lastModified; + } else { + this.loadedFileLastModified = null; + } + }, + /** * Post form data to dynamic iframe. * @param {string} newActionUrl OPTIONAL Change form action to this if specified */ postToFrame: function(newActionUrl) { + var fileUploader = document.getElementById('{$fieldNameSourceFile}'); + + if (fileUploader.files.length > 0) { + var file = fileUploader.files[0], + ifrElName = this.ifrElemName, + reader = new FileReader(); + + reader.readAsText(file, "UTF-8"); + + reader.onerror = function () { + jQuery('body').loader('hide'); + alert({ + content: '{$uploaderErrorMessage}' + }); + fileUploader.value = null; + jQuery('iframe#' + ifrElName).remove(); + return; + } + + if (file.lastModified !== this.loadedFileLastModified) { + alert({ + content: '{$uploaderErrorMessage}' + }); + fileUploader.value = null; + return; + } + } + if (!jQuery('[name="' + this.ifrElemName + '"]').length) { jQuery('body').append('<iframe name="' + this.ifrElemName + '" id="' + this.ifrElemName + '"/>'); jQuery('iframe#' + this.ifrElemName).attr('display', 'none'); diff --git a/app/code/Magento/Persistent/Model/QuoteManager.php b/app/code/Magento/Persistent/Model/QuoteManager.php index b6504d528fbe4..35b07ebdb7c44 100644 --- a/app/code/Magento/Persistent/Model/QuoteManager.php +++ b/app/code/Magento/Persistent/Model/QuoteManager.php @@ -5,6 +5,7 @@ */ namespace Magento\Persistent\Model; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Customer\Api\Data\GroupInterface; use Magento\Framework\App\ObjectManager; use Magento\Persistent\Helper\Data; @@ -64,6 +65,11 @@ class QuoteManager */ private $cartExtensionFactory; + /** + * @var CustomerInterfaceFactory + */ + private $customerDataFactory; + /** * @param \Magento\Persistent\Helper\Session $persistentSession * @param Data $persistentData @@ -71,6 +77,7 @@ class QuoteManager * @param CartRepositoryInterface $quoteRepository * @param CartExtensionFactory|null $cartExtensionFactory * @param ShippingAssignmentProcessor|null $shippingAssignmentProcessor + * @param CustomerInterfaceFactory|null $customerDataFactory */ public function __construct( \Magento\Persistent\Helper\Session $persistentSession, @@ -78,7 +85,8 @@ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, CartRepositoryInterface $quoteRepository, ?CartExtensionFactory $cartExtensionFactory = null, - ?ShippingAssignmentProcessor $shippingAssignmentProcessor = null + ?ShippingAssignmentProcessor $shippingAssignmentProcessor = null, + ?CustomerInterfaceFactory $customerDataFactory = null ) { $this->persistentSession = $persistentSession; $this->persistentData = $persistentData; @@ -88,6 +96,8 @@ public function __construct( ?? ObjectManager::getInstance()->get(CartExtensionFactory::class); $this->shippingAssignmentProcessor = $shippingAssignmentProcessor ?? ObjectManager::getInstance()->get(ShippingAssignmentProcessor::class); + $this->customerDataFactory = $customerDataFactory + ?? ObjectManager::getInstance()->get(CustomerInterfaceFactory::class); } /** @@ -109,14 +119,11 @@ public function setGuest($checkQuote = false) $quote->getPaymentsCollection()->walk('delete'); $quote->getAddressesCollection()->walk('delete'); $this->_setQuotePersistent = false; + $this->cleanCustomerData($quote); $quote->setIsActive(true) - ->setCustomerId(null) - ->setCustomerEmail(null) - ->setCustomerFirstname(null) - ->setCustomerLastname(null) - ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID) ->setIsPersistent(false) ->removeAllAddresses(); + //Create guest addresses $quote->getShippingAddress(); $quote->getBillingAddress(); @@ -129,6 +136,27 @@ public function setGuest($checkQuote = false) $this->persistentSession->setSession(null); } + /** + * Clear customer data in quote + * + * @param Quote $quote + */ + private function cleanCustomerData($quote) + { + /** + * Set empty customer object in quote to avoid restore customer id + * @see Quote::beforeSave() + */ + if ($quote->getCustomerId()) { + $quote->setCustomer($this->customerDataFactory->create()); + } + $quote->setCustomerId(null) + ->setCustomerEmail(null) + ->setCustomerFirstname(null) + ->setCustomerLastname(null) + ->setCustomerGroupId(GroupInterface::NOT_LOGGED_IN_ID); + } + /** * Emulate guest cart with persistent cart * diff --git a/app/code/Magento/Persistent/Observer/MakePersistentQuoteGuestObserver.php b/app/code/Magento/Persistent/Observer/MakePersistentQuoteGuestObserver.php index f2f9b96fa82e4..98c9c3df27852 100644 --- a/app/code/Magento/Persistent/Observer/MakePersistentQuoteGuestObserver.php +++ b/app/code/Magento/Persistent/Observer/MakePersistentQuoteGuestObserver.php @@ -1,16 +1,14 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Persistent\Observer; use Magento\Framework\Event\ObserverInterface; /** - * Make persistent quote to be guest + * Make persistent quote to be guest * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ @@ -38,26 +36,26 @@ class MakePersistentQuoteGuestObserver implements ObserverInterface protected $_persistentData = null; /** - * @var \Magento\Persistent\Model\QuoteManager + * @var \Magento\Checkout\Model\Session */ - protected $quoteManager; + private $checkoutSession; /** * @param \Magento\Persistent\Helper\Session $persistentSession * @param \Magento\Persistent\Helper\Data $persistentData * @param \Magento\Customer\Model\Session $customerSession - * @param \Magento\Persistent\Model\QuoteManager $quoteManager + * @param \Magento\Checkout\Model\Session $checkoutSession */ public function __construct( \Magento\Persistent\Helper\Session $persistentSession, \Magento\Persistent\Helper\Data $persistentData, \Magento\Customer\Model\Session $customerSession, - \Magento\Persistent\Model\QuoteManager $quoteManager + \Magento\Checkout\Model\Session $checkoutSession ) { $this->_persistentSession = $persistentSession; $this->_persistentData = $persistentData; $this->_customerSession = $customerSession; - $this->quoteManager = $quoteManager; + $this->checkoutSession = $checkoutSession; } /** @@ -74,7 +72,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) if (($this->_persistentSession->isPersistent() && !$this->_customerSession->isLoggedIn()) || $this->_persistentData->isShoppingCartPersist() ) { - $this->quoteManager->setGuest(true); + $this->checkoutSession->clearQuote()->clearStorage(); } } } diff --git a/app/code/Magento/Persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php b/app/code/Magento/Persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php index fe754711c910b..efc9ecd4c1a59 100644 --- a/app/code/Magento/Persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php +++ b/app/code/Magento/Persistent/Observer/RemoveGuestPersistenceOnEmptyCartObserver.php @@ -10,6 +10,8 @@ /** * Observer to remove persistent session if guest empties persistent cart previously created and added to by customer. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class RemoveGuestPersistenceOnEmptyCartObserver implements ObserverInterface { @@ -96,6 +98,8 @@ public function execute(\Magento\Framework\Event\Observer $observer) } if (!$cart || $cart->getItemsCount() == 0) { + $this->customerSession->setCustomerId(null) + ->setCustomerGroupId(null); $this->quoteManager->setGuest(); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php index 0c183084edca2..03d6ab02beb3c 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php @@ -9,6 +9,8 @@ namespace Magento\Persistent\Test\Unit\Model; use Magento\Checkout\Model\Session; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Customer\Model\GroupManagement; use Magento\Eav\Model\Entity\Collection\AbstractCollection; use Magento\Persistent\Helper\Data; @@ -78,6 +80,11 @@ class QuoteManagerTest extends TestCase */ private $shippingAssignmentProcessor; + /** + * @var CustomerInterfaceFactory|MockObject + */ + private $customerDataFactory; + protected function setUp(): void { $this->persistentSessionMock = $this->createMock(\Magento\Persistent\Helper\Session::class); @@ -124,13 +131,15 @@ protected function setUp(): void 'getItemsQty', 'getExtensionAttributes', 'setExtensionAttributes', - '__wakeup' + '__wakeup', + 'setCustomer' ]) ->disableOriginalConstructor() ->getMock(); $this->cartExtensionFactory = $this->createPartialMock(CartExtensionFactory::class, ['create']); $this->shippingAssignmentProcessor = $this->createPartialMock(ShippingAssignmentProcessor::class, ['create']); + $this->customerDataFactory = $this->createMock(CustomerInterfaceFactory::class); $this->model = new QuoteManager( $this->persistentSessionMock, @@ -138,7 +147,8 @@ protected function setUp(): void $this->checkoutSessionMock, $this->quoteRepositoryMock, $this->cartExtensionFactory, - $this->shippingAssignmentProcessor + $this->shippingAssignmentProcessor, + $this->customerDataFactory ); } @@ -189,6 +199,7 @@ public function testSetGuestWhenShoppingCartAndQuoteAreNotPersistent() public function testSetGuest() { + $customerId = 22; $this->checkoutSessionMock->expects($this->once()) ->method('getQuote')->willReturn($this->quoteMock); $this->quoteMock->expects($this->once())->method('getId')->willReturn(11); @@ -220,6 +231,7 @@ public function testSetGuest() ->method('getShippingAddress')->willReturn($quoteAddressMock); $this->quoteMock->expects($this->once()) ->method('getBillingAddress')->willReturn($quoteAddressMock); + $this->quoteMock->method('getCustomerId')->willReturn($customerId); $this->quoteMock->expects($this->once())->method('collectTotals')->willReturn($this->quoteMock); $this->quoteRepositoryMock->expects($this->once())->method('save')->with($this->quoteMock); $this->persistentSessionMock->expects($this->once()) @@ -229,7 +241,6 @@ public function testSetGuest() $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn(false); $this->quoteMock->expects($this->once())->method('getItemsQty')->willReturn(1); $extensionAttributes = $this->getMockBuilder(CartExtensionInterface::class) - ->addMethods(['getShippingAssignments', 'setShippingAssignments']) ->getMockForAbstractClass(); $shippingAssignment = $this->createMock(ShippingAssignmentInterface::class); $extensionAttributes->expects($this->once()) @@ -248,6 +259,11 @@ public function testSetGuest() $this->quoteMock->expects($this->once()) ->method('setExtensionAttributes') ->with($extensionAttributes); + $customerMock = $this->createMock(CustomerInterface::class); + $this->customerDataFactory->method('create')->willReturn($customerMock); + $this->quoteMock->expects($this->once()) + ->method('setCustomer') + ->with($customerMock); $this->model->setGuest(false); } diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/MakePersistentQuoteGuestObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/MakePersistentQuoteGuestObserverTest.php index 3622fe66099a4..bb78447cf852f 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/MakePersistentQuoteGuestObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/MakePersistentQuoteGuestObserverTest.php @@ -1,6 +1,5 @@ <?php /** - * * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -8,12 +7,12 @@ namespace Magento\Persistent\Test\Unit\Observer; +use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Framework\Event; use Magento\Framework\Event\Observer; use Magento\Persistent\Controller\Index; use Magento\Persistent\Helper\Data; use Magento\Persistent\Helper\Session; -use Magento\Persistent\Model\QuoteManager; use Magento\Persistent\Observer\MakePersistentQuoteGuestObserver; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -48,10 +47,10 @@ class MakePersistentQuoteGuestObserverTest extends TestCase /** * @var MockObject */ - protected $quoteManagerMock; + protected $checkoutSession; /** - * @var MockObject + * @var CheckoutSession|MockObject */ protected $eventManagerMock; @@ -60,6 +59,9 @@ class MakePersistentQuoteGuestObserverTest extends TestCase */ protected $actionMock; + /** + * @inheritdoc + */ protected function setUp(): void { $this->actionMock = $this->createMock(Index::class); @@ -67,7 +69,7 @@ protected function setUp(): void $this->sessionHelperMock = $this->createMock(Session::class); $this->helperMock = $this->createMock(Data::class); $this->customerSessionMock = $this->createMock(\Magento\Customer\Model\Session::class); - $this->quoteManagerMock = $this->createMock(QuoteManager::class); + $this->checkoutSession = $this->createMock(CheckoutSession::class); $this->eventManagerMock = $this->getMockBuilder(Event::class) ->addMethods(['getControllerAction']) @@ -81,7 +83,7 @@ protected function setUp(): void $this->sessionHelperMock, $this->helperMock, $this->customerSessionMock, - $this->quoteManagerMock + $this->checkoutSession ); } @@ -94,7 +96,8 @@ public function testExecute() $this->sessionHelperMock->expects($this->once())->method('isPersistent')->willReturn(true); $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->willReturn(false); $this->helperMock->expects($this->never())->method('isShoppingCartPersist'); - $this->quoteManagerMock->expects($this->once())->method('setGuest')->with(true); + $this->checkoutSession->expects($this->once())->method('clearQuote')->willReturnSelf(); + $this->checkoutSession->expects($this->once())->method('clearStorage')->willReturnSelf(); $this->model->execute($this->observerMock); } @@ -107,7 +110,8 @@ public function testExecuteWhenShoppingCartIsPersist() $this->sessionHelperMock->expects($this->once())->method('isPersistent')->willReturn(true); $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->willReturn(true); $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->willReturn(true); - $this->quoteManagerMock->expects($this->once())->method('setGuest')->with(true); + $this->checkoutSession->expects($this->once())->method('clearQuote')->willReturnSelf(); + $this->checkoutSession->expects($this->once())->method('clearStorage')->willReturnSelf(); $this->model->execute($this->observerMock); } @@ -120,7 +124,8 @@ public function testExecuteWhenShoppingCartIsNotPersist() $this->sessionHelperMock->expects($this->once())->method('isPersistent')->willReturn(true); $this->customerSessionMock->expects($this->once())->method('isLoggedIn')->willReturn(true); $this->helperMock->expects($this->once())->method('isShoppingCartPersist')->willReturn(false); - $this->quoteManagerMock->expects($this->never())->method('setGuest'); + $this->checkoutSession->expects($this->never())->method('clearQuote')->willReturnSelf(); + $this->checkoutSession->expects($this->never())->method('clearStorage')->willReturnSelf(); $this->model->execute($this->observerMock); } } diff --git a/app/code/Magento/Persistent/Test/Unit/Observer/RemoveGuestPersistenceOnEmptyCartObserverTest.php b/app/code/Magento/Persistent/Test/Unit/Observer/RemoveGuestPersistenceOnEmptyCartObserverTest.php index 4adc806fed415..7bef8feaaacc5 100644 --- a/app/code/Magento/Persistent/Test/Unit/Observer/RemoveGuestPersistenceOnEmptyCartObserverTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Observer/RemoveGuestPersistenceOnEmptyCartObserverTest.php @@ -137,6 +137,13 @@ public function testExecuteWithEmptyCart() ->with($customerId) ->willReturn($quoteMock); $quoteMock->expects($this->once())->method('getItemsCount')->willReturn($emptyCount); + $this->customerSessionMock->expects($this->once()) + ->method('setCustomerId') + ->with(null) + ->willReturnSelf(); + $this->customerSessionMock->expects($this->once()) + ->method('setCustomerGroupId') + ->with(null); $this->quoteManagerMock->expects($this->once())->method('setGuest'); $this->model->execute($this->observerMock); @@ -160,6 +167,13 @@ public function testExecuteWithNonexistentCart() ->method('getActiveForCustomer') ->with($customerId) ->willThrowException($exception); + $this->customerSessionMock->expects($this->once()) + ->method('setCustomerId') + ->with(null) + ->willReturnSelf(); + $this->customerSessionMock->expects($this->once()) + ->method('setCustomerGroupId') + ->with(null); $this->quoteManagerMock->expects($this->once())->method('setGuest'); $this->model->execute($this->observerMock); diff --git a/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml index eaebc7fdaf74a..fc17bc7f0f10a 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml @@ -21,6 +21,9 @@ </annotations> <before> <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushPageCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php index ef2df77e7daff..3600992011ed6 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Date.php @@ -111,7 +111,7 @@ public function getComponentName() public function convertDate($date, $hour = 0, $minute = 0, $second = 0, $setUtcTimeZone = true) { try { - $dateObj = $this->localeDate->date($date, $this->getLocale(), false); + $dateObj = $this->localeDate->date($date, $this->getLocale(), false, false); $dateObj->setTime($hour, $minute, $second); //convert store date to default date in UTC timezone without DST if ($setUtcTimeZone) { diff --git a/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Inventory/ParentItemProcessorTest.php b/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Inventory/ParentItemProcessorTest.php new file mode 100644 index 0000000000000..4b430e7a71886 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GroupedProduct/Model/Inventory/ParentItemProcessorTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Inventory; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\CatalogInventory\Api\StockRegistryInterface; +use Magento\CatalogInventory\Model\Stock\StockItemRepository; +use PHPUnit\Framework\TestCase; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\ObjectManagerInterface; + +/** + * Test stock status parent product + */ +class ParentItemProcessorTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + protected $objectManager; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * Test stock status parent product if children are out of stock + * + * @magentoDataFixture Magento/GroupedProduct/_files/product_grouped_with_simple_out_of_stock.php + * + * @return void + */ + public function testOutOfStockParentProduct(): void + { + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + /** @var Product $product */ + $product = $productRepository->get('simple_100000001'); + $product->setStockData(['qty' => 0, 'is_in_stock' => 0]); + $productRepository->save($product); + /** @var StockItemRepository $stockItemRepository */ + $stockItemRepository = $this->objectManager->create(StockItemRepository::class); + /** @var StockRegistryInterface $stockRegistry */ + $stockRegistry = $this->objectManager->create(StockRegistryInterface::class); + $stockItem = $stockRegistry->getStockItemBySku('grouped'); + $stockItem = $stockItemRepository->get($stockItem->getItemId()); + + $this->assertEquals(false, $stockItem->getIsInStock()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php index 3aadad7e9ebec..de6501ee78986 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\ProductRepository; use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; use PHPUnit\Framework\TestCase; @@ -60,11 +61,13 @@ public function testMarkQuoteRecollectAfterChangeProductPrice(): void $product->setPrice((float)$product->getPrice() + 10); $this->productRepository->save($product); + /** @var QuoteResource $quoteResource */ + $quoteResource = $quote->getResource(); /** @var AdapterInterface $connection */ - $connection = $quote->getResource()->getConnection(); + $connection = $quoteResource->getConnection(); $select = $connection->select() ->from( - $connection->getTableName('quote'), + $quoteResource->getTable('quote'), ['updated_at', 'trigger_recollect'] )->where( "reserved_order_id = 'test_order_with_simple_product_without_address'" diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Intl/DateFormatterFactory.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Intl/DateFormatterFactory.php new file mode 100644 index 0000000000000..42a381535b8b9 --- /dev/null +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Intl/DateFormatterFactory.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Stdlib\DateTime\Intl; + +/** + * Class to get Intl date formatter by locale + */ +class DateFormatterFactory +{ + /** + * Custom date formats by locale + */ + private const CUSTOM_DATE_FORMATS = [ + 'ar_SA' => [ + \IntlDateFormatter::SHORT => 'd/MM/y', + ] + ]; + + /** + * Create Intl Date formatter + * + * The Intl Date formatter gives date formats by ICU standard. + * http://userguide.icu-project.org/formatparse/datetime + * + * @param string $locale + * @param int $dateStyle + * @param int $timeStyle + * @param string|null $timeZone + * @param bool $useFourDigitsForYear + * @return \IntlDateFormatter + */ + public function create( + string $locale, + int $dateStyle, + int $timeStyle, + ?string $timeZone = null, + bool $useFourDigitsForYear = true + ): \IntlDateFormatter { + $formatter = new \IntlDateFormatter( + $locale, + $dateStyle, + $timeStyle, + $timeZone + ); + /** + * Process custom date formats + */ + $customDateFormat = $this->getCustomDateFormat($locale, $dateStyle, $timeStyle); + if ($customDateFormat !== null) { + $formatter->setPattern($customDateFormat); + } elseif ($dateStyle === \IntlDateFormatter::SHORT && $useFourDigitsForYear) { + /** + * Gives 4 places for year value in short style + */ + $longYearPattern = $this->setFourYearPlaces((string)$formatter->getPattern()); + $formatter->setPattern($longYearPattern); + } + + return $formatter; + } + + /** + * Get custom date format if it exists + * + * @param string $locale + * @param int $dateStyle + * @param int $timeStyle + * @return string + */ + private function getCustomDateFormat(string $locale, int $dateStyle, int $timeStyle): ?string + { + $customDateFormat = null; + if ($dateStyle !== \IntlDateFormatter::NONE && isset(self::CUSTOM_DATE_FORMATS[$locale][$dateStyle])) { + $customDateFormat = self::CUSTOM_DATE_FORMATS[$locale][$dateStyle]; + if ($timeStyle !== \IntlDateFormatter::NONE) { + $timeFormat = (new \IntlDateFormatter($locale, \IntlDateFormatter::NONE, $timeStyle)) + ->getPattern(); + $customDateFormat .= ' ' . $timeFormat; + } + } + + return $customDateFormat; + } + + /** + * Set 4 places for year value in format string + * + * @param string $format + * @return string + */ + private function setFourYearPlaces(string $format): string + { + return preg_replace( + '/(?<!y)yy(?!y)/', + 'y', + $format + ); + } +} diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php index cdf175767d6aa..1dfc621b1e1ea 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone.php @@ -12,6 +12,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Phrase; +use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; /** * Timezone library @@ -30,11 +31,6 @@ class Timezone implements TimezoneInterface \IntlDateFormatter::SHORT, ]; - /** - * @var array - */ - private $dateFormatterCache = []; - /** * @var string */ @@ -65,6 +61,11 @@ class Timezone implements TimezoneInterface */ protected $_localeResolver; + /** + * @var DateFormatterFactory + */ + private $dateFormatterFactory; + /** * @param ScopeResolverInterface $scopeResolver * @param ResolverInterface $localeResolver @@ -72,6 +73,7 @@ class Timezone implements TimezoneInterface * @param ScopeConfigInterface $scopeConfig * @param string $scopeType * @param string $defaultTimezonePath + * @param DateFormatterFactory $dateFormatterFactory */ public function __construct( ScopeResolverInterface $scopeResolver, @@ -79,7 +81,8 @@ public function __construct( \Magento\Framework\Stdlib\DateTime $dateTime, ScopeConfigInterface $scopeConfig, $scopeType, - $defaultTimezonePath + $defaultTimezonePath, + DateFormatterFactory $dateFormatterFactory ) { $this->_scopeResolver = $scopeResolver; $this->_localeResolver = $localeResolver; @@ -87,6 +90,7 @@ public function __construct( $this->_defaultTimezonePath = $defaultTimezonePath; $this->_scopeConfig = $scopeConfig; $this->_scopeType = $scopeType; + $this->dateFormatterFactory = $dateFormatterFactory; } /** @@ -122,11 +126,15 @@ public function getConfigTimezone($scopeType = null, $scopeCode = null) */ public function getDateFormat($type = \IntlDateFormatter::SHORT) { - return (new \IntlDateFormatter( - $this->_localeResolver->getLocale(), - $type, - \IntlDateFormatter::NONE - ))->getPattern(); + $formatter = $this->dateFormatterFactory->create( + (string)$this->_localeResolver->getLocale(), + (int)$type, + \IntlDateFormatter::NONE, + null, + false + ); + + return $formatter->getPattern(); } /** @@ -134,11 +142,13 @@ public function getDateFormat($type = \IntlDateFormatter::SHORT) */ public function getDateFormatWithLongYear() { - return preg_replace( - '/(?<!y)yy(?!y)/', - 'Y', - $this->getDateFormat() + $formatter = $this->dateFormatterFactory->create( + (string)$this->_localeResolver->getLocale(), + \IntlDateFormatter::SHORT, + \IntlDateFormatter::NONE ); + + return $formatter->getPattern(); } /** @@ -146,11 +156,13 @@ public function getDateFormatWithLongYear() */ public function getTimeFormat($type = \IntlDateFormatter::SHORT) { - return (new \IntlDateFormatter( - $this->_localeResolver->getLocale(), + $formatter = $this->dateFormatterFactory->create( + (string)$this->_localeResolver->getLocale(), \IntlDateFormatter::NONE, - $type - ))->getPattern(); + (int)$type + ); + + return $formatter->getPattern(); } /** @@ -166,10 +178,8 @@ public function getDateTimeFormat($type) */ public function date($date = null, $locale = null, $useTimezone = true, $includeTime = true) { - $locale = $locale ?: $this->_localeResolver->getLocale(); - $timezone = $useTimezone - ? $this->getConfigTimezone() - : date_default_timezone_get(); + $locale = (string)($locale ?: $this->_localeResolver->getLocale()); + $timezone = (string)($useTimezone ? $this->getConfigTimezone() : date_default_timezone_get()); switch (true) { case (empty($date)): @@ -179,9 +189,14 @@ public function date($date = null, $locale = null, $useTimezone = true, $include case ($date instanceof \DateTimeImmutable): return new \DateTime($date->format('Y-m-d H:i:s'), $date->getTimezone()); case (!is_numeric($date)): - $date = $this->appendTimeIfNeeded($date, $includeTime, $timezone, $locale); - $date = $this->parseLocaleDate($date, $locale, $timezone, $includeTime) - ?: (new \DateTime($date))->getTimestamp(); + $date = $this->appendTimeIfNeeded((string)$date, (bool)$includeTime, $timezone, $locale); + $formatter = $this->dateFormatterFactory->create( + $locale, + \IntlDateFormatter::SHORT, + $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE, + $timezone + ); + $date = $formatter->parse($date) ?: (new \DateTime($date))->getTimestamp(); break; } @@ -279,7 +294,6 @@ public function formatDateTime( if (!($date instanceof \DateTimeInterface)) { $date = new \DateTime($date); } - if ($timezone === null) { if ($date->getTimezone() == null || $date->getTimezone()->getName() == 'UTC' || $date->getTimezone()->getName() == '+00:00' @@ -290,14 +304,20 @@ public function formatDateTime( } } - $formatter = new \IntlDateFormatter( - $locale ?: $this->_localeResolver->getLocale(), - $dateType, - $timeType, - $timezone, + $formatter = $this->dateFormatterFactory->create( + (string)($locale ?: $this->_localeResolver->getLocale()), + (int)($dateType ?? \IntlDateFormatter::SHORT), + (int)($timeType ?? \IntlDateFormatter::SHORT), null, - $pattern + false ); + if ($timezone) { + $formatter->setTimeZone($timezone); + } + if ($pattern) { + $formatter->setPattern($pattern); + } + return $formatter->format($date); } @@ -338,11 +358,17 @@ public function convertConfigTimeToUtc($date, $format = 'Y-m-d H:i:s') * @return string * @throws LocalizedException */ - private function appendTimeIfNeeded($date, $includeTime, $timezone, $locale) + private function appendTimeIfNeeded(string $date, bool $includeTime, string $timezone, string $locale) { if ($includeTime && !preg_match('/\d{1}:\d{2}/', $date)) { - $convertedDate = $this->parseLocaleDate($date, $locale, $timezone, false); - if (!$convertedDate) { + $formatter = $this->dateFormatterFactory->create( + $locale, + \IntlDateFormatter::SHORT, + \IntlDateFormatter::NONE, + $timezone + ); + $timestamp = $formatter->parse($date); + if (!$timestamp) { throw new LocalizedException( new Phrase( 'Could not append time to DateTime' @@ -350,68 +376,15 @@ private function appendTimeIfNeeded($date, $includeTime, $timezone, $locale) ); } - $formatterWithHour = $this->getDateFormatter( + $formatterWithHour = $this->dateFormatterFactory->create( $locale, - $timezone, - \IntlDateFormatter::MEDIUM, - \IntlDateFormatter::SHORT + \IntlDateFormatter::SHORT, + \IntlDateFormatter::SHORT, + $timezone ); - $date = $formatterWithHour->format($convertedDate); - } - return $date; - } - - /** - * Parse date by locale format through IntlDateFormatter - * - * @param string $date - * @param string $locale - * @param string $timeZone - * @param bool $includeTime - * @return int|null Timestamp of date - */ - private function parseLocaleDate(string $date, string $locale, string $timeZone, bool $includeTime): ?int - { - $allowedStyles = [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT]; - $timeStyle = $includeTime ? \IntlDateFormatter::SHORT : \IntlDateFormatter::NONE; - - /** - * Try to parse date with different styles - */ - foreach ($allowedStyles as $style) { - $formatter = $this->getDateFormatter($locale, $timeZone, $style, $timeStyle); - $timeStamp = $formatter->parse($date); - if ($timeStamp) { - return $timeStamp; - } + $date = $formatterWithHour->format($timestamp); } - return null; - } - - /** - * Get date formatter for locale - * - * @param string $locale - * @param string $timeZone - * @param int $style - * @param int $timeStyle - * @return \IntlDateFormatter - */ - private function getDateFormatter(string $locale, string $timeZone, int $style, int $timeStyle): \IntlDateFormatter - { - $cacheKey = "{$locale}_{$timeZone}_{$style}_{$timeStyle}"; - if (isset($this->dateFormatterCache[$cacheKey])) { - return $this->dateFormatterCache[$cacheKey]; - } - - $this->dateFormatterCache[$cacheKey] = new \IntlDateFormatter( - $locale, - $style, - $timeStyle, - new \DateTimeZone($timeZone) - ); - - return $this->dateFormatterCache[$cacheKey]; + return $date; } } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php index 2e8110316ec29..38a62f006adbc 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/DateTime/TimezoneTest.php @@ -12,6 +12,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; use Magento\Framework\Stdlib\DateTime\Timezone; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; @@ -90,22 +91,23 @@ protected function tearDown(): void * @param string $date * @param string $locale * @param bool $includeTime - * @param int $expectedTimestamp + * @param int|string $expectedTime + * @param string|null $timeZone * @dataProvider dateIncludeTimeDataProvider */ - public function testDateIncludeTime($date, $locale, $includeTime, $expectedTimestamp) + public function testDateIncludeTime($date, $locale, $includeTime, $expectedTime, $timeZone = 'America/Chicago') { - $this->scopeConfig->method('getValue')->willReturn('America/Chicago'); - /** @var Timezone $timezone */ - $timezone = $this->objectManager->getObject(Timezone::class, ['scopeConfig' => $this->scopeConfig]); + if ($timeZone !== null) { + $this->scopeConfig->method('getValue')->willReturn($timeZone); + } /** @var \DateTime $dateTime */ - $dateTime = $timezone->date($date, $locale, true, $includeTime); - if (is_numeric($expectedTimestamp)) { - $this->assertEquals($expectedTimestamp, $dateTime->getTimestamp()); + $dateTime = $this->getTimezone()->date($date, $locale, $timeZone !== null, $includeTime); + if (is_numeric($expectedTime)) { + $this->assertEquals($expectedTime, $dateTime->getTimestamp()); } else { $format = $includeTime ? DateTime::DATETIME_PHP_FORMAT : DateTime::DATE_PHP_FORMAT; - $this->assertEquals($expectedTimestamp, date($format, $dateTime->getTimestamp())); + $this->assertEquals($expectedTime, date($format, $dateTime->getTimestamp())); } } @@ -158,16 +160,30 @@ public function dateIncludeTimeDataProvider(): array 1635570000 // expected timestamp ], 'Parse Saudi Arabia date without time' => [ - '31‏/8‏/2020 02020', + '4/09/2020', 'ar_SA', false, - '2020-08-31' + '2020-09-04' + ], + 'Parse Saudi Arabia date with time' => [ + '4/09/2020 10:10 مساء', + 'ar_SA', + true, + '2020-09-04 22:10:00', + null + ], + 'Parse Saudi Arabia date with zero time' => [ + '4/09/2020', + 'ar_SA', + true, + '2020-09-04 00:00:00', + null ], 'Parse date in short style with long year 1999' => [ - '9/11/1999', + '8/11/1999', 'en_US', false, - '1999-09-11' + '1999-08-11' ], 'Parse date in short style with long year 2099' => [ '9/2/2099', @@ -175,6 +191,59 @@ public function dateIncludeTimeDataProvider(): array false, '2099-09-02' ], + 'Parse date in short style with short year 1999' => [ + '8/11/99', + 'en_US', + false, + '1999-08-11' + ], + ]; + } + + /** + * @param string $locale + * @param int $style + * @param string $expectedFormat + * @dataProvider getDatetimeFormatDataProvider + */ + public function testGetDatetimeFormat(string $locale, int $style, string $expectedFormat): void + { + /** @var Timezone $timezone */ + $this->localeResolver->method('getLocale')->willReturn($locale); + $this->assertEquals($expectedFormat, $this->getTimezone()->getDateTimeFormat($style)); + } + + /** + * @return array + */ + public function getDatetimeFormatDataProvider(): array + { + return [ + ['en_US', \IntlDateFormatter::SHORT, 'M/d/yy h:mm a'], + ['ar_SA', \IntlDateFormatter::SHORT, 'd/MM/y h:mm a'] + ]; + } + + /** + * @param string $locale + * @param int $style + * @param string $expectedFormat + * @dataProvider getDateFormatWithLongYearDataProvider + */ + public function testGetDateFormatWithLongYear(string $locale, string $expectedFormat): void + { + /** @var Timezone $timezone */ + $this->localeResolver->method('getLocale')->willReturn($locale); + $this->assertEquals($expectedFormat, $this->getTimezone()->getDateFormatWithLongYear()); + } + + /** + * @return array + */ + public function getDateFormatWithLongYearDataProvider(): array + { + return [ + ['en_US', 'M/d/y'], ]; } @@ -320,7 +389,8 @@ private function getTimezone() $this->createMock(DateTime::class), $this->scopeConfig, $this->scopeType, - $this->defaultTimezonePath + $this->defaultTimezonePath, + new DateFormatterFactory() ); }