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]); |
|
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
![]() |
|||
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); |
|
400 | 2 | $status = $response->subscriptions[0]; |
|
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 |
|
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); |
|
450 | 2 | $status = $response->subscriptions[0]; |
|
451 | 2 | $activePeriod = $this->activePeriodOrCreate(); |
|
452 | |||
453 | 2 | if ($status->result == 'success') { |
|
454 | 1 | $this->state = 'canceled'; |
|
455 | 1 | $this->swap_at = $activePeriod |
|
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]; |
|
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); |
|
505 | 2 | $status = $response->subscriptions[0]; |
|
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 |