Completed
Push — master ( e17e1f...f6fd3a )
by Joachim
01:57
created

ConsignmentService::configureOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 0
cts 17
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 13
nc 1
nop 1
crap 2
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\OptimisticLockException;
10
use Doctrine\ORM\ORMException;
11
use Doctrine\ORM\QueryBuilder;
12
use Loevgaard\DandomainConsignment\Entity\Generated\ReportInterface;
13
use Loevgaard\DandomainConsignment\Entity\Report;
14
use Loevgaard\DandomainConsignment\Repository\ReportRepository;
15
use Loevgaard\DandomainConsignmentBundle\Exception\InvalidBarCodeException;
16
use Loevgaard\DandomainConsignmentBundle\Exception\InvalidVendorNumberException;
17
use Loevgaard\DandomainFoundation\Entity\Generated\ManufacturerInterface;
18
use Loevgaard\DandomainStock\Entity\Generated\StockMovementInterface;
19
use Loevgaard\DandomainStock\Entity\StockMovement;
20
use Psr\Log\LoggerInterface;
21
use Psr\Log\NullLogger;
22
use Symfony\Component\OptionsResolver\OptionsResolver;
23
24
abstract class ConsignmentService implements ConsignmentServiceInterface
25
{
26
    /**
27
     * @var EntityManager
28
     */
29
    protected $entityManager;
30
31
    /**
32
     * @var ReportRepository
33
     */
34
    protected $reportRepository;
35
36
    /**
37
     * The directory where report files will be saved.
38
     *
39
     * @var string
40
     */
41
    protected $reportDir;
42
43
    /**
44
     * @var LoggerInterface
45
     */
46
    protected $logger;
47
48
    /**
49
     * @var ManufacturerInterface
50
     */
51
    protected $manufacturer;
52
53
    /**
54
     * Contains the included product ids.
55
     *
56
     * @var array
57
     */
58
    protected $includedProductIds;
59
60
    /**
61
     * Contains the excluded product ids.
62
     *
63
     * @var array
64
     */
65
    protected $excludedProductIds;
66
67
    public function __construct(ManagerRegistry $managerRegistry, ReportRepository $reportRepository, string $reportDir)
68
    {
69
        $this->entityManager = $managerRegistry->getManager();
70
        $this->reportRepository = $reportRepository;
71
        $this->reportDir = rtrim($reportDir, '/');
72
        $this->logger = new NullLogger();
73
74
        if (!is_dir($this->reportDir)) {
75
            throw new \InvalidArgumentException('The report dir given is not a directory');
76
        }
77
78
        if (!is_writable($this->reportDir)) {
79
            throw new \InvalidArgumentException('The report dir given is not writable');
80
        }
81
    }
82
83
    /**
84
     * @param array $options
85
     *
86
     * @return ReportInterface
87
     *
88
     * @throws ORMException
89
     */
90
    public function generateReport(array $options = []): ReportInterface
91
    {
92
        // resolve options
93
        $resolver = new OptionsResolver();
94
        $this->configureOptions($resolver);
95
        $options = $resolver->resolve($options);
96
97
        $report = new Report();
98
        $report->setManufacturer($this->manufacturer);
99
100
        $this->reportRepository->persist($report);
101
102
        try {
103
            if ($options['valid_bar_codes']) {
104
                $this->validateBarCodes();
105
            }
106
107
            if ($options['valid_vendor_numbers']) {
108
                $this->validateVendorNumbers();
109
            }
110
111
            $qb = $this->queryBuilder($options);
112
113
            /** @var StockMovementInterface[] $stockMovements */
114
            $stockMovements = $qb->getQuery()->getResult();
115
116
            if (!count($stockMovements)) {
117
                throw new \Exception('No stock movements applicable for this report');
118
            }
119
120
            $lastStockMovement = null;
121
            foreach ($stockMovements as $stockMovement) {
122
                $report->addStockMovement($stockMovement);
123
124
                $lastStockMovement = $stockMovement;
125
            }
126
127
            if ($lastStockMovement && $options['update_last_stock_movement']) {
128
                $this->manufacturer->setConsignmentLastStockMovement($lastStockMovement);
129
            }
130
131
            $report->markAsSuccess();
132
        } catch (\Exception $e) {
133
            $report->markAsError($e->getMessage());
134
        }
135
136
        $this->reportRepository->flush();
137
138
        return $report;
139
    }
140
141
    /**
142
     * @param ReportInterface $report
143
     * @param array           $options
144
     *
145
     * @return \SplFileObject
146
     *
147
     * @throws ORMException
148
     * @throws OptimisticLockException
149
     */
150
    public function generateReportFile(ReportInterface $report, array $options = []): \SplFileObject
151
    {
152
        $file = $this->getFile();
153
154
        foreach ($report->getStockMovements() as $stockMovement) {
155
            $file->fputcsv([
156
                $stockMovement->getQuantity(),
157
                $stockMovement->getProduct()->getBarCodeNumber(),
158
            ]);
159
        }
160
161
        $report->setFile($file);
162
163
        $this->reportRepository->flush();
164
165
        return $file;
166
    }
167
168
    /**
169
     * @param ReportInterface $report
170
     * @param array           $options
171
     *
172
     * @return bool
173
     *
174
     * @throws ORMException
175
     * @throws OptimisticLockException
176
     */
177
    public function deliverReport(ReportInterface $report, array $options = []): bool
178
    {
179
        if (!$report->isDeliverable()) {
180
            return false;
181
        }
182
183
        $file = $report->getFile();
184
        if (!$file || !$file->isFile()) {
185
            $file = $this->generateReportFile($report);
186
        }
187
188
        $this->logger->info('File has been delivered to: '.$file->getPathname());
189
190
        /*
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...
191
        Example of mailing the report
192
        -------
193
        $recipients = ['[email protected]'];
194
195
        $attachment = \Swift_Attachment::fromPath($file->getPathname());
196
        $attachment->setFilename('report-'.$report->getId().'.csv');
197
198
        $message = \Swift_Message::newInstance()
199
            ->attach($attachment)
200
            ->setSubject('Consignment report (id: '.$report->getId().')')
201
            ->setFrom('[email protected]', 'Your Business')
202
            ->setTo($recipients)
203
            ->setBody('See the attached file.', 'text/plain')
204
        ;
205
206
        $this->mailer->send($message);
207
        */
208
209
        return true;
210
    }
211
212
    public function queryBuilder(array $options = [], string $alias = 's'): QueryBuilder
213
    {
214
        // resolve options
215
        $resolver = new OptionsResolver();
216
        $this->configureOptions($resolver);
217
        $options = $resolver->resolve($options);
218
219
        $includedProductIds = $this->getIncludedProductIds();
220
221
        if (!count($includedProductIds)) {
222
            throw new \RuntimeException('No included product ids. Something is wrong');
223
        }
224
225
        $qb = $this->entityManager->createQueryBuilder();
226
        $qb->select('s, p')
227
            ->from('Loevgaard\DandomainStock\Entity\StockMovement', 's')
228
            ->join('s.product', 'p')
229
            ->andWhere($qb->expr()->in('p.id', ':includedProductIds'))
230
            ->andWhere($qb->expr()->in('s.type', ':stockMovementTypes'))
231
            ->addOrderBy('s.id', 'asc')
232
            ->setParameters([
233
                'includedProductIds' => $includedProductIds,
234
                'stockMovementTypes' => $options['stock_movement_types'],
235
            ]);
236
237
        if (!$options['include_complaints']) {
238
            $qb->andWhere('s.complaint = 0');
239
        }
240
241
        if ($options['use_last_stock_movement'] && $this->manufacturer->getConsignmentLastStockMovement()) {
242
            $qb->andWhere($qb->expr()->gt('s.id', ':lastStockMovementId'))
243
                ->setParameter('lastStockMovementId', $this->manufacturer->getConsignmentLastStockMovement()->getId());
244
        }
245
246
        if ($options['start_date']) {
247
            $qb->andWhere($qb->expr()->gte('s.createdAt', ':startDate'))
248
                ->setParameter('startDate', $options['start_date']);
249
        }
250
251
        if ($options['end_date']) {
252
            $qb->andWhere($qb->expr()->lte('s.createdAt', ':endDate'))
253
                ->setParameter('endDate', $options['end_date']);
254
        }
255
256
        return $qb;
257
    }
258
259
    public function getProductQueryBuilder(string $alias = 'p'): QueryBuilder
260
    {
261
        $excludedProductIds = $this->getExcludedProductIds();
262
263
        $qb = $this->entityManager->createQueryBuilder();
264
        $qb->select($alias)
265
            ->from('Loevgaard\DandomainFoundation\Entity\Product', $alias)
266
            ->where($qb->expr()->isMemberOf(':manufacturer', $alias.'.manufacturers'))
267
            ->setParameter('manufacturer', $this->manufacturer);
268
269
        if (!empty($excludedProductIds)) {
270
            $qb->andWhere($qb->expr()->notIn($alias.'.id', ':excluded'))
271
                ->setParameter('excluded', $excludedProductIds);
272
        }
273
274
        return $qb;
275
    }
276
277
    /**
278
     * @param LoggerInterface $logger
279
     *
280
     * @return ConsignmentServiceInterface
281
     */
282
    public function setLogger(LoggerInterface $logger): ConsignmentServiceInterface
283
    {
284
        $this->logger = $logger;
285
286
        return $this;
287
    }
288
289
    /**
290
     * @param ManufacturerInterface $manufacturer
291
     *
292
     * @return ConsignmentServiceInterface
293
     */
294
    public function setManufacturer(ManufacturerInterface $manufacturer): ConsignmentServiceInterface
295
    {
296
        $this->manufacturer = $manufacturer;
297
298
        return $this;
299
    }
300
301
    /**
302
     * This method should return an array of included product ids
303
     * It excludes the excluded product ids, by using the getProductQueryBuilder method.
304
     *
305
     * @return array
306
     */
307
    protected function getIncludedProductIds(): array
308
    {
309
        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...
310
            $qb = $this->getProductQueryBuilder();
311
            $qb->select('p.id');
312
313
            $res = $qb->getQuery()->getArrayResult();
314
315
            $this->includedProductIds = array_map(function ($elm) {
316
                return array_values($elm)[0];
317
            }, $res);
318
        }
319
320
        return $this->includedProductIds;
321
    }
322
323
    /**
324
     * This method should return an array of excluded product ids.
325
     *
326
     * @return array
327
     */
328
    protected function getExcludedProductIds(): array
329
    {
330
        return [];
331
    }
332
333
    /**
334
     * This is a helper method which takes an array of product numbers and returns their respective product ids.
335
     *
336
     * @param array $numbers
337
     *
338
     * @return array
339
     */
340
    protected function getProductIdsFromProductNumbers(array $numbers): array
341
    {
342
        $qb = $this->entityManager->createQueryBuilder();
343
        $qb->select('p.id')
344
            ->from('Loevgaard\DandomainFoundation\Entity\Product', 'p')
345
            ->where($qb->expr()->in('p.number', ':numbers'))
346
            ->setParameter('numbers', $numbers);
347
348
        $res = $qb->getQuery()->getArrayResult();
349
350
        return array_map(function ($elm) {
351
            return array_values($elm)[0];
352
        }, $res);
353
    }
354
355
    /**
356
     * @throws InvalidBarCodeException
357
     */
358
    protected function validateBarCodes(): void
359
    {
360
        $qb = $this->queryBuilder();
361
        $qb->andWhere('p.validBarCode = 0');
362
363
        /** @var StockMovementInterface[] $stockMovements */
364
        $stockMovements = $qb->getQuery()->getResult();
365
366
        $c = count($stockMovements);
367
368 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...
369
            $this->logger->emergency('There are '.$c.' stock movements with invalid bar codes');
370
            $productNumbers = [];
371
            foreach ($stockMovements as $stockMovement) {
372
                $productNumbers[] = $stockMovement->getProduct()->getNumber();
373
            }
374
375
            throw new InvalidBarCodeException('Products with invalid bar codes: '.join(', ', $productNumbers), $productNumbers);
376
        }
377
    }
378
379
    /**
380
     * @throws InvalidVendorNumberException
381
     */
382
    protected function validateVendorNumbers(): void
383
    {
384
        $qb = $this->queryBuilder();
385
        $qb->andWhere($qb->expr()->orX(
386
            $qb->expr()->eq('p.vendorNumber', ':empty'),
387
            $qb->expr()->isNull('p.vendorNumber')
388
        ))->setParameter(':empty', '');
389
390
        /** @var StockMovementInterface[] $stockMovements */
391
        $stockMovements = $qb->getQuery()->getResult();
392
393
        $c = count($stockMovements);
394
395 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...
396
            $this->logger->critical('There are '.$c.' stock movements with invalid vendor numbers');
397
            $productNumbers = [];
398
            foreach ($stockMovements as $stockMovement) {
399
                $productNumbers[] = $stockMovement->getProduct()->getNumber();
400
            }
401
402
            throw new InvalidVendorNumberException('Products with invalid vendor numbers: '.join($productNumbers), $productNumbers);
403
        }
404
    }
405
406
    protected function getFile(string $extension = 'csv'): \SplFileObject
407
    {
408
        do {
409
            $filename = $this->reportDir.'/'.uniqid('consignment-', true).'.'.$extension;
410
        } while (file_exists($filename));
411
412
        return new \SplFileObject($filename, 'w+');
413
    }
414
415
    protected function configureOptions(OptionsResolver $resolver): void
416
    {
417
        $resolver->setDefaults([
418
            'valid_bar_codes' => false,
419
            'valid_vendor_numbers' => false,
420
            'update_last_stock_movement' => true,
421
            'stock_movement_types' => [
422
                StockMovement::TYPE_RETURN,
423
                StockMovement::TYPE_SALE,
424
                StockMovement::TYPE_REGULATION,
425
            ],
426
            'include_complaints' => false,
427
            'use_last_stock_movement' => true,
428
            'start_date' => null,
429
            'end_date' => null,
430
        ]);
431
    }
432
}
433