*/ 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(); } }