Issues (86)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Models/Appointment.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Timegridio\Concierge\Models;
4
5
use Carbon\Carbon;
6
use Illuminate\Database\Eloquent\Model as EloquentModel;
7
use McCool\LaravelAutoPresenter\HasPresenter;
8
use Timegridio\Concierge\Presenters\AppointmentPresenter;
9
10
/**
11
 * An Appointment can be understood as a reservation of a given Service,
12
 * provided by a given Business, targeted to a Contact, which will take place
13
 * on a determined Date and Time, and might have a duration and or comments.
14
 *
15
 * The Appointment can be issued by the Contact's User or by the Business owner.
16
 *
17
 * @property int $id
18
 * @property int $issuer_id
19
 * @property mixed $issuer
20
 * @property int $contact_id
21
 * @property Timegridio\Concierge\Models\Contact $contact
22
 * @property int $business_id
23
 * @property Timegridio\Concierge\Models\Business $business
24
 * @property int $service_id
25
 * @property Timegridio\Concierge\Models\Service $service
26
 * @property int $resource_id
27
 * @property Timegridio\Concierge\Models\Humanresource $resource
28
 * @property int $vacancy_id
29
 * @property Timegridio\Concierge\Models\Vacancy $vacancy
30
 * @property \Carbon\Carbon $start_at
31
 * @property \Carbon\Carbon $finish_at
32
 * @property int $duration
33
 * @property string $comments
34
 * @property string $hash
35
 * @property string $status
36
 * @property string $date
37
 * @property string $statusLabel
38
 * @property string $code
39
 * @property Carbon\Carbon $cancellationDeadline
40
 */
41
class Appointment extends EloquentModel implements HasPresenter
42
{
43
    /**
44
     * The attributes that are mass assignable.
45
     *
46
     * @var array
47
     */
48
    protected $fillable = [
49
        'issuer_id',
50
        'contact_id',
51
        'business_id',
52
        'service_id',
53
        'resource_id',
54
        'start_at',
55
        'finish_at',
56
        'duration',
57
        'comments',
58
    ];
59
60
    /**
61
     * The attributes that aren't mass assignable.
62
     *
63
     * @var array
64
     */
65
    protected $guarded = ['id', 'hash', 'status', 'vacancy_id'];
66
67
    /**
68
     * The attributes that should be mutated to dates.
69
     *
70
     * @var array
71
     */
72
    protected $dates = ['start_at', 'finish_at'];
73
74
    /**
75
     * Appointment Hard Status Constants.
76
     */
77
    const STATUS_RESERVED = 'R';
78
    const STATUS_CONFIRMED = 'C';
79
    const STATUS_CANCELED = 'A';
80
    const STATUS_SERVED = 'S';
81
82
    ///////////////
83
    // PRESENTER //
84
    ///////////////
85
86
    /**
87
     * Get Presenter Class.
88
     *
89
     * @return App\Presenters\AppointmentPresenter
90
     */
91 1
    public function getPresenterClass()
92
    {
93 1
        return AppointmentPresenter::class;
94
    }
95
96
    /**
97
     * Generate hash and save the model to the database.
98
     *
99
     * @param array $options
100
     *
101
     * @return bool
102
     */
103 30
    public function save(array $options = [])
104
    {
105 30
        $this->doHash();
106
107 30
        return parent::save($options);
108
    }
109
110
    ///////////////////
111
    // Relationships //
112
    ///////////////////
113
114
    /**
115
     * Get the issuer (the User that generated the Appointment).
116
     *
117
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
118
     */
119 4
    public function issuer()
120
    {
121 4
        return $this->belongsTo(config('auth.providers.users.model'));
122
    }
123
124
    /**
125
     * Get the target Contact (for whom is reserved the Appointment).
126
     *
127
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
128
     */
129 4
    public function contact()
130
    {
131 4
        return $this->belongsTo(Contact::class);
132
    }
133
134
    /**
135
     * Get the holding Business (that has taken the reservation).
136
     *
137
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
138
     */
139 10
    public function business()
140
    {
141 10
        return $this->belongsTo(Business::class);
142
    }
143
144
    /**
145
     * Get the reserved Service.
146
     *
147
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
148
     */
149 8
    public function service()
150
    {
151 8
        return $this->belongsTo(Service::class);
152
    }
153
154
    /**
155
     * Humanresource.
156
     *
157
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
158
     */
159 4
    public function humanresource()
160
    {
161 4
        return $this->belongsTo(Humanresource::class);
162
    }
163
164
    /**
165
     * Get the Vacancy (that justifies the availability of resources for the
166
     * Appointment generation).
167
     *
168
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
169
     */
170 6
    public function vacancy()
171
    {
172 6
        return $this->belongsTo(Vacancy::class);
173
    }
174
175
    ///////////
176
    // Other //
177
    ///////////
178
179
    /**
180
     * Get the User through Contact.
181
     *
182
     * @return User
183
     */
184 2
    public function user()
185
    {
186 2
        return $this->contact->user;
187
    }
188
189
    /**
190
     * Determine if the new Appointment will hash-crash with another existing
191
     * Appointment.
192
     *
193
     * @return bool
194
     */
195 5
    public function duplicates()
196
    {
197 5
        return !self::where('hash', $this->hash)->get()->isEmpty();
198
    }
199
200 2
    public function duration()
201
    {
202 2
        return $this->finish_at->diffInMinutes($this->start_at);
203
    }
204
205
    ///////////////
206
    // Accessors //
207
    ///////////////
208
209
    /**
210
     * Get Hash.
211
     *
212
     * @return string
213
     */
214 8
    public function getHashAttribute()
215
    {
216 8
        return isset($this->attributes['hash'])
217 8
            ? $this->attributes['hash']
218 8
            : $this->doHash();
219
    }
220
221
    /**
222
     * Get Finish At:
223
     * Calculates the start_at time plus duration in minutes.
224
     *
225
     * @return Carbon
226
     */
227 3
    public function getFinishAtAttribute()
228
    {
229 3
        if (array_get($this->attributes, 'finish_at') !== null) {
230
            return Carbon::parse($this->attributes['finish_at']);
231
        }
232
233 3
        if (is_numeric($this->duration)) {
234 3
            return $this->start_at->addMinutes($this->duration);
235
        }
236
237
        return $this->start_at;
238
    }
239
240
    /**
241
     * Get cancellation deadline (target date).
242
     *
243
     * @return Carbon\Carbon
244
     */
245
    public function getCancellationDeadlineAttribute()
246
    {
247
        $hours = $this->business->pref('appointment_cancellation_pre_hs');
248
249
        return $this->start_at
250
                    ->subHours($hours)
251
                    ->timezone($this->business->timezone);
252
    }
253
254
    /**
255
     * Get the human readable status name.
256
     *
257
     * @return string
258
     */
259
    public function getStatusLabelAttribute()
260
    {
261
        $labels = [
262
            Self::STATUS_RESERVED  => 'reserved',
263
            Self::STATUS_CONFIRMED => 'confirmed',
264
            Self::STATUS_CANCELED  => 'canceled',
265
            Self::STATUS_SERVED    => 'served',
266
            ];
267
268
        return array_key_exists($this->status, $labels)
269
            ? $labels[$this->status]
270
            : '';
271
    }
272
273
    /**
274
     * Get the date of the Appointment.
275
     *
276
     * @return string
277
     */
278 3
    public function getDateAttribute()
279
    {
280 3
        return $this->start_at
281 3
                    ->timezone($this->business->timezone)
282 3
                    ->toDateString();
283
    }
284
285
    /**
286
     * Get user-friendly unique identification code.
287
     *
288
     * @return string
289
     */
290 2
    public function getCodeAttribute()
291
    {
292 2
        $length = $this->business->pref('appointment_code_length');
293
294 2
        return strtoupper(substr($this->hash, 0, $length));
295
    }
296
297
    //////////////
298
    // Mutators //
299
    //////////////
300
301
    /**
302
     * Generate Appointment hash.
303
     *
304
     * @return string
305
     */
306 30
    public function doHash()
307
    {
308 30
        return $this->attributes['hash'] = md5(
309 30
            $this->start_at.'/'.
310 30
            $this->contact_id.'/'.
311 30
            $this->business_id.'/'.
312 30
            $this->service_id
313
        );
314
    }
315
316
    /**
317
     * Set start at.
318
     *
319
     * @param Carbon $datetime
320
     */
321 30
    public function setStartAtAttribute(Carbon $datetime)
322
    {
323 30
        $this->attributes['start_at'] = $datetime;
324 30
    }
325
326
    /**
327
     * Set finish_at attribute.
328
     *
329
     * @param Carbon $datetime
330
     */
331 11
    public function setFinishAtAttribute(Carbon $datetime)
332
    {
333 11
        $this->attributes['finish_at'] = $datetime;
334 11
    }
335
336
    /**
337
     * Set Comments.
338
     *
339
     * @param string $comments
340
     */
341 30
    public function setCommentsAttribute($comments)
342
    {
343 30
        $this->attributes['comments'] = trim($comments) ?: null;
344 30
    }
345
346
    /////////////////
347
    // HARD STATUS //
348
    /////////////////
349
350
    /**
351
     * Determine if is Reserved.
352
     *
353
     * @return bool
354
     */
355 1
    public function isReserved()
356
    {
357 1
        return $this->status == Self::STATUS_RESERVED;
358
    }
359
360
    ///////////////////////////
361
    // Calculated attributes //
362
    ///////////////////////////
363
364
    /**
365
     * Appointment Status Workflow.
366
     *
367
     * Hard Status: Those concrete values stored in DB
368
     * Soft Status: Those values calculated from stored values in DB
369
     *
370
     * Suggested transitions (Binding is not mandatory)
371
     *     Reserved -> Confirmed -> Served
372
     *     Reserved -> Served
373
     *     Reserved -> Canceled
374
     *     Reserved -> Confirmed -> Canceled
375
     *
376
     * Soft Status
377
     *     (Active)   [ Reserved  | Confirmed ]
378
     *     (InActive) [ Canceled  | Served    ]
379
     */
380
381
    /**
382
     * Determine if is Active.
383
     *
384
     * @return bool
385
     */
386 5
    public function isActive()
387
    {
388
        return
389 5
            $this->status == Self::STATUS_CONFIRMED ||
390 5
            $this->status == Self::STATUS_RESERVED;
391
    }
392
393
    /**
394
     * Determine if is Pending.
395
     *
396
     * @return bool
397
     */
398 3
    public function isPending()
399
    {
400 3
        return $this->isActive() && $this->isFuture();
401
    }
402
403
    /**
404
     * Determine if is Future.
405
     *
406
     * @return bool
407
     */
408 4
    public function isFuture()
409
    {
410 4
        return !$this->isDue();
411
    }
412
413
    /**
414
     * Determine if is due.
415
     *
416
     * @return bool
417
     */
418 5
    public function isDue()
419
    {
420 5
        return $this->start_at->isPast();
421
    }
422
423
    ////////////
424
    // Scopes //
425
    ////////////
426
427
    /**
428
     * Scope to Business.
429
     *
430
     * @param Illuminate\Database\Query $query
431
     *
432
     * @return Illuminate\Database\Query
433
     */
434 1
    public function scopeOfBusiness($query, $businessId)
435
    {
436 1
        return $query->where('business_id', $businessId);
437
    }
438
439
    /////////////////////////
440
    // Hard Status Scoping //
441
    /////////////////////////
442
443
    /**
444
     * Scope to Contacts Collection.
445
     *
446
     * @param Illuminate\Database\Query $query
447
     *
448
     * @return Illuminate\Database\Query
449
     */
450
    public function scopeForContacts($query, $contacts)
451
    {
452
        return $query->whereIn('contact_id', $contacts->pluck('id'));
453
    }
454
455
    /**
456
     * Scope to Unarchived Appointments.
457
     *
458
     * @param Illuminate\Database\Query $query
459
     *
460
     * @return Illuminate\Database\Query
461
     */
462 1
    public function scopeUnarchived($query)
463
    {
464 1
        $carbon = Carbon::parse('today midnight')->timezone('UTC');
465
466
        return $query
467
            ->where(function ($query) use ($carbon) {
468
469 1
                $query->whereIn('status', [Self::STATUS_RESERVED, Self::STATUS_CONFIRMED])
470 1
                    ->where('start_at', '<=', $carbon)
471
                    ->orWhere(function ($query) use ($carbon) {
472 1
                        $query->where('start_at', '>=', $carbon);
473 1
                    });
474 1
            });
475
    }
476
477
    /**
478
     * Scope to Served Appointments.
479
     *
480
     * @param Illuminate\Database\Query $query
481
     *
482
     * @return Illuminate\Database\Query
483
     */
484
    public function scopeServed($query)
485
    {
486
        return $query->where('status', '=', Self::STATUS_SERVED);
487
    }
488
489
    /**
490
     * Scope to Canceled Appointments.
491
     *
492
     * @param Illuminate\Database\Query $query
493
     *
494
     * @return Illuminate\Database\Query
495
     */
496
    public function scopeCanceled($query)
497
    {
498
        return $query->where('status', '=', Self::STATUS_CANCELED);
499
    }
500
501
    /////////////////////////
502
    // Soft Status Scoping //
503
    /////////////////////////
504
505
    /**
506
     * Scope to not Served Appointments.
507
     *
508
     * @param Illuminate\Database\Query $query
509
     *
510
     * @return Illuminate\Database\Query
511
     */
512
    public function scopeUnServed($query)
513
    {
514
        return $query->where('status', '<>', Self::STATUS_SERVED);
515
    }
516
517
    /**
518
     * Scope to Active Appointments.
519
     *
520
     * @param Illuminate\Database\Query $query
521
     *
522
     * @return Illuminate\Database\Query
523
     */
524 16
    public function scopeActive($query)
525
    {
526 16
        return $query->whereIn('status', [Self::STATUS_RESERVED, Self::STATUS_CONFIRMED]);
527
    }
528
529
    /**
530
     * Scope of date.
531
     *
532
     * @param Illuminate\Database\Query $query
533
     * @param Carbon                    $date
534
     *
535
     * @return Illuminate\Database\Query
536
     */
537
    public function scopeOfDate($query, Carbon $date)
538
    {
539
        $date->timezone('UTC');
540
541
        return $query->whereRaw('date(`start_at`) = ?', [$date->toDateString()]);
542
    }
543
544
    /**
545
     * Soft check of time interval affectation
546
     *
547
     * @param Illuminate\Database\Query $query
548
     * @param Carbon                    $startAt
549
     * @param Carbon                    $finishAt
550
     *
551
     * @return Illuminate\Database\Query
552
     */
553 10
    public function scopeAffectingInterval($query, Carbon $startAt, Carbon $finishAt)
554
    {
555 10
        $startAt->timezone('UTC');
556 10
        $finishAt->timezone('UTC');
557
558
        return $query
559
            ->where(function ($query) use ($startAt, $finishAt) {
560
561
                $query->where(function ($query) use ($startAt, $finishAt) {
562 10
                    $query->where('finish_at', '>=', $finishAt)
563 10
                            ->where('start_at', '<', $startAt);
564 10
                })
565
                ->orWhere(function ($query) use ($startAt, $finishAt) {
566 10
                    $query->where('finish_at', '<=', $finishAt)
567 10
                            ->where('finish_at', '>', $startAt);
568 10
                })
569 10
                ->orWhere(function ($query) use ($startAt, $finishAt) {
570 10
                    $query->where('start_at', '>=', $startAt)
571 10
                            ->where('start_at', '<', $finishAt);
572 10
                });
573
//                ->orWhere(function ($query) use ($startAt, $finishAt) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% 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...
574
//                    $query->where('start_at', '>', $startAt)
575
//                            ->where('finish_at', '>', $finishAt);
576
//                });
577
578 10
            });
579
    }
580
581
    /**
582
     * Scope Affecting Humanresource.
583
     *
584
     * @param Illuminate\Database\Query $query
585
     *
586
     * @return Illuminate\Database\Query
587
     */
588 10
    public function scopeAffectingHumanresource($query, $humanresourceId)
589
    {
590 10
        if (is_null($humanresourceId)) {
591 10
            return $query;
592
        }
593
594
        return $query->where('humanresource_id', $humanresourceId);
595
    }
596
597
    //////////////////////////
598
    // Soft Status Checkers //
599
    //////////////////////////
600
601
    /**
602
     * User is target contact of the appointment.
603
     *
604
     * @param int $userId
605
     *
606
     * @return bool
607
     */
608
    public function isTarget($userId)
609
    {
610
        return $this->contact->isProfileOf($userId);
611
    }
612
613
    /**
614
     * User is issuer of the appointment.
615
     *
616
     * @param int $userId
617
     *
618
     * @return bool
619
     */
620
    public function isIssuer($userId)
621
    {
622
        return $this->issuer ? $this->issuer->id == $userId : false;
623
    }
624
625
    /**
626
     * User is owner of business.
627
     *
628
     * @param int $userId
629
     *
630
     * @return bool
631
     */
632
    public function isOwner($userId)
633
    {
634
        return $this->business->owners->contains($userId);
635
    }
636
637
    /**
638
     * can be canceled by user.
639
     *
640
     * @param int $userId
641
     *
642
     * @return bool
643
     */
644
    public function canCancel($userId)
645
    {
646
        return $this->isOwner($userId) ||
647
            ($this->isIssuer($userId) && $this->isOnTimeToCancel()) ||
648
            ($this->isTarget($userId) && $this->isOnTimeToCancel());
649
    }
650
651
    /**
652
     * Determine if it is still possible to cancel according business policy.
653
     *
654
     * @return bool
655
     */
656
    public function isOnTimeToCancel()
657
    {
658
        $graceHours = $this->business->pref('appointment_cancellation_pre_hs');
659
660
        $diff = $this->start_at->diffInHours(Carbon::now());
661
662
        return intval($diff) >= intval($graceHours);
663
    }
664
665
    /**
666
     * can Serve.
667
     *
668
     * @param int $userId
669
     *
670
     * @return bool
671
     */
672
    public function canServe($userId)
673
    {
674
        return $this->isOwner($userId);
675
    }
676
677
    /**
678
     * can confirm.
679
     *
680
     * @param int $userId
681
     *
682
     * @return bool
683
     */
684
    public function canConfirm($userId)
685
    {
686
        return $this->isIssuer($userId) || $this->isOwner($userId);
687
    }
688
689
    /**
690
     * is Serveable by user.
691
     *
692
     * @param int $userId
693
     *
694
     * @return bool
695
     */
696
    public function isServeableBy($userId)
697
    {
698
        return $this->isServeable() && $this->canServe($userId);
699
    }
700
701
    /**
702
     * is Confirmable By user.
703
     *
704
     * @param int $userId
705
     *
706
     * @return bool
707
     */
708
    public function isConfirmableBy($userId)
709
    {
710
        return
711
            $this->isConfirmable() &&
712
            $this->shouldConfirmBy($userId) &&
713
            $this->canConfirm($userId);
714
    }
715
716
    /**
717
     * is cancelable By user.
718
     *
719
     * @param int $userId
720
     *
721
     * @return bool
722
     */
723
    public function isCancelableBy($userId)
724
    {
725
        return $this->isCancelable() && $this->canCancel($userId);
726
    }
727
728
    /**
729
     * Determine if the queried userId may confirm the appointment or not.
730
     *
731
     * @param int $userId
732
     *
733
     * @return bool
734
     */
735
    public function shouldConfirmBy($userId)
736
    {
737
        return ($this->isSelfIssued() && $this->isOwner($userId)) || $this->isIssuer($userId);
738
    }
739
740
    /**
741
     * Determine if the target Contact's User is the same of the Appointment
742
     * issuer User.
743
     *
744
     * @return bool
745
     */
746
    public function isSelfIssued()
747
    {
748
        if (!$this->issuer) {
749
            return false;
750
        }
751
        if (!$this->contact) {
752
            return false;
753
        }
754
        if (!$this->contact->user) {
755
            return false;
756
        }
757
758
        return $this->issuer->id == $this->contact->user->id;
759
    }
760
761
    /**
762
     * Determine if the Serve action can be performed.
763
     *
764
     * @return bool
765
     */
766 1
    public function isServeable()
767
    {
768 1
        return $this->isActive() && $this->isDue();
769
    }
770
771
    /**
772
     * Determine if the Confirm action can be performed.
773
     *
774
     * @return bool
775
     */
776 1
    public function isConfirmable()
777
    {
778 1
        return $this->status == self::STATUS_RESERVED && $this->isFuture();
779
    }
780
781
    /**
782
     * Determine if the cancelable action can be performed.
783
     *
784
     * @return bool
785
     */
786 1
    public function isCancelable()
787
    {
788 1
        return $this->isActive();
789
    }
790
791
    /////////////////////////
792
    // Hard Status Actions //
793
    /////////////////////////
794
795
    /**
796
     * Check and perform Confirm action.
797
     *
798
     * @return $this
799
     */
800 5
    public function doReserve()
801
    {
802 5
        if ($this->status === null) {
803 5
            $this->status = self::STATUS_RESERVED;
804
        }
805
806 5
        return $this;
807
    }
808
809
    /**
810
     * Check and perform Confirm action.
811
     *
812
     * @return $this
813
     */
814 2
    public function doConfirm()
815
    {
816 2
        if ($this->isConfirmable()) {
817 2
            $this->status = self::STATUS_CONFIRMED;
818
819 2
            $this->save();
820
        }
821
822 2
        return $this;
823
    }
824
825
    /**
826
     * Check and perform cancel action.
827
     *
828
     * @return $this
829
     */
830 2
    public function doCancel()
831
    {
832 2
        if ($this->isCancelable()) {
833 1
            $this->status = self::STATUS_CANCELED;
834
835 1
            $this->save();
836
        }
837
838 2
        return $this;
839
    }
840
841
    /**
842
     * Check and perform Serve action.
843
     *
844
     * @return $this
845
     */
846 3
    public function doServe()
847
    {
848 3
        if ($this->isServeable()) {
849 1
            $this->status = self::STATUS_SERVED;
850
851 1
            $this->save();
852
        }
853
854 3
        return $this;
855
    }
856
}
857