service = new BomConditionRuleService(); $this->service->setTenantId(1)->setApiUserId(1); $this->model = Model::factory()->screen()->create(); } /** @test */ public function it_can_get_all_rules_for_model() { // Arrange BomConditionRule::factory() ->count(3) ->create(['model_id' => $this->model->id]); // Act $result = $this->service->getRulesByModel($this->model->id); // Assert $this->assertCount(3, $result); $this->assertEquals($this->model->id, $result->first()->model_id); } /** @test */ public function it_filters_inactive_rules() { // Arrange BomConditionRule::factory() ->active() ->create(['model_id' => $this->model->id]); BomConditionRule::factory() ->inactive() ->create(['model_id' => $this->model->id]); // Act $result = $this->service->getRulesByModel($this->model->id); // Assert $this->assertCount(1, $result); $this->assertTrue($result->first()->is_active); } /** @test */ public function it_orders_rules_by_priority() { // Arrange BomConditionRule::factory() ->create(['model_id' => $this->model->id, 'priority' => 30, 'name' => 'low']); BomConditionRule::factory() ->create(['model_id' => $this->model->id, 'priority' => 10, 'name' => 'high']); BomConditionRule::factory() ->create(['model_id' => $this->model->id, 'priority' => 20, 'name' => 'medium']); // Act $result = $this->service->getRulesByModel($this->model->id); // Assert $this->assertEquals('high', $result->get(0)->name); $this->assertEquals('medium', $result->get(1)->name); $this->assertEquals('low', $result->get(2)->name); } /** @test */ public function it_can_create_rule() { // Arrange $data = [ 'name' => 'Test Rule', 'description' => 'Test description', 'condition_expression' => 'area > 5', 'component_code' => 'TEST-001', 'quantity_expression' => '2', 'priority' => 50, ]; // Act $result = $this->service->createRule($this->model->id, $data); // Assert $this->assertInstanceOf(BomConditionRule::class, $result); $this->assertEquals('Test Rule', $result->name); $this->assertEquals('area > 5', $result->condition_expression); $this->assertEquals('TEST-001', $result->component_code); $this->assertEquals($this->model->id, $result->model_id); $this->assertEquals(1, $result->tenant_id); } /** @test */ public function it_can_update_rule() { // Arrange $rule = BomConditionRule::factory() ->create(['model_id' => $this->model->id, 'name' => 'old_name']); $updateData = [ 'name' => 'new_name', 'condition_expression' => 'area <= 10', 'priority' => 100, ]; // Act $result = $this->service->updateRule($rule->id, $updateData); // Assert $this->assertEquals('new_name', $result->name); $this->assertEquals('area <= 10', $result->condition_expression); $this->assertEquals(100, $result->priority); $this->assertEquals(1, $result->updated_by); } /** @test */ public function it_can_delete_rule() { // Arrange $rule = BomConditionRule::factory() ->create(['model_id' => $this->model->id]); // Act $result = $this->service->deleteRule($rule->id); // Assert $this->assertTrue($result); $this->assertSoftDeleted('bom_condition_rules', ['id' => $rule->id]); } /** @test */ public function it_evaluates_simple_conditions() { // Arrange $rule = BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'condition_expression' => 'area > 5', 'component_code' => 'LARGE-CASE', 'quantity_expression' => '1', ]); // Test cases $testCases = [ ['area' => 3, 'expected' => false], ['area' => 6, 'expected' => true], ['area' => 5, 'expected' => false], // Exactly 5 should be false for > 5 ]; foreach ($testCases as $testCase) { // Act $result = $this->service->evaluateCondition($rule, $testCase); // Assert $this->assertEquals($testCase['expected'], $result); } } /** @test */ public function it_evaluates_complex_conditions() { // Arrange $rule = BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'condition_expression' => 'area > 3 AND screen_type = "SCREEN"', 'component_code' => 'SCREEN-CASE', 'quantity_expression' => '1', ]); // Test cases $testCases = [ ['area' => 5, 'screen_type' => 'SCREEN', 'expected' => true], ['area' => 5, 'screen_type' => 'SLAT', 'expected' => false], ['area' => 2, 'screen_type' => 'SCREEN', 'expected' => false], ['area' => 2, 'screen_type' => 'SLAT', 'expected' => false], ]; foreach ($testCases as $testCase) { // Act $result = $this->service->evaluateCondition($rule, $testCase); // Assert $this->assertEquals($testCase['expected'], $result, "Failed for area={$testCase['area']}, screen_type={$testCase['screen_type']}"); } } /** @test */ public function it_evaluates_quantity_expressions() { // Test simple quantity $rule1 = BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'quantity_expression' => '2', ]); $result1 = $this->service->evaluateQuantity($rule1, []); $this->assertEquals(2, $result1); // Test calculated quantity $rule2 = BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'quantity_expression' => 'CEIL(W1 / 1000)', ]); $result2 = $this->service->evaluateQuantity($rule2, ['W1' => 2500]); $this->assertEquals(3, $result2); // ceil(2500/1000) = 3 // Test formula-based quantity $rule3 = BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'quantity_expression' => 'W1 / 1000', ]); $result3 = $this->service->evaluateQuantity($rule3, ['W1' => 1500]); $this->assertEquals(1.5, $result3); } /** @test */ public function it_applies_rules_in_priority_order() { // Arrange - Create rules with different priorities $rules = [ BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'name' => 'High Priority Rule', 'condition_expression' => 'TRUE', 'component_code' => 'HIGH-PRIORITY', 'quantity_expression' => '1', 'priority' => 10, ]), BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'name' => 'Low Priority Rule', 'condition_expression' => 'TRUE', 'component_code' => 'LOW-PRIORITY', 'quantity_expression' => '1', 'priority' => 50, ]), BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'name' => 'Medium Priority Rule', 'condition_expression' => 'TRUE', 'component_code' => 'MEDIUM-PRIORITY', 'quantity_expression' => '1', 'priority' => 30, ]), ]; // Act $appliedRules = $this->service->applyRules($this->model->id, []); // Assert $this->assertCount(3, $appliedRules); $this->assertEquals('HIGH-PRIORITY', $appliedRules[0]['component_code']); $this->assertEquals('MEDIUM-PRIORITY', $appliedRules[1]['component_code']); $this->assertEquals('LOW-PRIORITY', $appliedRules[2]['component_code']); } /** @test */ public function it_skips_rules_with_false_conditions() { // Arrange BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'name' => 'Should Apply', 'condition_expression' => 'area > 3', 'component_code' => 'SHOULD-APPLY', 'quantity_expression' => '1', 'priority' => 10, ]); BomConditionRule::factory() ->create([ 'model_id' => $this->model->id, 'name' => 'Should Skip', 'condition_expression' => 'area <= 3', 'component_code' => 'SHOULD-SKIP', 'quantity_expression' => '1', 'priority' => 20, ]); // Act $appliedRules = $this->service->applyRules($this->model->id, ['area' => 5]); // Assert $this->assertCount(1, $appliedRules); $this->assertEquals('SHOULD-APPLY', $appliedRules[0]['component_code']); } /** @test */ public function it_validates_condition_syntax() { // Test valid conditions $validConditions = [ 'TRUE', 'FALSE', 'area > 5', 'area >= 5 AND W0 < 2000', 'screen_type = "SCREEN"', 'install_type != "WALL"', '(area > 3 AND screen_type = "SCREEN") OR install_type = "SIDE"', ]; foreach ($validConditions as $condition) { $isValid = $this->service->validateConditionSyntax($condition); $this->assertTrue($isValid, "Condition should be valid: {$condition}"); } // Test invalid conditions $invalidConditions = [ 'area > > 5', // Double operator 'area AND', // Incomplete expression 'unknown_var > 5', // Unknown variable (if validation is strict) 'area = "invalid"', // Type mismatch '', // Empty condition ]; foreach ($invalidConditions as $condition) { $isValid = $this->service->validateConditionSyntax($condition); $this->assertFalse($isValid, "Condition should be invalid: {$condition}"); } } /** @test */ public function it_respects_tenant_isolation() { // Arrange $otherTenantModel = Model::factory()->create(['tenant_id' => 2]); BomConditionRule::factory() ->create(['model_id' => $otherTenantModel->id, 'tenant_id' => 2]); // Act $result = $this->service->getRulesByModel($otherTenantModel->id); // Assert $this->assertCount(0, $result); } /** @test */ public function it_can_bulk_update_rules() { // Arrange $rules = BomConditionRule::factory() ->count(3) ->create(['model_id' => $this->model->id]); $updateData = [ [ 'id' => $rules[0]->id, 'name' => 'updated_rule_1', 'priority' => 5, ], [ 'id' => $rules[1]->id, 'condition_expression' => 'area > 10', 'priority' => 15, ], [ 'id' => $rules[2]->id, 'is_active' => false, ], ]; // Act $result = $this->service->bulkUpdateRules($this->model->id, $updateData); // Assert $this->assertTrue($result); $updated = BomConditionRule::whereIn('id', $rules->pluck('id'))->get(); $this->assertEquals('updated_rule_1', $updated->where('id', $rules[0]->id)->first()->name); $this->assertEquals('area > 10', $updated->where('id', $rules[1]->id)->first()->condition_expression); $this->assertFalse($updated->where('id', $rules[2]->id)->first()->is_active); } /** @test */ public function it_handles_complex_kss01_scenario() { // Arrange - Create KSS01 rules $rules = BomConditionRule::factory() ->screenRules() ->create(['model_id' => $this->model->id]); // Small screen test case $smallScreenParams = [ 'W0' => 1000, 'H0' => 800, 'W1' => 1120, 'H1' => 900, 'area' => 1.008, 'screen_type' => 'SCREEN', ]; // Act $appliedRules = $this->service->applyRules($this->model->id, $smallScreenParams); // Assert $this->assertGreaterThan(0, count($appliedRules)); // Check that case rule is applied correctly (small case for area <= 3) $caseRule = collect($appliedRules)->first(fn($rule) => str_contains($rule['component_code'], 'CASE')); $this->assertNotNull($caseRule); $this->assertEquals('CASE-SMALL', $caseRule['component_code']); // Check that screen-specific pipe is applied $pipeRule = collect($appliedRules)->first(fn($rule) => $rule['component_code'] === 'PIPE-SCREEN'); $this->assertNotNull($pipeRule); // Check that slat-specific pipe is NOT applied $slatPipeRule = collect($appliedRules)->first(fn($rule) => $rule['component_code'] === 'PIPE-SLAT'); $this->assertNull($slatPipeRule); } }