Hide fulfilled pending-setup orders from the customer hosting list.
Deploy Ladill Hosting / deploy (push) Successful in 45s
Deploy Ladill Hosting / deploy (push) Successful in 45s
Sales Centre left Active CustomerHostingOrder rows with pending-setup.local alongside real HostingAccounts, so customers saw two hostings. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\CustomerHostingOrder;
|
||||||
|
use App\Models\HostingAccount;
|
||||||
|
use App\Services\Hosting\PlaceholderPrimaryDomainService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy Sales Centre created Active CustomerHostingOrder rows with
|
||||||
|
* pending-setup.local while also assigning a real HostingAccount — customers
|
||||||
|
* then saw two hostings. Link/update those orders (or soft-delete duplicates).
|
||||||
|
*/
|
||||||
|
class RepairPlaceholderHostingOrdersCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'hosting:repair-placeholder-orders
|
||||||
|
{--dry-run : Show changes without writing}
|
||||||
|
{--delete-duplicates : Soft-delete Active placeholder orders that already have a HostingAccount for the same user}';
|
||||||
|
|
||||||
|
protected $description = 'Repair CustomerHostingOrders stuck on pending-setup.local after Hosting app assignment.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$orders = CustomerHostingOrder::query()
|
||||||
|
->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,11 +54,13 @@ class HostingProductController extends Controller
|
|||||||
{
|
{
|
||||||
$user = $request->user();
|
$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()
|
$orders = CustomerHostingOrder::query()
|
||||||
->forUser($user->id)
|
->forUser($user->id)
|
||||||
|
->inFlight()
|
||||||
->whereHas('product', fn ($q) => $q->ofType($type))
|
->whereHas('product', fn ($q) => $q->ofType($type))
|
||||||
->with(['product', 'domain', 'hostingAccount', 'hostingNode'])
|
->with(['product', 'domain', 'hostingAccount.sites', 'hostingNode'])
|
||||||
->latest()
|
->latest()
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
@@ -139,11 +141,12 @@ class HostingProductController extends Controller
|
|||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$serverOrders = app(ServerOrderService::class);
|
$serverOrders = app(ServerOrderService::class);
|
||||||
|
|
||||||
// New Hosting Orders
|
// In-flight orders only — Active accounts belong under Hosting Accounts
|
||||||
$orders = CustomerHostingOrder::query()
|
$orders = CustomerHostingOrder::query()
|
||||||
->forUser($user->id)
|
->forUser($user->id)
|
||||||
|
->inFlight()
|
||||||
->whereHas('product', fn ($q) => $q->ofType($type))
|
->whereHas('product', fn ($q) => $q->ofType($type))
|
||||||
->with(['product', 'vpsInstance'])
|
->with(['product', 'vpsInstance', 'hostingAccount.sites'])
|
||||||
->latest()
|
->latest()
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,53 @@ class CustomerHostingOrder extends Model
|
|||||||
return $query->where('user_id', $userId);
|
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
|
public function isPendingApproval(): bool
|
||||||
{
|
{
|
||||||
return $this->status === self::STATUS_PENDING_APPROVAL;
|
return $this->status === self::STATUS_PENDING_APPROVAL;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use App\Services\Hosting\PlaceholderPrimaryDomainService;
|
||||||
|
|
||||||
class HostingAccount extends Model
|
class HostingAccount extends Model
|
||||||
{
|
{
|
||||||
@@ -139,7 +140,15 @@ class HostingAccount extends Model
|
|||||||
public function linkedDomainLabel(): ?string
|
public function linkedDomainLabel(): ?string
|
||||||
{
|
{
|
||||||
if ($this->relationLoaded('sites') && $this->sites->isNotEmpty()) {
|
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) {
|
if ($domains->count() === 1) {
|
||||||
return (string) $domains->first();
|
return (string) $domains->first();
|
||||||
@@ -152,7 +161,11 @@ class HostingAccount extends Model
|
|||||||
: $preview;
|
: $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
|
public function databases(): HasMany
|
||||||
|
|||||||
@@ -196,7 +196,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-medium text-gray-900">{{ filled($order->domain_name) ? $order->domain_name : '—' }}</p>
|
<p class="font-medium text-gray-900">{{ $order->displayDomainLabel() }}</p>
|
||||||
@if ($order->server_ip)
|
@if ($order->server_ip)
|
||||||
<p class="text-xs text-gray-500">{{ $order->server_ip }}</p>
|
<p class="text-xs text-gray-500">{{ $order->server_ip }}</p>
|
||||||
@else
|
@else
|
||||||
|
|||||||
Reference in New Issue
Block a user