Files
portal-api/tests/Feature/Api/DomainOrder/AdminDomainOrderTest.php
T
baodev0 dfb926055d 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.
2026-07-10 14:16:35 +07:00

400 lines
16 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Api\DomainOrder;
use App\Enums\Domain\DomainProvisioningStatus;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Helpers\OrderHelper;
use App\Models\Customer;
use App\Models\DnsDomain;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use App\Models\Service;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
class AdminDomainOrderTest extends TestCase
{
use RefreshDatabase;
private User $admin;
private Customer $customer;
private Product $product;
private int $dnsServerId;
protected function setUp(): void
{
parent::setUp();
Role::firstOrCreate(['name' => '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);
}
}