'Full Administrator', 'guard_name' => 'admin']); $this->dnsServerId = DB::table('dns_manager_servers')->insertGetId([ 'name' => 'Test DNS Server', 'ip_address' => '127.0.0.1', 'password' => 'test-password', 'api_key' => 'test-key', 'created_at' => now(), 'updated_at' => now(), ]); $this->admin = User::factory()->create(); $this->admin->assignRole('Full Administrator'); $this->customer = Customer::factory()->create(['total' => 1_000_000]); $this->product = Product::factory()->create([ 'product_type' => 'domain', 'is_active' => true, ]); Setting::create([ 'key' => 'domain_pricing', 'value' => json_encode([ 'domains' => [ ['tld' => '.com', 'firstYear' => 320_000, 'renewal' => 340_000], ['tld' => '.vn', 'firstYear' => 400_000, 'renewal' => 400_000], ], ]), ]); } // ========================================================================= // Helpers // ========================================================================= private function actingAsAdmin(): static { return $this->actingAs($this->admin, 'admin'); } /** Create a fully-formed domain order in DB without going through the HTTP layer. */ private function createDomainOrder(string $domain = 'test.com', string $serviceStatus = 'pending'): array { $order = Order::create([ 'order_num' => OrderHelper::generateOrderNumber(), 'customer_id' => $this->customer->id, 'sub_total' => 320_000, 'discount_amount'=> 0, 'total' => 320_000, 'status' => OrderStatus::PROCESSING->value, 'payment_method' => 'balance', ]); $orderItem = OrderItem::create([ 'order_id' => $order->id, 'product_id' => $this->product->id, 'domain_name' => $domain, 'billing_cycle' => 'yearly', 'months' => 12, 'quantity' => 1, 'price' => 320_000, 'server_name' => $domain, ]); $invoice = Invoice::create([ 'invoice_number' => OrderHelper::generateOrderNumber('INV'), 'customer_id' => $this->customer->id, 'order_id' => $order->id, 'invoice_type' => 1, 'sub_total' => 320_000, 'discount_amount' => 0, 'tax' => 0, 'total' => 320_000, 'status' => InvoiceStatus::PAID->value, 'payment_method' => 'balance', 'paid_at' => now(), 'due_date' => now()->toDateString(), ]); InvoiceItem::create([ 'invoice_id' => $invoice->id, 'product_id' => $this->product->id, 'type' => 'domain', 'description' => "Đăng ký tên miền {$domain}", 'months' => 12, 'quantity' => 1, 'unit_price' => 320_000, 'total_price' => 320_000, ]); $service = Service::create([ 'customer_id' => $this->customer->id, 'product_id' => $this->product->id, 'order_item_id' => $orderItem->id, 'invoice_id' => $invoice->id, 'server_name' => $domain, 'status' => $serviceStatus, 'auto_renew' => false, ]); return compact('order', 'orderItem', 'invoice', 'service'); } // ========================================================================= // List & detail // ========================================================================= public function test_admin_can_list_domain_orders(): void { $this->createDomainOrder('list-test.com'); $response = $this->actingAsAdmin()->getJson('/api/v1/administrator/domain-orders'); $response->assertOk() ->assertJsonPath('error', false) ->assertJsonStructure(['data' => ['items', 'total', 'per_page', 'current_page', 'last_page']]); $this->assertCount(1, $response->json('data.items')); } public function test_non_admin_cannot_access_domain_orders(): void { $this->getJson('/api/v1/administrator/domain-orders')->assertStatus(401); } // ========================================================================= // Confirm registration // ========================================================================= public function test_confirm_registration_creates_dns_domain_and_attaches_customer(): void { ['order' => $order] = $this->createDomainOrder('register-me.com'); $response = $this->actingAsAdmin()->postJson("/api/v1/administrator/domain-orders/{$order->id}/confirm-registration", [ 'dns_manager_server_id' => $this->dnsServerId, 'registrar_reference' => 'REF-12345', 'registered_at' => now()->toDateString(), 'expires_at' => now()->addYear()->toDateString(), ]); $response->assertOk()->assertJsonPath('error', false); $this->assertDatabaseHas('dns_manager_domain', ['domain_name' => 'register-me.com', 'is_created' => 1]); $this->assertDatabaseHas('dns_domain_user', ['user_id' => $this->customer->id]); $this->assertDatabaseHas('services', [ 'status' => DomainProvisioningStatus::REGISTRAR_CREATED->value, 'registrar_reference' => 'REF-12345', ]); } public function test_cannot_confirm_already_confirmed_order(): void { ['order' => $order] = $this->createDomainOrder('already-confirmed.com', DomainProvisioningStatus::REGISTRAR_CREATED->value); $response = $this->actingAsAdmin()->postJson("/api/v1/administrator/domain-orders/{$order->id}/confirm-registration", []); $response->assertStatus(422)->assertJsonPath('error', true); } public function test_cannot_create_duplicate_dns_domain(): void { ['order' => $order] = $this->createDomainOrder('dup-dns.com'); // Pre-create the DNS domain so confirm hits the duplicate guard DnsDomain::create(['domain_name' => 'dup-dns.com', 'dns_manager_server_id' => $this->dnsServerId, 'is_created' => 1]); $response = $this->actingAsAdmin()->postJson("/api/v1/administrator/domain-orders/{$order->id}/confirm-registration", []); $response->assertStatus(422)->assertJsonPath('error', true); } // ========================================================================= // KYC // ========================================================================= public function test_update_kyc_url_sets_pending_kyc_status(): void { ['order' => $order, 'service' => $service] = $this->createDomainOrder('kyc-test.com', DomainProvisioningStatus::REGISTRAR_CREATED->value); $response = $this->actingAsAdmin()->patchJson("/api/v1/administrator/domain-orders/{$order->id}/kyc", [ 'kyc_url' => 'https://kyc.example.com/verify/abc123', ]); $response->assertOk()->assertJsonPath('error', false); $this->assertDatabaseHas('services', [ 'id' => $service->id, 'status' => DomainProvisioningStatus::PENDING_KYC->value, 'kyc_url'=> 'https://kyc.example.com/verify/abc123', ]); } // ========================================================================= // Activate // ========================================================================= public function test_activate_transitions_from_pending_kyc_to_active(): void { ['order' => $order, 'service' => $service] = $this->createDomainOrder('activate-me.com', DomainProvisioningStatus::PENDING_KYC->value); $response = $this->actingAsAdmin()->patchJson("/api/v1/administrator/domain-orders/{$order->id}/activate"); $response->assertOk()->assertJsonPath('error', false); $this->assertDatabaseHas('services', ['id' => $service->id, 'status' => DomainProvisioningStatus::ACTIVE->value]); $this->assertDatabaseHas('orders', ['id' => $order->id, 'status' => OrderStatus::ACTIVE->value]); } public function test_cannot_activate_pending_order_directly(): void { ['order' => $order] = $this->createDomainOrder('cannot-activate.com', DomainProvisioningStatus::PENDING->value); $response = $this->actingAsAdmin()->patchJson("/api/v1/administrator/domain-orders/{$order->id}/activate"); $response->assertStatus(422)->assertJsonPath('error', true); } // ========================================================================= // Reject & Refund // ========================================================================= public function test_reject_and_refund_restores_customer_balance(): void { $this->customer->update(['total' => 680_000]); // balance after paying 320k ['order' => $order, 'service' => $service] = $this->createDomainOrder('refund-me.com'); $response = $this->actingAsAdmin()->postJson("/api/v1/administrator/domain-orders/{$order->id}/reject-and-refund", [ 'reason' => 'Tên miền không khả dụng.', ]); $response->assertOk()->assertJsonPath('error', false); $this->assertEquals(680_000 + 320_000, $this->customer->fresh()->total); $this->assertDatabaseHas('services', [ 'id' => $service->id, 'status' => DomainProvisioningStatus::REFUNDED->value, ]); $this->assertDatabaseHas('invoices', ['order_id' => $order->id, 'status' => InvoiceStatus::REFUNDED->value]); $this->assertDatabaseHas('orders', ['id' => $order->id, 'status' => OrderStatus::CANCELLED->value]); $this->assertDatabaseHas('transactions', ['customer_id' => $this->customer->id, 'type' => 'refund', 'amount' => 320_000]); } public function test_reject_and_refund_is_idempotent(): void { ['order' => $order] = $this->createDomainOrder('idempotent.com'); $this->actingAsAdmin()->postJson("/api/v1/administrator/domain-orders/{$order->id}/reject-and-refund", [ 'reason' => 'First refund.', ])->assertOk(); // Second call must fail with 422 $this->actingAsAdmin()->postJson("/api/v1/administrator/domain-orders/{$order->id}/reject-and-refund", [ 'reason' => 'Second attempt.', ])->assertStatus(422)->assertJsonPath('error', true); } public function test_cannot_reject_non_pending_order(): void { ['order' => $order] = $this->createDomainOrder('not-pending.com', DomainProvisioningStatus::REGISTRAR_CREATED->value); $response = $this->actingAsAdmin()->postJson("/api/v1/administrator/domain-orders/{$order->id}/reject-and-refund", [ 'reason' => 'Should fail.', ]); $response->assertStatus(422)->assertJsonPath('error', true); } public function test_refund_uses_invoice_amount_not_current_pricing(): void { // Manually create an order where the invoice total differs from current pricing $order = Order::create([ 'order_num' => OrderHelper::generateOrderNumber(), 'customer_id' => $this->customer->id, 'sub_total' => 250_000, 'discount_amount'=> 0, 'total' => 250_000, 'status' => OrderStatus::PROCESSING->value, 'payment_method' => 'balance', ]); $orderItem = OrderItem::create([ 'order_id' => $order->id, 'product_id' => $this->product->id, 'domain_name' => 'old-price.com', 'billing_cycle'=> 'yearly', 'months' => 12, 'quantity' => 1, 'price' => 250_000, 'server_name' => 'old-price.com', ]); $invoice = Invoice::create([ 'invoice_number' => OrderHelper::generateOrderNumber('INV'), 'customer_id' => $this->customer->id, 'order_id' => $order->id, 'invoice_type' => 1, 'sub_total' => 250_000, 'discount_amount' => 0, 'tax' => 0, 'total' => 250_000, // old price, not 320k 'status' => InvoiceStatus::PAID->value, 'payment_method' => 'balance', 'paid_at' => now(), 'due_date' => now()->toDateString(), ]); Service::create([ 'customer_id' => $this->customer->id, 'product_id' => $this->product->id, 'order_item_id' => $orderItem->id, 'invoice_id' => $invoice->id, 'server_name' => 'old-price.com', 'status' => DomainProvisioningStatus::PENDING->value, 'auto_renew' => false, ]); $balanceBefore = (int) $this->customer->fresh()->total; $this->actingAsAdmin()->postJson("/api/v1/administrator/domain-orders/{$order->id}/reject-and-refund", [ 'reason' => 'Price test.', ])->assertOk(); // Must refund 250k (invoice amount), not 320k (current pricing) $this->assertEquals($balanceBefore + 250_000, $this->customer->fresh()->total); } public function test_transaction_rollback_on_error(): void { // Order with no service — will throw inside the transaction $order = Order::create([ 'order_num' => OrderHelper::generateOrderNumber(), 'customer_id' => $this->customer->id, 'sub_total' => 320_000, 'discount_amount'=> 0, 'total' => 320_000, 'status' => OrderStatus::PROCESSING->value, 'payment_method' => 'balance', ]); OrderItem::create([ 'order_id' => $order->id, 'product_id' => $this->product->id, 'domain_name' => 'rollback.com', 'billing_cycle'=> 'yearly', 'months' => 12, 'quantity' => 1, 'price' => 320_000, 'server_name' => 'rollback.com', ]); // No Invoice, no Service — rejectAndRefund will fail $balanceBefore = (int) $this->customer->fresh()->total; $this->actingAsAdmin()->postJson("/api/v1/administrator/domain-orders/{$order->id}/reject-and-refund", [ 'reason' => 'Should rollback.', ])->assertStatus(422); // firstOrFail or invoice null → exception // Balance must not have changed $this->assertEquals($balanceBefore, $this->customer->fresh()->total); } }