313
common/Billing/Subscription.php
Executable file
313
common/Billing/Subscription.php
Executable file
@@ -0,0 +1,313 @@
|
||||
<?php namespace Common\Billing;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Billing\Gateways\Contracts\CommonSubscriptionGatewayActions;
|
||||
use Common\Billing\Gateways\Paypal\Paypal;
|
||||
use Common\Billing\Gateways\Stripe\Stripe;
|
||||
use Common\Billing\Invoices\Invoice;
|
||||
use Common\Billing\Models\Price;
|
||||
use Common\Billing\Models\Product;
|
||||
use Common\Billing\Subscriptions\SubscriptionFactory;
|
||||
use Common\Core\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use LogicException;
|
||||
|
||||
class Subscription extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const MODEL_TYPE = 'subscription';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $appends = [
|
||||
'on_grace_period',
|
||||
'past_due',
|
||||
'on_trial',
|
||||
'valid',
|
||||
'active',
|
||||
'cancelled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'price_id' => 'integer',
|
||||
'product_id' => 'integer',
|
||||
'quantity' => 'integer',
|
||||
'trial_ends_at' => 'datetime',
|
||||
'ends_at' => 'datetime',
|
||||
'renews_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function getOnGracePeriodAttribute(): bool
|
||||
{
|
||||
return $this->onGracePeriod();
|
||||
}
|
||||
|
||||
public function getOnTrialAttribute(): bool
|
||||
{
|
||||
return $this->onTrial();
|
||||
}
|
||||
|
||||
public function getPastDueAttribute(): bool
|
||||
{
|
||||
return $this->gateway()?->isSubscriptionPastDue($this) ?? false;
|
||||
}
|
||||
|
||||
public function getValidAttribute(): bool
|
||||
{
|
||||
return $this->valid();
|
||||
}
|
||||
|
||||
public function getActiveAttribute(): bool
|
||||
{
|
||||
return $this->active();
|
||||
}
|
||||
|
||||
public function getCancelledAttribute(): bool
|
||||
{
|
||||
return $this->cancelled();
|
||||
}
|
||||
|
||||
public function invoices(): HasMany
|
||||
{
|
||||
return $this->hasMany(Invoice::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function price(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Price::class);
|
||||
}
|
||||
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
public function onTrial(): bool
|
||||
{
|
||||
return $this->trial_ends_at?->isFuture() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is active, on trial, or within its grace period.
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->active() || $this->onTrial() || $this->onGracePeriod();
|
||||
}
|
||||
|
||||
public function ended(): bool
|
||||
{
|
||||
return $this->cancelled() && !$this->onGracePeriod();
|
||||
}
|
||||
|
||||
public function active(): bool
|
||||
{
|
||||
if ($this->ended()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// first payment failed, don't show billing page and allow opening checkout routes
|
||||
$gateway = $this->gateway();
|
||||
if ($gateway?->isSubscriptionIncomplete($this)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !$this->ended();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is no longer active.
|
||||
*/
|
||||
public function cancelled(): bool
|
||||
{
|
||||
return !is_null($this->ends_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is within its grace period after cancellation.
|
||||
*/
|
||||
public function onGracePeriod(): bool
|
||||
{
|
||||
return $this->ends_at?->isFuture() ?? false;
|
||||
}
|
||||
|
||||
public function changePlan(Product $newProduct, Price $newPrice): self
|
||||
{
|
||||
$isSuccess = $this->gateway()?->changePlan(
|
||||
$this,
|
||||
$newProduct,
|
||||
$newPrice,
|
||||
);
|
||||
|
||||
if ($isSuccess) {
|
||||
$this->fill([
|
||||
'product_id' => $newProduct->id,
|
||||
'price_id' => $newPrice->id,
|
||||
'ends_at' => null,
|
||||
])->save();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function cancel(bool $atPeriodEnd = true): self
|
||||
{
|
||||
if ($this->gateway_name !== 'none') {
|
||||
$this->gateway()->cancelSubscription($this, $atPeriodEnd);
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 = $this->renews_at;
|
||||
}
|
||||
|
||||
$this->renews_at = null;
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark subscription as cancelled on local database
|
||||
* only, without interacting with payment gateway.
|
||||
*/
|
||||
public function markAsCancelled(): void
|
||||
{
|
||||
$this->fill([
|
||||
'ends_at' => $this->renews_at,
|
||||
'renews_at' => null,
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the subscription immediately and delete it from database.
|
||||
*/
|
||||
public function cancelAndDelete(): self
|
||||
{
|
||||
$this->cancel(false);
|
||||
$this->delete();
|
||||
$this->invoices()->delete();
|
||||
|
||||
$this->user->update([
|
||||
'card_last_four' => null,
|
||||
'card_brand' => null,
|
||||
'card_expires' => null,
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resume(): self
|
||||
{
|
||||
if (!$this->onGracePeriod()) {
|
||||
throw new LogicException(
|
||||
__(
|
||||
'Unable to resume subscription that is not within grace period.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->onTrial()) {
|
||||
$trialEnd = $this->trial_ends_at->getTimestamp();
|
||||
} else {
|
||||
$trialEnd = 'now';
|
||||
}
|
||||
|
||||
// To resume the subscription we need to set the plan parameter on the Stripe
|
||||
// subscription object. This will force Stripe to resume this subscription
|
||||
// where we left off. Then, we'll set the proper trial ending timestamp.
|
||||
if ($this->gateway_name !== 'none') {
|
||||
$this->gateway()->resumeSubscription($this, [
|
||||
'trial_end' => $trialEnd,
|
||||
]);
|
||||
}
|
||||
|
||||
// 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 "cancelled". Then we will save this record in the database.
|
||||
$this->renews_at = $this->ends_at;
|
||||
$this->ends_at = null;
|
||||
$this->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gateway this subscription was created with.
|
||||
*/
|
||||
public function gateway(): ?CommonSubscriptionGatewayActions
|
||||
{
|
||||
if ($this->gateway_name === 'stripe') {
|
||||
return app(Stripe::class);
|
||||
} elseif ($this->gateway_name === 'paypal') {
|
||||
return app(Paypal::class);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function toNormalizedArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->this->gateway_name,
|
||||
'model_type' => self::MODEL_TYPE,
|
||||
];
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'product_id' => $this->product_id,
|
||||
'price_id' => $this->price_id,
|
||||
'gateway_name' => $this->gateway_name,
|
||||
'user' => $this->user ? $this->user->getSearchableValues() : null,
|
||||
'description' => $this->description,
|
||||
'ends_at' => $this->ends_at,
|
||||
'created_at' => $this->created_at->timestamp ?? '_null',
|
||||
'updated_at' => $this->updated_at->timestamp ?? '_null',
|
||||
];
|
||||
}
|
||||
|
||||
protected function makeAllSearchableUsing($query)
|
||||
{
|
||||
return $query->with(['user']);
|
||||
}
|
||||
|
||||
public static function filterableFields(): array
|
||||
{
|
||||
return [
|
||||
'id',
|
||||
'product_id',
|
||||
'price_id',
|
||||
'gateway_name',
|
||||
'ends_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function newFactory()
|
||||
{
|
||||
return SubscriptionFactory::new();
|
||||
}
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user