- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env) - 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget) - 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget) - 리소스 한국어화: Product, Material 모델 레이블 추가 - 대시보드: 위젯 등록 및 캐시 최적화 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
436 lines
14 KiB
PHP
436 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Models\BomConditionRule;
|
|
use App\Models\Model;
|
|
use App\Services\BomConditionRuleService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class BomConditionRuleServiceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private BomConditionRuleService $service;
|
|
private Model $model;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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);
|
|
}
|
|
} |