Completed
Push — master ( 2eee10...777fce )
by Nicolaas
03:39
created

code/model/process/OrderStep.php (10 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * @description: see OrderStep.md
5
 *
6
 *
7
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
8
 * @package: ecommerce
9
 * @sub-package: model
10
 * @inspiration: Silverstripe Ltd, Jeremy
11
 **/
12
class OrderStep extends DataObject implements EditableEcommerceObject
13
{
14
15
16
    /**
17
     * standard SS variable.
18
     *
19
     * @return array
20
     */
21
    private static $db = array(
22
        'Name' => 'Varchar(50)',
23
        'Code' => 'Varchar(50)',
24
        'Description' => 'Text',
25
        'EmailSubject' => 'Varchar(200)',
26
        'CustomerMessage' => 'HTMLText',
27
        //customer privileges
28
        'CustomerCanEdit' => 'Boolean',
29
        'CustomerCanCancel' => 'Boolean',
30
        'CustomerCanPay' => 'Boolean',
31
        //What to show the customer...
32
        'ShowAsUncompletedOrder' => 'Boolean',
33
        'ShowAsInProcessOrder' => 'Boolean',
34
        'ShowAsCompletedOrder' => 'Boolean',
35
        'HideStepFromCustomer' => 'Boolean',
36
        //sorting index
37
        'Sort' => 'Int',
38
        'DeferTimeInSeconds' => 'Int',
39
        'DeferFromSubmitTime' => 'Boolean'
40
    );
41
42
43
44
    /**
45
     * standard SS variable.
46
     *
47
     * @return array
48
     */
49
    private static $indexes = array(
50
        'Code' => true,
51
        'Sort' => true,
52
    );
53
54
    /**
55
     * standard SS variable.
56
     *
57
     * @return array
58
     */
59
    private static $has_many = array(
60
        'Orders' => 'Order',
61
        'OrderEmailRecords' => 'OrderEmailRecord',
62
    );
63
64
    /**
65
     * standard SS variable.
66
     *
67
     * @return array
68
     */
69
    private static $field_labels = array(
70
        'Sort' => 'Sorting Index',
71
        'CustomerCanEdit' => 'Customer can edit order',
72
        'CustomerCanPay' => 'Customer can pay order',
73
        'CustomerCanCancel' => 'Customer can cancel order',
74
    );
75
76
    /**
77
     * standard SS variable.
78
     *
79
     * @return array
80
     */
81
    private static $summary_fields = array(
82
        'NameAndDescription' => 'Step',
83
        'ShowAsSummary' => 'Phase',
84
    );
85
86
    /**
87
     * standard SS variable.
88
     *
89
     * @return array
90
     */
91
    private static $casting = array(
92
        'Title' => 'Varchar',
93
        'CustomerCanEditNice' => 'Varchar',
94
        'CustomerCanPayNice' => 'Varchar',
95
        'CustomerCanCancelNice' => 'Varchar',
96
        'ShowAsUncompletedOrderNice' => 'Varchar',
97
        'ShowAsInProcessOrderNice' => 'Varchar',
98
        'ShowAsCompletedOrderNice' => 'Varchar',
99
        'HideStepFromCustomerNice' => 'Varchar',
100
        'HasCustomerMessageNice' => 'Varchar',
101
        'ShowAsSummary' => 'HTMLText',
102
        'NameAndDescription' => 'HTMLText'
103
    );
104
105
    /**
106
     * standard SS variable.
107
     *
108
     * @return array
109
     */
110
    private static $searchable_fields = array(
111
        'Name' => array(
112
            'title' => 'Name',
113
            'filter' => 'PartialMatchFilter',
114
        ),
115
        'Code' => array(
116
            'title' => 'Code',
117
            'filter' => 'PartialMatchFilter',
118
        ),
119
    );
120
121
122
    /**
123
     * casted variable.
124
     *
125
     * @return string
126
     */
127
    public function Title()
128
    {
129
        return $this->getTitle();
130
    }
131
    public function getTitle()
132
    {
133
        return $this->Name;
134
    }
135
136
    /**
137
     * casted variable.
138
     *
139
     * @return string
140
     */
141
    public function CustomerCanEditNice()
142
    {
143
        return $this->getCustomerCanEditNice();
144
    }
145
    public function getCustomerCanEditNice()
146
    {
147
        if ($this->CustomerCanEdit) {
148
            return _t('OrderStep.YES', 'Yes');
149
        }
150
151
        return _t('OrderStep.NO', 'No');
152
    }
153
154
    /**
155
     * casted variable.
156
     *
157
     * @return string
158
     */
159
    public function CustomerCanPayNice()
160
    {
161
        return $this->getCustomerCanPayNice();
162
    }
163
    public function getCustomerCanPayNice()
164
    {
165
        if ($this->CustomerCanPay) {
166
            return _t('OrderStep.YES', 'Yes');
167
        }
168
169
        return _t('OrderStep.NO', 'No');
170
    }
171
172
    /**
173
     * casted variable.
174
     *
175
     * @return string
176
     */
177
    public function CustomerCanCancelNice()
178
    {
179
        return $this->getCustomerCanCancelNice();
180
    }
181
    public function getCustomerCanCancelNice()
182
    {
183
        if ($this->CustomerCanCancel) {
184
            return _t('OrderStep.YES', 'Yes');
185
        }
186
187
        return _t('OrderStep.NO', 'No');
188
    }
189
190
    public function ShowAsUncompletedOrderNice()
191
    {
192
        return $this->getShowAsUncompletedOrderNice();
193
    }
194
    public function getShowAsUncompletedOrderNice()
195
    {
196
        if ($this->ShowAsUncompletedOrder) {
197
            return _t('OrderStep.YES', 'Yes');
198
        }
199
200
        return _t('OrderStep.NO', 'No');
201
    }
202
203
    /**
204
     * casted variable.
205
     *
206
     * @return string
207
     */
208
    public function ShowAsInProcessOrderNice()
209
    {
210
        return $this->getShowAsInProcessOrderNice();
211
    }
212
    public function getShowAsInProcessOrderNice()
213
    {
214
        if ($this->ShowAsInProcessOrder) {
215
            return _t('OrderStep.YES', 'Yes');
216
        }
217
218
        return _t('OrderStep.NO', 'No');
219
    }
220
221
    /**
222
     * casted variable.
223
     *
224
     * @return string
225
     */
226
    public function ShowAsCompletedOrderNice()
227
    {
228
        return $this->getShowAsCompletedOrderNice();
229
    }
230
    public function getShowAsCompletedOrderNice()
231
    {
232
        if ($this->ShowAsCompletedOrder) {
233
            return _t('OrderStep.YES', 'Yes');
234
        }
235
236
        return _t('OrderStep.NO', 'No');
237
    }
238
239
    /**
240
     * do not show in steps at all.
241
     * @return boolean
242
     */
243
    public function HideFromEveryone()
244
    {
245
        return false;
246
    }
247
248
    /**
249
     * casted variable.
250
     *
251
     * @return string
252
     */
253
    public function HideStepFromCustomerNice()
254
    {
255
        return $this->getHideStepFromCustomerNice();
256
    }
257
258
    public function getHideStepFromCustomerNice()
259
    {
260
        if ($this->HideStepFromCustomer) {
261
            return _t('OrderStep.YES', 'Yes');
262
        }
263
264
        return _t('OrderStep.NO', 'No');
265
    }
266
267
    /**
268
     * standard SS variable.
269
     *
270
     * @return string
271
     */
272
    private static $singular_name = 'Order Step';
273
    public function i18n_singular_name()
274
    {
275
        return _t('OrderStep.ORDERSTEP', 'Order Step');
276
    }
277
278
    /**
279
     * standard SS variable.
280
     *
281
     * @return string
282
     */
283
    private static $plural_name = 'Order Steps';
284
    public function i18n_plural_name()
285
    {
286
        return _t('OrderStep.ORDERSTEPS', 'Order Steps');
287
    }
288
289
    /**
290
     * Standard SS variable.
291
     *
292
     * @var string
293
     */
294
    private static $description = 'A step that any order goes through.';
295
296
    /**
297
     * SUPER IMPORTANT TO KEEP ORDER!
298
     * standard SS variable.
299
     *
300
     * @return string
301
     */
302
    private static $default_sort = '"Sort" ASC';
303
304
    /**
305
     * returns all the order steps
306
     * that the admin should / can edit....
307
     *
308
     * @return DataList
309
     */
310
    public static function admin_manageable_steps()
311
    {
312
        $lastStep = OrderStep::get()->Last();
313
314
        return OrderStep::get()->filter(array('ShowAsInProcessOrder' => 1))->exclude(array('ID' => $lastStep->ID));
315
    }
316
    /**
317
     * returns all the order steps
318
     * that the admin should / can edit....
319
     *
320
     * @return DataList
321
     */
322
    public static function non_admin_manageable_steps()
323
    {
324
        $lastStep = OrderStep::get()->Last();
325
326
        return OrderStep::get()->filterAny(array('ShowAsInProcessOrder' => 0, 'ID' => $lastStep->ID));
327
    }
328
329
    /**
330
     * return StatusIDs (orderstep IDs) from orders that are bad....
331
     * (basically StatusID values that do not exist)
332
     *
333
     * @return array
334
     */
335
    public static function bad_order_step_ids()
336
    {
337
        $badorderStatus = Order::get()
338
            ->leftJoin('OrderStep', '"OrderStep"."ID" = "Order"."StatusID"')
339
            ->where('"OrderStep"."ID" IS NULL AND "StatusID" > 0')
340
            ->column('StatusID');
341
        if (is_array($badorderStatus)) {
342
            return array_unique(array_values($badorderStatus));
343
        } else {
344
            return array(-1);
345
        }
346
    }
347
348
    /**
349
     * turns code into ID.
350
     *
351
     * @param string $code
352
     * @param int
353
     */
354
    public static function get_status_id_from_code($code)
355
    {
356
        $otherStatus = OrderStep::get()
357
            ->filter(array('Code' => $code))
358
            ->First();
359
        if ($otherStatus) {
360
            return $otherStatus->ID;
361
        }
362
363
        return 0;
364
    }
365
366
    /**
367
     *@return array
368
     **/
369
    public static function get_codes_for_order_steps_to_include()
370
    {
371
        $newArray = array();
372
        $array = EcommerceConfig::get('OrderStep', 'order_steps_to_include');
373
        if (is_array($array) && count($array)) {
374
            foreach ($array as $className) {
375
                $code = singleton($className)->getMyCode();
376
                $newArray[$className] = strtoupper($code);
377
            }
378
        }
379
380
        return $newArray;
381
    }
382
383
    /**
384
     * returns a list of ordersteps that have not been created yet.
385
     *
386
     * @return array
387
     **/
388
    public static function get_not_created_codes_for_order_steps_to_include()
389
    {
390
        $array = EcommerceConfig::get('OrderStep', 'order_steps_to_include');
391
        if (is_array($array) && count($array)) {
392
            foreach ($array as $className) {
393
                $obj = $className::get()->First();
394
                if ($obj) {
395
                    unset($array[$className]);
396
                }
397
            }
398
        }
399
400
        return $array;
401
    }
402
403
    /**
404
     *@return string
405
     **/
406
    public function getMyCode()
407
    {
408
        $array = Config::inst()->get($this->ClassName, 'defaults', Config::UNINHERITED);
409
        if (!isset($array['Code'])) {
410
            user_error($this->class.' does not have a default code specified');
411
        }
412
413
        return $array['Code'];
414
    }
415
416
    /**
417
     * IMPORTANT:: MUST HAVE Code must be defined!!!
418
     * standard SS variable.
419
     *
420
     * @return array
421
     */
422
    private static $defaults = array(
423
        'CustomerCanEdit' => 0,
424
        'CustomerCanCancel' => 0,
425
        'CustomerCanPay' => 1,
426
        'ShowAsUncompletedOrder' => 0,
427
        'ShowAsInProcessOrder' => 0,
428
        'ShowAsCompletedOrder' => 0,
429
        'Code' => 'ORDERSTEP',
430
    );
431
432
    /**
433
     * standard SS method.
434
     */
435
    public function populateDefaults()
436
    {
437
        parent::populateDefaults();
438
        $this->Description = $this->myDescription();
439
    }
440
441
    /**
442
     *@return FieldList
443
     **/
444
    public function getCMSFields()
445
    {
446
        $fields = parent::getCMSFields();
447
        //replacing
448
        if ($this->canBeDefered()) {
449
            if ($this->DeferTimeInSeconds) {
450
                $fields->addFieldToTab(
451
                    'Root.Queue',
452
                    HeaderField::create(
453
                        'WhenWillThisRun',
454
                        $this->humanReadeableDeferTimeInSeconds()
455
                    )
456
                );
457
            }
458
            $fields->addFieldToTab(
459
                'Root.Queue',
460
                $deferTimeInSecondsField = TextField::create(
461
                    'DeferTimeInSeconds',
462
                    _t('OrderStep.DeferTimeInSeconds', 'Seconds in queue')
463
                )
464
                ->setRightTitle(
465
                    _t(
466
                        'OrderStep.TIME_EXPLANATION',
467
                        '86,400 seconds is one day ...
468
                        <br />To make it easier, you can also enter things like <em>1 week</em>, <em>3 hours</em>, or <em>7 minutes</em>.
469
                        <br />Non-second entries will automatically be converted to seconds.'
470
                    )
471
                )
472
            );
473
            if ($this->DeferTimeInSeconds) {
474
                $fields->addFieldToTab(
475
                    'Root.Queue',
476
                    $deferTimeInSecondsField = CheckboxField::create(
477
                        'DeferFromSubmitTime',
478
                        _t('OrderStep.DeferFromSubmitTime', 'Calculated from submit time?')
479
                    )
480
                    ->setDescription(
481
                        _t(
482
                            'OrderStep.DeferFromSubmitTime_HELP',
483
                            'The time in the queue can be calculated from the moment the current orderstep starts or from the moment the order was submitted (in this case, check the box above) '
484
                            )
485
                        )
486
                );
487
            }
488
        }
489
        if ($this->hasCustomerMessage()) {
490
            $rightTitle = _t(
491
                'OrderStep.EXPLAIN_ORDER_NUMBER_IN_SUBJECT',
492
                'You can use [OrderNumber] as a tag that will be replaced with the actual Order Number.'
493
            );
494
            $fields->addFieldToTab(
495
                'Root.CustomerMessage',
496
                TextField::create('EmailSubject', _t('OrderStep.EMAILSUBJECT', 'Email Subject'))
497
                    ->setRightTitle($rightTitle)
498
            );
499
            if ($testEmailLink = $this->testEmailLink()) {
500
                $fields->addFieldToTab(
501
                    'Root.CustomerMessage',
502
                    new LiteralField(
503
                        'testEmailLink',
504
                        '<h3>
505
                            <a href="'.$testEmailLink.'" data-popup="true" target"_blank" onclick="emailPrompt(this, event);">
506
                                '._t('OrderStep.VIEW_EMAIL_EXAMPLE', 'View email example in browser').'
507
                            </a>
508
                        </h3>
509
                        <script language="javascript">
510
                            function emailPrompt(caller, event) {
511
                                event.preventDefault();
512
                                var href = jQuery(caller).attr("href");
513
                                var email = prompt("Enter an email address to receive a copy of this example in your inbox, leave blank to view in the browser");
514
                                if (email) {
515
                                    href += "&send=" + email;
516
                                }
517
                                window.open(href);
518
                            };
519
                        </script>'
520
                    )
521
                );
522
            }
523
524
            $fields->addFieldToTab('Root.CustomerMessage', $htmlEditorField = new HTMLEditorField('CustomerMessage', _t('OrderStep.CUSTOMERMESSAGE', 'Customer Message (if any)')));
525
            $htmlEditorField->setRows(3);
526
527
        } else {
528
            $fields->removeFieldFromTab('Root', 'OrderEmailRecords');
529
            $fields->removeFieldFromTab('Root.Main', 'EmailSubject');
530
            $fields->removeFieldFromTab('Root.Main', 'CustomerMessage');
531
        }
532
        //adding
533
        if (!$this->exists() || !$this->isDefaultStatusOption()) {
534
            $fields->removeFieldFromTab('Root.Main', 'Code');
535
            $fields->addFieldToTab('Root.Main', new DropdownField('ClassName', _t('OrderStep.TYPE', 'Type'), self::get_not_created_codes_for_order_steps_to_include()), 'Name');
536
        }
537
        if ($this->isDefaultStatusOption()) {
538
            $fields->replaceField('Code', $fields->dataFieldByName('Code')->performReadonlyTransformation());
539
        }
540
        //headers
541
        $fields->addFieldToTab('Root.Main', new HeaderField('WARNING1', _t('OrderStep.CAREFUL', 'CAREFUL! please edit details below with care'), 2), 'Description');
542
        $fields->addFieldToTab('Root.Main', new HeaderField('WARNING2', _t('OrderStep.CUSTOMERCANCHANGE', 'What can be changed during this step?'), 3), 'CustomerCanEdit');
543
        $fields->addFieldToTab('Root.Main', new HeaderField('WARNING5', _t('OrderStep.ORDERGROUPS', 'Order groups for customer?'), 3), 'ShowAsUncompletedOrder');
544
        $fields->addFieldToTab('Root.Main', new HeaderField('HideStepFromCustomerHeader', _t('OrderStep.HIDE_STEP_FROM_CUSTOMER_HEADER', 'Customer Interaction'), 3), 'HideStepFromCustomer');
545
        //final cleanup
546
        $fields->removeFieldFromTab('Root.Main', 'Sort');
547
        $fields->addFieldToTab('Root.Main', new TextareaField('Description', _t('OrderStep.DESCRIPTION', 'Explanation for internal use only')), 'WARNING1');
548
549
        return $fields;
550
    }
551
552
    /**
553
     * link to edit the record.
554
     *
555
     * @param string | Null $action - e.g. edit
556
     *
557
     * @return string
558
     */
559
    public function CMSEditLink($action = null)
560
    {
561
        return Controller::join_links(
562
            Director::baseURL(),
563
            '/admin/shop/'.$this->ClassName.'/EditForm/field/'.$this->ClassName.'/item/'.$this->ID.'/',
564
            $action
565
        );
566
    }
567
568
    /**
569
     * tells the order to display itself with an alternative display page.
570
     * in that way, orders can be displayed differently for certain steps
571
     * for example, in a print step, the order can be displayed in a
572
     * PRINT ONLY format.
573
     *
574
     * When the method return null, the order is displayed using the standard display page
575
     *
576
     * @see Order::DisplayPage
577
     *
578
     * @return null|object (Page)
579
     **/
580
    public function AlternativeDisplayPage()
581
    {
582
        return;
583
    }
584
585
    /**
586
     * Allows the opportunity for the Order Step to add any fields to Order::getCMSFields
587
     * Usually this is added before ActionNextStepManually.
588
     *
589
     * @param FieldList $fields
590
     * @param Order     $order
591
     *
592
     * @return FieldList
593
     **/
594
    public function addOrderStepFields(FieldList $fields, Order $order)
595
    {
596
        return $fields;
597
    }
598
599
    /**
600
     *@return ValidationResult
601
     **/
602
    public function validate()
603
    {
604
        $result = parent::validate();
605
        $anotherOrderStepWithSameNameOrCode = OrderStep::get()
606
            ->filter(
607
                array(
608
                    'Name' => $this->Name,
609
                    'Code' => strtoupper($this->Code),
610
                )
611
            )
612
            ->exclude(array('ID' => intval($this->ID)))
613
            ->First();
614
        if ($anotherOrderStepWithSameNameOrCode) {
615
            $result->error(_t('OrderStep.ORDERSTEPALREADYEXISTS', 'An order status with this name already exists. Please change the name and try again.'));
616
        }
617
618
        return $result;
619
    }
620
621
/**************************************************
622
* moving between statusses...
623
**************************************************/
624
    /**
625
     *initStep:
626
     * makes sure the step is ready to run.... (e.g. check if the order is ready to be emailed as receipt).
627
     * should be able to run this function many times to check if the step is ready.
628
     *
629
     * @see Order::doNextStatus
630
     *
631
     * @param Order object
632
     *
633
     * @return bool - true if the current step is ready to be run...
634
     **/
635
    public function initStep(Order $order)
636
    {
637
        user_error('Please implement the initStep method in a subclass ('.get_class().') of OrderStep', E_USER_WARNING);
638
639
        return true;
640
    }
641
642
    /**
643
     *doStep:
644
     * should only be able to run this function once
645
     * (init stops you from running it twice - in theory....)
646
     * runs the actual step.
647
     *
648
     * @see Order::doNextStatus
649
     *
650
     * @param Order object
651
     *
652
     * @return bool - true if run correctly.
653
     **/
654
    public function doStep(Order $order)
655
    {
656
        user_error('Please implement the initStep method in a subclass ('.get_class().') of OrderStep', E_USER_WARNING);
657
658
        return true;
659
    }
660
661
    /**
662
     * nextStep:
663
     * returns the next step (after it checks if everything is in place for the next step to run...).
664
     *
665
     * @see Order::doNextStatus
666
     *
667
     * @param Order $order
668
     *
669
     * @return OrderStep | Null (next step OrderStep object)
670
     **/
671
    public function nextStep(Order $order)
672
    {
673
        $nextOrderStepObject = OrderStep::get()
674
            ->filter(array('Sort:GreaterThan' => $this->Sort))
675
            ->First();
676
        if ($nextOrderStepObject) {
677
            return $nextOrderStepObject;
678
        }
679
680
        return;
681
    }
682
683
/**************************************************
684
* Boolean checks
685
**************************************************/
686
687
    /**
688
     * Checks if a step has passed (been completed) in comparison to the current step.
689
     *
690
     * @param string $code:       the name of the step to check
691
     * @param bool   $orIsEqualTo if set to true, this method will return TRUE if the step being checked is the current one
692
     *
693
     * @return bool
694
     **/
695
    public function hasPassed($code, $orIsEqualTo = false)
696
    {
697
        $otherStatus = OrderStep::get()
698
            ->filter(array('Code' => $code))
699
            ->First();
700
        if ($otherStatus) {
701
            if ($otherStatus->Sort < $this->Sort) {
702
                return true;
703
            }
704
            if ($orIsEqualTo && $otherStatus->Code == $this->Code) {
705
                return true;
706
            }
707
        } else {
708
            user_error("could not find $code in OrderStep", E_USER_NOTICE);
709
        }
710
711
        return false;
712
    }
713
714
    /**
715
     * @param string $code
716
     *
717
     * @return bool
718
     **/
719
    public function hasPassedOrIsEqualTo($code)
720
    {
721
        return $this->hasPassed($code, true);
722
    }
723
724
    /**
725
     * @param string $code
726
     *
727
     * @return bool
728
     **/
729
    public function hasNotPassed($code)
730
    {
731
        return (bool) !$this->hasPassed($code, true);
732
    }
733
734
    /**
735
     * Opposite of hasPassed.
736
     *
737
     * @param string $code
738
     *
739
     * @return bool
740
     **/
741
    public function isBefore($code)
742
    {
743
        return (bool) $this->hasPassed($code, false) ? false : true;
744
    }
745
746
    /**
747
     *@return bool
748
     **/
749
    protected function isDefaultStatusOption()
750
    {
751
        return in_array($this->Code, self::get_codes_for_order_steps_to_include());
752
    }
753
754
/**************************************************
755
* Email
756
**************************************************/
757
758
    /**
759
     * @var string
760
     */
761
    protected $emailClassName = '';
762
763
    /**
764
     * returns the email class used for emailing the
765
     * customer during a specific step (IF ANY!).
766
     *
767
     * @return string
768
     */
769
    public function getEmailClassName()
770
    {
771
        return $this->emailClassName;
772
    }
773
774
    /**
775
     * return true if done already or mailed successfully now.
776
     *
777
     * @param order         $order
778
     * @param string        $subject
779
     * @param string        $message
780
     * @param bool          $resend
781
     * @param bool | string $adminOnlyOrToEmail you can set to false = send to customer, true: send to admin, or email = send to email
782
     * @param string        $emailClassName
783
     *
784
     * @return boolean;
785
     */
786
    protected function sendEmailForStep(
787
        $order,
788
        $subject,
789
        $message = '',
790
        $resend = false,
791
        $adminOnlyOrToEmail = false,
792
        $emailClassName = ''
793
    ) {
794
        if (!$this->hasBeenSent($order) || $resend) {
795
            if (!$subject) {
796
                $subject = $this->EmailSubject;
797
            }
798
            if (!$emailClassName) {
799
                $emailClassName = $this->getEmailClassName();
800
            }
801
            $adminOnlyOrToEmailIsEmail = $adminOnlyOrToEmail && filter_var($adminOnlyOrToEmail, FILTER_VALIDATE_EMAIL);
802
            if ($this->hasCustomerMessage() || $adminOnlyOrToEmailIsEmail) {
803
                return $order->sendEmail(
804
                    $emailClassName,
805
                    $subject,
806
                    $message,
807
                    $resend,
808
                    $adminOnlyOrToEmail
809
                );
810
            } else {
811
                if (!$emailClassName) {
812
                    $emailClassName = 'Order_ErrorEmail';
813
                }
814
                //looks like we are sending an error, but we are just using this for notification
815
                $message = _t('OrderStep.THISMESSAGENOTSENTTOCUSTOMER', 'NOTE: This message was not sent to the customer.').'<br /><br /><br /><br />'.$message;
816
                $outcome = $order->sendAdminNotification(
817
                    $emailClassName,
818
                    $subject,
819
                    $message,
820
                    $resend
821
                );
822
            }
823
            if ($outcome || Director::isDev()) {
824
                return true;
825
            }
826
827
            return false;
828
        }
829
830
        return true;
831
    }
832
833
    /**
834
     * sets the email class used for emailing the
835
     * customer during a specific step (IF ANY!).
836
     *
837
     * @param string
838
     */
839
    public function setEmailClassName($s)
840
    {
841
        $this->emailClassName = $s;
842
    }
843
844
    /**
845
     * returns a link that can be used to test
846
     * the email being sent during this step
847
     * this method returns NULL if no email
848
     * is being sent OR if there is no suitable Order
849
     * to test with...
850
     *
851
     * @return string
0 ignored issues
show
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
852
     */
853
    protected function testEmailLink()
854
    {
855
        if ($this->getEmailClassName()) {
856
            $order = Order::get()->filter(array('StatusID' => $this->ID))
857
                ->sort('RAND() ASC')
858
                ->first();
859
            if(! $order) {
860
                $order = Order::get()
861
                    ->where('"OrderStep"."Sort" >= '.$this->Sort)
862
                    ->sort('IF("OrderStep"."Sort" > '.$this->Sort.', 0, 1) ASC, "OrderStep"."Sort" ASC, RAND() ASC')
863
                    ->innerJoin('OrderStep', '"OrderStep"."ID" = "Order"."StatusID"')
864
                    ->first();
865
            }
866
            if ($order) {
867
                return OrderConfirmationPage::get_email_link(
868
                    $order->ID,
869
                    $this->getEmailClassName(),
870
                    $actuallySendEmail = false,
871
                    $alternativeOrderStepID = $this->ID
872
                );
873
            }
874
        }
875
    }
876
877
    /**
878
     * Has an email been sent to the customer for this
879
     * order step.
880
     *"-10 days".
881
     *
882
     * @param Order $order
883
     * @param bool  $checkDateOfOrder
884
     *
885
     * @return bool
886
     **/
887
    public function hasBeenSent(Order $order, $checkDateOfOrder = true)
888
    {
889
        //if it has been more than a XXX days since the order was last edited (submitted) then we do not send emails as
890
        //this would be embarrasing.
891
        if ($checkDateOfOrder) {
892
            if ($log = $order->SubmissionLog()) {
893
                $lastEditedValue = $log->LastEdited;
894
            } else {
895
                $lastEditedValue = $order->LastEdited;
896
            }
897
            if ((strtotime($lastEditedValue) < strtotime('-'.EcommerceConfig::get('OrderStep', 'number_of_days_to_send_update_email').' days'))) {
898
                return true;
899
            }
900
        }
901
        $count = OrderEmailRecord::get()
902
            ->Filter(array(
903
                'OrderID' => $order->ID,
904
                'OrderStepID' => $this->ID,
905
                'Result' => 1,
906
            ))
907
            ->count();
908
909
        return $count ? true : false;
910
    }
911
912
    /**
913
     * For some ordersteps this returns true...
914
     *
915
     * @return bool
916
     **/
917
    protected function hasCustomerMessage()
918
    {
919
        return false;
920
    }
921
922
923
    /**
924
     * Formatted answer for "hasCustomerMessage".
925
     *
926
     * @return string
927
     */
928
    public function HasCustomerMessageNice()
929
    {
930
        return $this->getHasCustomerMessageNice();
931
    }
932
    public function getHasCustomerMessageNice()
933
    {
934
        return $this->hasCustomerMessage() ?  _t('OrderStep.YES', 'Yes') :  _t('OrderStep.NO', 'No');
935
    }
936
937
    /**
938
     * Formatted answer for "hasCustomerMessage".
939
     *
940
     * @return string
941
     */
942
    public function ShowAsSummary()
943
    {
944
        return $this->getShowAsSummary();
945
    }
946
947
    /**
948
     *
949
     *
950
     * @return string
951
     */
952
    public function getShowAsSummary()
953
    {
954
        $v = '<strong>';
955
        if ($this->ShowAsUncompletedOrder) {
956
            $v .= _t('OrderStep.UNCOMPLETED', 'Uncompleted');
957
        } elseif ($this->ShowAsInProcessOrder) {
958
            $v .= _t('OrderStep.INPROCESS', 'In process');
959
        } elseif ($this->ShowAsCompletedOrder) {
960
            $v .= _t('OrderStep.COMPLETED', 'Completed');
961
        }
962
        $v .= '</strong>';
963
        $canArray = array();
964
        if ($this->CustomerCanEdit) {
965
            $canArray[] = _t('OrderStep.EDITABLE', 'edit');
966
        }
967
        if ($this->CustomerCanPay) {
968
            $canArray[] = _t('OrderStep.PAY', 'pay');
969
        }
970
        if ($this->CustomerCanCancel) {
971
            $canArray[] = _t('OrderStep.CANCEL', 'cancel');
972
        }
973
        if (count($canArray)) {
974
            $v .=  '<br />'._t('OrderStep.CUSTOMER_CAN', 'Customer Can').': '.implode(', ', $canArray).'';
975
        }
976
        if ($this->hasCustomerMessage()) {
977
            $v .= '<br />'._t('OrderStep.CUSTOMER_MESSAGES', 'Includes message to customer');
978
        }
979
        if ($this->DeferTimeInSeconds) {
980
            $v .= '<br />'.$this->humanReadeableDeferTimeInSeconds();
981
        }
982
983
        return DBField::create_field('HTMLText', $v);
984
    }
985
986
    /**
987
     * @return string
0 ignored issues
show
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
988
     */
989
    protected function humanReadeableDeferTimeInSeconds()
990
    {
991
        if ($this->canBeDefered()) {
992
            $field = DBField::create_field('SS_DateTime', strtotime('+ '.$this->DeferTimeInSeconds.' seconds'));
993
            $descr0 = _t('OrderStep.THE', 'The').' '.'<span style="color: #338DC1">'.$this->getTitle().'</span>';
994
            $descr1 = _t('OrderStep.DELAY_VALUE', 'Order Step, for any order, will run');
995
            $descr2 = $field->ago();
996
            $descr3 = $this->DeferFromSubmitTime ?
997
                    _t('OrderStep.FROM_ORDER_SUBMIT_TIME', 'from the order being submitted') :
998
                    _t('OrderStep.FROM_START_OF_ORDSTEP', 'from the order arriving on this step');
999
            return $descr0. ' ' . $descr1.' <span style="color: #338DC1">'.$descr2.'</span> '.$descr3.'.';
1000
        }
1001
        // $dtF = new \DateTime('@0');
1002
        // $dtT = new \DateTime("@".$this->DeferTimeInSeconds);
1003
        //
1004
        // return $dtF->diff($dtT)->format('%a days, %h hours, %i minutes and %s seconds');
1005
    }
1006
1007
    /**
1008
     * Formatted answer for "hasCustomerMessage".
1009
     *
1010
     * @return string
1011
     */
1012
    public function NameAndDescription()
1013
    {
1014
        return $this->getNameAndDescription();
1015
    }
1016
1017
    public function getNameAndDescription()
1018
    {
1019
        $v = '<strong>'.$this->Name.'</strong><br /><em>'.$this->Description.'</em>';
1020
1021
        return DBField::create_field('HTMLText', $v);
1022
    }
1023
1024
    /**
1025
     * This allows you to set the time to something other than the standard DeferTimeInSeconds
1026
     * value based on the order provided.
1027
     *
1028
     * @param Order
1029
     *
1030
     * @return int
1031
     */
1032
    public function CalculatedDeferTimeInSeconds($order)
1033
    {
1034
        return $this->DeferTimeInSeconds;
1035
    }
1036
1037
    /**
1038
     * can this order step be delayed?
1039
     * in general, if there is a customer message
1040
     * we should be able to delay it
1041
     *
1042
     * This method can be overridden in any orderstep
1043
     * @return bool
1044
     **/
1045
    protected function canBeDefered()
1046
    {
1047
        return $this->hasCustomerMessage();
1048
    }
1049
1050
1051
/**************************************************
1052
* Order Status Logs
1053
**************************************************/
1054
1055
    /**
1056
     * The OrderStatusLog that is relevant to the particular step.
1057
     *
1058
     * @var string
1059
     */
1060
    protected $relevantLogEntryClassName = '';
1061
1062
    /**
1063
     * @return string
1064
     */
1065
    public function getRelevantLogEntryClassName()
1066
    {
1067
        return $this->relevantLogEntryClassName;
1068
    }
1069
1070
    /**
1071
     * @param string
1072
     */
1073
    public function setRelevantLogEntryClassName($s)
1074
    {
1075
        $this->relevantLogEntryClassName = $s;
1076
    }
1077
1078
    /**
1079
     * returns the OrderStatusLog that is relevant to this step.
1080
     *
1081
     * @param Order $order
1082
     *
1083
     * @return OrderStatusLog | null
1084
     */
1085
    public function RelevantLogEntry(Order $order)
1086
    {
1087
        if ($className = $this->getRelevantLogEntryClassName()) {
1088
            return $this->RelevantLogEntries($order)->Last();
1089
        }
1090
    }
1091
1092
    /**
1093
     * returns the OrderStatusLogs that are relevant to this step.
1094
     * It is important that getRelevantLogEntryClassName returns
1095
     * a specific enough ClassName and not a base class name.
1096
     *
1097
     * @param Order $order
1098
     *
1099
     * @return DataObjectSet | null
1100
     */
1101
    public function RelevantLogEntries(Order $order)
1102
    {
1103
        if ($className = $this->getRelevantLogEntryClassName()) {
1104
            return $className::get()->filter(
1105
                array(
1106
                    'OrderID' => $order->ID
1107
                )
1108
            );
1109
        }
1110
    }
1111
1112
/**************************************************
1113
* Silverstripe Standard Data Object Methods
1114
**************************************************/
1115
1116
    /**
1117
     * Standard SS method
1118
     * These are only created programmatically.
1119
     *
1120
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1121
     *
1122
     * @return bool
1123
     */
1124
    public function canCreate($member = null)
1125
    {
1126
        return false;
1127
    }
1128
1129
    /**
1130
     * Standard SS method.
1131
     *
1132
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1133
     *
1134
     * @return bool
0 ignored issues
show
Should the return type not be boolean|string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1135
     */
1136
    public function canView($member = null)
1137
    {
1138
        if (! $member) {
1139
            $member = Member::currentUser();
1140
        }
1141
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>|null, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1142
        if ($extended !== null) {
1143
            return $extended;
1144
        }
1145
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
1146
            return true;
1147
        }
1148
1149
        return parent::canEdit($member);
1150
    }
1151
1152
    /**
1153
     * the default for this is TRUE, but for completed order steps
1154
     *
1155
     * we do not allow this.
1156
     *
1157
     * @param  Order $order
1158
     * @param  Member $member optional
1159
     * @return bool
1160
     */
1161
    public function canOverrideCanViewForOrder($order, $member = null)
1162
    {
1163
        //return true if the order can have customer input
1164
        // orders recently saved can also be views
1165
        return
1166
            $this->CustomerCanEdit ||
1167
            $this->CustomerCanCancel ||
1168
            $this->CustomerCanPay;
1169
    }
1170
1171
    /**
1172
     * standard SS method.
1173
     *
1174
     * @param Member | NULL
1175
     *
1176
     * @return bool
0 ignored issues
show
Should the return type not be boolean|string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1177
     */
1178
    public function canEdit($member = null)
1179
    {
1180
        if (! $member) {
1181
            $member = Member::currentUser();
1182
        }
1183
        $extended = $this->extendedCan(__FUNCTION__, $member);
1184
        if ($extended !== null) {
1185
            return $extended;
1186
        }
1187
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
1188
            return true;
1189
        }
1190
1191
        return parent::canEdit($member);
1192
    }
1193
1194
    /**
1195
     * Standard SS method.
1196
     *
1197
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
1198
     *
1199
     * @return bool
0 ignored issues
show
Should the return type not be boolean|string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1200
     */
1201
    public function canDelete($member = null)
1202
    {
1203
        //cant delete last status if there are orders with this status
1204
        $nextOrderStepObject = $this->NextOrderStep();
1205
        if ($nextOrderStepObject) {
1206
            //do nothing
1207
        } else {
1208
            $orderCount = Order::get()
1209
                ->filter(array('StatusID' => intval($this->ID) - 0))
1210
                ->count();
1211
            if ($orderCount) {
1212
                return false;
1213
            }
1214
        }
1215
        if ($this->isDefaultStatusOption()) {
1216
            return false;
1217
        }
1218
        if (! $member) {
1219
            $member = Member::currentUser();
1220
        }
1221
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>|null, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1222
        if ($extended !== null) {
1223
            return $extended;
1224
        }
1225
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
1226
            return true;
1227
        }
1228
1229
        return parent::canEdit($member);
1230
    }
1231
1232
    /**
1233
     * standard SS method.
1234
     */
1235
    public function onBeforeWrite()
1236
    {
1237
        parent::onBeforeWrite();
1238
        //make sure only one of three conditions applies ...
1239
        if ($this->ShowAsUncompletedOrder) {
1240
            $this->ShowAsInProcessOrder = false;
1241
            $this->ShowAsCompletedOrder = false;
1242
        } elseif ($this->ShowAsInProcessOrder) {
1243
            $this->ShowAsUncompletedOrder = false;
1244
            $this->ShowAsCompletedOrder = false;
1245
        } elseif ($this->ShowAsCompletedOrder) {
1246
            $this->ShowAsUncompletedOrder = false;
1247
            $this->ShowAsInProcessOrder = false;
1248
        }
1249
        if (! $this->canBeDefered()) {
1250
            $this->DeferTimeInSeconds = 0;
1251
            $this->DeferFromSubmitTime = 0;
1252
        } else {
1253
            if (is_numeric($this->DeferTimeInSeconds)) {
1254
                $this->DeferTimeInSeconds = intval($this->DeferTimeInSeconds);
1255
            } else {
1256
                $this->DeferTimeInSeconds = strtotime('+'.$this->DeferTimeInSeconds);
1257
                if ($this->DeferTimeInSeconds > 0) {
1258
                    $this->DeferTimeInSeconds = $this->DeferTimeInSeconds - time();
1259
                }
1260
            }
1261
        }
1262
        $this->Code = strtoupper($this->Code);
1263
    }
1264
1265
    /**
1266
     * move linked orders to the next status
1267
     * standard SS method.
1268
     */
1269
    public function onBeforeDelete()
1270
    {
1271
        $ordersWithThisStatus = Order::get()->filter(array('StatusID' => $this->ID));
1272
        if ($ordersWithThisStatus->count()) {
1273
            $previousOrderStepObject = null;
1274
            $bestOrderStep = $this->NextOrderStep();
1275
            //backup
1276
            if ($bestOrderStep && $bestOrderStep->exists()) {
1277
                //do nothing
1278
            } else {
1279
                $bestOrderStep = $this->PreviousOrderStep();
1280
            }
1281
            if ($bestOrderStep) {
1282
                foreach ($ordersWithThisStatus as $orderWithThisStatus) {
1283
                    $orderWithThisStatus->StatusID = $bestOrderStep->ID;
1284
                    $orderWithThisStatus->write();
1285
                }
1286
            }
1287
        }
1288
        parent::onBeforeDelete();
1289
    }
1290
1291
    /**
1292
     * standard SS method.
1293
     */
1294
    public function onAfterDelete()
1295
    {
1296
        parent::onAfterDelete();
1297
        $this->requireDefaultRecords();
1298
    }
1299
1300
    protected function NextOrderStep()
1301
    {
1302
        return OrderStep::get()
1303
            ->filter(array('Sort:GreaterThan' => $this->Sort))
1304
            ->First();
1305
    }
1306
1307
    protected function PreviousOrderStep()
1308
    {
1309
        return OrderStep::get()
1310
            ->filter(array('Sort:LessThan' => $this->Sort))
1311
            ->First();
1312
    }
1313
1314
    /**
1315
     * standard SS method
1316
     * USED TO BE: Unpaid,Query,Paid,Processing,Sent,Complete,AdminCancelled,MemberCancelled,Cart.
1317
     */
1318
    public function requireDefaultRecords()
1319
    {
1320
        parent::requireDefaultRecords();
1321
        $orderStepsToInclude = EcommerceConfig::get('OrderStep', 'order_steps_to_include');
1322
        $codesToInclude = self::get_codes_for_order_steps_to_include();
1323
        $indexNumber = 0;
1324
        if ($orderStepsToInclude && count($orderStepsToInclude)) {
1325
            if ($codesToInclude && count($codesToInclude)) {
1326
                foreach ($codesToInclude as $className => $code) {
1327
                    $code = strtoupper($code);
1328
                    $filter = array('ClassName' => $className);
1329
                    $indexNumber += 10;
1330
                    $itemCount = OrderStep::get()->filter($filter)->Count();
1331
                    if ($itemCount) {
1332
                        //always reset code
1333
                        $obj = OrderStep::get()->filter($filter)->First();
1334
                        if ($obj->Code != $code) {
1335
                            $obj->Code = $code;
1336
                            $obj->write();
1337
                        }
1338
                        //replace default description
1339
                        $parentObj = singleton('OrderStep');
1340
                        if ($obj->Description == $parentObj->myDescription()) {
1341
                            $obj->Description = $obj->myDescription();
1342
                            $obj->write();
1343
                        }
1344
                        //check sorting order
1345
                        if ($obj->Sort != $indexNumber) {
1346
                            $obj->Sort = $indexNumber;
1347
                            $obj->write();
1348
                        }
1349
                    } else {
1350
                        $obj = $className::create($filter);
1351
                        $obj->Code = $code;
1352
                        $obj->Description = $obj->myDescription();
1353
                        $obj->Sort = $indexNumber;
1354
                        $obj->write();
1355
                        DB::alteration_message("Created \"$code\" as $className.", 'created');
1356
                    }
1357
                    $obj = OrderStep::get()
1358
                        ->filter($filter)
1359
                        ->First();
1360
                    if (! $obj) {
1361
                        user_error("There was an error in creating the $code OrderStep");
1362
                    }
1363
                }
1364
            }
1365
        }
1366
        $steps = OrderStep::get();
1367
        foreach ($steps as $step) {
1368
            if (!$step->Description) {
1369
                $step->Description = $step->myDescription();
1370
                $step->write();
1371
            }
1372
        }
1373
    }
1374
1375
    /**
1376
     * returns the standard EcommerceDBConfig for use within OrderSteps.
1377
     *
1378
     * @return EcommerceDBConfig
1379
     */
1380
    protected function EcomConfig()
1381
    {
1382
        return EcommerceDBConfig::current_ecommerce_db_config();
1383
    }
1384
1385
    /**
1386
     * Explains the current order step.
1387
     *
1388
     * @return string
1389
     */
1390
    protected function myDescription()
1391
    {
1392
        return _t('OrderStep.DESCRIPTION', 'No description has been provided for this step.');
1393
    }
1394
}
1395