Revert "feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가"
This reverts commit bf8036a64b.
This commit is contained in:
@@ -1,436 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,383 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Model;
|
||||
use App\Models\ModelFormula;
|
||||
use App\Models\ModelParameter;
|
||||
use App\Services\ModelFormulaService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ModelFormulaServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private ModelFormulaService $service;
|
||||
private Model $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->service = new ModelFormulaService();
|
||||
$this->service->setTenantId(1)->setApiUserId(1);
|
||||
|
||||
$this->model = Model::factory()->screen()->create();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_get_all_formulas_for_model()
|
||||
{
|
||||
// Arrange
|
||||
ModelFormula::factory()
|
||||
->count(3)
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
// Act
|
||||
$result = $this->service->getFormulasByModel($this->model->id);
|
||||
|
||||
// Assert
|
||||
$this->assertCount(3, $result);
|
||||
$this->assertEquals($this->model->id, $result->first()->model_id);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_filters_inactive_formulas()
|
||||
{
|
||||
// Arrange
|
||||
ModelFormula::factory()
|
||||
->create(['model_id' => $this->model->id, 'is_active' => true]);
|
||||
ModelFormula::factory()
|
||||
->create(['model_id' => $this->model->id, 'is_active' => false]);
|
||||
|
||||
// Act
|
||||
$result = $this->service->getFormulasByModel($this->model->id);
|
||||
|
||||
// Assert
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertTrue($result->first()->is_active);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_orders_formulas_by_sort_order()
|
||||
{
|
||||
// Arrange
|
||||
ModelFormula::factory()
|
||||
->create(['model_id' => $this->model->id, 'sort_order' => 3, 'name' => 'third']);
|
||||
ModelFormula::factory()
|
||||
->create(['model_id' => $this->model->id, 'sort_order' => 1, 'name' => 'first']);
|
||||
ModelFormula::factory()
|
||||
->create(['model_id' => $this->model->id, 'sort_order' => 2, 'name' => 'second']);
|
||||
|
||||
// Act
|
||||
$result = $this->service->getFormulasByModel($this->model->id);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals('first', $result->get(0)->name);
|
||||
$this->assertEquals('second', $result->get(1)->name);
|
||||
$this->assertEquals('third', $result->get(2)->name);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_create_formula()
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'name' => 'test_formula',
|
||||
'expression' => 'W0 * H0',
|
||||
'description' => 'Test calculation',
|
||||
'return_type' => 'NUMBER',
|
||||
'sort_order' => 1,
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->service->createFormula($this->model->id, $data);
|
||||
|
||||
// Assert
|
||||
$this->assertInstanceOf(ModelFormula::class, $result);
|
||||
$this->assertEquals('test_formula', $result->name);
|
||||
$this->assertEquals('W0 * H0', $result->expression);
|
||||
$this->assertEquals($this->model->id, $result->model_id);
|
||||
$this->assertEquals(1, $result->tenant_id);
|
||||
$this->assertEquals(1, $result->created_by);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_update_formula()
|
||||
{
|
||||
// Arrange
|
||||
$formula = ModelFormula::factory()
|
||||
->create(['model_id' => $this->model->id, 'name' => 'old_name']);
|
||||
|
||||
$updateData = [
|
||||
'name' => 'new_name',
|
||||
'expression' => 'W0 + H0',
|
||||
'description' => 'Updated description',
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->service->updateFormula($formula->id, $updateData);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals('new_name', $result->name);
|
||||
$this->assertEquals('W0 + H0', $result->expression);
|
||||
$this->assertEquals('Updated description', $result->description);
|
||||
$this->assertEquals(1, $result->updated_by);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_delete_formula()
|
||||
{
|
||||
// Arrange
|
||||
$formula = ModelFormula::factory()
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
// Act
|
||||
$result = $this->service->deleteFormula($formula->id);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($result);
|
||||
$this->assertSoftDeleted('model_formulas', ['id' => $formula->id]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_evaluates_simple_arithmetic_expressions()
|
||||
{
|
||||
// Arrange
|
||||
$formula = ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'expression' => 'W0 * H0',
|
||||
'return_type' => 'NUMBER',
|
||||
]);
|
||||
|
||||
$parameters = ['W0' => 1000, 'H0' => 800];
|
||||
|
||||
// Act
|
||||
$result = $this->service->evaluateFormula($formula, $parameters);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals(800000, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_evaluates_complex_expressions_with_functions()
|
||||
{
|
||||
// Arrange
|
||||
$formula = ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'expression' => 'CEIL(W0 / 600)',
|
||||
'return_type' => 'NUMBER',
|
||||
]);
|
||||
|
||||
$parameters = ['W0' => 1400];
|
||||
|
||||
// Act
|
||||
$result = $this->service->evaluateFormula($formula, $parameters);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals(3, $result); // ceil(1400/600) = ceil(2.33) = 3
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_evaluates_conditional_expressions()
|
||||
{
|
||||
// Arrange
|
||||
$formula = ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'expression' => 'IF(area <= 3, "SMALL", IF(area <= 6, "MEDIUM", "LARGE"))',
|
||||
'return_type' => 'STRING',
|
||||
]);
|
||||
|
||||
// Test cases
|
||||
$testCases = [
|
||||
['area' => 2, 'expected' => 'SMALL'],
|
||||
['area' => 5, 'expected' => 'MEDIUM'],
|
||||
['area' => 8, 'expected' => 'LARGE'],
|
||||
];
|
||||
|
||||
foreach ($testCases as $testCase) {
|
||||
// Act
|
||||
$result = $this->service->evaluateFormula($formula, $testCase);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals($testCase['expected'], $result);
|
||||
}
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_formula_dependencies()
|
||||
{
|
||||
// Arrange - Create formulas with dependencies
|
||||
$w1Formula = ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'name' => 'W1',
|
||||
'expression' => 'W0 + 120',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
$h1Formula = ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'name' => 'H1',
|
||||
'expression' => 'H0 + 100',
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
|
||||
$areaFormula = ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'name' => 'area',
|
||||
'expression' => 'W1 * H1 / 1000000',
|
||||
'sort_order' => 3,
|
||||
]);
|
||||
|
||||
$parameters = ['W0' => 1000, 'H0' => 800];
|
||||
|
||||
// Act
|
||||
$results = $this->service->evaluateAllFormulas($this->model->id, $parameters);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals(1120, $results['W1']); // 1000 + 120
|
||||
$this->assertEquals(900, $results['H1']); // 800 + 100
|
||||
$this->assertEquals(1.008, $results['area']); // 1120 * 900 / 1000000
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_formula_syntax()
|
||||
{
|
||||
// Test valid expressions
|
||||
$validExpressions = [
|
||||
'W0 + H0',
|
||||
'W0 * H0 / 1000',
|
||||
'CEIL(W0 / 600)',
|
||||
'IF(area > 5, "LARGE", "SMALL")',
|
||||
'SIN(angle * PI / 180)',
|
||||
];
|
||||
|
||||
foreach ($validExpressions as $expression) {
|
||||
$isValid = $this->service->validateExpressionSyntax($expression);
|
||||
$this->assertTrue($isValid, "Expression should be valid: {$expression}");
|
||||
}
|
||||
|
||||
// Test invalid expressions
|
||||
$invalidExpressions = [
|
||||
'W0 + + H0', // Double operator
|
||||
'W0 * )', // Unmatched parenthesis
|
||||
'UNKNOWN_FUNC(W0)', // Unknown function
|
||||
'', // Empty expression
|
||||
'W0 AND', // Incomplete expression
|
||||
];
|
||||
|
||||
foreach ($invalidExpressions as $expression) {
|
||||
$isValid = $this->service->validateExpressionSyntax($expression);
|
||||
$this->assertFalse($isValid, "Expression should be invalid: {$expression}");
|
||||
}
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_detects_circular_dependencies()
|
||||
{
|
||||
// Arrange - Create circular dependency
|
||||
ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'name' => 'A',
|
||||
'expression' => 'B + 10',
|
||||
]);
|
||||
|
||||
ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'name' => 'B',
|
||||
'expression' => 'C * 2',
|
||||
]);
|
||||
|
||||
ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'name' => 'C',
|
||||
'expression' => 'A / 3', // Circular dependency
|
||||
]);
|
||||
|
||||
// Act & Assert
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Circular dependency detected');
|
||||
|
||||
$this->service->evaluateAllFormulas($this->model->id, []);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_respects_tenant_isolation()
|
||||
{
|
||||
// Arrange
|
||||
$otherTenantModel = Model::factory()->create(['tenant_id' => 2]);
|
||||
ModelFormula::factory()
|
||||
->create(['model_id' => $otherTenantModel->id, 'tenant_id' => 2]);
|
||||
|
||||
// Act
|
||||
$result = $this->service->getFormulasByModel($otherTenantModel->id);
|
||||
|
||||
// Assert
|
||||
$this->assertCount(0, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_missing_parameters_gracefully()
|
||||
{
|
||||
// Arrange
|
||||
$formula = ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'expression' => 'W0 * H0',
|
||||
]);
|
||||
|
||||
$incompleteParameters = ['W0' => 1000]; // Missing H0
|
||||
|
||||
// Act & Assert
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Missing parameter: H0');
|
||||
|
||||
$this->service->evaluateFormula($formula, $incompleteParameters);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_bulk_update_formulas()
|
||||
{
|
||||
// Arrange
|
||||
$formulas = ModelFormula::factory()
|
||||
->count(3)
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
$updateData = [
|
||||
[
|
||||
'id' => $formulas[0]->id,
|
||||
'name' => 'updated_formula_1',
|
||||
'expression' => 'W0 + 100',
|
||||
],
|
||||
[
|
||||
'id' => $formulas[1]->id,
|
||||
'name' => 'updated_formula_2',
|
||||
'expression' => 'H0 + 50',
|
||||
],
|
||||
[
|
||||
'id' => $formulas[2]->id,
|
||||
'is_active' => false,
|
||||
],
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->service->bulkUpdateFormulas($this->model->id, $updateData);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($result);
|
||||
|
||||
$updated = ModelFormula::whereIn('id', $formulas->pluck('id'))->get();
|
||||
$this->assertEquals('updated_formula_1', $updated->where('id', $formulas[0]->id)->first()->name);
|
||||
$this->assertEquals('W0 + 100', $updated->where('id', $formulas[0]->id)->first()->expression);
|
||||
$this->assertFalse($updated->where('id', $formulas[2]->id)->first()->is_active);
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Model;
|
||||
use App\Models\ModelParameter;
|
||||
use App\Services\ModelParameterService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ModelParameterServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private ModelParameterService $service;
|
||||
private Model $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->service = new ModelParameterService();
|
||||
$this->service->setTenantId(1)->setApiUserId(1);
|
||||
|
||||
$this->model = Model::factory()->screen()->create();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_get_all_parameters_for_model()
|
||||
{
|
||||
// Arrange
|
||||
ModelParameter::factory()
|
||||
->count(3)
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
// Act
|
||||
$result = $this->service->getParametersByModel($this->model->id);
|
||||
|
||||
// Assert
|
||||
$this->assertCount(3, $result);
|
||||
$this->assertEquals($this->model->id, $result->first()->model_id);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_filters_inactive_parameters()
|
||||
{
|
||||
// Arrange
|
||||
ModelParameter::factory()
|
||||
->create(['model_id' => $this->model->id, 'is_active' => true]);
|
||||
ModelParameter::factory()
|
||||
->create(['model_id' => $this->model->id, 'is_active' => false]);
|
||||
|
||||
// Act
|
||||
$result = $this->service->getParametersByModel($this->model->id);
|
||||
|
||||
// Assert
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertTrue($result->first()->is_active);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_orders_parameters_by_sort_order()
|
||||
{
|
||||
// Arrange
|
||||
$param1 = ModelParameter::factory()
|
||||
->create(['model_id' => $this->model->id, 'sort_order' => 3, 'name' => 'third']);
|
||||
$param2 = ModelParameter::factory()
|
||||
->create(['model_id' => $this->model->id, 'sort_order' => 1, 'name' => 'first']);
|
||||
$param3 = ModelParameter::factory()
|
||||
->create(['model_id' => $this->model->id, 'sort_order' => 2, 'name' => 'second']);
|
||||
|
||||
// Act
|
||||
$result = $this->service->getParametersByModel($this->model->id);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals('first', $result->get(0)->name);
|
||||
$this->assertEquals('second', $result->get(1)->name);
|
||||
$this->assertEquals('third', $result->get(2)->name);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_create_parameter()
|
||||
{
|
||||
// Arrange
|
||||
$data = [
|
||||
'name' => 'test_param',
|
||||
'label' => 'Test Parameter',
|
||||
'type' => 'NUMBER',
|
||||
'default_value' => '100',
|
||||
'validation_rules' => ['required' => true, 'numeric' => true],
|
||||
'sort_order' => 1,
|
||||
'is_required' => true,
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->service->createParameter($this->model->id, $data);
|
||||
|
||||
// Assert
|
||||
$this->assertInstanceOf(ModelParameter::class, $result);
|
||||
$this->assertEquals('test_param', $result->name);
|
||||
$this->assertEquals($this->model->id, $result->model_id);
|
||||
$this->assertEquals(1, $result->tenant_id);
|
||||
$this->assertEquals(1, $result->created_by);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_update_parameter()
|
||||
{
|
||||
// Arrange
|
||||
$parameter = ModelParameter::factory()
|
||||
->create(['model_id' => $this->model->id, 'name' => 'old_name']);
|
||||
|
||||
$updateData = [
|
||||
'name' => 'new_name',
|
||||
'label' => 'New Label',
|
||||
'default_value' => '200',
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->service->updateParameter($parameter->id, $updateData);
|
||||
|
||||
// Assert
|
||||
$this->assertEquals('new_name', $result->name);
|
||||
$this->assertEquals('New Label', $result->label);
|
||||
$this->assertEquals('200', $result->default_value);
|
||||
$this->assertEquals(1, $result->updated_by);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_delete_parameter()
|
||||
{
|
||||
// Arrange
|
||||
$parameter = ModelParameter::factory()
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
// Act
|
||||
$result = $this->service->deleteParameter($parameter->id);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($result);
|
||||
$this->assertSoftDeleted('model_parameters', ['id' => $parameter->id]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_respects_tenant_isolation()
|
||||
{
|
||||
// Arrange
|
||||
$otherTenantModel = Model::factory()->create(['tenant_id' => 2]);
|
||||
$otherTenantParameter = ModelParameter::factory()
|
||||
->create(['model_id' => $otherTenantModel->id, 'tenant_id' => 2]);
|
||||
|
||||
// Act
|
||||
$result = $this->service->getParametersByModel($otherTenantModel->id);
|
||||
|
||||
// Assert
|
||||
$this->assertCount(0, $result);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_parameter_types()
|
||||
{
|
||||
// Test NUMBER type
|
||||
$numberParam = ModelParameter::factory()
|
||||
->number()
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
$this->assertEquals('NUMBER', $numberParam->type);
|
||||
$this->assertNull($numberParam->options);
|
||||
|
||||
// Test SELECT type
|
||||
$selectParam = ModelParameter::factory()
|
||||
->select()
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
$this->assertEquals('SELECT', $selectParam->type);
|
||||
$this->assertNotNull($selectParam->options);
|
||||
|
||||
// Test BOOLEAN type
|
||||
$booleanParam = ModelParameter::factory()
|
||||
->boolean()
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
$this->assertEquals('BOOLEAN', $booleanParam->type);
|
||||
$this->assertEquals('false', $booleanParam->default_value);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_bulk_update_parameters()
|
||||
{
|
||||
// Arrange
|
||||
$parameters = ModelParameter::factory()
|
||||
->count(3)
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
$updateData = [
|
||||
[
|
||||
'id' => $parameters[0]->id,
|
||||
'name' => 'updated_param_1',
|
||||
'sort_order' => 10,
|
||||
],
|
||||
[
|
||||
'id' => $parameters[1]->id,
|
||||
'name' => 'updated_param_2',
|
||||
'sort_order' => 20,
|
||||
],
|
||||
[
|
||||
'id' => $parameters[2]->id,
|
||||
'is_active' => false,
|
||||
],
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->service->bulkUpdateParameters($this->model->id, $updateData);
|
||||
|
||||
// Assert
|
||||
$this->assertTrue($result);
|
||||
|
||||
$updated = ModelParameter::whereIn('id', $parameters->pluck('id'))->get();
|
||||
$this->assertEquals('updated_param_1', $updated->where('id', $parameters[0]->id)->first()->name);
|
||||
$this->assertEquals('updated_param_2', $updated->where('id', $parameters[1]->id)->first()->name);
|
||||
$this->assertFalse($updated->where('id', $parameters[2]->id)->first()->is_active);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_required_fields()
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$this->service->createParameter($this->model->id, [
|
||||
'label' => 'Missing Name Parameter',
|
||||
'type' => 'NUMBER',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_parameter_validation_rules()
|
||||
{
|
||||
// Arrange
|
||||
$parameter = ModelParameter::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'type' => 'NUMBER',
|
||||
'validation_rules' => json_encode([
|
||||
'required' => true,
|
||||
'numeric' => true,
|
||||
'min' => 100,
|
||||
'max' => 1000,
|
||||
]),
|
||||
]);
|
||||
|
||||
// Act
|
||||
$validationRules = json_decode($parameter->validation_rules, true);
|
||||
|
||||
// Assert
|
||||
$this->assertArrayHasKey('required', $validationRules);
|
||||
$this->assertArrayHasKey('numeric', $validationRules);
|
||||
$this->assertArrayHasKey('min', $validationRules);
|
||||
$this->assertArrayHasKey('max', $validationRules);
|
||||
$this->assertEquals(100, $validationRules['min']);
|
||||
$this->assertEquals(1000, $validationRules['max']);
|
||||
}
|
||||
}
|
||||
@@ -1,405 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\BomConditionRule;
|
||||
use App\Models\Model;
|
||||
use App\Models\ModelFormula;
|
||||
use App\Models\ModelParameter;
|
||||
use App\Services\ProductFromModelService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProductFromModelServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private ProductFromModelService $service;
|
||||
private Model $model;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->service = new ProductFromModelService();
|
||||
$this->service->setTenantId(1)->setApiUserId(1);
|
||||
|
||||
$this->model = Model::factory()->screen()->create(['code' => 'KSS01']);
|
||||
$this->setupKSS01Model();
|
||||
}
|
||||
|
||||
private function setupKSS01Model(): void
|
||||
{
|
||||
// Create parameters
|
||||
ModelParameter::factory()
|
||||
->screenParameters()
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
// Create formulas
|
||||
ModelFormula::factory()
|
||||
->screenFormulas()
|
||||
->create(['model_id' => $this->model->id]);
|
||||
|
||||
// Create condition rules
|
||||
BomConditionRule::factory()
|
||||
->screenRules()
|
||||
->create(['model_id' => $this->model->id]);
|
||||
}
|
||||
|
||||
/** @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
|
||||
$result = $this->service->resolveBom($this->model->id, $inputParams);
|
||||
|
||||
// Assert
|
||||
$this->assertArrayHasKey('parameters', $result);
|
||||
$this->assertArrayHasKey('formulas', $result);
|
||||
$this->assertArrayHasKey('bom_items', $result);
|
||||
|
||||
// Check calculated formulas
|
||||
$formulas = $result['formulas'];
|
||||
$this->assertEquals(1120, $formulas['W1']); // W0 + 120
|
||||
$this->assertEquals(900, $formulas['H1']); // H0 + 100
|
||||
$this->assertEquals(1.008, $formulas['area']); // W1 * H1 / 1000000
|
||||
|
||||
// Check BOM items
|
||||
$bomItems = $result['bom_items'];
|
||||
$this->assertGreaterThan(0, count($bomItems));
|
||||
|
||||
// Check specific components
|
||||
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
|
||||
$this->assertNotNull($caseItem);
|
||||
$this->assertEquals('CASE-SMALL', $caseItem['component_code']); // area <= 3
|
||||
}
|
||||
|
||||
/** @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
|
||||
$result = $this->service->resolveBom($this->model->id, $inputParams);
|
||||
|
||||
// Assert
|
||||
$formulas = $result['formulas'];
|
||||
$this->assertEquals(2620, $formulas['W1']); // 2500 + 120
|
||||
$this->assertEquals(1600, $formulas['H1']); // 1500 + 100
|
||||
$this->assertEquals(4.192, $formulas['area']); // 2620 * 1600 / 1000000
|
||||
|
||||
// Check that large case is selected
|
||||
$bomItems = $result['bom_items'];
|
||||
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
|
||||
$this->assertEquals('CASE-MEDIUM', $caseItem['component_code']); // 3 < area <= 6
|
||||
|
||||
// Check bracket quantity calculation
|
||||
$bracketItem = collect($bomItems)->first(fn($item) => $item['component_code'] === 'BOTTOM-001');
|
||||
$this->assertNotNull($bracketItem);
|
||||
$this->assertEquals(3, $bracketItem['quantity']); // CEIL(2620 / 1000)
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_resolve_bom_for_maximum_size()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 3000,
|
||||
'H0' => 2000,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'MIXED',
|
||||
'power_source' => 'DC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->service->resolveBom($this->model->id, $inputParams);
|
||||
|
||||
// Assert
|
||||
$formulas = $result['formulas'];
|
||||
$this->assertEquals(3120, $formulas['W1']);
|
||||
$this->assertEquals(2100, $formulas['H1']);
|
||||
$this->assertEquals(6.552, $formulas['area']); // > 6
|
||||
|
||||
// Check that large case is selected
|
||||
$bomItems = $result['bom_items'];
|
||||
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
|
||||
$this->assertEquals('CASE-LARGE', $caseItem['component_code']); // area > 6
|
||||
|
||||
// Check motor capacity
|
||||
$this->assertEquals('2HP', $formulas['motor']); // area > 6
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_slat_type_differences()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 1500,
|
||||
'H0' => 1000,
|
||||
'screen_type' => 'SLAT',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->service->resolveBom($this->model->id, $inputParams);
|
||||
|
||||
// Assert
|
||||
$bomItems = $result['bom_items'];
|
||||
|
||||
// Check that SLAT pipe is used instead of SCREEN pipe
|
||||
$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);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_input_parameters()
|
||||
{
|
||||
// Test missing required parameter
|
||||
$incompleteParams = [
|
||||
'W0' => 1000,
|
||||
// Missing H0, screen_type, etc.
|
||||
];
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Missing required parameter');
|
||||
|
||||
$this->service->resolveBom($this->model->id, $incompleteParams);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_parameter_ranges()
|
||||
{
|
||||
// Test out-of-range parameter
|
||||
$invalidParams = [
|
||||
'W0' => 100, // Below minimum (500)
|
||||
'H0' => 800,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Parameter W0 value 100 is outside valid range');
|
||||
|
||||
$this->service->resolveBom($this->model->id, $invalidParams);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_select_parameter_options()
|
||||
{
|
||||
// Test invalid select option
|
||||
$invalidParams = [
|
||||
'W0' => 1000,
|
||||
'H0' => 800,
|
||||
'screen_type' => 'INVALID_TYPE',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Invalid value for parameter screen_type');
|
||||
|
||||
$this->service->resolveBom($this->model->id, $invalidParams);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_preview_product_before_creation()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 1200,
|
||||
'H0' => 900,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act
|
||||
$preview = $this->service->previewProduct($this->model->id, $inputParams);
|
||||
|
||||
// Assert
|
||||
$this->assertArrayHasKey('product_info', $preview);
|
||||
$this->assertArrayHasKey('bom_summary', $preview);
|
||||
$this->assertArrayHasKey('estimated_cost', $preview);
|
||||
|
||||
$productInfo = $preview['product_info'];
|
||||
$this->assertStringContains('KSS01', $productInfo['suggested_code']);
|
||||
$this->assertStringContains('1200x900', $productInfo['suggested_name']);
|
||||
|
||||
$bomSummary = $preview['bom_summary'];
|
||||
$this->assertArrayHasKey('total_components', $bomSummary);
|
||||
$this->assertArrayHasKey('component_categories', $bomSummary);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_create_product_from_resolved_bom()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 1000,
|
||||
'H0' => 800,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
$productData = [
|
||||
'code' => 'KSS01-001',
|
||||
'name' => '스크린 블라인드 1000x800',
|
||||
'description' => '매개변수 기반 생성 제품',
|
||||
];
|
||||
|
||||
// Act
|
||||
$result = $this->service->createProductFromModel($this->model->id, $inputParams, $productData);
|
||||
|
||||
// Assert
|
||||
$this->assertArrayHasKey('product', $result);
|
||||
$this->assertArrayHasKey('bom_items', $result);
|
||||
|
||||
$product = $result['product'];
|
||||
$this->assertEquals('KSS01-001', $product['code']);
|
||||
$this->assertEquals('스크린 블라인드 1000x800', $product['name']);
|
||||
|
||||
$bomItems = $result['bom_items'];
|
||||
$this->assertGreaterThan(0, count($bomItems));
|
||||
|
||||
// Verify BOM items are properly linked to the product
|
||||
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_handles_formula_evaluation_errors_gracefully()
|
||||
{
|
||||
// Arrange - Create a formula with invalid expression
|
||||
ModelFormula::factory()
|
||||
->create([
|
||||
'model_id' => $this->model->id,
|
||||
'name' => 'invalid_formula',
|
||||
'expression' => 'UNKNOWN_FUNCTION(W0)',
|
||||
'sort_order' => 999,
|
||||
]);
|
||||
|
||||
$inputParams = [
|
||||
'W0' => 1000,
|
||||
'H0' => 800,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act & Assert
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Formula evaluation failed');
|
||||
|
||||
$this->service->resolveBom($this->model->id, $inputParams);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_respects_tenant_isolation()
|
||||
{
|
||||
// Arrange
|
||||
$otherTenantModel = Model::factory()->create(['tenant_id' => 2]);
|
||||
|
||||
$inputParams = [
|
||||
'W0' => 1000,
|
||||
'H0' => 800,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act & Assert
|
||||
$this->expectException(\ModelNotFoundException::class);
|
||||
|
||||
$this->service->resolveBom($otherTenantModel->id, $inputParams);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_caches_formula_results_for_performance()
|
||||
{
|
||||
// Arrange
|
||||
$inputParams = [
|
||||
'W0' => 1000,
|
||||
'H0' => 800,
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
// Act - First call
|
||||
$start1 = microtime(true);
|
||||
$result1 = $this->service->resolveBom($this->model->id, $inputParams);
|
||||
$time1 = microtime(true) - $start1;
|
||||
|
||||
// Act - Second call with same parameters
|
||||
$start2 = microtime(true);
|
||||
$result2 = $this->service->resolveBom($this->model->id, $inputParams);
|
||||
$time2 = microtime(true) - $start2;
|
||||
|
||||
// Assert
|
||||
$this->assertEquals($result1['formulas'], $result2['formulas']);
|
||||
$this->assertEquals($result1['bom_items'], $result2['bom_items']);
|
||||
|
||||
// Second call should be faster due to caching
|
||||
$this->assertLessThan($time1, $time2 * 2); // Allow some variance
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_boundary_conditions_correctly()
|
||||
{
|
||||
// Test exactly at boundary values
|
||||
$boundaryTestCases = [
|
||||
// Test area exactly at 3 (boundary between small and medium case)
|
||||
[
|
||||
'W0' => 1612, // Will result in W1=1732, need H1=1732 for area=3
|
||||
'H0' => 1632, // Will result in H1=1732, area = 1732*1732/1000000 ≈ 3
|
||||
'expected_case' => 'CASE-SMALL', // area <= 3
|
||||
],
|
||||
// Test area exactly at 6 (boundary between medium and large case)
|
||||
[
|
||||
'W0' => 2329, // Will result in area slightly above 6
|
||||
'H0' => 2349,
|
||||
'expected_case' => 'CASE-LARGE', // area > 6
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($boundaryTestCases as $testCase) {
|
||||
$inputParams = [
|
||||
'W0' => $testCase['W0'],
|
||||
'H0' => $testCase['H0'],
|
||||
'screen_type' => 'SCREEN',
|
||||
'install_type' => 'WALL',
|
||||
'power_source' => 'AC',
|
||||
];
|
||||
|
||||
$result = $this->service->resolveBom($this->model->id, $inputParams);
|
||||
$bomItems = $result['bom_items'];
|
||||
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
|
||||
|
||||
$this->assertEquals($testCase['expected_case'], $caseItem['component_code'],
|
||||
"Failed boundary test for W0={$testCase['W0']}, H0={$testCase['H0']}, area={$result['formulas']['area']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user