Passed
Push — 1.3 ( 0611b0...cd4c68 )
by Morven
02:38
created

LineItemFactory::delete()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 7
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->Rate = 0;
183
        $default->ID = -1;
184
185
        // If no product available, return an empty rate
186
        if (empty($product)) {
187
            return $default;
188
        }
189
190
        if (empty($item)) {
191
            return $product->getTaxRate();
192
        }
193
194
        $parent = $this->getParent();
195
196
        // If order available, try to gt delivery location
197
        if (!empty($parent)) {
198
            $rate = null;
199
            $country = $parent->DeliveryCountry;
200
            $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...
201
            /** @var \SilverCommerce\TaxAdmin\Model\TaxCategory */
202
            $category = $product->TaxCategory();
203
204
            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

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

397
        $this->setQuantity(/** @scrutinizer ignore-type */ $item->Quantity);
Loading history...
398
399
        // Only lock a line item if we have explicitly asked to
400
        if (property_exists($item, 'Locked')) {
401
            $this->setLock($item->Locked);
0 ignored issues
show
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...
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

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