feat: add GeneratesUniqueSlug class for unique slug generation

feat: create PostCategoryFactory for generating post category data

feat: create PostFactory for generating post data

feat: add retry_requested_at column to services table

feat: add vcenter provisioning fields to products table

feat: add module_host_id to os table

feat: create email_notification_templates table for email templates

feat: add scenario and format fields to template table

feat: add admin fields to transactions table

fix: update ticket table columns and types

feat: add uuid to ticket table and backfill existing records

fix: make ticket_reply user_id nullable

feat: create notifications table for user notifications

fix: make ticket uuid nullable

feat: create post_categories table for blog categories

feat: create posts table for blog posts

feat: add invoice email template for PDF generation
This commit is contained in:
baodev0
2026-05-25 15:39:42 +07:00
parent a31d238135
commit 520f21cb11
73 changed files with 4682 additions and 440 deletions
+97
View File
@@ -0,0 +1,97 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EmailNotificationTemplate extends Model
{
protected $table = 'email_notification_templates';
protected $fillable = [
'name',
'type',
'subject',
'header_color',
'header_title',
'blocks',
'is_active',
];
protected $casts = [
'blocks' => 'array',
'is_active' => 'boolean',
];
/** Default blocks by template type */
public static function defaultBlocks(string $type = 'invoice_notification'): array
{
if ($type === 'vm_credentials') {
return [
['id' => 'greeting', 'type' => 'greeting', 'label' => 'Lời chào', 'enabled' => true, 'order' => 1, 'config' => []],
['id' => 'vm_credentials', 'type' => 'vm_credentials', 'label' => 'Thông tin truy cập VPS','enabled' => true, 'order' => 2, 'config' => [
'fields' => ['service_host', 'service_username', 'service_port', 'service_password', 'os_name'],
]],
['id' => 'product_info', 'type' => 'product_info', 'label' => 'Thông tin sản phẩm', 'enabled' => true, 'order' => 3, 'config' => [
'fields' => ['product_name', 'product_billing', 'product_price', 'service_due_exp'],
]],
['id' => 'custom_text', 'type' => 'custom_text', 'label' => 'Lời nhắn', 'enabled' => true, 'order' => 4, 'config' => [
'content' => 'Nếu bạn có bất kỳ thắc mắc nào, vui lòng liên hệ với chúng tôi để được hỗ trợ.',
]],
['id' => 'footer', 'type' => 'footer', 'label' => 'Footer', 'enabled' => true, 'order' => 5, 'config' => []],
];
}
// invoice_notification (default)
return [
['id' => 'greeting', 'type' => 'greeting', 'label' => 'Lời chào', 'enabled' => true, 'order' => 1, 'config' => []],
['id' => 'invoice_info', 'type' => 'invoice_info', 'label' => 'Thông tin hóa đơn', 'enabled' => true, 'order' => 2, 'config' => [
'fields' => ['invoice_number', 'created_at', 'due_date', 'total', 'status'],
]],
['id' => 'customer_info','type' => 'customer_info','label' => 'Thông tin khách hàng', 'enabled' => false, 'order' => 3, 'config' => [
'fields' => ['name', 'email'],
]],
['id' => 'order_items', 'type' => 'order_items', 'label' => 'Chi tiết sản phẩm', 'enabled' => true, 'order' => 4, 'config' => []],
['id' => 'attachments', 'type' => 'attachments', 'label' => 'File đính kèm', 'enabled' => true, 'order' => 5, 'config' => []],
['id' => 'custom_text', 'type' => 'custom_text', 'label' => 'Lời nhắn', 'enabled' => true, 'order' => 6, 'config' => [
'content' => 'Nếu bạn có bất kỳ thắc mắc nào, vui lòng liên hệ với chúng tôi để được hỗ trợ.',
]],
['id' => 'footer', 'type' => 'footer', 'label' => 'Footer', 'enabled' => true, 'order' => 7, 'config' => []],
];
}
/** Field definitions exposed per block type */
public static function blockFieldDefinitions(): array
{
return [
'invoice_info' => [
['key' => 'invoice_number', 'label' => 'Số hóa đơn'],
['key' => 'created_at', 'label' => 'Ngày tạo'],
['key' => 'due_date', 'label' => 'Hạn thanh toán'],
['key' => 'total', 'label' => 'Tổng tiền'],
['key' => 'sub_total', 'label' => 'Tiền dịch vụ'],
['key' => 'tax', 'label' => 'Thuế'],
['key' => 'status', 'label' => 'Trạng thái'],
['key' => 'payment_method', 'label' => 'Phương thức TT'],
],
'customer_info' => [
['key' => 'name', 'label' => 'Tên khách hàng'],
['key' => 'email', 'label' => 'Email'],
['key' => 'address', 'label' => 'Địa chỉ'],
],
'vm_credentials' => [
['key' => 'service_host', 'label' => 'Địa chỉ IP'],
['key' => 'service_username', 'label' => 'Username'],
['key' => 'service_port', 'label' => 'Port'],
['key' => 'service_password', 'label' => 'Mật khẩu'],
['key' => 'os_name', 'label' => 'Hệ điều hành'],
],
'product_info' => [
['key' => 'product_name', 'label' => 'Tên sản phẩm'],
['key' => 'product_billing', 'label' => 'Chu kỳ thanh toán'],
['key' => 'product_price', 'label' => 'Giá dịch vụ'],
['key' => 'service_due_exp', 'label' => 'Ngày hết hạn'],
],
];
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Customer;
use App\Models\User;
class Notification extends Model
{
use HasFactory;
protected $fillable = [
'recipient_type',
'recipient_id',
'type',
'title',
'body',
'link',
'is_read',
'read_at',
'data',
];
protected $casts = [
'is_read' => 'boolean',
'read_at' => 'datetime',
'data' => 'array',
];
public static function notifyCustomer(Customer $customer, array $attributes): void
{
self::create([
'recipient_type' => 'client',
'recipient_id' => $customer->id,
] + $attributes);
}
public static function notifyAdmins(array $attributes): void
{
$adminIds = User::query()
->whereHas('roles', fn ($query) => $query->where('guard_name', 'admin'))
->pluck('id');
foreach ($adminIds as $adminId) {
self::create([
'recipient_type' => 'admin',
'recipient_id' => $adminId,
...$attributes,
]);
}
}
}
+7 -1
View File
@@ -15,13 +15,19 @@ class Os extends Model
'version',
'os_group_id',
'template_id',
'status'
'module_host_id',
'status',
];
public function osGroup()
{
return $this->belongsTo(\App\Models\OsGroup::class);
}
public function host()
{
return $this->belongsTo(\App\Models\ModuleHost::class, 'module_host_id');
}
public function getStatus()
{
return match ($this->attributes['status']){
+34 -10
View File
@@ -2,28 +2,52 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use HasFactory;
use SoftDeletes;
protected $table = 'post';
protected $table = 'posts';
protected $fillable = [
'name',
'post_category_id',
'title',
'slug',
'thumbnail',
'excerpt',
'content',
'status'
'thumbnail',
'status',
'is_featured',
'view_count',
'reading_time',
'meta_title',
'meta_description',
'published_at',
];
public function getStatus()
protected $casts = [
'is_featured' => 'boolean',
'view_count' => 'integer',
'reading_time' => 'integer',
'published_at' => 'datetime',
];
public function category(): BelongsTo
{
return match ((int)$this->status) {
1 => '<span class="badge bg-success text-success-fg">Published</span>',
2 => '<span class="badge bg-secondary text-secondary-fg">Draft</span>',
default => '<span class="badge bg-warning text-warning-fg">Pending</span>',
};
return $this->belongsTo(PostCategory::class, 'post_category_id');
}
public function scopePublished(Builder $query): Builder
{
return $query
->where('status', 'published')
->whereNotNull('published_at')
->where('published_at', '<=', now());
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class PostCategory extends Model
{
use HasFactory;
use SoftDeletes;
protected $table = 'post_categories';
protected $fillable = [
'name',
'slug',
'description',
'icon',
'sort_order',
'status',
];
protected $casts = [
'sort_order' => 'integer',
];
public function posts(): HasMany
{
return $this->hasMany(Post::class, 'post_category_id');
}
}
+12
View File
@@ -35,6 +35,8 @@ class Product extends Model
'sort_order',
'product_categories_id',
'module_ip_address',
'module_host_id',
'module_datastore_id',
'published_at',
];
@@ -137,6 +139,16 @@ class Product extends Model
return $this->belongsTo(ModuleIpAddress::class, 'module_ip_address');
}
public function provisioningHost()
{
return $this->belongsTo(\App\Models\ModuleHost::class, 'module_host_id');
}
public function provisioningDatastore()
{
return $this->belongsTo(\App\Models\ModuleDatastore::class, 'module_datastore_id');
}
public function addons()
{
return $this->hasMany(Addon::class, 'type', 'product_type')
+16
View File
@@ -28,6 +28,7 @@ class Service extends Model
'expires_at' => 'datetime',
'suspended_at' => 'datetime',
'cancelled_at' => 'datetime',
'retry_requested_at' => 'datetime',
];
// -------------------------------------------------------------------------
@@ -77,6 +78,21 @@ class Service extends Model
return $this->status === 'active';
}
public function isPending(): bool
{
return $this->status === 'pending';
}
public function canRetryProvisioning(): bool
{
return $this->isPending() && $this->retry_requested_at === null;
}
public function retryRequested(): bool
{
return $this->retry_requested_at !== null;
}
public function isExpired(): bool
{
return $this->expires_at !== null && $this->expires_at->isPast();
+20 -1
View File
@@ -13,12 +13,31 @@ class Template extends Model
protected $fillable = [
'type_id',
'scenario',
'format',
'invoice_template_id',
'name',
'subject',
'message',
'status'
'status',
];
protected $casts = [
'status' => 'integer',
];
public const SCENARIOS = [
'vm_credentials' => 'Khởi tạo dịch vụ',
'renewal_reminder' => 'Nhắc gia hạn',
'news_notification'=> 'Thông báo tin tức',
'invoice_pdf' => 'Hoá đơn điện tử',
];
public function invoiceTemplate()
{
return $this->belongsTo(self::class, 'invoice_template_id');
}
public function type()
{
return $this->belongsTo(\App\Models\Type::class);
+1 -1
View File
@@ -19,7 +19,7 @@ class Ticket extends Model
'description',
'status',
'room',
'data'
'data',
];
public function customer()
+7
View File
@@ -19,6 +19,8 @@ class Transaction extends Model
'trans',
'amount',
'customer_id',
'user_id',
'note',
'status',
'type',
'proof_image',
@@ -29,4 +31,9 @@ class Transaction extends Model
{
return $this->belongsTo(Customer::class, 'customer_id');
}
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}