feat(admin): IP pool allocation, billing exports, service attachments
- IP pool: sheet import (xlsx/csv) matched by address, bulk allocate/revoke with notification email (ip_allocated/ip_revoked), ip_pool_logs history. - Orders/invoices: Excel export on branded template (BrandedReportWriter), shared billing filters, order item service info in list responses. - Web design quote Excel export; activity log export. - Service attachments with copy-to-invoice; deployment invoice attachments. - Customer email attachments stored on disk and attached by path. - Admin password change endpoint; user.fullname backfill migration; orders.user_id backfill from logs. - Disable deploy workflow until CI/CD is set up. - Ignore stored customer email and service attachment uploads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ea5697cb81
commit
46aa138eec
@@ -18,8 +18,6 @@ class UserFactory extends Factory
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
// Bảng `user` dùng một cột `fullname`; firstname/lastname không tồn tại nên mọi
|
||||
// test cần tài khoản admin đều chết ở đây với "Unknown column 'firstname'".
|
||||
'fullname' => 'System Admin',
|
||||
'username' => "admin",
|
||||
'email' => 'admin@tgs.com.vn',
|
||||
|
||||
@@ -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('service_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('service_id')->constrained('services')->cascadeOnDelete();
|
||||
$table->string('filename');
|
||||
$table->string('path');
|
||||
$table->string('disk', 50)->default('public');
|
||||
$table->string('mime_type', 100)->nullable();
|
||||
$table->unsignedBigInteger('size')->default(0);
|
||||
$table->string('uploaded_by', 100)->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('service_attachments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$userIds = DB::table('user')->pluck('id', 'username');
|
||||
|
||||
DB::table('log')
|
||||
->where('description', 'like', '% created new Order with Order id: %')
|
||||
->where('username', '!=', 'System')
|
||||
->orderBy('id')
|
||||
->each(function ($log) use ($userIds) {
|
||||
$userId = $userIds[$log->username] ?? null;
|
||||
if (! $userId || ! preg_match('/Order id: (\d+)$/', $log->description, $match)) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('orders')
|
||||
->where('id', (int) $match[1])
|
||||
->whereNull('user_id')
|
||||
->update(['user_id' => $userId]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void {}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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
|
||||
{
|
||||
if (! Schema::hasColumn('user', 'fullname')) {
|
||||
Schema::table('user', function (Blueprint $table) {
|
||||
$table->string('fullname', 255)->nullable()->after('id');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasColumns('user', ['firstname', 'lastname'])) {
|
||||
DB::table('user')
|
||||
->where(fn ($query) => $query->whereNull('fullname')->orWhere('fullname', ''))
|
||||
->update(['fullname' => DB::raw("TRIM(CONCAT(COALESCE(firstname, ''), ' ', COALESCE(lastname, '')))")]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void {}
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('ip_pools', function (Blueprint $table) {
|
||||
$table->string('ip_block')->nullable()->after('ip_address');
|
||||
$table->unsignedBigInteger('module_network_id')->nullable()->change();
|
||||
});
|
||||
|
||||
DB::table('ip_pools')
|
||||
->where('status', 'free')
|
||||
->whereNull('service_id')
|
||||
->whereRaw("UPPER(TRIM(allocation_status)) = 'ALLOCATED'")
|
||||
->update(['status' => 'assigned']);
|
||||
|
||||
Schema::table('ip_pools', function (Blueprint $table) {
|
||||
$table->dropColumn('allocation_status');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ip_pools', function (Blueprint $table) {
|
||||
$table->string('allocation_status')->nullable()->after('revoked_at');
|
||||
$table->dropColumn('ip_block');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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('ip_pools', function (Blueprint $table) {
|
||||
$table->foreignId('customer_id')->nullable()->after('customer_name')->constrained('customers')->nullOnDelete();
|
||||
});
|
||||
|
||||
Schema::create('ip_pool_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('ip_pool_id')->constrained('ip_pools')->cascadeOnDelete();
|
||||
$table->string('ip_address', 45);
|
||||
$table->string('action', 30)->index();
|
||||
$table->string('status_from', 20)->nullable();
|
||||
$table->string('status_to', 20)->nullable();
|
||||
$table->foreignId('customer_id')->nullable()->constrained('customers')->nullOnDelete();
|
||||
$table->string('customer_name')->nullable();
|
||||
$table->unsignedBigInteger('service_id')->nullable();
|
||||
$table->string('vm_name')->nullable();
|
||||
$table->text('email_to')->nullable();
|
||||
$table->unsignedBigInteger('user_id')->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ip_pool_logs');
|
||||
|
||||
Schema::table('ip_pools', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('customer_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Database\Seeders\TemplateSeeder;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$seeder = new TemplateSeeder();
|
||||
$seeder->seedScenarioIfMissing('ip_allocated');
|
||||
$seeder->seedScenarioIfMissing('ip_revoked');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('template')->whereIn('scenario', ['ip_allocated', 'ip_revoked'])->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Database\Seeders\TemplateSeeder;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
(new TemplateSeeder())->reseedScenario('ip_allocated');
|
||||
}
|
||||
|
||||
public function down(): void {}
|
||||
};
|
||||
+23
@@ -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('ip_pool_logs', function (Blueprint $table) {
|
||||
$table->date('effective_date')->nullable()->after('action');
|
||||
$table->string('service_name')->nullable()->after('customer_name');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ip_pool_logs', function (Blueprint $table) {
|
||||
$table->dropColumn(['effective_date', 'service_name']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -118,29 +118,7 @@ final class BillingTemplates
|
||||
'scenario' => 'domain_order_approved',
|
||||
'name' => 'Bàn giao tên miền',
|
||||
'subject' => 'Bàn giao tên miền {{domain_name}} - {{company_name}}',
|
||||
'body' => T::title('Bàn giao tên miền')
|
||||
. T::greeting('{{client_name}}')
|
||||
. T::paragraph('Công ty Cổ phần Giải pháp Công nghệ Thế Giới Số xin chân thành cảm ơn Quý khách đã tin tưởng lựa chọn dịch vụ đăng ký và quản lý tên miền của chúng tôi.')
|
||||
. T::heading('🌐 Thông tin tên miền')
|
||||
. T::infoTable([
|
||||
'Tên miền' => '{{domain_name}}',
|
||||
'Mã đơn hàng' => '{{order_num}}',
|
||||
'Ngày đăng ký' => '{{registered_at}}',
|
||||
'Ngày hết hạn' => '{{expires_at}}',
|
||||
'NS1' => '{{name_server_1}}',
|
||||
'NS2' => '{{name_server_2}}',
|
||||
])
|
||||
. T::heading('🔐 Thông tin tài khoản quản lý')
|
||||
. T::infoTable([
|
||||
'Trang đăng nhập' => '<a href="{{login_url}}" target="_blank" rel="noopener">{{login_url}}</a>',
|
||||
'Tên đăng nhập' => '{{customer_email}}',
|
||||
'Mật khẩu' => 'Mật khẩu tài khoản portal của Quý khách',
|
||||
])
|
||||
. T::note('Quý khách vui lòng bảo mật thông tin tài khoản. Nếu quên mật khẩu, sử dụng chức năng "Quên mật khẩu" tại trang đăng nhập.')
|
||||
. '{{kyc_block}}'
|
||||
. T::heading('📞 Hỗ trợ kỹ thuật 24/7')
|
||||
. T::supportLine()
|
||||
. T::paragraph('Một lần nữa, Thế Giới Số xin chân thành cảm ơn sự tin tưởng và đồng hành của Quý khách.'),
|
||||
'body' => self::domainOrderApprovedBody(),
|
||||
],
|
||||
[
|
||||
'scenario' => 'domain_order_rejected',
|
||||
@@ -160,4 +138,33 @@ final class BillingTemplates
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function domainOrderApprovedBody(): string
|
||||
{
|
||||
return <<<'HTML'
|
||||
<p style="font-size:15px;color:#1f2937;margin:0 0 8px">Kính gửi Quý Khách <strong>{{client_name}}</strong>,</p>
|
||||
<p style="font-size:14px;color:#4b5563;line-height:1.6;margin:0 0 24px">Công ty Cổ phần Giải pháp Công nghệ Thế Giới Số xin chân thành cảm ơn Quý Khách đã tin tưởng lựa chọn dịch vụ đăng ký và quản lý tên miền của chúng tôi.</p>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:12px"><tbody><tr><td style="font-size:15px;font-weight:bold;color:#f97316;padding-bottom:10px;border-bottom:2px solid #fed7aa">🌐 I. THÔNG TIN TÊN MIỀN</td></tr></tbody></table>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e5e7eb;border-radius:6px;overflow:hidden;margin-bottom:24px;font-size:12.5px"><tbody>
|
||||
<tr style="background:#fed7aa"><td style="padding:9px 10px;color:#9a3412;font-weight:bold;text-align:center">Tên miền</td><td style="padding:9px 10px;color:#9a3412;font-weight:bold;text-align:center">Ngày đăng ký</td><td style="padding:9px 10px;color:#9a3412;font-weight:bold;text-align:center">Ngày hết hạn</td><td style="padding:9px 10px;color:#9a3412;font-weight:bold;text-align:center">NS1</td><td style="padding:9px 10px;color:#9a3412;font-weight:bold;text-align:center">NS2</td></tr>
|
||||
<tr style="background:#fff7ed"><td style="padding:9px 10px;color:#111827;font-weight:bold;text-align:center">{{domain_name}}</td><td style="padding:9px 10px;color:#111827;text-align:center">{{registered_at}}</td><td style="padding:9px 10px;color:#9a3412;font-weight:bold;text-align:center">{{expires_at}}</td><td style="padding:9px 10px;color:#111827;text-align:center">{{name_server_1}}</td><td style="padding:9px 10px;color:#111827;text-align:center">{{name_server_2}}</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:12px"><tbody><tr><td style="font-size:15px;font-weight:bold;color:#f97316;padding-bottom:10px;border-bottom:2px solid #fed7aa">🔐 II. THÔNG TIN TÀI KHOẢN QUẢN LÝ</td></tr></tbody></table>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;margin-bottom:12px"><tbody><tr><td style="padding:16px"><p style="margin:0 0 8px;font-size:13px;color:#374151">🔗 <strong>Trang đăng nhập:</strong> <a href="{{login_url}}" style="color:#c2410c" target="_blank" rel="noopener">{{login_url}}</a></p><p style="margin:0 0 8px;font-size:13px;color:#374151">👤 <strong>Tên đăng nhập:</strong> {{customer_email}}</p><p style="margin:0;font-size:13px;color:#374151">🔑 <strong>Mật khẩu:</strong> Mật khẩu tài khoản portal của Quý Khách</p></td></tr></tbody></table>
|
||||
<p style="font-size:12.5px;color:#065f46;background:#ecfdf5;border-left:3px solid #6ee7b7;padding:10px 14px;margin:0 0 24px;border-radius:4px">🔒 Quý Khách vui lòng đổi mật khẩu ngay sau lần đăng nhập đầu tiên và bảo mật thông tin tài khoản.</p>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:12px"><tbody><tr><td style="font-size:15px;font-weight:bold;color:#f97316;padding-bottom:10px;border-bottom:2px solid #fed7aa">📋 III. LƯU Ý QUAN TRỌNG – XÁC THỰC TÊN MIỀN</td></tr></tbody></table>
|
||||
<p style="font-size:13.5px;color:#4b5563;line-height:1.7;margin:0 0 16px">Quý Khách cần hoàn tất thủ tục xác thực tên miền theo hướng dẫn để tránh tình trạng gián đoạn dịch vụ do chưa hoàn tất xác thực.</p>
|
||||
{{kyc_block}}
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#ecfdf5;border:1px solid #a7f3d0;border-radius:6px;margin:0 0 24px"><tbody><tr><td style="padding:16px"><p style="margin:0 0 8px;font-size:13px;color:#065f46;font-weight:bold">📮 Bản gốc vui lòng gửi về:</p><p style="margin:0 0 4px;font-size:13px;color:#374151">CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ</p><p style="margin:0 0 4px;font-size:13px;color:#374151">128/38 Tân Hương, Phường Phú Thọ Hòa, TP. Hồ Chí Minh</p><p style="margin:0;font-size:13px;color:#374151">Người nhận: Ms. Vân Anh – 0818 006 119 (P. HCNS)</p></td></tr></tbody></table>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:12px"><tbody><tr><td style="font-size:15px;font-weight:bold;color:#f97316;padding-bottom:10px;border-bottom:2px solid #fed7aa">📞 IV. HỖ TRỢ KỸ THUẬT 24/7</td></tr></tbody></table>
|
||||
<p style="font-size:13.5px;color:#374151;margin:0 0 4px">☎ <strong>Hotline:</strong> (028) 7309 7379</p>
|
||||
<p style="font-size:13.5px;color:#374151;margin:0 0 24px">✉ <strong>Email:</strong> <a href="mailto:support@tgs.com.vn" style="color:#c2410c">support@tgs.com.vn</a></p>
|
||||
<p style="font-size:14px;color:#4b5563;line-height:1.6;margin:0">Một lần nữa, Thế Giới Số xin chân thành cảm ơn sự tin tưởng và đồng hành của Quý Khách. Chúng tôi luôn sẵn sàng hỗ trợ để đảm bảo trải nghiệm dịch vụ tốt nhất.</p>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\EmailTemplates;
|
||||
|
||||
use App\Support\EmailTemplateShell as T;
|
||||
|
||||
final class IpTemplates
|
||||
{
|
||||
public const SERIF = "font-family: 'Times New Roman', Times, serif;";
|
||||
|
||||
public const ORANGE = '#f26522';
|
||||
|
||||
/**
|
||||
* @return array<int, array{scenario: string, name: string, subject: string, body: string}>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'scenario' => 'ip_allocated',
|
||||
'name' => 'Cấp phát địa chỉ IP',
|
||||
'subject' => 'Thông báo cấp phát {{ip_count}} địa chỉ IP',
|
||||
'body' => '<div style="' . self::SERIF . ' color: #2b2b2b; font-size: 16px; line-height: 1.85;">'
|
||||
. '<div style="text-align: center;">'
|
||||
. '<div style="font-size: 21px; font-weight: bold; letter-spacing: 1px; color: #2b2b2b;">THÔNG BÁO CẤP PHÁT TÀI NGUYÊN IP</div>'
|
||||
. '<table style="margin: 10px auto 0;" role="presentation" cellspacing="0" cellpadding="0" align="center"><tbody><tr><td style="width: 60px; height: 2px; background: ' . self::ORANGE . '; font-size: 0; line-height: 0;"> </td></tr></tbody></table>'
|
||||
. '<div style="font-size: 13px; color: #888888; margin-top: 10px; font-style: italic;">TP. Hồ Chí Minh, {{issued_on}}</div>'
|
||||
. '</div>'
|
||||
. '<p style="margin: 26px 0 14px;">Kính gửi Quý Khách hàng <b>{{client_name}}</b>,</p>'
|
||||
. '<p style="margin: 0 0 20px;">Thế Giới Số trân trọng gửi thông tin cấp phát tài nguyên IP phục vụ triển khai dịch vụ.</p>'
|
||||
. '<table style="margin: 0 0 10px;" role="presentation" width="100%" cellspacing="0" cellpadding="0"><tbody><tr>'
|
||||
. '<td style="' . self::SERIF . ' font-size: 15px; font-weight: bold; color: #2b2b2b;"><span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: ' . self::ORANGE . '; margin-right: 8px;"></span>Thông tin IP</td>'
|
||||
. '<td align="right"><span style="background: #fde6d6; color: #c2410c; font-size: 12px; font-weight: bold; padding: 4px 12px; border-radius: 999px; font-family: Arial, sans-serif;">● Đã cấp phát</span></td>'
|
||||
. '</tr></tbody></table>'
|
||||
. '{{ip_table}}'
|
||||
. '<div style="margin-top: 24px;">'
|
||||
. '<div style="margin-bottom: 6px;">Thế Giới Số xin chân thành cảm ơn Quý Khách hàng đã tin tưởng lựa chọn dịch vụ của chúng tôi.</div>'
|
||||
. '<div style="margin-bottom: 6px;">Trong quá trình sử dụng, nếu cần hỗ trợ kỹ thuật, Quý Khách hàng vui lòng liên hệ Thế Giới Số qua các kênh hỗ trợ bên dưới.</div>'
|
||||
. '<div>Chúc Quý Khách hàng sử dụng dịch vụ hiệu quả và thành công.</div>'
|
||||
. '</div>'
|
||||
. '{{note_block}}'
|
||||
. '<table style="margin: 22px 0 8px;" role="presentation" width="100%" cellspacing="0" cellpadding="0"><tbody><tr><td> </td>'
|
||||
. '<td style="' . self::SERIF . ' font-size: 15px; line-height: 1.7; color: #2b2b2b;" width="300" align="center">'
|
||||
. '<div>Trân trọng,</div><div style="font-weight: bold; margin-top: 4px;">BỘ PHẬN KỸ THUẬT</div><div style="color: #d9480f; font-weight: bold;">THẾ GIỚI SỐ</div>'
|
||||
. '</td></tr></tbody></table>'
|
||||
. '</div>',
|
||||
],
|
||||
[
|
||||
'scenario' => 'ip_revoked',
|
||||
'name' => 'Thu hồi địa chỉ IP',
|
||||
'subject' => 'Thông báo thu hồi {{ip_count}} địa chỉ IP',
|
||||
'body' => T::title('Thông báo thu hồi địa chỉ IP')
|
||||
. T::greeting('{{client_name}}')
|
||||
. T::paragraph('<strong>{{app_name}}</strong> xin thông báo sẽ thu hồi <strong>{{ip_count}}</strong> địa chỉ IP dưới đây kể từ ngày <strong>{{revoked_at}}</strong>. Quý khách vui lòng ngừng sử dụng các địa chỉ IP này để tránh gián đoạn dịch vụ.')
|
||||
. T::heading('Danh sách địa chỉ IP')
|
||||
. T::infoTable([], '{{ip_rows}}')
|
||||
. '{{note_block}}'
|
||||
. T::supportLine(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,39 @@ final class MarketingTemplates
|
||||
. T::button('https://tgs.com.vn/premium-hosting/', 'Xem bảng giá'),
|
||||
'variables' => [],
|
||||
],
|
||||
[
|
||||
'name' => 'Thông báo bổ sung hồ sơ bản khai tên miền',
|
||||
'subject' => 'V/v: Thế Giới Số thông báo bổ sung hồ sơ bản khai tên miền của Quý Công ty - {{customer_name}}',
|
||||
'body' => self::domainProfileSupplementBody(),
|
||||
'variables' => [
|
||||
['key' => 'customer_name', 'label' => 'Tên công ty', 'example' => 'CÔNG TY TNHH KHÁCH HÀNG MẪU'],
|
||||
['key' => 'domain_count', 'label' => 'Số lượng tên miền', 'example' => '1'],
|
||||
['key' => 'domain_name', 'label' => 'Tên miền cần bổ sung hồ sơ', 'example' => 'example.vn'],
|
||||
['key' => 'declaration_form_url', 'label' => 'Link tải mẫu bản khai', 'example' => 'https://example.com/mau-ban-khai'],
|
||||
['key' => 'digital_signature_url', 'label' => 'Link xác thực chữ ký số', 'example' => 'https://example.com/xac-thuc-chu-ky-so'],
|
||||
['key' => 'supplement_deadline', 'label' => 'Hạn bổ sung hồ sơ', 'example' => '31/12/2026'],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function domainProfileSupplementBody(): string
|
||||
{
|
||||
return <<<'HTML'
|
||||
<p style="box-sizing:border-box;margin:0 0 16px;font-size:15px;color:#1f2937">Kính gửi Quý Công ty <strong>{{customer_name}}</strong>,</p>
|
||||
<p style="box-sizing:border-box;margin:0 0 16px;color:#4b5563;line-height:1.6">Thế Giới Số trân trọng cảm ơn Quý Công ty đã tin tưởng sử dụng dịch vụ của chúng tôi.</p>
|
||||
<p style="box-sizing:border-box;margin:0 0 12px;color:#4b5563;line-height:1.6">Với mục đích giúp Quý Công ty xác định quyền sở hữu hợp pháp đối với tên miền và hạn chế rủi ro vi phạm quy định pháp luật về quản lý tài nguyên Internet, quy định cụ thể tại:</p>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="box-sizing:border-box;background:#fff7ed;border-left:3px solid #f97316;border-radius:4px;margin-bottom:20px"><tbody><tr><td style="padding:14px 16px"><p style="margin:0 0 8px;font-size:13px;color:#374151;line-height:1.6">- Nghị định 147/2024/NĐ-CP ngày 09/11/2024 của Chính phủ về quản lý, cung cấp, sử dụng dịch vụ Internet và thông tin trên mạng.</p><p style="margin:0;font-size:13px;color:#374151;line-height:1.6">- Thông tư 48/2025/TT-BKHCN ngày 25/12/2025 của Bộ Khoa học và Công nghệ hướng dẫn về quản lý và sử dụng tài nguyên Internet.</p></td></tr></tbody></table>
|
||||
<p style="box-sizing:border-box;margin:0 0 16px;color:#1f2937;line-height:1.6">Hệ thống Thế Giới Số ghi nhận Quý Công ty hiện có <strong>{{domain_count}} tên miền</strong> chưa đủ hồ sơ để xác định quyền sở hữu hợp pháp theo quy định, cụ thể như sau:</p>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="box-sizing:border-box;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden;margin-bottom:20px;font-size:13px"><tbody><tr style="background:#fed7aa"><td style="padding:9px 12px;color:#9a3412;font-weight:bold;width:15%;text-align:center">STT</td><td style="padding:9px 12px;color:#9a3412;font-weight:bold;text-align:center">Dịch vụ</td></tr><tr><td style="padding:9px 12px;color:#111827;text-align:center">1</td><td style="padding:9px 12px;color:#111827;font-weight:bold;text-align:center">{{domain_name}}</td></tr></tbody></table>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="box-sizing:border-box;background:#f8fafc;border:1px solid #cbd5e1;border-radius:8px;margin-bottom:24px"><tbody><tr><td style="padding:18px"><p style="margin:0 0 12px;color:#1e3a8a;font-weight:bold">📎 Hoàn tất xác thực Tổ chức – chọn 1 trong 2 cách sau</p>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="box-sizing:border-box;background:#fff;border:1px solid #bfdbfe;border-left:4px solid #1e40af;border-radius:6px"><tbody><tr><td style="padding:14px 16px"><p style="margin:0 0 10px;color:#1e3a8a;font-weight:bold">CÁCH 1 | Hoàn thiện Bản khai (bản giấy)</p><p style="margin:0 0 10px;font-size:13px;color:#374151;line-height:1.6">Tải mẫu <strong>Bản khai</strong>, in, ký tên, đóng dấu (và đóng dấu giáp lai nếu có nhiều trang), sau đó chụp ảnh hoặc scan và phản hồi email này.</p><a href="{{declaration_form_url}}" style="display:inline-block;color:#fff;background:#1e40af;padding:7px 16px;border-radius:4px;text-decoration:none;font-size:12px;font-weight:bold" target="_blank" rel="noopener">Tải mẫu Bản khai →</a><p style="margin:16px 0 0;font-size:13px;color:#374151;line-height:1.7"><strong>Gửi bản gốc đã ký, đóng dấu về:</strong><br>CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ<br>128/38 Tân Hương, Phường Phú Thọ Hòa, TP. Hồ Chí Minh<br>Người nhận: Ms. Vân Anh – 0818 006 119 (P. HCNS)</p></td></tr></tbody></table>
|
||||
<div style="padding:8px 0;text-align:center"><span style="display:inline-block;font-size:11.5px;color:#6b7280;font-weight:bold;letter-spacing:1px;background:#f1f5f9;border-radius:10px;padding:2px 14px">HOẶC</span></div>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="box-sizing:border-box;background:#fff;border:1px solid #a7f3d0;border-left:4px solid #059669;border-radius:6px"><tbody><tr><td style="padding:14px 16px"><p style="margin:0 0 8px;color:#065f46;font-weight:bold">CÁCH 2 | Xác thực Tổ chức bằng chữ ký số</p><p style="margin:0 0 10px;font-size:13px;color:#374151">Xác thực Tổ chức trực tuyến bằng chữ ký số.</p><a href="{{digital_signature_url}}" style="display:inline-block;color:#fff;background:#059669;padding:9px 20px;border-radius:4px;text-decoration:none;font-size:12.5px;font-weight:bold" target="_blank" rel="noopener">Xác thực Tổ chức bằng chữ ký số →</a></td></tr></tbody></table>
|
||||
</td></tr></tbody></table>
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="box-sizing:border-box;background:#fef2f2;border:1px solid #fca5a5;border-radius:6px;margin-bottom:20px"><tbody><tr><td style="padding:16px"><p style="margin:0 0 10px;color:#991b1b;font-weight:bold">Lưu ý:</p><p style="margin:0 0 10px;font-size:13px;color:#7f1d1d;line-height:1.7">Quý Công ty vui lòng hoàn tất trước ngày <strong>{{supplement_deadline}}</strong> để chúng tôi cập nhật quyền sở hữu hợp pháp đối với tên miền tới hệ thống Trung tâm Internet Việt Nam, tránh ảnh hưởng đến quyền lợi của chủ thể sử dụng tên miền.</p><p style="margin:0 0 4px 20px;font-size:13px;color:#7f1d1d">- Gián đoạn sử dụng tên miền.</p><p style="margin:0 0 4px 20px;font-size:13px;color:#7f1d1d">- Ảnh hưởng đến các nghiệp vụ duy trì tên miền.</p><p style="margin:0 0 12px 20px;font-size:13px;color:#7f1d1d">- Bất lợi khi xảy ra tranh chấp hoặc có thể bị thu hồi tên miền.</p><p style="margin:0;font-size:13px;color:#7f1d1d;font-style:italic">Nếu Quý Công ty đã hoàn tất việc bổ sung hồ sơ, xin vui lòng bỏ qua email này.</p></td></tr></tbody></table>
|
||||
<p style="box-sizing:border-box;margin:0 0 20px;color:#4b5563;line-height:1.6">Trong suốt quá trình thực hiện, nếu cần hỗ trợ, đội ngũ chuyên viên Thế Giới Số luôn sẵn sàng đồng hành cùng Quý Công ty.</p>
|
||||
<p style="box-sizing:border-box;margin:0;color:#1f2937">Trân trọng,</p>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,13 @@ final class TemplateVariables
|
||||
'price_row' => ['[Tự động] Dòng số tiền gia hạn', 'html', ''],
|
||||
'invoice_url' => ['Link đơn hàng', 'url', 'https://tgs.com.vn/don-hang'],
|
||||
'ip' => ['Địa chỉ IP', 'string', '103.0.0.10'],
|
||||
'ip_count' => ['Số địa chỉ IP', 'string', '2'],
|
||||
'ip_rows' => ['[Tự động] Danh sách địa chỉ IP', 'html', ''],
|
||||
'ip_table' => ['[Tự động] Bảng IP cấp phát (STT, IP, dịch vụ, ngày)', 'html', ''],
|
||||
'issued_on' => ['Ngày thông báo (ngày … tháng … năm …)', 'string', 'ngày 15 tháng 10 năm 2026'],
|
||||
'allocated_at' => ['Ngày cấp phát', 'string', '15/10/2026'],
|
||||
'revoked_at' => ['Ngày thu hồi', 'string', '31/10/2026'],
|
||||
'note_block' => ['[Tự động] Ghi chú', 'html', ''],
|
||||
'service_url' => ['Link dịch vụ', 'url', 'https://tgs.com.vn/dich-vu'],
|
||||
'new_password_row' => ['[Tự động] Dòng mật khẩu mới', 'html', ''],
|
||||
'snapshot_name' => ['Tên snapshot', 'string', 'backup-2026-10-08'],
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Support\MailPlaceholders;
|
||||
use Database\Seeders\EmailTemplates\AccountTemplates;
|
||||
use Database\Seeders\EmailTemplates\AdminTemplates;
|
||||
use Database\Seeders\EmailTemplates\BillingTemplates;
|
||||
use Database\Seeders\EmailTemplates\IpTemplates;
|
||||
use Database\Seeders\EmailTemplates\LegacyTemplates;
|
||||
use Database\Seeders\EmailTemplates\MarketingTemplates;
|
||||
use Database\Seeders\EmailTemplates\ServiceTemplates;
|
||||
@@ -25,6 +26,8 @@ class TemplateSeeder extends Seeder
|
||||
foreach (MarketingTemplates::all() as $item) {
|
||||
$this->createMarketingIfMissing($item);
|
||||
}
|
||||
|
||||
$this->createMarketingIfMissing(self::ipRevocationTemplate());
|
||||
}
|
||||
|
||||
public function seedScenarioIfMissing(string $scenario): void
|
||||
@@ -40,6 +43,15 @@ class TemplateSeeder extends Seeder
|
||||
}
|
||||
}
|
||||
|
||||
public function reseedScenario(string $scenario): void
|
||||
{
|
||||
foreach (self::templates() as $item) {
|
||||
if ($item['scenario'] === $scenario) {
|
||||
$this->upsert($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createMarketingIfMissing(array $item): void
|
||||
{
|
||||
$template = Template::firstOrNew(['name' => $item['name']]);
|
||||
@@ -51,7 +63,7 @@ class TemplateSeeder extends Seeder
|
||||
'type_id' => 0,
|
||||
'format' => 'html',
|
||||
'subject' => $item['subject'],
|
||||
'message' => EmailTemplateShell::page($item['body']),
|
||||
'message' => $item['message'] ?? EmailTemplateShell::page($item['body']),
|
||||
'status' => 1,
|
||||
]);
|
||||
$template->syncVariables($item['variables']);
|
||||
@@ -75,6 +87,7 @@ class TemplateSeeder extends Seeder
|
||||
[
|
||||
...AccountTemplates::all(),
|
||||
...ServiceTemplates::all(),
|
||||
...IpTemplates::all(),
|
||||
...BillingTemplates::all(),
|
||||
...AdminTemplates::all(),
|
||||
],
|
||||
@@ -94,6 +107,43 @@ class TemplateSeeder extends Seeder
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Template mẫu thông báo danh sách tài nguyên IP đã thu hồi.
|
||||
*
|
||||
* @return array{name: string, subject: string, message: string, variables: array{}}
|
||||
*/
|
||||
private static function ipRevocationTemplate(): array
|
||||
{
|
||||
$subject = 'V/v: Thế Giới Số gửi thông tin Thu hồi IP — {{service_name}} — {{customer_name}}';
|
||||
|
||||
$message = <<<'HTML'
|
||||
<div style="margin:0;padding:24px 10px;background:#f2f1ef;font-family:'Times New Roman',Times,serif;color:#2b2b2b">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" align="center" style="max-width:820px;margin:0 auto;background:#fff;border-radius:14px;border-collapse:separate;overflow:hidden">
|
||||
<tbody><tr><td><table width="100%" cellpadding="0" cellspacing="0" border="0"><tbody><tr><td width="70%" style="height:5px;background:#a8a29e;font-size:0;line-height:0"> </td><td width="30%" style="height:5px;background:#f26522;font-size:0;line-height:0"> </td></tr></tbody></table></td></tr>
|
||||
<tr><td style="padding:26px 40px 0;text-align:center"><img src="https://tgs.com.vn/wp-content/uploads/2024/11/cropped-LOGO-THE-GIOI-SO-2024-2.png" width="200" style="display:inline-block;border:0;max-width:200px;height:auto" alt="Thế Giới Số"></td></tr>
|
||||
<tr><td style="padding:14px 40px 0;text-align:center"><div style="font-size:21px;font-weight:bold;letter-spacing:1px">THÔNG BÁO THU HỒI TÀI NGUYÊN IP</div><table align="center" cellpadding="0" cellspacing="0" border="0" style="margin:10px auto 0"><tbody><tr><td style="width:30px;height:2px;background:#a8a29e;font-size:0"> </td><td style="width:4px;font-size:0"> </td><td style="width:30px;height:2px;background:#f26522;font-size:0"> </td></tr></tbody></table><div style="font-size:13px;color:#888;margin-top:10px;font-style:italic">{{notification_date}}</div></td></tr>
|
||||
<tr><td style="padding:26px 40px 6px;font-size:16px;line-height:1.85"><p style="margin:0 0 14px">Kính gửi Quý Khách hàng <b>{{customer_name}}</b>,</p><p style="margin:0 0 20px">Thế Giới Số trân trọng thông báo thông tin thu hồi tài nguyên IP theo yêu cầu vận hành/dịch vụ.</p>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:0 0 10px"><tbody><tr><td style="font-size:15px;font-weight:bold"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#a8a29e;margin-right:8px"></span>Thông tin IP</td><td align="right"><span style="background:#efedea;color:#5f5b57;font:700 12px Arial,sans-serif;padding:4px 12px;border-radius:999px">● Đã thu hồi</span></td></tr></tbody></table>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border:1px solid #e4e1de;border-radius:10px;border-collapse:separate;overflow:hidden"><tbody><tr><td style="padding:11px 14px;color:#5f5b57;font-size:13px;font-weight:bold;border-bottom:1px solid #e4e1de;background:#f7f6f4;text-align:center;width:60px">STT</td><td style="padding:11px 14px;color:#5f5b57;font-size:13px;font-weight:bold;border-bottom:1px solid #e4e1de;background:#f7f6f4;text-align:center">IP Address</td><td style="padding:11px 14px;color:#5f5b57;font-size:13px;font-weight:bold;border-bottom:1px solid #e4e1de;background:#f7f6f4;text-align:center">Dịch vụ</td><td style="padding:11px 14px;color:#5f5b57;font-size:13px;font-weight:bold;border-bottom:1px solid #e4e1de;background:#f7f6f4;text-align:center">Ngày thu hồi</td></tr><tr><td style="padding:13px 14px;font-size:15px;text-align:center;width:60px">1</td><td style="padding:13px 14px;font-size:15px;text-align:center;font-family:Consolas,'Courier New',monospace;font-weight:bold;color:#3a3633">{{ip_address}}</td><td style="padding:13px 14px;font-size:15px;text-align:center">{{service_name}}</td><td style="padding:13px 14px;font-size:15px;text-align:center">{{revocation_date}}</td></tr></tbody></table>
|
||||
<div style="font-size:16px;line-height:1.85;margin-top:24px"><div style="margin-bottom:6px">Thế Giới Số xin chân thành cảm ơn Quý Khách hàng đã tin tưởng và đồng hành cùng chúng tôi trong thời gian qua.</div><div style="margin-bottom:6px">Khi có nhu cầu <b>sử dụng lại dịch vụ</b> hoặc <b>cấp mới tài nguyên IP</b>,<br>Quý Khách hàng vui lòng liên hệ Thế Giới Số qua các kênh hỗ trợ sau.</div><div>Rất mong tiếp tục được phục vụ Quý Khách hàng trong thời gian tới.</div></div>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin:22px 0 24px"><tbody><tr><td></td><td width="300" align="center" style="font-size:15px;line-height:1.7"><div>Trân trọng,</div><div style="font-weight:bold;margin-top:4px">BỘ PHẬN KỸ THUẬT</div><div style="color:#d9480f;font-weight:bold">THẾ GIỚI SỐ</div></td></tr></tbody></table></td></tr>
|
||||
<tr><td style="border-top:1px solid #f26522;padding:18px 32px 22px;text-align:center;line-height:1.8"><div style="color:#d9480f;font-size:19px;font-weight:bold">CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ</div><div style="color:#f07022;font-size:12px">✆ Tel: (028) 7309 7379 | ✉ <a href="mailto:Support@tgs.com.vn" style="color:#f07022;text-decoration:none">Support@tgs.com.vn</a></div><div style="font-size:12px">🌐 <a href="https://www.TGS.com.vn" style="color:#1a73e8;text-decoration:none">www.TGS.com.vn</a> <span style="color:#1a73e8">&</span> <a href="https://www.thegioiso.CLOUD" style="color:#1a73e8;text-decoration:none">www.thegioiso.CLOUD</a> <span style="color:#1a73e8">&</span> <a href="https://www.thegioiSERVER.com" style="color:#1a73e8;text-decoration:none">www.thegioiSERVER.com</a> <span style="color:#1a73e8">&</span> <a href="https://www.CloudEmail.vn" style="color:#1a73e8;text-decoration:none">www.CloudEmail.vn</a></div><div style="color:#f07022;font-size:12px;font-weight:bold">🥇 Thế Giới Số 🥇 - Premium HOSTING • CLOUD • SERVER & WEBSITE | 15+ năm Phụng Sự 🚀</div></td></tr></tbody></table></div>
|
||||
HTML;
|
||||
|
||||
return [
|
||||
'name' => 'Thông báo thu hồi tài nguyên IP',
|
||||
'subject' => $subject,
|
||||
'message' => $message,
|
||||
'variables' => [
|
||||
['key' => 'customer_name', 'label' => 'Tên khách hàng', 'example' => 'CÔNG TY TNHH KHÁCH HÀNG MẪU'],
|
||||
['key' => 'service_name', 'label' => 'Tên dịch vụ', 'example' => 'Dịch vụ IP'],
|
||||
['key' => 'notification_date', 'label' => 'Ngày lập thông báo', 'example' => 'TP. Hồ Chí Minh, ngày 15 tháng 10 năm 2026'],
|
||||
['key' => 'ip_address', 'label' => 'Địa chỉ IP', 'example' => '192.0.2.1'],
|
||||
['key' => 'revocation_date', 'label' => 'Ngày thu hồi', 'example' => '15/10/2026'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function upsert(array $item): void
|
||||
{
|
||||
$scenario = $item['scenario'];
|
||||
|
||||
Reference in New Issue
Block a user