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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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) { |
|
|
|
|
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')); |
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|
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.