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
@@ -0,0 +1,29 @@
<?php
namespace Database\Factories;
use App\Models\PostCategory;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends Factory<PostCategory>
*/
class PostCategoryFactory extends Factory
{
protected $model = PostCategory::class;
public function definition(): array
{
$name = $this->faker->unique()->words(2, true);
return [
'name' => Str::title($name),
'slug' => Str::slug($name),
'description' => $this->faker->sentence(),
'icon' => null,
'sort_order' => $this->faker->numberBetween(0, 10),
'status' => $this->faker->randomElement(['active', 'inactive']),
];
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace Database\Factories;
use App\Models\Post;
use App\Models\PostCategory;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends Factory<Post>
*/
class PostFactory extends Factory
{
protected $model = Post::class;
public function definition(): array
{
$title = $this->faker->unique()->sentence(6);
return [
'post_category_id' => PostCategory::factory(),
'title' => $title,
'slug' => Str::slug($title),
'excerpt' => $this->faker->sentence(),
'content' => '<p>' . implode('</p><p>', $this->faker->paragraphs(4)) . '</p>',
'thumbnail' => null,
'status' => $this->faker->randomElement(['draft', 'published', 'hidden']),
'is_featured' => $this->faker->boolean(20),
'view_count' => $this->faker->numberBetween(0, 3000),
'reading_time' => $this->faker->numberBetween(2, 12),
'meta_title' => $title,
'meta_description' => $this->faker->sentence(),
'published_at' => now(),
];
}
}
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('services', function (Blueprint $table) {
$table->timestamp('retry_requested_at')->nullable()->after('cancelled_at')->index()
->comment('Set once when customer requests provisioning retry');
});
}
public function down(): void
{
Schema::table('services', function (Blueprint $table) {
$table->dropColumn('retry_requested_at');
});
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->unsignedBigInteger('module_host_id')->nullable()->after('module_ip_address');
$table->unsignedBigInteger('module_datastore_id')->nullable()->after('module_host_id');
$table->foreign('module_host_id')
->references('id')
->on('module_host')
->nullOnDelete();
$table->foreign('module_datastore_id')
->references('id')
->on('module_datastore')
->nullOnDelete();
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropForeign(['module_host_id']);
$table->dropForeign(['module_datastore_id']);
$table->dropColumn(['module_host_id', 'module_datastore_id']);
});
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('os', function (Blueprint $table) {
$table->unsignedBigInteger('module_host_id')->nullable()->after('template_id');
$table->foreign('module_host_id')
->references('id')
->on('module_host')
->nullOnDelete();
});
}
public function down(): void
{
Schema::table('os', function (Blueprint $table) {
$table->dropForeign(['module_host_id']);
$table->dropColumn('module_host_id');
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('email_notification_templates', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('type')->default('invoice_notification'); // invoice_notification | vm_credentials
$table->string('subject');
$table->string('header_color', 20)->default('#1a56db');
$table->string('header_title')->default('Thông báo hóa đơn');
$table->json('blocks'); // array of block configs
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('email_notification_templates');
}
};
@@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
Schema::table('template', function (Blueprint $table) {
// scenario: tình huống sử dụng template này
$table->string('scenario', 50)->nullable()->after('type_id')
->comment('vm_credentials | renewal_reminder | news_notification | invoice_pdf');
// format: loại nội dung
$table->enum('format', ['html', 'json'])->default('html')->after('scenario');
// với email template: chọn invoice PDF template đính kèm
$table->unsignedBigInteger('invoice_template_id')->nullable()->after('format');
$table->foreign('invoice_template_id')
->references('id')->on('template')->nullOnDelete();
});
// Seed default setting keys for email scenario assignments
$setting = DB::table('setting')->where('key', 'email_settings')->first();
if (!$setting) {
DB::table('setting')->insert([
'key' => 'email_settings',
'value' => json_encode([
'vm_template_id' => 2,
'renewal_template_id' => 6,
'news_template_id' => null,
'invoice_template_id' => null,
]),
'created_at' => now(),
'updated_at' => now(),
]);
}
// Tag existing templates with scenario
DB::table('template')->where('id', 2)->update(['scenario' => 'vm_credentials']);
DB::table('template')->where('id', 6)->update(['scenario' => 'renewal_reminder']);
}
public function down(): void
{
Schema::table('template', function (Blueprint $table) {
$table->dropForeign(['invoice_template_id']);
$table->dropColumn(['scenario', 'format', 'invoice_template_id']);
});
DB::table('setting')->where('key', 'email_settings')->delete();
}
};
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('transactions', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->after('customer_id')->constrained('user')->nullOnDelete();
$table->text('note')->nullable()->after('status');
});
}
public function down(): void
{
Schema::table('transactions', function (Blueprint $table) {
$table->dropConstrainedForeignId('user_id');
$table->dropColumn('note');
});
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Migrate existing integer status values before changing column type
DB::statement("UPDATE ticket SET status = 'open' WHERE status = '1'");
DB::statement("UPDATE ticket SET status = 'closed' WHERE status = '0'");
Schema::table('ticket', function (Blueprint $table) {
$table->string('ticket_num', 50)->nullable()->change();
$table->string('status', 20)->default('open')->change();
});
}
public function down(): void
{
DB::statement("UPDATE ticket SET status = '1' WHERE status IN ('open','pending')");
DB::statement("UPDATE ticket SET status = '0' WHERE status IN ('closed','resolved')");
Schema::table('ticket', function (Blueprint $table) {
$table->integer('ticket_num')->nullable()->change();
$table->tinyInteger('status')->default(1)->change();
});
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
return new class extends Migration
{
public function up(): void
{
Schema::table('ticket', function (Blueprint $table) {
$table->char('uuid', 36)->nullable()->unique()->after('id');
});
// Backfill UUID v7 for existing rows
DB::table('ticket')->whereNull('uuid')->orderBy('id')->each(function ($row) {
DB::table('ticket')
->where('id', $row->id)
->update(['uuid' => (string) Str::uuid()]);
});
Schema::table('ticket', function (Blueprint $table) {
$table->char('uuid', 36)->nullable(false)->change();
});
}
public function down(): void
{
Schema::table('ticket', function (Blueprint $table) {
$table->dropColumn('uuid');
});
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('ticket_reply', function (Blueprint $table) {
$table->unsignedBigInteger('user_id')->nullable()->change();
});
}
public function down(): void
{
Schema::table('ticket_reply', function (Blueprint $table) {
$table->unsignedBigInteger('user_id')->nullable(false)->change();
});
}
};
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->id();
$table->string('recipient_type', 20); // admin | client
$table->unsignedBigInteger('recipient_id');
$table->string('type', 50); // ticket_reply | deposit_success
$table->string('title');
$table->text('body')->nullable();
$table->string('link')->nullable();
$table->boolean('is_read')->default(false);
$table->timestamp('read_at')->nullable();
$table->json('data')->nullable();
$table->timestamps();
$table->index(['recipient_type', 'recipient_id', 'is_read']);
$table->index(['recipient_type', 'recipient_id', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('notifications');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('ticket', function (Blueprint $table) {
$table->uuid('uuid')->nullable()->default(null)->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('ticket', function (Blueprint $table) {
$table->uuid('uuid')->nullable(false)->change();
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('post_categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->string('icon')->nullable();
$table->integer('sort_order')->default(0);
$table->enum('status', ['active', 'inactive'])->default('active');
$table->softDeletes();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('post_categories');
}
};
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('post_category_id')->constrained('post_categories');
$table->string('title');
$table->string('slug')->unique();
$table->text('excerpt')->nullable();
$table->longText('content');
$table->string('thumbnail')->nullable();
$table->enum('status', ['draft', 'published', 'hidden'])->default('draft');
$table->boolean('is_featured')->default(false);
$table->unsignedBigInteger('view_count')->default(0);
$table->unsignedInteger('reading_time')->nullable();
$table->string('meta_title')->nullable();
$table->text('meta_description')->nullable();
$table->timestamp('published_at')->nullable();
$table->softDeletes();
$table->timestamps();
$table->index(['status', 'published_at']);
$table->index(['post_category_id', 'status']);
$table->index(['is_featured', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};