Subscription   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 495
Duplicated Lines 0 %

Test Coverage

Coverage 95.97%

Importance

Changes 0
Metric Value
wmc 47
eloc 134
dl 0
loc 495
ccs 143
cts 149
cp 0.9597
rs 8.64
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A active() 0 3 1
A isLocal() 0 3 1
A valid() 0 3 1
A onGracePeriod() 0 3 1
A onTrial() 0 3 1
A user() 0 3 1
A activePeriodOrCreate() 0 7 2
A activeFastspringPeriodOrCreate() 0 19 2
A activePeriod() 0 6 1
A overdue() 0 3 1
A canceled() 0 3 1
A activeLocalPeriodOrCreate() 0 19 2
A createPeriodFromFastspring() 0 19 1
A owner() 0 7 2
A type() 0 3 2
A deactivated() 0 3 1
A isFastspring() 0 3 1
A trial() 0 3 1
A cancelled() 0 3 1
A periods() 0 3 1
A swap() 0 41 5
A resume() 0 22 3
A cancelNow() 0 15 2
B createPeriodLocally() 0 57 9
A cancel() 0 17 3

How to fix   Complexity   

Complex Class

Complex classes like Subscription often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Subscription, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file implements a Subscription.
4
 *
5
 * @author    Bilal Gultekin <[email protected]>
6
 * @author    Justin Hartman <[email protected]>
7
 * @copyright 2019 22 Digital
8
 * @license   MIT
9
 * @since     v0.1
10
 */
11
12
namespace TwentyTwoDigital\CashierFastspring;
13
14
use Carbon\Carbon;
15
use Exception;
16
use Illuminate\Database\Eloquent\Model;
17
use LogicException;
18
use TwentyTwoDigital\CashierFastspring\Fastspring\Fastspring;
19
20
/**
21
 * This class describes a subscription.
22
 *
23
 * {@inheritdoc}
24
 */
25
class Subscription extends Model
26
{
27
    /**
28
     * The attributes that are not mass assignable.
29
     *
30
     * @var array
31
     */
32
    protected $guarded = [];
33
34
    /**
35
     * The attributes that should be mutated to dates.
36
     *
37
     * @var array
38
     */
39
    protected $dates = [
40
        'created_at', 'updated_at', 'swap_at',
41
    ];
42
43
    /**
44
     * The date on which the billing cycle should be anchored.
45
     *
46
     * @var string|null
47
     */
48
    protected $billingCycleAnchor = null;
49
50
    /**
51
     * Get the user that owns the subscription.
52
     */
53 1
    public function user()
54
    {
55 1
        return $this->owner();
56
    }
57
58
    /**
59
     * Get periods of the subscription.
60
     */
61 11
    public function periods()
62
    {
63 11
        return $this->hasMany('TwentyTwoDigital\CashierFastspring\SubscriptionPeriod');
64
    }
65
66
    /**
67
     * Get active period of the subscription.
68
     */
69 6
    public function activePeriod()
70
    {
71 6
        return $this->hasOne('TwentyTwoDigital\CashierFastspring\SubscriptionPeriod')
72 6
                    ->where('start_date', '<=', Carbon::now()->format('Y-m-d H:i:s'))
73 6
                    ->where('end_date', '>=', Carbon::now()->format('Y-m-d H:i:s'))
74 6
                    ->where('type', $this->type());
75
    }
76
77
    /**
78
     * Get active period or retrieve the active period from fastspring and create.
79
     *
80
     * Note: This is not eloquent relation, it returns SubscriptionPeriod model directly.
81
     *
82
     * @return \TwentyTwoDigital\CashierFastspring\SubscriptionPeriod
83
     */
84 10
    public function activePeriodOrCreate()
85
    {
86 10
        if ($this->isFastspring()) {
87 5
            return $this->activeFastspringPeriodOrCreate();
88
        }
89
90 5
        return $this->activeLocalPeriodOrCreate();
91
    }
92
93
    /**
94
     * Get active fastspring period or retrieve the active period from fastspring and create.
95
     *
96
     * @return \TwentyTwoDigital\CashierFastspring\SubscriptionPeriod
97
     */
98 5
    public function activeFastspringPeriodOrCreate()
99
    {
100
        // activePeriod is not used on purpose
101
        // because it caches and causes confusion
102
        // after this method is called
103 5
        $today = Carbon::today()->format('Y-m-d');
104
105 5
        $activePeriod = SubscriptionPeriod::where('subscription_id', $this->id)
106 5
            ->where('start_date', '<=', $today)
107 5
            ->where('end_date', '>=', $today)
108 5
            ->where('type', 'fastspring')
109 5
            ->first();
110
111
        // if there is any return it
112 5
        if ($activePeriod) {
113 2
            return $activePeriod;
114
        }
115
116 4
        return $this->createPeriodFromFastspring();
117
    }
118
119
    /**
120
     * Get active local period or create.
121
     *
122
     * @return \TwentyTwoDigital\CashierFastspring\SubscriptionPeriod
123
     */
124 5
    public function activeLocalPeriodOrCreate()
125
    {
126
        // activePeriod is not used on purpose
127
        // because it caches and causes confusion
128
        // after this method is called
129 5
        $today = Carbon::today()->format('Y-m-d');
130
131 5
        $activePeriod = SubscriptionPeriod::where('subscription_id', $this->id)
132 5
            ->where('start_date', '<=', $today)
133 5
            ->where('end_date', '>=', $today)
134 5
            ->where('type', 'local')
135 5
            ->first();
136
137
        // if there is any return it
138 5
        if ($activePeriod) {
139
            return $activePeriod;
140
        }
141
142 5
        return $this->createPeriodLocally();
143
    }
144
145
    /**
146
     * Create period with the information from fastspring.
147
     *
148
     * @return \TwentyTwoDigital\CashierFastspring\SubscriptionPeriod
149
     */
150 4
    protected function createPeriodFromFastspring()
151
    {
152 4
        $response = Fastspring::getSubscriptionsEntries([$this->fastspring_id]);
0 ignored issues
show
Bug introduced by
The method getSubscriptionsEntries() does not exist on TwentyTwoDigital\Cashier...g\Fastspring\Fastspring. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

152
        /** @scrutinizer ignore-call */ 
153
        $response = Fastspring::getSubscriptionsEntries([$this->fastspring_id]);
Loading history...
153
154
        $period = [
155
            // there is no info related to type in the entries endpoint
156
            // so we assume it is regular type
157
            // because we create first periods (especially including trial if there is any)
158
            // at the subscription creation
159 4
            'type' => 'fastspring',
160
161
            // dates
162 4
            'start_date'      => $response[0]->beginPeriodDate,
163 4
            'end_date'        => $response[0]->endPeriodDate,
164 4
            'subscription_id' => $this->id,
165
        ];
166
167
        // try to find or create
168 4
        return SubscriptionPeriod::firstOrCreate($period);
169
    }
170
171
    /**
172
     * Create period for non-fastspring/local subscriptions.
173
     *
174
     * Simply finds latest and add its dates $interval_length * $interval_unit
175
     * If there is no subscription period, it creates a subscription period started today
176
     *
177
     * @throws \Exception
178
     *
179
     * @return \TwentyTwoDigital\CashierFastspring\SubscriptionPeriod
180
     */
181 5
    protected function createPeriodLocally()
182
    {
183 5
        $lastPeriod = $this->periods()->orderBy('end_date', 'desc')->first();
184 5
        $today = Carbon::today();
185
186
        // there may be times subscriptionperiods not created more than
187
        // interval_length * interval_unit
188
        // For this kind of situations, we should fill the blank (actually we dont
189
        // have to but while we are calculating it is nice to save them)
190
        do {
191
            // add interval value to it to create next start_date
192
            // and sub one day to get next end_date
193 5
            switch ($this->interval_unit) {
194
                // fastspring adds month without overflow
195
                // so lets we do the same
196 5
                case 'month':
197 2
                    $start_date = $lastPeriod
198 1
                        ? $lastPeriod->start_date->addMonthsNoOverflow($this->interval_length)
199 2
                        : Carbon::now();
200
201 2
                    $end_date = $start_date->copy()->addMonthsNoOverflow($this->interval_length)->subDay();
202 2
                    break;
203
204 3
                case 'week':
205 1
                    $start_date = $lastPeriod
206 1
                        ? $lastPeriod->start_date->addWeeks($this->interval_length)
207 1
                        : Carbon::now();
208
209 1
                    $end_date = $start_date->copy()->addWeeks($this->interval_length)->subDay();
210 1
                    break;
211
212
                // probably same thing with the year
213 2
                case 'year':
214 1
                    $start_date = $lastPeriod
215 1
                        ? $lastPeriod->start_date->addYearsNoOverflow($this->interval_length)
216 1
                        : Carbon::now();
217
218 1
                    $end_date = $start_date->copy()->addYearsNoOverflow($this->interval_length)->subDay();
219 1
                    break;
220
221
                default:
222 1
                    throw new Exception('Unexcepted interval unit: ' . $subscription->interval_unit);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $subscription does not exist. Did you maybe mean $subscriptionPeriodData?
Loading history...
223
            }
224
225
            $subscriptionPeriodData = [
226 4
                'type'            => 'local',
227 4
                'start_date'      => $start_date->format('Y-m-d'),
228 4
                'end_date'        => $end_date->format('Y-m-d'),
229 4
                'subscription_id' => $this->id,
230
            ];
231
232 4
            $lastPeriod = SubscriptionPeriod::firstOrCreate($subscriptionPeriodData);
233 4
        } while (!($today->greaterThanOrEqualTo($lastPeriod->start_date)
234 4
            && $today->lessThanOrEqualTo($lastPeriod->end_date)
235
        ));
236
237 4
        return $lastPeriod;
238
    }
239
240
    /**
241
     * Get the model related to the subscription.
242
     *
243
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
244
     */
245 1
    public function owner()
246
    {
247 1
        $model = getenv('FASTSPRING_MODEL') ?: config('services.fastspring.model', 'App\\User');
248
249 1
        $model = new $model();
250
251 1
        return $this->belongsTo(get_class($model), $model->getForeignKey());
252
    }
253
254
    /**
255
     * Determine if the subscription is valid.
256
     * This includes following states on fastspring: active, trial, overdue, canceled.
257
     * The only state that you should stop serving is deactivated state.
258
     *
259
     * @return bool
260
     */
261 5
    public function valid()
262
    {
263 5
        return !$this->deactivated();
264
    }
265
266
    /**
267
     * Determine if the subscription is active.
268
     *
269
     * @return bool
270
     */
271 4
    public function active()
272
    {
273 4
        return $this->state == 'active';
274
    }
275
276
    /**
277
     * Determine if the subscription is deactivated.
278
     *
279
     * @return bool
280
     */
281 5
    public function deactivated()
282
    {
283 5
        return $this->state == 'deactivated';
284
    }
285
286
    /**
287
     * Determine if the subscription is not paid and in wait.
288
     *
289
     * @return bool
290
     */
291 4
    public function overdue()
292
    {
293 4
        return $this->state == 'overdue';
294
    }
295
296
    /**
297
     * Determine if the subscription is on trial.
298
     *
299
     * @return bool
300
     */
301 5
    public function trial()
302
    {
303 5
        return $this->state == 'trial';
304
    }
305
306
    /**
307
     * Determine if the subscription is cancelled.
308
     *
309
     * Note: That doesn't mean you should stop serving. This state means
310
     * user ordered to cancel at end of the billing period.
311
     * Subscription is converted into deactivated on the start of next payment period,
312
     * after cancelling it.
313
     *
314
     * @return bool
315
     */
316 7
    public function canceled()
317
    {
318 7
        return $this->state == 'canceled';
319
    }
320
321
    /**
322
     * ALIASES.
323
     */
324
325
    /**
326
     * Alias of canceled.
327
     *
328
     * @return bool
329
     */
330 2
    public function cancelled()
331
    {
332 2
        return $this->canceled();
333
    }
334
335
    /**
336
     * Determine if the subscription is within its trial period.
337
     *
338
     * @return bool
339
     */
340 5
    public function onTrial()
341
    {
342 5
        return $this->trial();
343
    }
344
345
    /**
346
     * Determine if the subscription is within its grace period after cancellation.
347
     *
348
     * @return bool
349
     */
350 7
    public function onGracePeriod()
351
    {
352 7
        return $this->canceled();
353
    }
354
355
    /**
356
     * Determine type of the subscription: fastspring, local.
357
     *
358
     * @return string
359
     */
360 11
    public function type()
361
    {
362 11
        return $this->fastspring_id ? 'fastspring' : 'local';
363
    }
364
365
    /**
366
     * Determine if the subscription is local.
367
     *
368
     * @return bool
369
     */
370 1
    public function isLocal()
371
    {
372 1
        return $this->type() == 'local';
373
    }
374
375
    /**
376
     * Determine if the subscription is fastspring.
377
     *
378
     * @return string
379
     */
380 11
    public function isFastspring()
381
    {
382 11
        return $this->type() == 'fastspring';
383
    }
384
385
    /**
386
     * Swap the subscription to a new Fastspring plan.
387
     *
388
     * @param string $plan     New plan
389
     * @param bool   $prorate  Prorate
390
     * @param int    $quantity Quantity of the product
391
     * @param array  $coupons  Coupons wanted to be applied
392
     *
393
     * @throws \Exception
394
     *
395
     * @return object Response of fastspring
396
     */
397 2
    public function swap($plan, $prorate, $quantity = 1, $coupons = [])
398
    {
399 2
        $response = Fastspring::swapSubscription($this->fastspring_id, $plan, $prorate, $quantity, $coupons);
0 ignored issues
show
Bug introduced by
The method swapSubscription() does not exist on TwentyTwoDigital\Cashier...g\Fastspring\Fastspring. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

399
        /** @scrutinizer ignore-call */ 
400
        $response = Fastspring::swapSubscription($this->fastspring_id, $plan, $prorate, $quantity, $coupons);
Loading history...
400 2
        $status = $response->subscriptions[0];
0 ignored issues
show
Bug introduced by
The property subscriptions does not seem to exist on TwentyTwoDigital\Cashier...g\Fastspring\Fastspring.
Loading history...
401
402 2
        if ($status->result == 'success') {
403
            // we update subscription
404
            // according to prorate value
405 2
            if ($prorate) {
406
                // if prorate is true
407
                // the plan is changed immediately
408
                // no need to fill swap columns
409
410
                // if the plan is in the trial state
411
                // then delete the current period
412
                // because it will change immediately
413
                // but period won't update because it exists
414 1
                if ($this->state == 'trial') {
415
                    $activePeriod = $this->activePeriodOrCreate();
416
                    $activePeriod->delete();
417
                }
418
419 1
                $this->plan = $plan;
420 1
                $this->save();
421
            } else {
422
                // if prorate is false
423
                // save plan swap_to
424
                // because the plan will change after a while
425 1
                $activePeriod = $this->activePeriodOrCreate();
426
427 1
                $this->swap_to = $plan;
428 1
                $this->swap_at = $activePeriod
0 ignored issues
show
introduced by
$activePeriod is of type TwentyTwoDigital\Cashier...ring\SubscriptionPeriod, thus it always evaluated to true.
Loading history...
429 1
                    ? $activePeriod->end_date
430
                    : null;
431 1
                $this->save();
432
            }
433
434 2
            return $this;
435
        }
436
437
        throw new Exception('Swap operation failed. Response: ' . json_encode($response));
438
    }
439
440
    /**
441
     * Cancel the subscription at the end of the billing period.
442
     *
443
     * @throws \Exception
444
     *
445
     * @return object Response of fastspring
446
     */
447 2
    public function cancel()
448
    {
449 2
        $response = Fastspring::cancelSubscription($this->fastspring_id);
0 ignored issues
show
Bug introduced by
The method cancelSubscription() does not exist on TwentyTwoDigital\Cashier...g\Fastspring\Fastspring. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

449
        /** @scrutinizer ignore-call */ 
450
        $response = Fastspring::cancelSubscription($this->fastspring_id);
Loading history...
450 2
        $status = $response->subscriptions[0];
0 ignored issues
show
Bug introduced by
The property subscriptions does not seem to exist on TwentyTwoDigital\Cashier...g\Fastspring\Fastspring.
Loading history...
451 2
        $activePeriod = $this->activePeriodOrCreate();
452
453 2
        if ($status->result == 'success') {
454 1
            $this->state = 'canceled';
455 1
            $this->swap_at = $activePeriod
0 ignored issues
show
introduced by
$activePeriod is of type TwentyTwoDigital\Cashier...ring\SubscriptionPeriod, thus it always evaluated to true.
Loading history...
456 1
                ? $activePeriod->end_date
457
                : null;
458 1
            $this->save();
459
460 1
            return $this;
461
        }
462
463 1
        throw new Exception('Cancel operation failed. Response: ' . json_encode($response));
464
    }
465
466
    /**
467
     * Cancel the subscription immediately.
468
     *
469
     * @throws \Exception
470
     *
471
     * @return object Response of fastspring
472
     */
473 2
    public function cancelNow()
474
    {
475 2
        $response = Fastspring::cancelSubscription($this->fastspring_id, ['billing_period' => 0]);
476 2
        $status = $response->subscriptions[0];
0 ignored issues
show
Bug introduced by
The property subscriptions does not seem to exist on TwentyTwoDigital\Cashier...g\Fastspring\Fastspring.
Loading history...
477
478 2
        if ($status->result == 'success') {
479
            // if it is canceled now
480
            // state should be deactivated
481 1
            $this->state = 'deactivated';
482 1
            $this->save();
483
484 1
            return $this;
485
        }
486
487 1
        throw new Exception('CancelNow operation failed. Response: ' . json_encode($response));
488
    }
489
490
    /**
491
     * Resume the cancelled subscription.
492
     *
493
     * @throws \LogicException
494
     * @throws \Exception
495
     *
496
     * @return object Response of fastspring
497
     */
498 3
    public function resume()
499
    {
500 3
        if (!$this->onGracePeriod()) {
501 1
            throw new LogicException('Unable to resume subscription that is not within grace period or not canceled.');
502
        }
503
504 2
        $response = Fastspring::uncancelSubscription($this->fastspring_id);
0 ignored issues
show
Bug introduced by
The method uncancelSubscription() does not exist on TwentyTwoDigital\Cashier...g\Fastspring\Fastspring. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

504
        /** @scrutinizer ignore-call */ 
505
        $response = Fastspring::uncancelSubscription($this->fastspring_id);
Loading history...
505 2
        $status = $response->subscriptions[0];
0 ignored issues
show
Bug introduced by
The property subscriptions does not seem to exist on TwentyTwoDigital\Cashier...g\Fastspring\Fastspring.
Loading history...
506
507 2
        if ($status->result == 'success') {
508 1
            $this->state = 'active';
509
510
            // set null swap columns
511 1
            $this->swap_at = null;
512 1
            $this->swap_to = null;
513
514 1
            $this->save();
515
516 1
            return $this;
517
        }
518
519 1
        throw new Exception('Resume operation failed. Response: ' . json_encode($response));
520
    }
521
}
522