- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env) - 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget) - 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget) - 리소스 한국어화: Product, Material 모델 레이블 추가 - 대시보드: 위젯 등록 및 캐시 최적화 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
405 lines
13 KiB
PHP
405 lines
13 KiB
PHP
<?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']}");
|
|
}
|
|
}
|
|
} |