Completed
Push — master ( 2c4859...5ed06f )
by Adolfo
14s queued 11s
created

HasSubscriptions::downgradeTo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace Sagitarius29\LaravelSubscriptions\Traits;
4
5
use Carbon\Carbon;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Eloquent\Relations\MorphMany;
8
use Sagitarius29\LaravelSubscriptions\Contracts\PlanContract;
9
use Sagitarius29\LaravelSubscriptions\Contracts\PlanIntervalContract;
10
use Sagitarius29\LaravelSubscriptions\Contracts\SubscriptionContact;
11
use Sagitarius29\LaravelSubscriptions\Entities\PlanInterval;
12
use Sagitarius29\LaravelSubscriptions\Entities\Subscription;
13
use Sagitarius29\LaravelSubscriptions\Exceptions\SubscriptionErrorException;
14
use Sagitarius29\LaravelSubscriptions\PlanFeature;
15
16
trait HasSubscriptions
17
{
18
    /**
19
     * @param  PlanContract|PlanIntervalContract  $planOrInterval
20
     * @return Model|SubscriptionContact
21
     */
22
    public function subscribeTo($planOrInterval): SubscriptionContact
23
    {
24
        if ($planOrInterval instanceof PlanContract) {
25
            return $this->subscribeToPlan($planOrInterval);
26
        }
27
28
        return $this->subscribeToInterval($planOrInterval);
29
    }
30
31
    /**
32
     * @param  PlanContract  $plan
33
     * @return Model|SubscriptionContact
34
     * @throws SubscriptionErrorException
35
     */
36
    public function subscribeToPlan(PlanContract $plan): SubscriptionContact
37
    {
38
        if ($plan->isDisabled()) {
39
            throw new SubscriptionErrorException(
40
                'This plan has been disabled, please subscribe to other plan.'
41
            );
42
        }
43
44
        if ($this->subscriptions()->unfinished()->count() >= 2) {
45
            throw new SubscriptionErrorException('You are changed to other plan previously');
46
        }
47
48
        if ($plan->hasManyIntervals()) {
49
            throw new SubscriptionErrorException(
50
                'This plan has many intervals, please use subscribeToInterval() function'
51
            );
52
        }
53
54
        $currentSubscription = $this->getActiveSubscription();
55
        $start_at = null;
0 ignored issues
show
Unused Code introduced by
$start_at is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
56
        $end_at = null;
0 ignored issues
show
Unused Code introduced by
$end_at is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
57
58
        if ($currentSubscription == null) {
59
            $start_at = now();
60
        } else {
61
            $start_at = $currentSubscription->getExpirationDate();
0 ignored issues
show
Bug introduced by
The method getExpirationDate does only exist in Sagitarius29\LaravelSubs...cts\SubscriptionContact, but not in Illuminate\Database\Eloquent\Model.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
62
        }
63
64
        if ($plan->isFree()) {
65
            $end_at = null;
66
        } else {
67
            $end_at = $this->calculateExpireDate($start_at, optional($plan->intervals())->first());
68
        }
69
70
        $subscription = Subscription::make($plan, $start_at, $end_at);
71
        $subscription = $this->subscriptions()->save($subscription);
72
73
        return $subscription;
74
    }
75
76
    public function subscriptions(): MorphMany
77
    {
78
        return $this->morphMany(config('subscriptions.entities.plan_subscription'), 'subscriber');
0 ignored issues
show
Bug introduced by
It seems like morphMany() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
79
    }
80
81
    /**
82
     * @return SubscriptionContact|Model|null
83
     */
84
    public function getActiveSubscription(): ?SubscriptionContact
85
    {
86
        return $this->subscriptions()
87
            ->current()
88
            ->first();
89
    }
90
91
    private function calculateExpireDate(Carbon $start_at, PlanIntervalContract $interval)
92
    {
93
        $end_at = Carbon::createFromTimestamp($start_at->timestamp);
94
95
        switch ($interval->getType()) {
96
            case PlanInterval::DAY:
97
                return $end_at->addDays($interval->getUnit());
98
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
99
            case PlanInterval::MONTH:
100
                return $end_at->addMonths($interval->getUnit());
101
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
102
            case PlanInterval::YEAR:
103
                return $end_at->addYears($interval->getUnit());
104
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
105
            default:
106
                throw new SubscriptionErrorException(
107
                    'The interval \''.$interval->getType().'\' selected is not available.'
108
                );
109
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
110
        }
111
    }
112
113
    public function subscribeToInterval(PlanIntervalContract $interval): SubscriptionContact
114
    {
115
        if ($interval->plan->isDisabled()) {
0 ignored issues
show
Bug introduced by
Accessing plan on the interface Sagitarius29\LaravelSubs...ts\PlanIntervalContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
116
            throw new SubscriptionErrorException(
117
                'This plan has been disabled, please subscribe to other plan.'
118
            );
119
        }
120
121
        if ($this->subscriptions()->unfinished()->count() >= 2) {
122
            throw new SubscriptionErrorException('You are changed to other plan previously');
123
        }
124
125
        $currentSubscription = $this->getActiveSubscription();
126
        $start_at = null;
0 ignored issues
show
Unused Code introduced by
$start_at is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
127
        $end_at = null;
0 ignored issues
show
Unused Code introduced by
$end_at is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
128
129
        if ($currentSubscription == null) {
130
            $start_at = now();
131
        } else {
132
            $start_at = $currentSubscription->getExpirationDate();
0 ignored issues
show
Bug introduced by
The method getExpirationDate does only exist in Sagitarius29\LaravelSubs...cts\SubscriptionContact, but not in Illuminate\Database\Eloquent\Model.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
133
        }
134
135
        $end_at = $this->calculateExpireDate($start_at, $interval);
136
137
        $subscription = Subscription::make($interval->plan, $start_at, $end_at);
0 ignored issues
show
Bug introduced by
Accessing plan on the interface Sagitarius29\LaravelSubs...ts\PlanIntervalContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
138
        $subscription = $this->subscriptions()->save($subscription);
139
140
        return $subscription;
141
    }
142
143
    public function changePlanTo(PlanContract $plan, PlanIntervalContract $interval = null)
144
    {
145
        if (! $this->hasActiveSubscription()) {
146
            throw new SubscriptionErrorException('You need a subscription for upgrade to other.');
147
        }
148
149
        if ($plan->hasManyIntervals() && $interval == null) {
150
            throw new SubscriptionErrorException('The plan has many intervals, please indicate a interval.');
151
        }
152
153
        if ($this->subscriptions()->unfinished()->count() >= 2) {
154
            throw new SubscriptionErrorException('You are changed to other plan previously');
155
        }
156
157
        $currentSubscription = $this->getActiveSubscription();
158
        $currentPlan = $currentSubscription->plan;
159
        $currentIntervalPrice = $currentPlan->isFree() ? 0.00 : $currentPlan->getInterval()->getPrice();
160
161
        $toInterval = $plan->getInterval();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Sagitarius29\LaravelSubs...\Contracts\PlanContract as the method getInterval() does only exist in the following implementations of said interface: Sagitarius29\LaravelSubscriptions\Entities\Plan.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
162
163
        if ($currentPlan->id == $plan->id) {
0 ignored issues
show
Bug introduced by
Accessing id on the interface Sagitarius29\LaravelSubs...\Contracts\PlanContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
164
            throw new SubscriptionErrorException('You can\'t change to same plan. You need change to other plan.');
165
        }
166
167
        if ($interval !== null) {
168
            $toInterval = $interval;
169
        }
170
171
        if ($currentIntervalPrice < $toInterval->getPrice()) {
172
            return $this->upgradeTo($toInterval);
173
        }
174
175
        return $this->downgradeTo($toInterval);
176
    }
177
178
    public function hasActiveSubscription(): bool
179
    {
180
        return $this->subscriptions()
181
            ->current()
182
            ->exists();
183
    }
184
185
    protected function upgradeTo(PlanIntervalContract $interval): SubscriptionContact
186
    {
187
        if (! $this->hasActiveSubscription()) {
188
            throw new SubscriptionErrorException('You need a subscription for upgrade to other.');
189
        }
190
191
        $this->forceUnsubscribe();
192
193
        return $this->subscribeToInterval($interval);
194
    }
195
196
    public function forceUnsubscribe()
197
    {
198
        $currentSubscription = $this->getActiveSubscription();
199
        if ($currentSubscription != null) {
200
            $currentSubscription->end_at = now()->subSecond();
201
            $currentSubscription->cancelled_at = now();
202
            $currentSubscription->save();
0 ignored issues
show
Bug introduced by
The method save does only exist in Illuminate\Database\Eloquent\Model, but not in Sagitarius29\LaravelSubs...cts\SubscriptionContact.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
203
        }
204
    }
205
206
    protected function downgradeTo(PlanIntervalContract $interval): SubscriptionContact
207
    {
208
        if (! $this->hasActiveSubscription()) {
209
            throw new SubscriptionErrorException('You need a subscription for upgrade to other.');
210
        }
211
212
        return $this->subscribeToInterval($interval);
213
    }
214
215
    public function renewSubscription(PlanIntervalContract $interval = null)
216
    {
217
        if ($this->subscriptions()->unfinished()->count() >= 2) {
218
            throw new SubscriptionErrorException('You are changed to other plan previously');
219
        }
220
221
        $currentSubscription = $this->getActiveSubscription();
222
223
        if ($interval === null) {
224
            $plan = $currentSubscription->plan;
225
226
            if ($plan->hasManyIntervals()) {
227
                throw new SubscriptionErrorException(
228
                    'The plan you want will subscribe has many intervals, please consider renew to a interval of plan'
229
                );
230
            }
231
232
            $interval = $plan->intervals()->first();
233
        }
234
235
        $newExpireDate = $this->calculateExpireDate($currentSubscription->end_at, $interval);
236
237
        $currentSubscription->end_at = $newExpireDate;
238
        $currentSubscription->save();
0 ignored issues
show
Bug introduced by
The method save does only exist in Illuminate\Database\Eloquent\Model, but not in Sagitarius29\LaravelSubs...cts\SubscriptionContact.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
239
240
        return $currentSubscription;
241
    }
242
243
    public function unsubscribe()
244
    {
245
        $currentSubscription = $this->getActiveSubscription();
246
247
        if (isset($currentSubscription)) {
248
            $currentSubscription->cancelled_at = now();
249
            $currentSubscription->save();
0 ignored issues
show
Bug introduced by
The method save does only exist in Illuminate\Database\Eloquent\Model, but not in Sagitarius29\LaravelSubs...cts\SubscriptionContact.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
250
        }
251
    }
252
253
    public function abilityFor(string $featureCode)
254
    {
255
        if (! in_array($featureCode, config('subscriptions.default_features.features'))) {
256
            throw new SubscriptionErrorException('The "'.$featureCode.'" is not available in the system.');
257
        }
258
259
        $defaultFeature = config('subscriptions.default_features.features.'.$featureCode);
260
        $activeSubscription = $this->getActiveSubscription();
261
262
        if ($activeSubscription == null) {
263
            return $defaultFeature;
264
        }
265
266
        $feature = $activeSubscription->plan->getFeatureByCode($featureCode);
267
268
        if ($feature == null) {
269
            return $defaultFeature;
270
        }
271
272
        return $activeSubscription->plan->getFeatureByCode($featureCode)->getValue();
273
    }
274
275
    public function abilitiesList()
276
    {
277
        $loadFeatures = config('subscriptions.default_features.features');
278
        $activeSubscription = $this->getActiveSubscription();
279
280
        if ($activeSubscription == null) {
281
            return $loadFeatures;
282
        }
283
284
        $features = $activeSubscription->plan->features;
285
286
        $features->each(function (PlanFeature $feature) use (&$loadFeatures) {
287
            $loadFeatures[$feature->getCode()] = $feature->getValue();
288
        });
289
290
        return $loadFeatures;
291
    }
292
293
    public function getConsumables()
294
    {
295
        //TODO
296
    }
297
}
298