437 lines
12 KiB
PHP
437 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\enum\UserLevel;
|
|
use Database\Factories\UserFactory;
|
|
use Filament\Models\Contracts\FilamentUser;
|
|
use Filament\Panel;
|
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
|
use Illuminate\Notifications\Notifiable;
|
|
use Illuminate\Support\Str;
|
|
use Laravel\Cashier\Billable;
|
|
use Laravel\Sanctum\HasApiTokens;
|
|
|
|
class User extends Authenticatable implements FilamentUser, MustVerifyEmail
|
|
{
|
|
/** @use HasFactory<UserFactory> */
|
|
use Billable, HasApiTokens, HasFactory, Notifiable;
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
protected $fillable = [
|
|
'name',
|
|
'email',
|
|
'password',
|
|
'level',
|
|
];
|
|
|
|
/**
|
|
* The attributes that should be hidden for serialization.
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
protected $hidden = [
|
|
'password',
|
|
'remember_token',
|
|
'level',
|
|
];
|
|
|
|
/**
|
|
* Get the attributes that should be cast.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'email_verified_at' => 'datetime',
|
|
'password' => 'hashed',
|
|
'level' => UserLevel::class,
|
|
];
|
|
}
|
|
|
|
public function initials(): string
|
|
{
|
|
return Str::of($this->name)
|
|
->explode(' ')
|
|
->map(fn (string $name) => Str::of($name)->substr(0, 1))
|
|
->implode('');
|
|
}
|
|
|
|
public function canAccessPanel(Panel $panel): bool
|
|
{
|
|
return $this->email === config('app.admin_email') && $this->level === UserLevel::SUPERADMIN && $this->hasVerifiedEmail();
|
|
}
|
|
|
|
/**
|
|
* Scope to query only super admin users.
|
|
*/
|
|
public function scopeIsSuperAdmin($query)
|
|
{
|
|
return $query->where('level', UserLevel::SUPERADMIN->value);
|
|
}
|
|
|
|
/**
|
|
* Scope to query only normal users.
|
|
*/
|
|
public function scopeIsNormalUser($query)
|
|
{
|
|
return $query->where('level', UserLevel::NORMALUSER->value);
|
|
}
|
|
|
|
/**
|
|
* Scope to query only banner users.
|
|
*/
|
|
public function scopeIsBannerUser($query)
|
|
{
|
|
return $query->where('level', UserLevel::BANNEDUSER->value);
|
|
}
|
|
|
|
/**
|
|
* Check if user is a super admin.
|
|
*/
|
|
public function isSuperAdmin(): bool
|
|
{
|
|
return $this->level === UserLevel::SUPERADMIN;
|
|
}
|
|
|
|
/**
|
|
* Check if user is a normal user.
|
|
*/
|
|
public function isNormalUser(): bool
|
|
{
|
|
return $this->level === UserLevel::NORMALUSER;
|
|
}
|
|
|
|
/**
|
|
* Check if user is a banner user.
|
|
*/
|
|
public function isBannerUser(): bool
|
|
{
|
|
return $this->level === UserLevel::BANNEDUSER;
|
|
}
|
|
|
|
/**
|
|
* Get user level name.
|
|
*/
|
|
public function getLevelName(): string
|
|
{
|
|
return $this->level->name;
|
|
}
|
|
|
|
public function tickets(): HasMany
|
|
{
|
|
return $this->hasMany(Ticket::class);
|
|
}
|
|
|
|
public function logs()
|
|
{
|
|
return $this->hasMany(Log::class);
|
|
}
|
|
|
|
public function usageLogs()
|
|
{
|
|
return $this->hasMany(UsageLog::class);
|
|
}
|
|
|
|
public function impersonationLogs()
|
|
{
|
|
return $this->hasMany(ImpersonationLog::class, 'admin_id');
|
|
}
|
|
|
|
public function impersonationTargets()
|
|
{
|
|
return $this->hasMany(ImpersonationLog::class, 'target_user_id');
|
|
}
|
|
|
|
/**
|
|
* Get all subscriptions for the user
|
|
*/
|
|
public function subscriptions()
|
|
{
|
|
return $this->hasMany(Subscription::class);
|
|
}
|
|
|
|
/**
|
|
* Get the current active subscription for the user
|
|
*/
|
|
public function currentSubscription()
|
|
{
|
|
return $this->hasOne(Subscription::class)
|
|
->where(function ($query) {
|
|
$query->where('status', 'active')
|
|
->orWhere('status', 'trialing');
|
|
})
|
|
->where(function ($query) {
|
|
$query->whereNull('ends_at')
|
|
->orWhere('ends_at', '>', now());
|
|
})
|
|
->latest();
|
|
}
|
|
|
|
/**
|
|
* Get the latest subscription (regardless of status)
|
|
*/
|
|
public function latestSubscription()
|
|
{
|
|
return $this->hasOne(Subscription::class)->latestOfMany();
|
|
}
|
|
|
|
/**
|
|
* Scope: Users with active subscriptions
|
|
*/
|
|
public function scopeWithActiveSubscription($query)
|
|
{
|
|
return $query->whereHas('subscriptions', function ($subscriptionQuery) {
|
|
$subscriptionQuery->where(function ($q) {
|
|
$q->where('status', 'active')
|
|
->orWhere('status', 'trialing');
|
|
})->where(function ($q) {
|
|
$q->whereNull('ends_at')
|
|
->orWhere('ends_at', '>', now());
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope: Users with trial subscriptions
|
|
*/
|
|
public function scopeWithTrialSubscription($query)
|
|
{
|
|
return $query->whereHas('subscriptions', function ($subscriptionQuery) {
|
|
$subscriptionQuery->where('status', 'trialing')
|
|
->where('trial_ends_at', '>', now());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope: Users with cancelled subscriptions
|
|
*/
|
|
public function scopeWithCancelledSubscription($query)
|
|
{
|
|
return $query->whereHas('subscriptions', function ($subscriptionQuery) {
|
|
$subscriptionQuery->where('status', 'cancelled')
|
|
->orWhere(function ($q) {
|
|
$q->where('ends_at', '<=', now());
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope: Users without any active subscriptions
|
|
*/
|
|
public function scopeWithoutActiveSubscription($query)
|
|
{
|
|
return $query->whereDoesntHave('subscriptions', function ($subscriptionQuery) {
|
|
$subscriptionQuery->where(function ($q) {
|
|
$q->where('status', 'active')
|
|
->orWhere('status', 'trialing');
|
|
})->where(function ($q) {
|
|
$q->whereNull('ends_at')
|
|
->orWhere('ends_at', '>', now());
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope: Users by subscription provider
|
|
*/
|
|
public function scopeBySubscriptionProvider($query, string $provider)
|
|
{
|
|
return $query->whereHas('subscriptions', function ($subscriptionQuery) use ($provider) {
|
|
$subscriptionQuery->where('provider', $provider)
|
|
->where(function ($q) {
|
|
$q->whereNull('ends_at')
|
|
->orWhere('ends_at', '>', now());
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Scope: Users with subscriptions expiring soon (within given days)
|
|
*/
|
|
public function scopeWithSubscriptionExpiringSoon($query, int $days = 7)
|
|
{
|
|
return $query->whereHas('subscriptions', function ($subscriptionQuery) use ($days) {
|
|
$subscriptionQuery->where('status', 'active')
|
|
->whereNotNull('ends_at')
|
|
->where('ends_at', '<=', now()->addDays($days))
|
|
->where('ends_at', '>', now());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if user has an active subscription
|
|
*/
|
|
public function hasActiveSubscription(): bool
|
|
{
|
|
return $this->subscriptions()
|
|
->where(function ($query) {
|
|
$query->where('status', 'active')
|
|
->orWhere('status', 'trialing');
|
|
})
|
|
->where(function ($query) {
|
|
$query->whereNull('ends_at')
|
|
->orWhere('ends_at', '>', now());
|
|
})
|
|
->exists();
|
|
}
|
|
|
|
/**
|
|
* Check if user is currently on trial
|
|
*/
|
|
public function isOnTrial(): bool
|
|
{
|
|
return $this->subscriptions()
|
|
->where('status', 'trialing')
|
|
->where('trial_ends_at', '>', now())
|
|
->exists();
|
|
}
|
|
|
|
/**
|
|
* Check if user has cancelled subscription
|
|
*/
|
|
public function hasCancelledSubscription(): bool
|
|
{
|
|
return $this->subscriptions()
|
|
->where(function ($query) {
|
|
$query->where('status', 'cancelled')
|
|
->orWhere(function ($q) {
|
|
$q->whereNotNull('ends_at')
|
|
->where('ends_at', '<=', now());
|
|
});
|
|
})
|
|
->exists();
|
|
}
|
|
|
|
/**
|
|
* Check if user has ever had a subscription
|
|
*/
|
|
public function hasHadSubscription(): bool
|
|
{
|
|
return $this->subscriptions()->exists();
|
|
}
|
|
|
|
/**
|
|
* Get user's subscription status as string
|
|
*/
|
|
public function getSubscriptionStatus(): string
|
|
{
|
|
if ($this->isOnTrial()) {
|
|
return 'trialing';
|
|
}
|
|
|
|
if ($this->hasActiveSubscription()) {
|
|
return 'active';
|
|
}
|
|
|
|
if ($this->hasCancelledSubscription()) {
|
|
return 'cancelled';
|
|
}
|
|
|
|
return 'none';
|
|
}
|
|
|
|
/**
|
|
* Get user's current subscription plan
|
|
*/
|
|
public function getCurrentPlan(): ?Plan
|
|
{
|
|
return $this->currentSubscription?->plan;
|
|
}
|
|
|
|
/**
|
|
* Get user's subscription expiry date
|
|
*/
|
|
public function getSubscriptionExpiryDate(): ?\Carbon\Carbon
|
|
{
|
|
return $this->currentSubscription?->ends_at;
|
|
}
|
|
|
|
/**
|
|
* Get user's trial end date
|
|
*/
|
|
public function getTrialEndDate(): ?\Carbon\Carbon
|
|
{
|
|
$trialSubscription = $this->subscriptions()
|
|
->where('status', 'trialing')
|
|
->where('trial_ends_at', '>', now())
|
|
->first();
|
|
|
|
return $trialSubscription?->trial_ends_at;
|
|
}
|
|
|
|
/**
|
|
* Check if user's subscription is expiring soon (within given days)
|
|
*/
|
|
public function isSubscriptionExpiringSoon(int $days = 7): bool
|
|
{
|
|
$currentSubscription = $this->currentSubscription;
|
|
|
|
return $currentSubscription &&
|
|
$currentSubscription->ends_at &&
|
|
$currentSubscription->ends_at->lte(now()->addDays($days)) &&
|
|
$currentSubscription->ends_at->gt(now());
|
|
}
|
|
|
|
/**
|
|
* Get total amount spent by user across all subscriptions
|
|
*/
|
|
public function getTotalSpent(): float
|
|
{
|
|
return $this->subscriptions()
|
|
->with('plan')
|
|
->get()
|
|
->sum(function ($subscription) {
|
|
return $subscription->plan ? $subscription->plan->price : 0;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get user's subscription provider
|
|
*/
|
|
public function getSubscriptionProvider(): ?string
|
|
{
|
|
return $this->currentSubscription?->provider;
|
|
}
|
|
|
|
/**
|
|
* Check if user can upgrade/downgrade their plan
|
|
*/
|
|
public function canChangePlan(): bool
|
|
{
|
|
return $this->hasActiveSubscription() &&
|
|
$this->currentSubscription?->isRecurring();
|
|
}
|
|
|
|
/**
|
|
* Get subscription metrics for analytics
|
|
*/
|
|
public function getSubscriptionMetrics(): array
|
|
{
|
|
$subscriptions = $this->subscriptions()->with('plan')->get();
|
|
|
|
return [
|
|
'total_subscriptions' => $subscriptions->count(),
|
|
'active_subscriptions' => $subscriptions->where(function ($sub) {
|
|
return in_array($sub->status, ['active', 'trialing']) &&
|
|
(! $sub->ends_at || $sub->ends_at->isFuture());
|
|
})->count(),
|
|
'total_spent' => $this->getTotalSpent(),
|
|
'current_plan' => $this->getCurrentPlan()?->name,
|
|
'provider' => $this->getSubscriptionProvider(),
|
|
'status' => $this->getSubscriptionStatus(),
|
|
'trial_ends_at' => $this->getTrialEndDate(),
|
|
'subscription_ends_at' => $this->getSubscriptionExpiryDate(),
|
|
'is_expiring_soon' => $this->isSubscriptionExpiringSoon(),
|
|
];
|
|
}
|
|
}
|