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,330 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
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 HostingAccount extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const TYPE_SHARED = 'shared';
|
||||
public const TYPE_VPS = 'vps';
|
||||
public const TYPE_DEDICATED = 'dedicated';
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_PROVISIONING = 'provisioning';
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
public const STATUS_SUSPENDED = 'suspended';
|
||||
public const STATUS_TERMINATED = 'terminated';
|
||||
public const STATUS_FAILED = 'failed';
|
||||
|
||||
public const RESOURCE_STATUS_ACTIVE = 'active';
|
||||
public const RESOURCE_STATUS_THROTTLED = 'throttled';
|
||||
public const RESOURCE_STATUS_SUSPENDED = 'suspended';
|
||||
|
||||
public const GRACE_PERIOD_MONTHS = 3;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'hosting_product_id',
|
||||
'hosting_node_id',
|
||||
'username',
|
||||
'primary_domain',
|
||||
'type',
|
||||
'status',
|
||||
'provider_account_id',
|
||||
'home_directory',
|
||||
'allocated_disk_gb',
|
||||
'disk_used_bytes',
|
||||
'inode_count',
|
||||
'cpu_usage_percent',
|
||||
'memory_used_mb',
|
||||
'process_count',
|
||||
'io_usage_mb',
|
||||
'last_usage_sync_at',
|
||||
'bandwidth_used_bytes',
|
||||
'bandwidth_reset_at',
|
||||
'php_version',
|
||||
'cpu_limit_percent',
|
||||
'memory_limit_mb',
|
||||
'process_limit',
|
||||
'io_limit_mb',
|
||||
'inode_limit',
|
||||
'resource_status',
|
||||
'uploads_restricted',
|
||||
'is_flagged',
|
||||
'warning_count',
|
||||
'cpu_breach_streak',
|
||||
'memory_breach_streak',
|
||||
'process_breach_streak',
|
||||
'io_breach_streak',
|
||||
'inode_breach_streak',
|
||||
'last_warning_at',
|
||||
'last_resource_breach_at',
|
||||
'throttled_at',
|
||||
'features_enabled',
|
||||
'resource_limits',
|
||||
'suspension_reason',
|
||||
'suspended_at',
|
||||
'provisioned_at',
|
||||
'expires_at',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'features_enabled' => 'array',
|
||||
'resource_limits' => 'array',
|
||||
'metadata' => 'array',
|
||||
'allocated_disk_gb' => 'integer',
|
||||
'inode_count' => 'integer',
|
||||
'cpu_usage_percent' => 'decimal:2',
|
||||
'memory_used_mb' => 'integer',
|
||||
'process_count' => 'integer',
|
||||
'io_usage_mb' => 'decimal:2',
|
||||
'cpu_limit_percent' => 'integer',
|
||||
'memory_limit_mb' => 'integer',
|
||||
'process_limit' => 'integer',
|
||||
'io_limit_mb' => 'integer',
|
||||
'inode_limit' => 'integer',
|
||||
'uploads_restricted' => 'boolean',
|
||||
'is_flagged' => 'boolean',
|
||||
'warning_count' => 'integer',
|
||||
'cpu_breach_streak' => 'integer',
|
||||
'memory_breach_streak' => 'integer',
|
||||
'process_breach_streak' => 'integer',
|
||||
'io_breach_streak' => 'integer',
|
||||
'inode_breach_streak' => 'integer',
|
||||
'last_usage_sync_at' => 'datetime',
|
||||
'last_warning_at' => 'datetime',
|
||||
'last_resource_breach_at' => 'datetime',
|
||||
'throttled_at' => 'datetime',
|
||||
'bandwidth_reset_at' => 'datetime',
|
||||
'suspended_at' => 'datetime',
|
||||
'provisioned_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(HostingProduct::class, 'hosting_product_id');
|
||||
}
|
||||
|
||||
public function node(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(HostingNode::class, 'hosting_node_id');
|
||||
}
|
||||
|
||||
public function sites(): HasMany
|
||||
{
|
||||
return $this->hasMany(HostedSite::class);
|
||||
}
|
||||
|
||||
public function databases(): HasMany
|
||||
{
|
||||
return $this->hasMany(HostedDatabase::class);
|
||||
}
|
||||
|
||||
public function teamMembers(): HasMany
|
||||
{
|
||||
return $this->hasMany(HostingAccountMember::class);
|
||||
}
|
||||
|
||||
public function developers(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'hosting_account_members')
|
||||
->withPivot(['role', 'invited_by_user_id', 'invited_at'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function alerts(): HasMany
|
||||
{
|
||||
return $this->hasMany(HostingAccountAlert::class);
|
||||
}
|
||||
|
||||
public function mailboxes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Mailbox::class, 'hosting_account_id');
|
||||
}
|
||||
|
||||
public function billingInvoices(): HasMany
|
||||
{
|
||||
return $this->hasMany(HostingBillingInvoice::class, 'hosting_account_id');
|
||||
}
|
||||
|
||||
public function planChanges(): HasMany
|
||||
{
|
||||
return $this->hasMany(HostingPlanChange::class, 'hosting_account_id');
|
||||
}
|
||||
|
||||
public function latestOrder(): HasOne
|
||||
{
|
||||
return $this->hasOne(CustomerHostingOrder::class, 'hosting_account_id')->latestOfMany();
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === 'active';
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
public function isThrottled(): bool
|
||||
{
|
||||
return $this->resource_status === self::RESOURCE_STATUS_THROTTLED;
|
||||
}
|
||||
|
||||
public function uploadsAreRestricted(): bool
|
||||
{
|
||||
return (bool) $this->uploads_restricted || (
|
||||
$this->inode_limit > 0 && $this->inode_count >= $this->inode_limit
|
||||
);
|
||||
}
|
||||
|
||||
public function assignedDurationMonths(): ?int
|
||||
{
|
||||
$months = data_get($this->metadata, 'assigned_duration_months');
|
||||
|
||||
return is_numeric($months) && (int) $months > 0 ? (int) $months : null;
|
||||
}
|
||||
|
||||
public function canBeRenewed(): bool
|
||||
{
|
||||
return $this->assignedDurationMonths() !== null;
|
||||
}
|
||||
|
||||
public function requestedDiskGb(): int
|
||||
{
|
||||
if ($this->allocated_disk_gb > 0) {
|
||||
return (int) $this->allocated_disk_gb;
|
||||
}
|
||||
|
||||
return (int) ($this->product?->disk_gb
|
||||
?? data_get($this->resource_limits, 'disk_gb')
|
||||
?? 0);
|
||||
}
|
||||
|
||||
public function diskLimitBytes(): int
|
||||
{
|
||||
return max($this->requestedDiskGb(), 0) * 1073741824;
|
||||
}
|
||||
|
||||
public function diskUsagePercent(): float
|
||||
{
|
||||
$limit = $this->diskLimitBytes();
|
||||
|
||||
if ($limit <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round(($this->disk_used_bytes / $limit) * 100, 2);
|
||||
}
|
||||
|
||||
public function inodeUsagePercent(): float
|
||||
{
|
||||
if (! $this->inode_limit || $this->inode_limit <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round(($this->inode_count / $this->inode_limit) * 100, 2);
|
||||
}
|
||||
|
||||
public function includedEmailAccounts(): ?int
|
||||
{
|
||||
$limit = $this->product?->max_email_accounts;
|
||||
|
||||
if ($limit === null) {
|
||||
$limit = data_get($this->resource_limits, 'max_email_accounts');
|
||||
}
|
||||
|
||||
return $limit === null ? null : (int) $limit;
|
||||
}
|
||||
|
||||
public function freeEmailAllowance(): ?int
|
||||
{
|
||||
$included = $this->includedEmailAccounts();
|
||||
|
||||
if ($included === null || $included < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return max($included, 0);
|
||||
}
|
||||
|
||||
public function paidMailboxCount(): int
|
||||
{
|
||||
return $this->relationLoaded('mailboxes')
|
||||
? $this->mailboxes->where('is_paid', true)->count()
|
||||
: $this->mailboxes()->where('is_paid', true)->count();
|
||||
}
|
||||
|
||||
public function renew(?int $durationMonths = null, array $metadata = []): void
|
||||
{
|
||||
$months = $durationMonths ?? $this->assignedDurationMonths();
|
||||
|
||||
if (! is_int($months) || $months < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$baseDate = $this->expires_at instanceof CarbonInterface && $this->expires_at->isFuture()
|
||||
? $this->expires_at->copy()
|
||||
: now();
|
||||
|
||||
$currentMetadata = (array) ($this->metadata ?? []);
|
||||
|
||||
$mergedMetadata = array_merge($currentMetadata, $metadata, [
|
||||
'assigned_duration_months' => $months,
|
||||
]);
|
||||
unset($mergedMetadata['expiry_alerts_sent']);
|
||||
|
||||
$this->update([
|
||||
'expires_at' => $baseDate->addMonthsNoOverflow($months),
|
||||
'metadata' => $mergedMetadata,
|
||||
]);
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at instanceof CarbonInterface && $this->expires_at->isPast();
|
||||
}
|
||||
|
||||
public function isInGracePeriod(): bool
|
||||
{
|
||||
if (! $this->isExpired()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->gracePeriodEndsAt()->isFuture();
|
||||
}
|
||||
|
||||
public function gracePeriodEndsAt(): ?CarbonInterface
|
||||
{
|
||||
if (! $this->expires_at instanceof CarbonInterface) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->expires_at->copy()->addMonths(self::GRACE_PERIOD_MONTHS);
|
||||
}
|
||||
|
||||
public function isPastGracePeriod(): bool
|
||||
{
|
||||
$endsAt = $this->gracePeriodEndsAt();
|
||||
|
||||
return $endsAt !== null && $endsAt->isPast();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user