314 lines
8.0 KiB
PHP
Executable File
314 lines
8.0 KiB
PHP
Executable File
<?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;
|
|
}
|
|
}
|