tenant = Tenant::factory()->create(); $this->user = User::factory()->create(); // Associate user with tenant $this->user->tenants()->attach($this->tenant->id, ['is_active' => true, 'is_default' => true]); // Create test design model $this->model = DesignModel::factory()->create([ 'tenant_id' => $this->tenant->id, 'code' => 'TEST01', 'name' => 'Test Model', 'is_active' => true ]); // Create test product and material $this->product = Product::factory()->create([ 'tenant_id' => $this->tenant->id, 'code' => 'PROD001', 'name' => 'Test Product' ]); $this->material = Material::factory()->create([ 'tenant_id' => $this->tenant->id, 'code' => 'MAT001', 'name' => 'Test Material' ]); // Create test parameters ModelParameter::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'parameter_name' => 'width', 'parameter_type' => 'NUMBER' ]); ModelParameter::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'parameter_name' => 'screen_type', 'parameter_type' => 'SELECT', 'options' => ['FABRIC', 'STEEL', 'PLASTIC'] ]); // Authenticate user Sanctum::actingAs($this->user, ['*']); Auth::login($this->user); } /** @test */ public function can_create_bom_condition_rule() { $ruleData = [ 'model_id' => $this->model->id, 'rule_name' => 'Large Width Rule', 'condition_expression' => 'width > 1000', 'action_type' => 'INCLUDE', 'target_type' => 'PRODUCT', 'target_id' => $this->product->id, 'quantity_multiplier' => 2.0, 'description' => 'Add extra product for large widths', 'sort_order' => 1 ]; $response = $this->postJson('/api/v1/design/bom-condition-rules', $ruleData); $response->assertStatus(201) ->assertJson([ 'success' => true, 'message' => 'message.created' ]); $this->assertDatabaseHas('bom_condition_rules', [ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Large Width Rule', 'condition_expression' => 'width > 1000', 'action_type' => 'INCLUDE' ]); } /** @test */ public function can_list_bom_condition_rules() { // Create test rules BomConditionRule::factory()->count(3)->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id ]); $response = $this->getJson('/api/v1/design/bom-condition-rules?model_id=' . $this->model->id); $response->assertStatus(200) ->assertJson([ 'success' => true, 'message' => 'message.fetched' ]) ->assertJsonStructure([ 'data' => [ 'data' => [ '*' => [ 'id', 'rule_name', 'condition_expression', 'action_type', 'target_type', 'target_id', 'quantity_multiplier', 'description', 'sort_order' ] ] ] ]); } /** @test */ public function can_update_bom_condition_rule() { $rule = BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Original Rule', 'condition_expression' => 'width > 500' ]); $updateData = [ 'rule_name' => 'Updated Rule', 'condition_expression' => 'width > 800', 'quantity_multiplier' => 1.5, 'description' => 'Updated description' ]; $response = $this->putJson('/api/v1/design/bom-condition-rules/' . $rule->id, $updateData); $response->assertStatus(200) ->assertJson([ 'success' => true, 'message' => 'message.updated' ]); $this->assertDatabaseHas('bom_condition_rules', [ 'id' => $rule->id, 'rule_name' => 'Updated Rule', 'condition_expression' => 'width > 800', 'quantity_multiplier' => 1.5 ]); } /** @test */ public function can_delete_bom_condition_rule() { $rule = BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id ]); $response = $this->deleteJson('/api/v1/design/bom-condition-rules/' . $rule->id); $response->assertStatus(200) ->assertJson([ 'success' => true, 'message' => 'message.deleted' ]); $this->assertSoftDeleted('bom_condition_rules', [ 'id' => $rule->id ]); } /** @test */ public function validates_condition_expression_syntax() { // Test invalid expression $invalidData = [ 'model_id' => $this->model->id, 'rule_name' => 'Invalid Rule', 'condition_expression' => 'width >>> 1000', // Invalid syntax 'action_type' => 'INCLUDE', 'target_type' => 'PRODUCT', 'target_id' => $this->product->id ]; $response = $this->postJson('/api/v1/design/bom-condition-rules', $invalidData); $response->assertStatus(422); // Test valid expression $validData = [ 'model_id' => $this->model->id, 'rule_name' => 'Valid Rule', 'condition_expression' => 'width > 1000 AND screen_type == "FABRIC"', 'action_type' => 'INCLUDE', 'target_type' => 'PRODUCT', 'target_id' => $this->product->id ]; $response = $this->postJson('/api/v1/design/bom-condition-rules', $validData); $response->assertStatus(201); } /** @test */ public function can_evaluate_simple_conditions() { // Create simple numeric rule BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Width Rule', 'condition_expression' => 'width > 1000', 'action_type' => 'INCLUDE', 'target_type' => 'PRODUCT', 'target_id' => $this->product->id ]); $parameters = ['width' => 1200]; $response = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ 'model_id' => $this->model->id, 'parameters' => $parameters ]); $response->assertStatus(200) ->assertJson([ 'success' => true, 'data' => [ 'matched_rules' => [ [ 'rule_name' => 'Width Rule', 'condition_result' => true ] ] ] ]); } /** @test */ public function can_evaluate_complex_conditions() { // Create complex rule with multiple conditions BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Complex Rule', 'condition_expression' => 'width > 800 AND screen_type == "FABRIC"', 'action_type' => 'INCLUDE', 'target_type' => 'MATERIAL', 'target_id' => $this->material->id ]); // Test case 1: Both conditions true $parameters1 = ['width' => 1000, 'screen_type' => 'FABRIC']; $response1 = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ 'model_id' => $this->model->id, 'parameters' => $parameters1 ]); $response1->assertStatus(200); $this->assertTrue($response1->json('data.matched_rules.0.condition_result')); // Test case 2: One condition false $parameters2 = ['width' => 1000, 'screen_type' => 'STEEL']; $response2 = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ 'model_id' => $this->model->id, 'parameters' => $parameters2 ]); $response2->assertStatus(200); $this->assertFalse($response2->json('data.matched_rules.0.condition_result')); } /** @test */ public function can_handle_different_action_types() { // Create rules for each action type $includeRule = BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Include Rule', 'condition_expression' => 'width > 1000', 'action_type' => 'INCLUDE', 'target_type' => 'PRODUCT', 'target_id' => $this->product->id, 'quantity_multiplier' => 2.0 ]); $excludeRule = BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Exclude Rule', 'condition_expression' => 'screen_type == "PLASTIC"', 'action_type' => 'EXCLUDE', 'target_type' => 'MATERIAL', 'target_id' => $this->material->id ]); $modifyRule = BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Modify Rule', 'condition_expression' => 'width > 1500', 'action_type' => 'MODIFY_QUANTITY', 'target_type' => 'PRODUCT', 'target_id' => $this->product->id, 'quantity_multiplier' => 1.5 ]); $parameters = ['width' => 1600, 'screen_type' => 'PLASTIC']; $response = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ 'model_id' => $this->model->id, 'parameters' => $parameters ]); $response->assertStatus(200); $data = $response->json('data'); $this->assertCount(3, $data['bom_actions']); // All three rules should match // Check action types $actionTypes = collect($data['bom_actions'])->pluck('action_type')->toArray(); $this->assertContains('INCLUDE', $actionTypes); $this->assertContains('EXCLUDE', $actionTypes); $this->assertContains('MODIFY_QUANTITY', $actionTypes); } /** @test */ public function can_handle_rule_dependencies_and_order() { // Create rules with different priorities (sort_order) BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'High Priority Rule', 'condition_expression' => 'width > 500', 'action_type' => 'INCLUDE', 'target_type' => 'PRODUCT', 'target_id' => $this->product->id, 'sort_order' => 1 ]); BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Low Priority Rule', 'condition_expression' => 'width > 500', 'action_type' => 'EXCLUDE', 'target_type' => 'PRODUCT', 'target_id' => $this->product->id, 'sort_order' => 2 ]); $parameters = ['width' => 1000]; $response = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ 'model_id' => $this->model->id, 'parameters' => $parameters ]); $response->assertStatus(200); $actions = $response->json('data.bom_actions'); $this->assertEquals('INCLUDE', $actions[0]['action_type']); // Higher priority first $this->assertEquals('EXCLUDE', $actions[1]['action_type']); } /** @test */ public function can_validate_target_references() { // Test invalid product reference $invalidProductData = [ 'model_id' => $this->model->id, 'rule_name' => 'Invalid Product Rule', 'condition_expression' => 'width > 1000', 'action_type' => 'INCLUDE', 'target_type' => 'PRODUCT', 'target_id' => 99999 // Non-existent product ]; $response = $this->postJson('/api/v1/design/bom-condition-rules', $invalidProductData); $response->assertStatus(422); // Test invalid material reference $invalidMaterialData = [ 'model_id' => $this->model->id, 'rule_name' => 'Invalid Material Rule', 'condition_expression' => 'width > 1000', 'action_type' => 'INCLUDE', 'target_type' => 'MATERIAL', 'target_id' => 99999 // Non-existent material ]; $response = $this->postJson('/api/v1/design/bom-condition-rules', $invalidMaterialData); $response->assertStatus(422); } /** @test */ public function enforces_tenant_isolation() { // Create rule for different tenant $otherTenant = Tenant::factory()->create(); $otherRule = BomConditionRule::factory()->create([ 'tenant_id' => $otherTenant->id ]); // Should not be able to access other tenant's rule $response = $this->getJson('/api/v1/design/bom-condition-rules/' . $otherRule->id); $response->assertStatus(404); } /** @test */ public function can_test_rule_against_multiple_scenarios() { BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Multi Test Rule', 'condition_expression' => 'width > 1000 OR screen_type == "STEEL"', 'action_type' => 'INCLUDE', 'target_type' => 'PRODUCT', 'target_id' => $this->product->id ]); $testScenarios = [ ['width' => 1200, 'screen_type' => 'FABRIC'], // Should match (width > 1000) ['width' => 800, 'screen_type' => 'STEEL'], // Should match (screen_type == STEEL) ['width' => 800, 'screen_type' => 'FABRIC'], // Should not match ['width' => 1200, 'screen_type' => 'STEEL'] // Should match (both conditions) ]; $response = $this->postJson('/api/v1/design/bom-condition-rules/test-scenarios', [ 'model_id' => $this->model->id, 'scenarios' => $testScenarios ]); $response->assertStatus(200); $results = $response->json('data.scenario_results'); $this->assertTrue($results[0]['matched']); $this->assertTrue($results[1]['matched']); $this->assertFalse($results[2]['matched']); $this->assertTrue($results[3]['matched']); } /** @test */ public function can_clone_rules_between_models() { // Create source rules $sourceRules = BomConditionRule::factory()->count(3)->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id ]); // Create target model $targetModel = DesignModel::factory()->create([ 'tenant_id' => $this->tenant->id, 'code' => 'TARGET01' ]); $response = $this->postJson('/api/v1/design/bom-condition-rules/clone', [ 'source_model_id' => $this->model->id, 'target_model_id' => $targetModel->id ]); $response->assertStatus(200) ->assertJson([ 'success' => true, 'message' => 'message.cloned' ]); // Verify rules were cloned $this->assertDatabaseCount('bom_condition_rules', 6); // 3 original + 3 cloned $clonedRules = BomConditionRule::where('model_id', $targetModel->id)->get(); $this->assertCount(3, $clonedRules); } /** @test */ public function can_handle_expression_with_calculated_values() { // Create formula that will be used in condition ModelFormula::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'formula_name' => 'area', 'expression' => 'width * 600' ]); // Create rule that uses calculated value BomConditionRule::factory()->create([ 'tenant_id' => $this->tenant->id, 'model_id' => $this->model->id, 'rule_name' => 'Area Rule', 'condition_expression' => 'area > 600000', 'action_type' => 'INCLUDE', 'target_type' => 'MATERIAL', 'target_id' => $this->material->id ]); $parameters = ['width' => 1200]; // area = 1200 * 600 = 720000 $response = $this->postJson('/api/v1/design/bom-condition-rules/evaluate', [ 'model_id' => $this->model->id, 'parameters' => $parameters ]); $response->assertStatus(200); $this->assertTrue($response->json('data.matched_rules.0.condition_result')); } }