Completed
Push — develop ( 3de1d7...233a23 )
by Abdelrahman
01:42
created

BookableBooking::calculatePrice()   C

Complexity

Conditions 14
Paths 33

Size

Total Lines 70
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 70
rs 5.6188
c 0
b 0
f 0
cc 14
eloc 47
nc 33
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Bookings\Models;
6
7
use Carbon\Carbon;
8
use Illuminate\Database\Eloquent\Model;
9
use Rinvex\Cacheable\CacheableEloquent;
10
use Illuminate\Database\Eloquent\Builder;
11
use Rinvex\Support\Traits\ValidatingTrait;
12
use Illuminate\Database\Eloquent\Relations\MorphTo;
13
14
abstract class BookableBooking extends Model
15
{
16
    use ValidatingTrait;
17
    use CacheableEloquent;
18
19
    /**
20
     * {@inheritdoc}
21
     */
22
    protected $fillable = [
23
        'bookable_id',
24
        'bookable_type',
25
        'customer_id',
26
        'customer_type',
27
        'starts_at',
28
        'ends_at',
29
        'price',
30
        'currency',
31
        'price_equation',
32
        'cancelled_at',
33
        'notes',
34
    ];
35
36
    /**
37
     * {@inheritdoc}
38
     */
39
    protected $casts = [
40
        'bookable_id' => 'integer',
41
        'bookable_type' => 'string',
42
        'customer_id' => 'integer',
43
        'customer_type' => 'string',
44
        'starts_at' => 'datetime',
45
        'ends_at' => 'datetime',
46
        'price' => 'float',
47
        'currency' => 'string',
48
        'price_equation' => 'json',
49
        'cancelled_at' => 'datetime',
50
        'notes' => 'string',
51
    ];
52
53
    /**
54
     * {@inheritdoc}
55
     */
56
    protected $observables = [
57
        'validating',
58
        'validated',
59
    ];
60
61
    /**
62
     * The default rules that the model will validate against.
63
     *
64
     * @var array
65
     */
66
    protected $rules = [
67
        'bookable_id' => 'required|integer',
68
        'bookable_type' => 'required|string',
69
        'customer_id' => 'required|integer',
70
        'customer_type' => 'required|string',
71
        'starts_at' => 'required|date',
72
        'ends_at' => 'required|date',
73
        'price' => 'required|numeric',
74
        'currency' => 'required|alpha|size:3',
75
        'price_equation' => 'nullable|array',
76
        'cancelled_at' => 'nullable|date',
77
        'notes' => 'nullable|string|max:10000',
78
    ];
79
80
    /**
81
     * Whether the model should throw a
82
     * ValidationException if it fails validation.
83
     *
84
     * @var bool
85
     */
86
    protected $throwValidationExceptions = true;
87
88
    /**
89
     * Create a new Eloquent model instance.
90
     *
91
     * @param array $attributes
92
     */
93
    public function __construct(array $attributes = [])
94
    {
95
        parent::__construct($attributes);
96
97
        $this->setTable(config('rinvex.bookings.tables.bookings'));
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103
    protected static function boot()
104
    {
105
        parent::boot();
106
107
        static::validating(function (self $bookableAvailability) {
108
            list($price, $priceEquation, $currency) = is_null($bookableAvailability->price)
109
                ? $bookableAvailability->calculatePrice($bookableAvailability->bookable, $bookableAvailability->starts_at, $bookableAvailability->ends_at) : [$bookableAvailability->price, $bookableAvailability->price_equation, $bookableAvailability->currency];
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 260 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
110
111
            $bookableAvailability->price_equation = $priceEquation;
112
            $bookableAvailability->currency = $currency;
113
            $bookableAvailability->price = $price;
114
        });
115
    }
116
117
    /**
118
     * Calculate the booking price.
119
     *
120
     * @param \Illuminate\Database\Eloquent\Model $bookable
121
     * @param \Carbon\Carbon                      $startsAt
122
     * @param \Carbon\Carbon                      $endsAt
0 ignored issues
show
Documentation introduced by
Should the type for parameter $endsAt not be null|Carbon?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
123
     *
124
     * @return array
125
     */
126
    public function calculatePrice(Model $bookable, Carbon $startsAt, Carbon $endsAt = null): array
127
    {
128
        $prices = $bookable->prices->map(function (Price $price) {
129
            return [
130
                'weekday' => $price->weekday,
131
                'starts_at' => $price->starts_at,
132
                'ends_at' => $price->ends_at,
133
                'percentage' => $price->percentage,
134
            ];
135
        });
136
137
        $totalUnits = 0;
138
        $totalPrice = 0;
139
        $method = 'add'.ucfirst($bookable->unit).'s';
140
141
        for ($date = clone $startsAt; $date->lt($endsAt ?? $date->addDay()); $date->$method()) {
142
            // Count units
143
            $totalUnits++;
144
145
            // Get applicable custom prices. Use first custom price matched, and ignore
146
            // others. We should not have multiple custom prices for same time range anyway!
147
            $customPrice = $prices->search(function ($price) use ($date, $bookable) {
148
                $dayMatched = $price['weekday'] === mb_strtolower($date->format('D'));
149
150
                return $bookable->unit === 'd' ? $dayMatched : $dayMatched && (new Carbon($date->format('H:i:s')))->between(new Carbon($price['starts_at']), new Carbon($price['ends_at']));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 188 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
151
            });
152
153
            // Use custom price if exists (custom price is a +/- percentage of original resource price)
154
            $totalPrice += $customPrice !== false ? $bookable->price + (($bookable->price * $prices[$customPrice]['percentage']) / 100) : $bookable->price;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 155 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
155
        }
156
157
        $bookableRates = $bookable->rates->map(function (BookableRate $bookableRate) {
158
            return [
159
                'percentage' => $bookableRate->percentage,
160
                'operator' => $bookableRate->operator,
161
                'amount' => $bookableRate->amount,
162
            ];
163
        })->toArray();
164
165
        foreach ($bookableRates as $bookableRate) {
166
            switch ($bookableRate['operator']) {
167
                case '^':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
168
                    $units = $totalUnits <= $bookableRate['amount'] ? $totalUnits : $bookableRate['amount'];
169
                    $totalPrice += (($bookableRate['percentage'] * $bookable->price) / 100) * $units;
170
                    break;
171
                case '>':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
172
                    $totalPrice += $totalUnits > $bookableRate['amount'] ? ((($bookableRate['percentage'] * $bookable->price) / 100) * $totalUnits) : 0;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 152 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
173
                    break;
174
                case '<':
175
                    $totalPrice += $totalUnits < $bookableRate['amount'] ? ((($bookableRate['percentage'] * $bookable->price) / 100) * $totalUnits) : 0;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 152 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
176
                    break;
177
                case '=':
178
                default:
179
                    $totalPrice += $totalUnits === $bookableRate['amount'] ? ((($bookableRate['percentage'] * $bookable->price) / 100) * $totalUnits) : 0;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 154 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
180
                    break;
181
            }
182
        }
183
184
        $priceEquation = [
185
            'price' => $bookable->price,
186
            'unit' => $bookable->unit,
187
            'currency' => $bookable->currency,
188
            'total_units' => $totalUnits,
189
            'total_price' => $totalPrice,
190
            'prices' => $prices,
191
            'rates' => $bookableRates,
192
        ];
193
194
        return [$totalPrice, $priceEquation, $bookable->currency];
195
    }
196
197
    /**
198
     * Get the owning resource model.
199
     *
200
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
201
     */
202
    public function bookable(): MorphTo
203
    {
204
        return $this->morphTo('bookable', 'bookable_type', 'bookable_id');
205
    }
206
207
    /**
208
     * Get the booking customer.
209
     *
210
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
211
     */
212
    public function customer(): MorphTo
213
    {
214
        return $this->morphTo('customer', 'customer_type', 'customer_id');
215
    }
216
217
    /**
218
     * Get bookings of the given resource.
219
     *
220
     * @param \Illuminate\Database\Eloquent\Builder $builder
221
     * @param \Illuminate\Database\Eloquent\Model   $bookable
222
     *
223
     * @return \Illuminate\Database\Eloquent\Builder
224
     */
225
    public function scopeOfBookable(Builder $builder, Model $bookable): Builder
226
    {
227
        return $builder->where('bookable_type', $bookable->getMorphClass())->where('bookable_id', $bookable->getKey());
228
    }
229
230
    /**
231
     * Get bookings of the given customer.
232
     *
233
     * @param \Illuminate\Database\Eloquent\Builder $builder
234
     * @param \Illuminate\Database\Eloquent\Model   $customer
235
     *
236
     * @return \Illuminate\Database\Eloquent\Builder
237
     */
238
    public function scopeOfCustomer(Builder $builder, Model $customer): Builder
239
    {
240
        return $builder->where('customer_type', $customer->getMorphClass())->where('customer_id', $customer->getKey());
241
    }
242
243
    /**
244
     * Get the past bookings.
245
     *
246
     * @param \Illuminate\Database\Eloquent\Builder $builder
247
     *
248
     * @return \Illuminate\Database\Eloquent\Builder
249
     */
250
    public function scopePast(Builder $builder): Builder
251
    {
252
        return $builder->whereNull('cancelled_at')
253
                       ->whereNotNull('ends_at')
254
                       ->where('ends_at', '<', now());
255
    }
256
257
    /**
258
     * Get the future bookings.
259
     *
260
     * @param \Illuminate\Database\Eloquent\Builder $builder
261
     *
262
     * @return \Illuminate\Database\Eloquent\Builder
263
     */
264
    public function scopeFuture(Builder $builder): Builder
265
    {
266
        return $builder->whereNull('cancelled_at')
267
                       ->whereNotNull('starts_at')
268
                       ->where('starts_at', '>', now());
269
    }
270
271
    /**
272
     * Get the current bookings.
273
     *
274
     * @param \Illuminate\Database\Eloquent\Builder $builder
275
     *
276
     * @return \Illuminate\Database\Eloquent\Builder
277
     */
278
    public function scopeCurrent(Builder $builder): Builder
279
    {
280
        return $builder->whereNull('cancelled_at')
281
                       ->whereNotNull('starts_at')
282
                       ->whereNotNull('ends_at')
283
                       ->where('starts_at', '<', now())
284
                       ->where('ends_at', '>', now());
285
    }
286
287
    /**
288
     * Get the cancelled bookings.
289
     *
290
     * @param \Illuminate\Database\Eloquent\Builder $builder
291
     *
292
     * @return \Illuminate\Database\Eloquent\Builder
293
     */
294
    public function scopeCancelled(Builder $builder): Builder
295
    {
296
        return $builder->whereNotNull('cancelled_at');
297
    }
298
299
    /**
300
     * Get bookings starts before the given date.
301
     *
302
     * @param \Illuminate\Database\Eloquent\Builder $builder
303
     * @param string                                $date
304
     *
305
     * @return \Illuminate\Database\Eloquent\Builder
306
     */
307
    public function scopeStartsBefore(Builder $builder, string $date): Builder
308
    {
309
        return $builder->whereNull('cancelled_at')
310
                       ->whereNotNull('starts_at')
311
                       ->where('starts_at', '<', new Carbon($date));
312
    }
313
314
    /**
315
     * Get bookings starts after the given date.
316
     *
317
     * @param \Illuminate\Database\Eloquent\Builder $builder
318
     * @param string                                $date
319
     *
320
     * @return \Illuminate\Database\Eloquent\Builder
321
     */
322
    public function scopeStartsAfter(Builder $builder, string $date): Builder
323
    {
324
        return $builder->whereNull('cancelled_at')
325
                       ->whereNotNull('starts_at')
326
                       ->where('starts_at', '>', new Carbon($date));
327
    }
328
329
    /**
330
     * Get bookings starts between the given dates.
331
     *
332
     * @param \Illuminate\Database\Eloquent\Builder $builder
333
     * @param string                                $startsAt
334
     * @param string                                $endsAt
335
     *
336
     * @return \Illuminate\Database\Eloquent\Builder
337
     */
338
    public function scopeStartsBetween(Builder $builder, string $startsAt, string $endsAt): Builder
339
    {
340
        return $builder->whereNull('cancelled_at')
341
                       ->whereNotNull('starts_at')
342
                       ->where('starts_at', '>=', new Carbon($startsAt))
343
                       ->where('starts_at', '<=', new Carbon($endsAt));
344
    }
345
346
    /**
347
     * Get bookings ends before the given date.
348
     *
349
     * @param \Illuminate\Database\Eloquent\Builder $builder
350
     * @param string                                $date
351
     *
352
     * @return \Illuminate\Database\Eloquent\Builder
353
     */
354
    public function scopeEndsBefore(Builder $builder, string $date): Builder
355
    {
356
        return $builder->whereNull('cancelled_at')
357
                       ->whereNotNull('ends_at')
358
                       ->where('ends_at', '<', new Carbon($date));
359
    }
360
361
    /**
362
     * Get bookings ends after the given date.
363
     *
364
     * @param \Illuminate\Database\Eloquent\Builder $builder
365
     * @param string                                $date
366
     *
367
     * @return \Illuminate\Database\Eloquent\Builder
368
     */
369
    public function scopeEndsAfter(Builder $builder, string $date): Builder
370
    {
371
        return $builder->whereNull('cancelled_at')
372
                       ->whereNotNull('ends_at')
373
                       ->where('ends_at', '>', new Carbon($date));
374
    }
375
376
    /**
377
     * Get bookings ends between the given dates.
378
     *
379
     * @param \Illuminate\Database\Eloquent\Builder $builder
380
     * @param string                                $startsAt
381
     * @param string                                $endsAt
382
     *
383
     * @return \Illuminate\Database\Eloquent\Builder
384
     */
385
    public function scopeEndsBetween(Builder $builder, string $startsAt, string $endsAt): Builder
386
    {
387
        return $builder->whereNull('cancelled_at')
388
                       ->whereNotNull('ends_at')
389
                       ->where('ends_at', '>=', new Carbon($startsAt))
390
                       ->where('ends_at', '<=', new Carbon($endsAt));
391
    }
392
393
    /**
394
     * Get bookings cancelled before the given date.
395
     *
396
     * @param \Illuminate\Database\Eloquent\Builder $builder
397
     * @param string                                $date
398
     *
399
     * @return \Illuminate\Database\Eloquent\Builder
400
     */
401
    public function scopeCancelledBefore(Builder $builder, string $date): Builder
402
    {
403
        return $builder->whereNotNull('cancelled_at')
404
                       ->where('cancelled_at', '<', new Carbon($date));
405
    }
406
407
    /**
408
     * Get bookings cancelled after the given date.
409
     *
410
     * @param \Illuminate\Database\Eloquent\Builder $builder
411
     * @param string                                $date
412
     *
413
     * @return \Illuminate\Database\Eloquent\Builder
414
     */
415
    public function scopeCancelledAfter(Builder $builder, string $date): Builder
416
    {
417
        return $builder->whereNotNull('cancelled_at')
418
                       ->where('cancelled_at', '>', new Carbon($date));
419
    }
420
421
    /**
422
     * Get bookings cancelled between the given dates.
423
     *
424
     * @param \Illuminate\Database\Eloquent\Builder $builder
425
     * @param string                                $startsAt
426
     * @param string                                $endsAt
427
     *
428
     * @return \Illuminate\Database\Eloquent\Builder
429
     */
430
    public function scopeCancelledBetween(Builder $builder, string $startsAt, string $endsAt): Builder
431
    {
432
        return $builder->whereNotNull('cancelled_at')
433
                       ->where('cancelled_at', '>=', new Carbon($startsAt))
434
                       ->where('cancelled_at', '<=', new Carbon($endsAt));
435
    }
436
437
    /**
438
     * Get bookings between the given dates.
439
     *
440
     * @param \Illuminate\Database\Eloquent\Builder $builder
441
     * @param string                                $startsAt
442
     * @param string                                $endsAt
443
     *
444
     * @return \Illuminate\Database\Eloquent\Builder
445
     */
446
    public function scopeRange(Builder $builder, string $startsAt, string $endsAt): Builder
447
    {
448
        return $builder->whereNull('cancelled_at')
449
                       ->whereNotNull('starts_at')
450
                       ->where('starts_at', '>=', new Carbon($startsAt))
451
                       ->where(function (Builder $builder) use ($endsAt) {
452
                           $builder->whereNull('ends_at')
453
                                 ->orWhere(function (Builder $builder) use ($endsAt) {
454
                                     $builder->whereNotNull('ends_at')
455
                                           ->where('ends_at', '<=', new Carbon($endsAt));
456
                                 });
457
                       });
458
    }
459
460
    /**
461
     * Check if the booking is cancelled.
462
     *
463
     * @return bool
464
     */
465
    public function isCancelled(): bool
466
    {
467
        return (bool) $this->cancelled_at;
468
    }
469
470
    /**
471
     * Check if the booking is past.
472
     *
473
     * @return bool
474
     */
475
    public function isPast(): bool
476
    {
477
        return ! $this->isCancelled() && $this->ends_at->isPast();
478
    }
479
480
    /**
481
     * Check if the booking is future.
482
     *
483
     * @return bool
484
     */
485
    public function isFuture(): bool
486
    {
487
        return ! $this->isCancelled() && $this->starts_at->isFuture();
488
    }
489
490
    /**
491
     * Check if the booking is current.
492
     *
493
     * @return bool
494
     */
495
    public function isCurrent(): bool
496
    {
497
        return ! $this->isCancelled() && now()->between($this->starts_at, $this->ends_at);
498
    }
499
}
500