Issues (74)

src/Subscription.php (7 issues)

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

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

385
        /** @scrutinizer ignore-call */ 
386
        $response = Fastspring::swapSubscription($this->fastspring_id, $plan, $prorate, $quantity, $coupons);
Loading history...
386 2
        $status = $response->subscriptions[0];
387
388 2
        if ($status->result == 'success') {
389
            // we update subscription
390
            // according to prorate value
391 2
            if ($prorate) {
392
                // if prorate is true
393
                // the plan is changed immediately
394
                // no need to fill swap columns
395
396
                // if the plan is in the trial state
397
                // then delete the current period
398
                // because it will change immediately
399
                // but period won't update because it exists
400 1
                if ($this->state == 'trial') {
401
                    $activePeriod = $this->activePeriodOrCreate();
402
                    $activePeriod->delete();
403
                }
404
405 1
                $this->plan = $plan;
406 1
                $this->save();
407
            } else {
408
                // if prorate is false
409
                // save plan swap_to
410
                // because the plan will change after a while
411 1
                $activePeriod = $this->activePeriodOrCreate();
412
413 1
                $this->swap_to = $plan;
414 1
                $this->swap_at = $activePeriod
0 ignored issues
show
$activePeriod is of type Bgultekin\CashierFastspring\SubscriptionPeriod, thus it always evaluated to true.
Loading history...
415 1
                    ? $activePeriod->end_date
416
                    : null;
417 1
                $this->save();
418
            }
419
420 2
            return $this;
421
        }
422
423
        // else
424
        // TODO: it might be better to create custom exception
425
        throw new Exception('Swap operation failed. Response: '.json_encode($response));
426
    }
427
428
    /**
429
     * Cancel the subscription at the end of the billing period.
430
     *
431
     * @return object Response of fastspring
432
     */
433 1
    public function cancel()
434
    {
435 1
        $response = Fastspring::cancelSubscription($this->fastspring_id);
0 ignored issues
show
The method cancelSubscription() does not exist on Bgultekin\CashierFastspring\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

435
        /** @scrutinizer ignore-call */ 
436
        $response = Fastspring::cancelSubscription($this->fastspring_id);
Loading history...
436 1
        $status = $response->subscriptions[0];
437 1
        $activePeriod = $this->activePeriodOrCreate();
438
439 1
        if ($status->result == 'success') {
440 1
            $this->state = 'canceled';
441 1
            $this->swap_at = $activePeriod
0 ignored issues
show
$activePeriod is of type Bgultekin\CashierFastspring\SubscriptionPeriod, thus it always evaluated to true.
Loading history...
442 1
                ? $activePeriod->end_date
443
                : null;
444 1
            $this->save();
445
446 1
            return $this;
447
        }
448
449
        // else
450
        // TODO: it might be better to create custom exception
451
        throw new Exception('Cancel operation failed. Response: '.json_encode($response));
452
    }
453
454
    /**
455
     * Cancel the subscription immediately.
456
     *
457
     * @return object Response of fastspring
458
     */
459 1
    public function cancelNow()
460
    {
461 1
        $response = Fastspring::cancelSubscription($this->fastspring_id, ['billing_period' => 0]);
462 1
        $status = $response->subscriptions[0];
463
464 1
        if ($status->result == 'success') {
465
            // if it is canceled now
466
            // state should be deactivated
467 1
            $this->state = 'deactivated';
468 1
            $this->save();
469
470 1
            return $this;
471
        }
472
473
        // else
474
        // TODO: it might be better to create custom exception
475
        throw new Exception('CancelNow operation failed. Response: '.json_encode($response));
476
    }
477
478
    /**
479
     * Resume the cancelled subscription.
480
     *
481
     * @throws \LogicException
482
     * @throws \Exception
483
     *
484
     * @return object Response of fastspring
485
     */
486 2
    public function resume()
487
    {
488 2
        if (!$this->onGracePeriod()) {
489 1
            throw new LogicException('Unable to resume subscription that is not within grace period or not canceled.');
490
        }
491
492 1
        $response = Fastspring::uncancelSubscription($this->fastspring_id);
0 ignored issues
show
The method uncancelSubscription() does not exist on Bgultekin\CashierFastspring\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

492
        /** @scrutinizer ignore-call */ 
493
        $response = Fastspring::uncancelSubscription($this->fastspring_id);
Loading history...
493 1
        $status = $response->subscriptions[0];
494
495 1
        if ($status->result == 'success') {
496 1
            $this->state = 'active';
497
498
            // set null swap columns
499 1
            $this->swap_at = null;
500 1
            $this->swap_to = null;
501
502 1
            $this->save();
503
504 1
            return $this;
505
        }
506
507
        // else
508
        // TODO: it might be better to create custom exception
509
        throw new Exception('Resume operation failed. Response: '.json_encode($response));
510
    }
511
}
512