Completed
Push — master ( e03318...346425 )
by Bram
01:48
created

Reservation::calculateTotal()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 2
nop 0
1
<?php
2
/**
3
 * Reservation.php
4
 *
5
 * @author Bram de Leeuw
6
 * Date: 09/03/17
7
 */
8
9
namespace Broarm\EventTickets;
10
11
use BetterButtonCustomAction;
12
use CalendarEvent;
13
use CheckboxField;
14
use Config;
15
use DataObject;
16
use DropdownField;
17
use Email;
18
use FieldList;
19
use Folder;
20
use GridField;
21
use GridFieldConfig_RecordViewer;
22
use HasManyList;
23
use ManyManyList;
24
use ReadonlyField;
25
use SilverStripe\Omnipay\GatewayInfo;
26
use SiteConfig;
27
use Tab;
28
use TabSet;
29
30
/**
31
 * Class Reservation
32
 *
33
 * @package Broarm\EventTickets
34
 *
35
 * @property string Status
36
 * @property string Title
37
 * @property float  Subtotal
38
 * @property float  Total
39
 * @property string Comments
40
 * @property string ReservationCode
41
 * @property string Gateway
42
 *
43
 * @property int    EventID
44
 * @property int    MainContactID
45
 *
46
 * @method CalendarEvent|TicketExtension Event()
47
 * @method Attendee MainContact()
48
 * @method HasManyList Payments()
49
 * @method HasManyList Attendees()
50
 * @method ManyManyList PriceModifiers()
51
 */
52
class Reservation extends DataObject
53
{
54
    /**
55
     * Time to wait before deleting the discarded cart
56
     * Give a string that is parsable by strtotime
57
     *
58
     * @var string
59
     */
60
    private static $delete_after = '+1 hour';
61
62
    /**
63
     * The address to whom the ticket notifications are sent
64
     * By default the admin email is used
65
     *
66
     * @config
67
     * @var string
68
     */
69
    private static $mail_sender;
70
71
    /**
72
     * The address from where the ticket mails are sent
73
     * By default the admin email is used
74
     *
75
     * @config
76
     * @var string
77
     */
78
    private static $mail_receiver;
79
80
    private static $db = array(
81
        'Status' => 'Enum("CART,PENDING,PAID,CANCELED","CART")',
82
        'Title' => 'Varchar(255)',
83
        'Subtotal' => 'Currency',
84
        'Total' => 'Currency',
85
        'Gateway' => 'Varchar(255)',
86
        'Comments' => 'Text',
87
        'AgreeToTermsAndConditions' => 'Boolean',
88
        'ReservationCode' => 'Varchar(255)'
89
    );
90
91
    private static $default_sort = 'Created DESC';
92
93
    private static $has_one = array(
94
        'Event' => 'CalendarEvent',
95
        'MainContact' => 'Broarm\EventTickets\Attendee'
96
    );
97
98
    private static $has_many = array(
99
        'Payments' => 'Payment',
100
        'Attendees' => 'Broarm\EventTickets\Attendee.Reservation'
101
    );
102
103
    private static $belongs_many_many = array(
104
        'PriceModifiers' => 'Broarm\EventTickets\PriceModifier'
105
    );
106
107
    private static $indexes = array(
108
        'ReservationCode' => 'unique("ReservationCode")'
109
    );
110
111
    private static $summary_fields = array(
112
        'ReservationCode' => 'Reservation',
113
        'Title' => 'Customer',
114
        'Total.Nice' => 'Total',
115
        'State' => 'Status',
116
        'GatewayNice' => 'Payment method',
117
        'Created.Nice' => 'Date'
118
    );
119
120
    /**
121
     * Actions usable on the cms detail view
122
     *
123
     * @var array
124
     */
125
    private static $better_buttons_actions = array(
126
        'send'
127
    );
128
129
    public function getCMSFields()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
130
    {
131
        $fields = new FieldList(new TabSet('Root', $mainTab = new Tab('Main')));
132
        $gridFieldConfig = GridFieldConfig_RecordViewer::create();
133
        $fields->addFieldsToTab('Root.Main', array(
134
            ReadonlyField::create('ReservationCode', _t('Reservation.Code', 'Code')),
135
            ReadonlyField::create('Created', _t('Reservation.Created', 'Date')),
136
            DropdownField::create('Status', _t('Reservation.Status', 'Status'), $this->getStates()),
137
            ReadonlyField::create('Title', _t('Reservation.MainContact', 'Main contact')),
138
            ReadonlyField::create('GateWayNice', _t('Reservation.Gateway', 'Gateway')),
139
            ReadonlyField::create('Total', _t('Reservation.Total', 'Total')),
140
            ReadonlyField::create('Comments', _t('Reservation.Comments', 'Comments')),
141
            CheckboxField::create('AgreeToTermsAndConditions', _t('Reservation.AgreeToTermsAndConditions', 'Agreed to terms and conditions'))->performReadonlyTransformation(),
142
            GridField::create('Attendees', 'Attendees', $this->Attendees(), $gridFieldConfig),
143
            GridField::create('Payments', 'Payments', $this->Payments(), $gridFieldConfig)
144
        ));
145
        $fields->addFieldsToTab('Root.Main', array());
146
        $this->extend('updateCMSFields', $fields);
147
        return $fields;
148
    }
149
150
    /**
151
     * Add utility actions to the reservation details view
152
     *
153
     * @return FieldList
154
     */
155
    public function getBetterButtonsActions()
156
    {
157
        /** @var FieldList $fields */
158
        $fields = parent::getBetterButtonsActions();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class DataObject as the method getBetterButtonsActions() does only exist in the following sub-classes of DataObject: Broarm\EventTickets\Reservation. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
159
        $fields->push(BetterButtonCustomAction::create('send', _t('Reservation.RESEND', 'Resend the reservation')));
160
161
        return $fields;
162
    }
163
164
    /**
165
     * Generate a reservation code if it does not yet exists
166
     */
167
    public function onBeforeWrite()
168
    {
169
        // Set the title to the name of the reservation holder
170
        $this->Title = $this->getName();
171
172
        // Create a validation code to be used for confirmation and in the barcode
173
        if ($this->exists() && empty($this->ReservationCode)) {
174
            $this->ReservationCode = $this->createReservationCode();
175
        }
176
177
        parent::onBeforeWrite();
178
    }
179
180
    /**
181
     * After deleting a reservation, delete the attendees and files
182
     */
183
    public function onBeforeDelete()
184
    {
185
        // If a reservation is deleted remove the names from the guest list
186
        foreach ($this->Attendees() as $attendee) {
187
            /** @var Attendee $attendee */
188
            if ($attendee->exists()) {
189
                $attendee->delete();
190
            }
191
        }
192
193
        // Remove the folder
194
        if (($folder = Folder::get()->find('Name', $this->ReservationCode)) && $folder->exists() && $folder->isEmpty()) {
195
            $folder->delete();
196
        }
197
198
        parent::onBeforeDelete();
199
    }
200
201
    /**
202
     * Gets a nice unnamespaced name
203
     *
204
     * @return string
205
     */
206
    public function singular_name()
207
    {
208
        $name = explode('\\', parent::singular_name());
209
        return trim(end($name));
210
    }
211
212
    /**
213
     * Returns the nice gateway title
214
     *
215
     * @return string
216
     */
217
    public function getGatewayNice()
218
    {
219
        return GatewayInfo::niceTitle($this->Gateway);
220
    }
221
222
    /**
223
     * Check if the cart is still in cart state and the delete_after time period has been exceeded
224
     *
225
     * @return bool
226
     */
227
    public function isDiscarded()
228
    {
229
        $deleteAfter = strtotime(self::config()->get('delete_after'), strtotime($this->Created));
230
        return ($this->Status === 'CART') && (time() > $deleteAfter);
231
    }
232
233
    /**
234
     * Get the full name
235
     *
236
     * @return string
237
     */
238
    public function getName()
239
    {
240
        /** @var Attendee $attendee */
241
        if (($mainContact = $this->MainContact()) && $mainContact->exists() && $name = $mainContact->getName()) {
242
            return $name;
243
        } else {
244
            return 'new reservation';
245
        }
246
    }
247
248
    /**
249
     * Return the translated state
250
     *
251
     * @return string
252
     */
253
    public function getState()
254
    {
255
        return _t("Reservation.{$this->Status}", $this->Status);
256
    }
257
258
    /**
259
     * Get a the translated map of available states
260
     *
261
     * @return array
262
     */
263
    private function getStates()
264
    {
265
        return array_map(function ($state) {
266
            return _t("Reservation.$state", $state);
267
        }, $this->dbObject('Status')->enumValues());
268
    }
269
270
    /**
271
     * Get the total by querying the sum of attendee ticket prices
272
     *
273
     * @return float
274
     */
275
    public function calculateTotal()
276
    {
277
        $total = $this->Subtotal = $this->Attendees()->leftJoin(
278
            'Broarm\EventTickets\Ticket',
279
            '`Broarm\EventTickets\Attendee`.`TicketID` = `Broarm\EventTickets\Ticket`.`ID`'
280
        )->sum('Price');
281
282
        // Calculate any price modifications if added
283
        if ($this->PriceModifiers()->exists()) {
284
            foreach ($this->PriceModifiers() as $priceModifier) {
285
                $priceModifier->updateTotal($total);
286
            }
287
        }
288
289
        return $this->Total = $total;
290
    }
291
292
    /**
293
     * Safely change to a state
294
     * todo check if state direction matches
295
     *
296
     * @param $state
297
     *
298
     * @return boolean
299
     */
300
    public function changeState($state)
301
    {
302
        $availableStates = $this->dbObject('Status')->enumValues();
303
        if (in_array($state, $availableStates)) {
304
            $this->Status = $state;
305
            return true;
306
        } else {
307
            user_error(_t('Reservation.STATE_CHANGE_ERROR', 'Selected state is not available'));
308
            return false;
309
        }
310
    }
311
312
    /**
313
     * Set the main contact id
314
     *
315
     * @param $id
316
     */
317
    public function setMainContact($id)
318
    {
319
        $this->MainContactID = $id;
320
        $this->write();
321
    }
322
323
    /**
324
     * Create a reservation code
325
     *
326
     * @return string
327
     */
328
    public function createReservationCode()
329
    {
330
        return uniqid($this->ID);
331
    }
332
333
    /**
334
     * Create the folder for the qr code and ticket file
335
     *
336
     * @return Folder|DataObject|null
337
     */
338
    public function fileFolder()
339
    {
340
        return Folder::find_or_make("/event-tickets/{$this->ReservationCode}/");
341
    }
342
343
    /**
344
     * Generate the qr codes and downloadable pdf
345
     */
346
    public function createFiles()
347
    {
348
        $folder = $this->fileFolder();
349
        /** @var Attendee $attendee */
350
        foreach ($this->Attendees() as $attendee) {
351
            $attendee->createQRCode($folder);
352
            $attendee->createTicketFile($folder);
353
        }
354
    }
355
356
    /**
357
     * Send the reservation mail
358
     */
359
    public function sendReservation()
360
    {
361
        // Get the mail sender or fallback to the admin email
362
        if (empty($from = self::config()->get('mail_sender'))) {
363
            $from = Config::inst()->get('Email', 'admin_email');
364
        }
365
366
        // Create the email with given template and reservation data
367
        $email = new Email();
368
        $email->setSubject(_t(
369
            'ReservationMail.TITLE',
370
            'Your order at {sitename}',
371
            null,
372
            array(
0 ignored issues
show
Documentation introduced by
array('sitename' => \Sit...t_site_config()->Title) is of type array<string,string,{"sitename":"string"}>, but the function expects a string.

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...
373
                'sitename' => SiteConfig::current_site_config()->Title
374
            )
375
        ));
376
        $email->setFrom($from);
377
        $email->setTo($this->MainContact()->Email);
378
        $email->setTemplate('ReservationMail');
379
        $email->populateTemplate($this);
380
        $this->extend('updateReservationMail', $email);
381
        $email->send();
382
    }
383
384
    /**
385
     * Send the reserved tickets
386
     */
387
    public function sendTickets()
388
    {
389
        // Get the mail sender or fallback to the admin email
390
        if (empty($from = self::config()->get('mail_sender'))) {
391
            $from = Config::inst()->get('Email', 'admin_email');
392
        }
393
394
        // Send the tickets to the main contact
395
        $email = new Email();
396
        $email->setSubject(_t(
397
            'MainContactMail.TITLE',
398
            'Uw tickets voor {event}',
399
            null,
400
            array(
0 ignored issues
show
Documentation introduced by
array('event' => $this->Event()->Title) is of type array<string,?,{"event":"?"}>, but the function expects a string.

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...
401
                'event' => $this->Event()->Title
402
            )
403
        ));
404
        $email->setFrom($from);
405
        $email->setTo($this->MainContact()->Email);
406
        $email->setTemplate('MainContactMail');
407
        $email->populateTemplate($this);
408
        $this->extend('updateMainContactMail', $email);
409
        $email->send();
410
411
412
        // Get the attendees for this event that are checked as receiver
413
        $ticketReceivers = $this->Attendees()->filter('TicketReceiver', 1)->exclude('ID', $this->MainContactID);
414
        if ($ticketReceivers->exists()) {
415
            /** @var Attendee $ticketReceiver */
416
            foreach ($ticketReceivers as $ticketReceiver) {
417
                $email = new Email();
418
                $email->setSubject(_t(
419
                    'AttendeeMail.TITLE',
420
                    'Your ticket for {event}',
421
                    null,
422
                    array(
0 ignored issues
show
Documentation introduced by
array('event' => $this->Event()->Title) is of type array<string,?,{"event":"?"}>, but the function expects a string.

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...
423
                        'event' => $this->Event()->Title
424
                    )
425
                ));
426
                $email->setFrom($from);
427
                $email->setTo($ticketReceiver->Email);
428
                $email->setTemplate('AttendeeMail');
429
                $email->populateTemplate($ticketReceiver);
430
                $this->extend('updateTicketMail', $email);
431
                $email->send();
432
            }
433
        }
434
    }
435
436
437
    /**
438
     * Send a booking notification to the ticket mail sender or the site admin
439
     */
440
    public function sendNotification()
441
    {
442
        if (empty($from = self::config()->get('mail_sender'))) {
443
            $from = Config::inst()->get('Email', 'admin_email');
444
        }
445
446
        if (empty($to = self::config()->get('mail_receiver'))) {
447
            $to = Config::inst()->get('Email', 'admin_email');
448
        }
449
450
        $email = new Email();
451
        $email->setSubject(_t(
452
            'NotificationMail.TITLE',
453
            'Nieuwe reservering voor {event}',
454
            null, array('event' => $this->Event()->Title)
0 ignored issues
show
Documentation introduced by
array('event' => $this->Event()->Title) is of type array<string,?,{"event":"?"}>, but the function expects a string.

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...
455
        ));
456
457
        $email->setFrom($from);
458
        $email->setTo($to);
459
        $email->setTemplate('NotificationMail');
460
        $email->populateTemplate($this);
461
        $this->extend('updateNotificationMail', $email);
462
        $email->send();
463
    }
464
465
    /**
466
     * Create the files and send the reservation, notification and tickets
467
     */
468
    public function send()
469
    {
470
        $this->createFiles();
471
        $this->sendReservation();
472
        $this->sendNotification();
473
        $this->sendTickets();
474
    }
475
476
    /**
477
     * Get the download link
478
     *
479
     * @return string|null
480
     */
481
    public function getDownloadLink()
482
    {
483
        /** @var Attendee $attendee */
484
        if (
485
            ($attendees = $this->Attendees())
486
            && ($attendee = $attendees->first())
487
            && ($file = $attendee->TicketFile())
488
            && $file->exists()
489
        ) {
490
            return $file->Link();
491
        }
492
493
        return null;
494
    }
495
496
    public function canView($member = null)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
497
    {
498
        return $this->Event()->canView($member);
499
    }
500
501
    public function canEdit($member = null)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
502
    {
503
        return $this->Event()->canEdit($member);
504
    }
505
506
    public function canDelete($member = null)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
507
    {
508
        return $this->Event()->canDelete($member);
509
    }
510
511
    public function canCreate($member = null)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
512
    {
513
        return $this->Event()->canCreate($member);
514
    }
515
}
516