CluPilotCloud/app/Models/MaintenanceWindow.php

131 lines
4.0 KiB
PHP

<?php
namespace App\Models;
use App\Models\Concerns\HasUuid;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
class MaintenanceWindow extends Model
{
/** @use HasFactory<\Database\Factories\MaintenanceWindowFactory> */
use HasFactory, HasUuid;
/** How far before the start the banner appears (Codex: 72 h). */
public const DISPLAY_HOURS = 72;
protected $fillable = [
'title', 'public_description', 'internal_notes', 'starts_at', 'ends_at',
'state', 'created_by', 'published_at', 'cancelled_at', 'cancellation_reason',
];
protected function casts(): array
{
return [
'starts_at' => 'datetime',
'ends_at' => 'datetime',
'published_at' => 'datetime',
'cancelled_at' => 'datetime',
];
}
public function hosts(): BelongsToMany
{
return $this->belongsToMany(Host::class);
}
public function deliveries(): HasMany
{
return $this->hasMany(MaintenanceNotification::class);
}
/** Derived lifecycle state — never stored (Codex). */
public function derivedState(): string
{
if ($this->state === 'cancelled') {
return 'cancelled';
}
if ($this->state === 'draft') {
return 'draft';
}
if (now()->lt($this->starts_at)) {
return 'upcoming';
}
if (now()->lte($this->ends_at)) {
return 'active';
}
return 'completed';
}
public function isPublished(): bool
{
return $this->state === 'scheduled';
}
/** Service-bearing customers on the assigned hosts (recomputed live). */
public function affectedCustomers(): Collection
{
$hostIds = $this->hosts()->pluck('hosts.id');
if ($hostIds->isEmpty()) {
return collect();
}
return Customer::query()
->whereHas('instances', fn (Builder $q) => $q
->whereIn('host_id', $hostIds)
->whereIn('status', ['active', 'provisioning', 'cancellation_scheduled']))
->get();
}
/**
* Scheduled windows within the display horizon that affect ONE instance's
* host — for a per-instance badge, so a customer with several instances
* never sees another instance's maintenance on this card.
*/
public static function forInstance(?Instance $instance): Collection
{
if ($instance === null || $instance->host_id === null) {
return collect();
}
return self::query()
->where('state', 'scheduled')
->where('ends_at', '>=', now())
->where('starts_at', '<=', now()->addHours(self::DISPLAY_HOURS))
->whereHas('hosts', fn (Builder $q) => $q->whereKey($instance->host_id))
->orderBy('starts_at')
->get();
}
/**
* Scheduled windows to show a given customer right now: within the display
* horizon (72 h before start until the end) and touching one of the
* customer's service instances' hosts.
*/
public static function bannerFor(Customer $customer): Collection
{
$hostIds = Instance::query()
->where('customer_id', $customer->id)
->whereIn('status', ['active', 'provisioning', 'cancellation_scheduled'])
->whereNotNull('host_id')
->pluck('host_id')->unique();
if ($hostIds->isEmpty()) {
return collect();
}
return self::query()
->where('state', 'scheduled')
->where('ends_at', '>=', now())
->where('starts_at', '<=', now()->addHours(self::DISPLAY_HOURS))
->whereHas('hosts', fn (Builder $q) => $q->whereIn('hosts.id', $hostIds))
->orderBy('starts_at')
->get();
}
}