Extract Ladill Servers as standalone app at servers.ladill.com.

VPS and dedicated server ordering, managed panels, SSO, billing, and launcher integration — forked from hosting infrastructure with server-focused routes and dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 19:18:30 +00:00
co-authored by Cursor
commit b6c8ac343f
382 changed files with 67315 additions and 0 deletions
+317
View File
@@ -0,0 +1,317 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class CustomerHostingOrder extends Model
{
use HasFactory, SoftDeletes;
public const STATUS_PENDING_PAYMENT = 'pending_payment';
public const STATUS_PENDING_APPROVAL = 'pending_approval';
public const STATUS_APPROVED = 'approved';
public const STATUS_PROVISIONING = 'provisioning';
public const STATUS_ACTIVE = 'active';
public const STATUS_SUSPENDED = 'suspended';
public const STATUS_CANCELLED = 'cancelled';
public const STATUS_EXPIRED = 'expired';
public const STATUS_FAILED = 'failed';
public const CYCLE_MONTHLY = 'monthly';
public const CYCLE_QUARTERLY = 'quarterly';
public const CYCLE_SEMIANNUAL = 'semiannual';
public const CYCLE_YEARLY = 'yearly';
public const CYCLE_BIENNIAL = 'biennial';
protected $fillable = [
'user_id',
'guest_email',
'hosting_product_id',
'domain_id',
'domain_name',
'billing_cycle',
'amount_paid',
'currency',
'payment_reference',
'status',
'approved_by',
'approved_at',
'approval_notes',
'hosting_node_id',
'hosting_account_id',
'vps_instance_id',
'server_username',
'server_ip',
'control_panel_url',
'provisioned_at',
'expires_at',
'suspended_at',
'cancelled_at',
'meta',
];
protected $casts = [
'amount_paid' => '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);
}
}