PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Subscribers / Statistics / SubscriberStatisticsRepository.php
mailpoet / lib / Subscribers / Statistics Last commit date
SubscriberStatistics.php 3 years ago SubscriberStatisticsRepository.php 1 month ago index.php 3 years ago
SubscriberStatisticsRepository.php
253 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Subscribers\Statistics;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Doctrine\Repository;
9 use MailPoet\Entities\StatisticsClickEntity;
10 use MailPoet\Entities\StatisticsNewsletterEntity;
11 use MailPoet\Entities\StatisticsOpenEntity;
12 use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
13 use MailPoet\Entities\SubscriberEntity;
14 use MailPoet\Entities\UserAgentEntity;
15 use MailPoet\Newsletter\Statistics\WooCommerceRevenue;
16 use MailPoet\Settings\TrackingConfig;
17 use MailPoet\WooCommerce\Helper as WCHelper;
18 use MailPoet\WooCommerce\OrderAttributionRevenueReader;
19 use MailPoetVendor\Doctrine\ORM\EntityManager;
20 use MailPoetVendor\Doctrine\ORM\QueryBuilder;
21
22 /**
23 * @extends Repository<SubscriberEntity>
24 */
25 class SubscriberStatisticsRepository extends Repository {
26 public const ENGAGEMENT_SCORE_UNKNOWN = 'unknown';
27 public const ENGAGEMENT_SCORE_DORMANT = 'dormant';
28 public const ENGAGEMENT_SCORE_LOW = 'low';
29 public const ENGAGEMENT_SCORE_GOOD = 'good';
30 public const ENGAGEMENT_SCORE_EXCELLENT = 'excellent';
31
32 public const MIN_SENT_EMAILS_FOR_ENGAGEMENT_SCORE = 3;
33 private const ENGAGEMENT_SCORE_LOW_MAX = 20;
34 private const ENGAGEMENT_SCORE_GOOD_MAX = 50;
35
36 /** @var WCHelper */
37 private $wcHelper;
38
39 /** @var TrackingConfig */
40 private $trackingConfig;
41
42 /** @var OrderAttributionRevenueReader */
43 private $orderAttributionRevenueReader;
44
45 public function __construct(
46 EntityManager $entityManager,
47 WCHelper $wcHelper,
48 TrackingConfig $trackingConfig,
49 OrderAttributionRevenueReader $orderAttributionRevenueReader
50 ) {
51 parent::__construct($entityManager);
52 $this->wcHelper = $wcHelper;
53 $this->trackingConfig = $trackingConfig;
54 $this->orderAttributionRevenueReader = $orderAttributionRevenueReader;
55 }
56
57 protected function getEntityClassName() {
58 return SubscriberEntity::class;
59 }
60
61 public function getStatistics(SubscriberEntity $subscriber, ?\DateTimeInterface $startTime = null) {
62 return new SubscriberStatistics(
63 $this->getStatisticsClickCount($subscriber, $startTime),
64 $this->getStatisticsOpenCount($subscriber, $startTime),
65 $this->getStatisticsMachineOpenCount($subscriber, $startTime),
66 $this->getTotalSentCount($subscriber, $startTime),
67 $this->getWooCommerceRevenue($subscriber, $startTime)
68 );
69 }
70
71 public function getEngagementScoreType(SubscriberEntity $subscriber): string {
72 $scoreTypes = $this->getEngagementScoreTypes([$subscriber]);
73 return $scoreTypes[(int)$subscriber->getId()] ?? self::ENGAGEMENT_SCORE_UNKNOWN;
74 }
75
76 /**
77 * @param SubscriberEntity[] $subscribers
78 * @return array<int, string>
79 */
80 public function getEngagementScoreTypes(array $subscribers): array {
81 if (!$subscribers) {
82 return [];
83 }
84 $yearAgo = new \DateTimeImmutable('-1 year');
85 $lifetimeSentCounts = $this->getTotalSentCounts($subscribers);
86 $recentSentCounts = $this->getTotalSentCounts($subscribers, $yearAgo);
87 $scoreTypes = [];
88 foreach ($subscribers as $subscriber) {
89 $subscriberId = (int)$subscriber->getId();
90 $scoreTypes[$subscriberId] = self::getEngagementScoreTypeFromData(
91 $subscriber->getEngagementScore(),
92 $lifetimeSentCounts[$subscriberId] ?? 0,
93 $recentSentCounts[$subscriberId] ?? 0
94 );
95 }
96 return $scoreTypes;
97 }
98
99 public static function getEngagementScoreTypeFromData(?float $score, int $lifetimeSentCount, int $recentSentCount): string {
100 if ($score === null) {
101 if (
102 $lifetimeSentCount >= self::MIN_SENT_EMAILS_FOR_ENGAGEMENT_SCORE
103 && $recentSentCount < self::MIN_SENT_EMAILS_FOR_ENGAGEMENT_SCORE
104 ) {
105 return self::ENGAGEMENT_SCORE_DORMANT;
106 }
107 return self::ENGAGEMENT_SCORE_UNKNOWN;
108 }
109 if ($score < self::ENGAGEMENT_SCORE_LOW_MAX) {
110 return self::ENGAGEMENT_SCORE_LOW;
111 }
112 if ($score < self::ENGAGEMENT_SCORE_GOOD_MAX) {
113 return self::ENGAGEMENT_SCORE_GOOD;
114 }
115 return self::ENGAGEMENT_SCORE_EXCELLENT;
116 }
117
118 public function getStatisticsClickCount(SubscriberEntity $subscriber, ?\DateTimeInterface $startTime = null): int {
119 $queryBuilder = $this->getStatisticsCountQuery(StatisticsClickEntity::class, $subscriber);
120 if ($startTime) {
121 $this->applyDateConstraint($queryBuilder, $startTime);
122 }
123 return (int)$queryBuilder
124 ->getQuery()
125 ->getSingleScalarResult();
126 }
127
128 public function getStatisticsOpenCountQuery(SubscriberEntity $subscriber, ?\DateTimeInterface $startTime = null): QueryBuilder {
129 $queryBuilder = $this->getStatisticsCountQuery(StatisticsOpenEntity::class, $subscriber);
130 if ($startTime) {
131 $this->applyDateConstraint($queryBuilder, $startTime);
132 }
133 return $queryBuilder;
134 }
135
136 public function getStatisticsOpenCount(SubscriberEntity $subscriber, ?\DateTimeInterface $startTime = null): int {
137 $queryBuilder = $this->getStatisticsOpenCountQuery($subscriber, $startTime);
138 if ($this->trackingConfig->areOpensSeparated()) {
139 $queryBuilder
140 ->andWhere('(stats.userAgentType = :userAgentType)')
141 ->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_HUMAN);
142 }
143 return (int)$queryBuilder
144 ->getQuery()
145 ->getSingleScalarResult();
146 }
147
148 public function getStatisticsMachineOpenCount(SubscriberEntity $subscriber, ?\DateTimeInterface $startTime = null): int {
149 return (int)$this->getStatisticsOpenCountQuery($subscriber, $startTime)
150 ->andWhere('(stats.userAgentType = :userAgentType)')
151 ->setParameter('userAgentType', UserAgentEntity::USER_AGENT_TYPE_MACHINE)
152 ->getQuery()
153 ->getSingleScalarResult();
154 }
155
156 public function getTotalSentCount(SubscriberEntity $subscriber, ?\DateTimeInterface $startTime = null): int {
157 $queryBuilder = $this->getStatisticsCountQuery(StatisticsNewsletterEntity::class, $subscriber);
158 if ($startTime) {
159 $queryBuilder
160 ->andWhere('stats.sentAt >= :dateTime')
161 ->setParameter('dateTime', $startTime);
162 }
163 return (int)$queryBuilder
164 ->getQuery()
165 ->getSingleScalarResult();
166 }
167
168 /**
169 * @param SubscriberEntity[] $subscribers
170 * @return array<int, int>
171 */
172 private function getTotalSentCounts(array $subscribers, ?\DateTimeInterface $startTime = null): array {
173 $queryBuilder = $this->entityManager->createQueryBuilder()
174 ->select('IDENTITY(stats.subscriber) AS subscriber_id, COUNT(DISTINCT stats.newsletter) AS sent_count')
175 ->from(StatisticsNewsletterEntity::class, 'stats')
176 ->where('stats.subscriber IN (:subscribers)')
177 ->groupBy('stats.subscriber')
178 ->setParameter('subscribers', $subscribers);
179 if ($startTime) {
180 $queryBuilder
181 ->andWhere('stats.sentAt >= :dateTime')
182 ->setParameter('dateTime', $startTime);
183 }
184 $rows = $queryBuilder->getQuery()->getResult();
185 $counts = [];
186 foreach ($rows as $row) {
187 $counts[(int)$row['subscriber_id']] = (int)$row['sent_count'];
188 }
189 return $counts;
190 }
191
192 public function getStatisticsCountQuery(string $entityName, SubscriberEntity $subscriber): QueryBuilder {
193 return $this->entityManager->createQueryBuilder()
194 ->select('COUNT(DISTINCT stats.newsletter) as cnt')
195 ->from($entityName, 'stats')
196 ->where('stats.subscriber = :subscriber')
197 ->setParameter('subscriber', $subscriber);
198 }
199
200 public function getWooCommerceRevenue(SubscriberEntity $subscriber, ?\DateTimeInterface $startTime = null): ?WooCommerceRevenue {
201 if (!$this->wcHelper->isWooCommerceActive()) {
202 return null;
203 }
204
205 $revenueStatus = $this->wcHelper->getPurchaseStates();
206 $currency = $this->wcHelper->getWoocommerceCurrency();
207 $wooBackedRevenue = $this->orderAttributionRevenueReader->getSubscriberRevenue((int)$subscriber->getId(), $startTime);
208 if (is_array($wooBackedRevenue)) {
209 return new WooCommerceRevenue(
210 $currency,
211 (float)$wooBackedRevenue['total'],
212 (int)$wooBackedRevenue['count'],
213 $this->wcHelper
214 );
215 }
216
217 $queryBuilder = $this->entityManager->createQueryBuilder()
218 ->select('stats.orderPriceTotal')
219 ->from(StatisticsWooCommercePurchaseEntity::class, 'stats')
220 ->where('stats.subscriber = :subscriber')
221 ->andWhere('stats.orderCurrency = :currency')
222 ->setParameter('subscriber', $subscriber)
223 ->setParameter('currency', $currency)
224 ->andWhere('stats.status IN (:revenue_status)')
225 ->setParameter('subscriber', $subscriber)
226 ->setParameter('currency', $currency)
227 ->setParameter('revenue_status', $revenueStatus)
228 ->groupBy('stats.orderId, stats.orderPriceTotal');
229 if ($startTime) {
230 $queryBuilder
231 ->andWhere('stats.createdAt >= :dateTime')
232 ->setParameter('dateTime', $startTime);
233 }
234 $purchases =
235 $queryBuilder->getQuery()
236 ->getResult();
237 $sum = array_sum(array_column($purchases, 'orderPriceTotal'));
238 return new WooCommerceRevenue(
239 $currency,
240 (float)$sum,
241 count($purchases),
242 $this->wcHelper
243 );
244 }
245
246 private function applyDateConstraint(QueryBuilder $queryBuilder, \DateTimeInterface $startTime): QueryBuilder {
247 $queryBuilder->join(StatisticsNewsletterEntity::class, 'sent_stats', 'WITH', 'stats.newsletter = sent_stats.newsletter AND stats.subscriber = sent_stats.subscriber AND sent_stats.sentAt >= :dateTime')
248 ->setParameter('dateTime', $startTime);
249
250 return $queryBuilder;
251 }
252 }
253