Completed
Push — master ( 5530b2...c323dc )
by Joachim
07:15
created

ConsignmentService::setManufacturer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
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\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
     * @param LoggerInterface $logger
248
     * @return ConsignmentServiceInterface
249
     */
250
    public function setLogger(LoggerInterface $logger) : ConsignmentServiceInterface
251
    {
252
        $this->logger = $logger;
253
        return $this;
254
    }
255
256
    /**
257
     * @param ManufacturerInterface $manufacturer
258
     * @return ConsignmentServiceInterface
259
     */
260
    public function setManufacturer(ManufacturerInterface $manufacturer) : ConsignmentServiceInterface
261
    {
262
        $this->manufacturer = $manufacturer;
263
        return $this;
264
    }
265
266
    /**
267
     * This method should return an array of included product ids
268
     * It excludes the excluded product ids, by using the getProductQueryBuilder method
269
     *
270
     * @return array
271
     */
272
    protected function getIncludedProductIds() : array
273
    {
274
        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...
275
            $qb = $this->getProductQueryBuilder();
276
            $qb->select('p.id');
277
278
            $res = $qb->getQuery()->getArrayResult();
279
280
            $this->includedProductIds = array_map(function ($elm) {
281
                return array_values($elm)[0];
282
            }, $res);
283
        }
284
285
        return $this->includedProductIds;
286
    }
287
288
    /**
289
     * This method should return an array of excluded product ids
290
     *
291
     * @return array
292
     */
293
    protected function getExcludedProductIds() : array
294
    {
295
        return [];
296
    }
297
298
    /**
299
     * This is a helper method which takes an array of product numbers and returns their respective product ids
300
     *
301
     * @param array $numbers
302
     * @return array
303
     */
304
    protected function getProductIdsFromProductNumbers(array $numbers) : array
305
    {
306
        $qb = $this->entityManager->createQueryBuilder();
307
        $qb->select('p.id')
308
            ->from('Loevgaard\DandomainFoundation\Entity\Product', 'p')
309
            ->where($qb->expr()->in('p.number', ':numbers'))
310
            ->setParameter('numbers', $numbers);
311
312
        $res = $qb->getQuery()->getArrayResult();
313
314
        return array_map(function ($elm) {
315
            return array_values($elm)[0];
316
        }, $res);
317
    }
318
319
    /**
320
     * @throws InvalidBarCodeException
321
     */
322
    protected function validateBarCodes() : void
323
    {
324
        $qb = $this->queryBuilder();
325
        $qb->andWhere('p.validBarCode = 0');
326
327
        /** @var StockMovementInterface[] $stockMovements */
328
        $stockMovements = $qb->getQuery()->getResult();
329
330
        $c = count($stockMovements);
331
332 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...
333
            $this->logger->emergency('There are '.$c.' stock movements with invalid bar codes');
334
            $productNumbers = [];
335
            foreach ($stockMovements as $stockMovement) {
336
                $productNumbers[] = $stockMovement->getProduct()->getNumber();
337
            }
338
339
            throw new InvalidBarCodeException('Products with invalid bar codes: '.join(', ', $productNumbers), $productNumbers);
340
        }
341
    }
342
343
    /**
344
     * @throws InvalidVendorNumberException
345
     */
346
    protected function validateVendorNumbers() : void
347
    {
348
        $qb = $this->queryBuilder();
349
        $qb->andWhere($qb->expr()->orX(
350
            $qb->expr()->eq('p.vendorNumber', ':empty'),
351
            $qb->expr()->isNull('p.vendorNumber')
352
        ))->setParameter(':empty', '');
353
354
        /** @var StockMovementInterface[] $stockMovements */
355
        $stockMovements = $qb->getQuery()->getResult();
356
357
        $c = count($stockMovements);
358
359 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...
360
            $this->logger->critical('There are '.$c.' stock movements with invalid vendor numbers');
361
            $productNumbers = [];
362
            foreach ($stockMovements as $stockMovement) {
363
                $productNumbers[] = $stockMovement->getProduct()->getNumber();
364
            }
365
366
            throw new InvalidVendorNumberException('Products with invalid vendor numbers: '.join($productNumbers), $productNumbers);
367
        }
368
    }
369
370
    protected function getFile(string $extension = 'csv') : \SplFileObject
371
    {
372
        do {
373
            $filename = $this->reportDir.'/'.uniqid('consignment-', true).'.'.$extension;
374
        } while(file_exists($filename));
375
376
        return new \SplFileObject($filename, 'w+');
377
    }
378
379
    protected function configureGenerateReportOptions(OptionsResolver $resolver) : void
380
    {
381
        $resolver->setDefined($this->queryBuilderOptions());
382
        $resolver->setDefaults([
383
            'valid_bar_codes' => false,
384
            'valid_vendor_numbers' => false,
385
            'update_last_stock_movement' => true,
386
        ]);
387
    }
388
389
    protected function configureQueryBuilderOptions(OptionsResolver $resolver) : void
390
    {
391
        $resolver->setDefined($this->queryBuilderOptions());
392
        $resolver->setDefaults([
393
            'stock_movement_types' => [
394
                StockMovement::TYPE_RETURN,
395
                StockMovement::TYPE_SALE,
396
                StockMovement::TYPE_REGULATION
397
            ],
398
            'include_complaints' => false,
399
            'use_last_stock_movement' => true,
400
            'start_date' => null,
401
            'end_date' => null,
402
        ]);
403
    }
404
405
    protected function queryBuilderOptions() : array
406
    {
407
        return [
408
            'stock_movement_types',
409
            'include_complaints',
410
            'use_last_stock_movement',
411
            'start_date',
412
            'end_date',
413
        ];
414
    }
415
}
416