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