$input * @return array */ public function assign(User $owner, array $input): array { $product = HostingProduct::query()->findOrFail((int) $input['hosting_product_id']); if (! $product->is_active) { throw ValidationException::withMessages([ 'hosting_product_id' => 'Selected hosting product is not active.', ]); } $username = isset($input['username']) && $input['username'] !== '' ? (string) $input['username'] : $this->generateUsername($owner); if (HostingAccount::query()->where('username', $username)->exists()) { throw ValidationException::withMessages([ 'username' => 'This username is already taken.', ]); } $primaryDomain = isset($input['primary_domain']) && $input['primary_domain'] !== '' ? (string) $input['primary_domain'] : null; $accountType = match ($product->type) { 'single_domain', 'multi_domain', 'wordpress' => HostingAccount::TYPE_SHARED, 'vps' => HostingAccount::TYPE_VPS, 'dedicated' => HostingAccount::TYPE_DEDICATED, 'email_standalone' => HostingAccount::TYPE_SHARED, default => HostingAccount::TYPE_SHARED, }; $node = null; $nodeId = null; if ($accountType === HostingAccount::TYPE_SHARED) { $node = $this->nodeCapacity->findAvailableNodeForProduct($product); $nodeId = $node?->id; } $resourceLimits = $this->resourcePolicy->defaultLimitsForProduct($product); $durationMonths = (int) $input['duration_months']; $hostingAccount = HostingAccount::create([ 'user_id' => $owner->id, 'hosting_product_id' => $product->id, 'hosting_node_id' => $nodeId, 'username' => $username, 'primary_domain' => $primaryDomain, 'type' => $accountType, 'status' => HostingAccount::STATUS_PENDING, 'allocated_disk_gb' => (int) ($product->disk_gb ?? 0), 'cpu_limit_percent' => $resourceLimits['cpu_limit_percent'], 'memory_limit_mb' => $resourceLimits['memory_limit_mb'], 'process_limit' => $resourceLimits['process_limit'], 'io_limit_mb' => $resourceLimits['io_limit_mb'], 'inode_limit' => $resourceLimits['inode_limit'], 'resource_status' => HostingAccount::RESOURCE_STATUS_ACTIVE, 'expires_at' => $this->expiresAtFromDuration($durationMonths), 'resource_limits' => array_merge([ 'disk_gb' => (int) ($product->disk_gb ?? 0), 'max_domains' => $product->max_domains, 'max_databases' => $product->max_databases, ], $resourceLimits), 'metadata' => [ 'assigned_by_admin' => $input['assigned_by_admin'] ?? null, 'assigned_at' => now()->toISOString(), 'assigned_duration_months' => $durationMonths, 'notes' => $input['notes'] ?? null, ], ]); if ($node) { ProvisionHostingAccountJob::dispatch($hostingAccount->id); return [ 'account' => $this->serializeAccount($hostingAccount->fresh(['product'])), 'provisioning' => true, 'message' => "Hosting product '{$product->name}' assigned. Provisioning is in progress.", ]; } $hostingAccount->update([ 'status' => HostingAccount::STATUS_ACTIVE, 'provisioned_at' => now(), ]); return [ 'account' => $this->serializeAccount($hostingAccount->fresh(['product'])), 'provisioning' => false, 'message' => "Hosting product '{$product->name}' assigned.", ]; } public function updateDuration(HostingAccount $account, int $durationMonths, ?int $adminId = null): array { $metadata = (array) ($account->metadata ?? []); $metadata['assigned_duration_months'] = $durationMonths; $metadata['duration_updated_by_admin'] = $adminId; $metadata['duration_updated_at'] = now()->toISOString(); $account->update([ 'expires_at' => $this->expiresAtFromDuration($durationMonths), 'metadata' => $metadata, ]); return $this->serializeAccount($account->fresh(['product'])); } public function renew(HostingAccount $account, ?int $adminId = null): array { if (! $account->canBeRenewed()) { throw ValidationException::withMessages([ 'account' => 'This hosting account does not have a configured renewal duration.', ]); } $months = $account->assignedDurationMonths(); $account->renew($months, [ 'renewed_by_admin' => $adminId, 'renewed_at' => now()->toISOString(), ]); return $this->serializeAccount($account->fresh(['product'])); } public function unsuspend(HostingAccount $account, HostingResourcePolicyService $resourcePolicy): void { if ( $account->status !== HostingAccount::STATUS_SUSPENDED && $account->resource_status !== HostingAccount::RESOURCE_STATUS_SUSPENDED ) { throw ValidationException::withMessages([ 'account' => 'This hosting account is not suspended.', ]); } $resourcePolicy->unsuspendAccount($account, 'Manual unsuspension by admin.'); } /** * @return array */ public function serializeAccount(HostingAccount $account): array { $product = $account->product; return [ 'id' => $account->id, 'username' => $account->username, 'primary_domain' => $account->primary_domain, 'type' => $account->type, 'status' => $account->status, 'resource_status' => $account->resource_status, 'expires_at' => $account->expires_at?->toIso8601String(), 'provisioned_at' => $account->provisioned_at?->toIso8601String(), 'metadata' => (array) ($account->metadata ?? []), 'product' => $product ? [ 'id' => $product->id, 'name' => $product->name, 'type' => $product->type, 'slug' => $product->slug, ] : null, ]; } /** * @return list> */ public function listProducts(): array { return HostingProduct::query() ->where('is_active', true) ->orderBy('sort_order') ->orderBy('name') ->get() ->map(fn (HostingProduct $product) => [ 'id' => $product->id, 'name' => $product->name, 'type' => $product->type, 'slug' => $product->slug, 'display_currency' => $product->display_currency, 'display_price_monthly' => $product->display_price_monthly, ]) ->values() ->all(); } private function generateUsername(User $user): string { $baseUsername = strtolower(str_replace([' ', '.', '_'], '', $user->name)); $baseUsername = preg_replace('/[^a-z0-9]/', '', $baseUsername); if (strlen($baseUsername) < 3) { $baseUsername = strtolower(substr($user->email, 0, str_contains($user->email, '@') ? strpos($user->email, '@') : strlen($user->email))); $baseUsername = preg_replace('/[^a-z0-9]/', '', $baseUsername); } if (strlen($baseUsername) < 3) { $baseUsername = 'user'.$user->id; } $username = substr($baseUsername, 0, 16); $counter = 1; $originalUsername = $username; while (HostingAccount::query()->where('username', $username)->exists()) { $suffix = (string) $counter; $maxBaseLength = 16 - strlen($suffix); $username = substr($originalUsername, 0, $maxBaseLength).$suffix; $counter++; } return $username; } private function expiresAtFromDuration(int $durationMonths): CarbonInterface { return now()->addMonthsNoOverflow($durationMonths); } }