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>
309 lines
8.2 KiB
PHP
309 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Services\Hosting\HostingPricingService;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
class HostingProduct extends Model
|
|
{
|
|
use HasFactory, SoftDeletes;
|
|
|
|
public const CATEGORY_SHARED = 'shared';
|
|
public const CATEGORY_VPS = 'vps';
|
|
public const CATEGORY_DEDICATED = 'dedicated';
|
|
public const CATEGORY_EMAIL = 'email';
|
|
|
|
public const TYPE_SINGLE_DOMAIN = 'single_domain';
|
|
public const TYPE_MULTI_DOMAIN = 'multi_domain';
|
|
public const TYPE_WORDPRESS = 'wordpress';
|
|
public const TYPE_VPS = 'vps';
|
|
public const TYPE_DEDICATED = 'dedicated';
|
|
public const TYPE_EMAIL_STANDALONE = 'email_standalone';
|
|
|
|
protected $fillable = [
|
|
'name',
|
|
'slug',
|
|
'description',
|
|
'category',
|
|
'type',
|
|
'price_monthly',
|
|
'price_quarterly',
|
|
'price_yearly',
|
|
'price_biennial',
|
|
'setup_fee',
|
|
'currency',
|
|
'disk_gb',
|
|
'bandwidth_gb',
|
|
'cpu_cores',
|
|
'ram_mb',
|
|
'max_domains',
|
|
'max_databases',
|
|
'max_email_accounts',
|
|
'max_ftp_accounts',
|
|
'ssl_included',
|
|
'backups_included',
|
|
'backup_retention_days',
|
|
'features',
|
|
'php_versions',
|
|
'contabo_product_id',
|
|
'contabo_region',
|
|
'is_active',
|
|
'is_featured',
|
|
'is_visible',
|
|
'sort_order',
|
|
];
|
|
|
|
protected $attributes = [
|
|
'currency' => 'GHS',
|
|
];
|
|
|
|
protected $casts = [
|
|
'price_monthly' => 'decimal:2',
|
|
'price_quarterly' => 'decimal:2',
|
|
'price_yearly' => 'decimal:2',
|
|
'price_biennial' => 'decimal:2',
|
|
'setup_fee' => 'decimal:2',
|
|
'disk_gb' => 'integer',
|
|
'bandwidth_gb' => 'integer',
|
|
'cpu_cores' => 'integer',
|
|
'ram_mb' => 'integer',
|
|
'max_domains' => 'integer',
|
|
'max_databases' => 'integer',
|
|
'max_email_accounts' => 'integer',
|
|
'max_ftp_accounts' => 'integer',
|
|
'ssl_included' => 'boolean',
|
|
'backups_included' => 'boolean',
|
|
'backup_retention_days' => 'integer',
|
|
'features' => 'array',
|
|
'php_versions' => 'array',
|
|
'is_active' => 'boolean',
|
|
'is_featured' => 'boolean',
|
|
'is_visible' => 'boolean',
|
|
'sort_order' => 'integer',
|
|
];
|
|
|
|
public function orders(): HasMany
|
|
{
|
|
return $this->hasMany(CustomerHostingOrder::class);
|
|
}
|
|
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
public function scopeVisible($query)
|
|
{
|
|
return $query->where('is_visible', true);
|
|
}
|
|
|
|
public function scopeOfCategory($query, string $category)
|
|
{
|
|
return $query->where('category', $category);
|
|
}
|
|
|
|
public function scopeOfType($query, string $type)
|
|
{
|
|
return $query->where('type', $type);
|
|
}
|
|
|
|
public function scopeSharedHosting($query)
|
|
{
|
|
return $query->where('category', self::CATEGORY_SHARED);
|
|
}
|
|
|
|
public function scopeServers($query)
|
|
{
|
|
return $query->whereIn('category', [self::CATEGORY_VPS, self::CATEGORY_DEDICATED]);
|
|
}
|
|
|
|
public function scopeOrdered($query)
|
|
{
|
|
return $query->orderBy('sort_order')->orderBy('price_monthly');
|
|
}
|
|
|
|
public function isSharedHosting(): bool
|
|
{
|
|
return $this->category === self::CATEGORY_SHARED;
|
|
}
|
|
|
|
public function isVps(): bool
|
|
{
|
|
return $this->category === self::CATEGORY_VPS;
|
|
}
|
|
|
|
public function isDedicated(): bool
|
|
{
|
|
return $this->category === self::CATEGORY_DEDICATED;
|
|
}
|
|
|
|
public function requiresContabo(): bool
|
|
{
|
|
return in_array($this->category, [self::CATEGORY_VPS, self::CATEGORY_DEDICATED]);
|
|
}
|
|
|
|
public function isActive(): bool
|
|
{
|
|
return (bool) $this->is_active;
|
|
}
|
|
|
|
public function isVisible(): bool
|
|
{
|
|
return (bool) $this->is_visible;
|
|
}
|
|
|
|
public function getPriceForCycle(string $cycle): ?float
|
|
{
|
|
// Use dynamic pricing for VPS/Dedicated
|
|
if ($this->requiresContabo()) {
|
|
// Contabo VPS does not offer quarterly billing
|
|
if ($this->isVps() && $cycle === 'quarterly') {
|
|
return null;
|
|
}
|
|
$pricing = $this->getDynamicPricing();
|
|
return match ($cycle) {
|
|
'monthly' => $pricing['monthly'],
|
|
'quarterly' => $pricing['quarterly'],
|
|
'semiannual' => $pricing['semiannual'],
|
|
'yearly' => $pricing['yearly'],
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
return match ($cycle) {
|
|
'monthly' => (float) $this->price_monthly,
|
|
'quarterly' => (float) $this->price_quarterly,
|
|
'semiannual' => null,
|
|
'yearly' => (float) $this->price_yearly,
|
|
'biennial' => (float) $this->price_biennial,
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
public function getDynamicPricing(): array
|
|
{
|
|
$pricingService = app(HostingPricingService::class);
|
|
return $pricingService->getDynamicPriceForProduct($this);
|
|
}
|
|
|
|
public function getPricingBreakdown(): array
|
|
{
|
|
$pricingService = app(HostingPricingService::class);
|
|
return $pricingService->getPricingBreakdown($this);
|
|
}
|
|
|
|
public function getDisplayPriceMonthlyAttribute(): float
|
|
{
|
|
if ($this->requiresContabo()) {
|
|
return $this->getDynamicPricing()['monthly'];
|
|
}
|
|
return (float) $this->price_monthly;
|
|
}
|
|
|
|
public function getDisplayPriceYearlyAttribute(): float
|
|
{
|
|
if ($this->requiresContabo()) {
|
|
return $this->getDynamicPricing()['yearly'];
|
|
}
|
|
return (float) $this->price_yearly;
|
|
}
|
|
|
|
public function getDisplayCurrencyAttribute(): string
|
|
{
|
|
return 'GHS';
|
|
}
|
|
|
|
public function getYearlyDiscount(): ?float
|
|
{
|
|
$monthlyPrice = $this->display_price_monthly;
|
|
$yearlyPrice = $this->display_price_yearly;
|
|
|
|
if (!$yearlyPrice || !$monthlyPrice) {
|
|
return null;
|
|
}
|
|
|
|
$monthlyTotal = $monthlyPrice * 12;
|
|
return round((($monthlyTotal - $yearlyPrice) / $monthlyTotal) * 100, 1);
|
|
}
|
|
|
|
public function catalogSummary(): ?string
|
|
{
|
|
$parts = [];
|
|
|
|
if ($this->disk_gb !== null) {
|
|
$parts[] = $this->formatDiskSize();
|
|
}
|
|
|
|
$domainLimit = $this->formatDomainLimit();
|
|
if ($domainLimit !== null) {
|
|
$parts[] = $domainLimit;
|
|
}
|
|
|
|
return $parts !== [] ? implode(' · ', $parts) : null;
|
|
}
|
|
|
|
public function formatDiskSize(): string
|
|
{
|
|
if (!$this->disk_gb) {
|
|
return 'Unlimited';
|
|
}
|
|
if ($this->disk_gb >= 1000) {
|
|
return round($this->disk_gb / 1000, 1) . ' TB';
|
|
}
|
|
return $this->disk_gb . ' GB';
|
|
}
|
|
|
|
public function formatRam(): string
|
|
{
|
|
if (!$this->ram_mb) {
|
|
return '-';
|
|
}
|
|
if ($this->ram_mb >= 1024) {
|
|
return round($this->ram_mb / 1024, 1) . ' GB';
|
|
}
|
|
return $this->ram_mb . ' MB';
|
|
}
|
|
|
|
public function formatBandwidth(): string
|
|
{
|
|
if (!$this->bandwidth_gb) {
|
|
return 'Unlimited';
|
|
}
|
|
if ($this->bandwidth_gb >= 1000) {
|
|
return round($this->bandwidth_gb / 1000, 1) . ' TB';
|
|
}
|
|
return $this->bandwidth_gb . ' GB';
|
|
}
|
|
|
|
public function formatDomainLimit(): ?string
|
|
{
|
|
if ($this->max_domains === null) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->max_domains === -1) {
|
|
return 'Unlimited sites';
|
|
}
|
|
|
|
return $this->max_domains.' site'.($this->max_domains === 1 ? '' : 's');
|
|
}
|
|
|
|
public function preferredNodeSegment(): ?string
|
|
{
|
|
if (! $this->isSharedHosting()) {
|
|
return null;
|
|
}
|
|
|
|
return match ($this->type) {
|
|
self::TYPE_WORDPRESS => HostingNode::SEGMENT_WORDPRESS,
|
|
self::TYPE_MULTI_DOMAIN => HostingNode::SEGMENT_HIGH_RESOURCE,
|
|
self::TYPE_SINGLE_DOMAIN => HostingNode::SEGMENT_STARTER,
|
|
default => HostingNode::SEGMENT_GENERAL,
|
|
};
|
|
}
|
|
}
|