diff --git a/app/Console/Commands/RepairPlaceholderHostingOrdersCommand.php b/app/Console/Commands/RepairPlaceholderHostingOrdersCommand.php new file mode 100644 index 0000000..e8ecee0 --- /dev/null +++ b/app/Console/Commands/RepairPlaceholderHostingOrdersCommand.php @@ -0,0 +1,130 @@ +with(['hostingAccount.sites']) + ->where(function ($q) { + $q->where('domain_name', 'pending-setup.local') + ->orWhereNull('domain_name') + ->orWhere('domain_name', ''); + }) + ->whereIn('status', [ + CustomerHostingOrder::STATUS_ACTIVE, + CustomerHostingOrder::STATUS_SUSPENDED, + CustomerHostingOrder::STATUS_PROVISIONING, + ]) + ->orderBy('id') + ->get(); + + if ($orders->isEmpty()) { + $this->info('No placeholder hosting orders found.'); + + return self::SUCCESS; + } + + $changed = 0; + + foreach ($orders as $order) { + $account = $order->hostingAccount; + if (! $account && $order->user_id) { + $account = HostingAccount::query() + ->with('sites') + ->where('user_id', $order->user_id) + ->when($order->hosting_product_id, fn ($q) => $q->where('hosting_product_id', $order->hosting_product_id)) + ->latest('id') + ->first(); + } + + $realDomain = null; + if ($account) { + if (! $account->relationLoaded('sites')) { + $account->load('sites'); + } + $realDomain = $account->linkedDomainLabel(); + if (PlaceholderPrimaryDomainService::isPlaceholderDomain($realDomain)) { + $realDomain = null; + } + } + + $this->line(sprintf( + '[order #%d] user=%s domain=%s account=%s → %s', + $order->id, + $order->user_id ?: '—', + $order->domain_name ?: '(empty)', + $account?->id ?: 'none', + $realDomain ?: '(no real domain yet)' + )); + + if ($this->option('dry-run')) { + continue; + } + + if ($account && $this->option('delete-duplicates') && (int) $order->hosting_account_id !== (int) $account->id) { + // Separate Active order + separate HostingAccount = duplicate UX. + $order->update([ + 'hosting_account_id' => $order->hosting_account_id ?: $account->id, + 'domain_name' => $realDomain ?: $order->domain_name, + 'server_username' => $order->server_username ?: $account->username, + 'status' => CustomerHostingOrder::STATUS_CANCELLED, + 'cancelled_at' => now(), + 'approval_notes' => trim(($order->approval_notes ? $order->approval_notes."\n" : ''). + 'Auto-cancelled: superseded by Hosting account #'.$account->id), + ]); + $order->delete(); + $this->info(' soft-deleted duplicate order'); + $changed++; + continue; + } + + $updates = []; + if ($account && ! $order->hosting_account_id) { + $updates['hosting_account_id'] = $account->id; + } + if ($realDomain) { + $updates['domain_name'] = $realDomain; + } + if ($account && ! $order->server_username) { + $updates['server_username'] = $account->username; + } + if ($account?->node?->ip_address && ! $order->server_ip) { + $account->loadMissing('node'); + $updates['server_ip'] = $account->node?->ip_address; + } + + if ($updates !== []) { + $order->update($updates); + $this->info(' updated: '.json_encode($updates)); + $changed++; + } + } + + if ($this->option('dry-run')) { + $this->warn('Dry run: no changes applied.'); + } else { + $this->info("Updated {$changed} order(s)."); + } + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Hosting/HostingProductController.php b/app/Http/Controllers/Hosting/HostingProductController.php index 01f24c2..0c328db 100644 --- a/app/Http/Controllers/Hosting/HostingProductController.php +++ b/app/Http/Controllers/Hosting/HostingProductController.php @@ -54,11 +54,13 @@ class HostingProductController extends Controller { $user = $request->user(); - // New Hosting Orders + // In-flight orders only — Active accounts belong under Hosting Accounts + // (legacy Sales Centre left Active pending-setup.local orders that duplicated accounts). $orders = CustomerHostingOrder::query() ->forUser($user->id) + ->inFlight() ->whereHas('product', fn ($q) => $q->ofType($type)) - ->with(['product', 'domain', 'hostingAccount', 'hostingNode']) + ->with(['product', 'domain', 'hostingAccount.sites', 'hostingNode']) ->latest() ->get(); @@ -139,11 +141,12 @@ class HostingProductController extends Controller $user = $request->user(); $serverOrders = app(ServerOrderService::class); - // New Hosting Orders + // In-flight orders only — Active accounts belong under Hosting Accounts $orders = CustomerHostingOrder::query() ->forUser($user->id) + ->inFlight() ->whereHas('product', fn ($q) => $q->ofType($type)) - ->with(['product', 'vpsInstance']) + ->with(['product', 'vpsInstance', 'hostingAccount.sites']) ->latest() ->get(); diff --git a/app/Models/CustomerHostingOrder.php b/app/Models/CustomerHostingOrder.php index 2709d4d..a24fdf3 100644 --- a/app/Models/CustomerHostingOrder.php +++ b/app/Models/CustomerHostingOrder.php @@ -146,6 +146,53 @@ class CustomerHostingOrder extends Model return $query->where('user_id', $userId); } + /** Orders still awaiting payment, approval, or provisioning — not live accounts. */ + public function scopeInFlight($query) + { + return $query->whereIn('status', [ + self::STATUS_PENDING_PAYMENT, + self::STATUS_PENDING_APPROVAL, + self::STATUS_APPROVED, + self::STATUS_PROVISIONING, + ]); + } + + public static function isPlaceholderDomainName(?string $domain): bool + { + $domain = strtolower(trim((string) $domain)); + + return $domain === '' || $domain === 'pending-setup.local' || str_ends_with($domain, '.ladill.com'); + } + + /** + * Label for UI lists — never surface pending-setup.local when a real + * hosting account / domain is available. + */ + public function displayDomainLabel(): string + { + if ($this->relationLoaded('hostingAccount') && $this->hostingAccount) { + $account = $this->hostingAccount; + if (! $account->relationLoaded('sites')) { + $account->load('sites'); + } + + $label = $account->linkedDomainLabel(); + if (filled($label) && ! static::isPlaceholderDomainName($label)) { + return $label; + } + } + + if (! static::isPlaceholderDomainName($this->domain_name)) { + return (string) $this->domain_name; + } + + if (filled($this->server_ip)) { + return (string) $this->server_ip; + } + + return 'Domain pending'; + } + public function isPendingApproval(): bool { return $this->status === self::STATUS_PENDING_APPROVAL; diff --git a/app/Models/HostingAccount.php b/app/Models/HostingAccount.php index f99dd31..afa67cb 100644 --- a/app/Models/HostingAccount.php +++ b/app/Models/HostingAccount.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Schema; +use App\Services\Hosting\PlaceholderPrimaryDomainService; class HostingAccount extends Model { @@ -139,7 +140,15 @@ class HostingAccount extends Model public function linkedDomainLabel(): ?string { if ($this->relationLoaded('sites') && $this->sites->isNotEmpty()) { - $domains = $this->sites->pluck('domain')->filter()->unique()->values(); + $domains = $this->sites + ->pluck('domain') + ->filter(fn ($domain) => filled($domain) && ! PlaceholderPrimaryDomainService::isPlaceholderDomain((string) $domain)) + ->unique() + ->values(); + + if ($domains->isEmpty()) { + return null; + } if ($domains->count() === 1) { return (string) $domains->first(); @@ -152,7 +161,11 @@ class HostingAccount extends Model : $preview; } - return filled($this->primary_domain) ? (string) $this->primary_domain : null; + if (filled($this->primary_domain) && ! PlaceholderPrimaryDomainService::isPlaceholderDomain($this->primary_domain)) { + return (string) $this->primary_domain; + } + + return null; } public function databases(): HasMany diff --git a/resources/views/hosting/type.blade.php b/resources/views/hosting/type.blade.php index 93ab4ef..e5e1b8c 100644 --- a/resources/views/hosting/type.blade.php +++ b/resources/views/hosting/type.blade.php @@ -196,7 +196,7 @@
{{ filled($order->domain_name) ? $order->domain_name : '—' }}
+{{ $order->displayDomainLabel() }}
@if ($order->server_ip){{ $order->server_ip }}
@else