suspendNewlyExpired($nodeProvider); $this->terminatePastGracePeriod($provisioning); return self::SUCCESS; } private function suspendNewlyExpired(SharedNodeProvider $nodeProvider): void { $accounts = HostingAccount::query() ->where('status', HostingAccount::STATUS_ACTIVE) ->whereNotNull('expires_at') ->where('expires_at', '<', now()->subDays(self::EXPIRY_SUSPENSION_GRACE_DAYS)) ->with(['node', 'sites', 'user']) ->get(); if ($accounts->isEmpty()) { $this->info('No newly expired accounts to suspend.'); return; } $this->info("Found {$accounts->count()} expired account(s) to suspend."); foreach ($accounts as $account) { try { $nodeProvider->serveExpiredPage($account); $reason = 'Hosting account expired on '.$account->expires_at->format('M d, Y').'. Renew to restore service.'; $account->update([ 'status' => HostingAccount::STATUS_SUSPENDED, 'suspension_reason' => $reason, 'suspended_at' => now(), 'metadata' => array_merge((array) ($account->metadata ?? []), [ 'suspension_origin' => 'expiry', 'expired_at' => $account->expires_at->toIso8601String(), 'expiry_suspension_grace_days' => self::EXPIRY_SUSPENSION_GRACE_DAYS, 'grace_period_ends_at' => $account->gracePeriodEndsAt()->toIso8601String(), ]), ]); if ($account->user) { $account->user->notify(new HostingSuspendedNotification($account->fresh(), $reason)); } Log::info('ProcessExpiredHostingAccounts: Suspended expired account', [ 'account_id' => $account->id, 'username' => $account->username, 'expired_at' => $account->expires_at, ]); $this->line(" Suspended: {$account->username} (expired {$account->expires_at->format('M d, Y')})"); } catch (\Throwable $e) { Log::error('ProcessExpiredHostingAccounts: Failed to suspend expired account', [ 'account_id' => $account->id, 'error' => $e->getMessage(), ]); $this->error(" Failed to suspend {$account->username}: {$e->getMessage()}"); } } } private function terminatePastGracePeriod(HostingProvisioningService $provisioning): void { $graceDeadline = now()->subMonths(HostingAccount::GRACE_PERIOD_MONTHS); $accounts = HostingAccount::query() ->where('status', HostingAccount::STATUS_SUSPENDED) ->whereNotNull('expires_at') ->where('expires_at', '<', $graceDeadline) ->whereJsonContains('metadata->suspension_origin', 'expiry') ->with(['node', 'sites']) ->get(); if ($accounts->isEmpty()) { $this->info('No accounts past grace period to terminate.'); return; } $this->info("Found {$accounts->count()} account(s) past grace period to terminate."); foreach ($accounts as $account) { try { $provisioning->terminateAccount($account); Log::info('ProcessExpiredHostingAccounts: Terminated account past grace period', [ 'account_id' => $account->id, 'username' => $account->username, 'expired_at' => $account->expires_at, ]); $this->line(" Terminated: {$account->username} (expired {$account->expires_at->format('M d, Y')})"); } catch (\Throwable $e) { Log::error('ProcessExpiredHostingAccounts: Failed to terminate account', [ 'account_id' => $account->id, 'error' => $e->getMessage(), ]); $this->error(" Failed to terminate {$account->username}: {$e->getMessage()}"); } } } }