feat: email gửi tay/nhắc gia hạn, gói hosting, sửa dữ liệu sản phẩm
- Gửi email theo template cho 1 khách, tab lịch sử email, nhắc gia hạn dịch vụ hàng loạt - Mail lưu from/bcc/sent_by; mẫu MARKETING và mẫu cũ trong TemplateSeeder - Gói hosting: giới hạn, trạng thái, lấy plan Plesk qua CLI gateway - Backfill payment_method và ngày đơn hàng còn thiếu - Bổ sung field CPU/RAM/Disk/Băng thông cho loại sản phẩm vps và cloud Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8651dde6b1
commit
30898ea46f
@@ -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('mail', function (Blueprint $table) {
|
||||
$table->string('from_address', 255)->nullable()->after('subject');
|
||||
$table->string('bcc', 1000)->nullable()->after('cc');
|
||||
$table->unsignedBigInteger('sent_by')->nullable()->index()->after('customer_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mail', function (Blueprint $table) {
|
||||
$table->dropIndex(['sent_by']);
|
||||
$table->dropColumn(['from_address', 'bcc', 'sent_by']);
|
||||
});
|
||||
}
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('service_plans', function (Blueprint $table) {
|
||||
$table->text('description')->nullable()->after('title');
|
||||
$table->unsignedInteger('bandwidth')->nullable()->after('domain');
|
||||
$table->unsignedInteger('databases')->nullable()->after('bandwidth');
|
||||
$table->unsignedInteger('mailboxes')->nullable()->after('databases');
|
||||
$table->unsignedInteger('mailbox_quota')->nullable()->after('mailboxes');
|
||||
$table->unsignedInteger('ftp_accounts')->nullable()->after('mailbox_quota');
|
||||
$table->unsignedInteger('subdomains')->nullable()->after('ftp_accounts');
|
||||
$table->unsignedInteger('domain_aliases')->nullable()->after('subdomains');
|
||||
$table->json('features')->nullable()->after('domain_aliases');
|
||||
$table->boolean('is_active')->default(true)->after('features');
|
||||
$table->unsignedInteger('sort_order')->default(0)->after('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('service_plans', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'description', 'bandwidth', 'databases', 'mailboxes', 'mailbox_quota',
|
||||
'ftp_accounts', 'subdomains', 'domain_aliases', 'features', 'is_active', 'sort_order',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('orders')
|
||||
->whereNull('payment_method')
|
||||
->whereExists(function ($query) {
|
||||
$query->selectRaw('1')
|
||||
->from('invoices')
|
||||
->whereColumn('invoices.order_id', 'orders.id')
|
||||
->whereNotNull('invoices.payment_method');
|
||||
})
|
||||
->update([
|
||||
'payment_method' => DB::raw('(SELECT invoices.payment_method FROM invoices WHERE invoices.order_id = orders.id AND invoices.payment_method IS NOT NULL ORDER BY invoices.id ASC LIMIT 1)'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Preserve recovered payment data. The application remains compatible
|
||||
// because orders.payment_method is still the original nullable string.
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('orders')
|
||||
->whereNull('date')
|
||||
->whereNotNull('created_at')
|
||||
->update(['date' => DB::raw('DATE(created_at)')]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Preserve recovered order dates. The date column remains nullable and
|
||||
// compatible with older application versions after a code rollback.
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private const TYPES = ['vps', 'cloud'];
|
||||
|
||||
private const FIELDS = [
|
||||
['key' => 'cpu', 'label' => 'CPU (vCPU)', 'sort_order' => 1],
|
||||
['key' => 'ram', 'label' => 'RAM (GB)', 'sort_order' => 2],
|
||||
['key' => 'disk', 'label' => 'Disk (GB)', 'sort_order' => 3],
|
||||
['key' => 'bandwidth', 'label' => 'Băng thông (GB)', 'sort_order' => 4],
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$now = now();
|
||||
|
||||
foreach (self::TYPES as $type) {
|
||||
foreach (self::FIELDS as $field) {
|
||||
$exists = DB::table('product_type_fields')
|
||||
->where('product_type', $type)
|
||||
->where('key', $field['key'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('product_type_fields')->insert($field + [
|
||||
'product_type' => $type,
|
||||
'field_type' => 'number',
|
||||
'is_required' => false,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Mail;
|
||||
use App\Models\Product;
|
||||
use App\Models\Service;
|
||||
use App\Models\Setting as SettingModel;
|
||||
use App\Models\Template;
|
||||
use App\Models\User;
|
||||
use App\Support\EmailTemplateShell as Shell;
|
||||
use Database\Seeders\EmailTemplates\MarketingTemplates;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Dữ liệu thử các luồng gửi email của admin (gửi 1 khách, email marketing, nhắc gia hạn, tab Email).
|
||||
* Chạy trên DB trống: php artisan migrate:fresh --seed --seeder=EmailScenarioDemoSeeder
|
||||
*/
|
||||
class EmailScenarioDemoSeeder extends Seeder
|
||||
{
|
||||
private const CUSTOMER_PASSWORD = '!admin@123!@#';
|
||||
|
||||
private const CUSTOMERS = [
|
||||
'pbao.dev@gmail.com' => ['Phạm Bảo (dev)', '0900000001', [['vps', 2], ['email', 25], ['vps', -4]]],
|
||||
'pbao.business@gmail.com' => ['Phạm Bảo Business', '0900000002', [['email', 5], ['server', 45]]],
|
||||
'phamgiabao48756@gmail.com' => ['Phạm Gia Bảo', '0900000003', [['website', 12], ['vps', 75]]],
|
||||
'pbao.drive@gmail.com' => ['Phạm Bảo Drive', '0900000004', [['server', 0], ['support', 150], ['email', 9, 'suspended']]],
|
||||
];
|
||||
|
||||
private const PRODUCT_KEYWORDS = [
|
||||
'vps' => 'VPS',
|
||||
'email' => 'Email',
|
||||
'server' => 'Máy Chủ',
|
||||
'website' => 'Website',
|
||||
'support' => 'IT Support',
|
||||
];
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
RoleSeeder::class,
|
||||
RolesAndPermissions::class,
|
||||
TestAccountSeeder::class,
|
||||
TemplateSeeder::class,
|
||||
Setting::class,
|
||||
ProductCategorySeeder::class,
|
||||
ProductTypeFieldSeeder::class,
|
||||
ProductMockupSeeder::class,
|
||||
VPSProductSeeder::class,
|
||||
]);
|
||||
|
||||
$this->enableMail();
|
||||
$marketing = Template::whereIn('name', array_column(MarketingTemplates::all(), 'name'))->orderBy('id')->get();
|
||||
$admin = User::where('username', 'admin')->firstOrFail();
|
||||
|
||||
foreach (self::CUSTOMERS as $email => [$name, $phone, $services]) {
|
||||
$customer = $this->seedCustomer($email, $name, $phone);
|
||||
|
||||
foreach ($services as $service) {
|
||||
$this->seedService($customer, ...$service);
|
||||
}
|
||||
|
||||
$this->seedMailHistory($customer, $admin, $marketing->first());
|
||||
}
|
||||
|
||||
$this->command?->info('Đã tạo ' . count(self::CUSTOMERS) . ' khách hàng, ' . count($marketing) . ' mẫu marketing và dịch vụ sắp hết hạn.');
|
||||
}
|
||||
|
||||
private function enableMail(): void
|
||||
{
|
||||
$setting = SettingModel::firstOrNew(['key' => 'email_settings']);
|
||||
$value = json_decode($setting->value ?? '{}', true) ?: [];
|
||||
$setting->value = json_encode([...$value, 'mail_enabled' => true], JSON_UNESCAPED_UNICODE);
|
||||
$setting->save();
|
||||
}
|
||||
|
||||
private function seedCustomer(string $email, string $name, string $phone): Customer
|
||||
{
|
||||
$customer = Customer::firstOrNew(['email' => $email]);
|
||||
$customer->fill([
|
||||
'fullname' => $name,
|
||||
'username' => Str::before($email, '@'),
|
||||
'phone' => $phone,
|
||||
'password' => Hash::make(self::CUSTOMER_PASSWORD),
|
||||
'email_verified_at' => now(),
|
||||
'email_verify' => 1,
|
||||
]);
|
||||
$customer->forceFill(['status' => 1])->save();
|
||||
|
||||
Service::where('customer_id', $customer->id)->where('note', 'seed:email-demo')->delete();
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
private function seedService(Customer $customer, string $kind, int $daysLeft, string $status = 'active'): void
|
||||
{
|
||||
$product = Product::where('name', 'like', '%' . self::PRODUCT_KEYWORDS[$kind] . '%')->first() ?? Product::first();
|
||||
|
||||
Service::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'customer_id' => $customer->id,
|
||||
'product_id' => $product?->id,
|
||||
'server_name' => Str::upper($kind) . '-' . Str::before($customer->email, '@'),
|
||||
'status' => $status,
|
||||
'auto_renew' => $daysLeft > 30,
|
||||
'started_at' => now()->addDays($daysLeft)->subYear(),
|
||||
'expires_at' => now()->addDays($daysLeft)->setTime(17, 0),
|
||||
'note' => 'seed:email-demo',
|
||||
]);
|
||||
}
|
||||
|
||||
private function seedMailHistory(Customer $customer, User $admin, Template $template): void
|
||||
{
|
||||
Mail::where('customer_id', $customer->id)->delete();
|
||||
|
||||
Mail::create([
|
||||
'template_id' => $template->id,
|
||||
'subject' => str_replace('{{client_name}}', $customer->fullname, $template->subject),
|
||||
'from_address' => config('mail.from.address'),
|
||||
'to' => $customer->email,
|
||||
'bcc' => 'audit@tgs.com.vn',
|
||||
'customer_id' => $customer->id,
|
||||
'sent_by' => $admin->id,
|
||||
'message' => str_replace(['{{client_name}}', '{{promo_code}}', '{{frontend_url}}'], [$customer->fullname, 'TGS30', '#'], $template->message),
|
||||
]);
|
||||
|
||||
Mail::create([
|
||||
'subject' => 'Chào mừng ' . $customer->fullname . ' đến với Thế Giới Số',
|
||||
'to' => $customer->email,
|
||||
'customer_id' => $customer->id,
|
||||
'message' => Template::renderWithLayout('Chào mừng', Shell::paragraph('Tài khoản của Quý khách đã được kích hoạt.')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -116,18 +116,31 @@ final class BillingTemplates
|
||||
],
|
||||
[
|
||||
'scenario' => 'domain_order_approved',
|
||||
'name' => 'Tên miền được duyệt',
|
||||
'subject' => 'Tên miền {{domain_name}} đã được duyệt đăng ký',
|
||||
'body' => T::title('Tên miền đã được duyệt')
|
||||
'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('Đơn đăng ký tên miền của Quý khách đã được <strong style="color: #16a34a;">duyệt và kích hoạt thành công</strong>.')
|
||||
. 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}}',
|
||||
'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::paragraph('Tên miền hiện đã hoạt động.')
|
||||
. 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::autoNotice(),
|
||||
. 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.'),
|
||||
],
|
||||
[
|
||||
'scenario' => 'domain_order_rejected',
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\EmailTemplates;
|
||||
|
||||
use App\Support\EmailTemplateShell as T;
|
||||
|
||||
final class LegacyTemplates
|
||||
{
|
||||
/**
|
||||
* @return array<int, array{scenario: string, name: string, subject: string, body: string}>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'scenario' => 'customer_signup_verify',
|
||||
'name' => '[Admin] Đơn hàng mới đã thanh toán',
|
||||
'subject' => '[ADMIN] Đơn hàng mới #{{order_num}} — {{customer_email}}',
|
||||
'body' => T::title('📦 Đơn hàng mới đã thanh toán')
|
||||
. T::subtitle('Đơn #{{order_num}} — {{created_at}}')
|
||||
. T::heading('Thông tin khách hàng')
|
||||
. T::infoTable([
|
||||
'Tên khách hàng' => '{{customer_name}}',
|
||||
'Email' => '{{customer_email}}',
|
||||
], '{{client_ip_row}}')
|
||||
. T::heading('Thông tin đơn hàng')
|
||||
. T::infoTable([
|
||||
'Mã đơn hàng' => '#{{order_num}}',
|
||||
'Sản phẩm' => '{{product_name}}',
|
||||
'Loại dịch vụ' => '{{product_type}}',
|
||||
'Chu kỳ thanh toán' => '{{billing_cycle}}',
|
||||
'Tổng tiền' => '{{total_formatted}}',
|
||||
'Trạng thái' => '<span style="color: #16a34a;">✅ Đã thanh toán</span>',
|
||||
])
|
||||
. '{{vm_block}}'
|
||||
. T::autoNotice(),
|
||||
],
|
||||
[
|
||||
'scenario' => 'customer_password_reset',
|
||||
'name' => '[Admin] Yêu cầu nạp tiền chờ duyệt (kèm chứng từ)',
|
||||
'subject' => '[ADMIN] Yêu cầu nạp tiền chờ duyệt - {{transaction_code}}',
|
||||
'body' => T::title('🔔 Yêu cầu nạp tiền chờ duyệt')
|
||||
. T::subtitle('{{transaction_code}} — {{created_at}}')
|
||||
. T::heading('Thông tin khách hàng')
|
||||
. T::infoTable([
|
||||
'Tên khách hàng' => '{{customer_name}}',
|
||||
'Email' => '{{customer_email}}',
|
||||
], '{{customer_phone_row}}{{client_ip_row}}')
|
||||
. T::heading('Thông tin giao dịch')
|
||||
. T::infoTable([
|
||||
'Mã giao dịch' => '{{transaction_code}}',
|
||||
'Số tiền' => '{{amount_formatted}}',
|
||||
'Trạng thái' => '<span style="color: #d97706;">Đang chờ duyệt</span>',
|
||||
], '{{proof_row}}')
|
||||
. T::note('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.'),
|
||||
],
|
||||
[
|
||||
'scenario' => 'vm_password_reset_success',
|
||||
'name' => 'Mật khẩu VPS đã được đặt lại',
|
||||
'subject' => 'Mật khẩu VPS của bạn đã được đặt lại',
|
||||
'body' => T::title('🔑 Mật khẩu VPS đã được đặt lại')
|
||||
. T::subtitle('Máy chủ: {{vm_name}}')
|
||||
. T::paragraph('Xin chào Quý khách,')
|
||||
. T::paragraph('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:')
|
||||
. T::infoTable([
|
||||
'Máy chủ' => '{{vm_name}}',
|
||||
'Mật khẩu mới' => '{{new_password}}',
|
||||
])
|
||||
. T::note('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.')
|
||||
. T::autoNotice(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders\EmailTemplates;
|
||||
|
||||
use App\Support\EmailTemplateShell as T;
|
||||
|
||||
final class MarketingTemplates
|
||||
{
|
||||
/**
|
||||
* @return array<int, array{name: string, subject: string, body: string, variables: array<int, array{key: string, label: string, example: string}>}>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'Khuyến mãi tháng 10',
|
||||
'subject' => 'Ưu đãi 30% Cloud VPS dành riêng cho {{client_name}}',
|
||||
'body' => T::title('ƯU ĐÃI THÁNG 10')
|
||||
. T::greeting('{{client_name}}')
|
||||
. T::paragraph('Thế Giới Số gửi tặng Quý khách mã giảm <strong>30%</strong> cho mọi gói Cloud VPS đăng ký mới hoặc gia hạn trong tháng 10.')
|
||||
. T::highlight('{{promo_code}}')
|
||||
. T::button('{{frontend_url}}', 'Đăng ký ngay'),
|
||||
'variables' => [['key' => 'promo_code', 'label' => 'Mã khuyến mãi', 'example' => 'TGS30']],
|
||||
],
|
||||
[
|
||||
'name' => 'Thông báo bảo trì hệ thống',
|
||||
'subject' => 'V/v: Thông báo bảo trì hệ thống ngày {{maintenance_date}}',
|
||||
'body' => T::title('THÔNG BÁO BẢO TRÌ')
|
||||
. T::greeting('{{client_name}}')
|
||||
. T::paragraph('Hệ thống sẽ bảo trì định kỳ vào <strong>{{maintenance_date}}</strong>, dự kiến gián đoạn tối đa 30 phút.')
|
||||
. T::note('Dịch vụ của Quý khách sẽ tự động hoạt động lại sau khi bảo trì.'),
|
||||
'variables' => [['key' => 'maintenance_date', 'label' => 'Ngày bảo trì', 'example' => '20/10/2026 01:00']],
|
||||
],
|
||||
[
|
||||
'name' => 'Ra mắt Premium Hosting',
|
||||
'subject' => 'Thế Giới Số ra mắt Premium Hosting thế hệ mới',
|
||||
'body' => T::title('PREMIUM HOSTING')
|
||||
. T::greeting('{{client_name}}')
|
||||
. T::paragraph('Hạ tầng NVMe, LiteSpeed và sao lưu hằng ngày. Chuyển đổi miễn phí từ nhà cung cấp cũ.')
|
||||
. T::button('https://tgs.com.vn/premium-hosting/', 'Xem bảng giá'),
|
||||
'variables' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ final class TemplateVariables
|
||||
'ftp_account' => ['Tài khoản FTP', 'string', 'ftp_example'],
|
||||
'ftp_password' => ['Mật khẩu FTP', 'string', 'Ftp#2026'],
|
||||
'vm_name' => ['Tên VPS', 'string', 'VPS-Pro-01'],
|
||||
'new_password' => ['Mật khẩu mới', 'string', 'Xk9#pL2m'],
|
||||
'vmware_id' => ['VM ID', 'string', 'vm-1024'],
|
||||
'suspended_at' => ['Thời gian tạm ngưng', 'string', '08/10/2026 09:00'],
|
||||
'server_name_suffix' => ['[Tự động] Tên máy chủ kèm theo', 'html', ''],
|
||||
@@ -57,6 +58,10 @@ final class TemplateVariables
|
||||
'action_status' => ['Trạng thái thao tác', 'string', 'Thành công'],
|
||||
|
||||
'domain_name' => ['Tên miền', 'string', 'example.vn'],
|
||||
'registered_at' => ['Ngày đăng ký', 'string', '08/10/2026'],
|
||||
'name_server_1' => ['Name server 1', 'string', 'ns9.tgs.com.vn'],
|
||||
'name_server_2' => ['Name server 2', 'string', 'ns10.tgs.com.vn'],
|
||||
'kyc_block' => ['[Tự động] Khối xác thực eKYC', 'html', ''],
|
||||
'hosting_plan' => ['Gói hosting', 'string', 'Premium Hosting 1'],
|
||||
'server_host_row' => ['[Tự động] Dòng máy chủ / FTP host', 'html', ''],
|
||||
'ftp_username_row' => ['[Tự động] Dòng tài khoản FTP', 'html', ''],
|
||||
|
||||
@@ -15,6 +15,10 @@ class ProductTypeFieldSeeder extends Seeder
|
||||
['product_type' => 'vps', 'key' => 'ram', 'label' => 'RAM (GB)', 'field_type' => 'number', 'sort_order' => 2],
|
||||
['product_type' => 'vps', 'key' => 'disk', 'label' => 'Disk (GB)', 'field_type' => 'number', 'sort_order' => 3],
|
||||
['product_type' => 'vps', 'key' => 'bandwidth', 'label' => 'Băng thông (GB)', 'field_type' => 'number', 'sort_order' => 4],
|
||||
['product_type' => 'cloud', 'key' => 'cpu', 'label' => 'CPU (vCPU)', 'field_type' => 'number', 'sort_order' => 1],
|
||||
['product_type' => 'cloud', 'key' => 'ram', 'label' => 'RAM (GB)', 'field_type' => 'number', 'sort_order' => 2],
|
||||
['product_type' => 'cloud', 'key' => 'disk', 'label' => 'Disk (GB)', 'field_type' => 'number', 'sort_order' => 3],
|
||||
['product_type' => 'cloud', 'key' => 'bandwidth', 'label' => 'Băng thông (GB)', 'field_type' => 'number', 'sort_order' => 4],
|
||||
|
||||
// Hosting
|
||||
['product_type' => 'hosting', 'key' => 'cpu', 'label' => 'CPU (vCPU)', 'field_type' => 'number', 'sort_order' => 1],
|
||||
|
||||
@@ -8,6 +8,8 @@ use App\Support\MailPlaceholders;
|
||||
use Database\Seeders\EmailTemplates\AccountTemplates;
|
||||
use Database\Seeders\EmailTemplates\AdminTemplates;
|
||||
use Database\Seeders\EmailTemplates\BillingTemplates;
|
||||
use Database\Seeders\EmailTemplates\LegacyTemplates;
|
||||
use Database\Seeders\EmailTemplates\MarketingTemplates;
|
||||
use Database\Seeders\EmailTemplates\ServiceTemplates;
|
||||
use Database\Seeders\EmailTemplates\TemplateVariables;
|
||||
use Illuminate\Database\Seeder;
|
||||
@@ -16,9 +18,31 @@ class TemplateSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
foreach (self::templates() as $item) {
|
||||
foreach ([...self::templates(), ...self::legacyTemplates()] as $item) {
|
||||
$this->upsert($item);
|
||||
}
|
||||
|
||||
foreach (MarketingTemplates::all() as $item) {
|
||||
$this->createMarketingIfMissing($item);
|
||||
}
|
||||
}
|
||||
|
||||
private function createMarketingIfMissing(array $item): void
|
||||
{
|
||||
$template = Template::firstOrNew(['name' => $item['name']]);
|
||||
if ($template->exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
$template->fill([
|
||||
'type_id' => 0,
|
||||
'format' => 'html',
|
||||
'subject' => $item['subject'],
|
||||
'message' => EmailTemplateShell::page($item['body']),
|
||||
'status' => 1,
|
||||
]);
|
||||
$template->syncVariables($item['variables']);
|
||||
$template->save();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,6 +70,17 @@ class TemplateSeeder extends Seeder
|
||||
return [$layout, ...$scenarios];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{scenario: string, name: string, subject: string, message: string}>
|
||||
*/
|
||||
public static function legacyTemplates(): array
|
||||
{
|
||||
return array_map(
|
||||
fn (array $item) => [...$item, 'message' => EmailTemplateShell::page($item['body'])],
|
||||
LegacyTemplates::all(),
|
||||
);
|
||||
}
|
||||
|
||||
private function upsert(array $item): void
|
||||
{
|
||||
$scenario = $item['scenario'];
|
||||
|
||||
Reference in New Issue
Block a user