'decimal:2', 'approved_at' => 'datetime', 'provisioned_at' => 'datetime', 'expires_at' => 'datetime', 'suspended_at' => 'datetime', 'cancelled_at' => 'datetime', 'meta' => 'array', ]; public function user(): BelongsTo { return $this->belongsTo(User::class)->withDefault(); } public function product(): BelongsTo { return $this->belongsTo(HostingProduct::class, 'hosting_product_id'); } public function domain(): BelongsTo { return $this->belongsTo(Domain::class); } public function approvedBy(): BelongsTo { return $this->belongsTo(User::class, 'approved_by'); } public function hostingNode(): BelongsTo { return $this->belongsTo(HostingNode::class); } public function hostingAccount(): BelongsTo { return $this->belongsTo(HostingAccount::class); } public function vpsInstance(): BelongsTo { return $this->belongsTo(VpsInstance::class); } public function provisioningQueue(): HasOne { return $this->hasOne(ProvisioningQueueItem::class); } public function serverAgent(): HasOne { return $this->hasOne(ServerAgent::class); } public function serverTasks(): HasMany { return $this->hasMany(ServerTask::class); } public function serverSites(): HasMany { return $this->hasMany(ServerSite::class); } public function serverDatabases(): HasMany { return $this->hasMany(ServerDatabase::class); } public function serverFiles(): HasMany { return $this->hasMany(ServerFile::class); } public function scopePendingApproval($query) { return $query->where('status', self::STATUS_PENDING_APPROVAL); } public function scopeActive($query) { return $query->where('status', self::STATUS_ACTIVE); } public function scopeForUser($query, int $userId) { return $query->where('user_id', $userId); } public function isPendingApproval(): bool { return $this->status === self::STATUS_PENDING_APPROVAL; } public function isApproved(): bool { return $this->status === self::STATUS_APPROVED; } public function isActive(): bool { return $this->status === self::STATUS_ACTIVE; } public function isProvisioning(): bool { return $this->status === self::STATUS_PROVISIONING; } public function canBeApproved(): bool { return $this->status === self::STATUS_PENDING_APPROVAL; } public function canBeCancelled(): bool { return in_array($this->status, [ self::STATUS_PENDING_PAYMENT, self::STATUS_PENDING_APPROVAL, self::STATUS_APPROVED, self::STATUS_ACTIVE, self::STATUS_SUSPENDED, ]); } public function approve(User $admin, ?string $notes = null): bool { if (!$this->canBeApproved()) { return false; } $this->update([ 'status' => self::STATUS_APPROVED, 'approved_by' => $admin->id, 'approved_at' => now(), 'approval_notes' => $notes, ]); return true; } public function markAsProvisioning(): void { $this->update(['status' => self::STATUS_PROVISIONING]); } public function markAsActive(array $provisioningDetails = []): void { $this->update(array_merge([ 'status' => self::STATUS_ACTIVE, 'provisioned_at' => now(), ], $provisioningDetails)); } public function markAsFailed(?string $reason = null): void { $meta = $this->meta ?? []; $meta['failure_reason'] = $reason; $meta['failed_at'] = now()->toIso8601String(); $this->update([ 'status' => self::STATUS_FAILED, 'meta' => $meta, ]); } /** * Return a paid order to the admin queue after upstream provisioning could not complete * (e.g. Contabo account balance). Customer still sees a generic pending state. */ public function holdForAdminProvisioning(string $reason, ?int $httpStatus = null): void { $meta = $this->meta ?? []; $meta['provisioning_hold'] = [ 'reason' => $reason, 'http_status' => $httpStatus, 'at' => now()->toIso8601String(), ]; unset($meta['failure_reason'], $meta['failed_at']); $this->update([ 'status' => self::STATUS_PENDING_APPROVAL, 'meta' => $meta, ]); } public function suspend(?string $reason = null): void { $meta = $this->meta ?? []; $meta['suspension_reason'] = $reason; $this->update([ 'status' => self::STATUS_SUSPENDED, 'suspended_at' => now(), 'meta' => $meta, ]); } public function cancel(?string $reason = null): void { $meta = $this->meta ?? []; $meta['cancellation_reason'] = $reason; $this->update([ 'status' => self::STATUS_CANCELLED, 'cancelled_at' => now(), 'meta' => $meta, ]); } public function getCycleLengthInMonths(): int { return match ($this->billing_cycle) { self::CYCLE_MONTHLY => 1, self::CYCLE_QUARTERLY => 3, self::CYCLE_SEMIANNUAL => 6, self::CYCLE_YEARLY => 12, self::CYCLE_BIENNIAL => 24, default => 12, }; } public function calculateExpiryDate(): \Carbon\Carbon { $startDate = $this->provisioned_at ?? now(); return $startDate->copy()->addMonths($this->getCycleLengthInMonths()); } public function customerFacingStatusLabel(): string { return self::customerFacingStatusLabelFor($this->status); } public static function customerFacingStatusLabelFor(?string $status): string { return match ($status) { self::STATUS_PENDING_PAYMENT => 'Pending Payment', self::STATUS_PENDING_APPROVAL, self::STATUS_APPROVED, self::STATUS_PROVISIONING => 'Pending', self::STATUS_ACTIVE => 'Active', self::STATUS_SUSPENDED => 'Suspended', self::STATUS_CANCELLED => 'Cancelled', self::STATUS_EXPIRED => 'Expired', self::STATUS_FAILED => 'Failed', default => 'Pending', }; } public function isCustomerPending(): bool { return in_array($this->status, [ self::STATUS_PENDING_APPROVAL, self::STATUS_APPROVED, self::STATUS_PROVISIONING, ], true); } }