Issues (3627)

CampaignBundle/Entity/LeadEventLogRepository.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\CampaignBundle\Entity;
13
14
use Doctrine\Common\Collections\ArrayCollection;
15
use Doctrine\DBAL\Types\Type;
16
use Mautic\CampaignBundle\Executioner\ContactFinder\Limiter\ContactLimiter;
17
use Mautic\CoreBundle\Entity\CommonRepository;
18
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
19
use Mautic\LeadBundle\Entity\TimelineTrait;
20
21
class LeadEventLogRepository extends CommonRepository
22
{
23
    use TimelineTrait;
24
    use ContactLimiterTrait;
25
    use SlaveConnectionTrait;
26
27
    public function getEntities(array $args = [])
28
    {
29
        $alias = $this->getTableAlias();
30
        $q     = $this
31
            ->createQueryBuilder($alias)
32
            ->join($alias.'.ipAddress', 'i');
33
34
        if (empty($args['campaign_id'])) {
35
            $q->join($alias.'.event', 'e')
36
                ->join($alias.'.campaign', 'c');
37
        } else {
38
            $q->andWhere(
39
                $q->expr()->eq('IDENTITY('.$this->getTableAlias().'.campaign)', (int) $args['campaign_id'])
40
            );
41
        }
42
43
        if (!empty($args['contact_id'])) {
44
            $q->andWhere(
45
                $q->expr()->eq('IDENTITY('.$this->getTableAlias().'.lead)', (int) $args['contact_id'])
46
            );
47
        }
48
49
        $args['qb'] = $q;
50
51
        return parent::getEntities($args);
52
    }
53
54
    /**
55
     * @return string
56
     */
57
    public function getTableAlias()
58
    {
59
        return 'll';
60
    }
61
62
    /**
63
     * Get a lead's page event log.
64
     *
65
     * @param int|null $leadId
66
     *
67
     * @return array
68
     */
69
    public function getLeadLogs($leadId = null, array $options = [])
70
    {
71
        $query = $this->getEntityManager()
72
                      ->getConnection()
73
                      ->createQueryBuilder()
74
                      ->select('ll.id as log_id,
75
                    ll.event_id,
76
                    ll.campaign_id,
77
                    ll.date_triggered as dateTriggered,
78
                    e.name AS event_name,
79
                    e.description AS event_description,
80
                    c.name AS campaign_name,
81
                    c.description AS campaign_description,
82
                    ll.metadata,
83
                    e.type,
84
                    ll.is_scheduled as isScheduled,
85
                    ll.trigger_date as triggerDate,
86
                    ll.channel,
87
                    ll.channel_id as channel_id,
88
                    ll.lead_id,
89
                    fl.reason as fail_reason
90
                    '
91
                      )
92
                        ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'll')
93
                        ->join('ll', MAUTIC_TABLE_PREFIX.'campaign_events', 'e', 'll.event_id = e.id')
94
                        ->join('ll', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'll.campaign_id = c.id')
95
                        ->leftJoin('ll', MAUTIC_TABLE_PREFIX.'campaign_lead_event_failed_log', 'fl', 'fl.log_id = ll.id')
96
                        ->andWhere('e.event_type != :eventType')
97
                        ->setParameter('eventType', 'decision');
98
99
        if ($leadId) {
100
            $query->where('ll.lead_id = '.(int) $leadId);
101
        }
102
103
        if (isset($options['scheduledState'])) {
104
            if ($options['scheduledState']) {
105
                // Include cancelled as well
106
                $query->andWhere(
107
                    $query->expr()->orX(
108
                        $query->expr()->eq('ll.is_scheduled', ':scheduled'),
109
                        $query->expr()->andX(
110
                            $query->expr()->eq('ll.is_scheduled', 0),
111
                            $query->expr()->isNull('ll.date_triggered', 0)
112
                        )
113
                    )
114
                );
115
            } else {
116
                $query->andWhere(
117
                    $query->expr()->eq('ll.is_scheduled', ':scheduled')
118
                );
119
            }
120
            $query->setParameter('scheduled', $options['scheduledState'], 'boolean');
121
        }
122
123
        if (isset($options['search']) && $options['search']) {
124
            $query->andWhere(
125
                $query->expr()->orX(
126
                    $query->expr()->like('e.name', $query->expr()->literal('%'.$options['search'].'%')),
127
                    $query->expr()->like('e.description', $query->expr()->literal('%'.$options['search'].'%')),
128
                    $query->expr()->like('c.name', $query->expr()->literal('%'.$options['search'].'%')),
129
                    $query->expr()->like('c.description', $query->expr()->literal('%'.$options['search'].'%'))
130
                )
131
            );
132
        }
133
134
        return $this->getTimelineResults($query, $options, 'e.name', 'll.date_triggered', ['metadata'], ['dateTriggered', 'triggerDate']);
135
    }
136
137
    /**
138
     * Get a lead's upcoming events.
139
     *
140
     * @param array $options
141
     *
142
     * @return array
143
     */
144
    public function getUpcomingEvents(array $options = null)
145
    {
146
        $leadIps = [];
147
148
        $query = $this->_em->getConnection()->createQueryBuilder();
149
        $query->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'll')
150
            ->select('ll.event_id,
151
                    ll.campaign_id,
152
                    ll.trigger_date,
153
                    ll.lead_id,
154
                    e.name AS event_name,
155
                    e.description AS event_description,
156
                    c.name AS campaign_name,
157
                    c.description AS campaign_description,
158
                    ll.metadata,
159
                    CONCAT(CONCAT(l.firstname, \' \'), l.lastname) AS lead_name')
160
            ->leftJoin('ll', MAUTIC_TABLE_PREFIX.'campaign_events', 'e', 'e.id = ll.event_id')
161
            ->leftJoin('ll', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'c.id = e.campaign_id')
162
            ->leftJoin('ll', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = ll.lead_id')
163
            ->where($query->expr()->eq('ll.is_scheduled', 1));
164
165
        if (isset($options['lead'])) {
166
            /** @var \Mautic\CoreBundle\Entity\IpAddress $ip */
167
            foreach ($options['lead']->getIpAddresses() as $ip) {
168
                $leadIps[] = $ip->getId();
169
            }
170
171
            $query->andWhere('ll.lead_id = :leadId')
172
                ->setParameter('leadId', $options['lead']->getId());
173
        }
174
175
        if (isset($options['type'])) {
176
            $query->andwhere('e.type = :type')
177
                  ->setParameter('type', $options['type']);
178
        }
179
180
        if (isset($options['eventType'])) {
181
            if (is_array($options['eventType'])) {
182
                $query->andWhere(
183
                    $query->expr()->in('e.event_type', array_map([$query->expr(), 'literal'], $options['eventType']))
184
                );
185
            } else {
186
                $query->andwhere('e.event_type = :eventTypes')
187
                    ->setParameter('eventTypes', $options['eventType']);
188
            }
189
        }
190
191
        if (isset($options['limit'])) {
192
            $query->setMaxResults($options['limit']);
193
        } else {
194
            $query->setMaxResults(10);
195
        }
196
197
        $query->orderBy('ll.trigger_date');
198
199
        if (!empty($ipIds)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ipIds seems to never exist and therefore empty should always be true.
Loading history...
200
            $query->orWhere('ll.ip_address IN ('.implode(',', $ipIds).')');
201
        }
202
203
        if (empty($options['canViewOthers']) && isset($this->currentUser)) {
204
            $query->andWhere('c.created_by = :userId')
205
                ->setParameter('userId', $this->currentUser->getId());
206
        }
207
208
        return $query->execute()->fetchAll();
209
    }
210
211
    /**
212
     * @param      $campaignId
213
     * @param bool $excludeScheduled
214
     * @param bool $excludeNegative
215
     * @param bool $all
216
     *
217
     * @return array
218
     */
219
    public function getCampaignLogCounts($campaignId, $excludeScheduled = false, $excludeNegative = true, $all = false)
220
    {
221
        $q = $this->getSlaveConnection()->createQueryBuilder()
222
                      ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'o');
223
224
        $join = 'innerJoin';
225
        if (true === $all) {
226
            $join = 'leftJoin';
227
        }
228
        $q->$join(
229
                    'o',
230
                    MAUTIC_TABLE_PREFIX.'campaign_leads',
231
                    'l',
232
                    'l.campaign_id = '.(int) $campaignId.' and l.manually_removed = 0 and o.lead_id = l.lead_id and l.rotation = o.rotation'
233
                );
234
235
        $expr = $q->expr()->andX(
236
            $q->expr()->eq('o.campaign_id', (int) $campaignId)
237
        );
238
239
        $groupBy = 'o.event_id';
240
        if ($excludeNegative) {
241
            $q->select('o.event_id, count(o.lead_id) as lead_count');
242
            $expr->add(
243
                $q->expr()->orX(
244
                    $q->expr()->isNull('o.non_action_path_taken'),
245
                    $q->expr()->eq('o.non_action_path_taken', ':false')
246
                )
247
            );
248
        } else {
249
            $q->select('o.event_id, count(o.lead_id) as lead_count, o.non_action_path_taken');
250
            $groupBy .= ', o.non_action_path_taken';
251
        }
252
253
        if ($excludeScheduled) {
254
            $expr->add(
255
                $q->expr()->eq('o.is_scheduled', ':false')
256
            );
257
        }
258
259
        // Exclude failed events
260
        $failedSq = $this->getSlaveConnection()->createQueryBuilder();
261
        $failedSq->select('null')
262
            ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_failed_log', 'fe')
263
            ->where(
264
                $failedSq->expr()->eq('fe.log_id', 'o.id')
265
            );
266
        $expr->add(
267
            sprintf('NOT EXISTS (%s)', $failedSq->getSQL())
268
        );
269
270
        $q->where($expr)
271
          ->setParameter('false', false, 'boolean')
272
          ->groupBy($groupBy);
273
274
        $results = $q->execute()->fetchAll();
275
276
        $return = [];
277
278
        //group by event id
279
        foreach ($results as $l) {
280
            if (!$excludeNegative) {
281
                if (!isset($return[$l['event_id']])) {
282
                    $return[$l['event_id']] = [
283
                        0 => 0,
284
                        1 => 0,
285
                    ];
286
                }
287
288
                $key                          = (int) $l['non_action_path_taken'] ? 0 : 1;
289
                $return[$l['event_id']][$key] = (int) $l['lead_count'];
290
            } else {
291
                $return[$l['event_id']] = (int) $l['lead_count'];
292
            }
293
        }
294
295
        return $return;
296
    }
297
298
    /**
299
     * Updates lead ID (e.g. after a lead merge).
300
     *
301
     * @param $fromLeadId
302
     * @param $toLeadId
303
     */
304
    public function updateLead($fromLeadId, $toLeadId)
305
    {
306
        // First check to ensure the $toLead doesn't already exist
307
        $results = $this->_em->getConnection()->createQueryBuilder()
308
            ->select('cl.event_id')
309
            ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'cl')
310
            ->where('cl.lead_id = '.$toLeadId)
311
            ->execute()
312
            ->fetchAll();
313
        $exists = [];
314
        foreach ($results as $r) {
315
            $exists[] = $r['event_id'];
316
        }
317
318
        $q = $this->_em->getConnection()->createQueryBuilder();
319
        $q->update(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log')
320
            ->set('lead_id', (int) $toLeadId)
321
            ->where('lead_id = '.(int) $fromLeadId);
322
323
        if (!empty($exists)) {
324
            $q->andWhere(
325
                $q->expr()->notIn('event_id', $exists)
326
            )->execute();
327
328
            // Delete remaining leads as the new lead already belongs
329
            $this->_em->getConnection()->createQueryBuilder()
330
                ->delete(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log')
331
                ->where('lead_id = '.(int) $fromLeadId)
332
                ->execute();
333
        } else {
334
            $q->execute();
335
        }
336
    }
337
338
    /**
339
     * @param $options
340
     *
341
     * @return array
342
     */
343
    public function getChartQuery($options)
344
    {
345
        $chartQuery = new ChartQuery($this->getSlaveConnection(), $options['dateFrom'], $options['dateTo']);
346
347
        // Load points for selected period
348
        $query = $this->getSlaveConnection()->createQueryBuilder();
349
        $query->select('ll.id, ll.date_triggered')
350
            ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'll')
351
            ->join('ll', MAUTIC_TABLE_PREFIX.'campaign_events', 'e', 'e.id = ll.event_id');
352
353
        if (isset($options['channel'])) {
354
            $query->andwhere("e.channel = '".$options['channel']."'");
355
        }
356
357
        if (isset($options['channelId'])) {
358
            $query->andwhere('e.channel_id = '.(int) $options['channelId']);
359
        }
360
361
        if (isset($options['type'])) {
362
            $query->andwhere("e.type = '".$options['type']."'");
363
        }
364
365
        if (isset($options['logChannel'])) {
366
            $query->andwhere("ll.channel = '".$options['logChannel']."'");
367
        }
368
369
        if (isset($options['logChannelId'])) {
370
            $query->andwhere('ll.channel_id = '.(int) $options['logChannelId']);
371
        }
372
373
        if (!isset($options['is_scheduled'])) {
374
            $query->andWhere($query->expr()->eq('ll.is_scheduled', 0));
375
        } else {
376
            $query->andWhere($query->expr()->eq('ll.is_scheduled', 1));
377
        }
378
379
        return $chartQuery->fetchTimeData('('.$query.')', 'date_triggered');
380
    }
381
382
    /**
383
     * @param int $eventId
384
     *
385
     * @return ArrayCollection
386
     *
387
     * @throws \Doctrine\ORM\Query\QueryException
388
     */
389
    public function getScheduled($eventId, \DateTime $now, ContactLimiter $limiter)
390
    {
391
        if ($limiter->hasCampaignLimit() && 0 === $limiter->getCampaignLimitRemaining()) {
392
            return new ArrayCollection();
393
        }
394
395
        $this->getSlaveConnection($limiter);
396
397
        $q = $this->createQueryBuilder('o');
398
399
        $q->select('o, e, c')
400
            ->indexBy('o', 'o.id')
401
            ->innerJoin('o.event', 'e')
402
            ->innerJoin('e.campaign', 'c')
403
            ->where(
404
                $q->expr()->andX(
405
                    $q->expr()->eq('IDENTITY(o.event)', ':eventId'),
406
                    $q->expr()->eq('o.isScheduled', ':true'),
407
                    $q->expr()->lte('o.triggerDate', ':now'),
408
                    $q->expr()->eq('c.isPublished', 1)
409
                )
410
            )
411
            ->setParameter('eventId', (int) $eventId)
412
            ->setParameter('now', $now)
413
            ->setParameter('true', true, Type::BOOLEAN);
414
415
        $this->updateOrmQueryFromContactLimiter('o', $q, $limiter);
416
417
        if ($limiter->hasCampaignLimit() && $limiter->getCampaignLimitRemaining() < $limiter->getBatchLimit()) {
418
            $q->setMaxResults($limiter->getCampaignLimitRemaining());
419
        }
420
421
        $result = new ArrayCollection($q->getQuery()->getResult());
422
423
        if ($limiter->hasCampaignLimit()) {
424
            $limiter->reduceCampaignLimitRemaining($result->count());
425
        }
426
427
        return $result;
428
    }
429
430
    /**
431
     * @return ArrayCollection
432
     *
433
     * @throws \Doctrine\ORM\Query\QueryException
434
     */
435
    public function getScheduledByIds(array $ids)
436
    {
437
        $this->getSlaveConnection();
438
        $q = $this->createQueryBuilder('o');
439
440
        $q->select('o, e, c')
441
            ->indexBy('o', 'o.id')
442
            ->innerJoin('o.event', 'e')
443
            ->innerJoin('e.campaign', 'c')
444
            ->where(
445
                $q->expr()->andX(
446
                    $q->expr()->in('o.id', $ids),
447
                    $q->expr()->eq('o.isScheduled', 1),
448
                    $q->expr()->eq('c.isPublished', 1)
449
                )
450
            );
451
452
        return new ArrayCollection($q->getQuery()->getResult());
453
    }
454
455
    /**
456
     * @param int $campaignId
457
     *
458
     * @return array
459
     */
460
    public function getScheduledCounts($campaignId, \DateTime $date, ContactLimiter $limiter)
461
    {
462
        $now = clone $date;
463
        $now->setTimezone(new \DateTimeZone('UTC'));
464
465
        $q = $this->getSlaveConnection($limiter)->createQueryBuilder();
466
467
        $expr = $q->expr()->andX(
468
            $q->expr()->eq('l.campaign_id', ':campaignId'),
469
            $q->expr()->eq('l.is_scheduled', ':true'),
470
            $q->expr()->lte('l.trigger_date', ':now'),
471
            $q->expr()->eq('c.is_published', 1)
472
        );
473
474
        $this->updateQueryFromContactLimiter('l', $q, $limiter, true);
475
476
        $results = $q->select('COUNT(*) as event_count, l.event_id')
477
            ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'l')
478
            ->join('l', MAUTIC_TABLE_PREFIX.'campaigns', 'c', 'l.campaign_id = c.id')
479
            ->where($expr)
480
            ->setParameter('campaignId', (int) $campaignId)
481
            ->setParameter('now', $now->format('Y-m-d H:i:s'))
482
            ->setParameter('true', true, \PDO::PARAM_BOOL)
483
            ->groupBy('l.event_id')
484
            ->execute()
485
            ->fetchAll();
486
487
        $events = [];
488
489
        foreach ($results as $result) {
490
            $events[$result['event_id']] = (int) $result['event_count'];
491
        }
492
493
        return $events;
494
    }
495
496
    /**
497
     * @param $eventId
498
     *
499
     * @return array
500
     */
501
    public function getDatesExecuted($eventId, array $contactIds)
502
    {
503
        $qb = $this->getSlaveConnection()->createQueryBuilder();
504
        $qb->select('log.lead_id, log.date_triggered')
505
            ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log')
506
            ->where(
507
                $qb->expr()->andX(
508
                    $qb->expr()->eq('log.event_id', $eventId),
509
                    $qb->expr()->eq('log.is_scheduled', 0),
510
                    $qb->expr()->in('log.lead_id', $contactIds)
511
                )
512
            );
513
514
        $results = $qb->execute()->fetchAll();
515
516
        $dates = [];
517
        foreach ($results as $result) {
518
            $dates[$result['lead_id']] = new \DateTime($result['date_triggered'], new \DateTimeZone('UTC'));
519
        }
520
521
        return $dates;
522
    }
523
524
    /**
525
     * @param int $contactId
526
     * @param int $campaignId
527
     * @param int $rotation
528
     *
529
     * @return bool
530
     */
531
    public function hasBeenInCampaignRotation($contactId, $campaignId, $rotation)
532
    {
533
        $qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
534
        $qb->select('log.rotation')
535
            ->from(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', 'log')
536
            ->where(
537
                $qb->expr()->andX(
538
                    $qb->expr()->eq('log.lead_id', ':contactId'),
539
                    $qb->expr()->eq('log.campaign_id', ':campaignId'),
540
                    $qb->expr()->in('log.rotation', ':rotation')
541
                )
542
            )
543
            ->setParameter('contactId', (int) $contactId)
544
            ->setParameter('campaignId', (int) $campaignId)
545
            ->setParameter('rotation', (int) $rotation)
546
            ->setMaxResults(1);
547
548
        $results = $qb->execute()->fetchAll();
549
550
        return !empty($results);
551
    }
552
553
    /**
554
     * @param string $message
555
     *
556
     * @throws \Doctrine\DBAL\DBALException
557
     */
558
    public function unscheduleEvents(Lead $campaignMember, $message)
559
    {
560
        $contactId  = $campaignMember->getLead()->getId();
561
        $campaignId = $campaignMember->getCampaign()->getId();
562
        $rotation   = $campaignMember->getRotation();
563
        $dateAdded  = (new \DateTime('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s');
564
        // Insert entries into the failed log so it's known why they were never executed
565
        $prefix = MAUTIC_TABLE_PREFIX;
566
        $sql    = <<<SQL
567
REPLACE INTO {$prefix}campaign_lead_event_failed_log( `log_id`, `date_added`, `reason`)
568
SELECT id, :dateAdded as date_added, :message as reason from {$prefix}campaign_lead_event_log
569
WHERE is_scheduled = 1 AND lead_id = :contactId AND campaign_id = :campaignId AND rotation = :rotation
570
SQL;
571
572
        $connection = $this->getEntityManager()->getConnection();
573
        $stmt       = $connection->prepare($sql);
574
        $stmt->bindParam('dateAdded', $dateAdded, \PDO::PARAM_STR);
575
        $stmt->bindParam('message', $message, \PDO::PARAM_STR);
576
        $stmt->bindParam('contactId', $contactId, \PDO::PARAM_INT);
577
        $stmt->bindParam('campaignId', $campaignId, \PDO::PARAM_INT);
578
        $stmt->bindParam('rotation', $rotation, \PDO::PARAM_INT);
579
        $stmt->execute();
580
581
        // Now unschedule them
582
        $qb = $connection->createQueryBuilder();
583
        $qb->update(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log')
584
            ->set('is_scheduled', 0)
585
            ->where(
586
                $qb->expr()->andX(
587
                    $qb->expr()->eq('is_scheduled', 1),
588
                    $qb->expr()->eq('lead_id', ':contactId'),
589
                    $qb->expr()->eq('campaign_id', ':campaignId'),
590
                    $qb->expr()->eq('rotation', ':rotation')
591
                )
592
            )
593
            ->setParameters(
594
                [
595
                    'contactId'     => (int) $contactId,
596
                    'campaignId'    => (int) $campaignId,
597
                    'rotation'      => (int) $rotation,
598
                ]
599
            )
600
            ->execute();
601
    }
602
603
    /**
604
     * Removes logs by event_id.
605
     * It uses batch processing for removing
606
     * large quantities of records.
607
     *
608
     * @param int $eventId
609
     */
610
    public function removeEventLogs($eventId)
611
    {
612
        $conn = $this->_em->getConnection();
613
        $conn->delete(MAUTIC_TABLE_PREFIX.'campaign_lead_event_log', [
614
            'event_id' => (int) $eventId,
615
        ]);
616
    }
617
}
618