feat(service): huỷ dịch vụ, SSL, đổi mật khẩu hosting, lịch sử thanh toán theo hình thức

- Lịch sử thanh toán hoá đơn trả thêm `method` (tự động gia hạn / số dư / admin / chuyển khoản / hoàn tiền), bỏ đuôi `(method)` khỏi ghi chú
- `DnsServer::panelUrl()` = https://<domain>/ dùng cho email cấp hosting và nút Plesk của khách
- Order item trả thêm `uuid`/`type` của dịch vụ để FE dẫn tới trang quản lý
- Yêu cầu huỷ dịch vụ, chứng chỉ SSL, lịch sử gia hạn, đổi mật khẩu hosting cho khách

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-28 16:40:24 +07:00
co-authored by Claude Sonnet 5.5
parent a6b641015e
commit b44a88bb52
72 changed files with 1701 additions and 231 deletions
+5
View File
@@ -37,6 +37,11 @@ class DnsServer extends Model
return $this->domain ?: $this->ip_address;
}
public function panelUrl(): string
{
return 'https://' . $this->publicHost() . '/';
}
protected function scopeIsActive($query)
{
return $query->where('status', 'activated');
+8
View File
@@ -28,4 +28,12 @@ class ModuleHostingPleskClient extends Model
{
return $this->belongsTo(DnsServer::class, 'dns_manager_server_id');
}
public static function forHosting($hosting): ?self
{
return static::query()
->where('customer_id', $hosting->customer_id)
->where('dns_manager_server_id', $hosting->dns_manager_server_id)
->first();
}
}
+12
View File
@@ -36,6 +36,16 @@ class Service extends Model
protected $table = 'services';
public function cancellationRequests()
{
return $this->hasMany(ServiceCancellationRequest::class);
}
public function sslCertificate()
{
return $this->hasOne(ServiceSslCertificate::class);
}
public const HOSTING_ACCOUNT_RELATIONS = [
'hostingAccount.server:id,name,ip_address,domain,panel_port',
'hostingAccount.domain:id,domain_name',
@@ -53,6 +63,7 @@ class Service extends Model
'server_name',
'status',
'auto_renew',
'cancel_at_expiry',
'started_at',
'expires_at',
'suspended_at',
@@ -71,6 +82,7 @@ class Service extends Model
protected $casts = [
'auto_renew' => 'boolean',
'cancel_at_expiry' => 'boolean',
'started_at' => 'datetime',
'expires_at' => 'datetime',
'suspended_at' => 'datetime',
+8
View File
@@ -9,10 +9,18 @@ class ServiceAttachment extends Model
{
public const UPLOAD_RULE = 'required|file|mimes:jpg,jpeg,png,gif,webp,pdf,doc,docx,xls,xlsx,zip|max:51200';
public const AUDIENCE_ADMIN = 'admin';
public const AUDIENCE_CUSTOMER = 'customer';
protected $guarded = [];
protected $appends = ['url'];
public function scopeForCustomer($query)
{
return $query->where('audience', self::AUDIENCE_CUSTOMER);
}
public function service()
{
return $this->belongsTo(Service::class);
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ServiceCancellationRequest extends Model
{
public const STATUS_PENDING = 'pending';
public const STATUS_APPROVED = 'approved';
public const STATUS_REJECTED = 'rejected';
protected $casts = ['resolved_at' => 'datetime'];
protected $guarded = [];
public function service()
{
return $this->belongsTo(Service::class);
}
public function customer()
{
return $this->belongsTo(Customer::class);
}
public function scopePending($query)
{
return $query->where('status', self::STATUS_PENDING);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ServiceSslCertificate extends Model
{
public const MAX_ENTRIES = 30;
public const MAX_FILE_BYTES = 524288;
protected $guarded = [];
protected $casts = ['data' => 'encrypted:array'];
public function service()
{
return $this->belongsTo(Service::class);
}
public static function emptyData(): array
{
return ['fields' => [], 'files' => []];
}
}