Completed
Push — master ( 70e4e5...5530b2 )
by Joachim
15:31
created

ConsignmentService::generateReport()   C

Complexity

Conditions 8
Paths 50

Size

Total Lines 50
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
dl 0
loc 50
ccs 0
cts 33
cp 0
rs 6.3636
c 0
b 0
f 0
cc 8
eloc 27
nc 50
nop 1
crap 72
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Loevgaard\DandomainConsignmentBundle\ConsignmentService;
6
7
use Doctrine\Common\Persistence\ManagerRegistry;
8
use Doctrine\ORM\EntityManager;
9
use Doctrine\ORM\QueryBuilder;
10
use Loevgaard\DandomainConsignment\Entity\Generated\ReportInterface;
11
use Loevgaard\DandomainConsignment\Entity\Report;
12
use Loevgaard\DandomainConsignment\Repository\ReportRepository;
13
use Loevgaard\DandomainConsignmentBundle\Exception\InvalidBarCodeException;
14
use Loevgaard\DandomainConsignmentBundle\Exception\InvalidVendorNumberException;
15
use Loevgaard\DandomainFoundation\Entity\Generated\ManufacturerInterface;
16
use Loevgaard\DandomainStock\Entity\Generated\StockMovementInterface;
17
use Loevgaard\DandomainStock\Entity\StockMovement;
18
use Psr\Log\LoggerInterface;
19
use Psr\Log\NullLogger;
20
use Symfony\Component\OptionsResolver\OptionsResolver;
21
22
abstract class ConsignmentService implements ConsignmentServiceInterface
23
{
24
    /**
25
     * @var EntityManager
26
     */
27
    protected $entityManager;
28
29
    /**
30
     * @var ReportRepository
31
     */
32
    protected $reportRepository;
33
34
    /**
35
     * The directory where report files will be saved
36
     *
37
     * @var string
38
     */
39
    protected $reportDir;
40
41
    /**
42
     * @var LoggerInterface
43
     */
44
    protected $logger;
45
46
    /**
47
     * @var ManufacturerInterface
48
     */
49
    protected $manufacturer;
50
51
    /**
52
     * Contains the included product ids
53
     *
54
     * @var array
55
     */
56
    protected $includedProductIds;
57
58
    public function __construct(ManagerRegistry $managerRegistry, ReportRepository $reportRepository, string $reportDir)
59
    {
60
        $this->entityManager = $managerRegistry->getManager();
61
        $this->reportRepository = $reportRepository;
62
        $this->reportDir = rtrim($reportDir, '/');
63
        $this->logger = new NullLogger();
64
65
        if(!is_dir($this->reportDir)) {
66
            throw new \InvalidArgumentException('The report dir given is not a directory');
67
        }
68
69
        if(!is_writable($this->reportDir)) {
70
            throw new \InvalidArgumentException('The report dir given is not writable');
71
        }
72
    }
73
74
    /**
75
     * @param array $options
76
     * @return ReportInterface
77
     * @throws \Doctrine\ORM\ORMException
78
     */
79
    public function generateReport(array $options = []): ReportInterface
80
    {
81
        // resolve options
82
        $resolver = new OptionsResolver();
83
        $this->configureGenerateReportOptions($resolver);
84
        $options = $resolver->resolve($options);
85
86
        $report = new Report();
87
        $report->setManufacturer($this->manufacturer);
88
89
        $this->reportRepository->persist($report);
90
91
        try {
92
            if($options['valid_bar_codes']) {
93
                $this->validateBarCodes();
94
            }
95
96
            if($options['valid_vendor_numbers']) {
97
                $this->validateVendorNumbers();
98
            }
99
100
            $qb = $this->queryBuilder();
101
102
            /** @var StockMovementInterface[] $stockMovements */
103
            $stockMovements = $qb->getQuery()->getResult();
104
105
            if(!count($stockMovements)) {
106
                throw new \Exception('No stock movements applicable for this report');
107
            }
108
109
            $lastStockMovement = null;
110
            foreach ($stockMovements as $stockMovement) {
111
                $report->addStockMovement($stockMovement);
112
113
                $lastStockMovement = $stockMovement;
114
            }
115
116
            if ($lastStockMovement && $options['update_last_stock_movement']) {
117
                $this->manufacturer->setConsignmentLastStockMovement($lastStockMovement);
118
            }
119
120
            $report->markAsSuccess();
121
        } catch (\Exception $e) {
122
            $report->markAsError($e->getMessage());
123
        }
124
125
        $this->reportRepository->flush();
126
127
        return $report;
128
    }
129
130
    public function generateReportFile(ReportInterface $report, array $options = []): \SplFileObject
131
    {
132
        $file = $this->getFile();
133
134
        foreach ($report->getStockMovements() as $stockMovement) {
135
            $file->fputcsv([
136
                $stockMovement->getQuantity(),
137
                $stockMovement->getProduct()->getBarCodeNumber(),
138
            ]);
139
        }
140
141
        $report->setFile($file);
142
143
        return $file;
144
    }
145
146
    public function deliverReport(ReportInterface $report, array $options = []) : bool
147
    {
148
        if(!$report->isDeliverable()) {
149
            return false;
150
        }
151
152
        $file = $report->getFile();
153
        if(!$file || $file->isFile()) {
154
            $file = $this->generateReportFile($report);
155
        }
156
157
        $this->logger->info('File has been delivered to: '.$file->getPathname());
158
159
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
160
        Example of mailing the report
161
        -------
162
        $recipients = ['[email protected]'];
163
164
        $attachment = \Swift_Attachment::fromPath($reportFile->getPathname());
165
        $attachment->setFilename('report-'.$report->getId().'.csv');
166
167
        $message = \Swift_Message::newInstance()
168
            ->attach($attachment)
169
            ->setSubject('Consignment report (id: '.$report->getId().')')
170
            ->setFrom('[email protected]', 'Your Business')
171
            ->setTo($recipients)
172
            ->setBody('See the attached file.', 'text/plain')
173
        ;
174
175
        $this->mailer->send($message);
176
        */
177
178
        return true;
179
    }
180
181
    public function queryBuilder(array $options = [], string $alias = 's'): QueryBuilder
182
    {
183
        // resolve options
184
        $resolver = new OptionsResolver();
185
        $this->configureQueryBuilderOptions($resolver);
186
        $options = $resolver->resolve($options);
187
188
        $includedProductIds = $this->getIncludedProductIds();
189
190
        if (!count($includedProductIds)) {
191
            throw new \RuntimeException('No included product ids. Something is wrong');
192
        }
193
194
        $qb = $this->entityManager->createQueryBuilder();
195
        $qb->select('s, p')
196
            ->from('AppBundle:StockMovement', 's')
197
            ->join('s.product', 'p')
198
            ->andWhere($qb->expr()->in('p.id', ':includedProductIds'))
199
            ->andWhere($qb->expr()->in('s.type', ':stockMovementTypes'))
200
            ->addOrderBy('s.id', 'asc')
201
            ->setParameters([
202
                'includedProductIds' => $includedProductIds,
203
                'stockMovementTypes' => $options['stock_movement_types']
204
            ]);
205
206
        if(!$options['include_complaints']) {
207
            $qb->andWhere('s.complaint = 0');
208
        }
209
210
        if ($options['use_last_stock_movement'] && $this->manufacturer->getConsignmentLastStockMovement()) {
211
            $qb->andWhere($qb->expr()->gt('s.id', ':lastStockMovementId'))
212
                ->setParameter('lastStockMovementId', $this->manufacturer->getConsignmentLastStockMovement()->getId());
213
        }
214
215
        if($options['start_date']) {
216
            $qb->andWhere($qb->expr()->gte('s.createdAt', ':startDate'))
217
                ->setParameter('startDate', $options['start_date']);
218
        }
219
220
        if($options['end_date']) {
221
            $qb->andWhere($qb->expr()->lte('s.createdAt', ':endDate'))
222
                ->setParameter('endDate', $options['end_date']);
223
        }
224
225
        return $qb;
226
    }
227
228
    public function getProductQueryBuilder(string $alias = 'p'): QueryBuilder
229
    {
230
        $excludedProductIds = $this->getExcludedProductIds();
231
232
        $qb = $this->entityManager->createQueryBuilder();
233
        $qb->select($alias)
234
            ->from('Loevgaard\DandomainFoundation\Entity\Product', $alias)
235
            ->where($qb->expr()->isMemberOf(':manufacturer', $alias.'.manufacturers'))
236
            ->setParameter('manufacturer', $this->manufacturer);
237
238
        if(!empty($excludedProductIds)) {
239
            $qb->andWhere($qb->expr()->notIn($alias.'.id', ':excluded'))
240
                ->setParameter('excluded', $excludedProductIds);
241
        }
242
243
        return $qb;
244
    }
245
246
    /**
247
     * This method should return an array of included product ids
248
     * It excludes the excluded product ids, by using the getProductQueryBuilder method
249
     *
250
     * @return array
251
     */
252
    protected function getIncludedProductIds() : array
253
    {
254
        if(!$this->includedProductIds) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->includedProductIds 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...
255
            $qb = $this->getProductQueryBuilder();
256
            $qb->select('p.id');
257
258
            $res = $qb->getQuery()->getArrayResult();
259
260
            $this->includedProductIds = array_map(function ($elm) {
261
                return array_values($elm)[0];
262
            }, $res);
263
        }
264
265
        return $this->includedProductIds;
266
    }
267
268
    /**
269
     * This method should return an array of excluded product ids
270
     *
271
     * @return array
272
     */
273
    protected function getExcludedProductIds() : array
274
    {
275
        return [];
276
    }
277
278
    /**
279
     * This is a helper method which takes an array of product numbers and returns their respective product ids
280
     *
281
     * @param array $numbers
282
     * @return array
283
     */
284
    protected function getProductIdsFromProductNumbers(array $numbers) : array
285
    {
286
        $qb = $this->entityManager->createQueryBuilder();
287
        $qb->select('p.id')
288
            ->from('Loevgaard\DandomainFoundation\Entity\Product', 'p')
289
            ->where($qb->expr()->in('p.number', ':numbers'))
290
            ->setParameter('numbers', $numbers);
291
292
        $res = $qb->getQuery()->getArrayResult();
293
294
        return array_map(function ($elm) {
295
            return array_values($elm)[0];
296
        }, $res);
297
    }
298
299
    /**
300
     * @throws InvalidBarCodeException
301
     */
302
    protected function validateBarCodes() : void
303
    {
304
        $qb = $this->queryBuilder();
305
        $qb->andWhere('p.validBarCode = 0');
306
307
        /** @var StockMovementInterface[] $stockMovements */
308
        $stockMovements = $qb->getQuery()->getResult();
309
310
        $c = count($stockMovements);
311
312 View Code Duplication
        if($c) {
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...
313
            $this->logger->emergency('There are '.$c.' stock movements with invalid bar codes');
314
            $productNumbers = [];
315
            foreach ($stockMovements as $stockMovement) {
316
                $productNumbers[] = $stockMovement->getProduct()->getNumber();
317
            }
318
319
            throw new InvalidBarCodeException('Products with invalid bar codes: '.join(', ', $productNumbers), $productNumbers);
320
        }
321
    }
322
323
    /**
324
     * @throws InvalidVendorNumberException
325
     */
326
    protected function validateVendorNumbers() : void
327
    {
328
        $qb = $this->queryBuilder();
329
        $qb->andWhere($qb->expr()->orX(
330
            $qb->expr()->eq('p.vendorNumber', ':empty'),
331
            $qb->expr()->isNull('p.vendorNumber')
332
        ))->setParameter(':empty', '');
333
334
        /** @var StockMovementInterface[] $stockMovements */
335
        $stockMovements = $qb->getQuery()->getResult();
336
337
        $c = count($stockMovements);
338
339 View Code Duplication
        if($c) {
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...
340
            $this->logger->critical('There are '.$c.' stock movements with invalid vendor numbers');
341
            $productNumbers = [];
342
            foreach ($stockMovements as $stockMovement) {
343
                $productNumbers[] = $stockMovement->getProduct()->getNumber();
344
            }
345
346
            throw new InvalidVendorNumberException('Products with invalid vendor numbers: '.join($productNumbers), $productNumbers);
347
        }
348
    }
349
350
    protected function getFile(string $extension = 'csv') : \SplFileObject
351
    {
352
        do {
353
            $filename = $this->reportDir.'/'.uniqid('consignment-', true).'.'.$extension;
354
        } while(file_exists($filename));
355
356
        return new \SplFileObject($filename, 'w+');
357
    }
358
359
    protected function configureGenerateReportOptions(OptionsResolver $resolver) : void
360
    {
361
        $resolver->setDefined($this->queryBuilderOptions());
362
        $resolver->setDefaults([
363
            'valid_bar_codes' => false,
364
            'valid_vendor_numbers' => false,
365
            'update_last_stock_movement' => true,
366
        ]);
367
    }
368
369
    protected function configureQueryBuilderOptions(OptionsResolver $resolver) : void
370
    {
371
        $resolver->setDefined($this->queryBuilderOptions());
372
        $resolver->setDefaults([
373
            'stock_movement_types' => [
374
                StockMovement::TYPE_RETURN,
375
                StockMovement::TYPE_SALE,
376
                StockMovement::TYPE_REGULATION
377
            ],
378
            'include_complaints' => false,
379
            'use_last_stock_movement' => true,
380
            'start_date' => null,
381
            'end_date' => null,
382
        ]);
383
    }
384
385
    protected function queryBuilderOptions() : array
386
    {
387
        return [
388
            'stock_movement_types',
389
            'include_complaints',
390
            'use_last_stock_movement',
391
            'start_date',
392
            'end_date',
393
        ];
394
    }
395
}
396