Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s
Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user