feat(service): huỷ dịch vụ, SSL, đổi mật khẩu hosting, lịch sử thanh toán theo hình thức
- Lịch sử thanh toán hoá đơn trả thêm `method` (tự động gia hạn / số dư / admin / chuyển khoản / hoàn tiền), bỏ đuôi `(method)` khỏi ghi chú - `DnsServer::panelUrl()` = https://<domain>/ dùng cho email cấp hosting và nút Plesk của khách - Order item trả thêm `uuid`/`type` của dịch vụ để FE dẫn tới trang quản lý - Yêu cầu huỷ dịch vụ, chứng chỉ SSL, lịch sử gia hạn, đổi mật khẩu hosting cho khách Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
a6b641015e
commit
b44a88bb52
@@ -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())->seedServiceLifecycleTemplates();
|
||||
}
|
||||
|
||||
public function down(): void {}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('service_attachments', function (Blueprint $table) {
|
||||
$table->string('audience', 20)->default('admin')->after('service_id')->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('service_attachments', function (Blueprint $table) {
|
||||
$table->dropColumn('audience');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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::create('service_ssl_certificates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('service_id')->unique()->constrained('services')->cascadeOnDelete();
|
||||
$table->longText('data');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('service_ssl_certificates');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Database\Seeders\TemplateSeeder;
|
||||
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('service_cancellation_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('service_id')->constrained('services')->cascadeOnDelete();
|
||||
$table->foreignId('customer_id')->constrained('customers')->cascadeOnDelete();
|
||||
$table->string('reason_code', 40)->nullable();
|
||||
$table->text('reason');
|
||||
$table->string('status', 20)->default('pending')->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
(new TemplateSeeder())->seedScenarioIfMissing('admin_service_cancellation_request');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('service_cancellation_requests');
|
||||
DB::table('template')->where('scenario', 'admin_service_cancellation_request')->delete();
|
||||
}
|
||||
};
|
||||
+24
@@ -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('service_cancellation_requests', function (Blueprint $table) {
|
||||
$table->text('admin_note')->nullable()->after('reason');
|
||||
$table->timestamp('resolved_at')->nullable()->after('status');
|
||||
$table->string('resolved_by', 100)->nullable()->after('resolved_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('service_cancellation_requests', function (Blueprint $table) {
|
||||
$table->dropColumn(['admin_note', 'resolved_at', 'resolved_by']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('services', function (Blueprint $table) {
|
||||
$table->boolean('cancel_at_expiry')->default(false)->after('auto_renew')->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('services', function (Blueprint $table) {
|
||||
$table->dropColumn('cancel_at_expiry');
|
||||
});
|
||||
}
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Database\Seeders\TemplateSeeder;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
(new TemplateSeeder())->seedScenarioIfMissing('admin_customer_service_renewed');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('template')->where('scenario', 'admin_customer_service_renewed')->delete();
|
||||
}
|
||||
};
|
||||
@@ -166,6 +166,44 @@ final class AdminTemplates
|
||||
. T::note('{{description}}', 'Nội dung:')
|
||||
. T::button('{{ticket_url}}', 'XEM TICKET'),
|
||||
],
|
||||
[
|
||||
'scenario' => 'admin_customer_service_renewed',
|
||||
'name' => '[Admin] Khách hàng gia hạn dịch vụ',
|
||||
'subject' => '[ADMIN] {{customer_name}} gia hạn dịch vụ {{product_name}}',
|
||||
'body' => T::title('Khách hàng vừa gia hạn dịch vụ')
|
||||
. T::paragraph('Một dịch vụ trên hệ thống <strong>{{company_name}}</strong> vừa được gia hạn thành công.')
|
||||
. T::infoTable([
|
||||
'Hình thức' => '{{renewal_source_label}}',
|
||||
'Khách hàng' => '{{customer_name}}',
|
||||
'Email' => '{{customer_email}}',
|
||||
'Dịch vụ' => '{{product_name}}{{server_name_suffix}}',
|
||||
'Thời gian gia hạn' => '{{months_label}}',
|
||||
'Hạn sử dụng trước' => '{{old_expires_at}}',
|
||||
'Hạn sử dụng mới' => '{{expires_at}}',
|
||||
'Hoá đơn' => '{{invoice_number}}',
|
||||
'Số tiền' => '{{amount_formatted}}',
|
||||
'Thanh toán' => '{{payment_method_label}}',
|
||||
])
|
||||
. T::button('{{admin_url}}', 'XEM DỊCH VỤ'),
|
||||
],
|
||||
[
|
||||
'scenario' => 'admin_service_cancellation_request',
|
||||
'name' => '[Admin] Khách yêu cầu hủy dịch vụ',
|
||||
'subject' => '[ADMIN] {{customer_name}} yêu cầu hủy dịch vụ {{product_name}}',
|
||||
'body' => T::title('Khách hàng yêu cầu hủy dịch vụ')
|
||||
. T::paragraph('Khách hàng vừa gửi yêu cầu hủy dịch vụ trên hệ thống <strong>{{company_name}}</strong>. Dịch vụ chưa bị hủy, cần nhân viên liên hệ và xử lý.')
|
||||
. T::infoTable([
|
||||
'Khách hàng' => '{{customer_name}}',
|
||||
'Email' => '{{customer_email}}',
|
||||
'Dịch vụ' => '{{product_name}}',
|
||||
'Tên dịch vụ' => '{{server_name}}',
|
||||
'Ngày hết hạn' => '{{expires_at}}',
|
||||
'Lý do hủy' => '{{cancel_reason}}',
|
||||
'Thời gian' => '{{created_at}}',
|
||||
'Địa chỉ IP' => '{{client_ip}}',
|
||||
])
|
||||
. T::button('{{admin_url}}', 'XEM DỊCH VỤ'),
|
||||
],
|
||||
[
|
||||
'scenario' => 'admin_login_alert',
|
||||
'name' => '[Admin] Có người đăng nhập trang quản trị',
|
||||
|
||||
@@ -132,6 +132,10 @@ final class ServiceLifecycleTemplates
|
||||
$items[] = self::genericNewService($hasAccount);
|
||||
}
|
||||
|
||||
foreach ([true, false] as $withPassword) {
|
||||
$items[] = self::resendAccount($withPassword);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
@@ -193,6 +197,27 @@ final class ServiceLifecycleTemplates
|
||||
);
|
||||
}
|
||||
|
||||
private static function resendAccount(bool $withPassword): array
|
||||
{
|
||||
$loginLink = '<a style="color: #c2410c;" href="{{login_url}}" target="_blank" rel="noopener">{{login_url}}</a>';
|
||||
$rows = ['Trang đăng nhập' => $loginLink, 'Email đăng nhập' => '{{account_email}}'];
|
||||
$rows['Mật khẩu'] = $withPassword ? '{{account_pass}}' : 'Mật khẩu hiện tại của Quý khách';
|
||||
|
||||
return self::template(
|
||||
'Tài khoản — Gửi lại thông tin tài khoản (' . ($withPassword ? 'kèm mật khẩu' : 'không kèm mật khẩu') . ')',
|
||||
'V/v: Thế Giới Số gửi lại thông tin tài khoản | {{client_name}}',
|
||||
T::greeting('{{client_name}}')
|
||||
. T::paragraph('Theo yêu cầu của Quý khách, <strong>{{app_name}}</strong> xin gửi lại thông tin tài khoản đăng nhập như sau:')
|
||||
. T::infoTable($rows)
|
||||
. ($withPassword
|
||||
? T::note('Quý khách vui lòng đổi mật khẩu ngay sau khi đăng nhập và không chia sẻ thông tin tài khoản cho người khác.', 'Bảo mật:')
|
||||
: T::note('Nếu không nhớ mật khẩu, Quý khách chọn <strong>"Quên mật khẩu"</strong> tại trang đăng nhập để đặt lại.'))
|
||||
. T::button('{{login_url}}', 'ĐĂNG NHẬP')
|
||||
. T::supportLine()
|
||||
. T::paragraph('Trân trọng,'),
|
||||
);
|
||||
}
|
||||
|
||||
private static function genericNewService(bool $hasAccount): array
|
||||
{
|
||||
$accessRows = '';
|
||||
|
||||
@@ -39,6 +39,8 @@ final class SubjectFormat
|
||||
'admin_trial_registration_request' => ['[ADMIN] yêu cầu dùng thử Cloud VPS', null, 'customer_name'],
|
||||
'admin_manual_deposit_pending' => ['[ADMIN] yêu cầu nạp tiền chờ duyệt {{transaction_code}}', null, 'customer_name'],
|
||||
'admin_ticket_notification' => ['[ADMIN] ticket mới #{{ticket_num}}', null, 'customer_name'],
|
||||
'admin_customer_service_renewed' => ['[ADMIN] {{customer_name}} gia hạn dịch vụ {{product_name}}', null, null],
|
||||
'admin_service_cancellation_request' => ['[ADMIN] {{customer_name}} yêu cầu hủy dịch vụ {{product_name}}', null, null],
|
||||
'admin_login_alert' => ['[ADMIN] {{admin_username}} đăng nhập trang quản trị từ IP {{client_ip}}', null, null],
|
||||
'admin_payment_received' => ['[ADMIN] khách hàng thanh toán {{payment_type}} #{{reference}}', null, 'customer_name'],
|
||||
'admin_service_renewed' => ['[ADMIN] {{admin_name}} gia hạn dịch vụ', 'product_name', 'customer_name'],
|
||||
|
||||
@@ -112,6 +112,8 @@ final class TemplateVariables
|
||||
'status_line' => ['Tình trạng hạn thanh toán', 'string', 'Hoá đơn sẽ đến hạn sau 3 ngày.'],
|
||||
'paid_row' => ['[Tự động] Dòng đã thanh toán', 'html', ''],
|
||||
'transaction_code' => ['Mã giao dịch', 'string', 'NAP20261008001'],
|
||||
'renewal_source_label' => ['Hình thức gia hạn', 'string', 'Khách hàng tự gia hạn'],
|
||||
'cancel_reason' => ['Lý do hủy dịch vụ', 'string', 'Không còn nhu cầu sử dụng'],
|
||||
'admin_username' => ['Tài khoản quản trị', 'string', 'admin'],
|
||||
'login_time' => ['Thời gian đăng nhập', 'string', '23/10/2026 08:30:00'],
|
||||
'client_ip' => ['Địa chỉ IP', 'string', '203.113.10.20'],
|
||||
|
||||
Reference in New Issue
Block a user