Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
7.48% covered (danger)
7.48%
24 / 321
11.27% covered (danger)
11.27%
8 / 71
CRAP
0.00% covered (danger)
0.00%
0 / 1
Subscription
7.48% covered (danger)
7.48%
24 / 321
11.27% covered (danger)
11.27%
8 / 71
13309.55
0.00% covered (danger)
0.00%
0 / 1
 boot
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
1.02
 user
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 owner
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 items
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasMultiplePlans
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasSinglePlan
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasPlan
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 findItemOrFail
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 valid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 incomplete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeIncomplete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 pastDue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopePastDue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 active
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
7
 canceled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeActive
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 syncStripeStatus
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 recurring
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 scopeRecurring
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 cancelled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeCancelled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeNotCancelled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 ended
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 scopeEnded
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onTrial
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 scopeOnTrial
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scopeNotOnTrial
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onGracePeriod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 proTeamEnded
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 scopeOnGracePeriod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 scopeNotOnGracePeriod
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 incrementQuantity
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 incrementAndInvoice
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 decrementQuantity
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 updateQuantity
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 reportUsage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 reportUsageFor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 usageRecords
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 usageRecordsFor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 anchorBillingCycleOn
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 skipTrial
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 endTrial
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 extendTrial
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 swap
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
42
 swapAndInvoice
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 parseSwapPlans
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 mergeItemsThatShouldBeDeletedDuringSwap
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getSwapOptions
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 addPlan
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 addPlanAndInvoice
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 removePlan
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 cancel
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 cancelAt
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 cancelNow
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 cancelNowAndInvoice
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 markAsCancelled
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 markAsCancelledOnlyInDB
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 resume
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 pending
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 invoice
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 latestInvoice
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 syncTaxPercentage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 syncTaxRates
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getPlanTaxRatesForPayload
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 hasIncompletePayment
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 latestPayment
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 guardAgainstIncomplete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 guardAgainstMultiplePlans
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 updateStripeSubscription
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 asStripeSubscription
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 plan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Models;
4
5use App\Observers\SubscriptionObserver;
6use Carbon\Carbon;
7use Carbon\CarbonInterface;
8use DateTimeInterface;
9use Illuminate\Database\Eloquent\Attributes\ObservedBy;
10use Illuminate\Database\Eloquent\Builder;
11use Illuminate\Database\Eloquent\Factories\HasFactory;
12use Illuminate\Database\Eloquent\Relations\BelongsTo;
13use Illuminate\Database\Eloquent\Relations\HasMany;
14use Illuminate\Notifications\Notifiable;
15use Illuminate\Support\Collection;
16use InvalidArgumentException;
17use Laravel\Cashier\Cashier;
18use Laravel\Cashier\Concerns\InteractsWithPaymentBehavior;
19use Laravel\Cashier\Concerns\Prorates;
20use Laravel\Cashier\Exceptions\IncompletePayment;
21use Laravel\Cashier\Exceptions\SubscriptionUpdateFailure;
22use Laravel\Cashier\Invoice;
23use Laravel\Cashier\Payment;
24use Laravel\Cashier\SubscriptionItem;
25use LogicException;
26use Stripe\Subscription as StripeSubscription;
27use Stripe\UsageRecord;
28
29/**
30 * This model should align with Laravel\Cashier\Subscription
31 * as much as possible especially in an upgrade
32 */
33#[ObservedBy([SubscriptionObserver::class])]
34class Subscription extends Moloquent
35{
36    use HasFactory, Notifiable, Prorates;
37    use InteractsWithPaymentBehavior;
38
39    /**
40     * The attributes that are not mass assignable.
41     *
42     * @var array
43     */
44    protected $guarded = [];
45
46    /**
47     * The relations to eager load on every query.
48     *
49     * @var array
50     */
51    protected $with = ['items'];
52
53    /**
54     * The attributes that should be cast to native types.
55     *
56     * @var array
57     */
58    protected $casts = [
59        'ends_at' => 'datetime',
60        'trial_ends_at' => 'datetime',
61        'quantity' => 'integer',
62    ];
63
64    /**
65     * The attributes that should be mutated to dates.
66     *
67     * @var array
68     */
69    protected $dates = [
70        'created_at',
71        'ends_at',
72        'trial_ends_at',
73        'updated_at',
74        'starts_at',
75    ];
76
77    /**
78     * The date on which the billing cycle should be anchored.
79     *
80     * @var string|null
81     */
82    protected $billingCycleAnchor = null;
83
84    /**
85     * Recent update to laravel cashier is forcing the stripe_plan field into stripe_price.
86     * Here is an attempt to ensure there's still a stripe_plan field.
87     */
88    public static function boot(): void
89    {
90        parent::boot();
91
92        self::creating(function ($model) {
93            $model->stripe_plan = $model->stripe_price ?? $model->stripe_plan;
94        });
95    }
96
97    /**
98     * Get the user that owns the subscription.
99     */
100    public function user(): BelongsTo
101    {
102        return $this->owner();
103    }
104
105    /**
106     * Get the model related to the subscription.
107     */
108    public function owner(): BelongsTo
109    {
110        $model = Cashier::$customerModel;
111
112        return $this->belongsTo($model, (new $model)->getForeignKey());
113    }
114
115    /**
116     * Get the subscription items related to the subscription.
117     */
118    public function items(): HasMany
119    {
120        return $this->hasMany(Cashier::$subscriptionItemModel);
121    }
122
123    /**
124     * Determine if the subscription has multiple plans.
125     */
126    public function hasMultiplePlans(): bool
127    {
128        return is_null($this->stripe_plan);
129    }
130
131    /**
132     * Determine if the subscription has a single plan.
133     */
134    public function hasSinglePlan(): bool
135    {
136        return ! $this->hasMultiplePlans();
137    }
138
139    /**
140     * Determine if the subscription has a specific plan.
141     */
142    public function hasPlan(string $plan): bool
143    {
144        if ($this->hasMultiplePlans()) {
145            return $this->items->contains(function (SubscriptionItem $item) use ($plan) {
146                return $item->stripe_plan === $plan;
147            });
148        }
149
150        return $this->stripe_plan === $plan;
151    }
152
153    /**
154     * Get the subscription item for the given plan.
155     *
156     *
157     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
158     */
159    public function findItemOrFail(string $plan): SubscriptionItem
160    {
161        return $this->items()->where('stripe_plan', $plan)->firstOrFail();
162    }
163
164    /**
165     * Determine if the subscription is active, on trial, or within its grace period.
166     */
167    public function valid(): bool
168    {
169        return $this->active() || $this->onTrial() || $this->onGracePeriod();
170    }
171
172    /**
173     * Determine if the subscription is incomplete.
174     */
175    public function incomplete(): bool
176    {
177        return $this->stripe_status === StripeSubscription::STATUS_INCOMPLETE;
178    }
179
180    /**
181     * Filter query by incomplete.
182     */
183    public function scopeIncomplete(Builder $query): void
184    {
185        $query->where('stripe_status', StripeSubscription::STATUS_INCOMPLETE);
186    }
187
188    /**
189     * Determine if the subscription is past due.
190     */
191    public function pastDue(): bool
192    {
193        return $this->stripe_status === StripeSubscription::STATUS_PAST_DUE;
194    }
195
196    /**
197     * Filter query by past due.
198     */
199    public function scopePastDue(Builder $query): void
200    {
201        $query->where('stripe_status', StripeSubscription::STATUS_PAST_DUE);
202    }
203
204    /**
205     * Determine if the subscription is active.
206     */
207    public function active(): bool
208    {
209        return (is_null($this->ends_at) || $this->onGracePeriod()) &&
210            $this->stripe_status !== StripeSubscription::STATUS_INCOMPLETE &&
211            $this->stripe_status !== StripeSubscription::STATUS_INCOMPLETE_EXPIRED &&
212            (! Cashier::$deactivatePastDue || $this->stripe_status !== StripeSubscription::STATUS_PAST_DUE) &&
213            $this->stripe_status !== StripeSubscription::STATUS_UNPAID;
214    }
215
216    public function canceled()
217    {
218        return ! is_null($this->ends_at);
219    }
220
221    /**
222     * Filter query by active.
223     */
224    public function scopeActive(Builder $query): void
225    {
226        $query->where(function ($query) {
227            $query->whereNull('ends_at')
228                ->orWhere(function ($query) {
229                    $query->onGracePeriod();
230                });
231        })->where('stripe_status', '!=', StripeSubscription::STATUS_INCOMPLETE)
232            ->where('stripe_status', '!=', StripeSubscription::STATUS_INCOMPLETE_EXPIRED)
233            ->where('stripe_status', '!=', StripeSubscription::STATUS_UNPAID);
234
235        if (Cashier::$deactivatePastDue) {
236            $query->where('stripe_status', '!=', StripeSubscription::STATUS_PAST_DUE);
237        }
238    }
239
240    /**
241     * Sync the Stripe status of the subscription.
242     */
243    public function syncStripeStatus(): void
244    {
245        $subscription = $this->asStripeSubscription();
246
247        $this->stripe_status = $subscription->status;
248
249        $this->save();
250    }
251
252    /**
253     * Determine if the subscription is recurring and not on trial.
254     */
255    public function recurring(): bool
256    {
257        return ! $this->onTrial() && ! $this->cancelled();
258    }
259
260    /**
261     * Filter query by recurring.
262     */
263    public function scopeRecurring(Builder $query): void
264    {
265        $query->notOnTrial()->notCancelled();
266    }
267
268    /**
269     * Determine if the subscription is no longer active.
270     */
271    public function cancelled(): bool
272    {
273        return ! is_null($this->ends_at);
274    }
275
276    /**
277     * Filter query by cancelled.
278     */
279    public function scopeCancelled(Builder $query): void
280    {
281        $query->whereNotNull('ends_at');
282    }
283
284    /**
285     * Filter query by not cancelled.
286     */
287    public function scopeNotCancelled(Builder $query): void
288    {
289        $query->whereNull('ends_at');
290    }
291
292    /**
293     * Determine if the subscription has ended and the grace period has expired.
294     */
295    public function ended(): bool
296    {
297        return $this->cancelled() && ! $this->onGracePeriod();
298    }
299
300    /**
301     * Filter query by ended.
302     */
303    public function scopeEnded(Builder $query): void
304    {
305        $query->cancelled()->notOnGracePeriod();
306    }
307
308    /**
309     * Determine if the subscription is within its trial period.
310     */
311    public function onTrial(): bool
312    {
313        return $this->trial_ends_at && $this->trial_ends_at->isFuture();
314    }
315
316    /**
317     * Filter query by on trial.
318     */
319    public function scopeOnTrial(Builder $query): void
320    {
321        $query->whereNotNull('trial_ends_at')->where('trial_ends_at', '>', Carbon::now());
322    }
323
324    /**
325     * Filter query by not on trial.
326     */
327    public function scopeNotOnTrial(Builder $query): void
328    {
329        $query->whereNull('trial_ends_at')->orWhere('trial_ends_at', '<=', Carbon::now());
330    }
331
332    /**
333     * Determine if the subscription is within its grace period after cancellation.
334     */
335    public function onGracePeriod(): bool
336    {
337        return $this->ends_at && $this->ends_at->isFuture();
338    }
339
340    public function proTeamEnded(): bool
341    {
342        return $this->ends_at && $this->ends_at->isPast();
343    }
344
345    /**
346     * Filter query by on grace period.
347     */
348    public function scopeOnGracePeriod(Builder $query): void
349    {
350        $query->whereNotNull('ends_at')->where('ends_at', '>', Carbon::now());
351    }
352
353    /**
354     * Filter query by not on grace period.
355     */
356    public function scopeNotOnGracePeriod(Builder $query): void
357    {
358        $query->whereNull('ends_at')->orWhere('ends_at', '<=', Carbon::now());
359    }
360
361    /**
362     * Increment the quantity of the subscription.
363     *
364     *
365     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
366     */
367    public function incrementQuantity(int $count = 1, ?string $plan = null): static
368    {
369        $this->guardAgainstIncomplete();
370
371        if ($plan) {
372            $this->findItemOrFail($plan)->setProrationBehavior($this->prorationBehavior)->incrementQuantity($count);
373
374            return $this->refresh();
375        }
376
377        $this->guardAgainstMultiplePlans();
378
379        return $this->updateQuantity($this->quantity + $count, $plan);
380    }
381
382    /**
383     *  Increment the quantity of the subscription, and invoice immediately.
384     *
385     *
386     * @throws \Laravel\Cashier\Exceptions\IncompletePayment
387     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
388     */
389    public function incrementAndInvoice(int $count = 1, ?string $plan = null): static
390    {
391        $this->guardAgainstIncomplete();
392
393        $this->alwaysInvoice();
394
395        return $this->incrementQuantity($count, $plan);
396    }
397
398    /**
399     * Decrement the quantity of the subscription.
400     *
401     *
402     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
403     */
404    public function decrementQuantity(int $count = 1, ?string $plan = null): static
405    {
406        $this->guardAgainstIncomplete();
407
408        if ($plan) {
409            $this->findItemOrFail($plan)->setProrationBehavior($this->prorationBehavior)->decrementQuantity($count);
410
411            return $this->refresh();
412        }
413
414        $this->guardAgainstMultiplePlans();
415
416        return $this->updateQuantity(max(1, $this->quantity - $count), $plan);
417    }
418
419    /**
420     * Update the quantity of the subscription.
421     *
422     *
423     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
424     */
425    public function updateQuantity(int $quantity, ?string $plan = null): static
426    {
427        $this->guardAgainstIncomplete();
428
429        if ($plan) {
430            $this->findItemOrFail($plan)->setProrationBehavior($this->prorationBehavior)->updateQuantity($quantity);
431
432            return $this->refresh();
433        }
434
435        $this->guardAgainstMultiplePlans();
436
437        $stripeSubscription = $this->asStripeSubscription();
438
439        $stripeSubscription->quantity = $quantity;
440        $stripeSubscription->payment_behavior = $this->paymentBehavior();
441        $stripeSubscription->proration_behavior = $this->prorateBehavior();
442
443        $stripeSubscription->save();
444
445        $this->quantity = $quantity;
446
447        $this->save();
448
449        return $this;
450    }
451
452    /**
453     * Report usage for a metered product.
454     *
455     * @param  \DateTimeInterface|int|null  $timestamp
456     */
457    public function reportUsage(int $quantity = 1, $timestamp = null, ?string $plan = null): UsageRecord
458    {
459        if (! $plan) {
460            $this->guardAgainstMultiplePlans();
461        }
462
463        return $this->findItemOrFail($plan ?? $this->stripe_plan)->reportUsage($quantity, $timestamp);
464    }
465
466    /**
467     * Report usage for specific price of a metered product.
468     *
469     * @param  \DateTimeInterface|int|null  $timestamp
470     */
471    public function reportUsageFor(string $plan, int $quantity = 1, $timestamp = null): UsageRecord
472    {
473        return $this->reportUsage($quantity, $timestamp, $plan);
474    }
475
476    /**
477     * Get the usage records for a metered product.
478     */
479    public function usageRecords(array $options = [], ?string $plan = null): Collection
480    {
481        if (! $plan) {
482            $this->guardAgainstMultiplePlans();
483        }
484
485        return $this->findItemOrFail($plan ?? $this->stripe_plan)->usageRecords($options);
486    }
487
488    /**
489     * Get the usage records for a specific price of a metered product.
490     */
491    public function usageRecordsFor(string $plan, array $options = []): Collection
492    {
493        return $this->usageRecords($options, $plan);
494    }
495
496    /**
497     * Change the billing cycle anchor on a plan change.
498     *
499     * @param  \DateTimeInterface|int|string  $date
500     */
501    public function anchorBillingCycleOn($date = 'now'): static
502    {
503        if ($date instanceof DateTimeInterface) {
504            $date = $date->getTimestamp();
505        }
506
507        $this->billingCycleAnchor = $date;
508
509        return $this;
510    }
511
512    /**
513     * Force the trial to end immediately.
514     *
515     * This method must be combined with swap, resume, etc.
516     */
517    public function skipTrial(): static
518    {
519        $this->trial_ends_at = null;
520
521        return $this;
522    }
523
524    /**
525     * Force the subscription's trial to end immediately.
526     */
527    public function endTrial(): static
528    {
529        if (is_null($this->trial_ends_at)) {
530            return $this;
531        }
532
533        $this->updateStripeSubscription([
534            'trial_end' => 'now',
535            'proration_behavior' => $this->prorateBehavior(),
536        ]);
537
538        $this->trial_ends_at = null;
539
540        $this->save();
541
542        return $this;
543    }
544
545    /**
546     * Extend an existing subscription's trial period.
547     */
548    public function extendTrial(CarbonInterface $date): static
549    {
550        if (! $date->isFuture()) {
551            throw new InvalidArgumentException("Extending a subscription's trial requires a date in the future.");
552        }
553
554        $this->updateStripeSubscription([
555            'trial_end' => $date->getTimestamp(),
556            'proration_behavior' => $this->prorateBehavior(),
557        ]);
558
559        $this->trial_ends_at = $date;
560
561        $this->save();
562
563        return $this;
564    }
565
566    /**
567     * Swap the subscription to new Stripe plans.
568     *
569     * @param  string|array  $plans
570     *
571     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
572     */
573    public function swap($plans, array $options = []): static
574    {
575        if (empty($plans = (array) $plans)) {
576            throw new InvalidArgumentException('Please provide at least one plan when swapping.');
577        }
578
579        $this->guardAgainstIncomplete();
580
581        $items = $this->mergeItemsThatShouldBeDeletedDuringSwap(
582            $this->parseSwapPlans($plans)
583        );
584
585        // $stripeSubscription = StripeSubscription::update(
586        //     $this->stripe_id,
587        //     $this->getSwapOptions($items, $options),
588        //     $this->owner->stripe()
589        // );
590        $stripeSubscription = $this->owner->stripe()->subscriptions->update(
591            $this->stripe_id,
592            $this->getSwapOptions($items, $options)
593        );
594
595        /** @var \Stripe\SubscriptionItem $firstItem */
596        $firstItem = $stripeSubscription->items->first();
597        $isSinglePlan = $stripeSubscription->items->count() === 1;
598
599        $this->fill([
600            'stripe_status' => $stripeSubscription->status,
601            'stripe_plan' => $isSinglePlan ? $firstItem->plan->id : null,
602            'quantity' => $isSinglePlan ? $firstItem->quantity : null,
603            'ends_at' => null,
604        ])->save();
605
606        foreach ($stripeSubscription->items as $item) {
607            $this->items()->updateOrCreate([
608                'stripe_id' => $item->id,
609            ], [
610                'stripe_plan' => $item->plan->id,
611                'quantity' => $item->quantity,
612            ]);
613        }
614
615        // Delete items that aren't attached to the subscription anymore...
616        $this->items()->whereNotIn('stripe_plan', $items->pluck('plan')->filter())->delete();
617
618        $this->unsetRelation('items');
619
620        if ($this->hasIncompletePayment()) {
621            (new Payment(
622                $stripeSubscription->latest_invoice->payment_intent
623            ))->validate();
624        }
625
626        return $this;
627    }
628
629    /**
630     * Swap the subscription to new Stripe plans, and invoice immediately.
631     *
632     * @param  string|array  $plans
633     *
634     * @throws \Laravel\Cashier\Exceptions\IncompletePayment
635     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
636     */
637    public function swapAndInvoice($plans, array $options = []): static
638    {
639        $this->alwaysInvoice();
640
641        return $this->swap($plans, $options);
642    }
643
644    /**
645     * Parse the given plans for a swap operation.
646     */
647    protected function parseSwapPlans(array $plans): Collection
648    {
649        $isSinglePlanSwap = $this->hasSinglePlan() && count($plans) === 1;
650
651        return collect($plans)->mapWithKeys(function ($options, $plan) use ($isSinglePlanSwap) {
652            $plan = is_string($options) ? $options : $plan;
653
654            $options = is_string($options) ? [] : $options;
655
656            return [$plan => array_merge([
657                'plan' => $plan,
658                'quantity' => $isSinglePlanSwap ? $this->quantity : null,
659                'tax_rates' => $this->getPlanTaxRatesForPayload($plan),
660            ], $options)];
661        });
662    }
663
664    /**
665     * Merge the items that should be deleted during swap into the given items collection.
666     */
667    protected function mergeItemsThatShouldBeDeletedDuringSwap(Collection $items): Collection
668    {
669        /** @var \Stripe\SubscriptionItem $stripeSubscriptionItem */
670        foreach ($this->asStripeSubscription()->items->data as $stripeSubscriptionItem) {
671            $plan = $stripeSubscriptionItem->plan;
672
673            if (! $item = $items->get($plan->id, [])) {
674                $item['deleted'] = true;
675
676                if ($plan->usage_type == 'metered') {
677                    $item['clear_usage'] = true;
678                }
679            }
680
681            $items->put($plan->id, $item + ['id' => $stripeSubscriptionItem->id]);
682        }
683
684        return $items;
685    }
686
687    /**
688     * Get the options array for a swap operation.
689     */
690    protected function getSwapOptions(Collection $items, array $options): array
691    {
692        $payload = [
693            'items' => $items->values()->all(),
694            'payment_behavior' => $this->paymentBehavior(),
695            'proration_behavior' => $this->prorateBehavior(),
696            'expand' => ['latest_invoice.payment_intent'],
697        ];
698
699        if ($payload['payment_behavior'] !== StripeSubscription::PAYMENT_BEHAVIOR_PENDING_IF_INCOMPLETE) {
700            $payload['cancel_at_period_end'] = false;
701        }
702
703        $payload = array_merge($payload, $options);
704
705        if (! is_null($this->billingCycleAnchor)) {
706            $payload['billing_cycle_anchor'] = $this->billingCycleAnchor;
707        }
708
709        $payload['trial_end'] = $this->onTrial()
710            ? $this->trial_ends_at->getTimestamp()
711            : 'now';
712
713        return $payload;
714    }
715
716    /**
717     * Add a new Stripe plan to the subscription.
718     *
719     *
720     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
721     */
722    public function addPlan(string $plan, ?int $quantity = 1, array $options = []): static
723    {
724        $this->guardAgainstIncomplete();
725
726        if ($this->items->contains('stripe_plan', $plan)) {
727            throw SubscriptionUpdateFailure::duplicatePlan($this, $plan);
728        }
729
730        $subscription = $this->asStripeSubscription();
731
732        $item = $subscription->items->create(array_merge([
733            'plan' => $plan,
734            'quantity' => $quantity,
735            'tax_rates' => $this->getPlanTaxRatesForPayload($plan),
736            'payment_behavior' => $this->paymentBehavior(),
737            'proration_behavior' => $this->prorateBehavior(),
738        ], $options));
739
740        $this->items()->create([
741            'stripe_id' => $item->id,
742            'stripe_plan' => $plan,
743            'quantity' => $quantity,
744        ]);
745
746        $this->unsetRelation('items');
747
748        if ($this->hasSinglePlan()) {
749            $this->fill([
750                'stripe_plan' => null,
751                'quantity' => null,
752            ])->save();
753        }
754
755        return $this;
756    }
757
758    /**
759     * Add a new Stripe plan to the subscription, and invoice immediately.
760     *
761     *
762     * @throws \Laravel\Cashier\Exceptions\IncompletePayment
763     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
764     */
765    public function addPlanAndInvoice(string $plan, int $quantity = 1, array $options = []): static
766    {
767        $this->alwaysInvoice();
768
769        return $this->addPlan($plan, $quantity, $options);
770    }
771
772    /**
773     * Remove a Stripe plan from the subscription.
774     *
775     *
776     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
777     */
778    public function removePlan(string $plan): static
779    {
780        if ($this->hasSinglePlan()) {
781            throw SubscriptionUpdateFailure::cannotDeleteLastPlan($this);
782        }
783
784        $stripeItem = $this->findItemOrFail($plan)->asStripeSubscriptionItem();
785
786        $stripeItem->delete(array_filter([
787            'clear_usage' => $stripeItem->plan->usage_type === 'metered' ? true : null,
788            'proration_behavior' => $this->prorateBehavior(),
789        ]));
790
791        $this->items()->where('stripe_plan', $plan)->delete();
792
793        $this->unsetRelation('items');
794
795        if ($this->items()->count() < 2) {
796            $item = $this->items()->first();
797
798            $this->fill([
799                'stripe_plan' => $item->stripe_plan,
800                'quantity' => $item->quantity,
801            ])->save();
802        }
803
804        return $this;
805    }
806
807    /**
808     * Cancel the subscription at the end of the billing period.
809     */
810    public function cancel(): static
811    {
812        $subscription = $this->asStripeSubscription();
813
814        $subscription->cancel_at_period_end = true;
815
816        $subscription = $subscription->save();
817
818        $this->stripe_status = $subscription->status;
819
820        // If the user was on trial, we will set the grace period to end when the trial
821        // would have ended. Otherwise, we'll retrieve the end of the billing period
822        // period and make that the end of the grace period for this current user.
823        if ($this->onTrial()) {
824            $this->ends_at = $this->trial_ends_at;
825        } else {
826            $this->ends_at = Carbon::createFromTimestamp(
827                $subscription->current_period_end
828            );
829        }
830
831        $this->save();
832
833        return $this;
834    }
835
836    /**
837     * Cancel the subscription at a specific moment in time.
838     *
839     * @param  \DateTimeInterface|int  $endsAt
840     */
841    public function cancelAt($endsAt): static
842    {
843        if ($endsAt instanceof DateTimeInterface) {
844            $endsAt = $endsAt->getTimestamp();
845        }
846
847        $subscription = $this->asStripeSubscription();
848
849        $subscription->proration_behavior = $this->prorateBehavior();
850
851        $subscription->cancel_at = $endsAt;
852
853        $subscription = $subscription->save();
854
855        $this->stripe_status = $subscription->status;
856
857        $this->ends_at = Carbon::createFromTimestamp($subscription->cancel_at);
858
859        $this->save();
860
861        return $this;
862    }
863
864    /**
865     * Cancel the subscription immediately without invoicing.
866     */
867    public function cancelNow(): static
868    {
869        $this->asStripeSubscription()->cancel([
870            'prorate' => $this->prorateBehavior() === 'create_prorations',
871        ]);
872
873        $this->markAsCancelled();
874
875        return $this;
876    }
877
878    /**
879     * Cancel the subscription immediately and invoice.
880     */
881    public function cancelNowAndInvoice(): static
882    {
883        $this->asStripeSubscription()->cancel([
884            'invoice_now' => true,
885            'prorate' => $this->prorateBehavior() === 'create_prorations',
886        ]);
887
888        $this->markAsCancelled();
889
890        return $this;
891    }
892
893    /**
894     * Mark the subscription as cancelled.
895     *
896     *
897     * @internal
898     */
899    public function markAsCancelled(): void
900    {
901        $this->fill([
902            'stripe_status' => StripeSubscription::STATUS_CANCELED,
903            'ends_at' => Carbon::now(),
904        ])->save();
905    }
906
907    public function markAsCancelledOnlyInDB(): void
908    {
909        $this->fill([
910            'stripe_status' => StripeSubscription::STATUS_CANCELED,
911            'ends_at' => Carbon::now(),
912        ])->save();
913    }
914
915    /**
916     * Resume the cancelled subscription.
917     *
918     *
919     * @throws \LogicException
920     */
921    public function resume(): static
922    {
923        if (! $this->onGracePeriod()) {
924            throw new LogicException('Unable to resume subscription that is not within grace period.');
925        }
926
927        $subscription = $this->asStripeSubscription();
928
929        $subscription->cancel_at_period_end = false;
930
931        if ($this->onTrial()) {
932            $subscription->trial_end = $this->trial_ends_at->getTimestamp();
933        } else {
934            $subscription->trial_end = 'now';
935        }
936
937        $subscription = $subscription->save();
938
939        // Finally, we will remove the ending timestamp from the user's record in the
940        // local database to indicate that the subscription is active again and is
941        // no longer "cancelled". Then we will save this record in the database.
942        $this->fill([
943            'stripe_status' => $subscription->status,
944            'ends_at' => null,
945        ])->save();
946
947        return $this;
948    }
949
950    /**
951     * Determine if the subscription has pending updates.
952     */
953    public function pending(): bool
954    {
955        return $this->asStripeSubscription()->pending_update !== null;
956    }
957
958    /**
959     * Invoice the subscription outside of the regular billing cycle.
960     *
961     * @return \Laravel\Cashier\Invoice|bool
962     *
963     * @throws \Laravel\Cashier\Exceptions\IncompletePayment
964     */
965    public function invoice(array $options = [])
966    {
967        try {
968            return $this->user->invoice(array_merge($options, ['subscription' => $this->stripe_id]));
969        } catch (IncompletePayment $exception) {
970            // Set the new Stripe subscription status immediately when payment fails...
971            $this->fill([
972                'stripe_status' => $exception->payment->invoice->subscription->status,
973            ])->save();
974
975            throw $exception;
976        }
977    }
978
979    /**
980     * Get the latest invoice for the subscription.
981     */
982    public function latestInvoice(): ?Invoice
983    {
984        $stripeSubscription = $this->asStripeSubscription(['latest_invoice']);
985
986        if ($stripeSubscription->latest_invoice) {
987            return new Invoice($this->owner, $stripeSubscription->latest_invoice);
988        }
989    }
990
991    /**
992     * Sync the tax percentage of the user to the subscription.
993     *
994     *
995     * @deprecated Please migrate to the new Tax Rates API.
996     */
997    public function syncTaxPercentage(): void
998    {
999        $subscription = $this->asStripeSubscription();
1000
1001        $subscription->tax_percent = $this->user->taxPercentage();
1002
1003        $subscription->save();
1004    }
1005
1006    /**
1007     * Sync the tax rates of the user to the subscription.
1008     */
1009    public function syncTaxRates(): void
1010    {
1011        $stripeSubscription = $this->asStripeSubscription();
1012
1013        $stripeSubscription->default_tax_rates = $this->user->taxRates() ?: null;
1014
1015        $stripeSubscription->proration_behavior = $this->prorateBehavior();
1016
1017        $stripeSubscription->save();
1018
1019        foreach ($this->items as $item) {
1020            $stripeSubscriptionItem = $item->asStripeSubscriptionItem();
1021
1022            $stripeSubscriptionItem->tax_rates = $this->getPlanTaxRatesForPayload($item->stripe_plan) ?: null;
1023
1024            $stripeSubscriptionItem->proration_behavior = $this->prorateBehavior();
1025
1026            $stripeSubscriptionItem->save();
1027        }
1028    }
1029
1030    /**
1031     * Get the plan tax rates for the Stripe payload.
1032     */
1033    public function getPlanTaxRatesForPayload(string $plan): ?array
1034    {
1035        if ($taxRates = $this->owner->planTaxRates()) {
1036            return $taxRates[$plan] ?? null;
1037        }
1038
1039        return null;
1040    }
1041
1042    /**
1043     * Determine if the subscription has an incomplete payment.
1044     */
1045    public function hasIncompletePayment(): bool
1046    {
1047        return $this->pastDue() || $this->incomplete();
1048    }
1049
1050    /**
1051     * Get the latest payment for a Subscription.
1052     */
1053    public function latestPayment(): ?Payment
1054    {
1055        $subscription = $this->asStripeSubscription(['latest_invoice.payment_intent']);
1056
1057        if ($invoice = $subscription->latest_invoice) {
1058            return $invoice->payment_intent
1059                ? new Payment($invoice->payment_intent)
1060                : null;
1061        }
1062    }
1063
1064    /**
1065     * Make sure a subscription is not incomplete when performing changes.
1066     *
1067     *
1068     * @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
1069     */
1070    public function guardAgainstIncomplete(): void
1071    {
1072        if ($this->incomplete()) {
1073            throw SubscriptionUpdateFailure::incompleteSubscription($this);
1074        }
1075    }
1076
1077    /**
1078     * Make sure a plan argument is provided when the subscription is a multi plan subscription.
1079     *
1080     *
1081     * @throws \InvalidArgumentException
1082     */
1083    public function guardAgainstMultiplePlans(): void
1084    {
1085        if ($this->hasMultiplePlans()) {
1086            throw new InvalidArgumentException(
1087                'This method requires a plan argument since the subscription has multiple plans.'
1088            );
1089        }
1090    }
1091
1092    /**
1093     * Update the underlying Stripe subscription information for the model.
1094     */
1095    public function updateStripeSubscription(array $options = []): StripeSubscription
1096    {
1097        return $this->owner->stripe()->subscriptions->update(
1098            $this->stripe_id,
1099            $options
1100        );
1101        // return StripeSubscription::update(
1102        //     $this->stripe_id,
1103        //     $options,
1104        //     $this->owner->stripe()
1105        // );
1106    }
1107
1108    /**
1109     * Get the subscription as a Stripe subscription object.
1110     */
1111    public function asStripeSubscription(array $expand = []): StripeSubscription
1112    {
1113        return $this->owner->stripe()->subscriptions->retrieve(
1114            $this->stripe_id,
1115            ['expand' => $expand]
1116        );
1117        // return StripeSubscription::retrieve(
1118        //     ['id' => $this->stripe_id, 'expand' => $expand],
1119        //     $this->owner->stripe()
1120        // );
1121    }
1122
1123    public function plan()
1124    {
1125        return $this->hasOne(Plans::class, 'stripe_id', 'stripe_plan');
1126    }
1127}