Passed
Push — 1.3 ( a964d7...0611b0 )
by Morven
04:25
created

LineItemFactory::setParent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 1
b 0
f 1
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
2
3
namespace SilverCommerce\OrdersAdmin\Factory;
4
5
use SilverStripe\ORM\DataObject;
6
use SilverStripe\ORM\ValidationException;
7
use SilverCommerce\TaxAdmin\Model\TaxRate;
8
use SilverStripe\Core\Config\Configurable;
9
use SilverStripe\Core\Injector\Injectable;
10
use SilverCommerce\OrdersAdmin\Model\Estimate;
11
use SilverCommerce\OrdersAdmin\Model\LineItem;
12
use SilverCommerce\OrdersAdmin\Model\LineItemCustomisation;
13
14
/**
15
 * Factory that handles setting up line items based on submitted data
16
 */
17
class LineItemFactory
18
{
19
    use Injectable, Configurable;
20
21
    const ITEM_CLASS = LineItem::class;
22
23
    const CUSTOM_CLASS = LineItemCustomisation::class;
24
25
    /**
26
     * Data that will be added to a customisation
27
     *
28
     * @var array
29
     */
30
    private static $custom_map = [
31
        "Title",
32
        "Value",
33
        "BasePrice"
34
    ];
35
36
    /**
37
     * Should the stock stock levels be globally checked on items added?
38
     * Using this setting will ignore individual product "Stocked" settings.
39
     *
40
     * @var string
41
     */
42
    private static $force_check_stock = false;
43
44
    /**
45
     * Current line item
46
     *
47
     * @var DataObject
48
     */
49
    protected $item;
50
51
    /**
52
     * Parent estimate/invoice
53
     *
54
     * @var Estimate
55
     */
56
    protected $parent;
57
58
    /**
59
     * DataObject that will act as the product
60
     *
61
     * @var \SilverStripe\ORM\DataObject
62
     */
63
    protected $product;
64
65
    /**
66
     * The number of product to add/update for this line item
67
     *
68
     * @var int
69
     */
70
    protected $quantity;
71
72
    /**
73
     * Should this item be locked (cannot be updated, only removed)?
74
     * (defaults to false)
75
     *
76
     * @var bool
77
     */
78
    protected $lock = false;
79
80
    /**
81
     * List of customisation data that will need to be setup
82
     *
83
     * @var array
84
     */
85
    protected $customisations = [];
86
87
    /**
88
     * The name of the param used on product to determin if stock level should
89
     * be checked.
90
     *
91
     * @var string
92
     */
93
    protected $product_stocked_param = "Stocked";
94
95
    /**
96
     * The name of the param used on product to track Stock Level.
97
     *
98
     * @var string
99
     */
100
    protected $product_stock_param = "StockLevel";
101
102
    /**
103
     * The name of the param used on product to determin if item is deliverable
104
     *
105
     * @var string
106
     */
107
    protected $product_deliverable_param = "Deliverable";
108
109
    /**
110
     * Either find an existing line item (based on the submitted data),
111
     * or return a new one.
112
     *
113
     * @return self
114
     */
115
    public function makeItem()
116
    {
117
        $custom = $this->getCustomisations();
118
        $class = self::ITEM_CLASS;
119
120
        // Setup initial line item
121
        $item = $class::create($this->getItemArray());
122
123
        // Find any item customisation associations
124
        $custom_association = null;
125
        $associations = array_merge(
126
            $item->hasMany(),
127
            $item->manyMany()
128
        );
129
130
        // Define association of item to customisations
131
        foreach ($associations as $key => $value) {
132
            $class = $value::create();
133
            if (is_a($class, self::CUSTOM_CLASS)) {
134
                $custom_association = $key;
135
                break;
136
            }
137
        }
138
139
        // Map any customisations to the current item
140
        if (isset($custom_association)) {
141
            foreach ($custom as $custom_data) {
142
                $customisation = $this->createCustomisation($custom_data);
143
                $customisation->write();
144
                $item->{$custom_association}()->add($customisation);
145
            }
146
        }
147
148
        // Setup Key
149
        $item->Key = $item->generateKey();
150
        $this->setItem($item);
151
        
152
        return $this;
153
    }
154
155
    /**
156
     * Update the current line item
157
     *
158
     * @return self
159
     */
160
    public function update()
161
    {
162
        $item = $this->getItem();
163
        $item->update($this->getItemArray());
164
        $item->Key = $item->generateKey();
165
        $this->setItem($item);
166
167
        return $this;
168
    }
169
170
    /**
171
     * Find the best possible tax rate for a line item. If the item is
172
     * linked to an invoice/estimate, then see if there is a Country
173
     * and Region set, else use product default
174
     *
175
     * @return TaxRate
176
     */
177
    public function findBestTaxRate()
178
    {
179
        $item = $this->getItem();
180
        $product = $this->getProduct();
181
        $default = TaxRate::create();
182
        $default->ID = -1;
183
184
        // If no product available, return an empty rate
185
        if (empty($product)) {
186
            return $default;
187
        }
188
189
        if (empty($item)) {
190
            return $product->getTaxRate();
191
        }
192
193
        $parent = $this->getParent();
194
195
        // If order available, try to gt delivery location
196
        if (!empty($parent)) {
197
            $country = $parent->DeliveryCountry;
198
            $region = $parent->DeliveryCounty;
0 ignored issues
show
Bug Best Practice introduced by
The property DeliveryCounty does not exist on SilverCommerce\OrdersAdmin\Model\Estimate. Since you implemented __get, consider adding a @property annotation.
Loading history...
199
            /** @var \SilverCommerce\TaxAdmin\Model\TaxCategory */
200
            $category = $product->TaxCategory();
201
202
            if ($category->exists() && strlen($country) >= 2 && strlen($region) >= 2) {
0 ignored issues
show
Bug introduced by
It seems like $region can also be of type null; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

202
            if ($category->exists() && strlen($country) >= 2 && strlen(/** @scrutinizer ignore-type */ $region) >= 2) {
Loading history...
203
                return $category->ValidTax($country, $region);
204
            }
205
        }
206
207
        // Finally, return product's default tax
208
        return $product->getTaxRate();
209
    }
210
211
    /**
212
     * Get an array of data for the line item
213
     *
214
     * @return array
215
     */
216
    protected function getItemArray()
217
    {
218
        $product = $this->getProduct();
219
        $qty = $this->getQuantity();
220
        $lock = $this->getLock();
221
222
        if (empty($product)) {
223
            throw new ValidationException(
224
                _t(
225
                    __CLASS__ . "NoProductSet",
226
                    "No product set"
227
                )
228
            );
229
        }
230
231
        // ensure that object price is something we can work with
232
        if (empty($product->BasePrice)) {
233
            throw new ValidationException("Product needs a 'BasePrice' param");
234
        }
235
236
        // Check if deliverable and stocked
237
        $stocked_param = $this->getProductStockedParam();
238
        $deliver_param = $this->getProductDeliverableParam();
239
240
        if (isset($product->{$deliver_param})) {
241
            $deliverable = (bool) $product->{$deliver_param};
242
        } else {
243
            $deliverable = true;
244
        }
245
246
        if (isset($product->{$stocked_param})) {
247
            $stocked = (bool) $product->{$stocked_param};
248
        } else {
249
            $stocked = false;
250
        }
251
252
        $tax_rate = $this->findBestTaxRate();
253
254
        // Setup initial line item
255
        return [
256
            "Title" => $product->Title,
257
            "BasePrice" => $product->BasePrice,
258
            "TaxRateID" => $tax_rate->ID,
259
            "StockID" => $product->StockID,
260
            "ProductClass" => $product->ClassName,
261
            "Quantity" => $qty,
262
            "Stocked" => $stocked,
263
            "Deliverable" => $deliverable,
264
            'Locked' => $lock
265
        ];
266
    }
267
268
    /**
269
     * Shortcut to get the item key from the item in this factory
270
     *
271
     * @return string
272
     */
273
    public function getKey()
274
    {
275
        $item = $this->getItem();
276
        if (!empty($item) && !empty($item->Key)) {
277
            return $item->Key;
278
        }
279
280
        return "";
281
    }
282
283
    /**
284
     * Create a customisation object to be added to the current order
285
     *
286
     * @param array $data An array of data to add to the customisation
287
     *
288
     * @return DataObject
289
     */
290
    protected function createCustomisation(array $data)
291
    {
292
        $mapped_data = [];
293
        $class = self::CUSTOM_CLASS;
294
295
        foreach ($data as $key => $value) {
296
            if (in_array($key, $this->config()->get('custom_map'))) {
297
                $mapped_data[$key] = $value;
298
            }
299
        }
300
301
        return $class::create($mapped_data);
302
    }
303
304
    /**
305
     * Check the available stock for the current line item. If stock checking
306
     * is disabled then returns true
307
     *
308
     * @return bool
309
     */
310
    public function checkStockLevel()
311
    {
312
        $qty = $this->getQuantity();
313
        $force = $this->config()->get('force_check_stock');
314
        $stock_item = $this->getItem()->findStockItem();
315
        $param = $this->getProductStockParam();
316
        $item = $this->getItem();
317
318
        // If we are checking stock and there is not enough, return false
319
        if (isset($stock_item)
320
            && ($force || isset($stock_item->{$param}) && $stock_item->{$param})
321
            && ($item->checkStockLevel($qty) < 0)
322
        ) {
323
            return false;
324
        }
325
326
        return true;
327
    }
328
329
    /**
330
     * Write the current line item
331
     *
332
     * @return self
333
     */
334
    public function write()
335
    {
336
        $item = $this->getItem();
337
        if (!empty($item)) {
338
            $item->write();
339
        }
340
        return $this;
341
    }
342
343
    /**
344
     * Remove the current item from the DB
345
     *
346
     * @return self
347
     */
348
    public function delete()
349
    {
350
        $item = $this->getItem();
351
        if (!empty($item)) {
352
            $item->delete();
353
        }
354
        return $this;
355
    }
356
357
    /**
358
     * Get current line item
359
     *
360
     * @return  DataObject
361
     */
362
    public function getItem()
363
    {
364
        return $this->item;
365
    }
366
367
    /**
368
     * Set current line item
369
     *
370
     * @param LineItem $item  Item to add
371
     * @param boolean  $setup Should we setup this factory based on the item?
372
     *
373
     * @return self
374
     */
375
    public function setItem(LineItem $item, $setup = true)
376
    {
377
        // If item has an assigned product, add it as well
378
        $this->item = $item;
379
380
        if (!$setup) {
381
            return $this;
382
        }
383
384
        $product = $item->FindStockItem();
385
        if (!empty($product)) {
386
            $this->setProduct($product);
387
        }
388
389
        $this->setQuantity($item->Quantity);
0 ignored issues
show
Bug introduced by
It seems like $item->Quantity can also be of type null; however, parameter $quantity of SilverCommerce\OrdersAdm...mFactory::setQuantity() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

389
        $this->setQuantity(/** @scrutinizer ignore-type */ $item->Quantity);
Loading history...
Bug Best Practice introduced by
The property Quantity does not exist on SilverCommerce\OrdersAdmin\Model\LineItem. Since you implemented __get, consider adding a @property annotation.
Loading history...
390
391
        // Only lock a line item if we have explicitly asked to
392
        if (property_exists($item, 'Locked')) {
393
            $this->setLock($item->Locked);
0 ignored issues
show
Bug introduced by
It seems like $item->Locked can also be of type null; however, parameter $lock of SilverCommerce\OrdersAdm...eItemFactory::setLock() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

393
            $this->setLock(/** @scrutinizer ignore-type */ $item->Locked);
Loading history...
Bug Best Practice introduced by
The property Locked does not exist on SilverCommerce\OrdersAdmin\Model\LineItem. Since you implemented __get, consider adding a @property annotation.
Loading history...
394
        }
395
396
        $this->setCustomisations($item->Customisations()->toArray());
397
398
        $this->setParent($item->Parent());
399
400
        return $this;
401
    }
402
403
    /**
404
     * Get dataObject that will act as the product
405
     *
406
     * @return DataObject
407
     */
408
    public function getProduct()
409
    {
410
        return $this->product;
411
    }
412
413
    /**
414
     * Set dataObject that will act as the product
415
     *
416
     * @param DataObject $product product object
417
     *
418
     * @return self
419
     */
420
    public function setProduct(DataObject $product)
421
    {
422
        $this->product = $product;
423
        return $this;
424
    }
425
426
    /**
427
     * Get list of customisation data that will need to be setup
428
     *
429
     * @return array
430
     */
431
    public function getCustomisations()
432
    {
433
        return $this->customisations;
434
    }
435
436
    /**
437
     * Set list of customisation data that will need to be setup
438
     *
439
     * @param array $customisations customisation data
440
     *
441
     * @return self
442
     */
443
    public function setCustomisations(array $customisations)
444
    {
445
        $this->customisations = $customisations;
446
        return $this;
447
    }
448
449
    /**
450
     * Get the number of products to add/update for this line item
451
     *
452
     * @return int
453
     */
454
    public function getQuantity()
455
    {
456
        return $this->quantity;
457
    }
458
459
    /**
460
     * Set the number of products to add/update for this line item
461
     *
462
     * @param int $quantity number of products
463
     *
464
     * @return self
465
     */
466
    public function setQuantity(int $quantity)
467
    {
468
        $this->quantity = $quantity;
469
        return $this;
470
    }
471
472
    /**
473
     * Get should this item be locked (cannot be updated, only removed)?
474
     *
475
     * @return bool
476
     */
477
    public function getLock()
478
    {
479
        $item = $this->getItem();
480
        if (empty($this->lock) && isset($item)) {
481
            return $item->Locked;
482
        }
483
484
        return $this->lock;
485
    }
486
487
    /**
488
     * Set should this item be locked (cannot be updated, only removed)?
489
     *
490
     * @param bool $lock Is item locked?
491
     *
492
     * @return self
493
     */
494
    public function setLock(bool $lock)
495
    {
496
        $this->lock = $lock;
497
        return $this;
498
    }
499
500
    /**
501
     * Get name of stocked parameter
502
     *
503
     * @return string
504
     */
505
    public function getProductStockedParam()
506
    {
507
        return $this->product_stocked_param;
508
    }
509
510
    /**
511
     * Get name of stocked parameter
512
     *
513
     * @param string $param Param name.
514
     *
515
     * @return self
516
     */
517
    public function setProductStockedParam(string $param)
518
    {
519
        $this->product_stocked_param = $param;
520
        return $this;
521
    }
522
523
    /**
524
     * Get the name of the param used on product to track Stock Level.
525
     *
526
     * @return string
527
     */
528
    public function getProductStockParam()
529
    {
530
        return $this->product_stock_param;
531
    }
532
533
    /**
534
     * Set the name of the param used on product to track Stock Level.
535
     *
536
     * @param string $param param name
537
     *
538
     * @return self
539
     */
540
    public function setProductStockParam(string $param)
541
    {
542
        $this->product_stock_param = $param;
543
        return $this;
544
    }
545
546
    /**
547
     * Get the name of the param used on product to determin if item is deliverable
548
     *
549
     * @return string
550
     */
551
    public function getProductDeliverableParam()
552
    {
553
        return $this->product_deliverable_param;
554
    }
555
556
    /**
557
     * Set the name of the param used on product to determin if item is deliverable
558
     *
559
     * @param string $param The param name
560
     *
561
     * @return self
562
     */
563
    public function setProductDeliverableParam(string $param)
564
    {
565
        $this->product_deliverable_param = $param;
566
        return $this;
567
    }
568
569
    /**
570
     * Get current parent estimate
571
     *
572
     * @return Estimate
573
     */ 
574
    public function getParent()
575
    {
576
        return $this->parent;
577
    }
578
579
    /**
580
     * Set current parent estimate
581
     *
582
     * @param Estimate $parent
583
     *
584
     * @return self
585
     */ 
586
    public function setParent(Estimate $parent)
587
    {
588
        $this->parent = $parent;
589
        return $this;
590
    }
591
}
592