update email template

This commit is contained in:
baodev0
2026-09-09 16:56:22 +07:00
parent d3d9f76ba0
commit 2365ef72e9
210 changed files with 6335 additions and 4688 deletions
@@ -14,8 +14,7 @@ return new class extends Migration {
{
Schema::create('user', function (Blueprint $table) {
$table->id();
$table->string('firstname', 255)->nullable();
$table->string('lastname', 255)->nullable();
$table->string('fullname', 255)->nullable();
$table->string('username', 100)->nullable();
$table->string('email', 100)->nullable();
$table->date('join_date')->nullable();
@@ -16,7 +16,8 @@ return new class extends Migration {
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('fullname', 100)->nullable();
$table->string('fullname', 255)->nullable();
$table->foreignId('saler')->nullable()->constrained('user')->nullOnDelete();
$table->string('avatar', 100)->nullable();
$table->integer('gender')->default(0);
$table->string('address', 100)->nullable();
@@ -41,7 +42,7 @@ return new class extends Migration {
$table->string('province_code', 20)->nullable();
$table->string('ward_code', 20)->nullable();
$table->string('remember_token', 100)->nullable();
$table->tinyInteger('status')->default(1);
$table->tinyInteger('status')->default(0)->comment('1: Active, 0: InActive');
$table->timestamps();
$table->softDeletes();
@@ -1,28 +0,0 @@
<?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('customers', function (Blueprint $table) {
//
$table->integer('unpaid_total')->default(0)->comment('Số nợ còn thiếu')->after('total');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('customers', function (Blueprint $table) {
//
});
}
};
@@ -0,0 +1,19 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
// WHMCS addresses (address1 + address2 combined) routinely exceed the old varchar(100).
return new class extends Migration
{
public function up(): void
{
// ponytail: raw SQL instead of Blueprint::change() to avoid adding doctrine/dbal just for this
DB::statement('ALTER TABLE `customers` MODIFY `address` TEXT NULL');
}
public function down(): void
{
DB::statement('ALTER TABLE `customers` MODIFY `address` VARCHAR(100) NULL');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
// Lets whmcs-style domain sync upsert idempotently against the admin_zonedns source
// (dns_manager_servers/domain/ns), the same pattern already used for orders/invoices.
return new class extends Migration
{
public function up(): void
{
Schema::table('dns_manager_servers', function (Blueprint $table) {
$table->unsignedBigInteger('legacy_id')->nullable()->unique()->after('id');
});
Schema::table('dns_manager_domain', function (Blueprint $table) {
$table->unsignedBigInteger('legacy_id')->nullable()->unique()->after('id');
});
Schema::table('dns_manager_ns', function (Blueprint $table) {
$table->unsignedBigInteger('legacy_id')->nullable()->unique()->after('id');
});
}
public function down(): void
{
Schema::table('dns_manager_servers', fn (Blueprint $t) => $t->dropColumn('legacy_id'));
Schema::table('dns_manager_domain', fn (Blueprint $t) => $t->dropColumn('legacy_id'));
Schema::table('dns_manager_ns', fn (Blueprint $t) => $t->dropColumn('legacy_id'));
}
};
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
// Lets whmcs addon sync upsert idempotently. product_categories/products already got legacy_id
// from earlier migrations (2026_06_15_000001_add_legacy_id_to_sync_tables and a partial run of this file).
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasColumn('product_categories', 'legacy_id')) {
Schema::table('product_categories', function (Blueprint $table) {
$table->unsignedBigInteger('legacy_id')->nullable()->unique()->after('id');
});
}
if (! Schema::hasColumn('products', 'legacy_id')) {
Schema::table('products', function (Blueprint $table) {
$table->unsignedBigInteger('legacy_id')->nullable()->unique()->after('id');
});
}
if (! Schema::hasColumn('product_addon', 'legacy_id')) {
Schema::table('product_addon', function (Blueprint $table) {
$table->unsignedBigInteger('legacy_id')->nullable()->unique()->after('id');
});
}
}
public function down(): void
{
Schema::table('product_addon', fn (Blueprint $t) => $t->dropColumn('legacy_id'));
}
};
@@ -0,0 +1,36 @@
<?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
{
Schema::create('registrars', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('short_name');
$table->string('status')->default('active');
$table->string('website')->nullable();
$table->timestamps();
});
DB::table('registrars')->insert([
['name' => 'ESC', 'short_name' => 'esc', 'status' => 'active', 'website' => null, 'created_at' => now(), 'updated_at' => now()],
['name' => 'Nhân Hòa', 'short_name' => 'nhanhoa', 'status' => 'active', 'website' => 'https://nhanhoa.com', 'created_at' => now(), 'updated_at' => now()],
['name' => 'PA', 'short_name' => 'PA', 'status' => 'active', 'website' => 'https://pavietnam.vn', 'created_at' => now(), 'updated_at' => now()],
['name' => 'TenTen', 'short_name' => 'TENTEN', 'status' => 'active', 'website' => 'https://tenten.vn', 'created_at' => now(), 'updated_at' => now()],
['name' => 'TinoDomain', 'short_name' => 'tinodomain', 'status' => 'active', 'website' => 'https://tino.org', 'created_at' => now(), 'updated_at' => now()],
['name' => 'VDC', 'short_name' => 'vdc', 'status' => 'active', 'website' => 'https://vdc.com.vn', 'created_at' => now(), 'updated_at' => now()],
['name' => 'Z.Com', 'short_name' => 'zcom', 'status' => 'active', 'website' => 'https://z.com', 'created_at' => now(), 'updated_at' => now()],
]);
}
public function down(): void
{
Schema::dropIfExists('registrars');
}
};
@@ -0,0 +1,26 @@
<?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('module_domain', function (Blueprint $table) {
$table->unsignedBigInteger('legacy_id')->nullable()->unique()->after('id');
$table->foreignId('registrar_id')->nullable()->after('domain_type_id')
->constrained('registrars')->nullOnDelete();
$table->text('note')->nullable()->after('status');
});
}
public function down(): void
{
Schema::table('module_domain', function (Blueprint $table) {
$table->dropForeign(['registrar_id']);
$table->dropColumn(['legacy_id', 'registrar_id', 'note']);
});
}
};
@@ -0,0 +1,24 @@
<?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('dns_manager_domain', function (Blueprint $table) {
$table->foreignId('registrar_id')->nullable()->after('plesk_id')
->constrained('registrars')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('dns_manager_domain', function (Blueprint $table) {
$table->dropForeign(['registrar_id']);
$table->dropColumn('registrar_id');
});
}
};
@@ -0,0 +1,29 @@
<?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('invoices', function (Blueprint $table) {
$table->date('billing_period_start')->nullable()->after('due_date');
$table->date('billing_period_end')->nullable()->after('billing_period_start');
// Prevents two renewal invoices being created for the same order in the same
// billing period (e.g. concurrent cron run + customer self-renew request).
// NULL values (non-renewal invoices) are not constrained by MySQL unique indexes.
$table->unique(['order_id', 'invoice_type', 'billing_period_start'], 'invoices_order_type_period_unique');
});
}
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->dropUnique('invoices_order_type_period_unique');
$table->dropColumn(['billing_period_start', 'billing_period_end']);
});
}
};
@@ -0,0 +1,24 @@
<?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) {
// Prevents crediting/recording the same reference twice (retried webhook,
// concurrent bank-statement poll, retried invoice payment recordTransaction()).
$table->unique(['customer_id', 'trans', 'type'], 'transactions_customer_trans_type_unique');
});
}
public function down(): void
{
Schema::table('transactions', function (Blueprint $table) {
$table->dropUnique('transactions_customer_trans_type_unique');
});
}
};
@@ -0,0 +1,24 @@
<?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('orders', function (Blueprint $table) {
// Mirrors invoice.paid_at for the order's most recently paid invoice, so the
// order stays in sync with payment state without joining to invoices to show it.
$table->timestamp('paid_at')->nullable()->after('payment_method');
});
}
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('paid_at');
});
}
};
@@ -0,0 +1,26 @@
<?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('product_categories', function (Blueprint $table) {
// Which billing_cycle values (one_time, monthly, quarterly, semi_annually,
// annually, biennially, triennially) are valid for products in this category —
// e.g. domain registration is annual-only, IT support can be one_time only,
// hosting/VPS are typically monthly..annually. Drives cycle pickers in the UI.
$table->json('billing_cycles')->nullable()->after('type');
});
}
public function down(): void
{
Schema::table('product_categories', function (Blueprint $table) {
$table->dropColumn('billing_cycles');
});
}
};
@@ -0,0 +1,25 @@
<?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('template', function (Blueprint $table) {
$table->json('metadata')->nullable()->after('format')
->comment('Danh sách variable hợp lệ (metadata.variables[]) + mail config (metadata.mail)');
$table->json('builder_data')->nullable()->after('metadata')
->comment('Dữ liệu project của Email Builder để mở lại chỉnh sửa');
});
}
public function down(): void
{
Schema::table('template', function (Blueprint $table) {
$table->dropColumn(['metadata', 'builder_data']);
});
}
};
@@ -0,0 +1,65 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const BACKFILL = [
'Domain Order Approved' => [
'scenario' => 'domain_order_approved',
'variables' => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'company_logo', 'label' => 'Logo công ty (URL)', 'type' => 'url', 'example' => ''],
['key' => 'client_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'domain_name', 'label' => 'Tên miền', 'type' => 'string', 'example' => 'example.vn'],
['key' => 'order_num', 'label' => 'Mã đơn hàng', 'type' => 'string', 'example' => 'DH001'],
],
],
'Domain Order Rejected' => [
'scenario' => 'domain_order_rejected',
'variables' => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'company_logo', 'label' => 'Logo công ty (URL)', 'type' => 'url', 'example' => ''],
['key' => 'client_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'domain_name', 'label' => 'Tên miền', 'type' => 'string', 'example' => 'example.vn'],
['key' => 'order_num', 'label' => 'Mã đơn hàng', 'type' => 'string', 'example' => 'DH001'],
['key' => 'reason', 'label' => 'Lý do từ chối', 'type' => 'string', 'example' => 'Tên miền đã có chủ sở hữu'],
],
],
'Order Created' => [
'scenario' => 'order_created',
'variables' => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'company_logo', 'label' => 'Logo công ty (URL)', 'type' => 'url', 'example' => ''],
['key' => 'client_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'order_num', 'label' => 'Mã đơn hàng', 'type' => 'string', 'example' => 'DH001'],
['key' => 'order_total', 'label' => 'Tổng tiền', 'type' => 'string', 'example' => '990.000 đ'],
['key' => 'invoice_num', 'label' => 'Mã hoá đơn', 'type' => 'string', 'example' => 'INV001'],
],
],
];
public function up(): void
{
foreach (self::BACKFILL as $name => $config) {
DB::table('template')
->where('name', $name)
->whereNull('scenario')
->update([
'scenario' => $config['scenario'],
'metadata' => json_encode(['variables' => $config['variables']]),
]);
}
}
public function down(): void
{
foreach (self::BACKFILL as $name => $config) {
DB::table('template')
->where('name', $name)
->where('scenario', $config['scenario'])
->update(['scenario' => null, 'metadata' => null]);
}
}
};
@@ -0,0 +1,254 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private function cardStyle(string $gradientFrom, string $gradientTo, string $headerTextColor = '#fde3d3'): string
{
return <<<CSS
* { box-sizing: border-box; }
.wrapper { max-width: 620px; margin: 30px auto; font-family: 'Segoe UI', Arial, sans-serif; }
.card { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }
.header { background: linear-gradient(135deg, {$gradientFrom} 0%, {$gradientTo} 100%); padding: 34px 40px; text-align: center; }
.header .icon { font-size: 44px; margin-bottom: 10px; }
.header h1 { color: #fff; margin: 0 0 6px; font-size: 22px; font-weight: 700; }
.header p { color: {$headerTextColor}; margin: 0; font-size: 14px; }
.body { padding: 34px 40px; }
.section-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #9ca3af; margin: 24px 0 10px; }
.section-label:first-child { margin-top: 0; }
.info-table { width: 100%; border-collapse: collapse; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
.info-table td { padding: 11px 16px; font-size: 14px; border-bottom: 1px solid #f3f4f6; }
.info-table tr:last-child td { border-bottom: none; }
.info-table td:first-child { color: #6b7280; width: 42%; }
.info-table td:last-child { font-weight: 600; color: #111827; word-break: break-word; }
.badge { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; background: #fff7ed; color: #c2410c; }
.badge-green { background: #dcfce7; color: #15803d; }
.notice { background: #fffbeb; border-left: 3px solid #f59e0b; padding: 12px 16px; border-radius: 0 6px 6px 0; font-size: 13px; color: #92400e; margin-top: 24px; line-height: 1.6; }
.footer { text-align: center; padding: 20px 40px; border-top: 1px solid #f3f4f6; font-size: 12px; color: #9ca3af; }
CSS;
}
private function header(): string
{
return <<<'HTML'
<tr class="Header"><td style="width: 700px;" valign="top"><table align="center" width="700" bgcolor="#ffffff" style="border-collapse: collapse; width: 700px; max-width: 100%; text-align: center; background-color: #ffffff; border-top: 5px solid #f16723;" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td style="padding: 0;"><a href="https://tgs.com.vn" style="display: block;" target="_blank" rel="noopener"><img src="https://res.cloudinary.com/izpast9i/image/upload/v1786499308/email_header.png" alt="Thư gửi từ Công Ty Cổ Phần Giải Pháp Công Nghệ Thế Giới Số" style="width: 100%; display: block; border: none;" width="700"></a></td></tr></tbody></table></td></tr>
HTML;
}
private function footer(): string
{
return <<<'HTML'
<tr class="Footer"><td align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="700" align="center" background="https://res.cloudinary.com/izpast9i/image/upload/v1786500298/email_footer.png" bgcolor="#F16723" style="border-collapse: collapse; width: 700px; background-image: url(&quot;https://res.cloudinary.com/izpast9i/image/upload/v1786500298/email_footer.png&quot;); background-size: cover; background-position: center; background-repeat: no-repeat;"><tbody><tr><td style="padding: 40px 30px 20px 30px; text-align: center;"><strong style="font-size: 14px; line-height: 22px; color: #ffffff;">CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ</strong></td></tr><tr><td style="padding: 6px 30px 16px 30px; text-align: center;"><span style="font-size: 12px; line-height: 18px; color: #ffffff;">Tel: (028) 7309.7379 &nbsp;-&nbsp; Email: <a href="mailto:support@tgs.com.vn" style="color: #ffffff; text-decoration: none;">support@tgs.com.vn</a></span></td></tr></tbody></table></td></tr></tbody></table></td></tr></tbody></table>
HTML;
}
private function wrap(string $bodyHtml): string
{
return '<table style="border-collapse: collapse; width: 100%; background-color: #efefef;" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td style="padding: 20px;" align="center" valign="top"><table style="padding-top: 20px; padding-bottom: 20px; border-collapse: collapse; background-color: #ffffff; width: 700px;" border="0" cellspacing="0" cellpadding="0"><tbody>'
. $this->header()
. '<tr class="NoiDungKM"><td style="width: 700px; padding: 0; background-color: #ffffff;" valign="top">' . $bodyHtml . '</td></tr>'
. $this->footer();
}
public function up(): void
{
$rows = [
[
'name' => '[Admin] Đơn hàng mới',
'scenario' => 'admin_order_notification',
'subject' => '[ADMIN] Đơn hàng mới #{$order_num} — {$customer_email}',
'variables' => [
['key' => 'order_num', 'label' => 'Mã đơn hàng', 'type' => 'string', 'example' => 'DH001'],
['key' => 'created_at', 'label' => 'Thời gian', 'type' => 'string', 'example' => now()->format('d/m/Y H:i:s')],
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'customer_email', 'label' => 'Email khách hàng', 'type' => 'string', 'example' => 'a@example.com'],
['key' => 'client_ip_row', 'label' => '[Tự động] Dòng IP đặt hàng', 'type' => 'html', 'example' => ''],
['key' => 'product_name', 'label' => 'Tên sản phẩm', 'type' => 'string', 'example' => 'Cloud VPS Pro'],
['key' => 'product_type', 'label' => 'Loại dịch vụ', 'type' => 'string', 'example' => 'VPS'],
['key' => 'billing_cycle', 'label' => 'Chu kỳ thanh toán', 'type' => 'string', 'example' => 'Hàng tháng'],
['key' => 'total_formatted', 'label' => 'Tổng tiền', 'type' => 'string', 'example' => '990.000 VNĐ'],
['key' => 'vm_block', 'label' => '[Tự động] Khối thông tin VPS', 'type' => 'html', 'example' => ''],
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
],
'message' => '<style>' . $this->cardStyle('#1e40af', '#1d4ed8', '#bfdbfe') . '.vm-box { background: #f0fdf4; border: 1.5px solid #86efac; border-radius: 10px; padding: 20px 24px; margin-top: 10px; } .vm-row { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #dcfce7; } .vm-row:last-child { border-bottom: none; } .vm-label { color: #15803d; font-size: 13px; width: 130px; flex-shrink: 0; } .vm-value { font-family: "Courier New", monospace; font-size: 14px; font-weight: 700; color: #111827; word-break: break-all; }</style><div class="wrapper"><div class="card"><div class="header"><div class="icon">📦</div><h1>Đơn hàng mới đã thanh toán</h1><p>Đơn #{$order_num} — {$created_at}</p></div><div class="body"><div class="section-label">Thông tin khách hàng</div><table class="info-table"><tr><td>Tên khách hàng</td><td>{$customer_name}</td></tr><tr><td>Email</td><td>{$customer_email}</td></tr>{$client_ip_row}</table><div class="section-label">Thông tin đơn hàng</div><table class="info-table"><tr><td>Mã đơn hàng</td><td>#{$order_num}</td></tr><tr><td>Sản phẩm</td><td>{$product_name}</td></tr><tr><td>Loại dịch vụ</td><td>{$product_type}</td></tr><tr><td>Chu kỳ thanh toán</td><td>{$billing_cycle}</td></tr><tr><td>Tổng tiền</td><td>{$total_formatted}</td></tr><tr><td>Trạng thái</td><td><span class="badge badge-green">✅ Đã thanh toán</span></td></tr></table>{$vm_block}</div><div class="footer">© ' . date('Y') . ' {$company_name} — Hệ thống quản lý dịch vụ<br>Email này được gửi tự động. Vui lòng không trả lời.</div></div></div>',
],
[
'name' => '[Admin] Đơn đăng ký tên miền mới',
'scenario' => 'admin_domain_order_new',
'subject' => '[ADMIN] Đơn đăng ký tên miền mới - {$domain_name}',
'variables' => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'domain_name', 'label' => 'Tên miền', 'type' => 'string', 'example' => 'example.vn'],
['key' => 'order_num', 'label' => 'Mã đơn hàng', 'type' => 'string', 'example' => 'DH001'],
['key' => 'order_total', 'label' => 'Tổng tiền', 'type' => 'string', 'example' => '350.000 VNĐ'],
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'customer_email', 'label' => 'Email khách hàng', 'type' => 'string', 'example' => 'a@example.com'],
['key' => 'created_at', 'label' => 'Thời gian', 'type' => 'string', 'example' => now()->format('d/m/Y H:i:s')],
['key' => 'admin_url', 'label' => 'Link xem đơn (admin)', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/admin/orders'],
],
'message' => $this->wrap('<div style="padding: 25px;"><p>Có một đơn đăng ký tên miền mới vừa được đặt tại <strong>{$company_name}</strong>.</p><p><strong>Thông tin đơn hàng:</strong></p><ul><li>Tên miền: <strong>{$domain_name}</strong></li><li>Mã đơn hàng: <strong>{$order_num}</strong></li><li>Tổng tiền: <strong>{$order_total}</strong></li><li>Khách hàng: <strong>{$customer_name}</strong></li><li>Email: <strong>{$customer_email}</strong></li><li>Thời gian: <strong>{$created_at}</strong></li></ul><table cellpadding="0" cellspacing="0" role="presentation" style="margin: 16px auto;"><tbody><tr><td style="border-radius: 4px; background-color: #f16723; text-align: center;"><a href="{$admin_url}" target="_blank" rel="noopener" style="font-family: sans-serif; display: block; font-size: 16px; font-weight: 600; line-height: 100%; color: #ffffff; text-decoration: none; padding: 16px 24px;">XEM ĐƠN HÀNG</a></td></tr></tbody></table><p style="text-align: justify; color: #666666; font-size: 13px;">Lưu ý: Đây là tin nhắn tự động. Vui lòng không trả lời tin nhắn này!</p></div>'),
],
[
'name' => '[Admin] Hosting account mới',
'scenario' => 'admin_hosting_account_created',
'subject' => '[ADMIN] Hosting account mới — {$domain_name}',
'variables' => [
['key' => 'domain_name', 'label' => 'Tên miền', 'type' => 'string', 'example' => 'example.vn'],
['key' => 'created_at', 'label' => 'Thời gian', 'type' => 'string', 'example' => now()->format('d/m/Y H:i:s')],
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'customer_email', 'label' => 'Email khách hàng', 'type' => 'string', 'example' => 'a@example.com'],
['key' => 'hosting_plan', 'label' => 'Gói hosting', 'type' => 'string', 'example' => 'Hosting Pro'],
['key' => 'server_name', 'label' => 'Tên server', 'type' => 'string', 'example' => 'host01'],
['key' => 'server_ip', 'label' => 'IP server', 'type' => 'string', 'example' => '1.2.3.4'],
['key' => 'plesk_client_id', 'label' => 'Plesk Client ID', 'type' => 'string', 'example' => '123'],
['key' => 'plesk_subscription_id', 'label' => 'Plesk Subscription ID', 'type' => 'string', 'example' => '456'],
['key' => 'ftp_username', 'label' => 'FTP Username', 'type' => 'string', 'example' => 'ftpuser'],
['key' => 'ftp_password_row', 'label' => '[Tự động] Dòng FTP Password', 'type' => 'html', 'example' => ''],
['key' => 'plesk_panel_url', 'label' => 'Plesk Panel URL', 'type' => 'url', 'example' => 'https://panel.tgs.com.vn'],
['key' => 'plesk_login', 'label' => 'Plesk Login', 'type' => 'string', 'example' => 'admin'],
['key' => 'plesk_password_row', 'label' => '[Tự động] Dòng Plesk Password', 'type' => 'html', 'example' => ''],
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
],
'message' => '<style>' . $this->cardStyle('#f06824', '#d9531a') . '.cred-box { background: #fff7ed; border: 1.5px solid #fdba74; border-radius: 10px; padding: 20px 24px; margin-top: 10px; } .cred-row { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #fed7aa; } .cred-row:last-child { border-bottom: none; } .cred-label { color: #c2410c; font-size: 13px; width: 150px; flex-shrink: 0; } .cred-value { font-family: "Courier New", monospace; font-size: 14px; font-weight: 700; color: #111827; word-break: break-all; } .note { margin-top: 16px; font-size: 12px; color: #9ca3af; }</style><div class="wrapper"><div class="card"><div class="header"><div class="icon">🖥️</div><h1>Hosting account mới được khởi tạo</h1><p>{$domain_name} — {$created_at}</p></div><div class="body"><div class="section-label">Thông tin khách hàng</div><table class="info-table"><tr><td>Tên khách hàng</td><td>{$customer_name}</td></tr><tr><td>Email</td><td>{$customer_email}</td></tr></table><div class="section-label">Thông tin dịch vụ Hosting</div><table class="info-table"><tr><td>Tên miền</td><td>{$domain_name}</td></tr><tr><td>Gói hosting</td><td>{$hosting_plan}</td></tr><tr><td>Server</td><td>{$server_name} ({$server_ip})</td></tr><tr><td>Plesk Client ID</td><td>{$plesk_client_id}</td></tr><tr><td>Plesk Subscription ID</td><td>{$plesk_subscription_id}</td></tr><tr><td>Trạng thái</td><td><span class="badge badge-green">✅ Active</span></td></tr></table><div class="section-label">Tài khoản FTP / System User (đã gửi cho khách)</div><div class="cred-box"><div class="cred-row"><span class="cred-label">👤 FTP Username</span><span class="cred-value">{$ftp_username}</span></div>{$ftp_password_row}</div><div class="section-label">Tài khoản đăng nhập Plesk Panel của khách hàng</div><div class="cred-box"><div class="cred-row"><span class="cred-label">🌐 Panel URL</span><span class="cred-value">{$plesk_panel_url}</span></div><div class="cred-row"><span class="cred-label">👤 Login</span><span class="cred-value">{$plesk_login}</span></div>{$plesk_password_row}</div><div class="note">Đây là tài khoản Plesk riêng của khách hàng (đã gửi kèm cho khách), khác với tài khoản quản trị của server dùng chung — tài khoản quản trị server không được đính kèm trong email này vì lý do bảo mật.</div></div><div class="footer">© ' . date('Y') . ' {$company_name} — Hệ thống quản lý dịch vụ<br>Email này được gửi tự động. Vui lòng không trả lời.</div></div></div>',
],
[
'name' => '[Admin] Yêu cầu nạp tiền chờ duyệt',
'scenario' => 'admin_manual_deposit_pending',
'subject' => '[ADMIN] Yêu cầu nạp tiền chờ duyệt - {$transaction_code}',
'variables' => [
['key' => 'transaction_code', 'label' => 'Mã giao dịch', 'type' => 'string', 'example' => 'DEP001'],
['key' => 'created_at', 'label' => 'Thời gian', 'type' => 'string', 'example' => now()->format('d/m/Y H:i:s')],
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'customer_email', 'label' => 'Email khách hàng', 'type' => 'string', 'example' => 'a@example.com'],
['key' => 'customer_phone_row', 'label' => '[Tự động] Dòng số điện thoại', 'type' => 'html', 'example' => ''],
['key' => 'client_ip_row', 'label' => '[Tự động] Dòng IP gửi yêu cầu', 'type' => 'html', 'example' => ''],
['key' => 'amount_formatted', 'label' => 'Số tiền', 'type' => 'string', 'example' => '500.000 VNĐ'],
['key' => 'proof_row', 'label' => '[Tự động] Dòng link chứng từ', 'type' => 'html', 'example' => ''],
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
],
'message' => '<style>' . $this->cardStyle('#b45309', '#f16723', '#ffedd5') . '</style><div class="wrapper"><div class="card"><div class="header"><div class="icon">🔔</div><h1>Yêu cầu nạp tiền chờ duyệt</h1><p>{$transaction_code} — {$created_at}</p></div><div class="body"><div class="section-label">Thông tin khách hàng</div><table class="info-table"><tr><td>Tên khách hàng</td><td>{$customer_name}</td></tr><tr><td>Email</td><td>{$customer_email}</td></tr>{$customer_phone_row}{$client_ip_row}</table><div class="section-label">Thông tin giao dịch</div><table class="info-table"><tr><td>Mã giao dịch</td><td>{$transaction_code}</td></tr><tr><td>Số tiền</td><td>{$amount_formatted}</td></tr><tr><td>Trạng thái</td><td><span class="badge">Đang chờ duyệt</span></td></tr>{$proof_row}</table><div class="notice">Vui lòng vào trang quản trị để kiểm tra chứng từ và duyệt hoặc từ chối giao dịch.</div></div><div class="footer">© ' . date('Y') . ' {$company_name} — Email này được gửi tự động.</div></div></div>',
],
[
'name' => '[Admin] Ticket hỗ trợ mới',
'scenario' => 'admin_ticket_notification',
'subject' => '[ADMIN] Ticket mới #{$ticket_num} — {$customer_email}',
'variables' => [
['key' => 'ticket_num', 'label' => 'Mã ticket', 'type' => 'string', 'example' => 'TCK001'],
['key' => 'subject_text', 'label' => 'Tiêu đề ticket', 'type' => 'string', 'example' => 'Không truy cập được VPS'],
['key' => 'room', 'label' => 'Phòng ban', 'type' => 'string', 'example' => 'Kỹ thuật'],
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'customer_email', 'label' => 'Email khách hàng', 'type' => 'string', 'example' => 'a@example.com'],
['key' => 'description', 'label' => 'Nội dung ticket (đã cắt bớt)', 'type' => 'string', 'example' => 'Không đăng nhập được vào VPS từ sáng nay...'],
['key' => 'ticket_url', 'label' => 'Link xem ticket', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/tickets/TCK001'],
],
'message' => '<style>' . $this->cardStyle('#f06824', '#e05510', '#fde8d8') . '.desc-box { background: #fff7f3; border: 1.5px solid #fcd9c5; border-radius: 10px; padding: 16px 20px; margin-top: 10px; font-size: 14px; color: #374151; line-height: 1.6; white-space: pre-wrap; word-break: break-word; } .cta { text-align: center; margin: 28px 0 4px; } .cta a { display: inline-block; background: #f06824; color: #fff; text-decoration: none; padding: 12px 32px; border-radius: 8px; font-size: 15px; font-weight: 700; letter-spacing: 0.02em; }</style><div class="wrapper"><div class="card"><div class="header"><div class="icon">🎫</div><h1>Ticket mới cần xử lý</h1><p>Ticket #{$ticket_num}</p></div><div class="body"><div class="section-label">Thông tin ticket</div><table class="info-table"><tr><td>Mã ticket</td><td>#{$ticket_num}</td></tr><tr><td>Tiêu đề</td><td>{$subject_text}</td></tr><tr><td>Phòng ban</td><td>{$room}</td></tr></table><div class="section-label">Thông tin khách hàng</div><table class="info-table"><tr><td>Khách hàng</td><td>{$customer_name}</td></tr><tr><td>Email khách</td><td>{$customer_email}</td></tr></table><div class="section-label">Nội dung</div><div class="desc-box">{$description}</div><div class="cta"><a href="{$ticket_url}">Xem ticket</a></div></div><div class="footer">© ' . date('Y') . ' Portal Admin — Hệ thống quản lý dịch vụ<br>Email này được gửi tự động. Vui lòng không trả lời.</div></div></div>',
],
[
'name' => '[Admin] Yêu cầu dùng thử VPS',
'scenario' => 'admin_trial_registration_request',
'subject' => '[ADMIN] Yêu cầu dùng thử Cloud VPS - {$customer_name}',
'variables' => [
['key' => 'customer_name', 'label' => 'Họ tên', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'customer_email', 'label' => 'Email', 'type' => 'string', 'example' => 'a@example.com'],
['key' => 'customer_phone', 'label' => 'Số điện thoại', 'type' => 'string', 'example' => '0900000000'],
['key' => 'cpu', 'label' => 'CPU', 'type' => 'string', 'example' => '4 core'],
['key' => 'ram', 'label' => 'RAM', 'type' => 'string', 'example' => '8GB'],
['key' => 'ssd', 'label' => 'SSD', 'type' => 'string', 'example' => '100GB'],
['key' => 'os', 'label' => 'Hệ điều hành', 'type' => 'string', 'example' => 'Ubuntu 22.04'],
['key' => 'other_row', 'label' => '[Tự động] Dòng yêu cầu khác', 'type' => 'html', 'example' => ''],
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'created_at', 'label' => 'Thời gian', 'type' => 'string', 'example' => now()->format('d/m/Y H:i:s')],
],
'message' => '<style>' . $this->cardStyle('#ea580c', '#f97316', '#ffedd5') . '</style><div class="wrapper"><div class="card"><div class="header"><div class="icon">🧪</div><h1>Yêu cầu dùng thử Cloud VPS</h1><p>{$created_at}</p></div><div class="body"><div class="section-label">Thông tin khách hàng</div><table class="info-table"><tr><td>Họ tên</td><td>{$customer_name}</td></tr><tr><td>Email</td><td>{$customer_email}</td></tr><tr><td>Số điện thoại</td><td>{$customer_phone}</td></tr></table><div class="section-label">Cấu hình yêu cầu</div><table class="info-table"><tr><td>CPU</td><td>{$cpu}</td></tr><tr><td>RAM</td><td>{$ram}</td></tr><tr><td>SSD</td><td>{$ssd}</td></tr><tr><td>Hệ điều hành</td><td>{$os}</td></tr>{$other_row}<tr><td>Trạng thái</td><td><span class="badge">Chờ khởi tạo</span></td></tr></table><div class="notice">Vui lòng vào trang quản trị để khởi tạo VPS dùng thử theo cấu hình khách hàng yêu cầu.</div></div><div class="footer">© ' . date('Y') . ' {$company_name} — Email này được gửi tự động.</div></div></div>',
],
[
'name' => 'Nạp tiền thành công',
'scenario' => 'customer_deposit_success',
'subject' => 'V/v: Thông báo nạp tiền thành công - {$amount_formatted} VNĐ',
'variables' => [
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'transaction_code', 'label' => 'Mã giao dịch', 'type' => 'string', 'example' => 'DEP001'],
['key' => 'amount_formatted', 'label' => 'Số tiền nạp', 'type' => 'string', 'example' => '500.000'],
['key' => 'credit_formatted', 'label' => 'Khuyến mãi', 'type' => 'string', 'example' => '0'],
['key' => 'current_total_formatted', 'label' => 'Tổng số dư hiện tại', 'type' => 'string', 'example' => '1.500.000'],
['key' => 'current_credit_formatted', 'label' => 'Số dư khuyến mãi hiện tại', 'type' => 'string', 'example' => '0'],
['key' => 'deposit_date', 'label' => 'Thời gian nạp', 'type' => 'string', 'example' => now()->format('d/m/Y H:i:s')],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'message' => '<style>' . $this->cardStyle('#16a34a', '#15803d', '#bbf7d0') . '</style><div class="wrapper"><div class="card"><div class="header"><div class="icon">💰</div><h1>Nạp tiền thành công</h1><p>{$transaction_code} — {$deposit_date}</p></div><div class="body"><p>Xin chào <strong>{$customer_name}</strong>, yêu cầu nạp tiền của bạn đã được duyệt thành công.</p><table class="info-table"><tr><td>Mã giao dịch</td><td>{$transaction_code}</td></tr><tr><td>Số tiền nạp</td><td>{$amount_formatted} VNĐ</td></tr><tr><td>Khuyến mãi</td><td>{$credit_formatted} VNĐ</td></tr><tr><td>Tổng số dư hiện tại</td><td>{$current_total_formatted} VNĐ</td></tr><tr><td>Số dư khuyến mãi hiện tại</td><td>{$current_credit_formatted} VNĐ</td></tr></table><p style="color:#6b7280;font-size:13px;margin-top:20px;">Nếu bạn cần hỗ trợ thêm, vui lòng liên hệ {$support_email} hoặc {$support_phone}.</p></div><div class="footer">Email này được gửi tự động. Vui lòng không trả lời email này.</div></div></div>',
],
[
'name' => 'Chào mừng khách hàng mới',
'scenario' => 'customer_welcome',
'subject' => 'Chào mừng bạn đến với {$app_name}',
'variables' => [
['key' => 'app_name', 'label' => 'Tên ứng dụng', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'full_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Quý khách'],
['key' => 'login_url', 'label' => 'Link đăng nhập', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/dang-nhap'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'message' => $this->wrap('<div style="padding: 25px;"><p style="text-align: center;"><img src="https://res.cloudinary.com/izpast9i/image/upload/v1786503590/email_banner_header.png" alt="Chào mừng bạn đến với Portal Thế Giới Số" width="100%"></p><p style="font-size: 21px; font-weight: bold; text-align: center;"><strong>Chào mừng Quý Khách đến với hệ thống quản trị dịch vụ của Thế Giới Số</strong></p><p style="font-weight: bold;"><strong>Kính chào <span style="color: #f16723;">{$full_name},</span></strong></p><p style="text-align: justify;">Chào mừng Quý khách đến với <strong>{$app_name}</strong>. Tài khoản của Quý khách đã được khởi tạo thành công và sẵn sàng sử dụng.</p><table cellspacing="0" cellpadding="0" style="margin: 16px auto 16px auto;"><tbody><tr><td align="center" style="background-color: #f16723; padding: 10px 20px; border-radius: 4px;"><a href="{$login_url}" target="_blank" rel="noopener" style="text-decoration: none;"><strong style="color: #ffffff;">Đăng nhập ngay</strong></a></td></tr></tbody></table><p style="text-align: justify; color: #666666; font-size: 13px;">Email này được gửi tự động. Vui lòng không trả lời email này.</p><p style="text-align: justify;">---<br>Hotline: {$support_phone}<br>Email: {$support_email}</p></div>'),
],
[
'name' => 'Đổi port Remote VPS',
'scenario' => 'vm_port_change',
'subject' => 'Port VPS của bạn đã được thay đổi',
'variables' => [
['key' => 'vm_name', 'label' => 'Tên VPS', 'type' => 'string', 'example' => 'vm-example-01'],
['key' => 'new_port', 'label' => 'Port mới', 'type' => 'string', 'example' => '13389'],
],
'message' => '<style>' . $this->cardStyle('#1e40af', '#1d4ed8', '#bfdbfe') . '</style><div class="wrapper"><div class="card"><div class="header"><div class="icon">🔌</div><h1>Port Remote Desktop đã được thay đổi</h1><p>Máy chủ: {$vm_name}</p></div><div class="body"><p>Xin chào Quý khách,</p><p>Port kết nối Remote Desktop (RDP) của máy chủ ảo <strong>{$vm_name}</strong> đã được thay đổi thành công.</p><table class="info-table"><tr><td>Máy chủ</td><td>{$vm_name}</td></tr><tr><td>Port mới</td><td>{$new_port}</td></tr></table><div class="notice">Vui lòng cập nhật lại port khi kết nối Remote Desktop tới máy chủ.</div></div><div class="footer">Email này được gửi tự động. Vui lòng không trả lời email này.</div></div></div>',
],
[
'name' => 'Đặt lại mật khẩu VPS thành công',
'scenario' => 'vm_password_reset_success',
'subject' => 'Mật khẩu VPS của bạn đã được đặt lại',
'variables' => [
['key' => 'vm_name', 'label' => 'Tên VPS', 'type' => 'string', 'example' => 'vm-example-01'],
['key' => 'new_password', 'label' => 'Mật khẩu mới', 'type' => 'string', 'example' => 'NewP@ss123'],
],
'message' => '<style>' . $this->cardStyle('#f06824', '#d9531a') . '</style><div class="wrapper"><div class="card"><div class="header"><div class="icon">🔑</div><h1>Mật khẩu VPS đã được đặt lại</h1><p>Máy chủ: {$vm_name}</p></div><div class="body"><p>Xin chào Quý khách,</p><p>Mật khẩu đăng nhập máy chủ ảo <strong>{$vm_name}</strong> đã được đặt lại thành công. Vui lòng sử dụng mật khẩu mới bên dưới để đăng nhập:</p><table class="info-table"><tr><td>Máy chủ</td><td>{$vm_name}</td></tr><tr><td>Mật khẩu mới</td><td>{$new_password}</td></tr></table><div class="notice">Vì lý do bảo mật, vui lòng đổi lại mật khẩu ngay sau khi đăng nhập thành công.</div></div><div class="footer">Email này được gửi tự động. Vui lòng không trả lời email này.</div></div></div>',
],
];
foreach ($rows as $row) {
$exists = DB::table('template')->where('scenario', $row['scenario'])->exists();
if ($exists) {
continue;
}
DB::table('template')->insert([
'type_id' => 0,
'scenario' => $row['scenario'],
'format' => 'html',
'name' => $row['name'],
'subject' => $row['subject'],
'message' => $row['message'],
'metadata' => json_encode(['variables' => $row['variables']]),
'status' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
public function down(): void
{
DB::table('template')->whereIn('scenario', [
'admin_order_notification',
'admin_domain_order_new',
'admin_hosting_account_created',
'admin_manual_deposit_pending',
'admin_ticket_notification',
'admin_trial_registration_request',
'customer_deposit_success',
'customer_welcome',
'vm_port_change',
'vm_password_reset_success',
])->delete();
}
};
@@ -0,0 +1,188 @@
<?php
use App\Models\Template;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
// Metadata cho 13 scenario "nhóm A" — trước đây dùng str_replace tay, chưa từng khai báo metadata.
private const LEGACY_VARIABLES = [
'vm_suspended' => [
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'vm_name', 'label' => 'Tên VPS', 'type' => 'string', 'example' => 'VPS-001'],
['key' => 'vmware_id', 'label' => 'VM ID (vCenter)', 'type' => 'string', 'example' => 'vm-12345'],
['key' => 'suspended_at', 'label' => 'Thời gian tạm ngưng', 'type' => 'string', 'example' => '09/09/2026 10:00'],
['key' => 'login_url', 'label' => 'Link đăng nhập', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/dang-nhap'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'ticket_created' => [
['key' => 'full_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'ticket_num', 'label' => 'Mã ticket', 'type' => 'string', 'example' => 'TCK001'],
['key' => 'subject', 'label' => 'Tiêu đề ticket', 'type' => 'string', 'example' => 'Không truy cập được VPS'],
['key' => 'room_row', 'label' => '[Tự động] Dòng phòng ban', 'type' => 'html', 'example' => ''],
['key' => 'ticket_url', 'label' => 'Link xem ticket', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/tickets/TCK001'],
['key' => 'app_name', 'label' => 'Tên ứng dụng', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'service_renewal_reminder' => [
['key' => 'client_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'product_name', 'label' => 'Tên dịch vụ', 'type' => 'string', 'example' => 'Cloud VPS Pro'],
['key' => 'server_name_suffix', 'label' => '[Tự động] Hậu tố tên máy chủ', 'type' => 'html', 'example' => ''],
['key' => 'expires_at', 'label' => 'Ngày hết hạn', 'type' => 'string', 'example' => '15/09/2026'],
['key' => 'price_row', 'label' => '[Tự động] Dòng số tiền gia hạn', 'type' => 'html', 'example' => ''],
['key' => 'login_url', 'label' => 'Link đăng nhập', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/dang-nhap'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'domain_order_received' => [
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'domain_name', 'label' => 'Tên miền', 'type' => 'string', 'example' => 'example.vn'],
['key' => 'order_num', 'label' => 'Mã đơn hàng', 'type' => 'string', 'example' => 'DH001'],
['key' => 'order_total', 'label' => 'Tổng tiền', 'type' => 'string', 'example' => '350.000 VNĐ'],
['key' => 'created_at', 'label' => 'Thời gian', 'type' => 'string', 'example' => '09/09/2026 10:00'],
['key' => 'order_url', 'label' => 'Link xem đơn hàng', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/orders/DH001'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'hosting_account_created' => [
['key' => 'app_name', 'label' => 'Tên ứng dụng', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'full_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'domain_name', 'label' => 'Tên miền', 'type' => 'string', 'example' => 'example.vn'],
['key' => 'hosting_plan', 'label' => 'Gói hosting', 'type' => 'string', 'example' => 'Hosting Pro'],
['key' => 'server_host_row', 'label' => '[Tự động] Dòng máy chủ/FTP host', 'type' => 'html', 'example' => ''],
['key' => 'ftp_username_row', 'label' => '[Tự động] Dòng tài khoản FTP', 'type' => 'html', 'example' => ''],
['key' => 'ftp_password_row', 'label' => '[Tự động] Dòng mật khẩu FTP', 'type' => 'html', 'example' => ''],
['key' => 'ftp_password_notice', 'label' => '[Tự động] Ghi chú bảo mật FTP', 'type' => 'html', 'example' => ''],
['key' => 'plesk_block', 'label' => '[Tự động] Khối tài khoản Plesk', 'type' => 'html', 'example' => ''],
['key' => 'login_url', 'label' => 'Link đăng nhập', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/dang-nhap'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'manual_deposit_status' => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'transaction_code', 'label' => 'Mã giao dịch', 'type' => 'string', 'example' => 'DEP001'],
['key' => 'amount_formatted', 'label' => 'Số tiền', 'type' => 'string', 'example' => '500.000'],
['key' => 'status_label', 'label' => 'Nhãn trạng thái', 'type' => 'string', 'example' => 'Đã duyệt'],
['key' => 'status_color', 'label' => '[Tự động] Màu chữ trạng thái', 'type' => 'string', 'example' => '#16a34a'],
['key' => 'status_bg', 'label' => '[Tự động] Màu nền trạng thái', 'type' => 'string', 'example' => '#dcfce7'],
['key' => 'status_message', 'label' => 'Nội dung thông báo', 'type' => 'string', 'example' => 'Yêu cầu nạp tiền đã được duyệt.'],
['key' => 'deposit_date', 'label' => 'Thời gian nạp', 'type' => 'string', 'example' => '09/09/2026 10:00'],
['key' => 'processed_date_row', 'label' => '[Tự động] Dòng thời gian xử lý', 'type' => 'html', 'example' => ''],
['key' => 'portal_url', 'label' => 'Link trang nạp tiền', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/nap-tien'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'order_payment_success' => [
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'order_num', 'label' => 'Mã đơn hàng', 'type' => 'string', 'example' => 'DH001'],
['key' => 'amount_formatted', 'label' => 'Tổng thanh toán', 'type' => 'string', 'example' => '990.000'],
['key' => 'vm_block', 'label' => '[Tự động] Khối thông tin VPS', 'type' => 'html', 'example' => ''],
['key' => 'addon_block', 'label' => '[Tự động] Khối addon', 'type' => 'html', 'example' => ''],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'password_reset_admin' => [
['key' => 'app_name', 'label' => 'Tên ứng dụng', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'full_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'email', 'label' => 'Email tài khoản', 'type' => 'string', 'example' => 'a@example.com'],
['key' => 'password', 'label' => 'Mật khẩu mới', 'type' => 'string', 'example' => 'NewP@ss123'],
['key' => 'login_url', 'label' => 'Link đăng nhập', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/dang-nhap'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'snapshot_action' => [
['key' => 'app_name', 'label' => 'Tên ứng dụng', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'full_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'vm_name', 'label' => 'Tên VPS', 'type' => 'string', 'example' => 'vm-example-01'],
['key' => 'snapshot_name', 'label' => 'Tên snapshot', 'type' => 'string', 'example' => 'snap-2026-09-09'],
['key' => 'action_at', 'label' => 'Thời gian thao tác', 'type' => 'string', 'example' => '09/09/2026 10:00'],
['key' => 'action_icon', 'label' => '[Tự động] Icon', 'type' => 'string', 'example' => '📸'],
['key' => 'action_title', 'label' => '[Tự động] Tiêu đề theo hành động', 'type' => 'string', 'example' => 'Tạo Snapshot thành công'],
['key' => 'action_desc', 'label' => '[Tự động] Mô tả theo hành động', 'type' => 'string', 'example' => 'Snapshot đã được tạo thành công'],
['key' => 'action_status', 'label' => '[Tự động] Trạng thái theo hành động', 'type' => 'string', 'example' => 'Tạo thành công ✓'],
['key' => 'login_url', 'label' => 'Link đăng nhập', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/dang-nhap'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'verify_account_welcome' => [
['key' => 'app_name', 'label' => 'Tên ứng dụng', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'full_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'verify_code', 'label' => 'Mã xác thực', 'type' => 'string', 'example' => '482913'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'vm_creation_failed' => [
['key' => 'app_name', 'label' => 'Tên ứng dụng', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'full_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'order_num', 'label' => 'Mã đơn hàng', 'type' => 'string', 'example' => 'DH001'],
['key' => 'product_name', 'label' => 'Tên sản phẩm', 'type' => 'string', 'example' => 'Cloud VPS Pro'],
['key' => 'invoice_url', 'label' => 'Link xem hoá đơn', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/hoa-don/1'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'vm_password_changed' => [
['key' => 'app_name', 'label' => 'Tên ứng dụng', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'full_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'vm_name', 'label' => 'Tên VPS', 'type' => 'string', 'example' => 'vm-example-01'],
['key' => 'new_password_row', 'label' => '[Tự động] Dòng mật khẩu mới', 'type' => 'html', 'example' => ''],
['key' => 'login_url', 'label' => 'Link đăng nhập', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/dang-nhap'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
'vm_rebuild_failed' => [
['key' => 'app_name', 'label' => 'Tên ứng dụng', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'full_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'vm_name', 'label' => 'Tên VPS', 'type' => 'string', 'example' => 'vm-example-01'],
['key' => 'ip', 'label' => 'Địa chỉ IP', 'type' => 'string', 'example' => '1.2.3.4'],
['key' => 'service_url', 'label' => 'Link xem dịch vụ', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/dich-vu'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'support_phone', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
];
public function up(): void
{
Template::query()->whereNotNull('scenario')->get()->each(function (Template $template) {
$template->subject = $this->convert((string) $template->subject);
$template->message = $this->convert((string) $template->message);
if (isset(self::LEGACY_VARIABLES[$template->scenario])) {
$metadata = $template->metadata ?? [];
$metadata['variables'] = self::LEGACY_VARIABLES[$template->scenario];
$template->metadata = $metadata;
}
$template->save();
});
}
public function down(): void
{
Template::query()->whereNotNull('scenario')->get()->each(function (Template $template) {
$template->subject = $this->revert((string) $template->subject);
$template->message = $this->revert((string) $template->message);
if (isset(self::LEGACY_VARIABLES[$template->scenario])) {
$metadata = $template->metadata ?? [];
unset($metadata['variables']);
$template->metadata = $metadata ?: null;
}
$template->save();
});
}
private function convert(string $content): string
{
return preg_replace('/\{\$([a-zA-Z_][a-zA-Z0-9_]*)\}/', '{{$1}}', $content) ?? $content;
}
private function revert(string $content): string
{
return preg_replace('/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/', '{\$$1}', $content) ?? $content;
}
};
@@ -0,0 +1,102 @@
<?php
use App\Models\Template;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
// id => [scenario, variables[]]
private const BACKFILL = [
1 => [
'scenario' => 'customer_signup_verify',
'variables' => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'company_logo', 'label' => 'Logo công ty (URL)', 'type' => 'url', 'example' => ''],
['key' => 'link_verify', 'label' => 'Link xác thực tài khoản', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/verify/xxx'],
],
],
3 => [
'scenario' => 'hosting_information',
'variables' => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'company_logo', 'label' => 'Logo công ty (URL)', 'type' => 'url', 'example' => ''],
['key' => 'client_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'product_name', 'label' => 'Tên sản phẩm', 'type' => 'string', 'example' => 'Hosting Pro'],
['key' => 'product_billing', 'label' => 'Chu kỳ thanh toán', 'type' => 'string', 'example' => 'Hàng tháng'],
['key' => 'product_price', 'label' => 'Giá dịch vụ', 'type' => 'string', 'example' => '150.000'],
['key' => 'service_host', 'label' => 'IP máy chủ', 'type' => 'string', 'example' => '1.2.3.4'],
['key' => 'service_username', 'label' => 'Username', 'type' => 'string', 'example' => 'user01'],
['key' => 'service_password', 'label' => 'Mật khẩu dịch vụ', 'type' => 'string', 'example' => 'P@ss123'],
['key' => 'service_due_exp', 'label' => 'Ngày hết hạn', 'type' => 'string', 'example' => '09/09/2026'],
['key' => 'ftp_account', 'label' => 'Tài khoản FTP', 'type' => 'string', 'example' => 'ftpuser'],
['key' => 'ftp_password', 'label' => 'Mật khẩu FTP', 'type' => 'string', 'example' => 'Ftp@123'],
],
],
4 => [
'scenario' => 'customer_password_reset',
'variables' => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'company_logo', 'label' => 'Logo công ty (URL)', 'type' => 'url', 'example' => ''],
['key' => 'password', 'label' => 'Mật khẩu mới', 'type' => 'string', 'example' => 'NewP@ss123'],
],
],
9 => [
'scenario' => 'service_expiry',
'variables' => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'company_logo', 'label' => 'Logo công ty (URL)', 'type' => 'url', 'example' => ''],
['key' => 'product_name', 'label' => 'Tên sản phẩm', 'type' => 'string', 'example' => 'Cloud VPS Pro'],
['key' => 'service_due_exp', 'label' => 'Ngày hết hạn', 'type' => 'string', 'example' => '09/09/2026'],
['key' => 'service_remaining', 'label' => 'Số ngày còn lại', 'type' => 'string', 'example' => '5'],
],
],
];
public function up(): void
{
foreach (self::BACKFILL as $id => $config) {
$template = Template::find($id);
if (!$template) {
continue;
}
$template->scenario = $config['scenario'];
$template->subject = $this->convert((string) $template->subject);
$template->message = $this->convert((string) $template->message);
$metadata = $template->metadata ?? [];
$metadata['variables'] = $config['variables'];
$template->metadata = $metadata;
$template->save();
}
}
public function down(): void
{
foreach (self::BACKFILL as $id => $config) {
$template = Template::find($id);
if (!$template || $template->scenario !== $config['scenario']) {
continue;
}
$template->scenario = null;
$template->subject = $this->revert((string) $template->subject);
$template->message = $this->revert((string) $template->message);
$metadata = $template->metadata ?? [];
unset($metadata['variables']);
$template->metadata = $metadata ?: null;
$template->save();
}
}
private function convert(string $content): string
{
return preg_replace('/\{\$([a-zA-Z_][a-zA-Z0-9_]*)\}/', '{{$1}}', $content) ?? $content;
}
private function revert(string $content): string
{
return preg_replace('/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/', '{\$$1}', $content) ?? $content;
}
};
@@ -0,0 +1,81 @@
<?php
use App\Models\Template;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
// id => variables[] (chỉ set metadata; convert {$var}->{{var}} nếu nội dung còn cú pháp cũ)
private const BACKFILL = [
2 => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'company_logo', 'label' => 'Logo công ty (URL)', 'type' => 'url', 'example' => ''],
['key' => 'product_name', 'label' => 'Tên sản phẩm', 'type' => 'string', 'example' => 'Cloud VPS Pro'],
['key' => 'product_billing', 'label' => 'Chu kỳ thanh toán', 'type' => 'string', 'example' => 'Hàng tháng'],
['key' => 'product_price', 'label' => 'Giá dịch vụ', 'type' => 'string', 'example' => '420.000'],
['key' => 'service_host', 'label' => 'IP máy chủ', 'type' => 'string', 'example' => '203.113.x.x'],
['key' => 'service_username', 'label' => 'Username', 'type' => 'string', 'example' => 'root'],
['key' => 'service_port', 'label' => 'Port', 'type' => 'string', 'example' => '22'],
['key' => 'service_password', 'label' => 'Mật khẩu', 'type' => 'string', 'example' => 'P@ssw0rd!'],
['key' => 'os_name', 'label' => 'Hệ điều hành', 'type' => 'string', 'example' => 'Ubuntu 22.04'],
['key' => 'service_due_exp', 'label' => 'Ngày hết hạn', 'type' => 'string', 'example' => '09/09/2026'],
],
6 => [
['key' => 'company_name', 'label' => 'Tên công ty', 'type' => 'string', 'example' => 'Thế Giới Số'],
['key' => 'domain_name', 'label' => 'Tên miền', 'type' => 'string', 'example' => 'example.vn'],
['key' => 'order_num', 'label' => 'Mã đơn hàng', 'type' => 'string', 'example' => 'DH001'],
['key' => 'order_total', 'label' => 'Tổng tiền', 'type' => 'string', 'example' => '350.000 VNĐ'],
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'type' => 'string', 'example' => 'Nguyễn Văn A'],
['key' => 'customer_email', 'label' => 'Email khách hàng', 'type' => 'string', 'example' => 'a@example.com'],
['key' => 'created_at', 'label' => 'Thời gian', 'type' => 'string', 'example' => '09/09/2026 10:00:00'],
['key' => 'admin_url', 'label' => 'Link xem đơn (admin)', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/admin/orders'],
],
23 => [
['key' => 'hero_image', 'label' => 'Ảnh banner', 'type' => 'url', 'example' => 'https://client.tgs.com.vn/index.php?rp=/images/em/368_30.4-CP.png'],
['key' => 'holiday_name', 'label' => 'Tên ngày lễ', 'type' => 'string', 'example' => 'Tết Dương lịch'],
['key' => 'holiday_from', 'label' => 'Ngày bắt đầu nghỉ', 'type' => 'string', 'example' => 'Thứ Năm, 01/01/2026'],
['key' => 'holiday_to', 'label' => 'Ngày kết thúc nghỉ', 'type' => 'string', 'example' => 'Thứ Năm, 01/01/2026'],
['key' => 'work_again', 'label' => 'Ngày làm việc trở lại', 'type' => 'string', 'example' => 'Thứ Sáu, 02/01/2026'],
['key' => 'ticket_url', 'label' => 'Link gửi ticket', 'type' => 'url', 'example' => 'https://portal.tgs.com.vn/submitticket.php'],
['key' => 'support_email', 'label' => 'Email hỗ trợ', 'type' => 'string', 'example' => 'support@tgs.com.vn'],
['key' => 'hotline', 'label' => 'Hotline hỗ trợ', 'type' => 'string', 'example' => '(028) 7309.7379'],
],
];
public function up(): void
{
foreach (self::BACKFILL as $id => $variables) {
$template = Template::find($id);
if (!$template) {
continue;
}
$template->subject = $this->convert((string) $template->subject);
$template->message = $this->convert((string) $template->message);
$metadata = $template->metadata ?? [];
$metadata['variables'] = $variables;
$template->metadata = $metadata;
$template->save();
}
}
public function down(): void
{
foreach (self::BACKFILL as $id => $variables) {
$template = Template::find($id);
if (!$template) {
continue;
}
$metadata = $template->metadata ?? [];
unset($metadata['variables']);
$template->metadata = $metadata ?: null;
$template->save();
}
}
private function convert(string $content): string
{
return preg_replace('/\{\$([a-zA-Z_][a-zA-Z0-9_]*)\}/', '{{$1}}', $content) ?? $content;
}
};