Files
portal-api/app/Models/Product.php
baodev0andClaude Opus 5.5 c5115b52dd feat: nạp tiền thủ công mã 30 phút, gia hạn trừ số dư, đồng bộ domain từ Plesk
- Nạp tiền: mã thanh toán sống 30 phút (ManualDepositDraft), khôi phục/huỷ mã, lệnh dọn mã hết hạn; total_pending chỉ tính giao dịch đã nộp chứng từ, admin duyệt/từ chối/xoá giao dịch
- Gia hạn dịch vụ: phát hành invoice gia hạn trên order gốc và trừ số dư ngay
- Đồng bộ domain từ Plesk theo server, ServiceOrderRecorder tạo order/invoice cho dịch vụ admin dựng
- Danh sách dịch vụ trả field theo loại (service_plan, dns_records_count), đếm dịch vụ theo loại

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:07:51 +07:00

173 lines
4.3 KiB
PHP

<?php
namespace App\Models;
use App\Traits\HasUuid;
use App\Traits\LogsActivity;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Product extends Model
{
use HasFactory, HasUuid, SoftDeletes, LogsActivity;
protected array $uuidColumns = ['uuid'];
protected string $slugColumn = 'name';
protected $fillable = [
'name',
'slug',
'short_description',
'description',
'thumbnail',
'gallery',
'setup_fee',
'trial_days',
'cpu',
'ram',
'gpu',
'disk',
'bandwidth',
'domain_count',
'stock',
'is_unlimited',
'is_active',
'sort_order',
'product_categories_id',
'service_plan_id',
'module_ip_address',
'module_host_id',
'module_datastore_id',
'published_at',
'features',
];
protected $casts = [
'is_active' => 'boolean',
'is_unlimited' => 'boolean',
'published_at' => 'date',
'gallery' => 'array',
'product_type' => 'string',
'features' => 'array',
];
protected static function boot()
{
parent::boot();
static::saving(function ($product) {
if ($product->product_categories_id) {
$category = ProductCategories::find($product->product_categories_id);
if ($category && $category->type) {
$product->product_type = $category->type;
}
}
});
}
/**
* Relationship: Product belongs to a Category
*/
public function category()
{
return $this->belongsTo(ProductCategories::class, 'product_categories_id');
}
/**
* Relationship: Product has many OS versions (pivot table: os_product)
*/
public function osVersions()
{
return $this->belongsToMany(Os::class, 'os_product', 'product_id', 'os_id')
->withTimestamps();
}
/**
* Relationship: Product has many OS via product_os table (VPS OS selection)
*/
public function operatingSystems()
{
return $this->belongsToMany(Os::class, 'os_product', 'product_id', 'os_id')
->withTimestamps();
}
/**
* Relationship: Product has many pricings
*/
public function pricings()
{
return $this->hasMany(ProductPricings::class, 'product_id');
}
/**
* Relationship: Product has many order items
*/
public function orderItems()
{
return $this->hasMany(OrderItem::class, 'product_id');
}
/**
* Relationship: Product has many orders through order items
*/
public function orders()
{
return $this->hasManyThrough(Order::class, OrderItem::class, 'product_id', 'id', 'id', 'order_id');
}
/**
* Legacy: Many-to-many categories (if still needed)
*/
public function categories()
{
return $this->belongsToMany(ProductCategories::class, 'product_product_category', 'product_id', 'product_category_id');
}
/**
* Relationship: Product has many addons (through product type)
* Addons are linked via type in product_addon table
*/
public function ipAddress()
{
return $this->belongsTo(ModuleIpAddress::class, 'module_ip_address');
}
public function servicePlan()
{
return $this->belongsTo(ServicePlan::class);
}
public function addons()
{
return $this->hasMany(Addon::class, 'type', 'product_type')
->orderBy('sort_order');
}
/**
* Get monthly pricing (or first available pricing)
*/
public function getMonthlyPricingAttribute()
{
$pricings = $this->pricings;
return $pricings->firstWhere('billing_cycle', 'monthly')
?? $pricings->where('is_active', 1)->sortBy('price')->first();
}
/**
* Get display price (sale price if available, otherwise regular price)
*/
public function getPriceAttribute()
{
$pricing = $this->monthlyPricing;
return $pricing ? $pricing->price : 0;
}
public function getPriceSaleAttribute()
{
$pricing = $this->monthlyPricing;
return $pricing ? $pricing->price_sale : null;
}
}