Issues (3627)

app/bundles/EmailBundle/Entity/StatRepository.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\EmailBundle\Entity;
13
14
use Doctrine\DBAL\Connection;
15
use Mautic\CoreBundle\Entity\CommonRepository;
16
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
17
use Mautic\CoreBundle\Helper\DateTimeHelper;
18
use Mautic\LeadBundle\Entity\TimelineTrait;
19
20
/**
21
 * Class StatRepository.
22
 */
23
class StatRepository extends CommonRepository
24
{
25
    use TimelineTrait;
26
27
    /**
28
     * @param $trackingHash
29
     *
30
     * @return mixed
31
     *
32
     * @throws \Doctrine\ORM\NoResultException
33
     * @throws \Doctrine\ORM\NonUniqueResultException
34
     */
35
    public function getEmailStatus($trackingHash)
36
    {
37
        $q = $this->createQueryBuilder('s');
38
        $q->select('s')
39
            ->leftJoin('s.lead', 'l')
40
            ->leftJoin('s.email', 'e')
41
            ->where(
42
                $q->expr()->eq('s.trackingHash', ':hash')
43
            )
44
            ->setParameter('hash', $trackingHash);
45
        $result = $q->getQuery()->getResult();
46
47
        return (!empty($result)) ? $result[0] : null;
48
    }
49
50
    /**
51
     * @param int $contactId
52
     * @param int $emailId
53
     *
54
     * @return array
55
     */
56
    public function getUniqueClickedLinksPerContactAndEmail($contactId, $emailId)
57
    {
58
        $q = $this->_em->getConnection()->createQueryBuilder();
59
        $q->select('distinct ph.url, ph.date_hit')
60
            ->from(MAUTIC_TABLE_PREFIX.'page_hits', 'ph')
61
            ->where('ph.email_id = :emailId')
62
            ->andWhere('ph.lead_id = :leadId')
63
            ->setParameter('leadId', $contactId)
64
            ->setParameter('emailId', $emailId);
65
66
        $result = $q->execute()->fetchAll();
67
68
        if ($result) {
69
            foreach ($result as $row) {
70
                $data[$row['date_hit']] = $row['url'];
71
            }
72
        }
73
74
        return $data;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data does not seem to be defined for all execution paths leading up to this point.
Loading history...
75
    }
76
77
    /**
78
     * @param int      $limit
79
     * @param int|null $createdByUserId
80
     * @param int|null $companyId
81
     * @param int|null $campaignId
82
     * @param int|null $segmentId
83
     *
84
     * @return array
85
     */
86
    public function getSentEmailToContactData(
87
        $limit,
88
        \DateTime $dateFrom,
89
        \DateTime $dateTo,
90
        $createdByUserId = null,
91
        $companyId = null,
92
        $campaignId = null,
93
        $segmentId = null
94
    ) {
95
        $q = $this->getEntityManager()->getConnection()->createQueryBuilder();
96
        $q->select('s.id, s.lead_id, s.email_address, s.is_read, s.email_id, s.date_sent, s.date_read')
97
            ->from(MAUTIC_TABLE_PREFIX.'email_stats', 's')
98
            ->leftJoin('s', MAUTIC_TABLE_PREFIX.'emails', 'e', 's.email_id = e.id')
99
            ->addSelect('e.name AS email_name')
100
            ->leftJoin('s', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 'ph.source = "email" and ph.source_id = s.email_id and ph.lead_id = s.lead_id')
101
            ->addSelect('COUNT(ph.id) AS link_hits');
102
103
        if (null !== $createdByUserId) {
104
            $q->andWhere('e.created_by = :userId')
105
                ->setParameter('userId', $createdByUserId);
106
        }
107
108
        $q->andWhere('s.date_sent BETWEEN :dateFrom AND :dateTo')
109
            ->setParameter('dateFrom', $dateFrom->format('Y-m-d H:i:s'))
110
            ->setParameter('dateTo', $dateTo->format('Y-m-d H:i:s'));
111
112
        $companyJoinOnExpr = $q->expr()->andX(
113
            $q->expr()->eq('s.lead_id', 'cl.lead_id')
114
        );
115
        if (null === $companyId) {
116
            // Must force a one to one relationship
117
            $companyJoinOnExpr->add(
118
                $q->expr()->eq('cl.is_primary', 1)
119
            );
120
        }
121
122
        $q->leftJoin('s', MAUTIC_TABLE_PREFIX.'companies_leads', 'cl', $companyJoinOnExpr)
123
            ->leftJoin('s', MAUTIC_TABLE_PREFIX.'companies', 'c', 'cl.company_id = c.id')
124
            ->addSelect('c.id AS company_id')
125
            ->addSelect('c.companyname AS company_name');
126
127
        if (null !== $companyId) {
128
            $q->andWhere('cl.company_id = :companyId')
129
                ->setParameter('companyId', $companyId);
130
        }
131
132
        $q->leftJoin('s', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 's.source = "campaign.event" and s.source_id = ce.id')
133
            ->leftJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id')
134
            ->addSelect('campaign.id AS campaign_id')
135
            ->addSelect('campaign.name AS campaign_name');
136
137
        if (null !== $campaignId) {
138
            $q->andWhere('ce.campaign_id = :campaignId')
139
                ->setParameter('campaignId', $campaignId);
140
        }
141
142
        $q->leftJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 's.list_id = ll.id')
143
            ->addSelect('ll.id AS segment_id')
144
            ->addSelect('ll.name AS segment_name');
145
146
        if (null !== $segmentId) {
147
            $sb = $this->getEntityManager()->getConnection()->createQueryBuilder();
148
            $sb->select('null')
149
                ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll')
150
                ->where(
151
                    $sb->expr()->andX(
152
                        $sb->expr()->eq('lll.leadlist_id', ':segmentId'),
153
                        $sb->expr()->eq('lll.lead_id', 'ph.lead_id'),
154
                        $sb->expr()->eq('lll.manually_removed', 0)
155
                    )
156
                );
157
158
            // Filter for both broadcasts and campaign related segments
159
            $q->andWhere(
160
                $q->expr()->orX(
161
                    $q->expr()->eq('s.list_id', ':segmentId'),
162
                    $q->expr()->andX(
163
                        $q->expr()->isNull('s.list_id'),
164
                        sprintf('EXISTS (%s)', $sb->getSQL())
165
                    )
166
                )
167
            )
168
                ->setParameter('segmentId', $segmentId);
169
        }
170
171
        $q->setMaxResults($limit);
172
        $q->groupBy('s.id');
173
        $q->orderBy('s.id', 'DESC');
174
175
        return $q->execute()->fetchAll();
176
    }
177
178
    /**
179
     * @param null $listId
180
     *
181
     * @return array
182
     */
183
    public function getSentStats($emailIds, $listId = null)
184
    {
185
        if (!is_array($emailIds)) {
186
            $emailIds = [(int) $emailIds];
187
        }
188
189
        $q = $this->_em->getConnection()->createQueryBuilder();
190
        $q->select('s.lead_id')
191
            ->from(MAUTIC_TABLE_PREFIX.'email_stats', 's')
192
            ->where(
193
                $q->expr()->in('s.email_id', $emailIds)
194
            );
195
196
        if ($listId) {
197
            $q->andWhere('s.list_id = :list')
198
                ->setParameter('list', $listId);
199
        }
200
201
        $result = $q->execute()->fetchAll();
202
203
        //index by lead
204
        $stats = [];
205
        foreach ($result as $r) {
206
            $stats[$r['lead_id']] = $r['lead_id'];
207
        }
208
209
        unset($result);
210
211
        return $stats;
212
    }
213
214
    /**
215
     * @param null $emailIds
216
     * @param null $listId
217
     * @param bool $combined
218
     *
219
     * @return array|int
220
     */
221
    public function getSentCount($emailIds = null, $listId = null, ChartQuery $chartQuery = null, $combined = false)
222
    {
223
        return $this->getStatusCount('is_sent', $emailIds, $listId, $chartQuery, $combined);
224
    }
225
226
    /**
227
     * @param null $emailIds
228
     * @param null $listId
229
     * @param bool $combined
230
     *
231
     * @return array|int
232
     */
233
    public function getReadCount($emailIds = null, $listId = null, ChartQuery $chartQuery = null, $combined = false)
234
    {
235
        return $this->getStatusCount('is_read', $emailIds, $listId, $chartQuery, $combined);
236
    }
237
238
    /**
239
     * @param null $emailIds
240
     * @param null $listId
241
     * @param bool $combined
242
     *
243
     * @return array|int
244
     */
245
    public function getFailedCount($emailIds = null, $listId = null, ChartQuery $chartQuery = null, $combined = false)
246
    {
247
        return $this->getStatusCount('is_failed', $emailIds, $listId, $chartQuery, $combined);
248
    }
249
250
    /**
251
     * @param      $column
252
     * @param null $emailIds
253
     * @param null $listId
254
     * @param bool $combined
255
     *
256
     * @return array|int
257
     */
258
    public function getStatusCount($column, $emailIds = null, $listId = null, ChartQuery $chartQuery = null, $combined = false)
259
    {
260
        $q = $this->_em->getConnection()->createQueryBuilder();
261
262
        $q->select('count(s.id) as count')
263
            ->from(MAUTIC_TABLE_PREFIX.'email_stats', 's');
264
265
        if ($emailIds) {
266
            if (!is_array($emailIds)) {
267
                $emailIds = [(int) $emailIds];
268
            }
269
            $q->where(
270
                $q->expr()->in('s.email_id', $emailIds)
271
            );
272
        }
273
274
        if ($listId) {
275
            if (!$combined) {
276
                if (true === $listId) {
277
                    $q->addSelect('s.list_id')
278
                        ->groupBy('s.list_id');
279
                } elseif (is_array($listId)) {
280
                    $q->andWhere(
281
                        $q->expr()->in('s.list_id', array_map('intval', $listId))
282
                    );
283
284
                    $q->addSelect('s.list_id')
285
                        ->groupBy('s.list_id');
286
                } else {
287
                    $q->andWhere('s.list_id = :list_id')
288
                        ->setParameter('list_id', $listId);
289
                }
290
            } else {
291
                $subQ = $this->getEntityManager()->getConnection()->createQueryBuilder();
292
                $subQ->select('null')
293
                    ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'list')
294
                    ->andWhere(
295
                        $q->expr()->andX(
296
                            $q->expr()->in('list.leadlist_id', array_map('intval', $listId)),
297
                            $q->expr()->eq('list.lead_id', 's.lead_id')
298
                        )
299
                    );
300
301
                $q->andWhere(sprintf('EXISTS (%s)', $subQ->getSQL()));
302
            }
303
        }
304
305
        if ('is_sent' === $column) {
306
            $q->andWhere('s.is_failed = :false')
307
                ->setParameter('false', false, 'boolean');
308
        } else {
309
            $q->andWhere($column.' = :true')
310
                ->setParameter('true', true, 'boolean');
311
        }
312
313
        if ($chartQuery) {
314
            $chartQuery->applyDateFilters($q, 'date_sent', 's');
315
        }
316
317
        $results = $q->execute()->fetchAll();
318
319
        if ((true === $listId || is_array($listId)) && !$combined) {
320
            // Return list group of counts
321
            $byList = [];
322
            foreach ($results as $result) {
323
                $byList[$result['list_id']] = $result['count'];
324
            }
325
326
            return $byList;
327
        }
328
329
        return (isset($results[0])) ? $results[0]['count'] : 0;
330
    }
331
332
    /**
333
     * @param           $emailIds
334
     * @param \DateTime $fromDate
335
     *
336
     * @return array
337
     */
338
    public function getOpenedRates($emailIds, \DateTime $fromDate = null)
339
    {
340
        $inIds = (!is_array($emailIds)) ? [$emailIds] : $emailIds;
341
342
        $sq = $this->_em->getConnection()->createQueryBuilder();
343
        $sq->select('e.email_id, count(e.id) as the_count')
344
            ->from(MAUTIC_TABLE_PREFIX.'email_stats', 'e')
345
            ->where(
346
                $sq->expr()->andX(
347
                    $sq->expr()->eq('e.is_failed', ':false'),
348
                    $sq->expr()->in('e.email_id', $inIds)
349
                )
350
            )->setParameter('false', false, 'boolean');
351
352
        if (null !== $fromDate) {
353
            //make sure the date is UTC
354
            $dt = new DateTimeHelper($fromDate);
355
            $sq->andWhere(
356
                $sq->expr()->gte('e.date_sent', $sq->expr()->literal($dt->toUtcString()))
357
            );
358
        }
359
        $sq->groupBy('e.email_id');
360
361
        //get a total number of sent emails first
362
        $totalCounts = $sq->execute()->fetchAll();
363
364
        $return = [];
365
        foreach ($inIds as $id) {
366
            $return[$id] = [
367
                'totalCount' => 0,
368
                'readCount'  => 0,
369
                'readRate'   => 0,
370
            ];
371
        }
372
373
        foreach ($totalCounts as $t) {
374
            if (null != $t['email_id']) {
375
                $return[$t['email_id']]['totalCount'] = (int) $t['the_count'];
376
            }
377
        }
378
379
        //now get a read count
380
        $sq->andWhere('e.is_read = :true')
381
            ->setParameter('true', true, 'boolean');
382
        $readCounts = $sq->execute()->fetchAll();
383
384
        foreach ($readCounts as $r) {
385
            $return[$r['email_id']]['readCount'] = (int) $r['the_count'];
386
            $return[$r['email_id']]['readRate']  = ($return[$r['email_id']]['totalCount']) ?
387
                round(($r['the_count'] / $return[$r['email_id']]['totalCount']) * 100, 2) :
388
                0;
389
        }
390
391
        return (!is_array($emailIds)) ? $return[$emailIds] : $return;
392
    }
393
394
    /**
395
     * @param array|int $emailIds
396
     *
397
     * @return int
398
     */
399
    public function getOpenedStatIds($emailIds = null, $listId = null)
400
    {
401
        $q = $this->_em->getConnection()->createQueryBuilder();
402
403
        $q->select('s.id')
404
            ->from(MAUTIC_TABLE_PREFIX.'email_stats', 's');
405
406
        if ($emailIds) {
407
            if (!is_array($emailIds)) {
408
                $emailIds = [(int) $emailIds];
409
            }
410
            $q->where(
411
                $q->expr()->in('s.email_id', $emailIds)
412
            );
413
        }
414
415
        $q->andWhere('open_count > 0');
416
417
        if ($listId) {
418
            $q->andWhere('s.list_id = '.(int) $listId);
419
        }
420
421
        return $q->execute()->fetchAll();
422
    }
423
424
    /**
425
     * Get a lead's email stat.
426
     *
427
     * @param int $leadId
428
     *
429
     * @return array
430
     *
431
     * @throws \Doctrine\ORM\NoResultException
432
     * @throws \Doctrine\ORM\NonUniqueResultException
433
     */
434
    public function getLeadStats($leadId, array $options = [])
435
    {
436
        $query = $this->getEntityManager()->getConnection()->createQueryBuilder();
437
        $query->from(MAUTIC_TABLE_PREFIX.'email_stats', 's')
438
            ->leftJoin('s', MAUTIC_TABLE_PREFIX.'emails', 'e', 's.email_id = e.id')
439
            ->leftJoin('s', MAUTIC_TABLE_PREFIX.'email_copies', 'ec', 's.copy_id = ec.id');
440
441
        if ($leadId) {
442
            $query->andWhere(
443
                $query->expr()->eq('s.lead_id', (int) $leadId)
444
            );
445
        }
446
447
        if (!empty($options['basic_select'])) {
448
            $query->select(
449
                's.email_id, s.id, s.date_read as dateRead, s.date_sent as dateSent, e.subject, e.name as email_name, s.is_read as isRead, s.is_failed as isFailed, ec.subject as storedSubject'
450
            );
451
        } else {
452
            $query->select(
453
                's.email_id, s.id, s.date_read as dateRead, s.date_sent as dateSent,e.subject, e.name as email_name, s.is_read as isRead, s.is_failed as isFailed, s.viewed_in_browser as viewedInBrowser, s.retry_count as retryCount, s.list_id, l.name as list_name, s.tracking_hash as idHash, s.open_details as openDetails, ec.subject as storedSubject, s.lead_id'
454
            )
455
                ->leftJoin('s', MAUTIC_TABLE_PREFIX.'lead_lists', 'l', 's.list_id = l.id');
456
        }
457
458
        if (isset($options['state'])) {
459
            $state = $options['state'];
460
            if ('read' == $state) {
461
                $query->andWhere(
462
                    $query->expr()->eq('s.is_read', 1)
463
                );
464
            } elseif ('failed' == $state) {
465
                $query->andWhere(
466
                    $query->expr()->eq('s.is_failed', 1)
467
                );
468
            }
469
        }
470
        $state = 'sent';
471
472
        if (isset($options['search']) && $options['search']) {
473
            $query->andWhere(
474
                $query->expr()->orX(
475
                    $query->expr()->like('ec.subject', $query->expr()->literal('%'.$options['search'].'%')),
476
                    $query->expr()->like('e.subject', $query->expr()->literal('%'.$options['search'].'%')),
477
                    $query->expr()->like('e.name', $query->expr()->literal('%'.$options['search'].'%'))
478
                )
479
            );
480
        }
481
482
        if (isset($options['fromDate']) && $options['fromDate']) {
483
            $dt = new DateTimeHelper($options['fromDate']);
484
            $query->andWhere(
485
                $query->expr()->gte('s.date_sent', $query->expr()->literal($dt->toUtcString()))
486
            );
487
        }
488
489
        $timeToReadParser = function (&$stat) {
490
            $dateSent = new DateTimeHelper($stat['dateSent']);
491
            if (!empty($stat['dateSent']) && !empty($stat['dateRead'])) {
492
                $stat['timeToRead'] = $dateSent->getDiff($stat['dateRead']);
493
            } else {
494
                $stat['timeToRead'] = false;
495
            }
496
        };
497
498
        return $this->getTimelineResults(
499
            $query,
500
            $options,
501
            'storedSubject, e.subject',
502
            's.date_'.$state,
503
            ['openDetails'],
504
            ['dateRead', 'dateSent'],
505
            $timeToReadParser
506
        );
507
    }
508
509
    /**
510
     * Get counts for Sent, Read and Failed emails.
511
     *
512
     * @param QueryBuilder $query
513
     *
514
     * @return array
515
     *
516
     * @throws \Doctrine\ORM\NoResultException
517
     * @throws \Doctrine\ORM\NonUniqueResultException
518
     */
519
    public function getIgnoredReadFailed($query = null)
520
    {
521
        $query->select('count(es.id) as sent, count(CASE WHEN es.is_read THEN 1 ELSE null END) as "read", count(CASE WHEN es.is_failed THEN 1 ELSE null END) as failed');
522
523
        $results = $query->execute()->fetch();
524
525
        $results['ignored'] = $results['sent'] - $results['read'] - $results['failed'];
526
        unset($results['sent']);
527
528
        return $results;
529
    }
530
531
    /**
532
     * Get pie graph data for Sent, Read and Failed email count.
533
     *
534
     * @param QueryBuilder $query
535
     *
536
     * @return array
537
     *
538
     * @throws \Doctrine\ORM\NoResultException
539
     * @throws \Doctrine\ORM\NonUniqueResultException
540
     */
541
    public function getMostEmails($query, $limit = 10, $offset = 0)
542
    {
543
        $query
544
            ->setMaxResults($limit)
545
            ->setFirstResult($offset);
546
547
        return $query->execute()->fetchAll();
548
    }
549
550
    /**
551
     * Get sent counts based grouped by email Id.
552
     *
553
     * @param array $emailIds
554
     *
555
     * @return array
556
     */
557
    public function getSentCounts($emailIds = [], \DateTime $fromDate = null)
558
    {
559
        $q = $this->_em->getConnection()->createQueryBuilder();
560
        $q->select('e.email_id, count(e.id) as sentcount')
561
            ->from(MAUTIC_TABLE_PREFIX.'email_stats', 'e')
562
            ->where(
563
                $q->expr()->andX(
564
                    $q->expr()->in('e.email_id', $emailIds),
565
                    $q->expr()->eq('e.is_failed', ':false')
566
                )
567
            )->setParameter('false', false, 'boolean');
568
569
        if (null !== $fromDate) {
570
            //make sure the date is UTC
571
            $dt = new DateTimeHelper($fromDate);
572
            $q->andWhere(
573
                $q->expr()->gte('e.date_read', $q->expr()->literal($dt->toUtcString()))
574
            );
575
        }
576
        $q->groupBy('e.email_id');
577
578
        //get a total number of sent emails first
579
        $results = $q->execute()->fetchAll();
580
581
        $counts = [];
582
583
        foreach ($results as $r) {
584
            $counts[$r['email_id']] = $r['sentcount'];
585
        }
586
587
        return $counts;
588
    }
589
590
    /**
591
     * Updates lead ID (e.g. after a lead merge).
592
     *
593
     * @param $fromLeadId
594
     * @param $toLeadId
595
     */
596
    public function updateLead($fromLeadId, $toLeadId)
597
    {
598
        $q = $this->_em->getConnection()->createQueryBuilder();
599
        $q->update(MAUTIC_TABLE_PREFIX.'email_stats')
600
            ->set('lead_id', (int) $toLeadId)
601
            ->where('lead_id = '.(int) $fromLeadId)
602
            ->execute();
603
    }
604
605
    /**
606
     * Delete a stat.
607
     *
608
     * @param $id
609
     */
610
    public function deleteStat($id)
611
    {
612
        $this->getEntityManager()->getConnection()->delete(MAUTIC_TABLE_PREFIX.'email_stats', ['id' => (int) $id]);
613
    }
614
615
    public function deleteStats(array $ids)
616
    {
617
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
618
619
        $qb->delete(MAUTIC_TABLE_PREFIX.'email_stats')
620
            ->where(
621
                $qb->expr()->in('id', $ids)
622
            )
623
            ->execute();
624
    }
625
626
    /**
627
     * @return string
628
     */
629
    public function getTableAlias()
630
    {
631
        return 's';
632
    }
633
634
    /**
635
     * @param $leadId
636
     * @param $emailId
637
     *
638
     * @return array
639
     */
640
    public function findContactEmailStats($leadId, $emailId)
641
    {
642
        return $this->createQueryBuilder('s')
643
            ->where('IDENTITY(s.lead) = '.(int) $leadId.' AND IDENTITY(s.email) = '.(int) $emailId)
644
            ->getQuery()
645
            ->getResult();
646
    }
647
648
    /**
649
     * @param $contacts
650
     * @param $emailId
651
     *
652
     * @return mixed
653
     */
654
    public function checkContactsSentEmail($contacts, $emailId)
655
    {
656
        $query = $this->getEntityManager()->getConnection()->createQueryBuilder();
657
        $query->from(MAUTIC_TABLE_PREFIX.'email_stats', 's');
658
        $query->select('id, lead_id')
659
            ->where('s.email_id = :email')
660
            ->andWhere('s.lead_id in (:contacts)')
661
            ->andWhere('is_failed = 0')
662
            ->setParameter(':email', $emailId)
663
            ->setParameter(':contacts', $contacts);
664
665
        return $query->execute()->fetch();
666
    }
667
668
    /**
669
     * @param $emailId
670
     *
671
     * @return array Formatted as [contactId => sentCount]
672
     */
673
    public function getSentCountForContacts(array $contacts, $emailId)
674
    {
675
        $query = $this->getEntityManager()->getConnection()->createQueryBuilder();
676
        $query->from(MAUTIC_TABLE_PREFIX.'email_stats', 's');
677
        $query->select('count(s.id) as sent_count, s.lead_id')
678
            ->where('s.email_id = :email')
679
            ->andWhere('s.lead_id in (:contacts)')
680
            ->andWhere('s.is_failed = 0')
681
            ->setParameter(':email', $emailId)
682
            ->setParameter(':contacts', $contacts, Connection::PARAM_INT_ARRAY)
683
            ->groupBy('s.lead_id');
684
685
        $results = $query->execute()->fetchAll();
686
687
        $contacts = [];
688
        foreach ($results as $result) {
689
            $contacts[$result['lead_id']] = $result['sent_count'];
690
        }
691
692
        return $contacts;
693
    }
694
}
695