Files
ladill-hosting/app/Models/HostingAccount.php
T
isaaccladandCursor e39a47fa1f
Deploy Ladill Hosting / deploy (push) Successful in 25s
Land on account overview and import hosted sites.
Route launcher and home hub entry to the account overview instead of the
control panel, label the sidebar app launcher, and extend hosting:import
to sync hosted_sites with platform_id matching for existing accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 17:13:46 +00:00

358 lines
9.6 KiB
PHP

<?php
namespace App\Models;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Collection;
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;
use Illuminate\Support\Facades\Schema;
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 = [
'platform_id',
'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');
}
/** The extracted hosting app does not store mailboxes locally — they live on Ladill Email. */
public static function tracksLocalMailboxes(): bool
{
static $hasTable = null;
return $hasTable ??= Schema::hasTable('mailboxes');
}
/** @return Collection<int, Mailbox> */
public function loadedMailboxes(): Collection
{
if (! static::tracksLocalMailboxes()) {
return new Collection;
}
if (! $this->relationLoaded('mailboxes')) {
$this->load('mailboxes');
}
return $this->mailboxes;
}
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
{
if (! static::tracksLocalMailboxes()) {
return 0;
}
return $this->loadedMailboxes()->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();
}
}