Order::getStatusI18N()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

500
            $this->GrandTotal() - ($includeAuthorized ? $this->/** @scrutinizer ignore-call */ TotalPaidOrAuthorized() : $this->TotalPaid()),
Loading history...
Bug introduced by
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

500
            $this->GrandTotal() - ($includeAuthorized ? $this->TotalPaidOrAuthorized() : $this->/** @scrutinizer ignore-call */ TotalPaid()),
Loading history...
501
            self::config()->rounding_precision
502
        );
503
    }
504
505
    /**
506
     * Get the order status. This will return a localized value if available.
507
     *
508
     * @return string the payment status
509
     */
510
    public function getStatusI18N()
511
    {
512
        return _t(__CLASS__ . '.STATUS_' . strtoupper($this->Status), $this->Status);
513
    }
514
515
    /**
516
     * Get the link for finishing order processing.
517
     */
518
    public function Link()
519
    {
520
        $link = CheckoutPage::find_link(false, 'order', $this->ID);
521
        
522
        if (Security::getCurrentUser()) {
523
            $link = Controller::join_links(AccountPage::find_link(), 'order', $this->ID);
524
        }
525
526
        $this->extend('updateLink', $link);
527
528
        return $link;
529
    }
530
531
    /**
532
     * Returns TRUE if the order can be cancelled
533
     * PRECONDITION: Order is in the DB.
534
     *
535
     * @return boolean
536
     */
537
    public function canCancel($member = null)
538
    {
539
        $extended = $this->extendedCan(__FUNCTION__, $member);
540
        if ($extended !== null) {
541
            return $extended;
542
        }
543
544
        switch ($this->Status) {
545
            case 'Unpaid' :
546
            return self::config()->cancel_before_payment;
547
            case 'Paid' :
548
            return self::config()->cancel_before_processing;
549
            case 'Processing' :
550
            return self::config()->cancel_before_sending;
551
            case 'Sent' :
552
            case 'Complete' :
553
            return self::config()->cancel_after_sending;
554
        }
555
        return false;
556
    }
557
558
    /**
559
     * Check if an order can be paid for.
560
     *
561
     * @return boolean
562
     */
563
    public function canPay($member = null)
564
    {
565
        $extended = $this->extendedCan(__FUNCTION__, $member);
566
        if ($extended !== null) {
567
            return $extended;
568
        }
569
570
        if (!in_array($this->Status, self::config()->payable_status)) {
571
            return false;
572
        }
573
        if ($this->TotalOutstanding(true) > 0 && empty($this->Paid)) {
574
            return true;
575
        }
576
        return false;
577
    }
578
579
    /**
580
     * Prevent deleting orders.
581
     *
582
     * @return boolean
583
     */
584
    public function canDelete($member = null)
585
    {
586
        $extended = $this->extendedCan(__FUNCTION__, $member);
587
        if ($extended !== null) {
588
            return $extended;
589
        }
590
591
        return false;
592
    }
593
594
    /**
595
     * Check if an order can be viewed.
596
     *
597
     * @return boolean
598
     */
599
    public function canView($member = null)
600
    {
601
        $extended = $this->extendedCan(__FUNCTION__, $member);
602
        if ($extended !== null) {
603
            return $extended;
604
        }
605
606
        return true;
607
    }
608
609
    /**
610
     * Check if an order can be edited.
611
     *
612
     * @return boolean
613
     */
614
    public function canEdit($member = null)
615
    {
616
        $extended = $this->extendedCan(__FUNCTION__, $member);
617
        if ($extended !== null) {
618
            return $extended;
619
        }
620
621
        return true;
622
    }
623
624
    /**
625
     * Prevent standard creation of orders.
626
     *
627
     * @return boolean
628
     */
629
    public function canCreate($member = null, $context = [])
630
    {
631
        $extended = $this->extendedCan(__FUNCTION__, $member, $context);
632
        if ($extended !== null) {
633
            return $extended;
634
        }
635
636
        return false;
637
    }
638
639
    /**
640
     * Return the currency of this order.
641
     * Note: this is a fixed value across the entire site.
642
     *
643
     * @return string
644
     */
645
    public function Currency()
646
    {
647
        return ShopConfigExtension::get_site_currency();
648
    }
649
650
    /**
651
     * Get the latest email for this order.z
652
     */
653
    public function getLatestEmail()
654
    {
655
        if ($this->hasMethod('overrideLatestEmail')) {
656
            return $this->overrideLatestEmail();
0 ignored issues
show
Bug introduced by
The method overrideLatestEmail() 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

656
            return $this->/** @scrutinizer ignore-call */ overrideLatestEmail();
Loading history...
657
        }
658
        if ($this->MemberID && ($this->Member()->LastEdited > $this->LastEdited || !$this->Email)) {
0 ignored issues
show
Bug introduced by
The property LastEdited does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
659
            return $this->Member()->Email;
0 ignored issues
show
Bug introduced by
The property Email does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
660
        }
661
        return $this->getField('Email');
662
    }
663
664
    /**
665
     * Gets the name of the customer.
666
     */
667
    public function getName()
668
    {
669
        $firstname = $this->FirstName ? $this->FirstName : $this->Member()->FirstName;
0 ignored issues
show
Bug introduced by
The property FirstName does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
670
        $surname = $this->FirstName ? $this->Surname : $this->Member()->Surname;
0 ignored issues
show
Bug introduced by
The property Surname does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
671
        return implode(' ', array_filter([$firstname, $surname]));
672
    }
673
674
    public function getTitle()
675
    {
676
        return $this->Reference . ' - ' . $this->dbObject('Placed')->Nice();
677
    }
678
679
    /**
680
     * Get shipping address, or member default shipping address.
681
     */
682
    public function getShippingAddress()
683
    {
684
        return $this->getAddress('Shipping');
685
    }
686
687
    /**
688
     * Get billing address, if marked to use seperate address, otherwise use shipping address,
689
     * or the member default billing address.
690
     */
691
    public function getBillingAddress()
692
    {
693
        if (!$this->SeparateBillingAddress && $this->ShippingAddressID === $this->BillingAddressID) {
694
            return $this->getShippingAddress();
695
        } else {
696
            return $this->getAddress('Billing');
697
        }
698
    }
699
700
    /**
701
     * @param string $type - Billing or Shipping
702
     * @return Address
703
     * @throws \Exception
704
     */
705
    protected function getAddress($type)
706
    {
707
        $address = $this->getComponent($type . 'Address');
708
709
        if (!$address || !$address->exists() && $this->Member()) {
0 ignored issues
show
introduced by
$address is of type SilverStripe\ORM\DataObject, thus it always evaluated to true.
Loading history...
710
            $address = $this->Member()->{"Default${type}Address"}();
711
        }
712
713
        if (empty($address->Surname) && empty($address->FirstName)) {
714
            if ($member = $this->Member()) {
715
                // If there's a member object, use information from the Member.
716
                // The information from Order should have precendence if set though!
717
                $address->FirstName = $this->FirstName ?: $member->FirstName;
0 ignored issues
show
Bug introduced by
The property FirstName does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
718
                $address->Surname = $this->Surname ?: $member->Surname;
0 ignored issues
show
Bug introduced by
The property Surname does not seem to exist on SilverShop\Extension\MemberExtension.
Loading history...
719
            } else {
720
                $address->FirstName = $this->FirstName;
721
                $address->Surname = $this->Surname;
722
            }
723
        }
724
725
        return $address;
726
    }
727
728
    /**
729
     * Check if the two addresses saved differ.
730
     *
731
     * @return boolean
732
     */
733
    public function getAddressesDiffer()
734
    {
735
        return $this->SeparateBillingAddress || $this->ShippingAddressID !== $this->BillingAddressID;
736
    }
737
738
    /**
739
     * Has this order been sent to the customer?
740
     * (at "Sent" status).
741
     *
742
     * @return boolean
743
     */
744
    public function IsSent()
745
    {
746
        return $this->Status == 'Sent';
747
    }
748
749
    /**
750
     * Is this order currently being processed?
751
     * (at "Sent" OR "Processing" status).
752
     *
753
     * @return boolean
754
     */
755
    public function IsProcessing()
756
    {
757
        return $this->IsSent() || $this->Status == 'Processing';
758
    }
759
760
    /**
761
     * Return whether this Order has been paid for (Status == Paid)
762
     * or Status == Processing, where it's been paid for, but is
763
     * currently in a processing state.
764
     *
765
     * @return boolean
766
     */
767
    public function IsPaid()
768
    {
769
        return (boolean)$this->Paid || $this->Status == 'Paid';
770
    }
771
772
    public function IsCart()
773
    {
774
        return $this->Status == 'Cart';
775
    }
776
777
    /**
778
     * Create a unique reference identifier string for this order.
779
     */
780
    public function generateReference()
781
    {
782
        $reference = str_pad($this->ID, self::$reference_id_padding, '0', STR_PAD_LEFT);
783
784
        $this->extend('generateReference', $reference);
785
786
        $candidate = $reference;
787
        //prevent generating references that are the same
788
        $count = 0;
789
        while (Order::get()->filter('Reference', $candidate)->count() > 0) {
790
            $count++;
791
            $candidate = $reference . '' . $count;
792
        }
793
        $this->Reference = $candidate;
794
    }
795
796
    /**
797
     * Get the reference for this order, or fall back to order ID.
798
     */
799
    public function getReference()
800
    {
801
        return $this->getField('Reference') ? $this->getField('Reference') : $this->ID;
802
    }
803
804
    /**
805
     * Force creating an order reference
806
     */
807
    protected function onBeforeWrite()
808
    {
809
        parent::onBeforeWrite();
810
        if (!$this->getField('Reference') && in_array($this->Status, self::$placed_status)) {
811
            $this->generateReference();
812
        }
813
814
        // perform status transition
815
        if ($this->isInDB() && $this->isChanged('Status')) {
816
            $this->statusTransition(
817
                empty($this->original['Status']) ? 'Cart' : $this->original['Status'],
818
                $this->Status
819
            );
820
        }
821
822
        // While the order is unfinished/cart, always store the current locale with the order.
823
        // We do this everytime an order is saved, because the user might change locale (language-switch).
824
        if ($this->Status == 'Cart') {
0 ignored issues
show
introduced by
The condition $this->Status == 'Cart' is always false.
Loading history...
825
            $this->Locale = ShopTools::get_current_locale();
826
        }
827
    }
828
829
    /**
830
     * Called from @see onBeforeWrite whenever status changes
831
     *
832
     * @param string $fromStatus status to transition away from
833
     * @param string $toStatus   target status
834
     */
835
    protected function statusTransition($fromStatus, $toStatus)
836
    {
837
        // Add extension hook to react to order status transitions.
838
        $this->extend('onStatusChange', $fromStatus, $toStatus);
839
840
        if ($toStatus == 'Paid' && !$this->Paid) {
841
            $this->setField('Paid', DBDatetime::now()->Rfc2822());
842
            foreach ($this->Items() as $item) {
843
                $item->onPayment();
844
            }
845
            //all payment is settled
846
            $this->extend('onPaid');
847
848
            if (!$this->ReceiptSent) {
849
                OrderEmailNotifier::create($this)->sendReceipt();
850
                $this->setField('ReceiptSent', DBDatetime::now()->Rfc2822());
851
            }
852
        }
853
854
        $logStatus = $this->config()->log_status;
855
        if (!empty($logStatus) && in_array($toStatus, $logStatus)) {
856
            $this->flagOrderStatusWrite = $fromStatus != $toStatus;
857
        }
858
    }
859
860
    /**
861
     * delete attributes, statuslogs, and payments
862
     */
863
    protected function onBeforeDelete()
864
    {
865
        foreach ($this->Items() as $item) {
866
            $item->delete();
867
        }
868
869
        foreach ($this->Modifiers() as $modifier) {
870
            $modifier->delete();
871
        }
872
873
        foreach ($this->OrderStatusLogs() as $logEntry) {
874
            $logEntry->delete();
875
        }
876
877
        // just remove the payment relations…
878
        // that way payment objects still persist (they might be relevant for book-keeping?)
879
        $this->Payments()->removeAll();
0 ignored issues
show
Bug introduced by
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

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