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:
@@ -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']),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user