diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index b2e3630d1b..6c7abf2afb 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -232,6 +232,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Statistics\Track\WooCommercePurchases::class); $container->autowire(\MailPoet\Statistics\Track\Unsubscribes::class)->setPublic(true); $container->autowire(\MailPoet\Statistics\StatisticsFormsRepository::class)->setPublic(true); + $container->autowire(\MailPoet\Statistics\StatisticsOpensRepository::class)->setPublic(true); $container->autowire(\MailPoet\Statistics\StatisticsUnsubscribesRepository::class); $container->autowire(\MailPoet\Statistics\StatisticsWooCommercePurchasesRepository::class)->setPublic(true); $container->autowire(\MailPoet\Router\Router::class) diff --git a/lib/Statistics/StatisticsOpensRepository.php b/lib/Statistics/StatisticsOpensRepository.php new file mode 100644 index 0000000000..a07b8300dd --- /dev/null +++ b/lib/Statistics/StatisticsOpensRepository.php @@ -0,0 +1,51 @@ + + */ +class StatisticsOpensRepository extends Repository { + protected function getEntityClassName(): string { + return StatisticsOpenEntity::class; + } + + public function recalculateSubscriberScore(SubscriberEntity $subscriber): void { + $subscriber->setEngagementScoreUpdatedAt(new \DateTimeImmutable()); + $newslettersSentCount = $this + ->entityManager + ->createQueryBuilder() + ->select('count(DISTINCT task.id)') + ->from(ScheduledTaskSubscriberEntity::class, 'scheduledTaskSubscriber') + ->where('scheduledTaskSubscriber.subscriber = :subscriber') + ->setParameter('subscriber', $subscriber) + ->join('scheduledTaskSubscriber.task', 'task') + ->andWhere('task.type = :sending') + ->setParameter('sending', Sending::TASK_TYPE) + ->getQuery() + ->getSingleScalarResult(); + if ($newslettersSentCount < 3) { + $this->entityManager->flush(); + return; + } + $opens = $this + ->entityManager + ->createQueryBuilder() + ->select('count(DISTINCT opens.newsletter)') + ->from(StatisticsOpenEntity::class, 'opens') + ->where('opens.subscriber = :subscriberId') + ->setParameter('subscriberId', $subscriber) + ->getQuery() + ->getSingleScalarResult(); + $score = (int)round(($opens / $newslettersSentCount) * 100); + $subscriber->setEngagementScore($score); + $this->entityManager->flush(); + } + +} diff --git a/lib/Statistics/Track/Opens.php b/lib/Statistics/Track/Opens.php index 76dd6c1c7d..9364610230 100644 --- a/lib/Statistics/Track/Opens.php +++ b/lib/Statistics/Track/Opens.php @@ -6,8 +6,16 @@ use MailPoet\Entities\NewsletterEntity; use MailPoet\Entities\SendingQueueEntity; use MailPoet\Entities\SubscriberEntity; use MailPoet\Models\StatisticsOpens; +use MailPoet\Statistics\StatisticsOpensRepository; class Opens { + /** @var StatisticsOpensRepository */ + private $statisticsOpensRepository; + + public function __construct(StatisticsOpensRepository $statisticsOpensRepository) { + $this->statisticsOpensRepository = $statisticsOpensRepository; + } + public function track($data, $displayImage = true) { if (!$data) { return $this->returnResponse($displayImage); @@ -27,6 +35,7 @@ class Opens { $newsletter->getId(), $queue->getId() ); + $this->statisticsOpensRepository->recalculateSubscriberScore($subscriber); } return $this->returnResponse($displayImage); } diff --git a/tests/integration/Statistics/StatisticsOpensRepositoryTest.php b/tests/integration/Statistics/StatisticsOpensRepositoryTest.php new file mode 100644 index 0000000000..0157cc96ee --- /dev/null +++ b/tests/integration/Statistics/StatisticsOpensRepositoryTest.php @@ -0,0 +1,105 @@ +cleanup(); + $this->repository = $this->diContainer->get(StatisticsOpensRepository::class); + $this->subscribersRepository = $this->diContainer->get(SubscribersRepository::class); + } + + public function testItLeavesScoreWhenNoData() { + $subscriber = $this->createSubscriber(); + $this->entityManager->flush(); + $this->repository->recalculateSubscriberScore($subscriber); + $newSubscriber = $this->subscribersRepository->findOneById($subscriber->getId()); + expect($newSubscriber->getEngagementScore())->null(); + expect($newSubscriber->getEngagementScoreUpdatedAt())->notNull(); + } + + public function testItUpdatesScoreTimeWhenNotEnoughNewsletters() { + $subscriber = $this->createSubscriber(); + $subscriber->setEngagementScoreUpdatedAt((new CarbonImmutable())->subDays(4)); + $this->createSendingTask($subscriber); + $this->entityManager->flush(); + $this->repository->recalculateSubscriberScore($subscriber); + $newSubscriber = $this->subscribersRepository->findOneById($subscriber->getId()); + expect($newSubscriber->getEngagementScore())->null(); + expect($newSubscriber->getEngagementScoreUpdatedAt())->notNull(); + $scoreUpdatedAt = new CarbonImmutable($newSubscriber->getEngagementScoreUpdatedAt()); + expect($scoreUpdatedAt->isAfter((new CarbonImmutable())->subMinutes(5)))->true(); + } + + public function testItUpdatesScore() { + $subscriber = $this->createSubscriber(); + $subscriber->setEngagementScoreUpdatedAt((new CarbonImmutable())->subDays(4)); + $this->createSendingTask($subscriber); + $this->createSendingTask($subscriber); + $task = $this->createSendingTask($subscriber); + $newsletter = new NewsletterEntity(); + $this->entityManager->persist($newsletter); + $queue = new SendingQueueEntity(); + $this->entityManager->persist($queue); + $queue->setNewsletter($newsletter); + $queue->setTask($task); + $newsletter->getQueues()->add($queue); + $newsletter->setSubject('newsletter 1'); + $newsletter->setStatus('sent'); + $newsletter->setType(NewsletterEntity::TYPE_STANDARD); + $open = new StatisticsOpenEntity($newsletter, $queue, $subscriber); + $this->entityManager->persist($open); + $this->entityManager->flush(); + + $this->repository->recalculateSubscriberScore($subscriber); + + $newSubscriber = $this->subscribersRepository->findOneById($subscriber->getId()); + expect($newSubscriber->getEngagementScore())->equals(33); + expect($newSubscriber->getEngagementScoreUpdatedAt())->notNull(); + $scoreUpdatedAt = new CarbonImmutable($newSubscriber->getEngagementScoreUpdatedAt()); + expect($scoreUpdatedAt->isAfter((new CarbonImmutable())->subMinutes(5)))->true(); + } + + private function createSubscriber(): SubscriberEntity { + $subscriber = new SubscriberEntity(); + $subscriber->setStatus(SubscriberEntity::STATUS_SUBSCRIBED); + $subscriber->setEmail('subscriber' . rand(0, 10000) . '@example.com'); + $this->entityManager->persist($subscriber); + return $subscriber; + } + + private function createSendingTask(SubscriberEntity $subscriber): ScheduledTaskEntity { + $task = new ScheduledTaskEntity(); + $task->setType(Sending::TASK_TYPE); + $this->entityManager->persist($task); + $sub = new ScheduledTaskSubscriberEntity($task, $subscriber); + $this->entityManager->persist($sub); + return $task; + } + + private function cleanup(): void { + $this->truncateEntity(ScheduledTaskEntity::class); + $this->truncateEntity(ScheduledTaskSubscriberEntity::class); + $this->truncateEntity(StatisticsOpenEntity::class); + $this->truncateEntity(SubscriberEntity::class); + $this->truncateEntity(NewsletterEntity::class); + $this->truncateEntity(SendingQueueEntity::class); + } +}