Completed
Push — master ( 037bfd...38dad0 )
by Abdelrahman
01:26 queued 13s
created

PlanSubscription::canUseFeature()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 8.8333
c 0
b 0
f 0
cc 7
nc 3
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Subscriptions\Models;
6
7
use DB;
8
use Carbon\Carbon;
9
use LogicException;
10
use Spatie\Sluggable\SlugOptions;
11
use Rinvex\Support\Traits\HasSlug;
12
use Illuminate\Database\Eloquent\Model;
13
use Illuminate\Database\Eloquent\Builder;
14
use Rinvex\Subscriptions\Services\Period;
15
use Rinvex\Support\Traits\HasTranslations;
16
use Rinvex\Support\Traits\ValidatingTrait;
17
use Illuminate\Database\Eloquent\SoftDeletes;
18
use Rinvex\Subscriptions\Traits\BelongsToPlan;
19
use Illuminate\Database\Eloquent\Relations\HasMany;
20
use Illuminate\Database\Eloquent\Relations\MorphTo;
21
22
/**
23
 * Rinvex\Subscriptions\Models\PlanSubscription.
24
 *
25
 * @property int                 $id
26
 * @property int                 $subscriber_id
27
 * @property string              $subscriber_type
28
 * @property int                 $plan_id
29
 * @property string              $slug
30
 * @property array               $title
31
 * @property array               $description
32
 * @property \Carbon\Carbon|null $trial_ends_at
33
 * @property \Carbon\Carbon|null $starts_at
34
 * @property \Carbon\Carbon|null $ends_at
35
 * @property \Carbon\Carbon|null $cancels_at
36
 * @property \Carbon\Carbon|null $canceled_at
37
 * @property \Carbon\Carbon|null $created_at
38
 * @property \Carbon\Carbon|null $updated_at
39
 * @property \Carbon\Carbon|null $deleted_at
40
 * @property-read \Rinvex\Subscriptions\Models\Plan                                                             $plan
41
 * @property-read \Illuminate\Database\Eloquent\Collection|\Rinvex\Subscriptions\Models\PlanSubscriptionUsage[] $usage
42
 * @property-read \Illuminate\Database\Eloquent\Model|\Eloquent                                                 $subscriber
43
 *
44
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription byPlanId($planId)
45
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription findEndedPeriod()
46
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription findEndedTrial()
47
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription findEndingPeriod($dayRange = 3)
48
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription findEndingTrial($dayRange = 3)
49
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription ofSubscriber(\Illuminate\Database\Eloquent\Model $subscriber)
50
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereCanceledAt($value)
51
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereCancelsAt($value)
52
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereCreatedAt($value)
53
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereDeletedAt($value)
54
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereDescription($value)
55
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereEndsAt($value)
56
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereId($value)
57
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereTitle($value)
58
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription wherePlanId($value)
59
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereSlug($value)
60
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereStartsAt($value)
61
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereTrialEndsAt($value)
62
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereUpdatedAt($value)
63
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereSubscriberId($value)
64
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereSubscriberType($value)
65
 * @mixin \Eloquent
66
 */
67
class PlanSubscription extends Model
68
{
69
    use HasSlug;
70
    use SoftDeletes;
71
    use BelongsToPlan;
72
    use HasTranslations;
73
    use ValidatingTrait;
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    protected $fillable = [
79
        'subscriber_id',
80
        'subscriber_type',
81
        'plan_id',
82
        'slug',
83
        'name',
84
        'description',
85
        'trial_ends_at',
86
        'starts_at',
87
        'ends_at',
88
        'cancels_at',
89
        'canceled_at',
90
    ];
91
92
    /**
93
     * {@inheritdoc}
94
     */
95
    protected $casts = [
96
        'subscriber_id' => 'integer',
97
        'subscriber_type' => 'string',
98
        'plan_id' => 'integer',
99
        'slug' => 'string',
100
        'trial_ends_at' => 'datetime',
101
        'starts_at' => 'datetime',
102
        'ends_at' => 'datetime',
103
        'cancels_at' => 'datetime',
104
        'canceled_at' => 'datetime',
105
        'deleted_at' => 'datetime',
106
    ];
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    protected $observables = [
112
        'validating',
113
        'validated',
114
    ];
115
116
    /**
117
     * The attributes that are translatable.
118
     *
119
     * @var array
120
     */
121
    public $translatable = [
122
        'name',
123
        'description',
124
    ];
125
126
    /**
127
     * The default rules that the model will validate against.
128
     *
129
     * @var array
130
     */
131
    protected $rules = [];
132
133
    /**
134
     * Whether the model should throw a
135
     * ValidationException if it fails validation.
136
     *
137
     * @var bool
138
     */
139
    protected $throwValidationExceptions = true;
140
141
    /**
142
     * Create a new Eloquent model instance.
143
     *
144
     * @param array $attributes
145
     */
146
    public function __construct(array $attributes = [])
147
    {
148
        parent::__construct($attributes);
149
150
        $this->setTable(config('rinvex.subscriptions.tables.plan_subscriptions'));
151
        $this->setRules([
152
            'name' => 'required|string|strip_tags|max:150',
153
            'description' => 'nullable|string|max:32768',
154
            'slug' => 'required|alpha_dash|max:150|unique:'.config('rinvex.subscriptions.tables.plan_subscriptions').',slug',
155
            'plan_id' => 'required|integer|exists:'.config('rinvex.subscriptions.tables.plans').',id',
156
            'subscriber_id' => 'required|integer',
157
            'subscriber_type' => 'required|string|strip_tags|max:150',
158
            'trial_ends_at' => 'nullable|date',
159
            'starts_at' => 'required|date',
160
            'ends_at' => 'required|date',
161
            'cancels_at' => 'nullable|date',
162
            'canceled_at' => 'nullable|date',
163
        ]);
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    protected static function boot()
170
    {
171
        parent::boot();
172
173
        static::validating(function (self $model) {
174
            if (! $model->starts_at || ! $model->ends_at) {
175
                $model->setNewPeriod();
176
            }
177
        });
178
    }
179
180
    /**
181
     * Get the options for generating the slug.
182
     *
183
     * @return \Spatie\Sluggable\SlugOptions
184
     */
185
    public function getSlugOptions(): SlugOptions
186
    {
187
        return SlugOptions::create()
188
                          ->doNotGenerateSlugsOnUpdate()
189
                          ->generateSlugsFrom('name')
190
                          ->saveSlugsTo('slug');
191
    }
192
193
    /**
194
     * Get the owning subscriber.
195
     *
196
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
197
     */
198
    public function subscriber(): MorphTo
199
    {
200
        return $this->morphTo('subscriber', 'subscriber_type', 'subscriber_id', 'id');
201
    }
202
203
    /**
204
     * The subscription may have many usage.
205
     *
206
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
207
     */
208
    public function usage(): hasMany
209
    {
210
        return $this->hasMany(config('rinvex.subscriptions.models.plan_subscription_usage'), 'subscription_id', 'id');
211
    }
212
213
    /**
214
     * Check if subscription is active.
215
     *
216
     * @return bool
217
     */
218
    public function active(): bool
219
    {
220
        return ! $this->ended() || $this->onTrial();
221
    }
222
223
    /**
224
     * Check if subscription is inactive.
225
     *
226
     * @return bool
227
     */
228
    public function inactive(): bool
229
    {
230
        return ! $this->active();
231
    }
232
233
    /**
234
     * Check if subscription is currently on trial.
235
     *
236
     * @return bool
237
     */
238
    public function onTrial(): bool
239
    {
240
        return $this->trial_ends_at ? Carbon::now()->lt($this->trial_ends_at) : false;
241
    }
242
243
    /**
244
     * Check if subscription is canceled.
245
     *
246
     * @return bool
247
     */
248
    public function canceled(): bool
249
    {
250
        return $this->canceled_at ? Carbon::now()->gte($this->canceled_at) : false;
251
    }
252
253
    /**
254
     * Check if subscription period has ended.
255
     *
256
     * @return bool
257
     */
258
    public function ended(): bool
259
    {
260
        return $this->ends_at ? Carbon::now()->gte($this->ends_at) : false;
261
    }
262
263
    /**
264
     * Cancel subscription.
265
     *
266
     * @param bool $immediately
267
     *
268
     * @return $this
269
     */
270
    public function cancel($immediately = false)
271
    {
272
        $this->canceled_at = Carbon::now();
273
274
        if ($immediately) {
275
            $this->ends_at = $this->canceled_at;
276
        }
277
278
        $this->save();
279
280
        return $this;
281
    }
282
283
    /**
284
     * Change subscription plan.
285
     *
286
     * @param \Rinvex\Subscriptions\Models\Plan $plan
287
     *
288
     * @return $this
289
     */
290
    public function changePlan(Plan $plan)
291
    {
292
        // If plans does not have the same billing frequency
293
        // (e.g., invoice_interval and invoice_period) we will update
294
        // the billing dates starting today, and sice we are basically creating
295
        // a new billing cycle, the usage data will be cleared.
296
        if ($this->plan->invoice_interval !== $plan->invoice_interval || $this->plan->invoice_period !== $plan->invoice_period) {
297
            $this->setNewPeriod($plan->invoice_interval, $plan->invoice_period);
298
            $this->usage()->delete();
299
        }
300
301
        // Attach new plan to subscription
302
        $this->plan_id = $plan->getKey();
303
        $this->save();
304
305
        return $this;
306
    }
307
308
    /**
309
     * Renew subscription period.
310
     *
311
     * @throws \LogicException
312
     *
313
     * @return $this
314
     */
315
    public function renew()
316
    {
317
        if ($this->ended() && $this->canceled()) {
318
            throw new LogicException('Unable to renew canceled ended subscription.');
319
        }
320
321
        $subscription = $this;
322
323
        DB::transaction(function () use ($subscription) {
324
            // Clear usage data
325
            $subscription->usage()->delete();
326
327
            // Renew period
328
            $subscription->setNewPeriod();
329
            $subscription->canceled_at = null;
330
            $subscription->save();
331
        });
332
333
        return $this;
334
    }
335
336
    /**
337
     * Get bookings of the given subscriber.
338
     *
339
     * @param \Illuminate\Database\Eloquent\Builder $builder
340
     * @param \Illuminate\Database\Eloquent\Model   $subscriber
341
     *
342
     * @return \Illuminate\Database\Eloquent\Builder
343
     */
344
    public function scopeOfSubscriber(Builder $builder, Model $subscriber): Builder
345
    {
346
        return $builder->where('subscriber_type', $subscriber->getMorphClass())->where('subscriber_id', $subscriber->getKey());
347
    }
348
349
    /**
350
     * Scope subscriptions with ending trial.
351
     *
352
     * @param \Illuminate\Database\Eloquent\Builder $builder
353
     * @param int                                   $dayRange
354
     *
355
     * @return \Illuminate\Database\Eloquent\Builder
356
     */
357
    public function scopeFindEndingTrial(Builder $builder, int $dayRange = 3): Builder
358
    {
359
        $from = Carbon::now();
360
        $to = Carbon::now()->addDays($dayRange);
361
362
        return $builder->whereBetween('trial_ends_at', [$from, $to]);
363
    }
364
365
    /**
366
     * Scope subscriptions with ended trial.
367
     *
368
     * @param \Illuminate\Database\Eloquent\Builder $builder
369
     *
370
     * @return \Illuminate\Database\Eloquent\Builder
371
     */
372
    public function scopeFindEndedTrial(Builder $builder): Builder
373
    {
374
        return $builder->where('trial_ends_at', '<=', now());
375
    }
376
377
    /**
378
     * Scope subscriptions with ending periods.
379
     *
380
     * @param \Illuminate\Database\Eloquent\Builder $builder
381
     * @param int                                   $dayRange
382
     *
383
     * @return \Illuminate\Database\Eloquent\Builder
384
     */
385
    public function scopeFindEndingPeriod(Builder $builder, int $dayRange = 3): Builder
386
    {
387
        $from = Carbon::now();
388
        $to = Carbon::now()->addDays($dayRange);
389
390
        return $builder->whereBetween('ends_at', [$from, $to]);
391
    }
392
393
    /**
394
     * Scope subscriptions with ended periods.
395
     *
396
     * @param \Illuminate\Database\Eloquent\Builder $builder
397
     *
398
     * @return \Illuminate\Database\Eloquent\Builder
399
     */
400
    public function scopeFindEndedPeriod(Builder $builder): Builder
401
    {
402
        return $builder->where('ends_at', '<=', now());
403
    }
404
405
    /**
406
     * Set new subscription period.
407
     *
408
     * @param string $invoice_interval
409
     * @param int    $invoice_period
0 ignored issues
show
Documentation introduced by
Should the type for parameter $invoice_period not be string|integer?

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...
410
     * @param string $start
411
     *
412
     * @return $this
413
     */
414
    protected function setNewPeriod($invoice_interval = '', $invoice_period = '', $start = '')
415
    {
416
        if (empty($invoice_interval)) {
417
            $invoice_interval = $this->plan->invoice_interval;
418
        }
419
420
        if (empty($invoice_period)) {
421
            $invoice_period = $this->plan->invoice_period;
422
        }
423
424
        $period = new Period($invoice_interval, $invoice_period, $start);
425
426
        $this->starts_at = $period->getStartDate();
427
        $this->ends_at = $period->getEndDate();
428
429
        return $this;
430
    }
431
432
    /**
433
     * Record feature usage.
434
     *
435
     * @param string $featureSlug
436
     * @param int    $uses
437
     *
438
     * @return \Rinvex\Subscriptions\Models\PlanSubscriptionUsage
439
     */
440
    public function recordFeatureUsage(string $featureSlug, int $uses = 1, bool $incremental = true): PlanSubscriptionUsage
441
    {
442
        $feature = $this->plan->features()->where('slug', $featureSlug)->first();
443
444
        $usage = $this->usage()->firstOrNew([
445
            'subscription_id' => $this->getKey(),
446
            'feature_id' => $feature->getKey(),
447
        ]);
448
449
        if ($feature->resettable_period) {
450
            // Set expiration date when the usage record is new or doesn't have one.
451
            if (is_null($usage->valid_until)) {
452
                // Set date from subscription creation date so the reset
453
                // period match the period specified by the subscription's plan.
454
                $usage->valid_until = $feature->getResetDate($this->created_at);
455
            } elseif ($usage->expired()) {
456
                // If the usage record has been expired, let's assign
457
                // a new expiration date and reset the uses to zero.
458
                $usage->valid_until = $feature->getResetDate($usage->valid_until);
459
                $usage->used = 0;
460
            }
461
        }
462
463
        $usage->used = ($incremental ? $usage->used + $uses : $uses);
464
465
        $usage->save();
466
467
        return $usage;
468
    }
469
470
    /**
471
     * Reduce usage.
472
     *
473
     * @param string $featureSlug
474
     * @param int    $uses
475
     *
476
     * @return \Rinvex\Subscriptions\Models\PlanSubscriptionUsage|null
477
     */
478
    public function reduceFeatureUsage(string $featureSlug, int $uses = 1): ?PlanSubscriptionUsage
479
    {
480
        $usage = $this->usage()->byFeatureSlug($featureSlug)->first();
481
482
        if (is_null($usage)) {
483
            return null;
484
        }
485
486
        $usage->used = max($usage->used - $uses, 0);
487
488
        $usage->save();
489
490
        return $usage;
491
    }
492
493
    /**
494
     * Determine if the feature can be used.
495
     *
496
     * @param string $featureSlug
497
     *
498
     * @return bool
499
     */
500
    public function canUseFeature(string $featureSlug): bool
501
    {
502
        $featureValue = $this->getFeatureValue($featureSlug);
503
        $usage = $this->usage()->byFeatureSlug($featureSlug)->first();
504
505
        if ($featureValue === 'true') {
506
            return true;
507
        }
508
509
        // If the feature value is zero, let's return false since
510
        // there's no uses available. (useful to disable countable features)
511
        if (! $usage || $usage->expired() || is_null($featureValue) || $featureValue === '0' || $featureValue === 'false') {
512
            return false;
513
        }
514
515
        // Check for available uses
516
        return $this->getFeatureRemainings($featureSlug) > 0;
517
    }
518
519
    /**
520
     * Get how many times the feature has been used.
521
     *
522
     * @param string $featureSlug
523
     *
524
     * @return int
525
     */
526
    public function getFeatureUsage(string $featureSlug): int
527
    {
528
        $usage = $this->usage()->byFeatureSlug($featureSlug)->first();
529
530
        return (! $usage || $usage->expired()) ? 0 : $usage->used;
531
    }
532
533
    /**
534
     * Get the available uses.
535
     *
536
     * @param string $featureSlug
537
     *
538
     * @return int
539
     */
540
    public function getFeatureRemainings(string $featureSlug): int
541
    {
542
        return $this->getFeatureValue($featureSlug) - $this->getFeatureUsage($featureSlug);
543
    }
544
545
    /**
546
     * Get feature value.
547
     *
548
     * @param string $featureSlug
549
     *
550
     * @return mixed
551
     */
552
    public function getFeatureValue(string $featureSlug)
553
    {
554
        $feature = $this->plan->features()->where('slug', $featureSlug)->first();
555
556
        return $feature->value ?? null;
557
    }
558
}
559