Refactor subscriber stats to accept startTime param

MAILPOET-5508
This commit is contained in:
John Oleksowicz
2023-08-08 11:52:13 -05:00
committed by Aschepikov
parent fad0880436
commit 06df45bb55
7 changed files with 232 additions and 37 deletions

View File

@ -9,6 +9,7 @@ use MailPoet\Entities\SubscriberEntity;
use MailPoet\Newsletter\Statistics\WooCommerceRevenue; use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
use MailPoet\Subscribers\Statistics\SubscriberStatisticsRepository; use MailPoet\Subscribers\Statistics\SubscriberStatisticsRepository;
use MailPoet\Subscribers\SubscribersRepository; use MailPoet\Subscribers\SubscribersRepository;
use MailPoetVendor\Carbon\Carbon;
class SubscriberStats extends APIEndpoint { class SubscriberStats extends APIEndpoint {
public $permissions = [ public $permissions = [
@ -38,7 +39,8 @@ class SubscriberStats extends APIEndpoint {
APIError::NOT_FOUND => __('This subscriber does not exist.', 'mailpoet'), APIError::NOT_FOUND => __('This subscriber does not exist.', 'mailpoet'),
]); ]);
} }
$statistics = $this->subscribersStatisticsRepository->getStatistics($subscriber); $oneYearAgo = (new Carbon())->subYear();
$statistics = $this->subscribersStatisticsRepository->getStatistics($subscriber, $oneYearAgo);
$response = [ $response = [
'email' => $subscriber->getEmail(), 'email' => $subscriber->getEmail(),
'total_sent' => $statistics->getTotalSentCount(), 'total_sent' => $statistics->getTotalSentCount(),

View File

@ -7,6 +7,7 @@ use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\StatisticsOpenEntity; use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Entities\SubscriberEntity; use MailPoet\Entities\SubscriberEntity;
use MailPoet\Subscribers\Statistics\SubscriberStatisticsRepository; use MailPoet\Subscribers\Statistics\SubscriberStatisticsRepository;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityManager; use MailPoetVendor\Doctrine\ORM\EntityManager;
use MailPoetVendor\Doctrine\ORM\QueryBuilder; use MailPoetVendor\Doctrine\ORM\QueryBuilder;
@ -32,13 +33,14 @@ class StatisticsOpensRepository extends Repository {
public function recalculateSubscriberScore(SubscriberEntity $subscriber): void { public function recalculateSubscriberScore(SubscriberEntity $subscriber): void {
$subscriber->setEngagementScoreUpdatedAt(new \DateTimeImmutable()); $subscriber->setEngagementScoreUpdatedAt(new \DateTimeImmutable());
$newslettersSentCount = $this->subscriberStatisticsRepository->getTotalSentCount($subscriber); $yearAgo = Carbon::now()->subYear();
$newslettersSentCount = $this->subscriberStatisticsRepository->getTotalSentCount($subscriber, $yearAgo);
if ($newslettersSentCount < 3) { if ($newslettersSentCount < 3) {
$subscriber->setEngagementScore(null); $subscriber->setEngagementScore(null);
$this->entityManager->flush(); $this->entityManager->flush();
return; return;
} }
$opensCount = $this->subscriberStatisticsRepository->getStatisticsOpenCount($subscriber); $opensCount = $this->subscriberStatisticsRepository->getStatisticsOpenCount($subscriber, $yearAgo);
$score = ($opensCount / $newslettersSentCount) * 100; $score = ($opensCount / $newslettersSentCount) * 100;
$subscriber->setEngagementScore($score); $subscriber->setEngagementScore($score);
$this->entityManager->flush(); $this->entityManager->flush();

View File

@ -35,55 +35,64 @@ class SubscriberStatisticsRepository extends Repository {
return SubscriberEntity::class; return SubscriberEntity::class;
} }
public function getStatistics(SubscriberEntity $subscriber) { public function getStatistics(SubscriberEntity $subscriber, ?Carbon $startTime = null) {
return new SubscriberStatistics( return new SubscriberStatistics(
$this->getStatisticsClickCount($subscriber), $this->getStatisticsClickCount($subscriber, $startTime),
$this->getStatisticsOpenCount($subscriber), $this->getStatisticsOpenCount($subscriber, $startTime),
$this->getStatisticsMachineOpenCount($subscriber), $this->getStatisticsMachineOpenCount($subscriber, $startTime),
$this->getTotalSentCount($subscriber), $this->getTotalSentCount($subscriber, $startTime),
$this->getWooCommerceRevenue($subscriber) $this->getWooCommerceRevenue($subscriber, $startTime)
); );
} }
public function getStatisticsClickCount(SubscriberEntity $subscriber): int { public function getStatisticsClickCount(SubscriberEntity $subscriber, ?Carbon $startTime = null): int {
$dateTime = (new Carbon())->subYear(); $queryBuilder = $this->getStatisticsCountQuery(StatisticsClickEntity::class, $subscriber);
return (int)$this->getStatisticsCountQuery(StatisticsClickEntity::class, $subscriber) if ($startTime) {
->andWhere('stats.createdAt > :dateTime') $queryBuilder
->setParameter('dateTime', $dateTime) ->andWhere('stats.createdAt >= :dateTime')
->setParameter('dateTime', $startTime);
}
return (int)$queryBuilder
->getQuery() ->getQuery()
->getSingleScalarResult(); ->getSingleScalarResult();
} }
public function getStatisticsOpenCountQuery(SubscriberEntity $subscriber): QueryBuilder { public function getStatisticsOpenCountQuery(SubscriberEntity $subscriber, ?Carbon $startTime = null): QueryBuilder {
$dateTime = (new Carbon())->subYear(); $queryBuilder = $this->getStatisticsCountQuery(StatisticsOpenEntity::class, $subscriber)
return $this->getStatisticsCountQuery(StatisticsOpenEntity::class, $subscriber) ->join('stats.newsletter', 'newsletter');
->join('stats.newsletter', 'newsletter') if ($startTime) {
->andWhere('(newsletter.sentAt > :dateTime OR newsletter.sentAt IS NULL)') $queryBuilder
->andWhere('stats.createdAt > :dateTime') ->andWhere('(newsletter.sentAt >= :dateTime OR newsletter.sentAt IS NULL)')
->setParameter('dateTime', $dateTime); ->andWhere('stats.createdAt >= :dateTime')
->setParameter('dateTime', $startTime);
}
return $queryBuilder;
} }
public function getStatisticsOpenCount(SubscriberEntity $subscriber): int { public function getStatisticsOpenCount(SubscriberEntity $subscriber, ?Carbon $startTime = null): int {
return (int)$this->getStatisticsOpenCountQuery($subscriber) return (int)$this->getStatisticsOpenCountQuery($subscriber, $startTime)
->andWhere('(stats.userAgentType = :userAgentType)') ->andWhere('(stats.userAgentType = :userAgentType)')
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN) ->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN)
->getQuery() ->getQuery()
->getSingleScalarResult(); ->getSingleScalarResult();
} }
public function getStatisticsMachineOpenCount(SubscriberEntity $subscriber): int { public function getStatisticsMachineOpenCount(SubscriberEntity $subscriber, ?Carbon $startTime = null): int {
return (int)$this->getStatisticsOpenCountQuery($subscriber) return (int)$this->getStatisticsOpenCountQuery($subscriber, $startTime)
->andWhere('(stats.userAgentType = :userAgentType)') ->andWhere('(stats.userAgentType = :userAgentType)')
->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_MACHINE) ->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_MACHINE)
->getQuery() ->getQuery()
->getSingleScalarResult(); ->getSingleScalarResult();
} }
public function getTotalSentCount(SubscriberEntity $subscriber): int { public function getTotalSentCount(SubscriberEntity $subscriber, ?Carbon $startTime = null): int {
$dateTime = (new Carbon())->subYear(); $queryBuilder = $this->getStatisticsCountQuery(StatisticsNewsletterEntity::class, $subscriber);
return $this->getStatisticsCountQuery(StatisticsNewsletterEntity::class, $subscriber) if ($startTime) {
->andWhere('stats.sentAt > :dateTime') $queryBuilder
->setParameter('dateTime', $dateTime) ->andWhere('stats.sentAt >= :dateTime')
->setParameter('dateTime', $startTime);
}
return $queryBuilder
->getQuery() ->getQuery()
->getSingleScalarResult(); ->getSingleScalarResult();
} }
@ -96,24 +105,27 @@ class SubscriberStatisticsRepository extends Repository {
->setParameter('subscriber', $subscriber); ->setParameter('subscriber', $subscriber);
} }
public function getWooCommerceRevenue(SubscriberEntity $subscriber) { public function getWooCommerceRevenue(SubscriberEntity $subscriber, ?Carbon $startTime = null): ?WooCommerceRevenue {
if (!$this->wcHelper->isWooCommerceActive()) { if (!$this->wcHelper->isWooCommerceActive()) {
return null; return null;
} }
$dateTime = (new Carbon())->subYear();
$currency = $this->wcHelper->getWoocommerceCurrency(); $currency = $this->wcHelper->getWoocommerceCurrency();
$purchases = $this->entityManager->createQueryBuilder() $queryBuilder = $this->entityManager->createQueryBuilder()
->select('stats.orderPriceTotal') ->select('stats.orderPriceTotal')
->from(StatisticsWooCommercePurchaseEntity::class, 'stats') ->from(StatisticsWooCommercePurchaseEntity::class, 'stats')
->where('stats.subscriber = :subscriber') ->where('stats.subscriber = :subscriber')
->andWhere('stats.orderCurrency = :currency') ->andWhere('stats.orderCurrency = :currency')
->andWhere('stats.createdAt > :dateTime')
->setParameter('subscriber', $subscriber) ->setParameter('subscriber', $subscriber)
->setParameter('currency', $currency) ->setParameter('currency', $currency)
->setParameter('dateTime', $dateTime) ->groupBy('stats.orderId, stats.orderPriceTotal');
->groupBy('stats.orderId, stats.orderPriceTotal') if ($startTime) {
$queryBuilder
->andWhere('stats.createdAt >= :dateTime')
->setParameter('dateTime', $startTime);
}
$purchases =
$queryBuilder
->getQuery() ->getQuery()
->getResult(); ->getResult();
$sum = array_sum(array_column($purchases, 'orderPriceTotal')); $sum = array_sum(array_column($purchases, 'orderPriceTotal'));

View File

@ -26,6 +26,7 @@ class StatisticsClicks {
) { ) {
$this->data = [ $this->data = [
'count' => 1, 'count' => 1,
'createdAt' => null,
]; ];
$this->newsletterLink = $newsletterLink; $this->newsletterLink = $newsletterLink;
$this->subscriber = $subscriber; $this->subscriber = $subscriber;
@ -36,6 +37,11 @@ class StatisticsClicks {
return $this; return $this;
} }
public function withCreatedAt(\DateTimeInterface $createdAt) {
$this->data['createdAt'] = $createdAt;
return $this;
}
public function create(): StatisticsClickEntity { public function create(): StatisticsClickEntity {
$entityManager = ContainerWrapper::getInstance()->get(EntityManager::class); $entityManager = ContainerWrapper::getInstance()->get(EntityManager::class);
$newsletter = $this->newsletterLink->getNewsletter(); $newsletter = $this->newsletterLink->getNewsletter();
@ -49,6 +55,9 @@ class StatisticsClicks {
$this->newsletterLink, $this->newsletterLink,
$this->data['count'] $this->data['count']
); );
if ($this->data['createdAt'] instanceof \DateTimeInterface) {
$entity->setCreatedAt($this->data['createdAt']);
}
$entityManager->persist($entity); $entityManager->persist($entity);
$entityManager->flush(); $entityManager->flush();
return $entity; return $entity;

View File

@ -33,6 +33,11 @@ class StatisticsOpens {
return $this; return $this;
} }
public function withCreatedAt(\DateTimeInterface $createdAt): self {
$this->data['createdAt'] = $createdAt;
return $this;
}
public function create(): StatisticsOpenEntity { public function create(): StatisticsOpenEntity {
$entityManager = ContainerWrapper::getInstance()->get(EntityManager::class); $entityManager = ContainerWrapper::getInstance()->get(EntityManager::class);
$queue = $this->newsletter->getLatestQueue(); $queue = $this->newsletter->getLatestQueue();
@ -43,6 +48,9 @@ class StatisticsOpens {
$this->subscriber $this->subscriber
); );
$entity->setUserAgentType($this->data['userAgentType'] ?? UserAgentEntity::USER_AGENT_TYPE_HUMAN); $entity->setUserAgentType($this->data['userAgentType'] ?? UserAgentEntity::USER_AGENT_TYPE_HUMAN);
if (($this->data['createdAt'] ?? null) instanceof \DateTimeInterface) {
$entity->setCreatedAt($this->data['createdAt']);
}
$entityManager->persist($entity); $entityManager->persist($entity);
$entityManager->flush(); $entityManager->flush();
return $entity; return $entity;

View File

@ -33,6 +33,11 @@ class StatisticsWooCommercePurchases {
$this->click = $click; $this->click = $click;
} }
public function withCreatedAt(\DateTimeInterface $createdAt): self {
$this->data['createdAt'] = $createdAt;
return $this;
}
public function create(): StatisticsWooCommercePurchaseEntity { public function create(): StatisticsWooCommercePurchaseEntity {
$newsletter = $this->click->getNewsletter(); $newsletter = $this->click->getNewsletter();
Assert::assertInstanceOf(NewsletterEntity::class, $newsletter); Assert::assertInstanceOf(NewsletterEntity::class, $newsletter);
@ -48,6 +53,9 @@ class StatisticsWooCommercePurchases {
(float)$this->data['order_price_total'] (float)$this->data['order_price_total']
); );
$entity->setSubscriber($this->subscriber); $entity->setSubscriber($this->subscriber);
if (($this->data['createdAt'] ?? null) instanceof \DateTimeInterface) {
$entity->setCreatedAt($this->data['createdAt']);
}
$entityManager = ContainerWrapper::getInstance()->get(EntityManager::class); $entityManager = ContainerWrapper::getInstance()->get(EntityManager::class);
$entityManager->persist($entity); $entityManager->persist($entity);

View File

@ -0,0 +1,154 @@
<?php declare(strict_types = 1);
namespace MailPoet\Subscribers\Statistics;
use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
use MailPoet\Test\DataFactories\Newsletter;
use MailPoet\Test\DataFactories\NewsletterLink;
use MailPoet\Test\DataFactories\StatisticsClicks;
use MailPoet\Test\DataFactories\StatisticsNewsletters;
use MailPoet\Test\DataFactories\StatisticsOpens;
use MailPoet\Test\DataFactories\StatisticsWooCommercePurchases;
use MailPoet\Test\DataFactories\Subscriber;
use MailPoetVendor\Carbon\Carbon;
/**
* @group woo
*/
class SubscriberStatisticsRepositoryTest extends \MailPoetTest {
/** @var SubscriberStatisticsRepository */
private $repository;
public function _before() {
parent::_before();
$this->repository = $this->diContainer->get(SubscriberStatisticsRepository::class);
}
public function testItFetchesClickCount(): void {
$yearAgo = Carbon::now()->subYear();
$monthAgo = Carbon::now()->subMonth();
$subscriber = (new Subscriber())->create();
$newsletter = (new Newsletter())->withSendingQueue()->create();
$link = (new NewsletterLink($newsletter))->create();
$click = (new StatisticsClicks($link, $subscriber))
->withCreatedAt($monthAgo)
->create();
$newsletter2 = (new Newsletter())->withSendingQueue()->create();
$link2 = (new NewsletterLink($newsletter2))->create();
$click2 = (new StatisticsClicks($link2, $subscriber))
->withCreatedAt($yearAgo)
->create();
$newsletter3 = (new Newsletter())->withSendingQueue()->create();
$link3 = (new NewsletterLink($newsletter3))->create();
$click3 = (new StatisticsClicks($link3, $subscriber))
->withCreatedAt(Carbon::now()->subYears(5))
->create();
$lifetimeCount = $this->repository->getStatisticsClickCount($subscriber, null);
expect($lifetimeCount)->equals(3);
$yearCount = $this->repository->getStatisticsClickCount($subscriber, $yearAgo);
expect($yearCount)->equals(2);
$monthCount = $this->repository->getStatisticsClickCount($subscriber, $monthAgo);
expect($monthCount)->equals(1);
expect($this->repository->getStatisticsClickCount($subscriber, Carbon::now()->subDays(27)))->equals(0);
}
public function testItFetchesOpenCount(): void {
$subscriber = (new Subscriber())->create();
$newsletter = (new Newsletter())->withSendingQueue()->create();
$yearAgo = Carbon::now()->subYear();
$open = (new StatisticsOpens($newsletter, $subscriber))->withCreatedAt($yearAgo)->create();
expect($this->repository->getStatisticsOpenCount($subscriber, null))->equals(1);
expect($this->repository->getStatisticsOpenCount($subscriber, $yearAgo))->equals(1);
expect($this->repository->getStatisticsOpenCount($subscriber, Carbon::now()->subMonth()))->equals(0);
expect($this->repository->getStatisticsMachineOpenCount($subscriber, null))->equals(0);
}
public function testItFetchesMachineOpenCount(): void {
$subscriber = (new Subscriber())->create();
$newsletter = (new Newsletter())->withSendingQueue()->create();
$yearAgo = Carbon::now()->subYear();
$open = (new StatisticsOpens($newsletter, $subscriber))->withMachineUserAgentType()->withCreatedAt($yearAgo)->create();
expect($this->repository->getStatisticsMachineOpenCount($subscriber, null))->equals(1);
expect($this->repository->getStatisticsMachineOpenCount($subscriber, $yearAgo))->equals(1);
expect($this->repository->getStatisticsMachineOpenCount($subscriber, Carbon::now()->subMonth()))->equals(0);
expect($this->repository->getStatisticsOpenCount($subscriber, null))->equals(0);
}
public function testItFetchesTotalSentCount(): void {
$subscriber = (new Subscriber())->create();
$twoYearsAgo = Carbon::now()->subYears(2);
$yearAgo = Carbon::now()->subYear();
$monthAgo = Carbon::now()->subMonth();
$newsletter = (new Newsletter())->withSendingQueue()->create();
$newsletter2 = (new Newsletter())->withSendingQueue()->create();
$newsletter3 = (new Newsletter())->withSendingQueue()->create();
$newsletterSendStat = (new StatisticsNewsletters($newsletter, $subscriber))->withSentAt($twoYearsAgo)->create();
$newsletterSendStat = (new StatisticsNewsletters($newsletter2, $subscriber))->withSentAt($yearAgo)->create();
$newsletterSendStat = (new StatisticsNewsletters($newsletter3, $subscriber))->withSentAt($monthAgo)->create();
expect($this->repository->getTotalSentCount($subscriber, $twoYearsAgo))->equals(3);
expect($this->repository->getTotalSentCount($subscriber, $yearAgo))->equals(2);
expect($this->repository->getTotalSentCount($subscriber, $monthAgo))->equals(1);
expect($this->repository->getTotalSentCount($subscriber, Carbon::now()->subDays(27)))->equals(0);
}
public function testItFetchesWooCommerceRevenueData(): void {
$subscriber = (new Subscriber())->create();
$twoYearsAgo = Carbon::now()->subYears(2);
$yearAgo = Carbon::now()->subYear();
$monthAgo = Carbon::now()->subMonth();
$newsletter = (new Newsletter())->withSendingQueue()->create();
$link = (new NewsletterLink($newsletter))->create();
$click = (new StatisticsClicks($link, $subscriber))
->create();
(new StatisticsWooCommercePurchases($click, [
'id' => 1,
'currency' => 'USD',
'total' => 10.00,
]))->withCreatedAt($twoYearsAgo)->create();
(new StatisticsWooCommercePurchases($click, [
'id' => 2,
'currency' => 'USD',
'total' => 20.00,
]))->withCreatedAt($yearAgo)->create();
(new StatisticsWooCommercePurchases($click, [
'id' => 3,
'currency' => 'USD',
'total' => 30.00,
]))->withCreatedAt($monthAgo)->create();
$twoYearsAgoResult = $this->repository->getWooCommerceRevenue($subscriber, $twoYearsAgo);
$this->assertInstanceOf(WooCommerceRevenue::class, $twoYearsAgoResult);
expect($twoYearsAgoResult->getOrdersCount())->equals(3);
expect($twoYearsAgoResult->getValue())->equals(60.00);
$yearAgoResult = $this->repository->getWooCommerceRevenue($subscriber, $yearAgo);
$this->assertInstanceOf(WooCommerceRevenue::class, $yearAgoResult);
expect($yearAgoResult->getOrdersCount())->equals(2);
expect($yearAgoResult->getValue())->equals(50.00);
$monthAgoResult = $this->repository->getWooCommerceRevenue($subscriber, $monthAgo);
$this->assertInstanceOf(WooCommerceRevenue::class, $monthAgoResult);
expect($monthAgoResult->getOrdersCount())->equals(1);
expect($monthAgoResult->getValue())->equals(30.00);
$daysAgoResult = $this->repository->getWooCommerceRevenue($subscriber, Carbon::now()->subDays(27));
$this->assertInstanceOf(WooCommerceRevenue::class, $daysAgoResult);
expect($daysAgoResult->getOrdersCount())->equals(0);
expect($daysAgoResult->getValue())->equals(0.00);
}
}