Completed
Push — master ( 584969...3f5763 )
by
unknown
11:30
created

CartRepository::getAbandonedRevenueQB()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 8
rs 9.4285
cc 1
eloc 5
nc 1
nop 0
1
<?php
2
3
namespace OroCRM\Bundle\MagentoBundle\Entity\Repository;
4
5
use Doctrine\ORM\NoResultException;
6
use Doctrine\ORM\QueryBuilder;
7
8
use Oro\Bundle\IntegrationBundle\Entity\Channel;
9
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper;
10
use Oro\Bundle\WorkflowBundle\Model\Workflow;
11
use Oro\Bundle\BatchBundle\ORM\Query\BufferedQueryResultIterator;
12
13
use OroCRM\Bundle\MagentoBundle\Entity\Cart;
14
use OroCRM\Bundle\MagentoBundle\Entity\CartStatus;
15
16
class CartRepository extends ChannelAwareEntityRepository
17
{
18
    /**
19
     * @var array
20
     */
21
    protected $excludedSteps = [
22
        'converted_to_opportunity',
23
        'abandoned'
24
    ];
25
26
    /**
27
     * @var array
28
     */
29
    protected $excludedStatuses = [
30
        CartStatus::STATUS_PURCHASED,
31
        CartStatus::STATUS_EXPIRED
32
    ];
33
34
    /**
35
     * @param \DateTime $dateFrom
36
     * @param \DateTime $dateTo
37
     * @param Workflow  $workflow
38
     * @param AclHelper $aclHelper
39
     *
40
     * @return array
41
     */
42
    public function getFunnelChartData(
43
        \DateTime $dateFrom = null,
44
        \DateTime $dateTo = null,
45
        Workflow $workflow = null,
46
        AclHelper $aclHelper = null
47
    ) {
48
        if (!$workflow) {
49
            return ['items' => [], 'nozzleSteps' => []];
50
        }
51
52
        $steps = $workflow->getStepManager()->getOrderedSteps();
53
54
        // regular and final steps should be calculated separately
55
        $regularSteps = [];
56
        $finalSteps   = [];
57 View Code Duplication
        foreach ($steps as $step) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
58
            if (!in_array($step->getName(), $this->excludedSteps)) {
59
                if ($step->isFinal()) {
60
                    $finalSteps[] = $step->getName();
61
                } else {
62
                    $regularSteps[] = $step->getName();
63
                }
64
            }
65
        }
66
67
        // regular steps should be calculated for whole period, final steps - for specified period
68
        $regularStepsData = $this->getStepData($regularSteps, null, null, $aclHelper);
69
        $finalStepsData   = $this->getStepData($finalSteps, $dateFrom, $dateTo, $aclHelper);
70
71
        // final calculation
72
        $data = [];
73 View Code Duplication
        foreach ($steps as $step) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
74
            $stepName = $step->getName();
75
            if (!in_array($stepName, $this->excludedSteps)) {
76
                if ($step->isFinal()) {
77
                    $stepValue = isset($finalStepsData[$stepName]) ? $finalStepsData[$stepName] : 0;
78
                    $data[]    = ['label' => $step->getLabel(), 'value' => $stepValue, 'isNozzle' => true];
79
                } else {
80
                    $stepValue = isset($regularStepsData[$stepName]) ? $regularStepsData[$stepName] : 0;
81
                    $data[]    = ['label' => $step->getLabel(), 'value' => $stepValue, 'isNozzle' => false];
82
                }
83
            }
84
        }
85
86
        return $data;
87
    }
88
89
    /**
90
     * @param array     $steps
91
     * @param AclHelper $aclHelper
92
     * @param \DateTime $dateFrom
93
     * @param \DateTime $dateTo
94
     *
95
     * @return array
96
     */
97
    protected function getStepData(
98
        array $steps,
99
        \DateTime $dateFrom = null,
100
        \DateTime $dateTo = null,
101
        AclHelper $aclHelper = null
102
    ) {
103
        $stepData = [];
104
105
        if (!$steps) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $steps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
106
            return $stepData;
107
        }
108
109
        $queryBuilder = $this->createQueryBuilder('cart')
110
            ->select('workflowStep.name as workflowStepName', 'SUM(cart.grandTotal) as total')
111
            ->leftJoin('cart.status', 'status')
112
            ->join('cart.workflowStep', 'workflowStep')
113
            ->groupBy('workflowStep.name');
114
115
        $queryBuilder->where($queryBuilder->expr()->in('workflowStep.name', $steps));
116
117
        if ($dateFrom) {
118
            $queryBuilder
119
                ->andWhere('cart.createdAt > :start')
120
                ->setParameter('start', $dateFrom);
121
        }
122
        if ($dateTo) {
123
            $queryBuilder
124
                ->andWhere('cart.createdAt < :end')
125
                ->setParameter('end', $dateTo);
126
        }
127
128
        if ($this->excludedStatuses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->excludedStatuses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
129
            $queryBuilder->andWhere($queryBuilder->expr()->notIn('status.name', $this->excludedStatuses));
130
        }
131
132
        $this->applyActiveChannelLimitation($queryBuilder);
133
134
        if ($aclHelper) {
135
            $query = $aclHelper->apply($queryBuilder);
136
        } else {
137
            $query = $queryBuilder->getQuery();
138
        }
139
140
        foreach ($query->getArrayResult() as $record) {
141
            $stepData[$record['workflowStepName']] = $record['total'] ? (float)$record['total'] : 0;
142
        }
143
144
        return $stepData;
145
    }
146
147
    /**
148
     * Update statuses for carts to 'expired'
149
     *
150
     * @param array $ids
151
     */
152
    public function markExpired(array $ids)
153
    {
154
        $em = $this->getEntityManager();
155
        foreach ($ids as $id) {
156
            /** @var Cart $cart */
157
            $cart = $em->getReference($this->getEntityName(), $id);
158
            $cart->setStatus($em->getReference('OroCRMMagentoBundle:CartStatus', 'expired'));
0 ignored issues
show
Documentation introduced by
$em->getReference('OroCR...CartStatus', 'expired') is of type object|null, but the function expects a object<OroCRM\Bundle\Mag...ndle\Entity\CartStatus>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
159
        }
160
161
        $em->flush();
162
    }
163
164
    /**
165
     * Returns iterator for fetching IDs pairs by channel and given status
166
     * Each item in iteration will be array with following data:
167
     * [
168
     *      'id'        => ENTITY_ID,
169
     *      'originId'  => ENTITY_ORIGIN_ID
170
     * ]
171
     *
172
     * @param Channel $channel
173
     * @param string  $status
174
     *
175
     * @return \Doctrine\ORM\QueryBuilder
176
     */
177
    public function getCartsByChannelIdsIterator(Channel $channel, $status = 'open')
178
    {
179
        $qb = $this->createQueryBuilder('c')
180
            ->select('c.id, c.originId')
181
            ->leftJoin('c.status', 'cstatus')
182
            ->andWhere('c.channel = :channel')
183
            ->andWhere('cstatus.name = :statusName')
184
            ->setParameter('channel', $channel)
185
            ->setParameter('statusName', $status);
186
187
        return new BufferedQueryResultIterator($qb);
188
    }
189
190
    /**
191
     * @param \DateTime $start
192
     * @param \DateTime $end
193
     * @param AclHelper $aclHelper
194
     *
195
     * @return int
196
     * @throws \Doctrine\ORM\NonUniqueResultException
197
     */
198 View Code Duplication
    public function getAbandonedRevenueByPeriod(\DateTime $start, \DateTime $end, AclHelper $aclHelper)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
199
    {
200
        $qb = $this->getAbandonedQB($start, $end);
201
        $qb->select('SUM(cart.grandTotal) as val');
202
        $this->applyActiveChannelLimitation($qb);
203
        $value = $aclHelper->apply($qb)->getOneOrNullResult();
204
205
        return $value['val'] ? : 0;
206
    }
207
208
    /**
209
     * @param \DateTime $start
210
     * @param \DateTime $end
211
     * @param AclHelper $aclHelper
212
     *
213
     * @return int
214
     * @throws \Doctrine\ORM\NonUniqueResultException
215
     */
216 View Code Duplication
    public function getAbandonedCountByPeriod(\DateTime $start, \DateTime $end, AclHelper $aclHelper)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
217
    {
218
        $qb = $this->getAbandonedQB($start, $end);
219
        $qb->select('COUNT(cart.grandTotal) as val');
220
        $this->applyActiveChannelLimitation($qb);
221
        $value = $aclHelper->apply($qb)->getOneOrNullResult();
222
223
        return $value['val'] ? : 0;
224
    }
225
226
    /**
227
     * @param \DateTime $start
228
     * @param \DateTime $end
229
     * @param AclHelper $aclHelper
230
     *
231
     * @return float|null
232
     * @throws \Doctrine\ORM\NonUniqueResultException
233
     */
234
    public function getAbandonRateByPeriod(\DateTime $start, \DateTime $end, AclHelper $aclHelper)
235
    {
236
        $result = null;
237
238
        $qb = $this->createQueryBuilder('cart');
239
        $qb->join('cart.status', 'cstatus')
240
            ->select('SUM(cart.grandTotal) as val')
241
            ->andWhere('cstatus.name = :statusName')
242
            ->setParameter('statusName', 'open')
243
            ->andWhere($qb->expr()->between('cart.createdAt', ':dateStart', ':dateEnd'))
244
            ->setParameter('dateStart', $start)
245
            ->setParameter('dateEnd', $end);
246
        $this->applyActiveChannelLimitation($qb);
247
        $allCards = $aclHelper->apply($qb)->getOneOrNullResult();
248
        $allCards = (int)$allCards['val'];
249
250
        if (0 !== $allCards) {
251
            $abandonedCartsCount = $this->getAbandonedCountByPeriod($start, $end, $aclHelper);
252
253
            $result = $abandonedCartsCount / $allCards;
254
        }
255
256
        return $result;
257
    }
258
259
    /**
260
     * @param \DateTime $start
261
     * @param \DateTime $end
262
     *
263
     * @return QueryBuilder
264
     */
265
    protected function getAbandonedQB(\DateTime $start = null, \DateTime $end = null)
266
    {
267
        $qb = $this->createQueryBuilder('cart');
268
        $qb->join('cart.status', 'cstatus')
269
            ->andWhere('cstatus.name = :statusName')
270
            ->setParameter('statusName', 'open')
271
            ->andWhere(
272
                $qb->expr()->not(
273
                    $qb->expr()->exists(
274
                        $this->_em->getRepository('OroCRMMagentoBundle:Order')
275
                            ->createQueryBuilder('mOrder')
276
                            ->where('mOrder.cart = cart')
277
                    )
278
                )
279
            );
280
        if ($start) {
281
            $qb
282
                ->andWhere('cart.createdAt > :start')
283
                ->setParameter('start', $start);
284
        }
285
        if ($end) {
286
            $qb
287
                ->andWhere('cart.createdAt < :end')
288
                ->setParameter('end', $end);
289
        }
290
291
        return $qb;
292
    }
293
294
    /**
295
     * @param AclHelper $aclHelper
296
     * @param \DateTime $from
297
     * @param \DateTime $to
298
     *
299
     * @return int
300
     */
301 View Code Duplication
    public function getCustomersCountWhatMakeCarts(AclHelper $aclHelper, \DateTime $from = null, \DateTime $to = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
302
    {
303
        $qb = $this->createQueryBuilder('c');
304
305
        try {
306
            $qb
307
                ->select('COUNT(DISTINCT c.customer) + SUM(CASE WHEN c.isGuest = true THEN 1 ELSE 0 END)');
308
            if ($from) {
309
                $qb
310
                    ->andWhere('c.createdAt > :from')
311
                    ->setParameter('from', $from);
312
            }
313
            if ($to) {
314
                $qb
315
                    ->andWhere('c.createdAt < :to')
316
                    ->setParameter('to', $to);
317
            }
318
            $this->applyActiveChannelLimitation($qb);
319
320
            return (int)$aclHelper->apply($qb)->getSingleScalarResult();
321
        } catch (NoResultException $ex) {
322
            return 0;
323
        }
324
    }
325
326
    /**
327
     * @param string $alias
328
     *
329
     * @return QueryBuilder
330
     */
331 View Code Duplication
    public function getCustomersCountWhatMakeCartsQB($alias)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
332
    {
333
        $qb = $this->createQueryBuilder($alias)
334
            ->select(
335
                sprintf(
336
                    'COUNT(DISTINCT %s.customer) + SUM(CASE WHEN %s.isGuest = true THEN 1 ELSE 0 END)',
337
                    $alias,
338
                    $alias
339
                )
340
            );
341
        $this->applyActiveChannelLimitation($qb);
342
343
        return $qb;
344
    }
345
346
    /**
347
     * @param string $alias
348
     * @param array  $steps
349
     * @param array  $excludedStatuses
350
     *
351
     * @return QueryBuilder
352
     */
353
    public function getStepDataQB($alias, array $steps, array $excludedStatuses = [])
354
    {
355
        $qb = $this->createQueryBuilder($alias)
356
            ->select('workflowStep.name as workflowStepName', sprintf('SUM(%s.grandTotal) as total', $alias))
357
            ->leftJoin(sprintf('%s.status', $alias), 'status')
358
            ->join(sprintf('%s.workflowStep', $alias), 'workflowStep')
359
            ->groupBy('workflowStep.name');
360
361
        $qb->where($qb->expr()->in('workflowStep.name', $steps));
362
363
        if ($excludedStatuses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $excludedStatuses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
364
            $qb->andWhere($qb->expr()->notIn('status.name', $excludedStatuses));
365
        }
366
        $this->applyActiveChannelLimitation($qb);
367
368
        return $qb;
369
    }
370
371
    /**
372
     * @return QueryBuilder
373
     */
374
    public function getAbandonedRevenueQB()
375
    {
376
        $qb = $this->getAbandonedQB();
377
        $qb->select('SUM(cart.grandTotal) as val');
378
        $this->applyActiveChannelLimitation($qb);
379
380
        return $qb;
381
    }
382
383
    /**
384
     * @return QueryBuilder
385
     */
386
    public function getAbandonedCountQB()
387
    {
388
        $qb = $this->getAbandonedQB();
389
        $qb->select('COUNT(cart.grandTotal) as val');
390
        $this->applyActiveChannelLimitation($qb);
391
392
        return $qb;
393
    }
394
395
    /**
396
     * @return QueryBuilder
397
     */
398
    public function getGrandTotalSumQB()
399
    {
400
        $qb = $this->createQueryBuilder('cart');
401
        $qb->join('cart.status', 'cstatus')
402
            ->select('SUM(cart.grandTotal) as val')
403
            ->andWhere('cstatus.name = :statusName')
404
            ->setParameter('statusName', 'open');
405
        $this->applyActiveChannelLimitation($qb);
406
407
        return $qb;
408
    }
409
}
410