Skip to content

Commit 34e9ae0

Browse files
committed
Add:getSummaryStatistics
1 parent 41e443b commit 34e9ae0

7 files changed

Lines changed: 227 additions & 2 deletions

File tree

src/Domain/Analytics/Repository/UserMessageViewRepository.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PhpList\Core\Domain\Analytics\Repository;
66

7+
use DateTimeInterface;
78
use PhpList\Core\Domain\Common\Repository\AbstractRepository;
89
use PhpList\Core\Domain\Common\Repository\CursorPaginationTrait;
910
use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface;
@@ -21,4 +22,23 @@ public function countByMessageId(int $messageId): int
2122
->getQuery()
2223
->getSingleScalarResult();
2324
}
25+
26+
/**
27+
* Counts views between two dates.
28+
*
29+
* @param DateTimeInterface $start
30+
* @param DateTimeInterface $end
31+
* @return int
32+
*/
33+
public function countBetween(DateTimeInterface $start, DateTimeInterface $end): int
34+
{
35+
return (int) $this->createQueryBuilder('umv')
36+
->select('COUNT(umv.id)')
37+
->where('umv.viewed >= :start')
38+
->andWhere('umv.viewed <= :end')
39+
->setParameter('start', $start)
40+
->setParameter('end', $end)
41+
->getQuery()
42+
->getSingleScalarResult();
43+
}
2444
}

src/Domain/Analytics/Service/AnalyticsService.php

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
namespace PhpList\Core\Domain\Analytics\Service;
66

7+
use DateTimeImmutable;
8+
use PhpList\Core\Domain\Analytics\Repository\UserMessageViewRepository;
79
use PhpList\Core\Domain\Analytics\Service\Manager\LinkTrackManager;
810
use PhpList\Core\Domain\Analytics\Service\Manager\UserMessageViewManager;
911
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
1012
use PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository;
1113
use PhpList\Core\Domain\Messaging\Repository\UserMessageForwardRepository;
14+
use PhpList\Core\Domain\Messaging\Repository\UserMessageRepository;
1215
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
1316

1417
class AnalyticsService
@@ -19,7 +22,9 @@ public function __construct(
1922
private readonly MessageRepository $messageRepository,
2023
private readonly UserMessageBounceRepository $messageBounceRepository,
2124
private readonly UserMessageForwardRepository $messageForwardRepository,
22-
private readonly SubscriberRepository $subscriberRepository
25+
private readonly SubscriberRepository $subscriberRepository,
26+
private readonly UserMessageRepository $userMessageRepository,
27+
private readonly UserMessageViewRepository $userMessageViewRepository
2328
) {
2429
}
2530

@@ -180,6 +185,86 @@ public function getTopDomains(int $limit = 50, int $minSubscribers = 5): array
180185
];
181186
}
182187

188+
public function getSummaryStatistics(): array
189+
{
190+
$now = new DateTimeImmutable();
191+
$thisMonthStart = $now->modify('first day of this month 00:00:00');
192+
$lastMonthStart = $now->modify('first day of last month 00:00:00');
193+
$lastMonthEnd = $thisMonthStart->modify('-1 second');
194+
195+
$totalSubscribers = $this->subscriberRepository->count([]);
196+
$subscribersThisMonth = $this->subscriberRepository->countCreatedBetween($thisMonthStart, $now);
197+
$subscribersLastMonth = $this->subscriberRepository->countCreatedBetween($lastMonthStart, $lastMonthEnd);
198+
199+
$activeCampaigns = $this->messageRepository->countActiveBetween($thisMonthStart, $now);
200+
$activeCampaignsLastMonth = $this->messageRepository->countActiveBetween($lastMonthStart, $lastMonthEnd);
201+
202+
$sentTotal = $this->userMessageRepository->countSentBetween($thisMonthStart, $now);
203+
$openTotal = $this->userMessageViewRepository->countBetween($thisMonthStart, $now);
204+
$bounceTotal = $this->messageBounceRepository->countBetween($thisMonthStart, $now);
205+
206+
$sentTotalLastMonth = $this->userMessageRepository->countSentBetween($lastMonthStart, $lastMonthEnd);
207+
$openTotalLastMonth = $this->userMessageViewRepository->countBetween($lastMonthStart, $lastMonthEnd);
208+
$bounceTotalLastMonth = $this->messageBounceRepository->countBetween($lastMonthStart, $lastMonthEnd);
209+
210+
$openRate = $this->calculateRate($openTotal, $sentTotal);
211+
$openRateLastMonth = $this->calculateRate($openTotalLastMonth, $sentTotalLastMonth);
212+
213+
$bounceRate = $this->calculateRate($bounceTotal, $sentTotal);
214+
$bounceRateLastMonth = $this->calculateRate($bounceTotalLastMonth, $sentTotalLastMonth);
215+
216+
return [
217+
'total_subscribers' => [
218+
'value' => $totalSubscribers,
219+
'change_vs_last_month' => $this->calculateChange($subscribersThisMonth, $subscribersLastMonth),
220+
],
221+
'active_campaigns' => [
222+
'value' => $activeCampaigns,
223+
'change_vs_last_month' => $this->calculateChange($activeCampaigns, $activeCampaignsLastMonth),
224+
],
225+
'open_rate' => [
226+
'value' => $openRate,
227+
'change_vs_last_month' => $this->calculateChange($openRate, $openRateLastMonth),
228+
],
229+
'bounce_rate' => [
230+
'value' => $bounceRate,
231+
'change_vs_last_month' => $this->calculateChange($bounceRate, $bounceRateLastMonth),
232+
],
233+
];
234+
}
235+
236+
/**
237+
* Calculate rate as a percentage.
238+
*
239+
* @param int $numerator
240+
* @param int $denominator
241+
* @return float
242+
*/
243+
private function calculateRate(int $numerator, int $denominator): float
244+
{
245+
if ($denominator === 0) {
246+
return 0.0;
247+
}
248+
249+
return round(($numerator / $denominator) * 100, 2);
250+
}
251+
252+
/**
253+
* Calculate percentage change between current and previous value.
254+
*
255+
* @param float|int $current
256+
* @param float|int $previous
257+
* @return float
258+
*/
259+
private function calculateChange(float|int $current, float|int $previous): float
260+
{
261+
if ($previous == 0) {
262+
return $current > 0 ? 100.0 : 0.0;
263+
}
264+
265+
return round((($current - $previous) / $previous) * 100, 2);
266+
}
267+
183268
/**
184269
* Get domains with most unconfirmed subscribers
185270
*

src/Domain/Messaging/Repository/MessageRepository.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PhpList\Core\Domain\Messaging\Repository;
66

77
use DateTimeImmutable;
8+
use DateTimeInterface;
89
use Doctrine\ORM\AbstractQuery;
910
use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface;
1011
use PhpList\Core\Domain\Common\Model\PaginatedResult;
@@ -145,4 +146,24 @@ public function getNonEmptyFields(int $id): array
145146

146147
return $message;
147148
}
149+
150+
/**
151+
* Counts active campaigns between two dates.
152+
* "Active" here means messages that were sent (or in process) during this period.
153+
*
154+
* @param DateTimeInterface $start
155+
* @param DateTimeInterface $end
156+
* @return int
157+
*/
158+
public function countActiveBetween(DateTimeInterface $start, DateTimeInterface $end): int
159+
{
160+
return (int) $this->createQueryBuilder('m')
161+
->select('COUNT(m.id)')
162+
->where('m.metadata.sent >= :start')
163+
->andWhere('m.metadata.sent <= :end')
164+
->setParameter('start', $start)
165+
->setParameter('end', $end)
166+
->getQuery()
167+
->getSingleScalarResult();
168+
}
148169
}

src/Domain/Messaging/Repository/UserMessageBounceRepository.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PhpList\Core\Domain\Messaging\Repository;
66

7+
use DateTimeInterface;
78
use PhpList\Core\Domain\Common\Repository\AbstractRepository;
89
use PhpList\Core\Domain\Common\Repository\CursorPaginationTrait;
910
use PhpList\Core\Domain\Common\Repository\Interfaces\PaginatableRepositoryInterface;
@@ -26,6 +27,25 @@ public function getCountByMessageId(int $messageId): int
2627
->getSingleScalarResult();
2728
}
2829

30+
/**
31+
* Counts bounces between two dates.
32+
*
33+
* @param DateTimeInterface $start
34+
* @param DateTimeInterface $end
35+
* @return int
36+
*/
37+
public function countBetween(DateTimeInterface $start, DateTimeInterface $end): int
38+
{
39+
return (int) $this->createQueryBuilder('umb')
40+
->select('COUNT(umb.id)')
41+
->where('umb.createdAt >= :start')
42+
->andWhere('umb.createdAt <= :end')
43+
->setParameter('start', $start)
44+
->setParameter('end', $end)
45+
->getQuery()
46+
->getSingleScalarResult();
47+
}
48+
2949
public function existsByMessageIdAndUserId(int $messageId, int $subscriberId): bool
3050
{
3151
return (bool) $this->createQueryBuilder('umb')

src/Domain/Messaging/Repository/UserMessageRepository.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@ public function findByUserAndMessage(Subscriber $subscriber, Message $campaign):
1818
return $this->findOneBy(['user' => $subscriber, 'message' => $campaign]);
1919
}
2020

21+
/**
22+
* Counts how many user messages have status "sent" between the given dates.
23+
*
24+
* @param DateTimeInterface $start
25+
* @param DateTimeInterface $end
26+
* @return int
27+
*/
28+
public function countSentBetween(DateTimeInterface $start, DateTimeInterface $end): int
29+
{
30+
return (int) $this->createQueryBuilder('um')
31+
->select('COUNT(um)')
32+
->where('um.createdAt >= :start')
33+
->andWhere('um.createdAt <= :end')
34+
->andWhere('um.status = :status')
35+
->setParameter('start', $start)
36+
->setParameter('end', $end)
37+
->setParameter('status', UserMessageStatus::Sent->value)
38+
->getQuery()
39+
->getSingleScalarResult();
40+
}
41+
2142
/**
2243
* Counts how many user messages have status "sent" since the given time.
2344
*/

src/Domain/Subscription/Repository/SubscriberRepository.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PhpList\Core\Domain\Subscription\Repository;
66

7+
use DateTimeInterface;
78
use Doctrine\ORM\QueryBuilder;
89
use InvalidArgumentException;
910
use PhpList\Core\Domain\Common\Model\Filter\FilterRequestInterface;
@@ -256,4 +257,23 @@ public function getDataById(int $subscriberId): array
256257
->getQuery()
257258
->getArrayResult()[0] ?? [];
258259
}
260+
261+
/**
262+
* Counts subscribers created between two dates.
263+
*
264+
* @param DateTimeInterface $start
265+
* @param DateTimeInterface $end
266+
* @return int
267+
*/
268+
public function countCreatedBetween(DateTimeInterface $start, DateTimeInterface $end): int
269+
{
270+
return (int) $this->createQueryBuilder('s')
271+
->select('COUNT(s.id)')
272+
->where('s.createdAt >= :start')
273+
->andWhere('s.createdAt <= :end')
274+
->setParameter('start', $start)
275+
->setParameter('end', $end)
276+
->getQuery()
277+
->getSingleScalarResult();
278+
}
259279
}

tests/Unit/Domain/Analytics/Service/AnalyticsServiceTest.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use DateTime;
88
use PhpList\Core\Domain\Analytics\Model\LinkTrack;
9+
use PhpList\Core\Domain\Analytics\Repository\UserMessageViewRepository;
910
use PhpList\Core\Domain\Analytics\Service\AnalyticsService;
1011
use PhpList\Core\Domain\Analytics\Service\Manager\LinkTrackManager;
1112
use PhpList\Core\Domain\Analytics\Service\Manager\UserMessageViewManager;
@@ -16,6 +17,7 @@
1617
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
1718
use PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository;
1819
use PhpList\Core\Domain\Messaging\Repository\UserMessageForwardRepository;
20+
use PhpList\Core\Domain\Messaging\Repository\UserMessageRepository;
1921
use PhpList\Core\Domain\Subscription\Model\Subscriber;
2022
use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository;
2123
use PHPUnit\Framework\MockObject\MockObject;
@@ -30,6 +32,8 @@ class AnalyticsServiceTest extends TestCase
3032
private UserMessageBounceRepository|MockObject $userMessageBounceRepository;
3133
private UserMessageForwardRepository|MockObject $userMessageForwardRepository;
3234
private SubscriberRepository|MockObject $subscriberRepository;
35+
private UserMessageRepository|MockObject $userMessageRepository;
36+
private UserMessageViewRepository|MockObject $userMessageViewRepository;
3337

3438
protected function setUp(): void
3539
{
@@ -39,14 +43,18 @@ protected function setUp(): void
3943
$this->userMessageBounceRepository = $this->createMock(UserMessageBounceRepository::class);
4044
$this->userMessageForwardRepository = $this->createMock(UserMessageForwardRepository::class);
4145
$this->subscriberRepository = $this->createMock(SubscriberRepository::class);
46+
$this->userMessageRepository = $this->createMock(UserMessageRepository::class);
47+
$this->userMessageViewRepository = $this->createMock(UserMessageViewRepository::class);
4248

4349
$this->subject = new AnalyticsService(
4450
$this->linkTrackManager,
4551
$this->userMessageViewManager,
4652
$this->messageRepository,
4753
$this->userMessageBounceRepository,
4854
$this->userMessageForwardRepository,
49-
$this->subscriberRepository
55+
$this->subscriberRepository,
56+
$this->userMessageRepository,
57+
$this->userMessageViewRepository
5058
);
5159
}
5260

@@ -328,4 +336,34 @@ public function testGetTopLocalParts(): void
328336
self::assertSame(1, $result['localParts'][1]['count']);
329337
self::assertSame(20, $result['localParts'][1]['percentage']);
330338
}
339+
340+
public function testGetSummaryStatistics(): void
341+
{
342+
$this->subscriberRepository->method('count')->willReturn(1000);
343+
$this->subscriberRepository->method('countCreatedBetween')->willReturnOnConsecutiveCalls(100, 50);
344+
345+
$this->messageRepository->method('countActiveBetween')->willReturnOnConsecutiveCalls(5, 4);
346+
347+
$this->userMessageRepository->method('countSentBetween')->willReturnOnConsecutiveCalls(500, 400);
348+
$this->userMessageViewRepository->method('countBetween')->willReturnOnConsecutiveCalls(250, 160);
349+
$this->userMessageBounceRepository->method('countBetween')->willReturnOnConsecutiveCalls(10, 8);
350+
351+
$result = $this->subject->getSummaryStatistics();
352+
353+
self::assertArrayHasKey('total_subscribers', $result);
354+
self::assertSame(1000, $result['total_subscribers']['value']);
355+
self::assertEquals(100.0, $result['total_subscribers']['change_vs_last_month']);
356+
357+
self::assertArrayHasKey('active_campaigns', $result);
358+
self::assertSame(5, $result['active_campaigns']['value']);
359+
self::assertEquals(25.0, $result['active_campaigns']['change_vs_last_month']);
360+
361+
self::assertArrayHasKey('open_rate', $result);
362+
self::assertEquals(50.0, $result['open_rate']['value']);
363+
self::assertEquals(25.0, $result['open_rate']['change_vs_last_month']);
364+
365+
self::assertArrayHasKey('bounce_rate', $result);
366+
self::assertEquals(2.0, $result['bounce_rate']['value']);
367+
self::assertEquals(0.0, $result['bounce_rate']['change_vs_last_month']);
368+
}
331369
}

0 commit comments

Comments
 (0)