Completed
Push — master ( a18731...e2e070 )
by
unknown
99:54 queued 47:45
created

getOpenWeightedPipelineAmount()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 16

Duplication

Lines 23
Ratio 100 %

Importance

Changes 3
Bugs 3 Features 1
Metric Value
dl 23
loc 23
rs 9.0856
c 3
b 3
f 1
cc 2
eloc 16
nc 2
nop 4
1
<?php
2
3
namespace OroCRM\Bundle\SalesBundle\Entity\Repository;
4
5
use Doctrine\ORM\EntityRepository;
6
use Doctrine\ORM\QueryBuilder;
7
8
use Oro\Bundle\DashboardBundle\Filter\DateFilterProcessor;
9
use Oro\Bundle\DataAuditBundle\Loggable\LoggableManager;
10
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper;
11
use Oro\Bundle\EntityExtendBundle\Tools\ExtendHelper;
12
use Oro\Component\DoctrineUtils\ORM\QueryUtils;
13
14
use OroCRM\Bundle\SalesBundle\Entity\Opportunity;
15
16
/**
17
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
18
 */
19
class OpportunityRepository extends EntityRepository
20
{
21
    const OPPORTUNITY_STATE_IN_PROGRESS      = 'In Progress';
22
    const OPPORTUNITY_STATE_IN_PROGRESS_CODE = 'in_progress';
23
    const OPPORTUNITY_STATUS_CLOSED_WON_CODE = 'won';
24
25
    /**
26
     * Get opportunities by state by current quarter
27
     *
28
     * @param           $aclHelper AclHelper
29
     * @param  array    $dateRange
30
     * @param  array    $states
31
     * @param int[]     $owners
32
     *
33
     * @param AclHelper $aclHelper
34
     * @param array     $dateRange
35
     * @param array     $states
36
     * @param int[]     $owners
37
     *
38
     * @return array
39
     */
40
    public function getOpportunitiesByStatus(AclHelper $aclHelper, $dateRange, $states, $owners = [])
41
    {
42
        $dateEnd   = $dateRange['end'];
43
        $dateStart = $dateRange['start'];
44
45
        return $this->getOpportunitiesDataByStatus($aclHelper, $dateStart, $dateEnd, $states, $owners);
46
    }
47
48
    /**
49
     * @param string $alias
50
     * @param string $orderBy
51
     * @param string $direction
52
     *
53
     * @return QueryBuilder
54
     *
55
     */
56
    public function getGroupedOpportunitiesByStatusQB(
57
        $alias,
58
        $orderBy = 'budget',
59
        $direction = 'DESC'
60
    ) {
61
        $statusClass = ExtendHelper::buildEnumValueClassName('opportunity_status');
62
        $repository  = $this->getEntityManager()->getRepository($statusClass);
63
64
        $qb = $repository->createQueryBuilder('s')
65
            ->select(
66
                's.name as label',
67
                sprintf('COUNT(%s.id) as quantity', $alias),
68
                // Use close revenue for calculating budget for opportunities with won statuses
69
                sprintf(
70
                    "SUM(
71
                        CASE WHEN s.id = 'won'
72
                            THEN
73
                                (CASE WHEN %s.closeRevenue IS NOT NULL THEN %s.closeRevenue ELSE 0 END)
74
                            ELSE
75
                                (CASE WHEN %s.budgetAmount IS NOT NULL THEN %s.budgetAmount ELSE 0 END)
76
                        END
77
                    ) as budget",
78
                    $alias,
79
                    $alias,
80
                    $alias,
81
                    $alias
82
                )
83
            )
84
            ->leftJoin('OroCRMSalesBundle:Opportunity', $alias, 'WITH', sprintf('%s.status = s', $alias))
85
            ->groupBy('s.name')
86
            ->orderBy($orderBy, $direction);
87
88
        return $qb;
89
    }
90
91
    /**
92
     * @param  AclHelper $aclHelper
93
     * @param            $dateStart
94
     * @param            $dateEnd
95
     * @param array      $states
96
     * @param int[]      $owners
97
     *
98
     * @return array
99
     */
100
    protected function getOpportunitiesDataByStatus(
101
        AclHelper $aclHelper,
102
        $dateStart = null,
103
        $dateEnd = null,
104
        $states = [],
105
        $owners = []
106
    ) {
107
        foreach ($states as $key => $name) {
108
            $resultData[$key] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$resultData was never initialized. Although not strictly required by PHP, it is generally a good practice to add $resultData = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
109
                'name'   => $key,
110
                'label'  => $name,
111
                'budget' => 0,
112
            ];
113
        }
114
115
        // select opportunity data
116
        $qb = $this->createQueryBuilder('opportunity');
117
        $qb->select('IDENTITY(opportunity.status) as name, SUM(opportunity.budgetAmount) as budget')
118
            ->groupBy('opportunity.status');
119
120 View Code Duplication
        if ($dateStart && $dateEnd) {
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...
121
            $qb->where($qb->expr()->between('opportunity.createdAt', ':dateFrom', ':dateTo'))
122
                ->setParameter('dateFrom', $dateStart)
123
                ->setParameter('dateTo', $dateEnd);
124
        }
125
126
        if ($owners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $owners of type integer[] 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...
127
            QueryUtils::applyOptimizedIn($qb, 'opportunity.owner', $owners);
128
        }
129
130
        $groupedData = $aclHelper->apply($qb)->getArrayResult();
131
132
        foreach ($groupedData as $statusData) {
133
            $status = $statusData['name'];
134
            $budget = (float)$statusData['budget'];
135
            if ($budget) {
136
                $resultData[$status]['budget'] = $budget;
0 ignored issues
show
Bug introduced by
The variable $resultData does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
137
            }
138
        }
139
140
        return $resultData;
141
    }
142
143
    /**
144
     * @param array       $ownerIds
145
     * @param \DateTime    $date
146
     * @param AclHelper   $aclHelper
147
     *
148
     * @param string|null $start
149
     * @param string|null $end
150
     *
151
     * @return mixed
152
     */
153
    public function getForecastOfOpportunitiesData($ownerIds, $date, AclHelper $aclHelper, $start = null, $end = null)
154
    {
155
        if ($date === null) {
156
            return $this->getForecastOfOpportunitiesCurrentData($ownerIds, $aclHelper, $start, $end);
157
        }
158
159
        return $this->getForecastOfOpportunitiesOldData($ownerIds, $date, $aclHelper);
160
    }
161
162
    /**
163
     * @param array       $ownerIds
164
     * @param AclHelper   $aclHelper
165
     * @param string|null $start
166
     * @param string|null $end
167
     *
168
     * @return mixed
169
     */
170
    protected function getForecastOfOpportunitiesCurrentData(
171
        $ownerIds,
172
        AclHelper $aclHelper,
173
        $start = null,
174
        $end = null
175
    ) {
176
        $qb = $this->createQueryBuilder('opportunity');
177
178
        $select = "
179
            COUNT( opportunity.id ) as inProgressCount,
180
            SUM( opportunity.budgetAmount ) as budgetAmount,
181
            SUM( opportunity.budgetAmount * opportunity.probability ) as weightedForecast";
182
        $qb
183
            ->select($select)
184
            ->andWhere('opportunity.status NOT IN (:notCountedStatuses)')
185
            ->setParameter('notCountedStatuses', ['lost', 'won']);
186
        if (!empty($ownerIds)) {
187
            $qb->join('opportunity.owner', 'owner');
188
            QueryUtils::applyOptimizedIn($qb, 'owner.id', $ownerIds);
189
        }
190
191
        $probabilityCondition = $qb->expr()->orX(
192
            $qb->expr()->andX(
193
                'opportunity.probability <> 0',
194
                'opportunity.probability <> 1'
195
            ),
196
            'opportunity.probability is NULL'
197
        );
198
199
        $qb->andWhere($probabilityCondition);
200
        if ($start) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $start of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
201
            $qb
202
                ->andWhere('opportunity.closeDate >= :startDate')
203
                ->setParameter('startDate', $start);
204
        }
205
        if ($end) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $end of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
206
            $qb
207
                ->andWhere('opportunity.closeDate <= :endDate')
208
                ->setParameter('endDate', $end);
209
        }
210
211
        return $aclHelper->apply($qb)->getOneOrNullResult();
212
    }
213
214
    /**
215
     * @param array     $ownerIds
216
     * @param \DateTime $date
217
     * @param AclHelper $aclHelper
218
     *
219
     * @return mixed
220
     */
221
    protected function getForecastOfOpportunitiesOldData($ownerIds, $date, AclHelper $aclHelper)
222
    {
223
        //clone date for avoiding wrong date on printing with current locale
224
        $newDate = clone $date;
225
        $qb      = $this->createQueryBuilder('opportunity')
226
            ->where('opportunity.createdAt < :date')
227
            ->setParameter('date', $newDate);
228
229
        $opportunities = $aclHelper->apply($qb)->getResult();
230
231
        $result['inProgressCount']  = 0;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$result was never initialized. Although not strictly required by PHP, it is generally a good practice to add $result = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
232
        $result['budgetAmount']     = 0;
233
        $result['weightedForecast'] = 0;
234
235
        $auditRepository = $this->getEntityManager()->getRepository('OroDataAuditBundle:Audit');
236
        /** @var Opportunity $opportunity */
237
        foreach ($opportunities as $opportunity) {
238
            $auditQb = $auditRepository->getLogEntriesQueryBuilder($opportunity);
239
            $auditQb->andWhere('a.action = :action')
240
                ->andWhere('a.loggedAt > :date')
241
                ->setParameter('action', LoggableManager::ACTION_UPDATE)
242
                ->setParameter('date', $newDate);
243
            $opportunityHistory = $aclHelper->apply($auditQb)->getResult();
244
245
            if ($oldProbability = $this->getHistoryOldValue($opportunityHistory, 'probability')) {
246
                $isProbabilityOk = $oldProbability !== 0 && $oldProbability !== 1;
247
                $probability     = $oldProbability;
248
            } else {
249
                $probability     = $opportunity->getProbability();
250
                $isProbabilityOk = !is_null($probability) && $probability !== 0 && $probability !== 1;
251
            }
252
253
            if ($isProbabilityOk
254
                && $this->isOwnerOk($ownerIds, $opportunityHistory, $opportunity)
255
                && $this->isStatusOk($opportunityHistory, $opportunity)
256
            ) {
257
                $result = $this->calculateOpportunityOldValue($result, $opportunityHistory, $opportunity, $probability);
258
            }
259
        }
260
261
        return $result;
262
    }
263
264
    /**
265
     * @param mixed  $opportunityHistory
266
     * @param string $field
267
     *
268
     * @return mixed
269
     */
270
    protected function getHistoryOldValue($opportunityHistory, $field)
271
    {
272
        $result = null;
273
274
        $opportunityHistory = is_array($opportunityHistory) ? $opportunityHistory : [$opportunityHistory];
275
        foreach ($opportunityHistory as $item) {
276
            if ($item->getField($field)) {
277
                $result = $item->getField($field)->getOldValue();
278
            }
279
        }
280
281
        return $result;
282
    }
283
284
    /**
285
     * @param array       $opportunityHistory
286
     * @param Opportunity $opportunity
287
     *
288
     * @return bool
289
     */
290
    protected function isStatusOk($opportunityHistory, $opportunity)
291
    {
292
        if ($oldStatus = $this->getHistoryOldValue($opportunityHistory, 'status')) {
293
            $isStatusOk = $oldStatus === self::OPPORTUNITY_STATE_IN_PROGRESS;
294
        } else {
295
            $isStatusOk = $opportunity->getStatus()->getName() === self::OPPORTUNITY_STATE_IN_PROGRESS_CODE;
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class OroCRM\Bundle\SalesBundle\Entity\Opportunity as the method getStatus() does only exist in the following sub-classes of OroCRM\Bundle\SalesBundle\Entity\Opportunity: OroCRM\Bundle\SalesBundl...s\Unit\Stub\Opportunity. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
296
        }
297
298
        return $isStatusOk;
299
    }
300
301
    /**
302
     * @param array       $ownerIds
303
     * @param array       $opportunityHistory
304
     * @param Opportunity $opportunity
305
     *
306
     * @return bool
307
     */
308
    protected function isOwnerOk($ownerIds, $opportunityHistory, $opportunity)
309
    {
310
        $userRepository = $this->getEntityManager()->getRepository('OroUserBundle:User');
311
        if ($oldOwner = $this->getHistoryOldValue($opportunityHistory, 'owner')) {
312
            $isOwnerOk = in_array($userRepository->findOneByUsername($oldOwner)->getId(), $ownerIds);
313
        } else {
314
            $isOwnerOk = in_array($opportunity->getOwner()->getId(), $ownerIds);
315
        }
316
317
        return $isOwnerOk;
318
    }
319
320
    /**
321
     * @param array       $result
322
     * @param array       $opportunityHistory
323
     * @param Opportunity $opportunity
324
     * @param mixed       $probability
325
     *
326
     * @return array
327
     */
328
    protected function calculateOpportunityOldValue($result, $opportunityHistory, $opportunity, $probability)
329
    {
330
        ++$result['inProgressCount'];
331
        $oldBudgetAmount = $this->getHistoryOldValue($opportunityHistory, 'budgetAmount');
332
333
        $budget = $oldBudgetAmount !== null ? $oldBudgetAmount : $opportunity->getBudgetAmount();
334
        $result['budgetAmount'] += $budget;
335
        $result['weightedForecast'] += $budget * $probability;
336
337
        return $result;
338
    }
339
340
    /**
341
     * @param AclHelper $aclHelper
342
     * @param \DateTime  $start
343
     * @param \DateTime  $end
344
     * @param int[]     $owners
345
     *
346
     * @return int
347
     */
348 View Code Duplication
    public function getOpportunitiesCount(
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...
349
        AclHelper $aclHelper,
350
        \DateTime $start = null,
351
        \DateTime $end = null,
352
        $owners = []
353
    ) {
354
        $qb = $this->createOpportunitiesCountQb($start, $end, $owners);
355
356
        return $aclHelper->apply($qb)->getSingleScalarResult();
357
    }
358
359
    /**
360
     * @param AclHelper $aclHelper
361
     * @param \DateTime  $start
362
     * @param \DateTime  $end
363
     * @param int[]     $owners
364
     *
365
     * @return int
366
     */
367 View Code Duplication
    public function getNewOpportunitiesCount(
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...
368
        AclHelper $aclHelper,
369
        \DateTime $start = null,
370
        \DateTime $end = null,
371
        $owners = []
372
    ) {
373
        $qb = $this->createOpportunitiesCountQb($start, $end, $owners);
374
375
        return $aclHelper->apply($qb)->getSingleScalarResult();
376
    }
377
378
    /**
379
     * @param \DateTime $start
380
     * @param \DateTime $end
381
     * @param int[]    $owners
382
     *
383
     * @return QueryBuilder
384
     */
385 View Code Duplication
    public function createOpportunitiesCountQb(\DateTime $start = null, \DateTime $end = null, $owners = [])
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...
386
    {
387
        $qb = $this->createQueryBuilder('o');
388
        $qb->select('COUNT(o.id)');
389
        if ($start) {
390
            $qb
391
                ->andWhere('o.createdAt > :start')
392
                ->setParameter('start', $start);
393
        }
394
        if ($end) {
395
            $qb
396
                ->andWhere('o.createdAt < :end')
397
                ->setParameter('end', $end);
398
        }
399
400
        if ($owners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $owners of type integer[] 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...
401
            QueryUtils::applyOptimizedIn($qb, 'o.owner', $owners);
402
        }
403
404
        return $qb;
405
    }
406
407
    /**
408
     * @param AclHelper $aclHelper
409
     * @param \DateTime  $start
410
     * @param \DateTime  $end
411
     * @param int[]     $owners
412
     *
413
     * @return double
414
     */
415
    public function getTotalServicePipelineAmount(
416
        AclHelper $aclHelper,
417
        \DateTime $start = null,
418
        \DateTime $end = null,
419
        $owners = []
420
    ) {
421
        $qb = $this->createQueryBuilder('o');
422
423
        $qb
424
            ->select('SUM(o.budgetAmount)')
425
            ->andWhere('o.closeDate IS NULL')
426
            ->andWhere('o.status = :status')
427
            ->andWhere('o.probability != 0')
428
            ->andWhere('o.probability != 1')
429
            ->setParameter('status', self::OPPORTUNITY_STATE_IN_PROGRESS_CODE);
430
        if ($start) {
431
            $qb
432
                ->andWhere('o.createdAt > :start')
433
                ->setParameter('start', $start);
434
        }
435
        if ($end) {
436
            $qb
437
                ->andWhere('o.createdAt < :end')
438
                ->setParameter('end', $end);
439
        }
440
441
        if ($owners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $owners of type integer[] 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...
442
            QueryUtils::applyOptimizedIn($qb, 'o.owner', $owners);
443
        }
444
445
        return $aclHelper->apply($qb)->getSingleScalarResult();
446
    }
447
448
    /**
449
     * @param AclHelper $aclHelper
450
     * @param \DateTime  $start
451
     * @param \DateTime  $end
452
     *
453
     * @return double
454
     */
455
    public function getTotalServicePipelineAmountInProgress(
456
        AclHelper $aclHelper,
457
        \DateTime $start = null,
458
        \DateTime $end = null
459
    ) {
460
        $qb = $this->createQueryBuilder('o');
461
462
        $qb
463
            ->select('SUM(o.budgetAmount)')
464
            ->andWhere('o.status = :status')
465
            ->andWhere('o.probability != 0')
466
            ->andWhere('o.probability != 1')
467
            ->setParameter('status', self::OPPORTUNITY_STATE_IN_PROGRESS_CODE);
468
        if ($start) {
469
            $qb
470
                ->andWhere('o.createdAt > :start')
471
                ->setParameter('start', $start);
472
        }
473
        if ($end) {
474
            $qb
475
                ->andWhere('o.createdAt < :end')
476
                ->setParameter('end', $end);
477
        }
478
479
        return $aclHelper->apply($qb)->getSingleScalarResult();
480
    }
481
482
    /**
483
     * @param AclHelper $aclHelper
484
     * @param \DateTime  $start
485
     * @param \DateTime  $end
486
     *
487
     * @return double
488
     */
489 View Code Duplication
    public function getWeightedPipelineAmount(AclHelper $aclHelper, \DateTime $start = null, \DateTime $end = 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...
490
    {
491
        $qb = $this->createQueryBuilder('o');
492
493
        $qb->select('SUM(o.budgetAmount * o.probability)');
494
        if ($start) {
495
            $qb
496
                ->andWhere('o.createdAt > :start')
497
                ->setParameter('start', $start);
498
        }
499
        if ($end) {
500
            $qb
501
                ->andWhere('o.createdAt < :end')
502
                ->setParameter('end', $end);
503
        }
504
505
        return $aclHelper->apply($qb)->getSingleScalarResult();
506
    }
507
508
    /**
509
     * @param AclHelper $aclHelper
510
     * @param \DateTime  $start
511
     * @param \DateTime  $end
512
     * @param int[]     $owners
513
     *
514
     * @return double
515
     */
516 View Code Duplication
    public function getOpenWeightedPipelineAmount(
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...
517
        AclHelper $aclHelper,
518
        \DateTime $start = null,
519
        \DateTime $end = null,
520
        $owners = []
521
    ) {
522
        $qb = $this->createQueryBuilder('o');
523
524
        $qb
525
            ->select('SUM(o.budgetAmount * o.probability)')
526
            ->andWhere('o.status = :status')
527
            ->andWhere('o.probability != 0')
528
            ->andWhere('o.probability != 1')
529
            ->setParameter('status', self::OPPORTUNITY_STATE_IN_PROGRESS_CODE);
530
531
        $this->setCreationPeriod($qb, $start, $end);
532
533
        if ($owners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $owners of type integer[] 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...
534
            QueryUtils::applyOptimizedIn($qb, 'o.owner', $owners);
535
        }
536
537
        return $aclHelper->apply($qb)->getSingleScalarResult();
538
    }
539
540
    /**
541
     * @param AclHelper $aclHelper
542
     * @param \DateTime  $start
543
     * @param \DateTime  $end
544
     * @param int[]     $owners
545
     *
546
     * @return double
547
     */
548
    public function getNewOpportunitiesAmount(
549
        AclHelper $aclHelper,
550
        \DateTime $start = null,
551
        \DateTime $end = null,
552
        $owners = []
553
    ) {
554
        $qb = $this
555
            ->createQueryBuilder('o')
556
            ->select('SUM(o.budgetAmount)');
557
558
        $this->setCreationPeriod($qb, $start, $end);
559
560
        if ($owners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $owners of type integer[] 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...
561
            QueryUtils::applyOptimizedIn($qb, 'o.owner', $owners);
562
        }
563
564
        return $aclHelper->apply($qb)->getSingleScalarResult();
565
    }
566
567
    /**
568
     * @param AclHelper $aclHelper
569
     * @param \DateTime  $start
570
     * @param \DateTime  $end
571
     * @param int[]     $owners
572
     *
573
     * @return int
574
     */
575 View Code Duplication
    public function getWonOpportunitiesToDateCount(
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...
576
        AclHelper $aclHelper,
577
        \DateTime $start = null,
578
        \DateTime $end = null,
579
        $owners = []
580
    ) {
581
        $qb = $this->createQueryBuilder('o');
582
        $qb->select('COUNT(o.id)')
583
            ->andWhere('o.status = :status')
584
            ->setParameter('status', self::OPPORTUNITY_STATUS_CLOSED_WON_CODE);
585
586
        $this->setClosedPeriod($qb, $start, $end);
587
588
        if ($owners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $owners of type integer[] 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...
589
            QueryUtils::applyOptimizedIn($qb, 'o.owner', $owners);
590
        }
591
592
        return $aclHelper->apply($qb)->getSingleScalarResult();
593
    }
594
595
    /**
596
     * @param AclHelper $aclHelper
597
     * @param \DateTime  $start
598
     * @param \DateTime  $end
599
     * @param int[]     $owners
600
     *
601
     * @return double
602
     */
603 View Code Duplication
    public function getWonOpportunitiesToDateAmount(
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...
604
        AclHelper $aclHelper,
605
        \DateTime $start = null,
606
        \DateTime $end = null,
607
        $owners = []
608
    ) {
609
        $qb = $this->createQueryBuilder('o');
610
        $qb->select('SUM(o.closeRevenue)')
611
            ->andWhere('o.status = :status')
612
            ->setParameter('status', self::OPPORTUNITY_STATUS_CLOSED_WON_CODE);
613
614
        $this->setClosedPeriod($qb, $start, $end);
615
616
        if ($owners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $owners of type integer[] 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...
617
            QueryUtils::applyOptimizedIn($qb, 'o.owner', $owners);
618
        }
619
620
        return $aclHelper->apply($qb)->getSingleScalarResult();
621
    }
622
623
    /**
624
     * @param QueryBuilder $qb
625
     * @param \DateTime|null $start
626
     * @param \DateTime|null $end
627
     */
628 View Code Duplication
    protected function setCreationPeriod(QueryBuilder $qb, \DateTime $start = null, \DateTime $end = 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...
629
    {
630
        $qb
631
            ->andWhere($qb->expr()->between('o.createdAt', ':dateStart', ':dateEnd'))
632
            ->setParameter('dateStart', $start)
633
            ->setParameter('dateEnd', $end);
634
    }
635
636
    /**
637
     * @param QueryBuilder $qb
638
     * @param \DateTime|null $start
639
     * @param \DateTime|null $end
640
     */
641 View Code Duplication
    protected function setClosedPeriod(QueryBuilder $qb, \DateTime $start = null, \DateTime $end = 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...
642
    {
643
        $qb
644
            ->andWhere($qb->expr()->between('o.closeDate', ':dateStart', ':dateEnd'))
645
            ->setParameter('dateStart', $start)
646
            ->setParameter('dateEnd', $end);
647
    }
648
649
    /**
650
     * Returns count of opportunities grouped by lead source
651
     *
652
     * @param AclHelper $aclHelper
653
     * @param DateFilterProcessor $dateFilterProcessor
654
     * @param array $dateRange
655
     * @param array $owners
656
     *
657
     * @return array [value, source]
658
     */
659 View Code Duplication
    public function getOpportunitiesCountGroupByLeadSource(
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...
660
        AclHelper $aclHelper,
661
        DateFilterProcessor $dateFilterProcessor,
662
        array $dateRange = [],
663
        array $owners = []
664
    ) {
665
        $qb = $this->getOpportunitiesGroupByLeadSourceQueryBuilder($dateFilterProcessor, $dateRange, $owners);
666
        $qb->addSelect('count(o.id) as value');
667
668
        return $aclHelper->apply($qb)->getArrayResult();
669
    }
670
671
    /**
672
     * Returns budget amount of opportunities grouped by lead source
673
     *
674
     * @param AclHelper $aclHelper
675
     * @param DateFilterProcessor $dateFilterProcessor
676
     * @param array $dateRange
677
     * @param array $owners
678
     *
679
     * @return array [value, source]
680
     */
681 View Code Duplication
    public function getOpportunitiesAmountGroupByLeadSource(
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...
682
        AclHelper $aclHelper,
683
        DateFilterProcessor $dateFilterProcessor,
684
        array $dateRange = [],
685
        array $owners = []
686
    ) {
687
        $qb = $this->getOpportunitiesGroupByLeadSourceQueryBuilder($dateFilterProcessor, $dateRange, $owners);
688
        $qb->addSelect("SUM(CASE WHEN o.status = 'won' THEN o.closeRevenue ELSE o.budgetAmount END) as value");
689
690
        return $aclHelper->apply($qb)->getArrayResult();
691
    }
692
693
    /**
694
     * Returns opportunities QB grouped by lead source filtered by $dateRange and $owners
695
     *
696
     * @param DateFilterProcessor $dateFilterProcessor
697
     * @param array $dateRange
698
     * @param array $owners
699
     *
700
     * @return QueryBuilder
701
     */
702
    protected function getOpportunitiesGroupByLeadSourceQueryBuilder(
703
        DateFilterProcessor $dateFilterProcessor,
704
        array $dateRange = [],
705
        array $owners = []
706
    ) {
707
        $qb = $this->createQueryBuilder('o')
708
            ->select('s.id as source')
709
            ->leftJoin('o.lead', 'l')
710
            ->leftJoin('l.source', 's')
711
            ->groupBy('source');
712
713
        $dateFilterProcessor->process($qb, $dateRange, 'o.createdAt');
714
715
        if ($owners) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $owners 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...
716
            QueryUtils::applyOptimizedIn($qb, 'o.owner', $owners);
717
        }
718
719
        return $qb;
720
    }
721
722
    /**
723
     * @param string $alias
724
     * @param array  $excludedStatuses
725
     *
726
     * @return QueryBuilder
727
     */
728
    public function getForecastQB($alias = 'o', array $excludedStatuses = ['lost', 'won'])
729
    {
730
        $qb     = $this->createQueryBuilder($alias);
731
        $qb->select([
732
            sprintf('COUNT(%s.id) as inProgressCount', $alias),
733
            sprintf('SUM(%s.budgetAmount) as budgetAmount', $alias),
734
            sprintf('SUM(%s.budgetAmount * %s.probability) as weightedForecast', $alias, $alias)
735
        ]);
736
737
        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...
738
            $qb->andWhere($qb->expr()->notIn(sprintf('%s.status', $alias), $excludedStatuses));
739
        }
740
741
        return $qb;
742
    }
743
}
744