customer = Customer::factory()->create(['total' => 1_000_000]); // Minimal domain product — product_type is set automatically from category // but we force it directly since we have no category seeder in tests $this->product = Product::factory()->create([ 'product_type' => 'domain', 'is_active' => true, ]); Setting::create([ 'key' => 'domain_pricing', 'value' => json_encode([ 'domains' => [ ['tld' => '.com.vn', 'firstYear' => 300_000, 'renewal' => 300_000], ['tld' => '.vn', 'firstYear' => 400_000, 'renewal' => 400_000], ['tld' => '.com', 'firstYear' => 320_000, 'renewal' => 340_000], ], ]), ]); } private function actingAsCustomer(): static { return $this->actingAs($this->customer, 'client'); } // ========================================================================= public function test_authenticated_customer_can_place_domain_order(): void { $response = $this->actingAsCustomer()->postJson('/v1/client/domain-orders', [ 'product_uuid' => $this->product->uuid, 'domain_name' => 'example.com', 'years' => 1, ]); $response->assertStatus(201) ->assertJsonPath('success', true) ->assertJsonPath('data.domain_name', 'example.com') ->assertJsonPath('data.status', 'pending'); $this->assertDatabaseHas('orders', ['status' => OrderStatus::PROCESSING->value]); $this->assertDatabaseHas('order_items', ['domain_name' => 'example.com']); $this->assertDatabaseHas('services', ['status' => DomainProvisioningStatus::PENDING->value]); $this->assertEquals(1_000_000 - 320_000, $this->customer->fresh()->total); } public function test_insufficient_balance_returns_422(): void { $this->customer->update(['total' => 1_000]); $response = $this->actingAsCustomer()->postJson('/v1/client/domain-orders', [ 'product_uuid' => $this->product->uuid, 'domain_name' => 'test.com', 'years' => 1, ]); $response->assertStatus(422)->assertJsonPath('success', false); $this->assertEquals(1_000, $this->customer->fresh()->total); } public function test_duplicate_pending_domain_is_rejected(): void { // Place the first order $this->actingAsCustomer()->postJson('/v1/client/domain-orders', [ 'product_uuid' => $this->product->uuid, 'domain_name' => 'taken.com', 'years' => 1, ])->assertStatus(201); // Replenish balance for second attempt $this->customer->update(['total' => 1_000_000]); $response = $this->actingAsCustomer()->postJson('/v1/client/domain-orders', [ 'product_uuid' => $this->product->uuid, 'domain_name' => 'taken.com', 'years' => 1, ]); $response->assertStatus(422)->assertJsonPath('success', false); } public function test_unauthenticated_cannot_place_order(): void { $this->postJson('/v1/client/domain-orders', [ 'product_uuid' => $this->product->uuid, 'domain_name' => 'test.com', ])->assertStatus(401); } public function test_invalid_product_type_rejected(): void { $nonDomainProduct = Product::factory()->create([ 'product_type' => 'vps', 'is_active' => true, ]); $response = $this->actingAsCustomer()->postJson('/v1/client/domain-orders', [ 'product_uuid' => $nonDomainProduct->uuid, 'domain_name' => 'test.com', 'years' => 1, ]); // Product::firstOrFail() will throw ModelNotFoundException → 500, or service rejects it $response->assertStatus(500); } public function test_domain_normalized_to_lowercase(): void { $response = $this->actingAsCustomer()->postJson('/v1/client/domain-orders', [ 'product_uuid' => $this->product->uuid, 'domain_name' => 'HTTPS://MY-SITE.COM/path', 'years' => 1, ]); $response->assertStatus(201); $this->assertDatabaseHas('order_items', ['domain_name' => 'my-site.com']); } public function test_unknown_tld_returns_422(): void { $response = $this->actingAsCustomer()->postJson('/v1/client/domain-orders', [ 'product_uuid' => $this->product->uuid, 'domain_name' => 'test.xyz', 'years' => 1, ]); $response->assertStatus(422)->assertJsonPath('success', false); } public function test_years_defaults_to_1_when_not_provided(): void { $response = $this->actingAsCustomer()->postJson('/v1/client/domain-orders', [ 'product_uuid' => $this->product->uuid, 'domain_name' => 'default-years.com', ]); $response->assertStatus(201); $this->assertDatabaseHas('order_items', ['domain_name' => 'default-years.com', 'months' => 12]); } }