Completed
Push — master ( a140bd...5dca79 )
by Nicolaas
03:30
created

ShoppingCart::addBuyable()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 32
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
dl 0
loc 32
rs 6.7272
c 0
b 0
f 0
eloc 23
nc 6
nop 3
1
<?php
2
/**
3
 * ShoppingCart - provides a global way to interface with the cart (current order).
4
 *
5
 * This can be used in other code by calling $cart = ShoppingCart::singleton();
6
 *
7
 * The shopping cart can be accessed as an order handler from the back-end
8
 * (e.g. when creating an order programmatically), while the accompagnying controller
9
 * is used by web-users to manipulate their order.
10
 *
11
 * A bunch of core functions are also stored in the order itself.
12
 * Methods and variables are in the shopping cart if they are relevant
13
 * only before (and while) the order is placed (e.g. latest update message),
14
 * and others are in the order because they are relevant even after the
15
 * order has been submitted (e.g. Total Cost).
16
 *
17
 * Key methods:
18
 *
19
 * //get Cart
20
 * $myCart = ShoppingCart::singleton();
21
 *
22
 * //get order
23
 * $myOrder = ShoppingCart::current_order();
24
 *
25
 * //view order (from another controller)
26
 * $this->redirect(ShoppingCart::current_order()->Link());
27
 *
28
 * //add item to cart
29
 * ShoppingCart::singleton()->addBuyable($myProduct);
30
 *
31
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
32
 * @package: ecommerce
33
 * @sub-package: control
34
 * @inspiration: Silverstripe Ltd, Jeremy
35
 **/
36
class ShoppingCart extends Object
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
37
{
38
    /**
39
     * List of names that can be used as session variables.
40
     * Also @see ShoppingCart::sessionVariableName.
41
     *
42
     * @var array
43
     */
44
    private static $session_variable_names = array('OrderID', 'Messages');
45
46
    /**
47
     * This is where we hold the (singleton) Shoppingcart.
48
     *
49
     * @var object (ShoppingCart)
50
     */
51
    private static $_singletoncart = null;
52
53
    /**
54
     * Feedback message to user (e.g. cart updated, could not delete item, someone in standing behind you).
55
     *
56
     *@var array
57
     **/
58
    protected $messages = array();
59
60
    /**
61
     * stores a reference to the current order object.
62
     *
63
     * @var object
64
     **/
65
    protected $order = null;
66
67
    /**
68
     * This variable is set to YES when we actually need an order (i.e. write it).
69
     *
70
     * @var bool
71
     */
72
    protected $requireSavedOrder = false;
73
74
    /**
75
     * Allows access to the cart from anywhere in code.
76
     *
77
     * @return ShoppingCart Object
78
     */
79
    public static function singleton()
80
    {
81
        if (!self::$_singletoncart) {
82
            self::$_singletoncart = Injector::inst()->get('ShoppingCart');
83
        }
84
85
        return self::$_singletoncart;
86
    }
87
88
    /**
89
     * Allows access to the current order from anywhere in the code..
90
     *
91
     * if you do not like the session Order then you can set it here ...
92
     *
93
     * @param Order $order (optional)
94
     *
95
     * @return Order
96
     */
97
    public static function current_order($order = null;)
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected ';', expecting ')'
Loading history...
98
    {
99
        return self::singleton()->currentOrder(0, $order);
100
    }
101
102
    /**
103
     * looks up current order id.
104
     * you may supply an ID here, so that it looks up the current order ID
105
     * only when none is supplied.
106
     *
107
     * @param int (optional) | Order $orderOrOrderID
108
     *
109
     * @return int;
110
     */
111
    public static function current_order_id($orderOrOrderID = 0)
112
    {
113
        $orderID = 0;
114
        if (!$orderOrOrderID) {
115
            $order = self::current_order();
116
            if ($order && $order->exists()) {
117
                $orderID = $order->ID;
118
            }
119
        }
120
        if($orderOrOrderID instanceof Order) {
121
            $orderID = $orderOrOrderID->ID;
122
        } elseif(intval($orderOrOrderID)) {
123
            $orderID = intval($orderOrOrderID);
124
        }
125
126
        return $orderID;
127
    }
128
129
    /**
130
     * Allows access to the current order from anywhere in the code..
131
     *
132
     * @return Order
133
     */
134
    public static function session_order()
135
    {
136
        $sessionVariableName = self::singleton()->sessionVariableName('OrderID');
137
        $orderIDFromSession = intval(Session::get($sessionVariableName)) - 0;
138
139
        return Order::get()->byID($orderIDFromSession);
140
    }
141
142
    /**
143
     * set a specific order, other than the one from session ....
144
     *
145
     * @param Order $order
146
     *
147
     * @return Order
148
     */
149
    public function setOrder($order)
150
    {
151
        $this->order = $order;
152
        return $this->order;
153
    }
154
155
    /**
156
     * Gets or creates the current order.
157
     * Based on the session ONLY unless the order has been explictely set.
158
     * IMPORTANT FUNCTION!
159
     *
160
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
161
     *
162
     * However, you can pass an order in case you want to manipulate an order that is not in sesssion
163
     *
164
     * @param int $recurseCount (optional)
165
     * @param Order $order (optional)
166
     *
167
     * @return Order
168
     */
169
    public function currentOrder($recurseCount = 0, $order = null)
170
    {
171
        if($order) {
172
            $this->order = $order;
173
        }
174
        if($this->allowWrites()) {
175
            if (!$this->order) {
176
                $this->order = self::session_order();
177
                $loggedInMember = Member::currentUser();
178
                if ($this->order) {
179
                    //first reason to set to null: it is already submitted
180
                    if ($this->order->IsSubmitted()) {
181
                        $this->order = null;
182
                    }
183
                    //second reason to set to null: make sure we have permissions
184
                    elseif (!$this->order->canView()) {
185
                        $this->order = null;
186
                    }
187
                    //logged in, add Member.ID to order->MemberID
188
                    elseif ($loggedInMember && $loggedInMember->exists()) {
189
                        if ($this->order->MemberID != $loggedInMember->ID) {
190
                            $updateMember = false;
191
                            if (!$this->order->MemberID) {
192
                                $updateMember = true;
193
                            }
194
                            if (!$loggedInMember->IsShopAdmin()) {
195
                                $updateMember = true;
196
                            }
197
                            if ($updateMember) {
198
                                $this->order->MemberID = $loggedInMember->ID;
199
                                $this->order->write();
200
                            }
201
                        }
202
                        //IF current order has nothing in it AND the member already has an order: use the old one first
203
                        //first, lets check if the current order is worthwhile keeping
204
                        if ($this->order->StatusID || $this->order->TotalItems()) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
205
                            //do NOTHING!
206
                        } else {
207
                            $firstStep = OrderStep::get()->First();
208
                            //we assume the first step always exists.
209
                            //TODO: what sort order?
210
                            $count = 0;
211
                            while (
212
                                $firstStep &&
213
                                $previousOrderFromMember = Order::get()
214
                                    ->where('
215
                                        "MemberID" = '.$loggedInMember->ID.'
216
                                        AND ("StatusID" = '.$firstStep->ID.' OR "StatusID" = 0)
217
                                        AND "Order"."ID" <> '.$this->order->ID
218
                                    )
219
                                    ->First()
220
                            ) {
221
                                //arbritary 12 attempts ...
222
                                if ($count > 12) {
223
                                    break;
224
                                }
225
                                ++$count;
226
                                if ($previousOrderFromMember && $previousOrderFromMember->canView()) {
227
                                    if ($previousOrderFromMember->StatusID || $previousOrderFromMember->TotalItems()) {
228
                                        $this->order->delete();
229
                                        $this->order = $previousOrderFromMember;
230
                                        break;
231
                                    } else {
232
                                        $previousOrderFromMember->delete();
233
                                    }
234
                                }
235
                            }
236
                        }
237
                    }
238
                }
239
                if (! $this->order) {
240
                    if ($loggedInMember) {
241
                        //find previour order...
242
                        $firstStep = OrderStep::get()->First();
243
                        if ($firstStep) {
244
                            $previousOrderFromMember = Order::get()
245
                                ->filter(array(
246
                                    'MemberID' => $loggedInMember->ID,
247
                                    'StatusID' => array($firstStep->ID, 0),
248
                                ))
249
                                ->First();
250
                            if ($previousOrderFromMember) {
251
                                if ($previousOrderFromMember->canView()) {
252
                                    $this->order = $previousOrderFromMember;
253
                                }
254
                            }
255
                        }
256
                    }
257
                    if ($this->order && !$this->order->exists()) {
258
                        $this->order = null;
259
                    }
260
                    if (! $this->order) {
261
                        //here we cleanup old orders, because they should be
262
                        //cleaned at the same rate that they are created...
263
                        if (EcommerceConfig::get('ShoppingCart', 'cleanup_every_time')) {
264
                            $cartCleanupTask = EcommerceTaskCartCleanup::create();
265
                            $cartCleanupTask->runSilently();
266
                        }
267
                        //create new order
268
                        $this->order = Order::create();
269
                        if ($loggedInMember) {
270
                            $this->order->MemberID = $loggedInMember->ID;
271
                        }
272
                        $this->order->write();
273
                    }
274
                    $sessionVariableName = $this->sessionVariableName('OrderID');
275
                    Session::set($sessionVariableName, intval($this->order->ID));
276
                }
277
                if ($this->order){
278
                    if($this->order->exists()) {
279
                        $this->order->calculateOrderAttributes($force = false);
280
                    }
281
                    if (! $this->order->SessionID) {
282
                        $this->order->write();
283
                    }
284
                    //add session ID...
285
                }
286
            }
287
            //try it again
288
            //but limit to three, just in case ...
289
            //just in case ...
290
            if (!$this->order && $recurseCount < 3) {
291
                ++$recurseCount;
292
293
                return $this->currentOrder($recurseCount, $order);
294
            }
295
296
            return $this->order;
297
        } else {
298
299
            //we still return an order so that we do not end up with errors...
300
            return Order::create();
301
        }
302
    }
303
304
305
    private static $_allow_writes_cache = null;
306
307
    /**
308
     * can the current user use sessions and therefore write to cart???
309
     * the method also returns if an order has explicitely been set
310
     * @return Boolean
311
     */
312
    protected function allowWrites()
313
    {
314
        if($this->order) {
315
            return true;
316
        }
317
        if(self::$_allow_writes_cache === null) {
318
            if ( php_sapi_name() !== 'cli' ) {
319
                if ( version_compare(phpversion(), '5.4.0', '>=') ) {
320
                    self::$_allow_writes_cache = session_status() === PHP_SESSION_ACTIVE ? true : false;
321
                } else {
322
                    self::$_allow_writes_cache = session_id() === '' ? true : false;
323
                }
324
            } else {
325
                self::$_allow_writes_cache = false;
326
            }
327
        }
328
329
        return self::$_allow_writes_cache;
330
331
    }
332
333
    /**
334
     * Allows access to the current order from anywhere in the code..
335
     *
336
     * @param Order $order (optional)
337
     *
338
     * @return ShoppingCart Object
339
     */
340
    public function Link($order = null)
341
    {
342
        $order = self::singleton()->currentOrder(0, $order = null);
343
        if ($order) {
344
            return $order->Link();
345
        }
346
    }
347
348
    /**
349
     * Adds any number of items to the cart.
350
     * Returns the order item on succes OR false on failure.
351
     *
352
     * @param DataObject $buyable    - the buyable (generally a product) being added to the cart
353
     * @param float      $quantity   - number of items add.
354
     * @param mixed      $parameters - array of parameters to target a specific order item. eg: group=1, length=5
355
     *                                 if you make it a form, it will save the form into the orderitem
356
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
357
     *
358
     * @return false | DataObject (OrderItem)
359
     */
360
    public function addBuyable(BuyableModel $buyable, $quantity = 1, $parameters = array())
361
    {
362
        if($this->allowWrites()) {
363
            if (!$buyable) {
364
                $this->addMessage(_t('Order.ITEMCOULDNOTBEFOUND', 'This item could not be found.'), 'bad');
365
                return false;
366
            }
367
            if (!$buyable->canPurchase()) {
368
                $this->addMessage(_t('Order.ITEMCOULDNOTBEADDED', 'This item is not for sale.'), 'bad');
369
                return false;
370
            }
371
            $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = false);
372
            $quantity = $this->prepareQuantity($buyable, $quantity);
373
            if ($item && $quantity) { //find existing order item or make one
374
                $item->Quantity += $quantity;
375
                $item->write();
376
                $this->currentOrder()->Attributes()->add($item); //save to current order
377
                //TODO: distinquish between incremented and set
378
                //TODO: use sprintf to allow product name etc to be included in message
379
                if ($quantity > 1) {
380
                    $msg = _t('Order.ITEMSADDED', 'Items added.');
381
                } else {
382
                    $msg = _t('Order.ITEMADDED', 'Item added.');
383
                }
384
                $this->addMessage($msg, 'good');
385
            } elseif (!$item) {
386
                $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
387
            } else {
388
                $this->addMessage(_t('Order.ITEMCOULDNOTBEADDED', 'Item could not be added.'), 'bad');
389
            }
390
391
            return $item;
392
        }
393
    }
394
395
    /**
396
     * Sets quantity for an item in the cart.
397
     *
398
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
399
     *
400
     * @param DataObject $buyable    - the buyable (generally a product) being added to the cart
401
     * @param float      $quantity   - number of items add.
402
     * @param array      $parameters - array of parameters to target a specific order item. eg: group=1, length=5
403
     *
404
     * @return false | DataObject (OrderItem) | null
405
     */
406
    public function setQuantity(BuyableModel $buyable, $quantity, array $parameters = array())
407
    {
408
        if($this->allowWrites()) {
409
            $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = false);
410
            $quantity = $this->prepareQuantity($buyable, $quantity);
411
            if ($item) {
412
                $item->Quantity = $quantity; //remove quantity
413
                $item->write();
414
                $this->addMessage(_t('Order.ITEMUPDATED', 'Item updated.'), 'good');
415
416
                return $item;
417
            } else {
418
                $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
419
            }
420
421
            return false;
422
        }
423
    }
424
425
    /**
426
     * Removes any number of items from the cart.
427
     *
428
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
429
     *
430
     * @param DataObject $buyable    - the buyable (generally a product) being added to the cart
431
     * @param float      $quantity   - number of items add.
432
     * @param array      $parameters - array of parameters to target a specific order item. eg: group=1, length=5
433
     *
434
     * @return false | OrderItem | null
435
     */
436
    public function decrementBuyable(BuyableModel $buyable, $quantity = 1, array $parameters = array())
437
    {
438
        if($this->allowWrites()) {
439
            $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = false);
440
            $quantity = $this->prepareQuantity($buyable, $quantity);
441
            if ($item) {
442
                $item->Quantity -= $quantity; //remove quantity
443
                if ($item->Quantity < 0) {
444
                    $item->Quantity = 0;
445
                }
446
                $item->write();
447
                if ($quantity > 1) {
448
                    $msg = _t('Order.ITEMSREMOVED', 'Items removed.');
449
                } else {
450
                    $msg = _t('Order.ITEMREMOVED', 'Item removed.');
451
                }
452
                $this->addMessage($msg, 'good');
453
454
                return $item;
455
            } else {
456
                $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
457
            }
458
459
            return false;
460
        }
461
462
    }
463
464
    /**
465
     * Delete item from the cart.
466
     *
467
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
468
     *
469
     * @param OrderItem $buyable    - the buyable (generally a product) being added to the cart
470
     * @param array     $parameters - array of parameters to target a specific order item. eg: group=1, length=5
471
     *
472
     * @return bool | item | null - successfully removed
473
     */
474
    public function deleteBuyable(BuyableModel $buyable, array $parameters = array())
475
    {
476
        if($this->allowWrites()) {
477
            $item = $this->prepareOrderItem($buyable, $parameters, $mustBeExistingItem = true);
478
            if ($item) {
479
                $this->currentOrder()->Attributes()->remove($item);
480
                $item->delete();
481
                $item->destroy();
482
                $this->addMessage(_t('Order.ITEMCOMPLETELYREMOVED', 'Item removed from cart.'), 'good');
483
484
                return $item;
485
            } else {
486
                $this->addMessage(_t('Order.ITEMNOTFOUND', 'Item could not be found.'), 'bad');
487
488
                return false;
489
            }
490
        }
491
    }
492
493
    /**
494
     * Checks and prepares variables for a quantity change (add, edit, remove) for an Order Item.
495
     *
496
     * @param DataObject    $buyable             - the buyable (generally a product) being added to the cart
497
     * @param float         $quantity            - number of items add.
498
     * @param bool          $mustBeExistingItems - if false, the Order Item gets created if it does not exist - if TRUE the order item is searched for and an error shows if there is no Order item.
499
     * @param array | Form  $parameters          - array of parameters to target a specific order item. eg: group=1, length=5*
500
     *                                           - form saved into item...
501
     *
502
     * @return bool | DataObject ($orderItem)
503
     */
504
    protected function prepareOrderItem(BuyableModel $buyable, $parameters = array(), $mustBeExistingItem = true)
505
    {
506
        $parametersArray = $parameters;
507
        $form = null;
508
        if ($parameters instanceof Form) {
509
            $parametersArray = array();
510
            $form = $parameters;
511
        }
512
        if (!$buyable) {
513
            user_error('No buyable was provided', E_USER_WARNING);
514
        }
515
        if (!$buyable->canPurchase()) {
516
            $item = $this->getExistingItem($buyable, $parametersArray);
517
            if ($item && $item->exists()) {
518
                $item->delete();
519
                $item->destroy();
520
            }
521
522
            return false;
523
        }
524
        $item = null;
525
        if ($mustBeExistingItem) {
526
            $item = $this->getExistingItem($buyable, $parametersArray);
527
        } else {
528
            $item = $this->findOrMakeItem($buyable, $parametersArray); //find existing order item or make one
529
        }
530
        if (!$item) {
531
            //check for existence of item
532
            return false;
533
        }
534
        if ($form) {
535
            $form->saveInto($item);
536
        }
537
538
        return $item;
539
    }
540
541
    /**
542
     * @todo: what does this method do???
543
     *
544
     * @return int
545
     *
546
     * @param DataObject ($buyable)
547
     * @param float $quantity
548
     */
549
    protected function prepareQuantity(BuyableModel $buyable, $quantity)
550
    {
551
        $quantity = round($quantity, $buyable->QuantityDecimals());
552
        if ($quantity < 0 || (!$quantity && $quantity !== 0)) {
553
            $this->addMessage(_t('Order.INVALIDQUANTITY', 'Invalid quantity.'), 'warning');
554
555
            return false;
556
        }
557
558
        return $quantity;
559
    }
560
561
    /**
562
     * Helper function for making / retrieving order items.
563
     * we do not need things like "canPurchase" here, because that is with the "addBuyable" method.
564
     * NOTE: does not write!
565
     *
566
     * @param DataObject $buyable
567
     * @param array      $parameters
568
     *
569
     * @return OrderItem
570
     */
571
    public function findOrMakeItem(BuyableModel $buyable, array $parameters = array())
572
    {
573
        if($this->allowWrites()) {
574
            if ($item = $this->getExistingItem($buyable, $parameters)) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
575
                //do nothing
576
            } else {
577
                //otherwise create a new item
578
                if (!($buyable instanceof BuyableModel)) {
579
                    $this->addMessage(_t('ShoppingCart.ITEMNOTFOUND', 'Item is not buyable.'), 'bad');
580
581
                    return false;
582
                }
583
                $className = $buyable->classNameForOrderItem();
584
                $item = new $className();
585
                if ($order = $this->currentOrder()) {
586
                    $item->OrderID = $order->ID;
587
                    $item->BuyableID = $buyable->ID;
588
                    $item->BuyableClassName = $buyable->ClassName;
589
                    if (isset($buyable->Version)) {
590
                        $item->Version = $buyable->Version;
591
                    }
592
                }
593
            }
594
            if ($parameters) {
595
                $item->Parameters = $parameters;
596
            }
597
598
            return $item;
599
        } else {
600
            return OrderItem::create();
601
        }
602
    }
603
604
    /**
605
     * submit the order so that it is no longer available
606
     * in the cart but will continue its journey through the
607
     * order steps.
608
     *
609
     * @return bool
610
     */
611
    public function submit()
612
    {
613
        if($this->allowWrites()) {
614
            $this->currentOrder()->tryToFinaliseOrder();
615
            $this->clear();
616
            //little hack to clear static memory
617
            OrderItem::reset_price_has_been_fixed($this->currentOrder()->ID);
618
619
            return true;
620
        }
621
622
        return false;
623
    }
624
625
    /**
626
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
627
     *
628
     * @return bool | null
629
     */
630
    public function save()
631
    {
632
        if($this->allowWrites()) {
633
            $this->currentOrder()->write();
634
            $this->addMessage(_t('Order.ORDERSAVED', 'Order Saved.'), 'good');
635
636
            return true;
637
        }
638
    }
639
640
    /**
641
     * Clears the cart contents completely by removing the orderID from session, and
642
     * thus creating a new cart on next request.
643
     *
644
     * @return bool
645
     */
646
    public function clear()
647
    {
648
        //we keep this here so that a flush can be added...
649
        set_time_limit(1 * 60);
650
        self::$_singletoncart = null;
651
        $this->order = null;
652
        $this->messages = array();
653
        foreach (self::$session_variable_names as $name) {
654
            $sessionVariableName = $this->sessionVariableName($name);
655
            Session::set($sessionVariableName, null);
656
            Session::clear($sessionVariableName);
657
            Session::save();
658
        }
659
        $memberID = Intval(Member::currentUserID());
660
        if ($memberID) {
661
            $orders = Order::get()->filter(array('MemberID' => $memberID));
662
            if ($orders && $orders->count()) {
663
                foreach ($orders as $order) {
664
                    if (! $order->IsSubmitted()) {
665
                        $order->delete();
666
                    }
667
                }
668
            }
669
        }
670
671
        return true;
672
    }
673
674
    /**
675
     * alias for clear.
676
     */
677
    public function reset()
678
    {
679
        return $this->clear();
680
    }
681
682
    /**
683
     * Removes a modifier from the cart
684
     * It does not actually remove it, but it just
685
     * sets it as "removed", to avoid that it is being
686
     * added again.
687
     *
688
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
689
     *
690
     * @param OrderModifier $modifier
691
     *
692
     * @return bool | null
693
     */
694
    public function removeModifier(OrderModifier $modifier)
695
    {
696
        if($this->allowWrites()) {
697
            $modifier = (is_numeric($modifier)) ? OrderModifier::get()->byID($modifier) : $modifier;
698
            if (!$modifier) {
699
                $this->addMessage(_t('Order.MODIFIERNOTFOUND', 'Modifier could not be found.'), 'bad');
700
701
                return false;
702
            }
703
            if (!$modifier->CanBeRemoved()) {
704
                $this->addMessage(_t('Order.MODIFIERCANNOTBEREMOVED', 'Modifier can not be removed.'), 'bad');
705
706
                return false;
707
            }
708
            $modifier->HasBeenRemoved = 1;
709
            $modifier->onBeforeRemove();
710
            $modifier->write();
711
            $modifier->onAfterRemove();
712
            $this->addMessage(_t('Order.MODIFIERREMOVED', 'Removed.'), 'good');
713
714
            return true;
715
        }
716
    }
717
718
    /**
719
     * Removes a modifier from the cart.
720
     *
721
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
722
     *
723
     * @param Int/ OrderModifier
724
     *
725
     * @return bool
726
     */
727
    public function addModifier($modifier)
728
    {
729
        if($this->allowWrites()) {
730
            if (is_numeric($modifier)) {
731
                $modifier = OrderModifier::get()->byID($modifier);
732
            } elseif (!(is_a($modifier, Object::getCustomClass('OrderModifier')))) {
733
                user_error('Bad parameter provided to ShoppingCart::addModifier', E_USER_WARNING);
734
            }
735
            if (!$modifier) {
736
                $this->addMessage(_t('Order.MODIFIERNOTFOUND', 'Modifier could not be found.'), 'bad');
737
738
                return false;
739
            }
740
            $modifier->HasBeenRemoved = 0;
741
            $modifier->write();
742
            $this->addMessage(_t('Order.MODIFIERREMOVED', 'Added.'), 'good');
743
744
            return true;
745
        }
746
    }
747
748
    /**
749
     * Sets an order as the current order.
750
     *
751
     * @param int | Order $order
752
     *
753
     * @return bool
754
     */
755
    public function loadOrder($order)
756
    {
757
        if($this->allowWrites()) {
758
            //TODO: how to handle existing order
759
            //TODO: permission check - does this belong to another member? ...or should permission be assumed already?
760
            if (is_numeric($order)) {
761
                $this->order = Order::get()->byID($order);
762
            } elseif (is_a($order, Object::getCustomClass('Order'))) {
763
                $this->order = $order;
764
            } else {
765
                user_error('Bad order provided as parameter to ShoppingCart::loadOrder()');
766
            }
767
            if ($this->order) {
768
                //first can view and then, if can view, set as session...
769
                if ($this->order->canView()) {
770
                    $this->order->init(true);
771
                    $sessionVariableName = $this->sessionVariableName('OrderID');
772
                    //we set session ID after can view check ...
773
                    Session::set($sessionVariableName, $this->order->ID);
774
                    $this->addMessage(_t('Order.LOADEDEXISTING', 'Order loaded.'), 'good');
775
776
                    return true;
777
                } else {
778
                    $this->addMessage(_t('Order.NOPERMISSION', 'You do not have permission to view this order.'), 'bad');
779
780
                    return false;
781
                }
782
            } else {
783
                $this->addMessage(_t('Order.NOORDER', 'Order can not be found.'), 'bad');
784
785
                return false;
786
            }
787
        } else {
788
            $this->addMessage(_t('Order.NOSAVE', 'You can not load orders as your session functionality is turned off.'), 'bad');
789
790
            return false;
791
        }
792
793
    }
794
795
    /**
796
     * NOTE: tried to copy part to the Order Class - but that was not much of a go-er.
797
     *
798
     * returns null if the current user does not allow order manipulation or saving (e.g. session disabled)
799
     *
800
     * @param int | Order $order
801
     *
802
     * @return Order | false | null
803
     **/
804
    public function copyOrder($oldOrder)
805
    {
806
        if($this->allowWrites()) {
807
            if (is_numeric($oldOrder)) {
808
                $oldOrder = Order::get()->byID(intval($oldOrder));
809
            } elseif (is_a($oldOrder, Object::getCustomClass('Order'))) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
810
                //$oldOrder = $oldOrder;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
811
            } else {
812
                user_error('Bad order provided as parameter to ShoppingCart::loadOrder()');
813
            }
814
            if ($oldOrder) {
815
                if ($oldOrder->canView() && $oldOrder->IsSubmitted()) {
816
                    $newOrder = Order::create();
817
                    //copying fields.
818
                    $newOrder->UseShippingAddress = $oldOrder->UseShippingAddress;
819
                    //important to set it this way...
820
                    $newOrder->setCurrency($oldOrder->CurrencyUsed());
821
                    $newOrder->MemberID = $oldOrder->MemberID;
822
                    //load the order
823
                    $newOrder->write();
824
                    $this->loadOrder($newOrder);
825
                    $items = OrderItem::get()
826
                        ->filter(array('OrderID' => $oldOrder->ID));
827
                    if ($items->count()) {
828
                        foreach ($items as $item) {
829
                            $buyable = $item->Buyable($current = true);
830
                            if ($buyable->canPurchase()) {
831
                                $this->addBuyable($buyable, $item->Quantity);
832
                            }
833
                        }
834
                    }
835
                    $newOrder->CreateOrReturnExistingAddress('BillingAddress');
836
                    $newOrder->CreateOrReturnExistingAddress('ShippingAddress');
837
                    $newOrder->write();
838
                    $this->addMessage(_t('Order.ORDERCOPIED', 'Order has been copied.'), 'good');
839
840
                    return $newOrder;
841
                } else {
842
                    $this->addMessage(_t('Order.NOPERMISSION', 'You do not have permission to view this order.'), 'bad');
843
844
                    return false;
845
                }
846
            } else {
847
                $this->addMessage(_t('Order.NOORDER', 'Order can not be found.'), 'bad');
848
849
                return false;
850
            }
851
        }
852
    }
853
854
    /**
855
     * sets country in order so that modifiers can be recalculated, etc...
856
     *
857
     * @param string - $countryCode
858
     *
859
     * @return bool
860
     **/
861
    public function setCountry($countryCode)
862
    {
863
        if($this->allowWrites()) {
864
            if (EcommerceCountry::code_allowed($countryCode)) {
865
                $this->currentOrder()->SetCountryFields($countryCode);
866
                $this->addMessage(_t('Order.UPDATEDCOUNTRY', 'Updated country.'), 'good');
867
868
                return true;
869
            } else {
870
                $this->addMessage(_t('Order.NOTUPDATEDCOUNTRY', 'Could not update country.'), 'bad');
871
872
                return false;
873
            }
874
        }
875
    }
876
877
    /**
878
     * sets region in order so that modifiers can be recalculated, etc...
879
     *
880
     * @param int | String - $regionID you can use the ID or the code.
881
     *
882
     * @return bool
883
     **/
884
    public function setRegion($regionID)
885
    {
886
        if (EcommerceRegion::regionid_allowed($regionID)) {
887
            $this->currentOrder()->SetRegionFields($regionID);
888
            $this->addMessage(_t('ShoppingCart.REGIONUPDATED', 'Region updated.'), 'good');
889
890
            return true;
891
        } else {
892
            $this->addMessage(_t('ORDER.NOTUPDATEDREGION', 'Could not update region.'), 'bad');
893
894
            return false;
895
        }
896
    }
897
898
    /**
899
     * sets the display currency for the cart.
900
     *
901
     * @param string $currencyCode
902
     *
903
     * @return bool
904
     **/
905
    public function setCurrency($currencyCode)
906
    {
907
        $currency = EcommerceCurrency::get_one_from_code($currencyCode);
908
        if ($currency) {
909
            if ($this->currentOrder()->MemberID) {
910
                $member = $this->currentOrder()->Member();
911
                if ($member && $member->exists()) {
912
                    $member->SetPreferredCurrency($currency);
913
                }
914
            }
915
            $this->currentOrder()->UpdateCurrency($currency);
916
            $msg = _t('Order.CURRENCYUPDATED', 'Currency updated.');
917
            $this->addMessage($msg, 'good');
918
919
            return true;
920
        } else {
921
            $msg = _t('Order.CURRENCYCOULDNOTBEUPDATED', 'Currency could not be updated.');
922
            $this->addMessage($msg, 'bad');
923
924
            return false;
925
        }
926
    }
927
928
    /**
929
     * Produces a debug of the shopping cart.
930
     */
931
    public function debug()
932
    {
933
        if (Director::isDev() || Permission::check('ADMIN')) {
934
            debug::show($this->currentOrder());
935
936
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Country</h1>';
937
            echo 'GEOIP Country: '.EcommerceCountry::get_country_from_ip().'<br />';
938
            echo 'Calculated Country Country: '.EcommerceCountry::get_country().'<br />';
939
940
            echo '<blockquote><blockquote><blockquote><blockquote>';
941
942
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Items</h1>';
943
            $items = $this->currentOrder()->Items();
944
            echo $items->sql();
945
            echo '<hr />';
946
            if ($items->count()) {
947
                foreach ($items as $item) {
948
                    Debug::show($item);
949
                }
950
            } else {
951
                echo '<p>there are no items for this order</p>';
952
            }
953
954
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Modifiers</h1>';
955
            $modifiers = $this->currentOrder()->Modifiers();
956
            if ($modifiers->count()) {
957
                foreach ($modifiers as $modifier) {
958
                    Debug::show($modifier);
959
                }
960
            } else {
961
                echo '<p>there are no modifiers for this order</p>';
962
            }
963
964
            echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Addresses</h1>';
965
            $billingAddress = $this->currentOrder()->BillingAddress();
966
            if ($billingAddress && $billingAddress->exists()) {
967
                Debug::show($billingAddress);
968
            } else {
969
                echo '<p>there is no billing address for this order</p>';
970
            }
971
            $shippingAddress = $this->currentOrder()->ShippingAddress();
972
            if ($shippingAddress && $shippingAddress->exists()) {
973
                Debug::show($shippingAddress);
974
            } else {
975
                echo '<p>there is no shipping address for this order</p>';
976
            }
977
978
            $currencyUsed = $this->currentOrder()->CurrencyUsed();
979
            if ($currencyUsed && $currencyUsed->exists()) {
980
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Currency</h1>';
981
                Debug::show($currencyUsed);
982
            }
983
984
            $cancelledBy = $this->currentOrder()->CancelledBy();
985
            if ($cancelledBy && $cancelledBy->exists()) {
986
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Cancelled By</h1>';
987
                Debug::show($cancelledBy);
988
            }
989
990
            $logs = $this->currentOrder()->OrderStatusLogs();
991
            if ($logs && $logs->count()) {
992
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Logs</h1>';
993
                foreach ($logs as $log) {
994
                    Debug::show($log);
995
                }
996
            }
997
998
            $payments = $this->currentOrder()->Payments();
999
            if ($payments  && $payments->count()) {
1000
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Payments</h1>';
1001
                foreach ($payments as $payment) {
1002
                    Debug::show($payment);
1003
                }
1004
            }
1005
1006
            $emails = $this->currentOrder()->Emails();
1007
            if ($emails && $emails->count()) {
1008
                echo '<hr /><hr /><hr /><hr /><hr /><hr /><h1>Emails</h1>';
1009
                foreach ($emails as $email) {
1010
                    Debug::show($email);
1011
                }
1012
            }
1013
1014
            echo '</blockquote></blockquote></blockquote></blockquote>';
1015
        } else {
1016
            echo 'Please log in as admin first';
1017
        }
1018
    }
1019
1020
    /**
1021
     * Stores a message that can later be returned via ajax or to $form->sessionMessage();.
1022
     *
1023
     * @param $message - the message, which could be a notification of successful action, or reason for failure
1024
     * @param $type - please use good, bad, warning
1025
     */
1026
    public function addMessage($message, $status = 'good')
1027
    {
1028
        //clean status for the lazy programmer
1029
        //TODO: remove the awkward replace
1030
        $status = strtolower($status);
1031
        str_replace(array('success', 'failure'), array('good', 'bad'), $status);
1032
        $statusOptions = array('good', 'bad', 'warning');
1033
        if (!in_array($status, $statusOptions)) {
1034
            user_error('Message status should be one of the following: '.implode(',', $statusOptions), E_USER_NOTICE);
1035
        }
1036
        $this->messages[] = array(
1037
            'Message' => $message,
1038
            'Type' => $status,
1039
        );
1040
    }
1041
1042
    /*******************************************************
1043
    * HELPER FUNCTIONS
1044
    *******************************************************/
1045
1046
    /**
1047
     * Gets an existing order item based on buyable and passed parameters.
1048
     *
1049
     * @param DataObject $buyable
1050
     * @param array      $parameters
1051
     *
1052
     * @return OrderItem | null
1053
     */
1054
    protected function getExistingItem(BuyableModel $buyable, array $parameters = array())
1055
    {
1056
        $filterString = $this->parametersToSQL($parameters);
1057
        if ($order = $this->currentOrder()) {
1058
            $orderID = $order->ID;
1059
            $obj = OrderItem::get()
1060
                ->where(
1061
                    " \"BuyableClassName\" = '".$buyable->ClassName."' AND
1062
                    \"BuyableID\" = ".$buyable->ID.' AND
1063
                    "OrderID" = '.$orderID.' '.
1064
                    $filterString
1065
                )
1066
                ->First();
1067
1068
            return $obj;
1069
        }
1070
    }
1071
1072
    /**
1073
     * Removes parameters that aren't in the default array, merges with default parameters, and converts raw2SQL.
1074
     *
1075
     * @param array $parameters
1076
     *
1077
     * @return cleaned array
1078
     */
1079
    protected function cleanParameters(array $params = array())
1080
    {
1081
        $defaultParamFilters = EcommerceConfig::get('ShoppingCart', 'default_param_filters');
1082
        $newarray = array_merge(array(), $defaultParamFilters); //clone array
1083
        if (!count($newarray)) {
1084
            return array(); //no use for this if there are not parameters defined
1085
        }
1086
        foreach ($newarray as $field => $value) {
1087
            if (isset($params[$field])) {
1088
                $newarray[$field] = Convert::raw2sql($params[$field]);
1089
            }
1090
        }
1091
1092
        return $newarray;
1093
    }
1094
1095
    /**
1096
     * @param array $parameters
1097
     *                          Converts parameter array to SQL query filter
1098
     */
1099
    protected function parametersToSQL(array $parameters = array())
1100
    {
1101
        $defaultParamFilters = EcommerceConfig::get('ShoppingCart', 'default_param_filters');
1102
        if (!count($defaultParamFilters)) {
1103
            return ''; //no use for this if there are not parameters defined
1104
        }
1105
        $cleanedparams = $this->cleanParameters($parameters);
1106
        $outputArray = array();
1107
        foreach ($cleanedparams as $field => $value) {
1108
            $outputarray[$field] = '"'.$field.'" = '.$value;
1109
        }
1110
        if (count($outputArray)) {
1111
            return implode(' AND ', $outputArray);
1112
        }
1113
1114
        return '';
1115
    }
1116
1117
    /*******************************************************
1118
    * UI MESSAGE HANDLING
1119
    *******************************************************/
1120
1121
    /**
1122
     * Retrieves all good, bad, and ugly messages that have been produced during the current request.
1123
     *
1124
     * @return array of messages
1125
     */
1126
    public function getMessages()
1127
    {
1128
        $sessionVariableName = $this->sessionVariableName('Messages');
1129
        //get old messages
1130
        $messages = unserialize(Session::get($sessionVariableName));
1131
        //clear old messages
1132
        Session::clear($sessionVariableName, '');
1133
        //set to form????
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1134
        if ($messages && count($messages)) {
1135
            $this->messages = array_merge($messages, $this->messages);
1136
        }
1137
1138
        return $this->messages;
1139
    }
1140
1141
    /**
1142
     *Saves current messages in session for retrieving them later.
1143
     *
1144
     *@return array of messages
1145
     */
1146
    protected function StoreMessagesInSession()
1147
    {
1148
        $sessionVariableName = $this->sessionVariableName('Messages');
1149
        Session::set($sessionVariableName, serialize($this->messages));
1150
    }
1151
1152
    /**
1153
     * This method is used to return data after an ajax call was made.
1154
     * When a asynchronious request is made to the shopping cart (ajax),
1155
     * then you will first action the request and then use this function
1156
     * to return some values.
1157
     *
1158
     * It can also be used without ajax, in wich case it will redirects back
1159
     * to the last page.
1160
     *
1161
     * Note that you can set the ajax response class in the configuration file.
1162
     *
1163
     *
1164
     * @param string $message
1165
     * @param string $status
1166
     * @param Form   $form
1167
     * @returns String (JSON)
1168
     */
1169
    public function setMessageAndReturn($message = '', $status = '', Form $form = null)
1170
    {
1171
        if ($message && $status) {
1172
            $this->addMessage($message, $status);
1173
        }
1174
        //TODO: handle passing back multiple messages
1175
1176
        if (Director::is_ajax()) {
1177
            $responseClass = EcommerceConfig::get('ShoppingCart', 'response_class');
1178
            $obj = new $responseClass();
1179
1180
            return $obj->ReturnCartData($this->getMessages());
1181
        } else {
1182
            //TODO: handle passing a message back to a form->sessionMessage
1183
            $this->StoreMessagesInSession();
1184
            if ($form) {
1185
                //lets make sure that there is an order
1186
                $this->currentOrder();
1187
                //nowe we can (re)calculate the order
1188
                $this->order->calculateOrderAttributes($force = false);
1189
                $form->sessionMessage($message, $status);
1190
                //let the form controller do the redirectback or whatever else is needed.
1191
            } else {
1192
                if (empty($_REQUEST['BackURL']) && Controller::has_curr()) {
1193
                    Controller::curr()->redirectBack();
1194
                } else {
1195
                    Controller::curr()->redirect(urldecode($_REQUEST['BackURL']));
1196
                }
1197
            }
1198
1199
            return;
1200
        }
1201
    }
1202
1203
    /**
1204
     * @return EcommerceDBConfig
1205
     */
1206
    protected function EcomConfig()
1207
    {
1208
        return EcommerceDBConfig::current_ecommerce_db_config();
1209
    }
1210
1211
    /**
1212
     * Return the name of the session variable that should be used.
1213
     *
1214
     * @param string $name
1215
     *
1216
     * @return string
1217
     */
1218
    protected function sessionVariableName($name = '')
1219
    {
1220
        if (!in_array($name, self::$session_variable_names)) {
1221
            user_error("Tried to set session variable $name, that is not in use", E_USER_NOTICE);
1222
        }
1223
        $sessionCode = EcommerceConfig::get('ShoppingCart', 'session_code');
1224
1225
        return $sessionCode.'_'.$name;
1226
    }
1227
}
1228