diff --git a/mailpoet/lib/Homepage/HomepageDataController.php b/mailpoet/lib/Homepage/HomepageDataController.php index c75789438d..0e599ecb27 100644 --- a/mailpoet/lib/Homepage/HomepageDataController.php +++ b/mailpoet/lib/Homepage/HomepageDataController.php @@ -8,6 +8,7 @@ use MailPoet\Automation\Integrations\MailPoet\Actions\SendEmailAction; use MailPoet\Automation\Integrations\MailPoet\Triggers\SomeoneSubscribesTrigger; use MailPoet\Automation\Integrations\MailPoet\Triggers\UserRegistrationTrigger; use MailPoet\Entities\NewsletterEntity; +use MailPoet\Entities\SubscriberEntity; use MailPoet\Form\FormsRepository; use MailPoet\Newsletter\NewslettersRepository; use MailPoet\Services\Bridge; @@ -15,6 +16,8 @@ use MailPoet\Settings\SettingsController; use MailPoet\Subscribers\SubscribersRepository; use MailPoet\Util\License\Features\Subscribers; use MailPoet\WooCommerce\Helper as WooCommerceHelper; +use MailPoet\WP\Functions as WPFunctions; +use MailPoetVendor\Carbon\Carbon; class HomepageDataController { public const UPSELL_SUBSCRIBERS_COUNT_REQUIRED = 600; @@ -40,6 +43,9 @@ class HomepageDataController { /** @var Subscribers */ private $subscribers; + /** @var WPFunctions */ + private $wp; + public function __construct( SettingsController $settingsController, SubscribersRepository $subscribersRepository, @@ -47,6 +53,7 @@ class HomepageDataController { NewslettersRepository $newslettersRepository, AutomationStorage $automationStorage, Subscribers $subscribers, + WPFunctions $wp, WooCommerceHelper $wooCommerceHelper ) { $this->settingsController = $settingsController; @@ -54,6 +61,7 @@ class HomepageDataController { $this->formsRepository = $formsRepository; $this->newslettersRepository = $newslettersRepository; $this->automationStorage = $automationStorage; + $this->wp = $wp; $this->wooCommerceHelper = $wooCommerceHelper; $this->subscribers = $subscribers; } @@ -73,6 +81,7 @@ class HomepageDataController { 'upsellStatus' => $showUpsell ? $this->getUpsellStatus($subscribersCount) : null, 'wooCustomersCount' => $this->wooCommerceHelper->getCustomersCount(), 'subscribersCount' => $subscribersCount, + 'subscribersStats' => $this->getSubscribersStats(), ]; } @@ -129,4 +138,17 @@ class HomepageDataController { 'canDisplay' => !$hasValidMssKey && $subscribersCount > self::UPSELL_SUBSCRIBERS_COUNT_REQUIRED, ]; } + + /** + * @return array{global:array{subscribed:int, unsubscribed:int}} + */ + private function getSubscribersStats(): array { + $thirtyDaysAgo = Carbon::createFromTimestamp($this->wp->currentTime('timestamp'))->subDays(30); + return [ + 'global' => [ + 'subscribed' => $this->subscribersRepository->getCountOfCreatedAfterWithStatues($thirtyDaysAgo, [SubscriberEntity::STATUS_SUBSCRIBED]), + 'unsubscribed' => $this->subscribersRepository->getCountOfUnsubscribedAfter($thirtyDaysAgo), + ], + ]; + } } diff --git a/mailpoet/lib/Subscribers/SubscribersRepository.php b/mailpoet/lib/Subscribers/SubscribersRepository.php index 6b7c4dc9d1..398fed1880 100644 --- a/mailpoet/lib/Subscribers/SubscribersRepository.php +++ b/mailpoet/lib/Subscribers/SubscribersRepository.php @@ -5,6 +5,7 @@ namespace MailPoet\Subscribers; use MailPoet\Config\SubscriberChangesNotifier; use MailPoet\Doctrine\Repository; use MailPoet\Entities\SegmentEntity; +use MailPoet\Entities\StatisticsUnsubscribeEntity; use MailPoet\Entities\SubscriberCustomFieldEntity; use MailPoet\Entities\SubscriberEntity; use MailPoet\Entities\SubscriberSegmentEntity; @@ -370,6 +371,35 @@ class SubscribersRepository extends Repository { return is_int($maxSubscriberId) ? $maxSubscriberId : 0; } + public function getCountOfCreatedAfterWithStatues(\DateTimeInterface $createdAfter, array $statuses): int { + $result = $this->entityManager->createQueryBuilder() + ->select('COUNT(s.id)') + ->from(SubscriberEntity::class, 's') + ->where('s.createdAt > :createdAfter') + ->andWhere('s.status IN (:statuses)') + ->andWhere('s.deletedAt IS NULL') + ->setParameter('createdAfter', $createdAfter) + ->setParameter('statuses', $statuses) + ->getQuery() + ->getSingleScalarResult(); + return intval($result); + } + + public function getCountOfUnsubscribedAfter(\DateTimeInterface $unsubscribedAfter): int { + $result = $this->entityManager->createQueryBuilder() + ->select('COUNT(s.id)') + ->from(StatisticsUnsubscribeEntity::class, 'su') + ->join('su.subscriber', 's') + ->where('s.createdAt > :unsubscribedAfter') + ->andWhere('s.status = :status') + ->andWhere('s.deletedAt IS NULL') + ->setParameter('unsubscribedAfter', $unsubscribedAfter) + ->setParameter('status', SubscriberEntity::STATUS_UNSUBSCRIBED) + ->getQuery() + ->getSingleScalarResult(); + return intval($result); + } + /** * @return int - number of processed ids */ diff --git a/mailpoet/tests/DataFactories/Subscriber.php b/mailpoet/tests/DataFactories/Subscriber.php index 91b124204d..0497e4fb40 100644 --- a/mailpoet/tests/DataFactories/Subscriber.php +++ b/mailpoet/tests/DataFactories/Subscriber.php @@ -136,7 +136,7 @@ class Subscriber { * @return $this */ public function withCreatedAt(DateTimeInterface $createdAt) { - $this->data['setCreatedAt'] = $createdAt; + $this->data['createdAt'] = $createdAt; return $this; } @@ -208,6 +208,7 @@ class Subscriber { if (isset($this->data['subscribedIp'])) $subscriber->setSubscribedIp($this->data['subscribedIp']); if (isset($this->data['confirmedIp'])) $subscriber->setConfirmedIp($this->data['confirmedIp']); if (isset($this->data['unconfirmedData'])) $subscriber->setUnconfirmedData($this->data['unconfirmedData']); + if (isset($this->data['createdAt'])) $subscriber->setCreatedAt($this->data['createdAt']); if (isset($this->data['source'])) { $subscriber->setSource($this->data['source']); } diff --git a/mailpoet/tests/integration/Homepage/HomepageDataControllerTest.php b/mailpoet/tests/integration/Homepage/HomepageDataControllerTest.php index 5ab1e62b25..204f6a8617 100644 --- a/mailpoet/tests/integration/Homepage/HomepageDataControllerTest.php +++ b/mailpoet/tests/integration/Homepage/HomepageDataControllerTest.php @@ -7,11 +7,13 @@ use MailPoet\Entities\NewsletterEntity; use MailPoet\Entities\NewsletterOptionEntity; use MailPoet\Entities\NewsletterOptionFieldEntity; use MailPoet\Entities\SettingEntity; +use MailPoet\Entities\StatisticsUnsubscribeEntity; use MailPoet\Entities\SubscriberEntity; use MailPoet\Settings\SettingsController; use MailPoet\Test\DataFactories\Form; use MailPoet\Test\DataFactories\Newsletter; use MailPoet\Test\DataFactories\Subscriber; +use MailPoetVendor\Carbon\Carbon; class HomepageDataControllerTest extends \MailPoetTest { /** @var HomepageDataController */ @@ -34,6 +36,8 @@ class HomepageDataControllerTest extends \MailPoetTest { expect($data['productDiscoveryStatus'])->notEmpty(); expect($data['wooCustomersCount'])->int(); expect($data['subscribersCount'])->int(); + expect($data['subscribersStats'])->array(); + expect($data['taskListStatus'])->notEmpty(); } public function testItFetchesSenderTaskListStatus(): void { @@ -169,6 +173,37 @@ class HomepageDataControllerTest extends \MailPoetTest { expect($productDiscoveryStatus['setUpAbandonedCartEmail'])->true(); } + public function testItFetchesSubscriberStatsForZeroSubscribers(): void { + $subscribersStats = $this->homepageDataController->getPageData()['subscribersStats']; + expect($subscribersStats['global']['subscribed'])->equals(0); + expect($subscribersStats['global']['unsubscribed'])->equals(0); + } + + public function testItFetchesCorrectGlobalSubscriberStats(): void { + $thirtyOneDaysAgo = Carbon::now()->subDays(31); + $twentyNineDaysAgo = Carbon::now()->subDays(29); + // Old subscribed + (new Subscriber())->withCreatedAt($thirtyOneDaysAgo)->withStatus(SubscriberEntity::STATUS_SUBSCRIBED)->create(); + // New subscribed + (new Subscriber())->withCreatedAt($twentyNineDaysAgo)->withStatus(SubscriberEntity::STATUS_SUBSCRIBED)->create(); + // Unsubscribed long time ago + $oldUnsubscribed = (new Subscriber())->withCreatedAt($thirtyOneDaysAgo)->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)->create(); + $oldUnsubscribedStats = new StatisticsUnsubscribeEntity(null, null, $oldUnsubscribed); + $oldUnsubscribedStats->setCreatedAt($thirtyOneDaysAgo); + $this->entityManager->persist($oldUnsubscribedStats); + $this->entityManager->flush(); + // Freshly unsubscribed + $newUnsubscribed = (new Subscriber())->withCreatedAt($twentyNineDaysAgo)->withStatus(SubscriberEntity::STATUS_UNSUBSCRIBED)->create(); + $newUnsubscribedStats = new StatisticsUnsubscribeEntity(null, null, $newUnsubscribed); + $newUnsubscribedStats->setCreatedAt($twentyNineDaysAgo); + $this->entityManager->persist($newUnsubscribedStats); + $this->entityManager->flush(); + + $subscribersStats = $this->homepageDataController->getPageData()['subscribersStats']; + expect($subscribersStats['global']['subscribed'])->equals(1); + expect($subscribersStats['global']['unsubscribed'])->equals(1); + } + private function cleanup(): void { $this->truncateEntity(SettingEntity::class); $this->truncateEntity(SubscriberEntity::class);