Files
sam-api/tests/Feature/Design/BomConditionRuleTest.php
kent bf8036a64b feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가
- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env)
- 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget)
- 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget)
- 리소스 한국어화: Product, Material 모델 레이블 추가
- 대시보드: 위젯 등록 및 캐시 최적화

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 23:31:14 +09:00

539 lines
18 KiB
PHP

<?php
namespace Tests\Feature\Design;
use Tests\TestCase;
use App\Models\Design\DesignModel;
use App\Models\Design\ModelParameter;
use App\Models\Design\ModelFormula;
use App\Models\Design\BomConditionRule;
use App\Models\Product;
use App\Models\Material;
use App\Models\User;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Laravel\Sanctum\Sanctum;
class BomConditionRuleTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Tenant $tenant;
private DesignModel $model;
private Product $product;
private Material $material;
protected function setUp(): void
{
parent::setUp();
// Create test tenant and user
$this->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'));
}
}