Deploy Ladill Hosting / deploy (push) Successful in 20s
The account overview queried a local mailboxes table that the extracted hosting app does not have; guard those lookups and skip email-addon upsells when mailboxes are managed on Ladill Email. Co-authored-by: Cursor <cursoragent@cursor.com>
357 lines
9.6 KiB
PHP
357 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 = [
|
|
'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();
|
|
}
|
|
}
|