Completed
Push — master ( efaae2...d18db4 )
by Bram
06:04
created

Reservation::sendTickets()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 39
rs 8.3626
c 0
b 0
f 0
cc 7
nc 4
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
 * @property boolean SentTickets
43
 * @property boolean SentReservation
44
 * @property boolean SentNotification
45
 *
46
 * @property int    EventID
47
 * @property int    MainContactID
48
 *
49
 * @method CalendarEvent|TicketExtension Event()
50
 * @method Attendee MainContact()
51
 * @method HasManyList Payments()
52
 * @method HasManyList Attendees()
53
 * @method ManyManyList PriceModifiers()
54
 */
55
class Reservation extends DataObject
56
{
57
    const STATUS_CART = 'CART';
58
    const STATUS_PENDING = 'PENDING';
59
    const STATUS_PAID = 'PAID';
60
    const STATUS_CANCELED = 'CANCELED';
61
    
62
    /**
63
     * Time to wait before deleting the discarded cart
64
     * Give a string that is parsable by strtotime
65
     *
66
     * @var string
67
     */
68
    private static $delete_after = '+1 hour';
69
70
    /**
71
     * The address to whom the ticket notifications are sent
72
     * By default the admin email is used
73
     *
74
     * @config
75
     * @var string
76
     */
77
    private static $mail_sender;
78
79
    /**
80
     * The address from where the ticket mails are sent
81
     * By default the admin email is used
82
     *
83
     * @config
84
     * @var string
85
     */
86
    private static $mail_receiver;
87
88
    private static $db = array(
89
        'Status' => 'Enum("CART,PENDING,PAID,CANCELED","CART")',
90
        'Title' => 'Varchar(255)',
91
        'Subtotal' => 'Currency',
92
        'Total' => 'Currency',
93
        'Gateway' => 'Varchar(255)',
94
        'Comments' => 'Text',
95
        'AgreeToTermsAndConditions' => 'Boolean',
96
        'ReservationCode' => 'Varchar(255)',
97
        'SentTickets' => 'Boolean',
98
        'SentReservation' => 'Boolean',
99
        'SentNotification' => 'Boolean',
100
    );
101
102
    private static $default_sort = 'Created DESC';
103
104
    private static $has_one = array(
105
        'Event' => 'CalendarEvent',
106
        'MainContact' => 'Broarm\EventTickets\Attendee'
107
    );
108
109
    private static $has_many = array(
110
        'Payments' => 'Payment',
111
        'Attendees' => 'Broarm\EventTickets\Attendee.Reservation'
112
    );
113
114
    private static $belongs_many_many = array(
115
        'PriceModifiers' => 'Broarm\EventTickets\PriceModifier'
116
    );
117
118
    private static $indexes = array(
119
        'ReservationCode' => 'unique("ReservationCode")'
120
    );
121
122
    private static $summary_fields = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
123
        'ReservationCode' => 'Reservation',
124
        'Title' => 'Customer',
125
        'Total.Nice' => 'Total',
126
        'State' => 'Status',
127
        'GatewayNice' => 'Payment method',
128
        'Created.Nice' => 'Date'
129
    );
130
131
    /**
132
     * Actions usable on the cms detail view
133
     *
134
     * @var array
135
     */
136
    private static $better_buttons_actions = array(
137
        'send'
138
    );
139
140
    public function getCMSFields()
141
    {
142
        $fields = new FieldList(new TabSet('Root', $mainTab = new Tab('Main')));
143
        $gridFieldConfig = GridFieldConfig_RecordViewer::create();
144
        $fields->addFieldsToTab('Root.Main', array(
145
            ReadonlyField::create('ReservationCode', _t('Reservation.Code', 'Code')),
146
            ReadonlyField::create('Created', _t('Reservation.Created', 'Date')),
147
            DropdownField::create('Status', _t('Reservation.Status', 'Status'), $this->getStates()),
148
            ReadonlyField::create('Title', _t('Reservation.MainContact', 'Main contact')),
149
            ReadonlyField::create('GateWayNice', _t('Reservation.Gateway', 'Gateway')),
150
            ReadonlyField::create('Total', _t('Reservation.Total', 'Total')),
151
            ReadonlyField::create('Comments', _t('Reservation.Comments', 'Comments')),
152
            CheckboxField::create('AgreeToTermsAndConditions', _t('Reservation.AgreeToTermsAndConditions', 'Agreed to terms and conditions'))->performReadonlyTransformation(),
153
            GridField::create('Attendees', 'Attendees', $this->Attendees(), $gridFieldConfig),
154
            GridField::create('Payments', 'Payments', $this->Payments(), $gridFieldConfig),
155
            GridField::create('PriceModifiers', 'PriceModifiers', $this->PriceModifiers(), $gridFieldConfig)
156
        ));
157
        $fields->addFieldsToTab('Root.Main', array());
158
        $this->extend('updateCMSFields', $fields);
159
        return $fields;
160
    }
161
162
    /**
163
     * Add utility actions to the reservation details view
164
     *
165
     * @return FieldList
166
     */
167
    public function getBetterButtonsActions()
168
    {
169
        /** @var FieldList $fields */
170
        $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...
171
        $fields->push(BetterButtonCustomAction::create('send', _t('Reservation.RESEND', 'Resend the reservation')));
172
173
        return $fields;
174
    }
175
176
    /**
177
     * Generate a reservation code if it does not yet exists
178
     */
179
    public function onBeforeWrite()
180
    {
181
        // Set the title to the name of the reservation holder
182
        $this->Title = $this->getName();
183
184
        // Create a validation code to be used for confirmation and in the barcode
185
        if ($this->exists() && empty($this->ReservationCode)) {
186
            $this->ReservationCode = $this->createReservationCode();
187
        }
188
189
        parent::onBeforeWrite();
190
    }
191
192
    /**
193
     * After deleting a reservation, delete the attendees and files
194
     */
195
    public function onBeforeDelete()
196
    {
197
        // If a reservation is deleted remove the names from the guest list
198
        foreach ($this->Attendees() as $attendee) {
199
            /** @var Attendee $attendee */
200
            if ($attendee->exists()) {
201
                $attendee->delete();
202
            }
203
        }
204
205
        // Remove the folder
206
        if (($folder = Folder::get()->find('Name', $this->ReservationCode)) && $folder->exists() && $folder->isEmpty()) {
207
            $folder->delete();
208
        }
209
210
        parent::onBeforeDelete();
211
    }
212
213
    /**
214
     * Gets a nice unnamespaced name
215
     *
216
     * @return string
217
     */
218
    public function singular_name()
219
    {
220
        $name = explode('\\', parent::singular_name());
221
        return trim(end($name));
222
    }
223
224
    /**
225
     * Returns the nice gateway title
226
     *
227
     * @return string
228
     */
229
    public function getGatewayNice()
230
    {
231
        return GatewayInfo::niceTitle($this->Gateway);
232
    }
233
234
    /**
235
     * Check if the cart is still in cart state and the delete_after time period has been exceeded
236
     *
237
     * @return bool
238
     */
239
    public function isDiscarded()
240
    {
241
        $deleteAfter = strtotime(self::config()->get('delete_after'), strtotime($this->Created));
242
        return ($this->Status === 'CART') && (time() > $deleteAfter);
243
    }
244
245
    /**
246
     * Get the full name
247
     *
248
     * @return string
249
     */
250
    public function getName()
251
    {
252
        /** @var Attendee $attendee */
253
        if (($mainContact = $this->MainContact()) && $mainContact->exists() && $name = $mainContact->getName()) {
254
            return $name;
255
        } else {
256
            return 'new reservation';
257
        }
258
    }
259
260
    /**
261
     * Return the translated state
262
     *
263
     * @return string
264
     */
265
    public function getState()
266
    {
267
        return _t("Reservation.{$this->Status}", $this->Status);
268
    }
269
270
    /**
271
     * Get a the translated map of available states
272
     *
273
     * @return array
274
     */
275
    private function getStates()
276
    {
277
        return array_map(function ($state) {
278
            return _t("Reservation.$state", $state);
279
        }, $this->dbObject('Status')->enumValues());
280
    }
281
282
    /**
283
     * Get the total by querying the sum of attendee ticket prices
284
     *
285
     * @return float
0 ignored issues
show
Documentation introduced by
Should the return type not be string?

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

Loading history...
286
     */
287
    public function calculateTotal()
288
    {
289
        $total = $this->Subtotal = $this->Attendees()->leftJoin(
290
            'Broarm\EventTickets\Ticket',
291
            '`Broarm\EventTickets\Attendee`.`TicketID` = `Broarm\EventTickets\Ticket`.`ID`'
292
        )->sum('Price');
293
294
        // Calculate any price modifications if added
295
        if ($this->PriceModifiers()->exists()) {
296
            foreach ($this->PriceModifiers() as $priceModifier) {
297
                $priceModifier->updateTotal($total);
298
            }
299
        }
300
301
        return $this->Total = $total;
302
    }
303
304
    /**
305
     * Safely change to a state
306
     * todo check if state direction matches
307
     *
308
     * @param $state
309
     *
310
     * @return boolean
311
     */
312
    public function changeState($state)
313
    {
314
        $availableStates = $this->dbObject('Status')->enumValues();
315
        if (in_array($state, $availableStates)) {
316
            $this->Status = $state;
317
            return true;
318
        } else {
319
            user_error(_t('Reservation.STATE_CHANGE_ERROR', 'Selected state is not available'));
320
            return false;
321
        }
322
    }
323
324
    /**
325
     * Complete the reservation
326
     *
327
     * @throws \ValidationException
328
     */
329
    public function complete()
330
    {
331
        $this->changeState('PAID');
332
        $this->send();
333
        $this->write();
334
        $this->extend('onAfterComplete');
335
    }
336
337
    /**
338
     * Set the main contact id
339
     * @param $id
340
     *
341
     * @throws \ValidationException
342
     */
343
    public function setMainContact($id)
344
    {
345
        $this->MainContactID = $id;
346
        $this->write();
347
    }
348
349
    /**
350
     * Create a reservation code
351
     *
352
     * @return string
353
     */
354
    public function createReservationCode()
355
    {
356
        return uniqid($this->ID);
357
    }
358
359
    /**
360
     * Generate the qr codes and downloadable pdf
361
     */
362
    public function createFiles()
363
    {
364
        /** @var Attendee $attendee */
365
        foreach ($this->Attendees() as $attendee) {
366
            $attendee->createQRCode();
367
            $attendee->createTicketFile();
368
        }
369
    }
370
371
    /**
372
     * Send the reservation mail
373
     *
374
     * @return mixed
375
     */
376 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...
377
    {
378
        // Get the mail sender or fallback to the admin email
379
        if (($from = self::config()->get('mail_sender')) && empty($from)) {
380
            $from = Config::inst()->get('Email', 'admin_email');
381
        }
382
383
        // Create the email with given template and reservation data
384
        $email = new Email();
385
        $email->setSubject(_t(
386
            'ReservationMail.TITLE',
387
            'Your order at {sitename}',
388
            null,
389
            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...
390
                'sitename' => SiteConfig::current_site_config()->Title
391
            )
392
        ));
393
        $email->setFrom($from);
394
        $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...
395
        $email->setTemplate('ReservationMail');
396
        $email->populateTemplate($this);
397
        $this->extend('updateReservationMail', $email);
398
        return $email->send();
399
    }
400
401
    /**
402
     * Send the reserved tickets
403
     *
404
     * @return mixed
405
     */
406
    public function sendTickets()
407
    {
408
        // Get the mail sender or fallback to the admin email
409
        if (($from = self::config()->get('mail_sender')) && empty($from)) {
410
            $from = Config::inst()->get('Email', 'admin_email');
411
        }
412
413
        // Send the tickets to the main contact
414
        $email = new Email();
415
        $email->setSubject(_t(
416
            'MainContactMail.TITLE',
417
            'Uw tickets voor {event}',
418
            null,
419
            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...
420
                'event' => $this->Event()->Title
421
            )
422
        ));
423
        $email->setFrom($from);
424
        $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...
425
        $email->setTemplate('MainContactMail');
426
        $email->populateTemplate($this);
427
        $this->extend('updateMainContactMail', $email);
428
        $sent = $email->send();
429
430
431
        // Get the attendees for this event that are checked as receiver
432
        $ticketReceivers = $this->Attendees()->filter('TicketReceiver', 1)->exclude('ID', $this->MainContactID);
433
        if ($ticketReceivers->exists()) {
434
            /** @var Attendee $ticketReceiver */
435
            foreach ($ticketReceivers as $ticketReceiver) {
436
                $sentAttendee = $ticketReceiver->sendTicket();
437
                if ($sent && !$sentAttendee) {
438
                    $sent = $sentAttendee;
439
                }
440
            }
441
        }
442
443
        return $sent;
444
    }
445
446
447
    /**
448
     * Send a booking notification to the ticket mail sender or the site admin
449
     * @return mixed
450
     */
451
    public function sendNotification()
452
    {
453
        if (($from = self::config()->get('mail_sender')) && empty($from)) {
454
            $from = Config::inst()->get('Email', 'admin_email');
455
        }
456
457
        if (($to = self::config()->get('mail_receiver')) && empty($to)) {
458
            $to = Config::inst()->get('Email', 'admin_email');
459
        }
460
461
        $email = new Email();
462
        $email->setSubject(_t(
463
            'NotificationMail.TITLE',
464
            'Nieuwe reservering voor {event}',
465
            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...
466
        ));
467
468
        $email->setFrom($from);
469
        $email->setTo($to);
470
        $email->setTemplate('NotificationMail');
471
        $email->populateTemplate($this);
472
        $this->extend('updateNotificationMail', $email);
473
        return $email->send();
474
    }
475
476
    /**
477
     * Create the files and send the reservation, notification and tickets
478
     */
479
    public function send()
480
    {
481
        $this->createFiles();
482
        $this->SentReservation = (boolean)$this->sendReservation();
483
        $this->SentNotification = (boolean)$this->sendNotification();
484
        $this->SentTickets = (boolean)$this->sendTickets();
485
    }
486
487
    /**
488
     * Get the download link
489
     *
490
     * @return string|null
491
     */
492
    public function getDownloadLink()
493
    {
494
        /** @var Attendee $attendee */
495
        if (
496
            ($attendee = $this->Attendees()->first())
497
            && ($file = $attendee->TicketFile())
498
            && $file->exists()
499
        ) {
500
            return $file->Link();
501
        }
502
503
        return null;
504
    }
505
506
    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...
507
    {
508
        return $this->Event()->canView($member);
509
    }
510
511
    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...
512
    {
513
        return $this->Event()->canEdit($member);
514
    }
515
516
    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...
517
    {
518
        return $this->Event()->canDelete($member);
519
    }
520
521
    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...
522
    {
523
        return $this->Event()->canCreate($member);
524
    }
525
}
526