Files
zemailnator/app/Models/User.php
idevakk 34183dc3cb feat(payment): implement secure 1:1 Polar customer binding system
- Add polar_cust_id column to users table for direct customer mapping
  - Rewrite PolarProvider customer logic to use stored customer IDs
  - Eliminate email-based customer lookup to prevent cross-user contamination
  - Implement self-healing mechanism for invalid customer IDs
  - Maintain external_id binding for Polar's reference system
  - Add comprehensive logging for customer lookup operations
2025-12-04 13:05:47 -08:00

438 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',
'polar_cust_id',
];
/**
* 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(),
];
}
}