Files
2026-03-11 10:11:30 +01:00

262 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
use Spatie\Permission\Models\Role;
/**
* App\Models\RolePromotion
*
* @property int $id
* @property string $name
* @property string|null $description
* @property array $applicable_roles
* @property int $additional_days
* @property \Carbon\Carbon|null $start_date
* @property \Carbon\Carbon|null $end_date
* @property bool $is_active
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
* @property-read Collection|RolePromotionStat[] $statistics
*/
class RolePromotion extends Model // @phpstan-ignore missingType.iterableValue
{
protected $fillable = [
'name',
'description',
'applicable_roles',
'additional_days',
'start_date',
'end_date',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
'start_date' => 'date',
'end_date' => 'date',
'additional_days' => 'integer',
'applicable_roles' => 'array',
];
/**
* Get the statistics for this promotion
*
* @return HasMany<RolePromotionStat, $this>
*/
public function statistics(): HasMany
{
return $this->hasMany(RolePromotionStat::class);
}
/**
* Get the roles this promotion applies to
*/
public function getApplicableRolesModels(): mixed
{
if (empty($this->applicable_roles)) {
return static::getCustomRoles();
}
return Role::whereIn('id', $this->applicable_roles)->get();
}
/**
* Scope to get only valid (non-expired) promotions
*/
public function scopeValid(mixed $query): mixed
{
$now = Carbon::now();
return $query->where('is_active', true)
->where(function ($q) use ($now) {
$q->whereNull('end_date')
->orWhere('end_date', '>=', $now);
})
->where(function ($q) use ($now) {
$q->whereNull('start_date')
->orWhere('start_date', '<=', $now);
});
}
/**
* Get custom roles (excluding system roles)
*/
public static function getCustomRoles(): mixed
{
$systemRoles = ['Admin', 'User', 'Moderator', 'Disabled', 'Friend'];
return Role::whereNotIn('name', $systemRoles)->get();
}
/**
* Check if the promotion is currently active
*/
public function isCurrentlyActive(): bool
{
if (! $this->is_active) {
return false;
}
$now = Carbon::now();
if ($this->start_date && $now->lt($this->start_date)) {
return false;
}
if ($this->end_date && $now->gt($this->end_date)) {
return false;
}
return true;
}
/**
* Get active promotions for a specific role
*/
public static function getActivePromotions(?int $roleId = null): mixed
{
$query = static::where('is_active', true);
$now = Carbon::now();
$query->where(function ($q) use ($now) {
$q->whereNull('start_date')
->orWhere('start_date', '<=', $now);
});
$query->where(function ($q) use ($now) {
$q->whereNull('end_date')
->orWhere('end_date', '>=', $now);
});
if ($roleId !== null) {
$query->where(function ($q) use ($roleId) {
// If applicable_roles is empty, applies to all custom roles
$q->whereJsonLength('applicable_roles', 0)
->orWhereJsonContains('applicable_roles', $roleId);
});
}
return $query->get();
}
/**
* Calculate total additional days from active promotions
*/
public static function calculateAdditionalDays(int $roleId): int
{
$promotions = static::getActivePromotions($roleId);
return $promotions->sum('additional_days');
}
/**
* Get summary statistics for this promotion
*
* @return array<string, mixed>
*/
public function getSummaryStatistics(): array
{
return RolePromotionStat::getPromotionSummary($this->id);
}
/**
* Get the count of users who have received this promotion
*/
public function getTotalUpgrades(): int
{
return $this->statistics()->count();
}
/**
* Get the count of unique users who have received this promotion
*/
public function getUniqueUsersCount(): int
{
return $this->statistics()->distinct('user_id')->count('user_id');
}
/**
* Get the total days added across all applications of this promotion
*/
public function getTotalDaysAdded(): int
{
return $this->statistics()->sum('days_added');
}
/**
* Track a promotion application
*/
public function trackApplication(
int $userId,
int $roleId,
?Carbon $previousExpiryDate = null,
?Carbon $newExpiryDate = null
): RolePromotionStat {
return RolePromotionStat::recordPromotion(
$userId,
$this->id,
$roleId,
$this->additional_days,
$previousExpiryDate,
$newExpiryDate
);
}
/**
* Get statistics grouped by role
*
* @return array<int, array<string, mixed>>
*/
public function getStatisticsByRole(): array
{
return $this->statistics()
->with('role')
->get()
->groupBy('role_id')
->map(function ($stats, $roleId) {
/** @var RolePromotionStat|null $firstStat */
$firstStat = $stats->first();
return [
'role_id' => $roleId,
'role_name' => $firstStat?->role?->name,
'total_upgrades' => $stats->count(),
'total_days_added' => $stats->sum('days_added'),
'unique_users' => $stats->unique('user_id')->count(),
];
})
->values()
->all();
}
/**
* Get statistics for a specific time period
*
* @return array<string, mixed>
*/
public function getStatisticsForPeriod(Carbon $startDate, Carbon $endDate): array
{
$stats = $this->statistics()
->appliedBetween($startDate, $endDate)
->get();
return [
'period' => [
'start' => $startDate->toDateString(),
'end' => $endDate->toDateString(),
],
'total_upgrades' => $stats->count(),
'total_days_added' => $stats->sum('days_added'),
'unique_users' => $stats->unique('user_id')->count(),
'roles_affected' => $stats->unique('role_id')->count(),
];
}
}