diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php index a461279d1b..e87b21c79b 100644 --- a/mailpoet/lib/DI/ContainerConfigurator.php +++ b/mailpoet/lib/DI/ContainerConfigurator.php @@ -417,6 +417,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Newsletter\Scheduler\ReEngagementScheduler::class)->setPublic(true); $container->autowire(\MailPoet\Newsletter\Sending\ScheduledTasksRepository::class)->setPublic(true); $container->autowire(\MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository::class)->setPublic(true); + $container->autowire(\MailPoet\Newsletter\Sending\ScheduledTaskSubscribersListingRepository::class)->setPublic(true); $container->autowire(\MailPoet\Newsletter\Sending\SendingQueuesRepository::class)->setPublic(true); $container->autowire(\MailPoet\Newsletter\ViewInBrowser\ViewInBrowserController::class)->setPublic(true); $container->autowire(\MailPoet\Newsletter\ViewInBrowser\ViewInBrowserRenderer::class)->setPublic(true); diff --git a/mailpoet/lib/Newsletter/Sending/ScheduledTaskSubscribersListingRepository.php b/mailpoet/lib/Newsletter/Sending/ScheduledTaskSubscribersListingRepository.php new file mode 100644 index 0000000000..93c19d0993 --- /dev/null +++ b/mailpoet/lib/Newsletter/Sending/ScheduledTaskSubscribersListingRepository.php @@ -0,0 +1,127 @@ +queryBuilder; + $this->applyFromClause($queryBuilder); + $this->applyParameters($queryBuilder, $definition->getParameters()); + + // total count + $countQueryBuilder = clone $queryBuilder; + $countQueryBuilder->select('COUNT(sts.subscriber) AS subscriberCount'); + $totalCount = intval($countQueryBuilder->getQuery()->getSingleScalarResult()); + + // Sent count + $sentCountQuery = clone $queryBuilder; + $sentCountQuery->select('COUNT(sts.subscriber) AS subscriberCount'); + $sentCountQuery->andWhere('sts.processed = :processedStatus'); + $sentCountQuery->andWhere('sts.failed = :failedStatus'); + $sentCountQuery->setParameter('processedStatus', ScheduledTaskSubscriberEntity::STATUS_PROCESSED); + $sentCountQuery->setParameter('failedStatus', ScheduledTaskSubscriberEntity::FAIL_STATUS_OK); + $sentCount = intval($sentCountQuery->getQuery()->getSingleScalarResult()); + + // Failed count + $failedCountQuery = clone $queryBuilder; + $failedCountQuery->select('COUNT(sts.subscriber) AS subscriberCount'); + $failedCountQuery->andWhere('sts.failed = :failedStatus'); + $failedCountQuery->setParameter('failedStatus', ScheduledTaskSubscriberEntity::FAIL_STATUS_FAILED); + $failedCount = intval($failedCountQuery->getQuery()->getSingleScalarResult()); + + // Unprocessed count + $unprocessedCountQuery = clone $queryBuilder; + $unprocessedCountQuery->select('COUNT(sts.subscriber) AS subscriberCount'); + $unprocessedCountQuery->andWhere('sts.processed = :processedStatus'); + $unprocessedCountQuery->setParameter('processedStatus', ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED); + $unprocessedCount = intval($unprocessedCountQuery->getQuery()->getSingleScalarResult()); + + return [ + [ + 'name' => 'all', + 'label' => __('All', 'mailpoet'), + 'count' => $totalCount, + ], + [ + 'name' => ScheduledTaskSubscriberEntity::SENDING_STATUS_SENT, + 'label' => __('Sent', 'mailpoet'), + 'count' => $sentCount, + ], + [ + 'name' => ScheduledTaskSubscriberEntity::SENDING_STATUS_FAILED, + 'label' => __('Failed', 'mailpoet'), + 'count' => $failedCount, + ], + [ + 'name' => ScheduledTaskSubscriberEntity::SENDING_STATUS_UNPROCESSED, + 'label' => __('Unprocessed', 'mailpoet'), + 'count' => $unprocessedCount, + ], + ]; + } + + protected function applySelectClause(QueryBuilder $queryBuilder) { + $queryBuilder->select("PARTIAL sts.{task,subscriber,processed,failed,error,createdAt,updatedAt}, PARTIAL s.{id, email, firstName, lastName}"); + } + + protected function applyFromClause(QueryBuilder $queryBuilder) { + $queryBuilder->from(ScheduledTaskSubscriberEntity::class, 'sts') + ->leftJoin('sts.subscriber', 's'); + } + + protected function applyGroup(QueryBuilder $queryBuilder, string $group) { + if ($group === ScheduledTaskSubscriberEntity::SENDING_STATUS_SENT) { + $queryBuilder->andWhere('sts.processed = :processedStatus'); + $queryBuilder->andWhere('sts.failed = :failedStatus'); + $queryBuilder->setParameter('processedStatus', ScheduledTaskSubscriberEntity::STATUS_PROCESSED); + $queryBuilder->setParameter('failedStatus', ScheduledTaskSubscriberEntity::FAIL_STATUS_OK); + } elseif ($group === ScheduledTaskSubscriberEntity::SENDING_STATUS_FAILED) { + $queryBuilder->andWhere('sts.failed = :failedStatus'); + $queryBuilder->setParameter('failedStatus', ScheduledTaskSubscriberEntity::FAIL_STATUS_FAILED); + } elseif ($group === ScheduledTaskSubscriberEntity::SENDING_STATUS_UNPROCESSED) { + $queryBuilder->andWhere('sts.processed = :processedStatus'); + $queryBuilder->setParameter('processedStatus', ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED); + } + } + + protected function applySorting(QueryBuilder $queryBuilder, string $sortBy, string $sortOrder) { + // ScheduledTaskSubscriber doesn't have id column so the default fallback value 'id' + // generated in MailPoet\Listing\Handler needs to be changed to something else + if ($sortBy === 'id') { + $sortBy = 'subscriber'; + } + $queryBuilder->addOrderBy("sts.$sortBy", $sortOrder); + } + + protected function applySearch(QueryBuilder $queryBuilder, string $search) { + $search = Helpers::escapeSearch($search); + $queryBuilder + ->andWhere('s.email LIKE :search or s.firstName LIKE :search or s.lastName LIKE :search') + ->setParameter('search', "%$search%"); + } + + protected function applyFilters(QueryBuilder $queryBuilder, array $filters) { + // the parent class requires this method, but scheduled task subscribers listing doesn't currently support this feature. + } + + protected function applyParameters(QueryBuilder $queryBuilder, array $parameters) { + if (isset($parameters['task_ids']) && !empty($parameters['task_ids'])) { + $queryBuilder->andWhere('sts.task IN (:taskIds)') + ->setParameter('taskIds', $parameters['task_ids']); + } + } + + public function getCount(ListingDefinition $definition): int { + $queryBuilder = clone $this->queryBuilder; + $this->applyFromClause($queryBuilder); + $this->applyConstraints($queryBuilder, $definition); + $queryBuilder->select("COUNT(DISTINCT sts.subscriber)"); + return intval($queryBuilder->getQuery()->getSingleScalarResult()); + } +} diff --git a/mailpoet/tests/DataFactories/ScheduledTaskSubscriber.php b/mailpoet/tests/DataFactories/ScheduledTaskSubscriber.php new file mode 100644 index 0000000000..769c5a9e47 --- /dev/null +++ b/mailpoet/tests/DataFactories/ScheduledTaskSubscriber.php @@ -0,0 +1,40 @@ +entityManager = $diContainer->get(EntityManager::class); + } + + public function createUnprocessed(ScheduledTaskEntity $task, SubscriberEntity $subscriberEntity): ScheduledTaskSubscriberEntity { + $taskSubscriber = new ScheduledTaskSubscriberEntity($task, $subscriberEntity); + $this->entityManager->persist($taskSubscriber); + $this->entityManager->flush(); + return $taskSubscriber; + } + + public function createProcessed(ScheduledTaskEntity $task, SubscriberEntity $subscriberEntity): ScheduledTaskSubscriberEntity { + $taskSubscriber = new ScheduledTaskSubscriberEntity($task, $subscriberEntity, 1); + $this->entityManager->persist($taskSubscriber); + $this->entityManager->flush(); + return $taskSubscriber; + } + + public function createFailed(ScheduledTaskEntity $task, SubscriberEntity $subscriberEntity, string $error = null): ScheduledTaskSubscriberEntity { + $taskSubscriber = new ScheduledTaskSubscriberEntity($task, $subscriberEntity, 1, 1, $error); + $this->entityManager->persist($taskSubscriber); + $this->entityManager->flush(); + return $taskSubscriber; + } +} diff --git a/mailpoet/tests/integration/Newsletter/Sending/ScheduledTaskSubscribersListingRepositoryTest.php b/mailpoet/tests/integration/Newsletter/Sending/ScheduledTaskSubscribersListingRepositoryTest.php new file mode 100644 index 0000000000..3426c02628 --- /dev/null +++ b/mailpoet/tests/integration/Newsletter/Sending/ScheduledTaskSubscribersListingRepositoryTest.php @@ -0,0 +1,139 @@ +cleanup(); + $this->listingHandler = $this->diContainer->get(Handler::class); + $this->repository = $this->diContainer->get(ScheduledTaskSubscribersListingRepository::class); + $this->scheduledTaskFactory = new ScheduledTaskFactory(); + $this->subscriberFactory = new SubscriberFactory(); + $this->taskSubscriberFactory = new TaskSubscriberFactory(); + + // Subscribers + $subscriberUnprocessed = $this->subscriberFactory->withEmail('subscriberUprocessed@email.com')->create(); + $subscriberProcessed = $this->subscriberFactory->withEmail('subscriberProcessed@email.com')->create(); + $subscriberFailed = $this->subscriberFactory->withEmail('subscriberFailed@email.com')->create(); + $this->subscriberFactory->withEmail('subscriberNotIncluded@email.com')->create(); + + // Scheduled Task + $this->scheduledTask = $this->scheduledTaskFactory->create('sending', ScheduledTaskEntity::STATUS_COMPLETED, Carbon::now()->subDay()); + + // Task Subscribers + $this->taskSubscriberFactory->createUnprocessed($this->scheduledTask, $subscriberUnprocessed); + $this->taskSubscriberFactory->createProcessed($this->scheduledTask, $subscriberProcessed); + $this->taskSubscriberFactory->createFailed($this->scheduledTask, $subscriberFailed, 'Error Message'); + } + + public function testItGenerateCorrectGroups() { + $listingData = [ + 'group' => 'all', + 'params' => [ 'task_ids' => [$this->scheduledTask->getId()]], + ]; + [$all, $sent, $failed, $unprocessed] = $this->repository->getGroups($this->listingHandler->getListingDefinition($listingData)); + expect($all['name'])->equals('all'); + expect($all['label'])->equals('All'); + expect($all['count'])->equals(3); + + expect($sent['name'])->equals('sent'); + expect($sent['label'])->equals('Sent'); + expect($sent['count'])->equals(1); + + expect($failed['name'])->equals('failed'); + expect($failed['label'])->equals('Failed'); + expect($failed['count'])->equals(1); + + expect($unprocessed['name'])->equals('unprocessed'); + expect($unprocessed['label'])->equals('Unprocessed'); + expect($unprocessed['count'])->equals(1); + } + + public function testItReturnCorrectDataAndCountForGroupAll() { + $listingData = [ + 'group' => 'all', + 'params' => [ 'task_ids' => [$this->scheduledTask->getId()]], + ]; + $tasksSubscribers = $this->repository->getData($this->listingHandler->getListingDefinition($listingData)); + $count = $this->repository->getCount($this->listingHandler->getListingDefinition($listingData)); + expect($tasksSubscribers)->count(3); + expect($count)->equals(3); + + $this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $tasksSubscribers[0]); + $this->assertInstanceOf(SubscriberEntity::class, $tasksSubscribers[0]->getSubscriber()); + expect($tasksSubscribers[0]->getSubscriber()->getEmail())->equals('subscriberUprocessed@email.com'); + + $this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $tasksSubscribers[1]); + $this->assertInstanceOf(SubscriberEntity::class, $tasksSubscribers[1]->getSubscriber()); + expect($tasksSubscribers[1]->getSubscriber()->getEmail())->equals('subscriberProcessed@email.com'); + + $this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $tasksSubscribers[2]); + $this->assertInstanceOf(SubscriberEntity::class, $tasksSubscribers[2]->getSubscriber()); + expect($tasksSubscribers[2]->getSubscriber()->getEmail())->equals('subscriberFailed@email.com'); + } + + public function testItCanFilterByGroup() { + $listingData = [ + 'group' => 'failed', + 'params' => [ 'task_ids' => [$this->scheduledTask->getId()]], + ]; + $tasksSubscribers = $this->repository->getData($this->listingHandler->getListingDefinition($listingData)); + $count = $this->repository->getCount($this->listingHandler->getListingDefinition($listingData)); + expect($tasksSubscribers)->count(1); + expect($count)->equals(1); + + $this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $tasksSubscribers[0]); + $this->assertInstanceOf(SubscriberEntity::class, $tasksSubscribers[0]->getSubscriber()); + expect($tasksSubscribers[0]->getSubscriber()->getEmail())->equals('subscriberFailed@email.com'); + } + + public function testItCanSearchByEmail() { + $listingData = [ + 'group' => 'all', + 'params' => [ 'task_ids' => [$this->scheduledTask->getId()]], + 'search' => 'subscriberProcessed@', + ]; + $tasksSubscribers = $this->repository->getData($this->listingHandler->getListingDefinition($listingData)); + $count = $this->repository->getCount($this->listingHandler->getListingDefinition($listingData)); + expect($tasksSubscribers)->count(1); + expect($count)->equals(1); + + $this->assertInstanceOf(ScheduledTaskSubscriberEntity::class, $tasksSubscribers[0]); + $this->assertInstanceOf(SubscriberEntity::class, $tasksSubscribers[0]->getSubscriber()); + expect($tasksSubscribers[0]->getSubscriber()->getEmail())->equals('subscriberProcessed@email.com'); + } + + public function cleanup() { + $this->truncateEntity(ScheduledTaskEntity::class); + $this->truncateEntity(SubscriberEntity::class); + $this->truncateEntity(ScheduledTaskSubscriberEntity::class); + } +}