Completed
Push — master ( 77d576...27d56a )
by Bram
07:37
created

Reservation::complete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 1
eloc 4
nc 1
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\Attendee, 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
     * Complete the reservation
314
     *
315
     * @throws \ValidationException
316
     */
317
    public function complete()
318
    {
319
        $this->changeState('PAID');
320
        $this->send();
321
        $this->write();
322
    }
323
324
    /**
325
     * Set the main contact id
326
     * @param $id
327
     *
328
     * @throws \ValidationException
329
     */
330
    public function setMainContact($id)
331
    {
332
        $this->MainContactID = $id;
333
        $this->write();
334
    }
335
336
    /**
337
     * Create a reservation code
338
     *
339
     * @return string
340
     */
341
    public function createReservationCode()
342
    {
343
        return uniqid($this->ID);
344
    }
345
346
    /**
347
     * Generate the qr codes and downloadable pdf
348
     */
349
    public function createFiles()
350
    {
351
        /** @var Attendee $attendee */
352
        foreach ($this->Attendees() as $attendee) {
353
            $attendee->createQRCode();
354
            $attendee->createTicketFile();
355
        }
356
    }
357
358
    /**
359
     * Send the reservation mail
360
     */
361 View Code Duplication
    public function sendReservation()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
362
    {
363
        // Get the mail sender or fallback to the admin email
364
        if (empty($from = self::config()->get('mail_sender'))) {
365
            $from = Config::inst()->get('Email', 'admin_email');
366
        }
367
368
        // Create the email with given template and reservation data
369
        $email = new Email();
370
        $email->setSubject(_t(
371
            'ReservationMail.TITLE',
372
            'Your order at {sitename}',
373
            null,
374
            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...
375
                'sitename' => SiteConfig::current_site_config()->Title
376
            )
377
        ));
378
        $email->setFrom($from);
379
        $email->setTo($this->MainContact()->Email);
0 ignored issues
show
Documentation introduced by
The property Email does not exist on object<Broarm\EventTickets\Attendee>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
380
        $email->setTemplate('ReservationMail');
381
        $email->populateTemplate($this);
382
        $this->extend('updateReservationMail', $email);
383
        $email->send();
384
    }
385
386
    /**
387
     * Send the reserved tickets
388
     */
389
    public function sendTickets()
390
    {
391
        // Get the mail sender or fallback to the admin email
392
        if (empty($from = self::config()->get('mail_sender'))) {
393
            $from = Config::inst()->get('Email', 'admin_email');
394
        }
395
396
        // Send the tickets to the main contact
397
        $email = new Email();
398
        $email->setSubject(_t(
399
            'MainContactMail.TITLE',
400
            'Uw tickets voor {event}',
401
            null,
402
            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...
403
                'event' => $this->Event()->Title
404
            )
405
        ));
406
        $email->setFrom($from);
407
        $email->setTo($this->MainContact()->Email);
0 ignored issues
show
Documentation introduced by
The property Email does not exist on object<Broarm\EventTickets\Attendee>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
408
        $email->setTemplate('MainContactMail');
409
        $email->populateTemplate($this);
410
        $this->extend('updateMainContactMail', $email);
411
        $email->send();
412
413
414
        // Get the attendees for this event that are checked as receiver
415
        $ticketReceivers = $this->Attendees()->filter('TicketReceiver', 1)->exclude('ID', $this->MainContactID);
416
        if ($ticketReceivers->exists()) {
417
            /** @var Attendee $ticketReceiver */
418
            foreach ($ticketReceivers as $ticketReceiver) {
419
                $ticketReceiver->sendTicket();
420
            }
421
        }
422
    }
423
424
425
    /**
426
     * Send a booking notification to the ticket mail sender or the site admin
427
     */
428
    public function sendNotification()
429
    {
430
        if (empty($from = self::config()->get('mail_sender'))) {
431
            $from = Config::inst()->get('Email', 'admin_email');
432
        }
433
434
        if (empty($to = self::config()->get('mail_receiver'))) {
435
            $to = Config::inst()->get('Email', 'admin_email');
436
        }
437
438
        $email = new Email();
439
        $email->setSubject(_t(
440
            'NotificationMail.TITLE',
441
            'Nieuwe reservering voor {event}',
442
            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...
443
        ));
444
445
        $email->setFrom($from);
446
        $email->setTo($to);
447
        $email->setTemplate('NotificationMail');
448
        $email->populateTemplate($this);
449
        $this->extend('updateNotificationMail', $email);
450
        $email->send();
451
    }
452
453
    /**
454
     * Create the files and send the reservation, notification and tickets
455
     */
456
    public function send()
457
    {
458
        $this->createFiles();
459
        $this->sendReservation();
460
        $this->sendNotification();
461
        $this->sendTickets();
462
    }
463
464
    /**
465
     * Get the download link
466
     *
467
     * @return string|null
468
     */
469
    public function getDownloadLink()
470
    {
471
        /** @var Attendee $attendee */
472
        if (
473
            ($attendee = $this->Attendees()->first())
474
            && ($file = $attendee->TicketFile())
475
            && $file->exists()
476
        ) {
477
            return $file->Link();
478
        }
479
480
        return null;
481
    }
482
483
    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...
484
    {
485
        return $this->Event()->canView($member);
486
    }
487
488
    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...
489
    {
490
        return $this->Event()->canEdit($member);
491
    }
492
493
    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...
494
    {
495
        return $this->Event()->canDelete($member);
496
    }
497
498
    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...
499
    {
500
        return $this->Event()->canCreate($member);
501
    }
502
}
503