'array', 'installed_software' => 'array', 'health_status' => 'array', 'last_health_check_at' => 'datetime', 'cpu_cores' => 'integer', 'ram_mb' => 'integer', 'disk_gb' => 'integer', 'oversell_ratio' => 'decimal:2', 'allocated_disk_gb' => 'integer', 'used_disk_gb' => 'integer', 'bandwidth_tb' => 'integer', 'max_accounts' => 'integer', 'current_accounts' => 'integer', 'current_load_percent' => 'decimal:2', ]; protected $hidden = [ 'ssh_private_key', ]; public const ROLE_PRIMARY = 'primary'; public const ROLE_EXTENSION = 'extension'; public const TYPE_SHARED = 'shared'; public const TYPE_VPS = 'vps'; public const TYPE_DEDICATED = 'dedicated'; public const SEGMENT_GENERAL = 'general'; public const SEGMENT_STARTER = 'starter'; public const SEGMENT_WORDPRESS = 'wordpress'; public const SEGMENT_HIGH_RESOURCE = 'high_resource'; public const PROVIDER_CONTABO = 'contabo'; public const PROVIDER_LOCAL = 'local'; public const PROVIDER_MANUAL = 'manual'; public const PROVIDER_LEGACY_RC = 'legacy_rc'; public const STATUS_PROVISIONING = 'provisioning'; public const STATUS_ACTIVE = 'active'; public const STATUS_MAINTENANCE = 'maintenance'; public const STATUS_FULL = 'full'; public const STATUS_DECOMMISSIONED = 'decommissioned'; public function primary(): BelongsTo { return $this->belongsTo(self::class, 'primary_hosting_node_id'); } public function extensions(): HasMany { return $this->hasMany(self::class, 'primary_hosting_node_id'); } public function accounts(): HasMany { return $this->hasMany(HostingAccount::class); } public function isPrimary(): bool { return ($this->role ?? self::ROLE_PRIMARY) === self::ROLE_PRIMARY; } public function isExtension(): bool { return $this->role === self::ROLE_EXTENSION; } public function poolRoot(): self { return $this->isExtension() && $this->primary ? $this->primary : $this; } public function isAvailable(): bool { if ($this->status !== self::STATUS_ACTIVE) { return false; } if ($this->max_accounts && $this->current_accounts >= $this->max_accounts) { return false; } return true; } public function supportsSegment(?string $segment): bool { if ($segment === null || $segment === '') { return true; } return in_array($this->segment, [$segment, self::SEGMENT_GENERAL], true); } public function logicalCapacityGb(): float { if (! $this->disk_gb || $this->disk_gb <= 0) { return 0; } $ratio = (float) ($this->oversell_ratio ?: 1); return round($this->disk_gb * max($ratio, 1), 2); } public function logicalCapacityPercent(): float { $logicalCapacity = $this->logicalCapacityGb(); if ($logicalCapacity <= 0) { return 0; } return round(($this->allocated_disk_gb / $logicalCapacity) * 100, 2); } public function physicalDiskPercent(): float { if (! $this->disk_gb || $this->disk_gb <= 0) { return 0; } return round(($this->used_disk_gb / $this->disk_gb) * 100, 2); } public function accountCapacityPercent(): float { if (! $this->max_accounts || $this->max_accounts <= 0) { return 0; } return round(($this->current_accounts / $this->max_accounts) * 100, 2); } public function hasLogicalCapacityFor(int $requestedDiskGb = 0): bool { if ($requestedDiskGb <= 0) { return true; } if (! $this->disk_gb || $this->disk_gb <= 0) { return false; } return ($this->used_disk_gb + $requestedDiskGb) <= $this->disk_gb; } public function availableLogicalDiskGb(): float { if (! $this->disk_gb || $this->disk_gb <= 0) { return 0; } return max($this->disk_gb - $this->used_disk_gb, 0); } public function canProvision(int $requestedDiskGb = 0, ?string $segment = null): bool { if (! $this->isAvailable()) { return false; } if (! $this->supportsSegment($segment)) { return false; } return $this->hasLogicalCapacityFor($requestedDiskGb); } public function incrementAccountCount(): void { $this->increment('current_accounts'); if ($this->max_accounts && $this->current_accounts >= $this->max_accounts) { $this->update(['status' => self::STATUS_FULL]); } } public function decrementAccountCount(): void { $this->decrement('current_accounts'); if ($this->status === self::STATUS_FULL && $this->current_accounts < $this->max_accounts) { $this->update(['status' => self::STATUS_ACTIVE]); } } public function scopeAvailable($query) { return $query->where('status', self::STATUS_ACTIVE) ->whereRaw('current_accounts < COALESCE(max_accounts, 999999)'); } public function scopeOfType($query, string $type) { return $query->where('type', $type); } }