Order   F
last analyzed

Complexity

Total Complexity 105

Size/Duplication

Total Lines 874
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 307
c 2
b 0
f 0
dl 0
loc 874
rs 2
wmc 105

40 Methods

Rating   Name   Duplication   Size   Complexity  
A getStatusI18N() 0 3 1
A Total() 0 3 1
A setTotal() 0 3 1
A GrandTotal() 0 3 1
A getModifier() 0 4 1
A calculate() 0 7 2
A TotalOutstanding() 0 5 2
A getCMSFields() 0 23 3
A getDefaultSearchContext() 0 53 1
A SubTotal() 0 7 2
A get_order_status_options() 0 7 2
A getComponents() 0 10 3
A fieldLabels() 0 9 1
A getAddressesDiffer() 0 3 2
A Currency() 0 3 1
A canPay() 0 14 5
A canView() 0 8 2
A IsCart() 0 3 1
A canEdit() 0 8 2
A getShippingAddress() 0 3 1
B statusTransition() 0 22 7
A debug() 0 26 6
A provideI18nEntities() 0 14 2
A getBillingAddress() 0 6 3
A IsPaid() 0 3 2
B onBeforeWrite() 0 19 7
A getReference() 0 3 2
A getName() 0 5 3
A IsSent() 0 3 1
A getTitle() 0 3 1
A IsProcessing() 0 3 2
A canCreate() 0 8 2
B canCancel() 0 19 7
A canDelete() 0 8 2
A onAfterWrite() 0 22 2
A onBeforeDelete() 0 19 4
A getLatestEmail() 0 6 4
B getAddress() 0 21 9
A generateReference() 0 14 2
A Link() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like Order often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Order, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverShop\Model;
4
5
use SilverShop\Cart\OrderTotalCalculator;
6
use SilverShop\Checkout\OrderEmailNotifier;
7
use SilverShop\Extension\MemberExtension;
8
use SilverShop\Extension\ShopConfigExtension;
9
use SilverShop\Model\Modifiers\OrderModifier;
10
use SilverShop\ORM\Filters\MultiFieldPartialMatchFilter;
11
use SilverShop\ORM\OrderItemList;
12
use SilverShop\Page\AccountPage;
13
use SilverShop\Page\CheckoutPage;
14
use SilverShop\ShopTools;
15
use SilverStripe\Control\Controller;
16
use SilverStripe\Control\Director;
17
use SilverStripe\Dev\Debug;
18
use SilverStripe\Forms\CheckboxSetField;
19
use SilverStripe\Forms\DateField;
20
use SilverStripe\Forms\DropdownField;
21
use SilverStripe\Forms\FieldList;
22
use SilverStripe\Forms\LiteralField;
23
use SilverStripe\Forms\Tab;
24
use SilverStripe\Forms\TabSet;
25
use SilverStripe\Omnipay\Extensions\Payable;
26
use SilverStripe\ORM\DataObject;
27
use SilverStripe\ORM\FieldType\DBCurrency;
28
use SilverStripe\ORM\FieldType\DBDatetime;
29
use SilverStripe\ORM\FieldType\DBEnum;
30
use SilverStripe\ORM\Filters\GreaterThanFilter;
31
use SilverStripe\ORM\Filters\LessThanFilter;
32
use SilverStripe\ORM\HasManyList;
33
use SilverStripe\ORM\Search\SearchContext;
34
use SilverStripe\ORM\UnsavedRelationList;
35
use SilverStripe\Security\Member;
36
use SilverStripe\Security\Security;
37
38
/**
39
 * The order class is a databound object for handling Orders
40
 * within SilverStripe.
41
 *
42
 * @mixin Payable
43
 *
44
 * @property DBCurrency $Total
45
 * @property string $Reference
46
 * @property DBDatetime $Placed
47
 * @property DBDatetime $Paid
48
 * @property DBDatetime $ReceiptSent
49
 * @property DBDatetime $Printed
50
 * @property DBDatetime $Dispatched
51
 * @property DBEnum $Status
52
 * @property string $FirstName
53
 * @property string $Surname
54
 * @property string $Email
55
 * @property string $Notes
56
 * @property string $IPAddress
57
 * @property bool $SeparateBillingAddress
58
 * @property string $Locale
59
 * @property int $MemberID
60
 * @property int $ShippingAddressID
61
 * @property int $BillingAddressID
62
 * @method   Member|MemberExtension Member()
63
 * @method   Address BillingAddress()
64
 * @method   Address ShippingAddress()
65
 * @method   OrderItem[]|HasManyList Items()
66
 * @method   OrderModifier[]|HasManyList Modifiers()
67
 * @method   OrderStatusLog[]|HasManyList OrderStatusLogs()
68
 */
69
class Order extends DataObject
70
{
71
    /**
72
     * Status codes and what they mean:
73
     *
74
     * Unpaid (default): Order created but no successful payment by customer yet
75
     * Query: Order not being processed yet (customer has a query, or could be out of stock)
76
     * Paid: Order successfully paid for by customer
77
     * Processing: Order paid for, package is currently being processed before shipping to customer
78
     * Sent: Order paid for, processed for shipping, and now sent to the customer
79
     * Complete: Order completed (paid and shipped). Customer assumed to have received their goods
80
     * AdminCancelled: Order cancelled by the administrator
81
     * MemberCancelled: Order cancelled by the customer (Member)
82
     */
83
    private static $db = [
0 ignored issues
show
introduced by Sam Minnée
The private property $db is not used, and could be removed.
Loading history...
84
        'Total' => 'Currency',
85
        'Reference' => 'Varchar', //allow for customised order numbering schemes
86
        //status
87
        'Placed' => 'Datetime', //date the order was placed (went from Cart to Order)
88
        'Paid' => 'Datetime', //no outstanding payment left
89
        'ReceiptSent' => 'Datetime', //receipt emailed to customer
90
        'Printed' => 'Datetime',
91
        'Dispatched' => 'Datetime', //products have been sent to customer
92
        'Status' => "Enum('Unpaid,Paid,Processing,Sent,Complete,AdminCancelled,MemberCancelled,Cart','Cart')",
93
        //customer (for guest orders)
94
        'FirstName' => 'Varchar',
95
        'Surname' => 'Varchar',
96
        'Email' => 'Varchar',
97
        'Notes' => 'Text',
98
        'IPAddress' => 'Varchar(15)',
99
        //separate shipping
100
        'SeparateBillingAddress' => 'Boolean',
101
        // keep track of customer locale
102
        'Locale' => 'Locale',
103
    ];
104
105
    private static $has_one = [
0 ignored issues
show
introduced by Sam Minnée
The private property $has_one is not used, and could be removed.
Loading history...
106
        'Member' => Member::class,
107
        'ShippingAddress' => Address::class,
108
        'BillingAddress' => Address::class,
109
    ];
110
111
    private static $has_many = [
0 ignored issues
show
introduced by Sam Minnée
The private property $has_many is not used, and could be removed.
Loading history...
112
        'Items' => OrderItem::class,
113
        'Modifiers' => OrderModifier::class,
114
        'OrderStatusLogs' => OrderStatusLog::class,
115
    ];
116
117
    private static $defaults = [
0 ignored issues
show
introduced by Sam Minnée
The private property $defaults is not used, and could be removed.
Loading history...
118
        'Status' => 'Cart',
119
    ];
120
121
    private static $casting = [
0 ignored issues
show
introduced by Sam Minnée
The private property $casting is not used, and could be removed.
Loading history...
122
        'FullBillingAddress' => 'Text',
123
        'FullShippingAddress' => 'Text',
124
        'Total' => 'Currency',
125
        'SubTotal' => 'Currency',
126
        'TotalPaid' => 'Currency',
127
        'Shipping' => 'Currency',
128
        'TotalOutstanding' => 'Currency',
129
    ];
130
131
    private static $summary_fields = [
0 ignored issues
show
introduced by Sam Minnée
The private property $summary_fields is not used, and could be removed.
Loading history...
132
        'Reference',
133
        'Placed',
134
        'Name',
135
        'LatestEmail',
136
        'Total',
137
        'StatusI18N',
138
    ];
139
140
    private static $searchable_fields = [
0 ignored issues
show
introduced by Sam Minnée
The private property $searchable_fields is not used, and could be removed.
Loading history...
141
        'Reference',
142
        'Name',
143
        'Email',
144
        'Status' => [
145
            'filter' => 'ExactMatchFilter',
146
            'field' => CheckboxSetField::class,
147
        ],
148
    ];
149
150
    private static $table_name = 'SilverShop_Order';
0 ignored issues
show
introduced by Sam Minnée
The private property $table_name is not used, and could be removed.
Loading history...
151
152
    private static $singular_name = 'Order';
0 ignored issues
show
introduced by Sam Minnée
The private property $singular_name is not used, and could be removed.
Loading history...
153
154
    private static $plural_name = 'Orders';
0 ignored issues
show
introduced by Sam Minnée
The private property $plural_name is not used, and could be removed.
Loading history...
155
156
    private static $default_sort = '"Placed" DESC, "Created" DESC';
0 ignored issues
show
introduced by Sam Minnée
The private property $default_sort is not used, and could be removed.
Loading history...
157
158
    /**
159
     * Statuses for orders that have been placed.
160
     *
161
     * @config
162
     */
163
    private static $placed_status = [
164
        'Paid',
165
        'Unpaid',
166
        'Processing',
167
        'Sent',
168
        'Complete',
169
        'MemberCancelled',
170
        'AdminCancelled',
171
    ];
172
173
    /**
174
     * Statuses for which an order can be paid for
175
     *
176
     * @config
177
     */
178
    private static $payable_status = [
179
        'Cart',
180
        'Unpaid',
181
        'Processing',
182
        'Sent',
183
    ];
184
185
    /**
186
     * Statuses that shouldn't show in user account.
187
     *
188
     * @config
189
     */
190
    private static $hidden_status = ['Cart'];
0 ignored issues
show
introduced by Sam Minnée
The private property $hidden_status is not used, and could be removed.
Loading history...
191
192
193
    /**
194
     * Statuses that should be logged in the Order-Status-Log
195
     *
196
     * @config
197
     * @var    array
198
     */
199
    private static $log_status = [];
200
201
    /**
202
     * Whether or not an order can be cancelled before payment
203
     *
204
     * @config
205
     * @var    bool
206
     */
207
    private static $cancel_before_payment = true;
208
209
    /**
210
     * Whether or not an order can be cancelled before processing
211
     *
212
     * @config
213
     * @var    bool
214
     */
215
    private static $cancel_before_processing = false;
216
217
    /**
218
     * Whether or not an order can be cancelled before sending
219
     *
220
     * @config
221
     * @var    bool
222
     */
223
    private static $cancel_before_sending = false;
224
225
    /**
226
     * Whether or not an order can be cancelled after sending
227
     *
228
     * @config
229
     * @var    bool
230
     */
231
    private static $cancel_after_sending = false;
232
233
    /**
234
     * Place an order before payment processing begins
235
     *
236
     * @config
237
     * @var    boolean
238
     */
239
    private static $place_before_payment = false;
0 ignored issues
show
introduced by Mark Guinn
The private property $place_before_payment is not used, and could be removed.
Loading history...
240
241
    /**
242
     * Modifiers represent the additional charges or
243
     * deductions associated to an order, such as
244
     * shipping, taxes, vouchers etc.
245
     *
246
     * @config
247
     * @var    array
248
     */
249
    private static $modifiers = [];
0 ignored issues
show
introduced by Sam Minnée
The private property $modifiers is not used, and could be removed.
Loading history...
250
251
    /**
252
     * Rounding precision of order amounts
253
     *
254
     * @config
255
     * @var    int
256
     */
257
    private static $rounding_precision = 2;
258
259
    /**
260
     * Minimal length (number of decimals) of order reference ids
261
     *
262
     * @config
263
     * @var    int
264
     */
265
    private static $reference_id_padding = 5;
266
267
    /**
268
     * Will allow completion of orders with GrandTotal=0,
269
     * which could be the case for orders paid with loyalty points or vouchers.
270
     * Will send the "Paid" date on the order, even though no actual payment was taken.
271
     * Will trigger the payment related extension points:
272
     * Order->onPayment, OrderItem->onPayment, Order->onPaid.
273
     *
274
     * @config
275
     * @var    boolean
276
     */
277
    private static $allow_zero_order_total = false;
0 ignored issues
show
introduced by Mark Guinn
The private property $allow_zero_order_total is not used, and could be removed.
Loading history...
278
279
    /**
280
     * A flag indicating that an order-status-log entry should be written
281
     *
282
     * @var bool
283
     */
284
    protected $flagOrderStatusWrite = false;
285
286
    public static function get_order_status_options()
287
    {
288
        $values = array();
289
        foreach (singleton(Order::class)->dbObject('Status')->enumValues(false) as $value) {
290
            $values[$value] = _t(__CLASS__ . '.STATUS_' . strtoupper($value), $value);
291
        }
292
        return $values;
293
    }
294
295
    /**
296
     * Create CMS fields for cms viewing and editing orders
297
     */
298
    public function getCMSFields()
299
    {
300
        $fields = FieldList::create(TabSet::create('Root', Tab::create('Main')));
301
        $fs = '<div class="field">';
302
        $fe = '</div>';
303
        $parts = array(
304
            DropdownField::create('Status', $this->fieldLabel('Status'), self::get_order_status_options()),
305
            LiteralField::create('Customer', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Customer') . $fe),
306
            LiteralField::create('Addresses', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Addresses') . $fe),
307
            LiteralField::create('Content', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Content') . $fe),
308
        );
309
        if ($this->Notes) {
310
            $parts[] = LiteralField::create('Notes', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Notes') . $fe);
311
        }
312
        $fields->addFieldsToTab('Root.Main', $parts);
313
        $this->extend('updateCMSFields', $fields);
314
        if ($payments = $fields->fieldByName('Root.Payments.Payments')) {
315
            $fields->removeByName('Payments');
316
            $fields->insertAfter('Content', $payments);
317
            $payments->addExtraClass('order-payments');
318
        }
319
320
        return $fields;
321
    }
322
323
    /**
324
     * Augment field labels
325
     */
326
    public function fieldLabels($includerelations = true)
327
    {
328
        $labels = parent::fieldLabels($includerelations);
329
330
        $labels['Name'] = _t('SilverShop\Generic.Customer', 'Customer');
331
        $labels['LatestEmail'] = _t(__CLASS__ . '.db_Email', 'Email');
332
        $labels['StatusI18N'] = _t(__CLASS__ . '.db_Status', 'Status');
333
334
        return $labels;
335
    }
336
337
    /**
338
     * Adjust scafolded search context
339
     *
340
     * @return SearchContext the updated search context
341
     */
342
    public function getDefaultSearchContext()
343
    {
344
        $context = parent::getDefaultSearchContext();
345
        $fields = $context->getFields();
346
347
        $validStates = self::config()->placed_status;
348
        $statusOptions = array_filter(self::get_order_status_options(), function ($k) use ($validStates) {
349
            return in_array($k, $validStates);
350
        }, ARRAY_FILTER_USE_KEY);
351
352
        $fields->push(
353
            // TODO: Allow filtering by multiple statuses
354
            DropdownField::create('Status', $this->fieldLabel('Status'))
355
                ->setSource($statusOptions)
356
                ->setHasEmptyDefault(true)
357
        );
358
359
        // add date range filtering
360
        $fields->insertBefore(
361
            'Status',
362
            DateField::create('DateFrom', _t(__CLASS__ . '.DateFrom', 'Date from'))
363
        );
364
365
        $fields->insertBefore(
366
            'Status',
367
            DateField::create('DateTo', _t(__CLASS__ . '.DateTo', 'Date to'))
368
        );
369
370
        // get the array, to maniplulate name, and fullname seperately
371
        $filters = $context->getFilters();
372
        $filters['DateFrom'] = GreaterThanFilter::create('Placed');
373
        $filters['DateTo'] = LessThanFilter::create('Placed');
374
375
        // filter customer need to use a bunch of different sources
376
        $filters['Name'] = MultiFieldPartialMatchFilter::create(
377
            'FirstName',
378
            false,
379
            ['SplitWords'],
380
            [
381
                'Surname',
382
                'Member.FirstName',
383
                'Member.Surname',
384
                'BillingAddress.FirstName',
385
                'BillingAddress.Surname',
386
                'ShippingAddress.FirstName',
387
                'ShippingAddress.Surname',
388
            ]
389
        );
390
391
        $context->setFilters($filters);
392
393
        $this->extend('updateDefaultSearchContext', $context);
394
        return $context;
395
    }
396
397
    /**
398
     * Hack for swapping out relation list with OrderItemList
399
     *
400
     * @inheritdoc
401
     */
402
    public function getComponents($componentName, $id = null)
403
    {
404
        $components = parent::getComponents($componentName, $id);
405
        if ($componentName === 'Items' && get_class($components) !== UnsavedRelationList::class) {
406
            $query = $components->dataQuery();
407
            $components = OrderItemList::create(OrderItem::class, 'OrderID');
408
            $components->setDataQuery($query);
409
            $components = $components->forForeignID($this->ID);
410
        }
411
        return $components;
412
    }
413
414
    /**
415
     * Returns the subtotal of the items for this order.
416
     */
417
    public function SubTotal()
418
    {
419
        if ($this->Items()->exists()) {
420
            return $this->Items()->SubTotal();
421
        }
422
423
        return 0;
424
    }
425
426
    /**
427
     * Calculate the total
428
     *
429
     * @return float the final total
430
     */
431
    public function calculate()
432
    {
433
        if (!$this->IsCart()) {
434
            return $this->Total;
0 ignored issues
show
Bug Best Practice introduced by Mark Guinn
The expression return $this->Total returns the type SilverStripe\ORM\FieldType\DBCurrency which is incompatible with the documented return type double.
Loading history...
435
        }
436
        $calculator = OrderTotalCalculator::create($this);
437
        return $this->Total = $calculator->calculate();
438
    }
439
440
    /**
441
     * This is needed to maintain backwards compatiability with
442
     * some subsystems using modifiers. eg discounts
443
     */
444
    public function getModifier($className, $forcecreate = false)
445
    {
446
        $calculator = OrderTotalCalculator::create($this);
447
        return $calculator->getModifier($className, $forcecreate);
448
    }
449
450
    /**
451
     * Enforce rounding precision when setting total
452
     */
453
    public function setTotal($val)
454
    {
455
        $this->setField('Total', round($val, self::$rounding_precision));
456
    }
457
458
    /**
459
     * Get final value of order.
460
     * Retrieves value from DataObject's record array.
461
     */
462
    public function Total()
463
    {
464
        return $this->getField('Total');
465
    }
466
467
    /**
468
     * Alias for Total.
469
     */
470
    public function GrandTotal()
471
    {
472
        return $this->Total();
473
    }
474
475
    /**
476
     * Calculate how much is left to be paid on the order.
477
     * Enforces rounding precision.
478
     *
479
     * Payments that have been authorized via a non-manual gateway should count towards the total paid amount.
480
     * However, it's possible to exclude these by setting the $includeAuthorized parameter to false, which is
481
     * useful to determine the status of the Order. Order status should only change to 'Paid' when all
482
     * payments are 'Captured'.
483
     *
484
     * @param  bool $includeAuthorized whether or not to include authorized payments (excluding manual payments)
485
     * @return float
486
     */
487
    public function TotalOutstanding($includeAuthorized = true)
488
    {
489
        return round(
490
            $this->GrandTotal() - ($includeAuthorized ? $this->TotalPaidOrAuthorized() : $this->TotalPaid()),
0 ignored issues
show
Bug introduced by Roman Schmid
The method TotalPaidOrAuthorized() does not exist on SilverShop\Model\Order. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

490
            $this->GrandTotal() - ($includeAuthorized ? $this->/** @scrutinizer ignore-call */ TotalPaidOrAuthorized() : $this->TotalPaid()),
Loading history...
Bug introduced by Roman Schmid
The method TotalPaid() does not exist on SilverShop\Model\Order. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

490
            $this->GrandTotal() - ($includeAuthorized ? $this->TotalPaidOrAuthorized() : $this->/** @scrutinizer ignore-call */ TotalPaid()),
Loading history...
491
            self::config()->rounding_precision
492
        );
493
    }
494
495
    /**
496
     * Get the order status. This will return a localized value if available.
497
     *
498
     * @return string the payment status
499
     */
500
    public function getStatusI18N()
501
    {
502
        return _t(__CLASS__ . '.STATUS_' . strtoupper($this->Status), $this->Status);
503
    }
504
505
    /**
506
     * Get the link for finishing order processing.
507
     */
508
    public function Link()
509
    {
510
        $link = CheckoutPage::find_link(false, 'order', $this->ID);
511
        
512
        if (Security::getCurrentUser()) {
513
            $link = Controller::join_links(AccountPage::find_link(), 'order', $this->ID);
514
        }
515
516
        $this->extend('updateLink', $link);
517
518
        return $link;
519
    }
520
521
    /**
522
     * Returns TRUE if the order can be cancelled
523
     * PRECONDITION: Order is in the DB.
524
     *
525
     * @return boolean
526
     */
527
    public function canCancel($member = null)
528
    {
529
        $extended = $this->extendedCan(__FUNCTION__, $member);
530
        if ($extended !== null) {
531
            return $extended;
532
        }
533
534
        switch ($this->Status) {
535
            case 'Unpaid' :
536
            return self::config()->cancel_before_payment;
537
            case 'Paid' :
538
            return self::config()->cancel_before_processing;
539
            case 'Processing' :
540
            return self::config()->cancel_before_sending;
541
            case 'Sent' :
542
            case 'Complete' :
543
            return self::config()->cancel_after_sending;
544
        }
545
        return false;
546
    }
547
548
    /**
549
     * Check if an order can be paid for.
550
     *
551
     * @return boolean
552
     */
553
    public function canPay($member = null)
554
    {
555
        $extended = $this->extendedCan(__FUNCTION__, $member);
556
        if ($extended !== null) {
557
            return $extended;
558
        }
559
560
        if (!in_array($this->Status, self::config()->payable_status)) {
561
            return false;
562
        }
563
        if ($this->TotalOutstanding(true) > 0 && empty($this->Paid)) {
564
            return true;
565
        }
566
        return false;
567
    }
568
569
    /**
570
     * Prevent deleting orders.
571
     *
572
     * @return boolean
573
     */
574
    public function canDelete($member = null)
575
    {
576
        $extended = $this->extendedCan(__FUNCTION__, $member);
577
        if ($extended !== null) {
578
            return $extended;
579
        }
580
581
        return false;
582
    }
583
584
    /**
585
     * Check if an order can be viewed.
586
     *
587
     * @return boolean
588
     */
589
    public function canView($member = null)
590
    {
591
        $extended = $this->extendedCan(__FUNCTION__, $member);
592
        if ($extended !== null) {
593
            return $extended;
594
        }
595
596
        return true;
597
    }
598
599
    /**
600
     * Check if an order can be edited.
601
     *
602
     * @return boolean
603
     */
604
    public function canEdit($member = null)
605
    {
606
        $extended = $this->extendedCan(__FUNCTION__, $member);
607
        if ($extended !== null) {
608
            return $extended;
609
        }
610
611
        return true;
612
    }
613
614
    /**
615
     * Prevent standard creation of orders.
616
     *
617
     * @return boolean
618
     */
619
    public function canCreate($member = null, $context = array())
620
    {
621
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
622
        if ($extended !== null) {
623
            return $extended;
624
        }
625
626
        return false;
627
    }
628
629
    /**
630
     * Return the currency of this order.
631
     * Note: this is a fixed value across the entire site.
632
     *
633
     * @return string
634
     */
635
    public function Currency()
636
    {
637
        return ShopConfigExtension::get_site_currency();
638
    }
639
640
    /**
641
     * Get the latest email for this order.z
642
     */
643
    public function getLatestEmail()
644
    {
645
        if ($this->MemberID && ($this->Member()->LastEdited > $this->LastEdited || !$this->Email)) {
0 ignored issues
show
Bug introduced by Mark Guinn
The property LastEdited does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
646
            return $this->Member()->Email;
0 ignored issues
show
Bug introduced by Mark Guinn
The property Email does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
647
        }
648
        return $this->getField('Email');
649
    }
650
651
    /**
652
     * Gets the name of the customer.
653
     */
654
    public function getName()
655
    {
656
        $firstname = $this->FirstName ? $this->FirstName : $this->Member()->FirstName;
0 ignored issues
show
Bug introduced by Mark Guinn
The property FirstName does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
657
        $surname = $this->FirstName ? $this->Surname : $this->Member()->Surname;
0 ignored issues
show
Bug introduced by Mark Guinn
The property Surname does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
658
        return implode(' ', array_filter(array($firstname, $surname)));
659
    }
660
661
    public function getTitle()
662
    {
663
        return $this->Reference . ' - ' . $this->dbObject('Placed')->Nice();
664
    }
665
666
    /**
667
     * Get shipping address, or member default shipping address.
668
     */
669
    public function getShippingAddress()
670
    {
671
        return $this->getAddress('Shipping');
672
    }
673
674
    /**
675
     * Get billing address, if marked to use seperate address, otherwise use shipping address,
676
     * or the member default billing address.
677
     */
678
    public function getBillingAddress()
679
    {
680
        if (!$this->SeparateBillingAddress && $this->ShippingAddressID === $this->BillingAddressID) {
681
            return $this->getShippingAddress();
682
        } else {
683
            return $this->getAddress('Billing');
684
        }
685
    }
686
687
    /**
688
     * @param string $type - Billing or Shipping
689
     * @return Address
690
     * @throws \Exception
691
     */
692
    protected function getAddress($type)
693
    {
694
        $address = $this->getComponent($type . 'Address');
695
696
        if (!$address || !$address->exists() && $this->Member()) {
0 ignored issues
show
introduced by Mark Guinn
$address is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
697
            $address = $this->Member()->{"Default${type}Address"}();
698
        }
699
700
        if (empty($address->Surname) && empty($address->FirstName)) {
701
            if ($member = $this->Member()) {
702
                // If there's a member object, use information from the Member.
703
                // The information from Order should have precendence if set though!
704
                $address->FirstName = $this->FirstName ?: $member->FirstName;
0 ignored issues
show
Bug introduced by Roman Schmid
The property FirstName does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
705
                $address->Surname = $this->Surname ?: $member->Surname;
0 ignored issues
show
Bug introduced by Roman Schmid
The property Surname does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
706
            } else {
707
                $address->FirstName = $this->FirstName;
708
                $address->Surname = $this->Surname;
709
            }
710
        }
711
712
        return $address;
713
    }
714
715
    /**
716
     * Check if the two addresses saved differ.
717
     *
718
     * @return boolean
719
     */
720
    public function getAddressesDiffer()
721
    {
722
        return $this->SeparateBillingAddress || $this->ShippingAddressID !== $this->BillingAddressID;
723
    }
724
725
    /**
726
     * Has this order been sent to the customer?
727
     * (at "Sent" status).
728
     *
729
     * @return boolean
730
     */
731
    public function IsSent()
732
    {
733
        return $this->Status == 'Sent';
734
    }
735
736
    /**
737
     * Is this order currently being processed?
738
     * (at "Sent" OR "Processing" status).
739
     *
740
     * @return boolean
741
     */
742
    public function IsProcessing()
743
    {
744
        return $this->IsSent() || $this->Status == 'Processing';
745
    }
746
747
    /**
748
     * Return whether this Order has been paid for (Status == Paid)
749
     * or Status == Processing, where it's been paid for, but is
750
     * currently in a processing state.
751
     *
752
     * @return boolean
753
     */
754
    public function IsPaid()
755
    {
756
        return (boolean)$this->Paid || $this->Status == 'Paid';
757
    }
758
759
    public function IsCart()
760
    {
761
        return $this->Status == 'Cart';
762
    }
763
764
    /**
765
     * Create a unique reference identifier string for this order.
766
     */
767
    public function generateReference()
768
    {
769
        $reference = str_pad($this->ID, self::$reference_id_padding, '0', STR_PAD_LEFT);
770
771
        $this->extend('generateReference', $reference);
772
773
        $candidate = $reference;
774
        //prevent generating references that are the same
775
        $count = 0;
776
        while (Order::get()->filter('Reference', $candidate)->count() > 0) {
777
            $count++;
778
            $candidate = $reference . '' . $count;
779
        }
780
        $this->Reference = $candidate;
781
    }
782
783
    /**
784
     * Get the reference for this order, or fall back to order ID.
785
     */
786
    public function getReference()
787
    {
788
        return $this->getField('Reference') ? $this->getField('Reference') : $this->ID;
789
    }
790
791
    /**
792
     * Force creating an order reference
793
     */
794
    protected function onBeforeWrite()
795
    {
796
        parent::onBeforeWrite();
797
        if (!$this->getField('Reference') && in_array($this->Status, self::$placed_status)) {
798
            $this->generateReference();
799
        }
800
801
        // perform status transition
802
        if ($this->isInDB() && $this->isChanged('Status')) {
803
            $this->statusTransition(
804
                empty($this->original['Status']) ? 'Cart' : $this->original['Status'],
805
                $this->Status
806
            );
807
        }
808
809
        // While the order is unfinished/cart, always store the current locale with the order.
810
        // We do this everytime an order is saved, because the user might change locale (language-switch).
811
        if ($this->Status == 'Cart') {
0 ignored issues
show
introduced by Mark Guinn
The condition $this->Status == 'Cart' is always false.
Loading history...
812
            $this->Locale = ShopTools::get_current_locale();
813
        }
814
    }
815
816
    /**
817
     * Called from @see onBeforeWrite whenever status changes
818
     *
819
     * @param string $fromStatus status to transition away from
820
     * @param string $toStatus   target status
821
     */
822
    protected function statusTransition($fromStatus, $toStatus)
823
    {
824
        // Add extension hook to react to order status transitions.
825
        $this->extend('onStatusChange', $fromStatus, $toStatus);
826
827
        if ($toStatus == 'Paid' && !$this->Paid) {
828
            $this->Paid = DBDatetime::now()->Rfc2822();
0 ignored issues
show
Documentation Bug introduced by Sam Minnée
It seems like SilverStripe\ORM\FieldTy...etime::now()->Rfc2822() of type string is incompatible with the declared type SilverStripe\ORM\FieldType\DBDatetime of property $Paid.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
829
            foreach ($this->Items() as $item) {
830
                $item->onPayment();
831
            }
832
            //all payment is settled
833
            $this->extend('onPaid');
834
835
            if (!$this->ReceiptSent) {
836
                OrderEmailNotifier::create($this)->sendReceipt();
837
                $this->ReceiptSent = DBDatetime::now()->Rfc2822();
0 ignored issues
show
Documentation Bug introduced by Sam Minnée
It seems like SilverStripe\ORM\FieldTy...etime::now()->Rfc2822() of type string is incompatible with the declared type SilverStripe\ORM\FieldType\DBDatetime of property $ReceiptSent.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
838
            }
839
        }
840
841
        $logStatus = $this->config()->log_status;
842
        if (!empty($logStatus) && in_array($toStatus, $logStatus)) {
843
            $this->flagOrderStatusWrite = $fromStatus != $toStatus;
844
        }
845
    }
846
847
    /**
848
     * delete attributes, statuslogs, and payments
849
     */
850
    protected function onBeforeDelete()
851
    {
852
        foreach ($this->Items() as $item) {
853
            $item->delete();
854
        }
855
856
        foreach ($this->Modifiers() as $modifier) {
857
            $modifier->delete();
858
        }
859
860
        foreach ($this->OrderStatusLogs() as $logEntry) {
861
            $logEntry->delete();
862
        }
863
864
        // just remove the payment relations…
865
        // that way payment objects still persist (they might be relevant for book-keeping?)
866
        $this->Payments()->removeAll();
0 ignored issues
show
Bug introduced by Mark Guinn
The method Payments() does not exist on SilverShop\Model\Order. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

866
        $this->/** @scrutinizer ignore-call */ 
867
               Payments()->removeAll();
Loading history...
867
868
        parent::onBeforeDelete();
869
    }
870
871
    public function onAfterWrite()
872
    {
873
        parent::onAfterWrite();
874
875
        //create an OrderStatusLog
876
        if ($this->flagOrderStatusWrite) {
877
            $this->flagOrderStatusWrite = false;
878
            $log = OrderStatusLog::create();
879
880
            // populate OrderStatusLog
881
            $log->Title = _t(
882
                'SilverShop\ShopEmail.StatusChanged',
883
                'Status for order #{OrderNo} changed to "{OrderStatus}"',
884
                '',
885
                ['OrderNo' => $this->Reference, 'OrderStatus' => $this->getStatusI18N()]
886
            );
887
            $log->Note = _t('SilverShop\ShopEmail.StatusChange' . $this->Status . 'Note', $this->Status . 'Note');
888
            $log->OrderID = $this->ID;
889
            OrderEmailNotifier::create($this)->sendStatusChange($log->Title, $log->Note);
890
            $log->SentToCustomer = true;
891
            $this->extend('updateOrderStatusLog', $log);
892
            $log->write();
893
        }
894
    }
895
896
    public function debug()
897
    {
898
        if (Director::is_cli()) {
899
            // Temporarily disabled.
900
            // TODO: Reactivate when the following issue got fixed: https://github.com/silverstripe/silverstripe-framework/issues/7827
901
            return '';
902
        }
903
904
        $val = "<div class='order'><h1>" . static::class . "</h1>\n<ul>\n";
905
        if ($this->record) {
0 ignored issues
show
Bug Best Practice introduced by Mark Guinn
The expression $this->record 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...
906
            foreach ($this->record as $fieldName => $fieldVal) {
907
                $val .= "\t<li>$fieldName: " . Debug::text($fieldVal) . "</li>\n";
908
            }
909
        }
910
        $val .= "</ul>\n";
911
        $val .= "<div class='items'><h2>Items</h2>";
912
        if ($items = $this->Items()) {
0 ignored issues
show
Unused Code introduced by Mark Guinn
The assignment to $items is dead and can be removed.
Loading history...
913
            $val .= $this->Items()->debug();
914
        }
915
        $val .= "</div><div class='modifiers'><h2>Modifiers</h2>";
916
        if ($modifiers = $this->Modifiers()) {
917
            $val .= $modifiers->debug();
918
        }
919
        $val .= "</div></div>";
920
921
        return $val;
922
    }
923
924
    /**
925
     * Provide i18n entities for the order class
926
     *
927
     * @return array
928
     */
929
    public function provideI18nEntities()
930
    {
931
        $entities = parent::provideI18nEntities();
932
933
        // collect all the payment status values
934
        foreach ($this->dbObject('Status')->enumValues() as $value) {
935
            $key = strtoupper($value);
936
            $entities[__CLASS__ . ".STATUS_$key"] = array(
937
                $value,
938
                "Translation of the order status '$value'",
939
            );
940
        }
941
942
        return $entities;
943
    }
944
}
945