131 lines
4.0 KiB
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();
|
|
}
|
|
}
|