1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace OroCRM\Bundle\SalesBundle\Tests\Unit\Provider; |
4
|
|
|
|
5
|
|
|
use Doctrine\ORM\EntityRepository; |
6
|
|
|
use Doctrine\ORM\Query; |
7
|
|
|
use Doctrine\ORM\QueryBuilder; |
8
|
|
|
|
9
|
|
|
use Symfony\Bridge\Doctrine\RegistryInterface; |
10
|
|
|
|
11
|
|
|
use Oro\Bundle\DashboardBundle\Filter\DateFilterProcessor; |
12
|
|
|
use Oro\Bundle\DashboardBundle\Model\WidgetOptionBag; |
13
|
|
|
use Oro\Bundle\EntityExtendBundle\Tools\ExtendHelper; |
14
|
|
|
use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper; |
15
|
|
|
use Oro\Bundle\UserBundle\Dashboard\OwnerHelper; |
16
|
|
|
|
17
|
|
|
use OroCRM\Bundle\SalesBundle\Dashboard\Provider\OpportunityByStatusProvider; |
18
|
|
|
|
19
|
|
|
class OpportunityByStatusProviderTest extends \PHPUnit_Framework_TestCase |
20
|
|
|
{ |
21
|
|
|
/** @var RegistryInterface|\PHPUnit_Framework_MockObject_MockObject */ |
22
|
|
|
protected $registry; |
23
|
|
|
|
24
|
|
|
/** @var AclHelper|\PHPUnit_Framework_MockObject_MockObject */ |
25
|
|
|
protected $aclHelper; |
26
|
|
|
|
27
|
|
|
/** @var DateFilterProcessor|\PHPUnit_Framework_MockObject_MockObject */ |
28
|
|
|
protected $dateFilterProcessor; |
29
|
|
|
|
30
|
|
|
/** @var OwnerHelper|\PHPUnit_Framework_MockObject_MockObject */ |
31
|
|
|
protected $ownerHelper; |
32
|
|
|
|
33
|
|
|
protected $opportunityStatuses = [ |
34
|
|
|
['id' => 'won', 'name' => 'Won'], |
35
|
|
|
['id' => 'identification_alignment', 'name' => 'Identification'], |
36
|
|
|
['id' => 'in_progress', 'name' => 'Open'], |
37
|
|
|
['id' => 'needs_analysis', 'name' => 'Analysis'], |
38
|
|
|
['id' => 'negotiation', 'name' => 'Negotiation'], |
39
|
|
|
['id' => 'solution_development', 'name' => 'Development'], |
40
|
|
|
['id' => 'lost', 'name' => 'Lost'] |
41
|
|
|
]; |
42
|
|
|
|
43
|
|
|
/** @var OpportunityByStatusProvider */ |
44
|
|
|
protected $provider; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* {@inheritdoc} |
48
|
|
|
*/ |
49
|
|
View Code Duplication |
protected function setUp() |
|
|
|
|
50
|
|
|
{ |
51
|
|
|
$this->registry = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry') |
52
|
|
|
->disableOriginalConstructor() |
53
|
|
|
->getMock(); |
54
|
|
|
$this->aclHelper = $this->getMockBuilder('Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper') |
55
|
|
|
->disableOriginalConstructor() |
56
|
|
|
->getMock(); |
57
|
|
|
$this->dateFilterProcessor = $this->getMockBuilder('Oro\Bundle\DashboardBundle\Filter\DateFilterProcessor') |
58
|
|
|
->disableOriginalConstructor() |
59
|
|
|
->getMock(); |
60
|
|
|
$this->ownerHelper = $this->getMockBuilder('Oro\Bundle\UserBundle\Dashboard\OwnerHelper') |
61
|
|
|
->disableOriginalConstructor() |
62
|
|
|
->getMock(); |
63
|
|
|
|
64
|
|
|
$this->provider = new OpportunityByStatusProvider( |
65
|
|
|
$this->registry, |
66
|
|
|
$this->aclHelper, |
67
|
|
|
$this->dateFilterProcessor, |
68
|
|
|
$this->ownerHelper |
69
|
|
|
); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @param WidgetOptionBag $widgetOptions |
74
|
|
|
* @param string $expectation |
75
|
|
|
* |
76
|
|
|
* @dataProvider getOpportunitiesGroupedByStatusDQLDataProvider |
77
|
|
|
*/ |
78
|
|
|
public function testGetOpportunitiesGroupedByStatusDQL($widgetOptions, $expectation) |
79
|
|
|
{ |
80
|
|
|
$this->ownerHelper->expects($this->once()) |
|
|
|
|
81
|
|
|
->method('getOwnerIds') |
82
|
|
|
->willReturn([]); |
83
|
|
|
|
84
|
|
|
$opportunityQB = new QueryBuilder($this->getMock('Doctrine\ORM\EntityManagerInterface')); |
85
|
|
|
$opportunityQB |
86
|
|
|
->from('OroCRM\Bundle\SalesBundle\Entity\Opportunity', 'o', null); |
87
|
|
|
|
88
|
|
|
$statusesQB = $this->getMockQueryBuilder(); |
89
|
|
|
$statusesQB->expects($this->once()) |
|
|
|
|
90
|
|
|
->method('select') |
91
|
|
|
->with('s.id, s.name') |
92
|
|
|
->willReturnSelf(); |
93
|
|
|
$statusesQB->expects($this->once()) |
94
|
|
|
->method('getQuery') |
95
|
|
|
->willReturnSelf(); |
96
|
|
|
$statusesQB->expects($this->once()) |
97
|
|
|
->method('getArrayResult') |
98
|
|
|
->willReturn($this->opportunityStatuses); |
99
|
|
|
|
100
|
|
|
$repository = $this->getMockRepository(); |
101
|
|
|
$repository->expects($this->exactly(2)) |
|
|
|
|
102
|
|
|
->method('createQueryBuilder') |
103
|
|
|
->withConsecutive(['o'], ['s']) |
104
|
|
|
->willReturnOnConsecutiveCalls($opportunityQB, $statusesQB); |
105
|
|
|
|
106
|
|
|
$this->registry->expects($this->exactly(2)) |
|
|
|
|
107
|
|
|
->method('getRepository') |
108
|
|
|
->withConsecutive( |
109
|
|
|
['OroCRMSalesBundle:Opportunity'], |
110
|
|
|
[ExtendHelper::buildEnumValueClassName('opportunity_status')] |
111
|
|
|
) |
112
|
|
|
->willReturn($repository); |
113
|
|
|
|
114
|
|
|
$mockResult = $this->getMockQueryBuilder(); |
115
|
|
|
$mockResult->expects($this->once()) |
116
|
|
|
->method('getArrayResult') |
117
|
|
|
->willReturn([]); |
118
|
|
|
|
119
|
|
|
$self = $this; |
120
|
|
|
$this->aclHelper->expects($this->once()) |
|
|
|
|
121
|
|
|
->method('apply') |
122
|
|
|
->with( |
123
|
|
|
$this->callback(function ($query) use ($self, $expectation) { |
124
|
|
|
/** @var Query $query */ |
125
|
|
|
$self->assertEquals($expectation, $query->getDQL()); |
126
|
|
|
|
127
|
|
|
return true; |
128
|
|
|
}) |
129
|
|
|
) |
130
|
|
|
->willReturn($mockResult); |
131
|
|
|
|
132
|
|
|
$this->provider->getOpportunitiesGroupedByStatus($widgetOptions); |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
public function getOpportunitiesGroupedByStatusDQLDataProvider() |
136
|
|
|
{ |
137
|
|
|
return [ |
138
|
|
|
'request quantities' => [ |
139
|
|
|
'widgetOptions' => new WidgetOptionBag([ |
140
|
|
|
'excluded_statuses' => [], |
141
|
|
|
'useQuantityAsData' => true |
142
|
|
|
]), |
143
|
|
|
'expected DQL' => |
144
|
|
|
'SELECT IDENTITY (o.status) status, COUNT(o.id) as quantity ' |
145
|
|
|
. 'FROM OroCRM\Bundle\SalesBundle\Entity\Opportunity o ' |
146
|
|
|
. 'GROUP BY status ' |
147
|
|
|
. 'ORDER BY quantity DESC' |
148
|
|
|
], |
149
|
|
|
'request quantities with excluded statuses - should not affect DQL' => [ |
150
|
|
|
'widgetOptions' => new WidgetOptionBag([ |
151
|
|
|
'excluded_statuses' => ['in_progress', 'won'], |
152
|
|
|
'useQuantityAsData' => true |
153
|
|
|
]), |
154
|
|
|
'expected DQL' => |
155
|
|
|
'SELECT IDENTITY (o.status) status, COUNT(o.id) as quantity ' |
156
|
|
|
. 'FROM OroCRM\Bundle\SalesBundle\Entity\Opportunity o ' |
157
|
|
|
. 'GROUP BY status ' |
158
|
|
|
. 'ORDER BY quantity DESC' |
159
|
|
|
], |
160
|
|
|
'request budget amounts' => [ |
161
|
|
|
'widgetOptions' => new WidgetOptionBag([ |
162
|
|
|
'excluded_statuses' => [], |
163
|
|
|
'useQuantityAsData' => false |
164
|
|
|
]), |
165
|
|
|
'expected DQL' => <<<DQL |
166
|
|
|
SELECT IDENTITY (o.status) status, SUM( |
167
|
|
|
CASE WHEN o.status = 'won' |
168
|
|
|
THEN (CASE WHEN o.closeRevenue IS NOT NULL THEN o.closeRevenue ELSE 0 END) |
169
|
|
|
ELSE (CASE WHEN o.budgetAmount IS NOT NULL THEN o.budgetAmount ELSE 0 END) |
170
|
|
|
END |
171
|
|
|
) as budget FROM OroCRM\Bundle\SalesBundle\Entity\Opportunity o GROUP BY status ORDER BY budget DESC |
172
|
|
|
DQL |
173
|
|
|
], |
174
|
|
|
'request budget amounts with excluded statuses - should not affect DQL' => [ |
175
|
|
|
'widgetOptions' => new WidgetOptionBag([ |
176
|
|
|
'excluded_statuses' => ['in_progress', 'won'], |
177
|
|
|
'useQuantityAsData' => false |
178
|
|
|
]), |
179
|
|
|
'expected DQL' => <<<DQL |
180
|
|
|
SELECT IDENTITY (o.status) status, SUM( |
181
|
|
|
CASE WHEN o.status = 'won' |
182
|
|
|
THEN (CASE WHEN o.closeRevenue IS NOT NULL THEN o.closeRevenue ELSE 0 END) |
183
|
|
|
ELSE (CASE WHEN o.budgetAmount IS NOT NULL THEN o.budgetAmount ELSE 0 END) |
184
|
|
|
END |
185
|
|
|
) as budget FROM OroCRM\Bundle\SalesBundle\Entity\Opportunity o GROUP BY status ORDER BY budget DESC |
186
|
|
|
DQL |
187
|
|
|
] |
188
|
|
|
]; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* @param WidgetOptionBag $widgetOptions |
193
|
|
|
* @param array $result |
194
|
|
|
* @param string $expected |
195
|
|
|
* |
196
|
|
|
* @dataProvider getOpportunitiesGroupedByStatusResultDataProvider |
197
|
|
|
*/ |
198
|
|
|
public function testGetOpportunitiesGroupedByStatusResultFormatter($widgetOptions, $result, $expected) |
199
|
|
|
{ |
200
|
|
|
$this->ownerHelper->expects($this->once()) |
|
|
|
|
201
|
|
|
->method('getOwnerIds') |
202
|
|
|
->willReturn([]); |
203
|
|
|
|
204
|
|
|
$opportunityQB = new QueryBuilder($this->getMock('Doctrine\ORM\EntityManagerInterface')); |
205
|
|
|
$opportunityQB |
206
|
|
|
->from('OroCRM\Bundle\SalesBundle\Entity\Opportunity', 'o', null); |
207
|
|
|
|
208
|
|
|
$statusesQB = $this->getMockQueryBuilder(); |
209
|
|
|
$statusesQB->expects($this->once()) |
|
|
|
|
210
|
|
|
->method('select') |
211
|
|
|
->with('s.id, s.name') |
212
|
|
|
->willReturnSelf(); |
213
|
|
|
$statusesQB->expects($this->once()) |
214
|
|
|
->method('getQuery') |
215
|
|
|
->willReturnSelf(); |
216
|
|
|
$statusesQB->expects($this->once()) |
217
|
|
|
->method('getArrayResult') |
218
|
|
|
->willReturn($this->opportunityStatuses); |
219
|
|
|
|
220
|
|
|
$repository = $this->getMockRepository(); |
221
|
|
|
$repository->expects($this->exactly(2)) |
|
|
|
|
222
|
|
|
->method('createQueryBuilder') |
223
|
|
|
->withConsecutive(['o'], ['s']) |
224
|
|
|
->willReturnOnConsecutiveCalls($opportunityQB, $statusesQB); |
225
|
|
|
|
226
|
|
|
$this->registry->expects($this->exactly(2)) |
|
|
|
|
227
|
|
|
->method('getRepository') |
228
|
|
|
->withConsecutive( |
229
|
|
|
['OroCRMSalesBundle:Opportunity'], |
230
|
|
|
[ExtendHelper::buildEnumValueClassName('opportunity_status')] |
231
|
|
|
) |
232
|
|
|
->willReturn($repository); |
233
|
|
|
|
234
|
|
|
$mockResult = $this->getMockQueryBuilder(); |
235
|
|
|
$mockResult->expects($this->once()) |
236
|
|
|
->method('getArrayResult') |
237
|
|
|
->willReturn($result); |
238
|
|
|
|
239
|
|
|
$this->aclHelper->expects($this->once()) |
|
|
|
|
240
|
|
|
->method('apply') |
241
|
|
|
->willReturn($mockResult); |
242
|
|
|
|
243
|
|
|
$data = $this->provider->getOpportunitiesGroupedByStatus($widgetOptions); |
244
|
|
|
|
245
|
|
|
$this->assertEquals($expected, $data); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
public function getOpportunitiesGroupedByStatusResultDataProvider() |
249
|
|
|
{ |
250
|
|
|
return [ |
251
|
|
|
'result with all statuses, no exclusions - only labels should be added' => [ |
252
|
|
|
'widgetOptions' => new WidgetOptionBag([ |
253
|
|
|
'excluded_statuses' => [], |
254
|
|
|
'useQuantityAsData' => true |
255
|
|
|
]), |
256
|
|
|
'result data' => [ |
257
|
|
|
0 => ['quantity' => 700, 'status' => 'won'], |
258
|
|
|
1 => ['quantity' => 600, 'status' => 'identification_alignment'], |
259
|
|
|
2 => ['quantity' => 500, 'status' => 'in_progress'], |
260
|
|
|
3 => ['quantity' => 400, 'status' => 'needs_analysis'], |
261
|
|
|
4 => ['quantity' => 300, 'status' => 'negotiation'], |
262
|
|
|
5 => ['quantity' => 200, 'status' => 'solution_development'], |
263
|
|
|
6 => ['quantity' => 100, 'status' => 'lost'], |
264
|
|
|
], |
265
|
|
|
'expected formatted result' => [ |
266
|
|
|
0 => ['quantity' => 700, 'status' => 'won', 'label' => 'Won'], |
267
|
|
|
1 => ['quantity' => 600, 'status' => 'identification_alignment', 'label' => 'Identification'], |
268
|
|
|
2 => ['quantity' => 500, 'status' => 'in_progress', 'label' => 'Open'], |
269
|
|
|
3 => ['quantity' => 400, 'status' => 'needs_analysis', 'label' => 'Analysis'], |
270
|
|
|
4 => ['quantity' => 300, 'status' => 'negotiation', 'label' => 'Negotiation'], |
271
|
|
|
5 => ['quantity' => 200, 'status' => 'solution_development', 'label' => 'Development'], |
272
|
|
|
6 => ['quantity' => 100, 'status' => 'lost', 'label' => 'Lost'], |
273
|
|
|
] |
274
|
|
|
], |
275
|
|
|
'result with all statuses, with exclusions - excluded should be removed, labels' => [ |
276
|
|
|
'widgetOptions' => new WidgetOptionBag([ |
277
|
|
|
'excluded_statuses' => ['identification_alignment', 'solution_development'], |
278
|
|
|
'useQuantityAsData' => true |
279
|
|
|
]), |
280
|
|
|
'result data' => [ |
281
|
|
|
0 => ['quantity' => 700, 'status' => 'won'], |
282
|
|
|
1 => ['quantity' => 600, 'status' => 'identification_alignment'], |
283
|
|
|
2 => ['quantity' => 500, 'status' => 'in_progress'], |
284
|
|
|
3 => ['quantity' => 400, 'status' => 'needs_analysis'], |
285
|
|
|
4 => ['quantity' => 300, 'status' => 'negotiation'], |
286
|
|
|
5 => ['quantity' => 200, 'status' => 'solution_development'], |
287
|
|
|
6 => ['quantity' => 100, 'status' => 'lost'], |
288
|
|
|
], |
289
|
|
|
'expected formatted result' => [ |
290
|
|
|
0 => ['quantity' => 700, 'status' => 'won', 'label' => 'Won'], |
291
|
|
|
2 => ['quantity' => 500, 'status' => 'in_progress', 'label' => 'Open'], |
292
|
|
|
3 => ['quantity' => 400, 'status' => 'needs_analysis', 'label' => 'Analysis'], |
293
|
|
|
4 => ['quantity' => 300, 'status' => 'negotiation', 'label' => 'Negotiation'], |
294
|
|
|
6 => ['quantity' => 100, 'status' => 'lost', 'label' => 'Lost'], |
295
|
|
|
] |
296
|
|
|
], |
297
|
|
|
'result with NOT all statuses, no exclusions - all statuses, labels' => [ |
298
|
|
|
'widgetOptions' => new WidgetOptionBag([ |
299
|
|
|
'excluded_statuses' => [], |
300
|
|
|
'useQuantityAsData' => true |
301
|
|
|
]), |
302
|
|
|
'result data' => [ |
303
|
|
|
0 => ['quantity' => 700, 'status' => 'won'], |
304
|
|
|
1 => ['quantity' => 300, 'status' => 'negotiation'], |
305
|
|
|
], |
306
|
|
|
'expected formatted result' => [ |
307
|
|
|
0 => ['quantity' => 700, 'status' => 'won', 'label' => 'Won'], |
308
|
|
|
1 => ['quantity' => 300, 'status' => 'negotiation', 'label' => 'Negotiation'], |
309
|
|
|
2 => ['quantity' => 0, 'status' => 'identification_alignment', 'label' => 'Identification'], |
310
|
|
|
3 => ['quantity' => 0, 'status' => 'in_progress', 'label' => 'Open'], |
311
|
|
|
4 => ['quantity' => 0, 'status' => 'needs_analysis', 'label' => 'Analysis'], |
312
|
|
|
5 => ['quantity' => 0, 'status' => 'solution_development', 'label' => 'Development'], |
313
|
|
|
6 => ['quantity' => 0, 'status' => 'lost', 'label' => 'Lost'], |
314
|
|
|
] |
315
|
|
|
], |
316
|
|
|
'result with NOT all statuses AND exclusions - all statuses(except excluded), labels' => [ |
317
|
|
|
'widgetOptions' => new WidgetOptionBag([ |
318
|
|
|
'excluded_statuses' => ['identification_alignment', 'lost', 'in_progress'], |
319
|
|
|
'useQuantityAsData' => true |
320
|
|
|
]), |
321
|
|
|
'result data' => [ |
322
|
|
|
0 => ['quantity' => 700, 'status' => 'won'], |
323
|
|
|
1 => ['quantity' => 500, 'status' => 'in_progress'], |
324
|
|
|
2 => ['quantity' => 300, 'status' => 'negotiation'], |
325
|
|
|
3 => ['quantity' => 100, 'status' => 'lost'], |
326
|
|
|
], |
327
|
|
|
'expected formatted result' => [ |
328
|
|
|
0 => ['quantity' => 700, 'status' => 'won', 'label' => 'Won'], |
329
|
|
|
2 => ['quantity' => 300, 'status' => 'negotiation', 'label' => 'Negotiation'], |
330
|
|
|
4 => ['quantity' => 0, 'status' => 'needs_analysis', 'label' => 'Analysis'], |
331
|
|
|
5 => ['quantity' => 0, 'status' => 'solution_development', 'label' => 'Development'], |
332
|
|
|
] |
333
|
|
|
], |
334
|
|
|
]; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* @return EntityRepository|\PHPUnit_Framework_MockObject_MockObject |
339
|
|
|
*/ |
340
|
|
|
protected function getMockRepository() |
341
|
|
|
{ |
342
|
|
|
return $this->getMockBuilder('Doctrine\ORM\EntityRepository') |
343
|
|
|
->disableOriginalConstructor() |
344
|
|
|
->setMethods(['createQueryBuilder']) |
345
|
|
|
->getMockForAbstractClass(); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* @return QueryBuilder|\PHPUnit_Framework_MockObject_MockObject |
350
|
|
|
*/ |
351
|
|
|
protected function getMockQueryBuilder() |
352
|
|
|
{ |
353
|
|
|
return $this->getMockBuilder('Doctrine\ORM\QueryBuilder') |
354
|
|
|
->disableOriginalConstructor() |
355
|
|
|
->setMethods(['select', 'where', 'setParameter', 'getQuery', 'getArrayResult']) |
356
|
|
|
->getMockForAbstractClass(); |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
|
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.