Initial Ladill Hosting app with Gitea deploy pipeline.
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:
isaacclad
2026-06-06 16:24:20 +00:00
co-authored by Cursor
commit e251a4cf60
367 changed files with 66268 additions and 0 deletions
+308
View File
@@ -0,0 +1,308 @@
<?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,
};
}
}