Add feature tests for domain order management

- Implement AdminDomainOrderTest to cover admin functionalities for domain orders including listing, confirming registration, KYC updates, activation, rejection, and refunds.
- Implement ClientDomainOrderTest to cover client functionalities for placing domain orders, including validation for balance, duplicate orders, and domain normalization.
- Ensure proper handling of domain pricing and service statuses throughout the tests.
This commit is contained in:
baodev0
2026-07-10 14:16:35 +07:00
parent 61c0e026c8
commit dfb926055d
81 changed files with 35743 additions and 481 deletions
+3 -4
View File
@@ -17,12 +17,11 @@ class CustomerFactory extends Factory
public function definition()
{
return [
'firstname' => $this->faker->name(),
'lastname' => $this->faker->name(),
'fullname' => $this->faker->name(),
'phone' => $this->faker->unique()->phoneNumber(),
'email' => $this->faker->unique()->email(),
'email' => $this->faker->unique()->safeEmail(),
'email_verify' => now(),
'password' => '$2y$10$2Tcx805kaC.Ug/IUD34pW.R.D0l0Gpy.IEeXkrjhBr1uu048pEFp.'
'password' => '$2y$10$2Tcx805kaC.Ug/IUD34pW.R.D0l0Gpy.IEeXkrjhBr1uu048pEFp.',
];
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
*/
class ProductFactory extends Factory
{
public function definition(): array
{
$slug = Str::slug($this->faker->unique()->words(3, true));
$categoryId = DB::table('product_categories')->insertGetId([
'name' => 'Test Category ' . $this->faker->unique()->numberBetween(1, 9999),
'slug' => 'cat-' . $slug,
'type' => null, // null so boot() hook does not override product_type
'is_active' => true,
'sort_order' => 0,
'created_at' => now(),
'updated_at' => now(),
]);
return [
'product_categories_id' => $categoryId,
'product_type' => 'domain',
'name' => $this->faker->unique()->words(2, true),
'slug' => $slug,
'is_active' => true,
'is_unlimited' => true,
'setup_fee' => 0,
'trial_days' => 0,
'sort_order' => 0,
];
}
}
@@ -7,9 +7,11 @@ use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('ip_pools', function (Blueprint $table) {
$table->softDeletes();
});
if (!Schema::hasColumn('ip_pools', 'deleted_at')) {
Schema::table('ip_pools', function (Blueprint $table) {
$table->softDeletes();
});
}
}
public function down(): void
@@ -9,9 +9,11 @@ use Illuminate\Support\Str;
return new class extends Migration {
public function up(): void
{
Schema::table('ip_pools', function (Blueprint $table) {
$table->uuid('uuid')->nullable()->unique()->after('id');
});
if (!Schema::hasColumn('ip_pools', 'uuid')) {
Schema::table('ip_pools', function (Blueprint $table) {
$table->uuid('uuid')->nullable()->unique()->after('id');
});
}
// Backfill existing rows
DB::table('ip_pools')->whereNull('uuid')->orderBy('id')->each(function ($row) {
@@ -2,6 +2,7 @@
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 {
@@ -11,18 +12,25 @@ return new class extends Migration {
public function up(): void
{
Schema::table('ip_pools', function (Blueprint $table) {
$table->string('gateway')->nullable()->after('ip_address');
$table->unsignedTinyInteger('cidr')->default(24)->after('gateway');
$table
->enum('status', ['free', 'assigned', 'reserved'])
->default('free')
->after('module_network_id');
$table->foreignId('service_id')->nullable()->after('status')->constrained('services')->nullOnDelete();
$table->index(['status', 'module_network_id']);
if (!Schema::hasColumn('ip_pools', 'gateway')) {
$table->string('gateway')->nullable()->after('ip_address');
}
if (!Schema::hasColumn('ip_pools', 'cidr')) {
$table->unsignedTinyInteger('cidr')->default(24)->after('gateway');
}
if (!Schema::hasColumn('ip_pools', 'status')) {
$table->enum('status', ['free', 'assigned', 'reserved'])->default('free')->after('module_network_id');
}
if (!Schema::hasColumn('ip_pools', 'service_id')) {
$table->foreignId('service_id')->nullable()->after('status')->constrained('services')->nullOnDelete();
}
});
if (!collect(\DB::select("SHOW INDEX FROM ip_pools WHERE Key_name = 'ip_pools_status_module_network_id_index'"))->count()) {
Schema::table('ip_pools', function (Blueprint $table) {
$table->index(['status', 'module_network_id']);
});
}
}
/**
@@ -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('order_items', function (Blueprint $table) {
$table->string('domain_name', 255)->nullable()->after('server_name')->index();
});
}
public function down(): void
{
Schema::table('order_items', function (Blueprint $table) {
$table->dropIndex(['domain_name']);
$table->dropColumn('domain_name');
});
}
};
@@ -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('services', function (Blueprint $table) {
$table->string('registrar_reference', 255)->nullable()->after('note');
$table->text('kyc_url')->nullable()->after('registrar_reference');
$table->text('rejection_reason')->nullable()->after('kyc_url');
$table->timestamp('refunded_at')->nullable()->after('rejection_reason');
});
}
public function down(): void
{
Schema::table('services', function (Blueprint $table) {
$table->dropColumn(['registrar_reference', 'kyc_url', 'rejection_reason', 'refunded_at']);
});
}
};
@@ -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('customers', function (Blueprint $table) {
$table->string('email_verify_code', 6)->nullable()->after('email_verify');
$table->timestamp('email_verify_code_expired_at')->nullable()->after('email_verify_code');
});
}
public function down(): void
{
Schema::table('customers', function (Blueprint $table) {
$table->dropColumn(['email_verify_code', 'email_verify_code_expired_at']);
});
}
};
+7
View File
@@ -125,6 +125,13 @@ class RolesAndPermissions extends Seeder
'auto-banking.manager',
'system-log.manager',
// Tên miền
'domain.view',
'domain.create',
'domain.edit',
'domain.destroy',
'domain.manager',
// Vai trò & Phân quyền
'role.view',
'role.create',
+31
View File
@@ -60,6 +60,37 @@ class Setting extends Seeder
'password' => '',
'status' => 0
], JSON_THROW_ON_ERROR)
],
[
'key' => 'domain_pricing',
'value' => json_encode([
'domains' => [
['tld' => '.com', 'setupFee' => 'Miễn phí', 'firstYear' => 320000, 'renewal' => 340000],
['tld' => '.net', 'setupFee' => 'Miễn phí', 'firstYear' => 369000, 'renewal' => 409000],
['tld' => '.org', 'setupFee' => 'Miễn phí', 'firstYear' => 339000, 'renewal' => 369000],
['tld' => '.biz', 'setupFee' => 'Miễn phí', 'firstYear' => 399000, 'renewal' => 409000],
['tld' => '.info', 'setupFee' => 'Miễn phí', 'firstYear' => 490000, 'renewal' => 509000],
['tld' => '.Pro', 'setupFee' => 'Miễn phí', 'firstYear' => 469000, 'renewal' => 489000],
['tld' => '.name', 'setupFee' => 'Miễn phí', 'firstYear' => 259000, 'renewal' => 259000],
['tld' => '.ws', 'setupFee' => 'Miễn phí', 'firstYear' => 909000, 'renewal' => 909000],
['tld' => '.tv', 'setupFee' => 'Miễn phí', 'firstYear' => 765000, 'renewal' => 819000],
['tld' => '.mobi', 'setupFee' => 'Miễn phí', 'firstYear' => 589000, 'renewal' => 589000],
['tld' => '.uk', 'setupFee' => 'Miễn phí', 'firstYear' => 537000, 'renewal' => 537000],
['tld' => '.asia', 'setupFee' => 'Miễn phí', 'firstYear' => 380000, 'renewal' => 452000],
['tld' => '.me', 'setupFee' => 'Miễn phí', 'firstYear' => 1029000, 'renewal' => 1029000],
['tld' => '.us', 'setupFee' => 'Miễn phí', 'firstYear' => 323000, 'renewal' => 323000],
],
'services' => [
['name' => 'Chứng thực Rapid SSL', 'setupFee' => 230000, 'maintenance' => 750000, 'note' => '01 Domain'],
['name' => 'Chứng thực Quick SSL', 'setupFee' => 230000, 'maintenance' => 2300000, 'note' => '01 Domain'],
['name' => 'Chứng thực Wildcard SSL', 'setupFee' => 230000, 'maintenance' => 3300000, 'note' => 'Unlimited Subdomain'],
['name' => 'Chứng thực True BusinessID', 'setupFee' => 540000, 'maintenance' => 3900000, 'note' => '01 Domain'],
['name' => 'Chứng thực True BusinessID WildCard', 'setupFee' => 900000, 'maintenance' => 15000000, 'note' => 'Unlimited Subdomain'],
['name' => 'Đăng ký DNS riêng', 'setupFee' => 200000, 'maintenance' => 'Miễn phí', 'note' => "dns1.tenmiencuaban.com\ndns2.tenmiencuaban.com"],
['name' => 'Bảo mật thông tin tên miền', 'setupFee' => 'Miễn phí', 'maintenance' => 90000, 'note' => 'Toàn bộ thông tin Whois tên miền của bạn sẽ được giấu kín hoàn toàn với mọi công cụ Whois'],
['name' => 'Khóa tên miền', 'setupFee' => 'Miễn phí', 'maintenance' => 'Miễn phí', 'note' => 'Tăng cường bảo mật cho tên miền'],
],
], JSON_THROW_ON_ERROR)
]
]);
+21
View File
@@ -51,6 +51,27 @@ class TemplateSeeder extends Seeder
'type_id' => 0,
'status' => 1
],
[
'name' => 'Domain Order New',
'subject' => '[ADMIN] Đơn đăng ký tên miền mới - {$domain_name}',
'message' => '<table cellpadding=0 cellspacing=0 role=presentation style=width:100%;font-family:Montserrat,-apple-system><tr><td style=background-color:#eceff1;font-family:Montserrat,-apple-system,sans-serif align=center><table cellpadding=0 cellspacing=0 role=presentation style=width:600px><tr><td align=center><table cellpadding=0 cellspacing=0 role=presentation style=width:100%><tr><td style=padding:48px;text-align:center> <tr style=background-color:#fff><td style="text-align:center;padding:20px;border-bottom:.5px solid #ececec"><img alt={$company_name} src={$company_logo} style=max-height:100px><tr><td style=border-radius:4px;background-color:#fff;padding:48px;text-align:left;font-family:Montserrat,-apple-system,sans-serif;font-size:16px;line-height:24px;color:#626262><p style=margin-bottom:10px;font-size:20px;font-weight:600>Thông báo đơn đăng ký tên miền mới<p>Có một đơn đăng ký tên miền mới vừa được đặt tại <strong>{$company_name}</strong>.<p><strong>Thông tin đơn hàng:</strong><ul><li>Tên miền: <strong>{$domain_name}</strong><li>Mã đơn hàng: <strong>{$order_num}</strong><li>Tổng tiền: <strong>{$order_total}</strong><li>Khách hàng: <strong>{$customer_name}</strong><li>Email: <strong>{$customer_email}</strong><li>Thời gian: <strong>{$created_at}</strong></ul><table cellpadding=0 cellspacing=0 role=presentation><tr><td style=border-radius:4px;background-color:#f16723;text-align:center;font-family:Montserrat,-apple-system,sans-serif;padding-left:48px;padding-right:48px><a href={$admin_url} style="font-family:sans-serif;display:block;font-size:16px;font-weight:600;line-height:100%;color:#fff;text-decoration:none;padding:16px 24px 16px 24px">XEM ĐƠN HÀNG</a></table><p>Lưu ý:<ul><li>Đây là tin nhắn tự động. Vui lòng không trả lời tin nhắn này!</ul><p> </table><tr><td style=padding:48px;text-align:center> </table></table>',
'type_id' => 0,
'status' => 1
],
[
'name' => 'Domain Order Approved',
'subject' => 'Tên miền {$domain_name} đã được duyệt đăng ký',
'message' => '<table cellpadding=0 cellspacing=0 role=presentation style=width:100%;font-family:Montserrat,-apple-system><tr><td style=background-color:#eceff1;font-family:Montserrat,-apple-system,sans-serif align=center><table cellpadding=0 cellspacing=0 role=presentation style=width:600px><tr><td align=center><table cellpadding=0 cellspacing=0 role=presentation style=width:100%><tr><td style=padding:48px;text-align:center> <tr style=background-color:#fff><td style="text-align:center;padding:20px;border-bottom:.5px solid #ececec"><img alt={$company_name} src={$company_logo} style=max-height:100px><tr><td style=border-radius:4px;background-color:#fff;padding:48px;text-align:left;font-family:Montserrat,-apple-system,sans-serif;font-size:16px;line-height:24px;color:#626262><p style=margin-bottom:10px;font-size:20px;font-weight:600>Kính chào {$client_name},<p>Chúng tôi xin thông báo rằng đơn đăng ký tên miền của bạn đã được <strong style=color:#22c55e>duyệt và kích hoạt thành công</strong>.<p><strong>Thông tin tên miền:</strong><ul><li>Tên miền: <strong>{$domain_name}</strong><li>Mã đơn hàng: <strong>{$order_num}</strong></ul><p>Tên miền của bạn hiện đã hoạt động. Nếu bạn có bất kỳ câu hỏi nào, vui lòng liên hệ với chúng tôi để được hỗ trợ.<p>Lưu ý:<ul><li>Đây là tin nhắn tự động. Vui lòng không trả lời tin nhắn này!</ul><p> </table><tr><td style=padding:48px;text-align:center> </table></table>',
'type_id' => 0,
'status' => 1
],
[
'name' => 'Domain Order Rejected',
'subject' => 'Tên miền {$domain_name} đã bị từ chối đăng ký',
'message' => '<table cellpadding=0 cellspacing=0 role=presentation style=width:100%;font-family:Montserrat,-apple-system><tr><td style=background-color:#eceff1;font-family:Montserrat,-apple-system,sans-serif align=center><table cellpadding=0 cellspacing=0 role=presentation style=width:600px><tr><td align=center><table cellpadding=0 cellspacing=0 role=presentation style=width:100%><tr><td style=padding:48px;text-align:center> <tr style=background-color:#fff><td style="text-align:center;padding:20px;border-bottom:.5px solid #ececec"><img alt={$company_name} src={$company_logo} style=max-height:100px><tr><td style=border-radius:4px;background-color:#fff;padding:48px;text-align:left;font-family:Montserrat,-apple-system,sans-serif;font-size:16px;line-height:24px;color:#626262><p style=margin-bottom:10px;font-size:20px;font-weight:600>Kính chào {$client_name},<p>Chúng tôi xin thông báo rằng đơn đăng ký tên miền của bạn đã bị <strong style=color:#ef4444>từ chối</strong>.<p><strong>Thông tin đơn hàng:</strong><ul><li>Tên miền: <strong>{$domain_name}</strong><li>Mã đơn hàng: <strong>{$order_num}</strong><li>Lý do từ chối: <strong>{$reason}</strong></ul><p>Số dư đã thanh toán sẽ được hoàn lại vào tài khoản của bạn. Nếu bạn có bất kỳ câu hỏi nào, vui lòng liên hệ với chúng tôi để được hỗ trợ.<p>Lưu ý:<ul><li>Đây là tin nhắn tự động. Vui lòng không trả lời tin nhắn này!</ul><p> </table><tr><td style=padding:48px;text-align:center> </table></table>',
'type_id' => 0,
'status' => 1
],
[
'name' => 'Service Expiry',
'subject' => 'Thông báo dịch vụ sắp hết hạn',