Completed
Push — master ( ce2dd0...10a6ac )
by Nicolaas
04:02
created

code/CheckoutPage.php (7 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
 * CheckoutPage is a CMS page-type that shows the order
5
 * details to the customer for their current shopping
6
 * cart on the site. It also lets the customer review
7
 * the items in their cart, and manipulate them (add more,
8
 * deduct or remove items completely). The most important
9
 * thing is that the {@link CheckoutPage_Controller} handles
10
 * the {@link OrderForm} form instance, allowing the customer
11
 * to fill out their shipping details, confirming their order
12
 * and making a payment.
13
 *
14
 * @see CheckoutPage_Controller->Order()
15
 * @see OrderForm
16
 * @see CheckoutPage_Controller->OrderForm()
17
 *
18
 * The CheckoutPage_Controller is also responsible for setting
19
 * up the modifier forms for each of the OrderModifiers that are
20
 * enabled on the site (if applicable - some don't require a form
21
 * for user input). A usual implementation of a modifier form would
22
 * be something like allowing the customer to enter a discount code
23
 * so they can receive a discount on their order.
24
 * @see OrderModifier
25
 * @see CheckoutPage_Controller->ModifierForms()
26
 *
27
 * TO DO: get rid of all the messages...
28
 *
29
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
30
 * @package: ecommerce
31
 * @sub-package: Pages
32
 * @inspiration: Silverstripe Ltd, Jeremy
33
 **/
34
class CheckoutPage extends CartPage
35
{
36
    /**
37
     * standard SS variable.
38
     *
39
     * @Var Boolean
40
     */
41
    private static $hide_ancestor = 'CartPage';
42
43
    /**
44
     * standard SS variable.
45
     *
46
     * @Var string
47
     */
48
    private static $icon = 'ecommerce/images/icons/CheckoutPage';
49
50
    /**
51
     * standard SS variable.
52
     *
53
     * @Var Array
54
     */
55
    private static $db = array(
56
        'TermsAndConditionsMessage' => 'Varchar(200)',
57
    );
58
59
    /**
60
     * standard SS variable.
61
     *
62
     * @Var Array
63
     */
64
    private static $has_one = array(
65
        'TermsPage' => 'Page',
66
    );
67
68
    /**
69
     * standard SS variable.
70
     *
71
     * @Var Array
72
     */
73
    private static $defaults = array(
74
        'TermsAndConditionsMessage' => 'You must agree with the terms and conditions before proceeding.',
75
    );
76
77
    /**
78
     * standard SS variable.
79
     *
80
     * @Var String
81
     */
82
    private static $singular_name = 'Checkout Page';
83
    public function i18n_singular_name()
84
    {
85
        return _t('CheckoutPage.SINGULARNAME', 'Checkout Page');
86
    }
87
88
    /**
89
     * standard SS variable.
90
     *
91
     * @Var String
92
     */
93
    private static $plural_name = 'Checkout Pages';
94
    public function i18n_plural_name()
95
    {
96
        return _t('CheckoutPage.PLURALNAME', 'Checkout Pages');
97
    }
98
99
    /**
100
     * Standard SS variable.
101
     *
102
     * @var string
103
     */
104
    private static $description = 'A page where the customer can view the current order (cart) and finalise (submit) the order. Every e-commerce site needs an Order Confirmation Page.';
105
106
    /**
107
     * Returns the Terms and Conditions Page (if there is one).
108
     *
109
     * @return Page | NULL
110
     */
111
    public static function find_terms_and_conditions_page()
112
    {
113
        $checkoutPage = CheckoutPage::get()->First();
114
        if ($checkoutPage && $checkoutPage->TermsPageID) {
115
            return Page::get()->byID($checkoutPage->TermsPageID);
116
        }
117
    }
118
119
    /**
120
     * Returns the link or the Link to the Checkout page on this site.
121
     *
122
     * @param string $action [optional]
0 ignored issues
show
Should the type for parameter $action not be string|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...
123
     *
124
     * @return string (URLSegment)
125
     */
126
    public static function find_link($action = null)
127
    {
128
        $page = CheckoutPage::get()->First();
129
        if ($page) {
130
            return $page->Link($action);
131
        }
132
        user_error('No Checkout Page has been created - it is recommended that you create this page type for correct functioning of E-commerce.', E_USER_NOTICE);
133
134
        return '';
135
    }
136
137
    /**
138
     * Returns the link or the Link to the Checkout page on this site
139
     * for the last step.
140
     *
141
     * @param string $step
142
     *
143
     * @return string (URLSegment)
144
     */
145
    public static function find_last_step_link($step = '')
146
    {
147
        if (!$step) {
148
            $steps = EcommerceConfig::get('CheckoutPage_Controller', 'checkout_steps');
149
            if ($steps && count($steps)) {
150
                $step = array_pop($steps);
151
            }
152
        }
153
        if ($step) {
154
            $step = 'checkoutstep/'.strtolower($step).'/#'.$step;
155
        }
156
157
        return self::find_link($step);
158
    }
159
160
    /**
161
     * Returns the link to the next step.
162
     *
163
     * @param string - $currentStep       is the step that has just been actioned....
164
     * @param bool -   $doPreviousInstead - return previous rather than next step
165
     *
166
     * @return string (URLSegment)
167
     */
168
    public static function find_next_step_link($currentStep, $doPreviousInstead = false)
169
    {
170
        $nextStep = null;
171
        if ($link = self::find_link()) {
172
            $steps = EcommerceConfig::get('CheckoutPage_Controller', 'checkout_steps');
173
            if (in_array($currentStep, $steps)) {
174
                $key = array_search($currentStep, $steps);
175
                if ($key !== false) {
176
                    if ($doPreviousInstead) {
177
                        --$key;
178
                    } else {
179
                        ++$key;
180
                    }
181
                    if (isset($steps[$key])) {
182
                        $nextStep = $steps[$key];
183
                    }
184
                }
185
            } else {
186
                if ($doPreviousInstead) {
187
                    $nextStep = array_shift($steps);
188
                } else {
189
                    $nextStep = array_pop($steps);
190
                }
191
            }
192
            if ($nextStep) {
193
                return $link.'checkoutstep'.'/'.$nextStep.'/';
194
            } else {
195
            }
196
197
            return $link;
198
        }
199
200
        return '';
201
    }
202
203
    /**
204
     * Returns the link to the checkout page on this site, using
205
     * a specific Order ID that already exists in the database.
206
     *
207
     * @param int $orderID ID of the {@link Order}
208
     *
209
     * @return string Link to checkout page
210
     */
211
    public static function get_checkout_order_link($orderID)
212
    {
213
        if ($page = self::find_link()) {
214
            return $page->Link('showorder').'/'.$orderID.'/';
215
        }
216
217
        return '';
218
    }
219
220
    /**
221
     * Standard SS function, we only allow for one checkout page to exist
222
     * but we do allow for extensions to exist at the same time.
223
     *
224
     * @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...
225
     *
226
     * @return bool
227
     **/
228
    public function canCreate($member = null)
229
    {
230
        return CheckoutPage::get()->Filter(array('ClassName' => 'CheckoutPage'))->Count() ? false : $this->canEdit($member);
231
    }
232
233
    /**
234
     * Shop Admins can edit.
235
     *
236
     * @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...
237
     *
238
     * @return bool
239
     */
240
    public function canEdit($member = null)
241
    {
242
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
243
            return true;
244
        }
245
246
        return parent::canEdit($member);
247
    }
248
249
    /**
250
     * Standard SS method.
251
     *
252
     * @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...
253
     *
254
     * @return bool
255
     */
256
    public function canDelete($member = null)
257
    {
258
        return false;
259
    }
260
261
    /**
262
     * Standard SS method.
263
     *
264
     * @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...
265
     *
266
     * @return bool
267
     */
268
    public function canPublish($member = null)
269
    {
270
        return $this->canEdit($member);
271
    }
272
273
    /**
274
     * Standard SS function.
275
     *
276
     * @return FieldList
277
     **/
278
    public function getCMSFields()
279
    {
280
        $fields = parent :: getCMSFields();
281
        $fields->removeFieldFromTab('Root.Messages.Messages.Actions', 'ProceedToCheckoutLabel');
282
        $fields->removeFieldFromTab('Root.Messages.Messages.Actions', 'ContinueShoppingLabel');
283
        $fields->removeFieldFromTab('Root.Messages.Messages.Actions', 'ContinuePageID');
284
        $fields->removeFieldFromTab('Root.Messages.Messages.Actions', 'LoadOrderLinkLabel');
285
        $fields->removeFieldFromTab('Root.Messages.Messages.Actions', 'CurrentOrderLinkLabel');
286
        $fields->removeFieldFromTab('Root.Messages.Messages.Actions', 'SaveOrderLinkLabel');
287
        $fields->removeFieldFromTab('Root.Messages.Messages.Actions', 'DeleteOrderLinkLabel');
288
        $termsPageIDField = OptionalTreeDropdownField::create(
289
            'TermsPageID',
290
            _t('CheckoutPage.TERMSANDCONDITIONSPAGE', 'Terms and conditions page'),
291
            'SiteTree'
292
        );
293
        $termsPageIDField->setRightTitle(_t('CheckoutPage.TERMSANDCONDITIONSPAGE_RIGHT', 'This is optional. To remove this page clear the reminder message below.'));
294
        $fields->addFieldToTab('Root.Terms', $termsPageIDField);
295
        $fields->addFieldToTab(
296
            'Root.Terms',
297
            $termsPageIDFieldMessage = new TextField(
298
                'TermsAndConditionsMessage',
299
                _t('CheckoutPage.TERMSANDCONDITIONSMESSAGE', 'Reminder Message')
300
            )
301
        );
302
        $termsPageIDFieldMessage->setRightTitle(
303
            _t('CheckoutPage.TERMSANDCONDITIONSMESSAGE_RIGHT', "Shown if the user does not tick the 'I agree with the Terms and Conditions' box. Leave blank to allow customer to proceed without ticking this box")
304
        );
305
        //The Content field has a slightly different meaning for the Checkout Page.
306
        $fields->removeFieldFromTab('Root.Main', 'Content');
307
        $fields->addFieldToTab('Root.Messages.Messages.AlwaysVisible', $htmlEditorField = new HTMLEditorField('Content', _t('CheckoutPage.CONTENT', 'General note - always visible on the checkout page')));
308
        $htmlEditorField->setRows(3);
309
        if (OrderModifier_Descriptor::get()->count()) {
310
            $fields->addFieldToTab('Root.Messages.Messages.OrderExtras', $this->getOrderModifierDescriptionField());
311
        }
312
        if (CheckoutPage_StepDescription::get()->count()) {
313
            $fields->addFieldToTab('Root.Messages.Messages.CheckoutSteps', $this->getCheckoutStepDescriptionField());
314
        }
315
316
        return $fields;
317
    }
318
319
    /**
320
     * @return GridField
321
     */
322
    protected function getOrderModifierDescriptionField()
323
    {
324
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
325
            new GridFieldToolbarHeader(),
326
            new GridFieldSortableHeader(),
327
            new GridFieldDataColumns(),
328
            new GridFieldEditButton(),
329
            new GridFieldDetailForm()
330
        );
331
        $title = _t('CheckoutPage.ORDERMODIFIERDESCRIPTMESSAGES', 'Messages relating to order form extras (e.g. tax or shipping)');
332
        $source = OrderModifier_Descriptor::get();
333
334
        return new GridField('OrderModifier_Descriptor', $title, $source, $gridFieldConfig);
335
    }
336
337
    /**
338
     * @return GridField
339
     */
340
    protected function getCheckoutStepDescriptionField()
341
    {
342
        $gridFieldConfig = GridFieldConfig::create()->addComponents(
343
            new GridFieldToolbarHeader(),
344
            new GridFieldSortableHeader(),
345
            new GridFieldDataColumns(),
346
            new GridFieldEditButton(),
347
            new GridFieldDetailForm()
348
        );
349
        $title = _t('CheckoutPage.CHECKOUTSTEPESCRIPTIONS', 'Checkout Step Descriptions');
350
        $source = CheckoutPage_StepDescription::get();
351
352
        return new GridField('CheckoutPage_StepDescription', $title, $source, $gridFieldConfig);
353
    }
354
355
    public function requireDefaultRecords()
356
    {
357
        parent::requireDefaultRecords();
358
        $checkoutPage = CheckoutPage::get()->first();
359
        if (!$checkoutPage) {
360
            $checkoutPage = self::create();
361
            $checkoutPage->Title = 'Checkout';
362
            $checkoutPage->MenuTitle = 'Checkout';
363
            $checkoutPage->URLSegment = 'checkout';
364
            $checkoutPage->writeToStage('Stage');
365
            $checkoutPage->publish('Stage', 'Live');
366
        }
367
    }
368
}
369
370
class CheckoutPage_Controller extends CartPage_Controller
371
{
372
    private static $allowed_actions = array(
373
        'checkoutstep',
374
        'OrderFormAddress',
375
        'saveorder',
376
        'CreateAccountForm',
377
        'retrieveorder',
378
        'loadorder',
379
        'startneworder',
380
        'showorder',
381
        'LoginForm',
382
        'OrderForm',
383
    );
384
385
    /**
386
     * FOR STEP STUFF SEE BELOW.
387
     **/
388
389
    /**
390
     * Standard SS function
391
     * if set to false, user can edit order, if set to true, user can only review order.
392
     **/
393
    public function init()
394
    {
395
        parent::init();
396
397
        Requirements::themedCSS('CheckoutPage', 'ecommerce');
398
        $ajaxifyArray = EcommerceConfig::get('CheckoutPage_Controller', 'ajaxify_steps');
399
        if (count($ajaxifyArray)) {
400
            foreach ($ajaxifyArray as $js) {
401
                Requirements::javascript($js);
402
            }
403
        }
404
        Requirements::javascript('ecommerce/javascript/EcomPayment.js');
405
        Requirements::customScript('
406
            if (typeof EcomOrderForm != "undefined") {
407
                EcomOrderForm.set_TermsAndConditionsMessage(\''.convert::raw2js($this->TermsAndConditionsMessage).'\');
408
            }',
409
            'TermsAndConditionsMessage'
410
        );
411
        $this->steps = EcommerceConfig::get('CheckoutPage_Controller', 'checkout_steps');
412
        $this->currentStep = $this->request->Param('ID');
413
        if ($this->currentStep && in_array($this->currentStep, $this->steps)) {
414
            //do nothing
415
        } else {
416
            $this->currentStep = array_shift($this->steps);
417
        }
418
        //redirect to current order -
419
        // this is only applicable when people submit order (start to pay)
420
        // and then return back
421
        if ($checkoutPageCurrentOrderID = Session::get('CheckoutPageCurrentOrderID')) {
422
            if ($this->currentOrder->ID != $checkoutPageCurrentOrderID) {
423
                $this->clearRetrievalOrderID();
424
            }
425
        }
426
        if ($this->currentOrder) {
427
            $this->setRetrievalOrderID($this->currentOrder->ID);
428
        }
429
    }
430
431
    /**
432
     * Returns a ArrayList of {@link OrderModifierForm} objects. These
433
     * forms are used in the OrderInformation HTML table for the user to fill
434
     * in as needed for each modifier applied on the site.
435
     *
436
     * @return ArrayList (ModifierForms) | Null
437
     */
438
    public function ModifierForms()
439
    {
440
        if ($this->currentOrder) {
441
            return $this->currentOrder->getModifierForms();
442
        }
443
    }
444
445
    /**
446
     * Returns a form allowing a user to enter their
447
     * details to checkout their order.
448
     *
449
     * @return OrderForm object
450
     */
451
    public function OrderFormAddress()
452
    {
453
        $form = OrderFormAddress::create($this, 'OrderFormAddress');
454
        $this->data()->extend('updateOrderFormAddress', $form);
455
        //load session data
456
        if ($data = Session::get("FormInfo.{$form->FormName()}.data")) {
457
            $form->loadDataFrom($data);
458
        }
459
460
        return $form;
461
    }
462
463
    /**
464
     * Returns a form allowing a user to enter their
465
     * details to checkout their order.
466
     *
467
     * @return OrderForm object
468
     */
469
    public function OrderForm()
470
    {
471
        $form = OrderForm::create($this, 'OrderForm');
472
        $this->data()->extend('updateOrderForm', $form);
473
        //load session data
474
        if ($data = Session :: get("FormInfo.{$form->FormName()}.data")) {
475
            $form->loadDataFrom($data);
476
        }
477
478
        return $form;
479
    }
480
481
    /**
482
     * Can the user proceed? It must be an editable order (see @link CartPage)
483
     * and is must also contain items.
484
     *
485
     * @return bool
486
     */
487
    public function CanCheckout()
488
    {
489
        return $this->currentOrder->getTotalItems() && !$this->currentOrder->IsSubmitted();
490
    }
491
492
    /**
493
     * Catch for incompatable coding only....
494
     */
495
    public function ModifierForm($request)
496
    {
497
        user_error('Make sure that you set the controller for your ModifierForm to a controller directly associated with the Modifier', E_USER_WARNING);
498
499
        return array();
500
    }
501
502
    /**
503
     * STEP STUFF ---------------------------------------------------------------------------.
504
     *
505
506
507
     /**
508
     *@var String
509
     **/
510
    protected $currentStep = '';
511
512
    /**
513
     *@var array
514
     **/
515
    protected $steps = array();
516
517
    /**
518
     * returns a dataobject set of the steps.
519
     * Or just one step if that is more relevant.
520
     *
521
     * @param int $number - if set, it returns that one step.
522
     */
523
    public function CheckoutSteps($number = 0)
524
    {
525
        $where = '';
526
        $dos = CheckoutPage_StepDescription::get()
527
            ->Sort('ID', 'ASC');
528
        if ($number) {
529
            $dos = $dos->Filter(array('ID' => $number));
530
        }
531
        if ($number) {
532
            if ($dos->count()) {
533
                return $dos->First();
534
            }
535
        }
536
        $returnData = new ArrayList(array());
537
        $completed = 1;
538
        $completedClass = 'completed';
539
        foreach ($dos as $do) {
540
            if ($this->currentStep && $do->Code() == $this->currentStep) {
541
                $do->LinkingMode = 'current';
542
                $completed = 0;
543
                $completedClass = 'notCompleted';
544
            } else {
545
                if ($completed) {
546
                    $do->Link = $this->Link('checkoutstep').'/'.$do->Code.'/';
547
                }
548
                $do->LinkingMode = "link $completedClass";
549
            }
550
            $do->Completed = $completed;
551
            $returnData->push($do);
552
        }
553
        if (EcommerceConfig::get('OrderConfirmationPage_Controller', 'include_as_checkout_step')) {
554
            $orderConfirmationPage = OrderConfirmationPage::get()->First();
555
            if ($orderConfirmationPage) {
556
                $do = $orderConfirmationPage->CurrentCheckoutStep(false);
557
                if ($do) {
558
                    $returnData->push($do);
559
                }
560
            }
561
        }
562
563
        return $returnData;
564
    }
565
566
    /**
567
     * returns the heading for the Checkout Step.
568
     *
569
     * @param int $number
570
     *
571
     * @return string
572
     */
573
    public function StepsContentHeading($number)
574
    {
575
        $do = $this->CheckoutSteps($number);
576
        if ($do) {
577
            return $do->Heading;
578
        }
579
580
        return '';
581
    }
582
583
    /**
584
     * returns the top of the page content for the Checkout Step.
585
     *
586
     * @param int $number
587
     *
588
     * @return string
589
     */
590
    public function StepsContentAbove($number)
591
    {
592
        $do = $this->CheckoutSteps($number);
593
        if ($do) {
594
            return $do->Above;
595
        }
596
597
        return '';
598
    }
599
600
    /**
601
     * returns the bottom of the page content for the Checkout Step.
602
     *
603
     * @param int $number
604
     *
605
     * @return string
606
     */
607
    public function StepsContentBelow($number)
608
    {
609
        $do = $this->CheckoutSteps($number);
610
        if ($do) {
611
            return $do->Below;
612
        }
613
614
        return '';
615
    }
616
617
    /**
618
     * sets the current checkout step
619
     * if it is ajax it returns the current controller
620
     * as the inner for the page.
621
     *
622
     * @param SS_HTTPRequest $request
623
     *
624
     * @return array
625
     */
626
    public function checkoutstep(SS_HTTPRequest $request)
627
    {
628
        if ($this->request->isAjax()) {
629
            Requirements::clear();
630
631
            return $this->renderWith('LayoutCheckoutPageInner');
632
        }
633
634
        return array();
635
    }
636
637
    /**
638
     * when you extend the CheckoutPage you can change this...
639
     *
640
     * @return bool
641
     */
642
    public function HasCheckoutSteps()
643
    {
644
        return true;
645
    }
646
647
    /**
648
     * @param string $step
649
     *
650
     * @return bool
651
     **/
652
    public function CanShowStep($step)
653
    {
654
        if ($this->ShowOnlyCurrentStep()) {
655
            return $step == $this->currentStep;
656
        } else {
657
            return in_array($step, $this->steps);
658
        }
659
    }
660
661
    /**
662
     * Is this the final step in the process.
663
     *
664
     * @return bool
665
     */
666
    public function ShowOnlyCurrentStep()
667
    {
668
        return $this->currentStep ? true : false;
669
    }
670
671
    /**
672
     * Is this the final step in the process?
673
     *
674
     * @return bool
675
     */
676
    public function IsFinalStep()
677
    {
678
        foreach ($this->steps as $finalStep) {
679
            //do nothing...
680
        }
681
682
        return $this->currentStep == $finalStep;
683
    }
684
685
    /**
686
     * returns the percentage of steps done (0 - 100).
687
     *
688
     * @return int
0 ignored issues
show
Should the return type not be double?

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...
689
     */
690
    public function PercentageDone()
691
    {
692
        return round($this->currentStepNumber() / $this->numberOfSteps(), 2) * 100;
693
    }
694
695
    /**
696
     * returns the number of the current step (e.g. step 1).
697
     *
698
     * @return int
0 ignored issues
show
Should the return type not be false|integer|string?

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...
699
     */
700
    protected function currentStepNumber()
701
    {
702
        $key = 1;
703
        if ($this->currentStep) {
704
            $key = array_search($this->currentStep, $this->steps);
705
            ++$key;
706
        }
707
708
        return $key;
709
    }
710
711
    /**
712
     * returns the total number of steps (e.g. 3)
713
     * we add one for the confirmation page.
714
     *
715
     * @return int
716
     */
717
    protected function numberOfSteps()
718
    {
719
        return count($this->steps) + 1;
720
    }
721
722
    /**
723
     * Here are some additional rules that can be applied to steps.
724
     * If you extend the checkout page, you canm overrule these rules.
725
     */
726
    protected function applyStepRules()
727
    {
728
        //no items, back to beginning.
729
        //has step xxx been completed? if not go back one?
730
        //extend
731
        //reset current step if different
732
    }
733
}
734