Completed
Push — develop ( e2ce13...d4327a )
by Abdelrahman
01:28
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 Rinvex\Subscriptions\Traits\BelongsToPlan;
18
use Illuminate\Database\Eloquent\Relations\HasMany;
19
use Illuminate\Database\Eloquent\Relations\MorphTo;
20
21
/**
22
 * Rinvex\Subscriptions\Models\PlanSubscription.
23
 *
24
 * @property int                 $id
25
 * @property int                 $subscriber_id
26
 * @property string              $subscriber_type
27
 * @property int                 $plan_id
28
 * @property string              $slug
29
 * @property array               $title
30
 * @property array               $description
31
 * @property \Carbon\Carbon|null $trial_ends_at
32
 * @property \Carbon\Carbon|null $starts_at
33
 * @property \Carbon\Carbon|null $ends_at
34
 * @property \Carbon\Carbon|null $cancels_at
35
 * @property \Carbon\Carbon|null $canceled_at
36
 * @property \Carbon\Carbon|null $created_at
37
 * @property \Carbon\Carbon|null $updated_at
38
 * @property \Carbon\Carbon|null $deleted_at
39
 * @property-read \Rinvex\Subscriptions\Models\Plan                                                             $plan
40
 * @property-read \Illuminate\Database\Eloquent\Collection|\Rinvex\Subscriptions\Models\PlanSubscriptionUsage[] $usage
41
 * @property-read \Illuminate\Database\Eloquent\Model|\Eloquent                                                 $subscriber
42
 *
43
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription byPlanId($planId)
44
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription findEndedPeriod()
45
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription findEndedTrial()
46
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription findEndingPeriod($dayRange = 3)
47
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription findEndingTrial($dayRange = 3)
48
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription ofSubscriber(\Illuminate\Database\Eloquent\Model $subscriber)
49
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereCanceledAt($value)
50
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereCancelsAt($value)
51
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereCreatedAt($value)
52
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereDeletedAt($value)
53
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereDescription($value)
54
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereEndsAt($value)
55
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereId($value)
56
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereTitle($value)
57
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription wherePlanId($value)
58
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereSlug($value)
59
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereStartsAt($value)
60
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereTrialEndsAt($value)
61
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereUpdatedAt($value)
62
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereSubscriberId($value)
63
 * @method static \Illuminate\Database\Eloquent\Builder|\Rinvex\Subscriptions\Models\PlanSubscription whereSubscriberType($value)
64
 * @mixin \Eloquent
65
 */
66
class PlanSubscription extends Model
67
{
68
    use HasSlug;
69
    use BelongsToPlan;
70
    use HasTranslations;
71
    use ValidatingTrait;
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    protected $fillable = [
77
        'subscriber_id',
78
        'subscriber_type',
79
        'plan_id',
80
        'slug',
81
        'name',
82
        'description',
83
        'trial_ends_at',
84
        'starts_at',
85
        'ends_at',
86
        'cancels_at',
87
        'canceled_at',
88
    ];
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    protected $casts = [
94
        'subscriber_id' => 'integer',
95
        'subscriber_type' => 'string',
96
        'plan_id' => 'integer',
97
        'slug' => 'string',
98
        'trial_ends_at' => 'datetime',
99
        'starts_at' => 'datetime',
100
        'ends_at' => 'datetime',
101
        'cancels_at' => 'datetime',
102
        'canceled_at' => 'datetime',
103
        'deleted_at' => 'datetime',
104
    ];
105
106
    /**
107
     * {@inheritdoc}
108
     */
109
    protected $observables = [
110
        'validating',
111
        'validated',
112
    ];
113
114
    /**
115
     * The attributes that are translatable.
116
     *
117
     * @var array
118
     */
119
    public $translatable = [
120
        'name',
121
        'description',
122
    ];
123
124
    /**
125
     * The default rules that the model will validate against.
126
     *
127
     * @var array
128
     */
129
    protected $rules = [];
130
131
    /**
132
     * Whether the model should throw a
133
     * ValidationException if it fails validation.
134
     *
135
     * @var bool
136
     */
137
    protected $throwValidationExceptions = true;
138
139
    /**
140
     * Create a new Eloquent model instance.
141
     *
142
     * @param array $attributes
143
     */
144
    public function __construct(array $attributes = [])
145
    {
146
        parent::__construct($attributes);
147
148
        $this->setTable(config('rinvex.subscriptions.tables.plan_subscriptions'));
149
        $this->setRules([
150
            'name' => 'required|string|strip_tags|max:150',
151
            'description' => 'nullable|string|max:32768',
152
            'slug' => 'required|alpha_dash|max:150|unique:'.config('rinvex.subscriptions.tables.plan_subscriptions').',slug',
153
            'plan_id' => 'required|integer|exists:'.config('rinvex.subscriptions.tables.plans').',id',
154
            'subscriber_id' => 'required|integer',
155
            'subscriber_type' => 'required|string|strip_tags|max:150',
156
            'trial_ends_at' => 'nullable|date',
157
            'starts_at' => 'required|date',
158
            'ends_at' => 'required|date',
159
            'cancels_at' => 'nullable|date',
160
            'canceled_at' => 'nullable|date',
161
        ]);
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167
    protected static function boot()
168
    {
169
        parent::boot();
170
171
        static::validating(function (self $model) {
172
            if (! $model->starts_at || ! $model->ends_at) {
173
                $model->setNewPeriod();
174
            }
175
        });
176
    }
177
178
    /**
179
     * Get the options for generating the slug.
180
     *
181
     * @return \Spatie\Sluggable\SlugOptions
182
     */
183
    public function getSlugOptions(): SlugOptions
184
    {
185
        return SlugOptions::create()
186
                          ->doNotGenerateSlugsOnUpdate()
187
                          ->generateSlugsFrom('name')
188
                          ->saveSlugsTo('slug');
189
    }
190
191
    /**
192
     * Get the owning subscriber.
193
     *
194
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
195
     */
196
    public function subscriber(): MorphTo
197
    {
198
        return $this->morphTo('subscriber', 'subscriber_type', 'subscriber_id', 'id');
199
    }
200
201
    /**
202
     * The subscription may have many usage.
203
     *
204
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
205
     */
206
    public function usage(): hasMany
207
    {
208
        return $this->hasMany(config('rinvex.subscriptions.models.plan_subscription_usage'), 'subscription_id', 'id');
209
    }
210
211
    /**
212
     * Check if subscription is active.
213
     *
214
     * @return bool
215
     */
216
    public function active(): bool
217
    {
218
        return ! $this->ended() || $this->onTrial();
219
    }
220
221
    /**
222
     * Check if subscription is inactive.
223
     *
224
     * @return bool
225
     */
226
    public function inactive(): bool
227
    {
228
        return ! $this->active();
229
    }
230
231
    /**
232
     * Check if subscription is currently on trial.
233
     *
234
     * @return bool
235
     */
236
    public function onTrial(): bool
237
    {
238
        return $this->trial_ends_at ? Carbon::now()->lt($this->trial_ends_at) : false;
239
    }
240
241
    /**
242
     * Check if subscription is canceled.
243
     *
244
     * @return bool
245
     */
246
    public function canceled(): bool
247
    {
248
        return $this->canceled_at ? Carbon::now()->gte($this->canceled_at) : false;
249
    }
250
251
    /**
252
     * Check if subscription period has ended.
253
     *
254
     * @return bool
255
     */
256
    public function ended(): bool
257
    {
258
        return $this->ends_at ? Carbon::now()->gte($this->ends_at) : false;
259
    }
260
261
    /**
262
     * Cancel subscription.
263
     *
264
     * @param bool $immediately
265
     *
266
     * @return $this
267
     */
268
    public function cancel($immediately = false)
269
    {
270
        $this->canceled_at = Carbon::now();
271
272
        if ($immediately) {
273
            $this->ends_at = $this->canceled_at;
274
        }
275
276
        $this->save();
277
278
        return $this;
279
    }
280
281
    /**
282
     * Change subscription plan.
283
     *
284
     * @param \Rinvex\Subscriptions\Models\Plan $plan
285
     *
286
     * @return $this
287
     */
288
    public function changePlan(Plan $plan)
289
    {
290
        // If plans does not have the same billing frequency
291
        // (e.g., invoice_interval and invoice_period) we will update
292
        // the billing dates starting today, and sice we are basically creating
293
        // a new billing cycle, the usage data will be cleared.
294
        if ($this->plan->invoice_interval !== $plan->invoice_interval || $this->plan->invoice_period !== $plan->invoice_period) {
295
            $this->setNewPeriod($plan->invoice_interval, $plan->invoice_period);
296
            $this->usage()->delete();
297
        }
298
299
        // Attach new plan to subscription
300
        $this->plan_id = $plan->getKey();
301
        $this->save();
302
303
        return $this;
304
    }
305
306
    /**
307
     * Renew subscription period.
308
     *
309
     * @throws \LogicException
310
     *
311
     * @return $this
312
     */
313
    public function renew()
314
    {
315
        if ($this->ended() && $this->canceled()) {
316
            throw new LogicException('Unable to renew canceled ended subscription.');
317
        }
318
319
        $subscription = $this;
320
321
        DB::transaction(function () use ($subscription) {
322
            // Clear usage data
323
            $subscription->usage()->delete();
324
325
            // Renew period
326
            $subscription->setNewPeriod();
327
            $subscription->canceled_at = null;
328
            $subscription->save();
329
        });
330
331
        return $this;
332
    }
333
334
    /**
335
     * Get bookings of the given subscriber.
336
     *
337
     * @param \Illuminate\Database\Eloquent\Builder $builder
338
     * @param \Illuminate\Database\Eloquent\Model   $subscriber
339
     *
340
     * @return \Illuminate\Database\Eloquent\Builder
341
     */
342
    public function scopeOfSubscriber(Builder $builder, Model $subscriber): Builder
343
    {
344
        return $builder->where('subscriber_type', $subscriber->getMorphClass())->where('subscriber_id', $subscriber->getKey());
345
    }
346
347
    /**
348
     * Scope subscriptions with ending trial.
349
     *
350
     * @param \Illuminate\Database\Eloquent\Builder $builder
351
     * @param int                                   $dayRange
352
     *
353
     * @return \Illuminate\Database\Eloquent\Builder
354
     */
355
    public function scopeFindEndingTrial(Builder $builder, int $dayRange = 3): Builder
356
    {
357
        $from = Carbon::now();
358
        $to = Carbon::now()->addDays($dayRange);
359
360
        return $builder->whereBetween('trial_ends_at', [$from, $to]);
361
    }
362
363
    /**
364
     * Scope subscriptions with ended trial.
365
     *
366
     * @param \Illuminate\Database\Eloquent\Builder $builder
367
     *
368
     * @return \Illuminate\Database\Eloquent\Builder
369
     */
370
    public function scopeFindEndedTrial(Builder $builder): Builder
371
    {
372
        return $builder->where('trial_ends_at', '<=', now());
373
    }
374
375
    /**
376
     * Scope subscriptions with ending periods.
377
     *
378
     * @param \Illuminate\Database\Eloquent\Builder $builder
379
     * @param int                                   $dayRange
380
     *
381
     * @return \Illuminate\Database\Eloquent\Builder
382
     */
383
    public function scopeFindEndingPeriod(Builder $builder, int $dayRange = 3): Builder
384
    {
385
        $from = Carbon::now();
386
        $to = Carbon::now()->addDays($dayRange);
387
388
        return $builder->whereBetween('ends_at', [$from, $to]);
389
    }
390
391
    /**
392
     * Scope subscriptions with ended periods.
393
     *
394
     * @param \Illuminate\Database\Eloquent\Builder $builder
395
     *
396
     * @return \Illuminate\Database\Eloquent\Builder
397
     */
398
    public function scopeFindEndedPeriod(Builder $builder): Builder
399
    {
400
        return $builder->where('ends_at', '<=', now());
401
    }
402
403
    /**
404
     * Set new subscription period.
405
     *
406
     * @param string $invoice_interval
407
     * @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...
408
     * @param string $start
409
     *
410
     * @return $this
411
     */
412
    protected function setNewPeriod($invoice_interval = '', $invoice_period = '', $start = '')
413
    {
414
        if (empty($invoice_interval)) {
415
            $invoice_interval = $this->plan->invoice_interval;
416
        }
417
418
        if (empty($invoice_period)) {
419
            $invoice_period = $this->plan->invoice_period;
420
        }
421
422
        $period = new Period($invoice_interval, $invoice_period, $start);
423
424
        $this->starts_at = $period->getStartDate();
425
        $this->ends_at = $period->getEndDate();
426
427
        return $this;
428
    }
429
430
    /**
431
     * Record feature usage.
432
     *
433
     * @param string $featureSlug
434
     * @param int    $uses
435
     *
436
     * @return \Rinvex\Subscriptions\Models\PlanSubscriptionUsage
437
     */
438
    public function recordFeatureUsage(string $featureSlug, int $uses = 1, bool $incremental = true): PlanSubscriptionUsage
439
    {
440
        $feature = $this->plan->features()->where('slug', $featureSlug)->first();
441
442
        $usage = $this->usage()->firstOrNew([
443
            'subscription_id' => $this->getKey(),
444
            'feature_id' => $feature->getKey(),
445
        ]);
446
447
        if ($feature->resettable_period) {
448
            // Set expiration date when the usage record is new or doesn't have one.
449
            if (is_null($usage->valid_until)) {
450
                // Set date from subscription creation date so the reset
451
                // period match the period specified by the subscription's plan.
452
                $usage->valid_until = $feature->getResetDate($this->created_at);
453
            } elseif ($usage->expired()) {
454
                // If the usage record has been expired, let's assign
455
                // a new expiration date and reset the uses to zero.
456
                $usage->valid_until = $feature->getResetDate($usage->valid_until);
457
                $usage->used = 0;
458
            }
459
        }
460
461
        $usage->used = ($incremental ? $usage->used + $uses : $uses);
462
463
        $usage->save();
464
465
        return $usage;
466
    }
467
468
    /**
469
     * Reduce usage.
470
     *
471
     * @param string $featureSlug
472
     * @param int    $uses
473
     *
474
     * @return \Rinvex\Subscriptions\Models\PlanSubscriptionUsage|null
475
     */
476
    public function reduceFeatureUsage(string $featureSlug, int $uses = 1): ?PlanSubscriptionUsage
477
    {
478
        $usage = $this->usage()->byFeatureSlug($featureSlug)->first();
479
480
        if (is_null($usage)) {
481
            return null;
482
        }
483
484
        $usage->used = max($usage->used - $uses, 0);
485
486
        $usage->save();
487
488
        return $usage;
489
    }
490
491
    /**
492
     * Determine if the feature can be used.
493
     *
494
     * @param string $featureSlug
495
     *
496
     * @return bool
497
     */
498
    public function canUseFeature(string $featureSlug): bool
499
    {
500
        $featureValue = $this->getFeatureValue($featureSlug);
501
        $usage = $this->usage()->byFeatureSlug($featureSlug)->first();
502
503
        if ($featureValue === 'true') {
504
            return true;
505
        }
506
507
        // If the feature value is zero, let's return false since
508
        // there's no uses available. (useful to disable countable features)
509
        if (! $usage || $usage->expired() || is_null($featureValue) || $featureValue === '0' || $featureValue === 'false') {
510
            return false;
511
        }
512
513
        // Check for available uses
514
        return $this->getFeatureRemainings($featureSlug) > 0;
515
    }
516
517
    /**
518
     * Get how many times the feature has been used.
519
     *
520
     * @param string $featureSlug
521
     *
522
     * @return int
523
     */
524
    public function getFeatureUsage(string $featureSlug): int
525
    {
526
        $usage = $this->usage()->byFeatureSlug($featureSlug)->first();
527
528
        return (! $usage || $usage->expired()) ? 0 : $usage->used;
529
    }
530
531
    /**
532
     * Get the available uses.
533
     *
534
     * @param string $featureSlug
535
     *
536
     * @return int
537
     */
538
    public function getFeatureRemainings(string $featureSlug): int
539
    {
540
        return $this->getFeatureValue($featureSlug) - $this->getFeatureUsage($featureSlug);
541
    }
542
543
    /**
544
     * Get feature value.
545
     *
546
     * @param string $featureSlug
547
     *
548
     * @return mixed
549
     */
550
    public function getFeatureValue(string $featureSlug)
551
    {
552
        $feature = $this->plan->features()->where('slug', $featureSlug)->first();
553
554
        return $feature->value ?? null;
555
    }
556
}
557