artisan('migrate'); $this->seed(ParameterBasedBomTestSeeder::class); // Create test user and authenticate $this->user = User::factory()->create(['tenant_id' => 1]); Sanctum::actingAs($this->user); // Set required headers $this->withHeaders([ 'X-API-KEY' => config('app.api_key', 'test-api-key'), 'Accept' => 'application/json', 'Content-Type' => 'application/json', ]); $this->kss01Model = Model::where('code', 'KSS01')->first(); } /** @test */ public function it_can_get_model_parameters() { // Act $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters"); // Assert $response->assertOk() ->assertJsonStructure([ 'success', 'message', 'data' => [ '*' => [ 'id', 'name', 'label', 'type', 'default_value', 'validation_rules', 'options', 'sort_order', 'is_required', 'is_active', ] ] ]); $parameters = $response->json('data'); $this->assertCount(5, $parameters); // W0, H0, screen_type, install_type, power_source // Check parameter order $this->assertEquals('W0', $parameters[0]['name']); $this->assertEquals('H0', $parameters[1]['name']); $this->assertEquals('screen_type', $parameters[2]['name']); } /** @test */ public function it_can_get_model_formulas() { // Act $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/formulas"); // Assert $response->assertOk() ->assertJsonStructure([ 'success', 'message', 'data' => [ '*' => [ 'id', 'name', 'expression', 'description', 'return_type', 'sort_order', 'is_active', ] ] ]); $formulas = $response->json('data'); $this->assertGreaterThanOrEqual(7, count($formulas)); // W1, H1, area, weight, motor, bracket, guide // Check formula order $this->assertEquals('W1', $formulas[0]['name']); $this->assertEquals('H1', $formulas[1]['name']); $this->assertEquals('area', $formulas[2]['name']); } /** @test */ public function it_can_get_model_condition_rules() { // Act $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/rules"); // Assert $response->assertOk() ->assertJsonStructure([ 'success', 'message', 'data' => [ '*' => [ 'id', 'name', 'description', 'condition_expression', 'component_code', 'quantity_expression', 'priority', 'is_active', ] ] ]); $rules = $response->json('data'); $this->assertGreaterThanOrEqual(7, count($rules)); // Various case, bottom, shaft, pipe rules // Check rules are ordered by priority $priorities = collect($rules)->pluck('priority'); $this->assertEquals($priorities->sort()->values(), $priorities->values()); } /** @test */ public function it_can_resolve_bom_for_small_screen() { // Arrange $inputParams = [ 'W0' => 1000, 'H0' => 800, 'screen_type' => 'SCREEN', 'install_type' => 'WALL', 'power_source' => 'AC', ]; // Act $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ 'parameters' => $inputParams ]); // Assert $response->assertOk() ->assertJsonStructure([ 'success', 'message', 'data' => [ 'model_id', 'input_parameters', 'calculated_formulas' => [ 'W1', 'H1', 'area', 'weight', 'motor', 'bracket', 'guide', ], 'bom_items' => [ '*' => [ 'component_code', 'quantity', 'rule_name', 'condition_expression', ] ], 'summary' => [ 'total_components', 'total_weight', 'component_categories', ] ] ]); $data = $response->json('data'); // Check calculated formulas $formulas = $data['calculated_formulas']; $this->assertEquals(1120, $formulas['W1']); // 1000 + 120 $this->assertEquals(900, $formulas['H1']); // 800 + 100 $this->assertEquals(1.008, $formulas['area']); // 1120 * 900 / 1000000 $this->assertEquals('0.5HP', $formulas['motor']); // area <= 3 // Check BOM items $bomItems = $data['bom_items']; $this->assertGreaterThan(0, count($bomItems)); // Check specific components $caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE')); $this->assertEquals('CASE-SMALL', $caseItem['component_code']); $screenPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SCREEN'); $this->assertNotNull($screenPipe); $slatPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SLAT'); $this->assertNull($slatPipe); } /** @test */ public function it_can_resolve_bom_for_large_screen() { // Arrange $inputParams = [ 'W0' => 2500, 'H0' => 1500, 'screen_type' => 'SCREEN', 'install_type' => 'SIDE', 'power_source' => 'AC', ]; // Act $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ 'parameters' => $inputParams ]); // Assert $response->assertOk(); $data = $response->json('data'); $formulas = $data['calculated_formulas']; // Check large screen calculations $this->assertEquals(2620, $formulas['W1']); // 2500 + 120 $this->assertEquals(1600, $formulas['H1']); // 1500 + 100 $this->assertEquals(4.192, $formulas['area']); // 2620 * 1600 / 1000000 $this->assertEquals('1HP', $formulas['motor']); // 3 < area <= 6 // Check medium case is selected $bomItems = $data['bom_items']; $caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE')); $this->assertEquals('CASE-MEDIUM', $caseItem['component_code']); // Check bracket quantity $bottomItem = collect($bomItems)->first(fn($item) => $item['component_code'] === 'BOTTOM-001'); $this->assertEquals(3, $bottomItem['quantity']); // CEIL(2620 / 1000) } /** @test */ public function it_can_resolve_bom_for_slat_type() { // Arrange $inputParams = [ 'W0' => 1500, 'H0' => 1000, 'screen_type' => 'SLAT', 'install_type' => 'WALL', 'power_source' => 'AC', ]; // Act $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ 'parameters' => $inputParams ]); // Assert $response->assertOk(); $bomItems = $response->json('data.bom_items'); // Check that SLAT pipe is used $screenPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SCREEN'); $slatPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SLAT'); $this->assertNull($screenPipe); $this->assertNotNull($slatPipe); $this->assertEquals('PIPE-SLAT', $slatPipe['component_code']); } /** @test */ public function it_validates_missing_required_parameters() { // Arrange - Missing H0 parameter $incompleteParams = [ 'W0' => 1000, 'screen_type' => 'SCREEN', 'install_type' => 'WALL', 'power_source' => 'AC', ]; // Act $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ 'parameters' => $incompleteParams ]); // Assert $response->assertStatus(422) ->assertJsonValidationErrors(['parameters.H0']); } /** @test */ public function it_validates_parameter_ranges() { // Arrange - W0 below minimum $invalidParams = [ 'W0' => 100, // Below minimum (500) 'H0' => 800, 'screen_type' => 'SCREEN', 'install_type' => 'WALL', 'power_source' => 'AC', ]; // Act $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ 'parameters' => $invalidParams ]); // Assert $response->assertStatus(422) ->assertJsonValidationErrors(['parameters.W0']); } /** @test */ public function it_validates_select_parameter_options() { // Arrange - Invalid screen_type $invalidParams = [ 'W0' => 1000, 'H0' => 800, 'screen_type' => 'INVALID_TYPE', 'install_type' => 'WALL', 'power_source' => 'AC', ]; // Act $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ 'parameters' => $invalidParams ]); // Assert $response->assertStatus(422) ->assertJsonValidationErrors(['parameters.screen_type']); } /** @test */ public function it_can_create_product_from_model() { // Arrange $inputParams = [ 'W0' => 1200, 'H0' => 900, 'screen_type' => 'SCREEN', 'install_type' => 'WALL', 'power_source' => 'AC', ]; $productData = [ 'code' => 'KSS01-TEST-001', 'name' => '테스트 스크린 블라인드 1200x900', 'description' => 'API 테스트로 생성된 제품', ]; // Act $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/products", [ 'parameters' => $inputParams, 'product' => $productData, ]); // Assert $response->assertCreated() ->assertJsonStructure([ 'success', 'message', 'data' => [ 'product' => [ 'id', 'code', 'name', 'description', 'product_type', 'model_id', 'parameter_values', ], 'bom_items' => [ '*' => [ 'id', 'product_id', 'component_code', 'component_type', 'quantity', 'unit', ] ], 'summary' => [ 'total_components', 'calculated_formulas', ] ] ]); $data = $response->json('data'); $product = $data['product']; // Check product data $this->assertEquals('KSS01-TEST-001', $product['code']); $this->assertEquals('테스트 스크린 블라인드 1200x900', $product['name']); $this->assertEquals($this->kss01Model->id, $product['model_id']); // Check parameter values are stored $parameterValues = json_decode($product['parameter_values'], true); $this->assertEquals(1200, $parameterValues['W0']); $this->assertEquals(900, $parameterValues['H0']); // Check BOM items $bomItems = $data['bom_items']; $this->assertGreaterThan(0, count($bomItems)); foreach ($bomItems as $item) { $this->assertEquals($product['id'], $item['product_id']); $this->assertNotEmpty($item['component_code']); $this->assertGreaterThan(0, $item['quantity']); } } /** @test */ public function it_can_preview_product_without_creating() { // Arrange $inputParams = [ 'W0' => 1800, 'H0' => 1200, 'screen_type' => 'SCREEN', 'install_type' => 'SIDE', 'power_source' => 'DC', ]; // Act $response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/preview", [ 'parameters' => $inputParams ]); // Assert $response->assertOk() ->assertJsonStructure([ 'success', 'message', 'data' => [ 'suggested_product' => [ 'code', 'name', 'description', ], 'calculated_values' => [ 'W1', 'H1', 'area', 'weight', 'motor', ], 'bom_preview' => [ 'total_components', 'components' => [ '*' => [ 'component_code', 'quantity', 'description', ] ] ], 'cost_estimate' => [ 'total_material_cost', 'estimated_labor_cost', 'total_estimated_cost', ] ] ]); $data = $response->json('data'); // Check suggested product info $suggestedProduct = $data['suggested_product']; $this->assertStringContains('KSS01', $suggestedProduct['code']); $this->assertStringContains('1800x1200', $suggestedProduct['name']); // Check calculated values $calculatedValues = $data['calculated_values']; $this->assertEquals(1920, $calculatedValues['W1']); // 1800 + 120 $this->assertEquals(1300, $calculatedValues['H1']); // 1200 + 100 // Check BOM preview $bomPreview = $data['bom_preview']; $this->assertGreaterThan(0, $bomPreview['total_components']); $this->assertNotEmpty($bomPreview['components']); } /** @test */ public function it_handles_authentication_properly() { // Arrange - Remove authentication Sanctum::actingAs(null); // Act $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters"); // Assert $response->assertUnauthorized(); } /** @test */ public function it_handles_tenant_isolation() { // Arrange - Create user with different tenant $otherTenantUser = User::factory()->create(['tenant_id' => 2]); Sanctum::actingAs($otherTenantUser); // Act $response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters"); // Assert $response->assertNotFound(); } /** @test */ public function it_handles_performance_for_large_datasets() { // Arrange - Use performance test model with many parameters/formulas/rules $performanceModel = Model::where('code', 'PERF-TEST')->first(); if (!$performanceModel) { $this->markTestSkipped('Performance test model not found. Run in testing environment.'); } $inputParams = array_fill_keys( ModelParameter::where('model_id', $performanceModel->id)->pluck('name')->toArray(), 100 ); // Act $startTime = microtime(true); $response = $this->postJson("/api/v1/design/models/{$performanceModel->id}/bom/resolve", [ 'parameters' => $inputParams ]); $executionTime = microtime(true) - $startTime; // Assert $response->assertOk(); $this->assertLessThan(2.0, $executionTime, 'BOM resolution should complete within 2 seconds'); $data = $response->json('data'); $this->assertArrayHasKey('calculated_formulas', $data); $this->assertArrayHasKey('bom_items', $data); } /** @test */ public function it_returns_appropriate_error_for_nonexistent_model() { // Act $response = $this->getJson('/api/v1/design/models/999999/parameters'); // Assert $response->assertNotFound() ->assertJson([ 'success' => false, 'message' => 'error.model_not_found', ]); } /** @test */ public function it_handles_concurrent_requests_safely() { // Arrange $inputParams = [ 'W0' => 1000, 'H0' => 800, 'screen_type' => 'SCREEN', 'install_type' => 'WALL', 'power_source' => 'AC', ]; // Act - Simulate concurrent requests $promises = []; for ($i = 0; $i < 5; $i++) { $promises[] = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [ 'parameters' => $inputParams ]); } // Assert - All should succeed with same results foreach ($promises as $response) { $response->assertOk(); $formulas = $response->json('data.calculated_formulas'); $this->assertEquals(1120, $formulas['W1']); $this->assertEquals(900, $formulas['H1']); } } }