%PDF- %GIF98; %PNG;
Server : ApacheSystem : Linux host.digitalbabaji.in 4.18.0-513.11.1.el8_9.x86_64 #1 SMP Wed Jan 17 02:00:40 EST 2024 x86_64 User : addictionfreeind ( 1003) PHP Version : 7.2.34 Disable Function : exec,passthru,shell_exec,system Directory : /home/addictionfreeind/public_html/admin1/vendor/laravel/cashier/src/ |
Upload File : |
<?php
namespace Laravel\Cashier;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use Laravel\Cashier\Concerns\AllowsCoupons;
use Laravel\Cashier\Concerns\HandlesPaymentFailures;
use Laravel\Cashier\Concerns\InteractsWithPaymentBehavior;
use Laravel\Cashier\Concerns\Prorates;
use Laravel\Cashier\Database\Factories\SubscriptionFactory;
use Laravel\Cashier\Exceptions\IncompletePayment;
use Laravel\Cashier\Exceptions\SubscriptionUpdateFailure;
use LogicException;
use Stripe\Subscription as StripeSubscription;
/**
* @property \Laravel\Cashier\Billable|\Illuminate\Database\Eloquent\Model $owner
*/
class Subscription extends Model
{
use AllowsCoupons;
use HandlesPaymentFailures;
use HasFactory;
use InteractsWithPaymentBehavior;
use Prorates;
/**
* The attributes that are not mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* The relations to eager load on every query.
*
* @var array
*/
protected $with = ['items'];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'ends_at' => 'datetime',
'quantity' => 'integer',
'trial_ends_at' => 'datetime',
];
/**
* The date on which the billing cycle should be anchored.
*
* @var string|null
*/
protected $billingCycleAnchor = null;
/**
* Get the user that owns the subscription.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->owner();
}
/**
* Get the model related to the subscription.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function owner()
{
$model = Cashier::$customerModel;
return $this->belongsTo($model, (new $model)->getForeignKey());
}
/**
* Get the subscription items related to the subscription.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function items()
{
return $this->hasMany(Cashier::$subscriptionItemModel);
}
/**
* Determine if the subscription has multiple prices.
*
* @return bool
*/
public function hasMultiplePrices()
{
return is_null($this->stripe_price);
}
/**
* Determine if the subscription has a single price.
*
* @return bool
*/
public function hasSinglePrice()
{
return ! $this->hasMultiplePrices();
}
/**
* Determine if the subscription has a specific product.
*
* @param string $product
* @return bool
*/
public function hasProduct($product)
{
return $this->items->contains(function (SubscriptionItem $item) use ($product) {
return $item->stripe_product === $product;
});
}
/**
* Determine if the subscription has a specific price.
*
* @param string $price
* @return bool
*/
public function hasPrice($price)
{
if ($this->hasMultiplePrices()) {
return $this->items->contains(function (SubscriptionItem $item) use ($price) {
return $item->stripe_price === $price;
});
}
return $this->stripe_price === $price;
}
/**
* Get the subscription item for the given price.
*
* @param string $price
* @return \Laravel\Cashier\SubscriptionItem
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
public function findItemOrFail($price)
{
return $this->items()->where('stripe_price', $price)->firstOrFail();
}
/**
* Determine if the subscription is active, on trial, or within its grace period.
*
* @return bool
*/
public function valid()
{
return $this->active() || $this->onTrial() || $this->onGracePeriod();
}
/**
* Determine if the subscription is incomplete.
*
* @return bool
*/
public function incomplete()
{
return $this->stripe_status === StripeSubscription::STATUS_INCOMPLETE;
}
/**
* Filter query by incomplete.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeIncomplete($query)
{
$query->where('stripe_status', StripeSubscription::STATUS_INCOMPLETE);
}
/**
* Determine if the subscription is past due.
*
* @return bool
*/
public function pastDue()
{
return $this->stripe_status === StripeSubscription::STATUS_PAST_DUE;
}
/**
* Filter query by past due.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopePastDue($query)
{
$query->where('stripe_status', StripeSubscription::STATUS_PAST_DUE);
}
/**
* Determine if the subscription is active.
*
* @return bool
*/
public function active()
{
return ! $this->ended() &&
(! Cashier::$deactivateIncomplete || $this->stripe_status !== StripeSubscription::STATUS_INCOMPLETE) &&
$this->stripe_status !== StripeSubscription::STATUS_INCOMPLETE_EXPIRED &&
(! Cashier::$deactivatePastDue || $this->stripe_status !== StripeSubscription::STATUS_PAST_DUE) &&
$this->stripe_status !== StripeSubscription::STATUS_UNPAID;
}
/**
* Filter query by active.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeActive($query)
{
$query->where(function ($query) {
$query->whereNull('ends_at')
->orWhere(function ($query) {
$query->onGracePeriod();
});
})->where('stripe_status', '!=', StripeSubscription::STATUS_INCOMPLETE_EXPIRED)
->where('stripe_status', '!=', StripeSubscription::STATUS_UNPAID);
if (Cashier::$deactivatePastDue) {
$query->where('stripe_status', '!=', StripeSubscription::STATUS_PAST_DUE);
}
if (Cashier::$deactivateIncomplete) {
$query->where('stripe_status', '!=', StripeSubscription::STATUS_INCOMPLETE);
}
}
/**
* Sync the Stripe status of the subscription.
*
* @return void
*/
public function syncStripeStatus()
{
$subscription = $this->asStripeSubscription();
$this->stripe_status = $subscription->status;
$this->save();
}
/**
* Determine if the subscription is recurring and not on trial.
*
* @return bool
*/
public function recurring()
{
return ! $this->onTrial() && ! $this->canceled();
}
/**
* Filter query by recurring.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeRecurring($query)
{
$query->notOnTrial()->notCanceled();
}
/**
* Determine if the subscription is no longer active.
*
* @return bool
*/
public function canceled()
{
return ! is_null($this->ends_at);
}
/**
* Filter query by canceled.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeCanceled($query)
{
$query->whereNotNull('ends_at');
}
/**
* Filter query by not canceled.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeNotCanceled($query)
{
$query->whereNull('ends_at');
}
/**
* Determine if the subscription has ended and the grace period has expired.
*
* @return bool
*/
public function ended()
{
return $this->canceled() && ! $this->onGracePeriod();
}
/**
* Filter query by ended.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeEnded($query)
{
$query->canceled()->notOnGracePeriod();
}
/**
* Determine if the subscription is within its trial period.
*
* @return bool
*/
public function onTrial()
{
return $this->trial_ends_at && $this->trial_ends_at->isFuture();
}
/**
* Filter query by on trial.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeOnTrial($query)
{
$query->whereNotNull('trial_ends_at')->where('trial_ends_at', '>', Carbon::now());
}
/**
* Determine if the subscription's trial has expired.
*
* @return bool
*/
public function hasExpiredTrial()
{
return $this->trial_ends_at && $this->trial_ends_at->isPast();
}
/**
* Filter query by expired trial.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeExpiredTrial($query)
{
$query->whereNotNull('trial_ends_at')->where('trial_ends_at', '<', Carbon::now());
}
/**
* Filter query by not on trial.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeNotOnTrial($query)
{
$query->whereNull('trial_ends_at')->orWhere('trial_ends_at', '<=', Carbon::now());
}
/**
* Determine if the subscription is within its grace period after cancellation.
*
* @return bool
*/
public function onGracePeriod()
{
return $this->ends_at && $this->ends_at->isFuture();
}
/**
* Filter query by on grace period.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeOnGracePeriod($query)
{
$query->whereNotNull('ends_at')->where('ends_at', '>', Carbon::now());
}
/**
* Filter query by not on grace period.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return void
*/
public function scopeNotOnGracePeriod($query)
{
$query->whereNull('ends_at')->orWhere('ends_at', '<=', Carbon::now());
}
/**
* Increment the quantity of the subscription.
*
* @param int $count
* @param string|null $price
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function incrementQuantity($count = 1, $price = null)
{
$this->guardAgainstIncomplete();
if ($price) {
$this->findItemOrFail($price)->setProrationBehavior($this->prorationBehavior)->incrementQuantity($count);
return $this->refresh();
}
$this->guardAgainstMultiplePrices();
return $this->updateQuantity($this->quantity + $count, $price);
}
/**
* Increment the quantity of the subscription, and invoice immediately.
*
* @param int $count
* @param string|null $price
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\IncompletePayment
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function incrementAndInvoice($count = 1, $price = null)
{
$this->guardAgainstIncomplete();
$this->alwaysInvoice();
return $this->incrementQuantity($count, $price);
}
/**
* Decrement the quantity of the subscription.
*
* @param int $count
* @param string|null $price
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function decrementQuantity($count = 1, $price = null)
{
$this->guardAgainstIncomplete();
if ($price) {
$this->findItemOrFail($price)->setProrationBehavior($this->prorationBehavior)->decrementQuantity($count);
return $this->refresh();
}
$this->guardAgainstMultiplePrices();
return $this->updateQuantity(max(1, $this->quantity - $count), $price);
}
/**
* Update the quantity of the subscription.
*
* @param int $quantity
* @param string|null $price
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function updateQuantity($quantity, $price = null)
{
$this->guardAgainstIncomplete();
if ($price) {
$this->findItemOrFail($price)->setProrationBehavior($this->prorationBehavior)->updateQuantity($quantity);
return $this->refresh();
}
$this->guardAgainstMultiplePrices();
$stripeSubscription = $this->updateStripeSubscription([
'payment_behavior' => $this->paymentBehavior(),
'proration_behavior' => $this->prorateBehavior(),
'quantity' => $quantity,
'expand' => ['latest_invoice.payment_intent'],
]);
$this->fill([
'stripe_status' => $stripeSubscription->status,
'quantity' => $stripeSubscription->quantity,
])->save();
$this->handlePaymentFailure($this);
return $this;
}
/**
* Report usage for a metered product.
*
* @param int $quantity
* @param \DateTimeInterface|int|null $timestamp
* @param string|null $price
* @return \Stripe\UsageRecord
*/
public function reportUsage($quantity = 1, $timestamp = null, $price = null)
{
if (! $price) {
$this->guardAgainstMultiplePrices();
}
return $this->findItemOrFail($price ?? $this->stripe_price)->reportUsage($quantity, $timestamp);
}
/**
* Report usage for specific price of a metered product.
*
* @param string $price
* @param int $quantity
* @param \DateTimeInterface|int|null $timestamp
* @return \Stripe\UsageRecord
*/
public function reportUsageFor($price, $quantity = 1, $timestamp = null)
{
return $this->reportUsage($quantity, $timestamp, $price);
}
/**
* Get the usage records for a metered product.
*
* @param array $options
* @param string|null $price
* @return \Illuminate\Support\Collection
*/
public function usageRecords(array $options = [], $price = null)
{
if (! $price) {
$this->guardAgainstMultiplePrices();
}
return $this->findItemOrFail($price ?? $this->stripe_price)->usageRecords($options);
}
/**
* Get the usage records for a specific price of a metered product.
*
* @param string $price
* @param array $options
* @return \Illuminate\Support\Collection
*/
public function usageRecordsFor($price, array $options = [])
{
return $this->usageRecords($options, $price);
}
/**
* Change the billing cycle anchor on a price change.
*
* @param \DateTimeInterface|int|string $date
* @return $this
*/
public function anchorBillingCycleOn($date = 'now')
{
if ($date instanceof DateTimeInterface) {
$date = $date->getTimestamp();
}
$this->billingCycleAnchor = $date;
return $this;
}
/**
* Force the trial to end immediately.
*
* This method must be combined with swap, resume, etc.
*
* @return $this
*/
public function skipTrial()
{
$this->trial_ends_at = null;
return $this;
}
/**
* Force the subscription's trial to end immediately.
*
* @return $this
*/
public function endTrial()
{
if (is_null($this->trial_ends_at)) {
return $this;
}
$this->updateStripeSubscription([
'trial_end' => 'now',
'proration_behavior' => $this->prorateBehavior(),
]);
$this->trial_ends_at = null;
$this->save();
return $this;
}
/**
* Extend an existing subscription's trial period.
*
* @param \Carbon\CarbonInterface $date
* @return $this
*/
public function extendTrial(CarbonInterface $date)
{
if (! $date->isFuture()) {
throw new InvalidArgumentException("Extending a subscription's trial requires a date in the future.");
}
$this->updateStripeSubscription([
'trial_end' => $date->getTimestamp(),
'proration_behavior' => $this->prorateBehavior(),
]);
$this->trial_ends_at = $date;
$this->save();
return $this;
}
/**
* Swap the subscription to new Stripe prices.
*
* @param string|array $prices
* @param array $options
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\IncompletePayment
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function swap($prices, array $options = [])
{
if (empty($prices = (array) $prices)) {
throw new InvalidArgumentException('Please provide at least one price when swapping.');
}
$this->guardAgainstIncomplete();
$items = $this->mergeItemsThatShouldBeDeletedDuringSwap(
$this->parseSwapPrices($prices)
);
$stripeSubscription = $this->owner->stripe()->subscriptions->update(
$this->stripe_id, $this->getSwapOptions($items, $options)
);
/** @var \Stripe\SubscriptionItem $firstItem */
$firstItem = $stripeSubscription->items->first();
$isSinglePrice = $stripeSubscription->items->count() === 1;
$this->fill([
'stripe_status' => $stripeSubscription->status,
'stripe_price' => $isSinglePrice ? $firstItem->price->id : null,
'quantity' => $isSinglePrice ? ($firstItem->quantity ?? null) : null,
'ends_at' => null,
])->save();
$stripePrices = [];
foreach ($stripeSubscription->items as $item) {
$stripePrices[] = $item->price->id;
$this->items()->updateOrCreate([
'stripe_id' => $item->id,
], [
'stripe_product' => $item->price->product,
'stripe_price' => $item->price->id,
'quantity' => $item->quantity ?? null,
]);
}
// Delete items that aren't attached to the subscription anymore...
$this->items()->whereNotIn('stripe_price', $stripePrices)->delete();
$this->unsetRelation('items');
$this->handlePaymentFailure($this);
return $this;
}
/**
* Swap the subscription to new Stripe prices, and invoice immediately.
*
* @param string|array $prices
* @param array $options
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\IncompletePayment
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function swapAndInvoice($prices, array $options = [])
{
$this->alwaysInvoice();
return $this->swap($prices, $options);
}
/**
* Parse the given prices for a swap operation.
*
* @param array $prices
* @return \Illuminate\Support\Collection
*/
protected function parseSwapPrices(array $prices)
{
$isSinglePriceSwap = $this->hasSinglePrice() && count($prices) === 1;
return Collection::make($prices)->mapWithKeys(function ($options, $price) use ($isSinglePriceSwap) {
$price = is_string($options) ? $options : $price;
$options = is_string($options) ? [] : $options;
$payload = [
'tax_rates' => $this->getPriceTaxRatesForPayload($price),
];
if (! isset($options['price_data'])) {
$payload['price'] = $price;
}
if ($isSinglePriceSwap && ! is_null($this->quantity)) {
$payload['quantity'] = $this->quantity;
}
return [$price => array_merge($payload, $options)];
});
}
/**
* Merge the items that should be deleted during swap into the given items collection.
*
* @param \Illuminate\Support\Collection $items
* @return \Illuminate\Support\Collection
*/
protected function mergeItemsThatShouldBeDeletedDuringSwap(Collection $items)
{
/** @var \Stripe\SubscriptionItem $stripeSubscriptionItem */
foreach ($this->asStripeSubscription()->items->data as $stripeSubscriptionItem) {
$price = $stripeSubscriptionItem->price;
if (! $item = $items->get($price->id, [])) {
$item['deleted'] = true;
if ($price->recurring->usage_type == 'metered') {
$item['clear_usage'] = true;
}
}
$items->put($price->id, $item + ['id' => $stripeSubscriptionItem->id]);
}
return $items;
}
/**
* Get the options array for a swap operation.
*
* @param \Illuminate\Support\Collection $items
* @param array $options
* @return array
*/
protected function getSwapOptions(Collection $items, array $options = [])
{
$payload = array_filter([
'items' => $items->values()->all(),
'payment_behavior' => $this->paymentBehavior(),
'promotion_code' => $this->promotionCodeId,
'proration_behavior' => $this->prorateBehavior(),
'expand' => ['latest_invoice.payment_intent'],
]);
if ($payload['payment_behavior'] !== StripeSubscription::PAYMENT_BEHAVIOR_PENDING_IF_INCOMPLETE) {
$payload['cancel_at_period_end'] = false;
}
$payload = array_merge($payload, $options);
if (! is_null($this->billingCycleAnchor)) {
$payload['billing_cycle_anchor'] = $this->billingCycleAnchor;
}
$payload['trial_end'] = $this->onTrial()
? $this->trial_ends_at->getTimestamp()
: 'now';
return $payload;
}
/**
* Add a new Stripe price to the subscription.
*
* @param string $price
* @param int|null $quantity
* @param array $options
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function addPrice($price, $quantity = 1, array $options = [])
{
$this->guardAgainstIncomplete();
if ($this->items->contains('stripe_price', $price)) {
throw SubscriptionUpdateFailure::duplicatePrice($this, $price);
}
$stripeSubscriptionItem = $this->owner->stripe()->subscriptionItems
->create(array_filter(array_merge([
'subscription' => $this->stripe_id,
'price' => $price,
'quantity' => $quantity,
'tax_rates' => $this->getPriceTaxRatesForPayload($price),
'payment_behavior' => $this->paymentBehavior(),
'proration_behavior' => $this->prorateBehavior(),
], $options)));
$this->items()->create([
'stripe_id' => $stripeSubscriptionItem->id,
'stripe_product' => $stripeSubscriptionItem->price->product,
'stripe_price' => $stripeSubscriptionItem->price->id,
'quantity' => $stripeSubscriptionItem->quantity ?? null,
]);
$this->unsetRelation('items');
$stripeSubscription = $this->asStripeSubscription();
if ($this->hasSinglePrice()) {
$this->fill([
'stripe_price' => null,
'quantity' => null,
]);
}
$this->fill([
'stripe_status' => $stripeSubscription->status,
])->save();
$this->handlePaymentFailure($this);
return $this;
}
/**
* Add a new Stripe price to the subscription, and invoice immediately.
*
* @param string $price
* @param int $quantity
* @param array $options
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\IncompletePayment
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function addPriceAndInvoice($price, $quantity = 1, array $options = [])
{
$this->alwaysInvoice();
return $this->addPrice($price, $quantity, $options);
}
/**
* Add a new Stripe metered price to the subscription.
*
* @param string $price
* @param array $options
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function addMeteredPrice($price, array $options = [])
{
return $this->addPrice($price, null, $options);
}
/**
* Add a new Stripe metered price to the subscription, and invoice immediately.
*
* @param string $price
* @param array $options
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\IncompletePayment
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function addMeteredPriceAndInvoice($price, array $options = [])
{
return $this->addPriceAndInvoice($price, null, $options);
}
/**
* Remove a Stripe price from the subscription.
*
* @param string $price
* @return $this
*
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function removePrice($price)
{
if ($this->hasSinglePrice()) {
throw SubscriptionUpdateFailure::cannotDeleteLastPrice($this);
}
$stripeItem = $this->findItemOrFail($price)->asStripeSubscriptionItem();
$stripeItem->delete(array_filter([
'clear_usage' => $stripeItem->price->recurring->usage_type === 'metered' ? true : null,
'proration_behavior' => $this->prorateBehavior(),
]));
$this->items()->where('stripe_price', $price)->delete();
$this->unsetRelation('items');
if ($this->items()->count() < 2) {
$item = $this->items()->first();
$this->fill([
'stripe_price' => $item->stripe_price,
'quantity' => $item->quantity,
])->save();
}
return $this;
}
/**
* Cancel the subscription at the end of the billing period.
*
* @return $this
*/
public function cancel()
{
$stripeSubscription = $this->updateStripeSubscription([
'cancel_at_period_end' => true,
]);
$this->stripe_status = $stripeSubscription->status;
// If the user was on trial, we will set the grace period to end when the trial
// would have ended. Otherwise, we'll retrieve the end of the billing period
// period and make that the end of the grace period for this current user.
if ($this->onTrial()) {
$this->ends_at = $this->trial_ends_at;
} else {
$this->ends_at = Carbon::createFromTimestamp(
$stripeSubscription->current_period_end
);
}
$this->save();
return $this;
}
/**
* Cancel the subscription at a specific moment in time.
*
* @param \DateTimeInterface|int $endsAt
* @return $this
*/
public function cancelAt($endsAt)
{
if ($endsAt instanceof DateTimeInterface) {
$endsAt = $endsAt->getTimestamp();
}
$stripeSubscription = $this->updateStripeSubscription([
'cancel_at' => $endsAt,
'proration_behavior' => $this->prorateBehavior(),
]);
$this->stripe_status = $stripeSubscription->status;
$this->ends_at = Carbon::createFromTimestamp($stripeSubscription->cancel_at);
$this->save();
return $this;
}
/**
* Cancel the subscription immediately without invoicing.
*
* @return $this
*/
public function cancelNow()
{
$this->owner->stripe()->subscriptions->cancel($this->stripe_id, [
'prorate' => $this->prorateBehavior() === 'create_prorations',
]);
$this->markAsCanceled();
return $this;
}
/**
* Cancel the subscription immediately and invoice.
*
* @return $this
*/
public function cancelNowAndInvoice()
{
$this->owner->stripe()->subscriptions->cancel($this->stripe_id, [
'invoice_now' => true,
'prorate' => $this->prorateBehavior() === 'create_prorations',
]);
$this->markAsCanceled();
return $this;
}
/**
* Mark the subscription as canceled.
*
* @return void
*
* @internal
*/
public function markAsCanceled()
{
$this->fill([
'stripe_status' => StripeSubscription::STATUS_CANCELED,
'ends_at' => Carbon::now(),
])->save();
}
/**
* Resume the canceled subscription.
*
* @return $this
*
* @throws \LogicException
*/
public function resume()
{
if (! $this->onGracePeriod()) {
throw new LogicException('Unable to resume subscription that is not within grace period.');
}
$stripeSubscription = $this->updateStripeSubscription([
'cancel_at_period_end' => false,
'trial_end' => $this->onTrial() ? $this->trial_ends_at->getTimestamp() : 'now',
]);
// Finally, we will remove the ending timestamp from the user's record in the
// local database to indicate that the subscription is active again and is
// no longer "canceled". Then we shall save this record in the database.
$this->fill([
'stripe_status' => $stripeSubscription->status,
'ends_at' => null,
])->save();
return $this;
}
/**
* Determine if the subscription has pending updates.
*
* @return bool
*/
public function pending()
{
return ! is_null($this->asStripeSubscription()->pending_update);
}
/**
* Invoice the subscription outside of the regular billing cycle.
*
* @param array $options
* @return \Laravel\Cashier\Invoice
*
* @throws \Laravel\Cashier\Exceptions\IncompletePayment
*/
public function invoice(array $options = [])
{
try {
return $this->user->invoice(array_merge($options, ['subscription' => $this->stripe_id]));
} catch (IncompletePayment $exception) {
// Set the new Stripe subscription status immediately when payment fails...
$this->fill([
'stripe_status' => $exception->payment->invoice->subscription->status,
])->save();
throw $exception;
}
}
/**
* Get the latest invoice for the subscription.
*
* @return \Laravel\Cashier\Invoice|null
*/
public function latestInvoice()
{
$stripeSubscription = $this->asStripeSubscription(['latest_invoice']);
if ($stripeSubscription->latest_invoice) {
return new Invoice($this->owner, $stripeSubscription->latest_invoice);
}
}
/**
* Fetches upcoming invoice for this subscription.
*
* @param array $options
* @return \Laravel\Cashier\Invoice|null
*/
public function upcomingInvoice(array $options = [])
{
return $this->owner->upcomingInvoice(array_merge([
'subscription' => $this->stripe_id,
], $options));
}
/**
* Preview the upcoming invoice with new Stripe prices.
*
* @param string|array $prices
* @param array $options
* @return \Laravel\Cashier\Invoice|null
*/
public function previewInvoice($prices, array $options = [])
{
if (empty($prices = (array) $prices)) {
throw new InvalidArgumentException('Please provide at least one price when swapping.');
}
$this->guardAgainstIncomplete();
$items = $this->mergeItemsThatShouldBeDeletedDuringSwap(
$this->parseSwapPrices($prices)
);
$swapOptions = Collection::make($this->getSwapOptions($items))
->only([
'billing_cycle_anchor',
'cancel_at_period_end',
'items',
'proration_behavior',
'trial_end',
])
->mapWithKeys(function ($value, $key) {
return ["subscription_$key" => $value];
})
->merge($options)
->all();
return $this->upcomingInvoice($swapOptions);
}
/**
* Get a collection of the subscription's invoices.
*
* @param bool $includePending
* @param array $parameters
* @return \Illuminate\Support\Collection|\Laravel\Cashier\Invoice[]
*/
public function invoices($includePending = false, $parameters = [])
{
return $this->owner->invoices(
$includePending, array_merge($parameters, ['subscription' => $this->stripe_id])
);
}
/**
* Get an array of the subscription's invoices, including pending invoices.
*
* @param array $parameters
* @return \Illuminate\Support\Collection|\Laravel\Cashier\Invoice[]
*/
public function invoicesIncludingPending(array $parameters = [])
{
return $this->invoices(true, $parameters);
}
/**
* Sync the tax rates of the user to the subscription.
*
* @return void
*/
public function syncTaxRates()
{
$this->updateStripeSubscription([
'default_tax_rates' => $this->user->taxRates() ?: null,
'proration_behavior' => $this->prorateBehavior(),
]);
foreach ($this->items as $item) {
$item->updateStripeSubscriptionItem([
'tax_rates' => $this->getPriceTaxRatesForPayload($item->stripe_price) ?: null,
'proration_behavior' => $this->prorateBehavior(),
]);
}
}
/**
* Get the price tax rates for the Stripe payload.
*
* @param string $price
* @return array|null
*/
public function getPriceTaxRatesForPayload($price)
{
if ($taxRates = $this->owner->priceTaxRates()) {
return $taxRates[$price] ?? null;
}
}
/**
* Determine if the subscription has an incomplete payment.
*
* @return bool
*/
public function hasIncompletePayment()
{
return $this->pastDue() || $this->incomplete();
}
/**
* Get the latest payment for a Subscription.
*
* @return \Laravel\Cashier\Payment|null
*/
public function latestPayment()
{
$subscription = $this->asStripeSubscription(['latest_invoice.payment_intent']);
if ($invoice = $subscription->latest_invoice) {
return $invoice->payment_intent
? new Payment($invoice->payment_intent)
: null;
}
}
/**
* The discount that applies to the subscription, if applicable.
*
* @return \Laravel\Cashier\Discount|null
*/
public function discount()
{
$subscription = $this->asStripeSubscription(['discount.promotion_code']);
return $subscription->discount
? new Discount($subscription->discount)
: null;
}
/**
* Apply a coupon to the subscription.
*
* @param string $coupon
* @return void
*/
public function applyCoupon($coupon)
{
$this->updateStripeSubscription([
'coupon' => $coupon,
]);
}
/**
* Apply a promotion code to the subscription.
*
* @param string $promotionCodeId
* @return void
*/
public function applyPromotionCode($promotionCodeId)
{
$this->updateStripeSubscription([
'promotion_code' => $promotionCodeId,
]);
}
/**
* Make sure a subscription is not incomplete when performing changes.
*
* @return void
*
* @throws \Laravel\Cashier\Exceptions\SubscriptionUpdateFailure
*/
public function guardAgainstIncomplete()
{
if ($this->incomplete()) {
throw SubscriptionUpdateFailure::incompleteSubscription($this);
}
}
/**
* Make sure a price argument is provided when the subscription is a subscription with multiple prices.
*
* @return void
*
* @throws \InvalidArgumentException
*/
public function guardAgainstMultiplePrices()
{
if ($this->hasMultiplePrices()) {
throw new InvalidArgumentException(
'This method requires a price argument since the subscription has multiple prices.'
);
}
}
/**
* Update the underlying Stripe subscription information for the model.
*
* @param array $options
* @return \Stripe\Subscription
*/
public function updateStripeSubscription(array $options = [])
{
return $this->owner->stripe()->subscriptions->update(
$this->stripe_id, $options
);
}
/**
* Get the subscription as a Stripe subscription object.
*
* @param array $expand
* @return \Stripe\Subscription
*/
public function asStripeSubscription(array $expand = [])
{
return $this->owner->stripe()->subscriptions->retrieve(
$this->stripe_id, ['expand' => $expand]
);
}
/**
* Create a new factory instance for the model.
*
* @return \Illuminate\Database\Eloquent\Factories\Factory
*/
protected static function newFactory()
{
return SubscriptionFactory::new();
}
}