Files
newznab-tmux/app/Services/RegistrationStatusService.php
T
2026-07-14 13:26:18 +02:00

354 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\RegistrationPeriod;
use App\Models\RegistrationStatusHistory;
use App\Models\Settings;
use App\Models\User;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
class RegistrationStatusService
{
/**
* @return array<int, string>
*/
public function statusOptions(): array
{
return [
Settings::REGISTER_STATUS_OPEN => 'Open',
Settings::REGISTER_STATUS_INVITE => 'Invite',
Settings::REGISTER_STATUS_CLOSED => 'Closed',
];
}
public function statusLabel(int $status): string
{
return $this->statusOptions()[$status] ?? 'Unknown';
}
public function getManualStatus(): int
{
$manualStatus = (int) (Settings::settingValue('registerstatus') ?? Settings::REGISTER_STATUS_OPEN);
if (! array_key_exists($manualStatus, $this->statusOptions())) {
return Settings::REGISTER_STATUS_OPEN;
}
return $manualStatus;
}
public function getActivePeriod(?CarbonInterface $at = null): ?RegistrationPeriod
{
$at ??= now();
return RegistrationPeriod::query()
->activeAt($at)
->orderBy('starts_at')
->first();
}
public function getNextUpcomingPeriod(?CarbonInterface $at = null): ?RegistrationPeriod
{
$at ??= now();
return RegistrationPeriod::query()
->enabled()
->upcoming($at)
->orderBy('starts_at')
->first();
}
/**
* @return array{
* manual_status: int,
* manual_status_label: string,
* effective_status: int,
* effective_status_label: string,
* active_period: RegistrationPeriod|null,
* scheduled_override_active: bool,
* reason: string,
* message: string,
* available: bool,
* is_open: bool,
* is_invite_only: bool,
* is_closed: bool
* }
*/
public function resolve(?CarbonInterface $at = null): array
{
$at ??= now();
$manualStatus = $this->getManualStatus();
$activePeriod = $this->getActivePeriod($at);
$scheduledOverrideActive = $activePeriod !== null && $manualStatus !== Settings::REGISTER_STATUS_OPEN;
$effectiveStatus = $activePeriod !== null ? Settings::REGISTER_STATUS_OPEN : $manualStatus;
$reason = match (true) {
$scheduledOverrideActive => 'scheduled_open_period',
$effectiveStatus === Settings::REGISTER_STATUS_OPEN => 'manual_open',
$effectiveStatus === Settings::REGISTER_STATUS_INVITE => 'manual_invite',
default => 'manual_closed',
};
return [
'manual_status' => $manualStatus,
'manual_status_label' => $this->statusLabel($manualStatus),
'effective_status' => $effectiveStatus,
'effective_status_label' => $this->statusLabel($effectiveStatus),
'active_period' => $activePeriod,
'scheduled_override_active' => $scheduledOverrideActive,
'reason' => $reason,
'message' => $this->buildMessage($effectiveStatus, $activePeriod, $scheduledOverrideActive),
'available' => $effectiveStatus !== Settings::REGISTER_STATUS_CLOSED,
'is_open' => $effectiveStatus === Settings::REGISTER_STATUS_OPEN,
'is_invite_only' => $effectiveStatus === Settings::REGISTER_STATUS_INVITE,
'is_closed' => $effectiveStatus === Settings::REGISTER_STATUS_CLOSED,
];
}
public function updateManualStatus(int $newStatus, ?User $changedBy = null, ?string $note = null): void
{
$oldStatus = $this->getManualStatus();
Settings::settingsUpdate([
'registerstatus' => $newStatus,
]);
$this->forgetDashboardSnapshot();
if ($oldStatus === $newStatus && blank($note)) {
return;
}
$description = $oldStatus === $newStatus
? sprintf('Manual registration status note added while status remained %s.', $this->statusLabel($newStatus))
: sprintf(
'Manual registration status changed from %s to %s.',
$this->statusLabel($oldStatus),
$this->statusLabel($newStatus)
);
RegistrationStatusHistory::record(
RegistrationStatusHistory::ACTION_MANUAL_STATUS_CHANGED,
$description,
$changedBy?->id,
$oldStatus,
$newStatus,
null,
[
'note' => $note,
]
);
}
/**
* @param array{name: string, starts_at: mixed, ends_at: mixed, is_enabled: bool, notes?: string|null} $data
*/
public function createPeriod(array $data, ?User $changedBy = null): RegistrationPeriod
{
$period = RegistrationPeriod::query()->create([
'name' => $data['name'],
'starts_at' => $data['starts_at'],
'ends_at' => $data['ends_at'],
'is_enabled' => $data['is_enabled'],
'notes' => $data['notes'] ?? null,
'created_by' => $changedBy?->id,
'updated_by' => $changedBy?->id,
]);
RegistrationStatusHistory::record(
RegistrationStatusHistory::ACTION_PERIOD_CREATED,
sprintf('Scheduled open period "%s" created.', $period->name),
$changedBy?->id,
null,
null,
$period->id,
[
'period' => $this->serializePeriod($period),
]
);
$this->forgetDashboardSnapshot();
return $period;
}
/**
* @param array{name: string, starts_at: mixed, ends_at: mixed, is_enabled: bool, notes?: string|null} $data
*/
public function updatePeriod(RegistrationPeriod $period, array $data, ?User $changedBy = null): RegistrationPeriod
{
$before = $this->serializePeriod($period);
$period->fill([
'name' => $data['name'],
'starts_at' => $data['starts_at'],
'ends_at' => $data['ends_at'],
'is_enabled' => $data['is_enabled'],
'notes' => $data['notes'] ?? null,
'updated_by' => $changedBy?->id,
]);
$period->save();
RegistrationStatusHistory::record(
RegistrationStatusHistory::ACTION_PERIOD_UPDATED,
sprintf('Scheduled open period "%s" updated.', $period->name),
$changedBy?->id,
null,
null,
$period->id,
[
'before' => $before,
'after' => $this->serializePeriod($period),
]
);
$this->forgetDashboardSnapshot();
return $period;
}
public function togglePeriod(RegistrationPeriod $period, ?User $changedBy = null, ?string $note = null): RegistrationPeriod
{
$period->forceFill([
'is_enabled' => ! $period->is_enabled,
'updated_by' => $changedBy?->id,
])->save();
RegistrationStatusHistory::record(
RegistrationStatusHistory::ACTION_PERIOD_TOGGLED,
sprintf(
'Scheduled open period "%s" %s.',
$period->name,
$period->is_enabled ? 'enabled' : 'disabled'
),
$changedBy?->id,
null,
null,
$period->id,
[
'note' => $note,
'period' => $this->serializePeriod($period),
]
);
$this->forgetDashboardSnapshot();
return $period;
}
/**
* @return Collection<int, RegistrationPeriod>
*/
public function disableExpiredPeriods(?CarbonInterface $at = null): Collection
{
$at ??= now();
$expiredPeriods = RegistrationPeriod::query()
->where('is_enabled', true)
->where('ends_at', '<', $at)
->get();
foreach ($expiredPeriods as $period) {
$period->forceFill([
'is_enabled' => false,
'updated_by' => null,
])->save();
RegistrationStatusHistory::record(
RegistrationStatusHistory::ACTION_PERIOD_COMPLETED,
sprintf('Scheduled open period "%s" completed after reaching its end time.', $period->name),
null,
null,
null,
$period->id,
[
'completed_at' => $at->toDateTimeString(),
'period' => $this->serializePeriod($period),
]
);
}
if ($expiredPeriods->isNotEmpty()) {
$this->forgetDashboardSnapshot();
}
return $expiredPeriods;
}
public function deletePeriod(RegistrationPeriod $period, ?User $changedBy = null, ?string $note = null): void
{
$snapshot = $this->serializePeriod($period);
$periodName = $period->name;
$period->delete();
RegistrationStatusHistory::record(
RegistrationStatusHistory::ACTION_PERIOD_DELETED,
sprintf('Scheduled open period "%s" deleted.', $periodName),
$changedBy?->id,
null,
null,
null,
[
'note' => $note,
'period' => $snapshot,
]
);
$this->forgetDashboardSnapshot();
}
private function buildMessage(
int $effectiveStatus,
?RegistrationPeriod $activePeriod,
bool $scheduledOverrideActive
): string {
if ($scheduledOverrideActive && $activePeriod !== null) {
return sprintf(
'Registrations are temporarily open until %s because of the scheduled period "%s".',
$activePeriod->ends_at->format('Y-m-d H:i'),
$activePeriod->name
);
}
return match ($effectiveStatus) {
Settings::REGISTER_STATUS_OPEN => 'Registrations are currently open.',
Settings::REGISTER_STATUS_INVITE => 'Registrations are currently invite only.',
default => 'Registrations are currently closed.',
};
}
/**
* @return array<string, mixed>
*/
private function serializePeriod(RegistrationPeriod $period): array
{
return [
'id' => $period->id,
'name' => $period->name,
'starts_at' => $period->starts_at?->toDateTimeString(),
'ends_at' => $period->ends_at?->toDateTimeString(),
'is_enabled' => $period->is_enabled,
'notes' => $period->notes,
'created_by' => $period->created_by,
'updated_by' => $period->updated_by,
];
}
/**
* Drop the cached admin dashboard snapshot so the per-minute warmer (or
* the next admin request) rebuilds with fresh registration data. Avoids
* an inline rebuild here to keep write-path latency low.
*/
private function forgetDashboardSnapshot(): void
{
Cache::forget(AdminDashboardSnapshotService::CACHE_KEY);
Cache::forget('api_registration_status');
}
}