diff --git a/app/code/Magento/Sales/Model/EmailSenderHandler.php b/app/code/Magento/Sales/Model/EmailSenderHandler.php index 3a7a5727d8341..e4af2bc6b9933 100644 --- a/app/code/Magento/Sales/Model/EmailSenderHandler.php +++ b/app/code/Magento/Sales/Model/EmailSenderHandler.php @@ -3,187 +3,137 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model; +use Exception; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Config\ValueFactory; -use Magento\Framework\App\Config\ValueInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Sales\Model\Order\Email\Container\IdentityInterface; +use Magento\Sales\Model\Order\Email\Container\NullIdentity; +use Magento\Sales\Model\Order\Email\Sender; use Magento\Sales\Model\ResourceModel\Collection\AbstractCollection; +use Magento\Store\Model\StoreManagerInterface; /** * Sales emails sending * * Performs handling of cron jobs related to sending emails to customers - * after creation/modification of Order, Invoice, Shipment or Creditmemo. + * after creation/modification of Order, Invoice, Shipment or CreditMemo. */ class EmailSenderHandler { - /** - * Email sender model. - * - * @var \Magento\Sales\Model\Order\Email\Sender - */ - protected $emailSender; + /** @var Sender $emailSender */ + private $emailSender; - /** - * Entity resource model. - * - * @var \Magento\Sales\Model\ResourceModel\EntityAbstract - */ - protected $entityResource; + /** @var AbstractCollection $entityCollection */ + private $entityCollection; - /** - * Entity collection model. - * - * @var AbstractCollection - */ - protected $entityCollection; + /** @var ScopeConfigInterface $globalConfig */ + private $globalConfig; - /** - * Global configuration storage. - * - * @var \Magento\Framework\App\Config\ScopeConfigInterface - */ - protected $globalConfig; + /** @var DateTime|null $dateTime */ + private $dateTime; - /** - * @var IdentityInterface - */ + /** @var IdentityInterface|NullIdentity|null $identityContainer */ private $identityContainer; - /** - * @var \Magento\Store\Model\StoreManagerInterface - */ + /** @var StoreManagerInterface|null $storeManager */ private $storeManager; - /** - * Config data factory - * - * @var ValueFactory - */ + /** @var ValueFactory|null $configValueFactory */ private $configValueFactory; - /** - * @var string - */ - private $modifyStartFromDate; + /** @var string|null $modifyStartFromDate */ + private $modifyStartFromDate = null; /** - * @param \Magento\Sales\Model\Order\Email\Sender $emailSender - * @param \Magento\Sales\Model\ResourceModel\EntityAbstract $entityResource + * @param Sender $emailSender * @param AbstractCollection $entityCollection - * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig + * @param ScopeConfigInterface $globalConfig * @param IdentityInterface|null $identityContainer - * @param \Magento\Store\Model\StoreManagerInterface|null $storeManager + * @param StoreManagerInterface|null $storeManager * @param ValueFactory|null $configValueFactory * @param string|null $modifyStartFromDate + * @param DateTime|null $dateTime */ public function __construct( - \Magento\Sales\Model\Order\Email\Sender $emailSender, - \Magento\Sales\Model\ResourceModel\EntityAbstract $entityResource, + Sender $emailSender, AbstractCollection $entityCollection, - \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig, + ScopeConfigInterface $globalConfig, IdentityInterface $identityContainer = null, - \Magento\Store\Model\StoreManagerInterface $storeManager = null, + StoreManagerInterface $storeManager = null, ?ValueFactory $configValueFactory = null, - ?string $modifyStartFromDate = null + ?string $modifyStartFromDate = null, + ?DateTime $dateTime = null ) { $this->emailSender = $emailSender; - $this->entityResource = $entityResource; $this->entityCollection = $entityCollection; $this->globalConfig = $globalConfig; - - $this->identityContainer = $identityContainer ?: ObjectManager::getInstance() - ->get(\Magento\Sales\Model\Order\Email\Container\NullIdentity::class); - $this->storeManager = $storeManager ?: ObjectManager::getInstance() - ->get(\Magento\Store\Model\StoreManagerInterface::class); - + $this->identityContainer = $identityContainer ?: ObjectManager::getInstance()->get(NullIdentity::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); $this->configValueFactory = $configValueFactory ?: ObjectManager::getInstance()->get(ValueFactory::class); $this->modifyStartFromDate = $modifyStartFromDate ?: $this->modifyStartFromDate; + $this->dateTime = $dateTime ?: ObjectManager::getInstance()->get(DateTime::class); } /** - * Handles asynchronous email sending + * Handles asynchronous email sending. + * * @return void + * @throws Exception */ - public function sendEmails() + public function sendEmails(): void { - if ($this->globalConfig->getValue('sales_email/general/async_sending')) { - $this->entityCollection->addFieldToFilter('send_email', ['eq' => 1]); - $this->entityCollection->addFieldToFilter('email_sent', ['null' => true]); - $this->filterCollectionByStartFromDate($this->entityCollection); - $this->entityCollection->setPageSize( - $this->globalConfig->getValue('sales_email/general/sending_limit') - ); - - /** @var \Magento\Store\Api\Data\StoreInterface[] $stores */ - $stores = $this->getStores(clone $this->entityCollection); - - /** @var \Magento\Store\Model\Store $store */ - foreach ($stores as $store) { - $this->identityContainer->setStore($store); - if (!$this->identityContainer->isEnabled()) { - continue; - } - $entityCollection = clone $this->entityCollection; - $entityCollection->addFieldToFilter('store_id', $store->getId()); - - /** @var \Magento\Sales\Model\AbstractModel $item */ - foreach ($entityCollection->getItems() as $item) { - if ($this->emailSender->send($item, true)) { - $this->entityResource->saveAttribute( - $item->setEmailSent(true), - 'email_sent' - ); - } - } - } + if (!$this->globalConfig->isSetFlag('sales_email/general/async_sending')) { + return; } - } - /** - * Get stores for given entities. - * - * @param ResourceModel\Collection\AbstractCollection $entityCollection - * @return \Magento\Store\Api\Data\StoreInterface[] - * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - private function getStores( - AbstractCollection $entityCollection - ): array { - $stores = []; - - $entityCollection->addAttributeToSelect('store_id')->getSelect()->group('store_id'); - /** @var \Magento\Sales\Model\EntityInterface $item */ - foreach ($entityCollection->getItems() as $item) { - /** @var \Magento\Store\Model\StoreManagerInterface $store */ - $store = $this->storeManager->getStore($item->getStoreId()); - $stores[$item->getStoreId()] = $store; - } + $this->entityCollection->addFieldToFilter('send_email', ['eq' => true]); + $this->entityCollection->addFieldToFilter('email_sent', ['null' => true]); + $this->filterCollectionByStartFromDate($this->entityCollection); + $this->entityCollection->setPageSize((int) $this->globalConfig->getValue('sales_email/general/sending_limit')); - return $stores; + foreach ($this->storeManager->getStores() as $store) { + $this->identityContainer->setStore($store); + + if (!$this->identityContainer->isEnabled()) { + continue; + } + + $entityCloneByStoreCollection = clone $this->entityCollection; + $entityCloneByStoreCollection->addFieldToFilter('store_id', $store->getId()); + + foreach ($entityCloneByStoreCollection->getItems() as $item) { + $this->emailSender->send($item, true); + } + } } /** * Filter collection by start from date * * @param AbstractCollection $collection + * * @return void */ private function filterCollectionByStartFromDate(AbstractCollection $collection): void { - /** @var $configValue ValueInterface */ $configValue = $this->configValueFactory->create(); $configValue->load('sales_email/general/async_sending', 'path'); - if ($configValue->getId()) { - $startFromDate = date( - 'Y-m-d H:i:s', - strtotime($configValue->getUpdatedAt() . ' ' . $this->modifyStartFromDate) - ); - - $collection->addFieldToFilter('created_at', ['from' => $startFromDate]); + if (!$configValue->getId()) { + return; } + + $startFromDate = $this->dateTime->date( + 'Y-m-d H:i:s', + strtotime($configValue->getUpdatedAt() . ' ' . $this->modifyStartFromDate) + ); + + $collection->addFieldToFilter('created_at', ['gteq' => $startFromDate]); } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php b/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php index 2a7b44efa5261..c840ab4c3ba6c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/EmailSenderHandlerTest.php @@ -7,21 +7,19 @@ namespace Magento\Sales\Test\Unit\Model; -use Magento\Config\Model\Config\Backend\Encrypted; -use Magento\Framework\App\Config; -use Magento\Framework\App\Config\Value; +use Exception; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Config\ValueFactory; -use Magento\Framework\DB\Select; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Sales\Model\AbstractModel; +use Magento\Framework\App\Config\ValueInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\Sales\Model\EmailSenderHandler; use Magento\Sales\Model\Order\Email\Container\IdentityInterface; +use Magento\Sales\Model\Order\Email\Container\NullIdentity; use Magento\Sales\Model\Order\Email\Sender; use Magento\Sales\Model\ResourceModel\Collection\AbstractCollection; -use Magento\Sales\Model\ResourceModel\EntityAbstract; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** @@ -29,274 +27,215 @@ */ class EmailSenderHandlerTest extends TestCase { - /** - * Subject of testing. - * - * @var EmailSenderHandler - */ - protected $object; + /** @var Sender $emailSender */ + private $emailSender; + + /** @var AbstractCollection $entityCollection */ + private $entityCollection; + + /** @var ScopeConfigInterface $globalConfig */ + private $globalConfig; + + /** @var DateTime|null $dateTime */ + private $dateTime; + + /** @var IdentityInterface|NullIdentity|null $identityContainer */ + private $identityContainer; + + /** @var StoreManagerInterface|null $storeManager */ + private $storeManager; + + /** @var ValueFactory|null $configValueFactory */ + private $configValueFactory; + + /** @var string|null $modifyStartFromDate */ + private $modifyStartFromDate = '-1 day'; + + /** @var EmailSenderHandler $testClass */ + private $testClass; /** - * Email sender model mock. + * Setup method. * - * @var Sender|MockObject + * @return void */ - protected $emailSender; + public function setUp(): void + { + $this->emailSender = $this->getMockBuilder(Sender::class) + ->addMethods(['send']) + ->disableOriginalConstructor() + ->getMock(); + + $this->entityCollection = $this->getMockBuilder(AbstractCollection::class) + ->disableOriginalConstructor() + ->disableOriginalClone() + ->getMock(); + + $this->globalConfig = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + + $this->dateTime = $this->getMockBuilder(DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->identityContainer = $this->getMockBuilder(IdentityInterface::class) + ->getMockForAbstractClass(); + + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + + $this->configValueFactory = $this->createMock(ValueFactory::class); + + $this->testClass = new EmailSenderHandler( + $this->emailSender, + $this->entityCollection, + $this->globalConfig, + $this->identityContainer, + $this->storeManager, + $this->configValueFactory, + $this->modifyStartFromDate, + $this->dateTime + ); + } /** - * Entity resource model mock. + * Async email config option IS NOT enabled. * - * @var EntityAbstract|MockObject + * @return void + * @throws Exception */ - protected $entityResource; + public function testAsyncEmailSendingIsNotEnabled(): void + { + $this->globalConfig->expects($this->once()) + ->method('isSetFlag') + ->with('sales_email/general/async_sending') + ->willReturn(false); + + $this->testClass->sendEmails(); + } /** - * Entity collection model mock. + * Test Async email sending. * - * @var AbstractCollection|MockObject + * @return void + * @throws Exception */ - protected $entityCollection; + public function testAsyncEmailSending(): void + { + $this->buildEntityFiltersForEmailSender(); + $this->sendEmailByStoreScope(); + + $this->testClass->sendEmails(); + } /** - * Global configuration storage mock. + * Build entity filter for email sender. * - * @var Config|MockObject + * @return void */ - protected $globalConfig; + private function buildEntityFiltersForEmailSender(): void + { + $this->globalConfig->expects($this->once()) + ->method('isSetFlag') + ->with('sales_email/general/async_sending') + ->willReturn(true); - /** - * @var IdentityInterface|MockObject - */ - private $identityContainerMock; + $this->entityCollection->expects($this->at(0)) + ->method('addFieldToFilter') + ->with('send_email', ['eq' => true]); - /** - * @var StoreManagerInterface|MockObject - */ - private $storeManagerMock; + $this->entityCollection->expects($this->at(1)) + ->method('addFieldToFilter') + ->with('email_sent', ['null' => true]); - /** - * @var ValueFactory|MockObject - */ - private $configValueFactory; + $configValue = $this->getMockBuilder(ValueInterface::class) + ->addMethods(['load', 'getId', 'getUpdatedAt']) + ->getMockForAbstractClass(); - /** - * @var string - */ - private $modifyStartFromDate = '-1 day'; + $this->configValueFactory->expects($this->once()) + ->method('create') + ->willReturn($configValue); - protected function setUp(): void - { - $objectManager = new ObjectManager($this); + $configValue->expects($this->once()) + ->method('load') + ->with('sales_email/general/async_sending', 'path'); - $this->emailSender = $this->getMockBuilder(Sender::class) - ->addMethods(['send']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + $configValue->expects($this->once()) + ->method('getId') + ->willReturn(rand(1, PHP_INT_MAX)); - $this->entityResource = $this->getMockForAbstractClass( - EntityAbstract::class, - [], - '', - false, - false, - true, - ['saveAttribute'] - ); + $updatedAt = date('Y-m-d H:i:s'); + $strToTime = strtotime($updatedAt . ' ' . $this->modifyStartFromDate); + $startFromDate = date('Y-m-d H:i:s', $strToTime); - $this->entityCollection = $this->getMockForAbstractClass( - AbstractCollection::class, - [], - '', - false, - false, - true, - ['addFieldToFilter', 'getItems', 'addAttributeToSelect', 'getSelect'] - ); + $configValue->expects($this->once()) + ->method('getUpdatedAt') + ->willReturn($updatedAt); - $this->globalConfig = $this->createMock(Config::class); + $this->dateTime->expects($this->once()) + ->method('date') + ->with('Y-m-d H:i:s', $strToTime) + ->willReturn($startFromDate); - $this->identityContainerMock = $this->createMock( - IdentityInterface::class - ); + $this->entityCollection->expects($this->at(2)) + ->method('addFieldToFilter') + ->with('created_at', ['gteq' => $startFromDate]); - $this->storeManagerMock = $this->createMock( - StoreManagerInterface::class - ); + $pageSize = rand(1, PHP_INT_MAX); - $this->configValueFactory = $this->createMock( - ValueFactory::class - ); + $this->globalConfig->expects($this->once()) + ->method('getValue') + ->with('sales_email/general/sending_limit') + ->willReturn($pageSize); - $this->object = $objectManager->getObject( - EmailSenderHandler::class, - [ - 'emailSender' => $this->emailSender, - 'entityResource' => $this->entityResource, - 'entityCollection' => $this->entityCollection, - 'globalConfig' => $this->globalConfig, - 'identityContainer' => $this->identityContainerMock, - 'storeManager' => $this->storeManagerMock, - 'configValueFactory' => $this->configValueFactory, - 'modifyStartFromDate' => $this->modifyStartFromDate - ] - ); + $this->entityCollection->expects($this->once()) + ->method('setPageSize') + ->with($pageSize); } /** - * @param int $configValue - * @param array|null $collectionItems - * @param bool|null $emailSendingResult - * @dataProvider executeDataProvider + * Send email by store scope. + * * @return void */ - public function testExecute($configValue, $collectionItems, $emailSendingResult) + private function sendEmailByStoreScope(): void { - $path = 'sales_email/general/async_sending'; + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); - $this->globalConfig - ->expects($this->at(0)) - ->method('getValue') - ->with($path) - ->willReturn($configValue); + $this->storeManager->expects($this->once()) + ->method('getStores') + ->willReturn([$store, $store]); - if ($configValue) { - $this->entityCollection - ->expects($this->at(0)) - ->method('addFieldToFilter') - ->with('send_email', ['eq' => 1]); - - $this->entityCollection - ->expects($this->at(1)) - ->method('addFieldToFilter') - ->with('email_sent', ['null' => true]); - - $nowDate = date('Y-m-d H:i:s'); - $fromDate = date('Y-m-d H:i:s', strtotime($nowDate . ' ' . $this->modifyStartFromDate)); - $this->entityCollection - ->expects($this->at(2)) - ->method('addFieldToFilter') - ->with('created_at', ['from' => $fromDate]); - - $this->entityCollection - ->expects($this->any()) - ->method('addAttributeToSelect') - ->with('store_id') - ->willReturnSelf(); - - $selectMock = $this->createMock(Select::class); - - $selectMock - ->expects($this->atLeastOnce()) - ->method('group') - ->with('store_id') - ->willReturnSelf(); - - $this->entityCollection - ->expects($this->any()) - ->method('getSelect') - ->willReturn($selectMock); - - $this->entityCollection - ->expects($this->any()) - ->method('getItems') - ->willReturn($collectionItems); - - /** @var Value|Encrypted|MockObject $valueMock */ - $backendModelMock = $this->getMockBuilder(Value::class) - ->disableOriginalConstructor() - ->onlyMethods(['load', 'getId']) - ->addMethods(['getUpdatedAt']) - ->getMock(); - $backendModelMock->expects($this->once())->method('load')->willReturnSelf(); - $backendModelMock->expects($this->once())->method('getId')->willReturn(1); - $backendModelMock->expects($this->once())->method('getUpdatedAt')->willReturn($nowDate); - - $this->configValueFactory->expects($this->once()) - ->method('create') - ->willReturn($backendModelMock); - - if ($collectionItems) { - - /** @var AbstractModel|MockObject $collectionItem */ - $collectionItem = $collectionItems[0]; - - $this->emailSender - ->expects($this->once()) - ->method('send') - ->with($collectionItem, true) - ->willReturn($emailSendingResult); - - $storeMock = $this->createMock(Store::class); - - $this->storeManagerMock - ->expects($this->any()) - ->method('getStore') - ->willReturn($storeMock); - - $this->identityContainerMock - ->expects($this->any()) - ->method('setStore') - ->with($storeMock); - - $this->identityContainerMock - ->expects($this->any()) - ->method('isEnabled') - ->willReturn(true); - - if ($emailSendingResult) { - $collectionItem - ->expects($this->once()) - ->method('setEmailSent') - ->with(true) - ->willReturn($collectionItem); - - $this->entityResource - ->expects($this->once()) - ->method('saveAttribute') - ->with($collectionItem); - } - } - } - - $this->object->sendEmails(); - } + $this->identityContainer->expects($this->exactly(2)) + ->method('setStore') + ->with($store); - /** - * @return array - */ - public function executeDataProvider() - { - $entityModel = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['setEmailSent', 'getOrder'] - ); + $this->identityContainer->expects($this->exactly(2)) + ->method('isEnabled') + ->willReturnOnConsecutiveCalls(false, true); + + $storeId = rand(1, PHP_INT_MAX); + + $store->expects($this->once()) + ->method('getId') + ->willReturn($storeId); + + $this->entityCollection->expects($this->at(4)) + ->method('addFieldToFilter') + ->with('store_id', $storeId); + + $collectionItem = $this->getMockBuilder(DataObject::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->entityCollection->expects($this->once()) + ->method('getItems') + ->willReturn([$collectionItem]); - return [ - [ - 'configValue' => 1, - 'collectionItems' => [clone $entityModel], - 'emailSendingResult' => true, - ], - [ - 'configValue' => 1, - 'collectionItems' => [clone $entityModel], - 'emailSendingResult' => false, - ], - [ - 'configValue' => 1, - 'collectionItems' => [], - 'emailSendingResult' => null, - ], - [ - 'configValue' => 0, - 'collectionItems' => null, - 'emailSendingResult' => null, - ] - ]; + $this->emailSender->expects($this->once()) + ->method('send') + ->with($collectionItem, true); } }