Revert "feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가"

This reverts commit bf8036a64b.
This commit is contained in:
2025-09-30 23:56:25 +09:00
parent bf8036a64b
commit 802a511aa0
81 changed files with 102 additions and 22632 deletions

View File

@@ -1,538 +0,0 @@
<?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'));
}
}

View File

@@ -1,708 +0,0 @@
<?php
namespace Tests\Feature\Design;
use Tests\TestCase;
use App\Models\Design\DesignModel;
use App\Models\Design\ModelVersion;
use App\Models\Design\ModelParameter;
use App\Models\Design\ModelFormula;
use App\Models\Design\BomConditionRule;
use App\Models\Design\BomTemplate;
use App\Models\Design\BomTemplateItem;
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 BomResolverTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Tenant $tenant;
private DesignModel $model;
private ModelVersion $modelVersion;
private BomTemplate $bomTemplate;
private Product $product1;
private Product $product2;
private Material $material1;
private Material $material2;
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' => 'KSS01',
'name' => 'Screen Door System',
'is_active' => true
]);
// Create model version
$this->modelVersion = ModelVersion::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'version_no' => '1.0',
'status' => 'RELEASED'
]);
// Create products and materials
$this->product1 = Product::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'BRACKET001',
'name' => 'Wall Bracket',
'unit' => 'EA'
]);
$this->product2 = Product::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'MOTOR001',
'name' => 'DC Motor',
'unit' => 'EA'
]);
$this->material1 = Material::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'FABRIC001',
'name' => 'Screen Fabric',
'unit' => 'M2'
]);
$this->material2 = Material::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'RAIL001',
'name' => 'Guide Rail',
'unit' => 'M'
]);
// Create BOM template
$this->bomTemplate = BomTemplate::factory()->create([
'tenant_id' => $this->tenant->id,
'model_version_id' => $this->modelVersion->id,
'name' => 'Base BOM Template'
]);
// Create BOM template items
BomTemplateItem::factory()->create([
'bom_template_id' => $this->bomTemplate->id,
'ref_type' => 'PRODUCT',
'ref_id' => $this->product1->id,
'quantity' => 2,
'waste_rate' => 5,
'order' => 1
]);
BomTemplateItem::factory()->create([
'bom_template_id' => $this->bomTemplate->id,
'ref_type' => 'MATERIAL',
'ref_id' => $this->material1->id,
'quantity' => 1,
'waste_rate' => 10,
'order' => 2
]);
// Create test parameters
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'W0',
'parameter_type' => 'NUMBER',
'default_value' => '800',
'min_value' => 500,
'max_value' => 2000,
'unit' => 'mm'
]);
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'H0',
'parameter_type' => 'NUMBER',
'default_value' => '600',
'min_value' => 400,
'max_value' => 1500,
'unit' => 'mm'
]);
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'screen_type',
'parameter_type' => 'SELECT',
'options' => ['FABRIC', 'STEEL', 'PLASTIC'],
'default_value' => 'FABRIC'
]);
// Create formulas
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'W1',
'expression' => 'W0 + 100',
'sort_order' => 1
]);
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'H1',
'expression' => 'H0 + 100',
'sort_order' => 2
]);
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'area',
'expression' => '(W1 * H1) / 1000000',
'sort_order' => 3
]);
// Authenticate user
Sanctum::actingAs($this->user, ['*']);
Auth::login($this->user);
}
/** @test */
public function can_resolve_basic_bom_without_rules()
{
$inputParameters = [
'W0' => 1000,
'H0' => 800
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'model' => ['id', 'code', 'name'],
'input_parameters',
'calculated_values',
'matched_rules',
'base_bom_template_id',
'resolved_bom',
'summary'
]
]);
$data = $response->json('data');
// Check calculated values
$this->assertEquals(1100, $data['calculated_values']['W1']); // 1000 + 100
$this->assertEquals(900, $data['calculated_values']['H1']); // 800 + 100
$this->assertEquals(0.99, $data['calculated_values']['area']); // (1100 * 900) / 1000000
// Check resolved BOM contains base template items
$this->assertCount(2, $data['resolved_bom']);
$this->assertEquals('PRODUCT', $data['resolved_bom'][0]['target_type']);
$this->assertEquals($this->product1->id, $data['resolved_bom'][0]['target_id']);
$this->assertEquals(2, $data['resolved_bom'][0]['quantity']);
}
/** @test */
public function can_resolve_bom_with_include_rules()
{
// Create rule to include motor for large widths
BomConditionRule::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'rule_name' => 'Large Width Motor',
'condition_expression' => 'W0 > 1200',
'action_type' => 'INCLUDE',
'target_type' => 'PRODUCT',
'target_id' => $this->product2->id,
'quantity_multiplier' => 1,
'sort_order' => 1
]);
$inputParameters = [
'W0' => 1500, // Triggers the rule
'H0' => 800
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200);
$data = $response->json('data');
// Should have base template items + motor
$this->assertCount(3, $data['resolved_bom']);
// Check if motor was added
$motorItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->product2->id);
$this->assertNotNull($motorItem);
$this->assertEquals('PRODUCT', $motorItem['target_type']);
$this->assertEquals(1, $motorItem['quantity']);
$this->assertEquals('Large Width Motor', $motorItem['reason']);
}
/** @test */
public function can_resolve_bom_with_exclude_rules()
{
// Create rule to exclude fabric for steel type
BomConditionRule::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'rule_name' => 'Steel Type Exclude Fabric',
'condition_expression' => 'screen_type == "STEEL"',
'action_type' => 'EXCLUDE',
'target_type' => 'MATERIAL',
'target_id' => $this->material1->id,
'sort_order' => 1
]);
$inputParameters = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'STEEL'
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200);
$data = $response->json('data');
// Should only have bracket (fabric excluded)
$this->assertCount(1, $data['resolved_bom']);
$this->assertEquals($this->product1->id, $data['resolved_bom'][0]['target_id']);
// Verify fabric is not in BOM
$fabricItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->material1->id);
$this->assertNull($fabricItem);
}
/** @test */
public function can_resolve_bom_with_modify_quantity_rules()
{
// Create rule to modify bracket quantity for large areas
BomConditionRule::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'rule_name' => 'Large Area Extra Brackets',
'condition_expression' => 'area > 1.0',
'action_type' => 'MODIFY_QUANTITY',
'target_type' => 'PRODUCT',
'target_id' => $this->product1->id,
'quantity_multiplier' => 2.0,
'sort_order' => 1
]);
$inputParameters = [
'W0' => 1200, // area = (1300 * 900) / 1000000 = 1.17
'H0' => 800
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200);
$data = $response->json('data');
// Find bracket item
$bracketItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->product1->id);
$this->assertNotNull($bracketItem);
$this->assertEquals(4, $bracketItem['quantity']); // 2 * 2.0
$this->assertEquals('Large Area Extra Brackets', $bracketItem['reason']);
}
/** @test */
public function can_resolve_bom_with_multiple_rules()
{
// Create multiple rules that should all apply
BomConditionRule::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'rule_name' => 'Add Motor',
'condition_expression' => 'W0 > 1000',
'action_type' => 'INCLUDE',
'target_type' => 'PRODUCT',
'target_id' => $this->product2->id,
'quantity_multiplier' => 1,
'sort_order' => 1
]);
BomConditionRule::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'rule_name' => 'Add Rail',
'condition_expression' => 'screen_type == "FABRIC"',
'action_type' => 'INCLUDE',
'target_type' => 'MATERIAL',
'target_id' => $this->material2->id,
'quantity_multiplier' => 2.0,
'sort_order' => 2
]);
$inputParameters = [
'W0' => 1200, // Triggers first rule
'H0' => 800,
'screen_type' => 'FABRIC' // Triggers second rule
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200);
$data = $response->json('data');
// Should have all items: bracket, fabric (base) + motor, rail (rules)
$this->assertCount(4, $data['resolved_bom']);
// Check all matched rules
$this->assertCount(2, $data['matched_rules']);
// Verify motor was added
$motorItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->product2->id);
$this->assertNotNull($motorItem);
// Verify rail was added
$railItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->material2->id);
$this->assertNotNull($railItem);
$this->assertEquals(2.0, $railItem['quantity']);
}
/** @test */
public function can_preview_bom_without_saving()
{
$inputParameters = [
'W0' => 1000,
'H0' => 800
];
$response = $this->postJson('/api/v1/design/bom-resolver/preview', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'model',
'input_parameters',
'calculated_values',
'resolved_bom',
'summary'
]
]);
// Verify it's the same as resolve but without persistence
$data = $response->json('data');
$this->assertIsArray($data['resolved_bom']);
$this->assertIsArray($data['summary']);
}
/** @test */
public function can_compare_bom_by_different_parameters()
{
$parameters1 = [
'W0' => 800,
'H0' => 600,
'screen_type' => 'FABRIC'
];
$parameters2 = [
'W0' => 1200,
'H0' => 800,
'screen_type' => 'STEEL'
];
$response = $this->postJson('/api/v1/design/bom-resolver/compare', [
'model_id' => $this->model->id,
'parameters1' => $parameters1,
'parameters2' => $parameters2
]);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'parameters_diff' => [
'set1',
'set2',
'changed'
],
'calculated_values_diff' => [
'set1',
'set2',
'changed'
],
'bom_diff' => [
'added',
'removed',
'modified',
'summary'
],
'summary_diff'
]
]);
$data = $response->json('data');
// Check parameter differences
$this->assertEquals(400, $data['parameters_diff']['changed']['W0']); // 1200 - 800
$this->assertEquals(200, $data['parameters_diff']['changed']['H0']); // 800 - 600
$this->assertEquals('STEEL', $data['parameters_diff']['changed']['screen_type']);
}
/** @test */
public function can_save_and_retrieve_bom_resolution()
{
$inputParameters = [
'W0' => 1000,
'H0' => 800
];
// First resolve and save
$resolveResponse = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters,
'save_resolution' => true,
'purpose' => 'ESTIMATION'
]);
$resolveResponse->assertStatus(200);
$resolutionId = $resolveResponse->json('data.resolution_id');
$this->assertNotNull($resolutionId);
// Then retrieve saved resolution
$retrieveResponse = $this->getJson('/api/v1/design/bom-resolver/resolution/' . $resolutionId);
$retrieveResponse->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'resolution_id',
'model_id',
'input_parameters',
'calculated_values',
'resolved_bom',
'purpose',
'saved_at'
]
]);
}
/** @test */
public function handles_missing_bom_template_gracefully()
{
// Delete the BOM template
$this->bomTemplate->delete();
$inputParameters = [
'W0' => 1000,
'H0' => 800
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200);
$data = $response->json('data');
// Should return empty BOM with calculated values
$this->assertEmpty($data['resolved_bom']);
$this->assertNull($data['base_bom_template_id']);
$this->assertNotEmpty($data['calculated_values']);
}
/** @test */
public function validates_input_parameters()
{
// Test missing required parameter
$invalidParameters = [
'H0' => 800
// Missing W0
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $invalidParameters
]);
$response->assertStatus(422);
// Test parameter out of range
$outOfRangeParameters = [
'W0' => 3000, // Max is 2000
'H0' => 800
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $outOfRangeParameters
]);
$response->assertStatus(422);
}
/** @test */
public function enforces_tenant_isolation()
{
// Create model for different tenant
$otherTenant = Tenant::factory()->create();
$otherModel = DesignModel::factory()->create([
'tenant_id' => $otherTenant->id
]);
$inputParameters = [
'W0' => 1000,
'H0' => 800
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $otherModel->id,
'parameters' => $inputParameters
]);
$response->assertStatus(404);
}
/** @test */
public function can_handle_bom_with_waste_rates()
{
$inputParameters = [
'W0' => 1000,
'H0' => 800
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200);
$data = $response->json('data');
// Check waste rate calculation
$bracketItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->product1->id);
$this->assertNotNull($bracketItem);
$this->assertEquals(2, $bracketItem['quantity']); // Base quantity
$this->assertEquals(2.1, $bracketItem['actual_quantity']); // 2 * (1 + 5/100)
$fabricItem = collect($data['resolved_bom'])->firstWhere('target_id', $this->material1->id);
$this->assertNotNull($fabricItem);
$this->assertEquals(1, $fabricItem['quantity']); // Base quantity
$this->assertEquals(1.1, $fabricItem['actual_quantity']); // 1 * (1 + 10/100)
}
/** @test */
public function can_test_kss01_specific_scenario()
{
// Test KSS01 specific logic using the built-in test method
$kssParameters = [
'W0' => 1200,
'H0' => 800,
'screen_type' => 'FABRIC',
'install_type' => 'WALL'
];
$response = $this->postJson('/api/v1/design/bom-resolver/kss01-test', [
'parameters' => $kssParameters
]);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'model' => ['code', 'name'],
'input_parameters',
'calculated_values' => ['W1', 'H1', 'area'],
'resolved_bom',
'summary'
]
]);
$data = $response->json('data');
$this->assertEquals('KSS01', $data['model']['code']);
$this->assertEquals(1300, $data['calculated_values']['W1']); // 1200 + 100
$this->assertEquals(900, $data['calculated_values']['H1']); // 800 + 100
}
/** @test */
public function can_handle_formula_calculation_errors()
{
// Create formula with potential division by zero
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'ratio',
'expression' => 'W0 / (H0 - 800)', // Division by zero when H0 = 800
'sort_order' => 4
]);
$inputParameters = [
'W0' => 1000,
'H0' => 800 // This will cause division by zero
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
// Should handle the error gracefully
$response->assertStatus(422)
->assertJsonFragment([
'error' => 'Formula calculation error'
]);
}
/** @test */
public function generates_comprehensive_summary()
{
$inputParameters = [
'W0' => 1000,
'H0' => 800
];
$response = $this->postJson('/api/v1/design/bom-resolver/resolve', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200);
$data = $response->json('data');
$summary = $data['summary'];
$this->assertArrayHasKey('total_items', $summary);
$this->assertArrayHasKey('material_count', $summary);
$this->assertArrayHasKey('product_count', $summary);
$this->assertArrayHasKey('total_estimated_value', $summary);
$this->assertArrayHasKey('generated_at', $summary);
$this->assertEquals(2, $summary['total_items']);
$this->assertEquals(1, $summary['material_count']);
$this->assertEquals(1, $summary['product_count']);
}
}

View File

@@ -1,436 +0,0 @@
<?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\User;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Laravel\Sanctum\Sanctum;
class ModelFormulaTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Tenant $tenant;
private DesignModel $model;
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 parameters
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'W0',
'parameter_type' => 'NUMBER'
]);
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'H0',
'parameter_type' => 'NUMBER'
]);
// Authenticate user
Sanctum::actingAs($this->user, ['*']);
Auth::login($this->user);
}
/** @test */
public function can_create_model_formula()
{
$formulaData = [
'model_id' => $this->model->id,
'formula_name' => 'W1',
'expression' => 'W0 + 100',
'description' => 'Outer width calculation',
'sort_order' => 1
];
$response = $this->postJson('/api/v1/design/model-formulas', $formulaData);
$response->assertStatus(201)
->assertJson([
'success' => true,
'message' => 'message.created'
]);
$this->assertDatabaseHas('model_formulas', [
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'W1',
'expression' => 'W0 + 100'
]);
}
/** @test */
public function can_list_model_formulas()
{
// Create test formulas
ModelFormula::factory()->count(3)->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id
]);
$response = $this->getJson('/api/v1/design/model-formulas?model_id=' . $this->model->id);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.fetched'
])
->assertJsonStructure([
'data' => [
'data' => [
'*' => [
'id',
'formula_name',
'expression',
'description',
'dependencies',
'sort_order'
]
]
]
]);
}
/** @test */
public function can_update_model_formula()
{
$formula = ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'area',
'expression' => 'W0 * H0'
]);
$updateData = [
'formula_name' => 'area_updated',
'expression' => '(W0 * H0) / 1000000',
'description' => 'Area in square meters'
];
$response = $this->putJson('/api/v1/design/model-formulas/' . $formula->id, $updateData);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.updated'
]);
$this->assertDatabaseHas('model_formulas', [
'id' => $formula->id,
'formula_name' => 'area_updated',
'expression' => '(W0 * H0) / 1000000'
]);
}
/** @test */
public function can_delete_model_formula()
{
$formula = ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id
]);
$response = $this->deleteJson('/api/v1/design/model-formulas/' . $formula->id);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.deleted'
]);
$this->assertSoftDeleted('model_formulas', [
'id' => $formula->id
]);
}
/** @test */
public function validates_formula_expression_syntax()
{
// Test invalid expression
$invalidData = [
'model_id' => $this->model->id,
'formula_name' => 'invalid_formula',
'expression' => 'W0 +++ H0' // Invalid syntax
];
$response = $this->postJson('/api/v1/design/model-formulas', $invalidData);
$response->assertStatus(422);
// Test valid expression
$validData = [
'model_id' => $this->model->id,
'formula_name' => 'valid_formula',
'expression' => 'sqrt(W0^2 + H0^2)'
];
$response = $this->postJson('/api/v1/design/model-formulas', $validData);
$response->assertStatus(201);
}
/** @test */
public function can_calculate_formulas_with_dependencies()
{
// Create formulas with dependencies
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'W1',
'expression' => 'W0 + 100',
'sort_order' => 1
]);
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'H1',
'expression' => 'H0 + 100',
'sort_order' => 2
]);
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'area',
'expression' => 'W1 * H1',
'sort_order' => 3
]);
$inputParameters = [
'W0' => 800,
'H0' => 600
];
$response = $this->postJson('/api/v1/design/model-formulas/calculate', [
'model_id' => $this->model->id,
'parameters' => $inputParameters
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
'data' => [
'W1' => 900, // 800 + 100
'H1' => 700, // 600 + 100
'area' => 630000 // 900 * 700
]
]);
}
/** @test */
public function can_detect_circular_dependencies()
{
// Create circular dependency: A depends on B, B depends on A
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'A',
'expression' => 'B + 10'
]);
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'B',
'expression' => 'A + 20'
]);
$response = $this->postJson('/api/v1/design/model-formulas/calculate', [
'model_id' => $this->model->id,
'parameters' => ['W0' => 100]
]);
$response->assertStatus(422) // Validation error for circular dependency
->assertJsonFragment([
'error' => 'Circular dependency detected'
]);
}
/** @test */
public function can_handle_complex_mathematical_expressions()
{
// Test various mathematical functions
$complexFormulas = [
['name' => 'sqrt_test', 'expression' => 'sqrt(W0^2 + H0^2)'],
['name' => 'trig_test', 'expression' => 'sin(W0 * pi() / 180)'],
['name' => 'conditional_test', 'expression' => 'if(W0 > 1000, W0 * 1.2, W0 * 1.1)'],
['name' => 'round_test', 'expression' => 'round(W0 / 100) * 100'],
['name' => 'max_test', 'expression' => 'max(W0, H0)']
];
foreach ($complexFormulas as $formulaData) {
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => $formulaData['name'],
'expression' => $formulaData['expression']
]);
}
$response = $this->postJson('/api/v1/design/model-formulas/calculate', [
'model_id' => $this->model->id,
'parameters' => ['W0' => 1200, 'H0' => 800]
]);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'sqrt_test',
'trig_test',
'conditional_test',
'round_test',
'max_test'
]
]);
}
/** @test */
public function can_validate_formula_dependencies()
{
// Create formula that references non-existent parameter
$invalidFormulaData = [
'model_id' => $this->model->id,
'formula_name' => 'invalid_ref',
'expression' => 'NONEXISTENT_PARAM + 100'
];
$response = $this->postJson('/api/v1/design/model-formulas', $invalidFormulaData);
$response->assertStatus(422);
// Create valid formula that references existing parameter
$validFormulaData = [
'model_id' => $this->model->id,
'formula_name' => 'valid_ref',
'expression' => 'W0 + 100'
];
$response = $this->postJson('/api/v1/design/model-formulas', $validFormulaData);
$response->assertStatus(201);
}
/** @test */
public function enforces_tenant_isolation()
{
// Create formula for different tenant
$otherTenant = Tenant::factory()->create();
$otherFormula = ModelFormula::factory()->create([
'tenant_id' => $otherTenant->id
]);
// Should not be able to access other tenant's formula
$response = $this->getJson('/api/v1/design/model-formulas/' . $otherFormula->id);
$response->assertStatus(404);
}
/** @test */
public function can_export_and_import_formulas()
{
// Create test formulas
ModelFormula::factory()->count(3)->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id
]);
// Export formulas
$response = $this->getJson('/api/v1/design/model-formulas/export?model_id=' . $this->model->id);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'formulas' => [
'*' => [
'formula_name',
'expression',
'description',
'sort_order'
]
]
]
]);
$exportData = $response->json('data.formulas');
// Import formulas to new model
$newModel = DesignModel::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'TEST02'
]);
$response = $this->postJson('/api/v1/design/model-formulas/import', [
'model_id' => $newModel->id,
'formulas' => $exportData
]);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.bulk_import'
]);
$this->assertDatabaseCount('model_formulas', 6); // 3 original + 3 imported
}
/** @test */
public function can_reorder_formulas_for_calculation_sequence()
{
$formula1 = ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'area',
'expression' => 'W1 * H1',
'sort_order' => 1
]);
$formula2 = ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'W1',
'expression' => 'W0 + 100',
'sort_order' => 2
]);
// Reorder so W1 is calculated before area
$reorderData = [
['id' => $formula2->id, 'sort_order' => 1],
['id' => $formula1->id, 'sort_order' => 2]
];
$response = $this->postJson('/api/v1/design/model-formulas/reorder', [
'items' => $reorderData
]);
$response->assertStatus(200);
$this->assertDatabaseHas('model_formulas', [
'id' => $formula2->id,
'sort_order' => 1
]);
}
}

View File

@@ -1,339 +0,0 @@
<?php
namespace Tests\Feature\Design;
use Tests\TestCase;
use App\Models\Design\DesignModel;
use App\Models\Design\ModelParameter;
use App\Models\User;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Laravel\Sanctum\Sanctum;
class ModelParameterTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Tenant $tenant;
private DesignModel $model;
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
]);
// Authenticate user
Sanctum::actingAs($this->user, ['*']);
Auth::login($this->user);
}
/** @test */
public function can_create_model_parameter()
{
$parameterData = [
'model_id' => $this->model->id,
'parameter_name' => 'width',
'parameter_type' => 'NUMBER',
'is_required' => true,
'default_value' => '800',
'min_value' => 100,
'max_value' => 2000,
'unit' => 'mm',
'description' => 'Width parameter for model',
'sort_order' => 1
];
$response = $this->postJson('/api/v1/design/model-parameters', $parameterData);
$response->assertStatus(201)
->assertJson([
'success' => true,
'message' => 'message.created'
]);
$this->assertDatabaseHas('model_parameters', [
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'width',
'parameter_type' => 'NUMBER'
]);
}
/** @test */
public function can_list_model_parameters()
{
// Create test parameters
ModelParameter::factory()->count(3)->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id
]);
$response = $this->getJson('/api/v1/design/model-parameters?model_id=' . $this->model->id);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.fetched'
])
->assertJsonStructure([
'data' => [
'data' => [
'*' => [
'id',
'parameter_name',
'parameter_type',
'is_required',
'default_value',
'min_value',
'max_value',
'unit',
'description',
'sort_order'
]
]
]
]);
}
/** @test */
public function can_show_model_parameter()
{
$parameter = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'height',
'parameter_type' => 'NUMBER'
]);
$response = $this->getJson('/api/v1/design/model-parameters/' . $parameter->id);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.fetched',
'data' => [
'id' => $parameter->id,
'parameter_name' => 'height',
'parameter_type' => 'NUMBER'
]
]);
}
/** @test */
public function can_update_model_parameter()
{
$parameter = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'depth',
'min_value' => 50
]);
$updateData = [
'parameter_name' => 'depth_updated',
'min_value' => 100,
'max_value' => 500,
'description' => 'Updated depth parameter'
];
$response = $this->putJson('/api/v1/design/model-parameters/' . $parameter->id, $updateData);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.updated'
]);
$this->assertDatabaseHas('model_parameters', [
'id' => $parameter->id,
'parameter_name' => 'depth_updated',
'min_value' => 100,
'max_value' => 500
]);
}
/** @test */
public function can_delete_model_parameter()
{
$parameter = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id
]);
$response = $this->deleteJson('/api/v1/design/model-parameters/' . $parameter->id);
$response->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.deleted'
]);
$this->assertSoftDeleted('model_parameters', [
'id' => $parameter->id
]);
}
/** @test */
public function validates_parameter_type_and_constraints()
{
// Test NUMBER type with invalid range
$invalidData = [
'model_id' => $this->model->id,
'parameter_name' => 'invalid_width',
'parameter_type' => 'NUMBER',
'min_value' => 1000,
'max_value' => 500 // max < min
];
$response = $this->postJson('/api/v1/design/model-parameters', $invalidData);
$response->assertStatus(422);
// Test SELECT type with options
$validSelectData = [
'model_id' => $this->model->id,
'parameter_name' => 'screen_type',
'parameter_type' => 'SELECT',
'options' => ['FABRIC', 'STEEL', 'PLASTIC'],
'default_value' => 'FABRIC'
];
$response = $this->postJson('/api/v1/design/model-parameters', $validSelectData);
$response->assertStatus(201);
}
/** @test */
public function can_validate_parameter_values()
{
// Create NUMBER parameter
$numberParam = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'test_number',
'parameter_type' => 'NUMBER',
'min_value' => 100,
'max_value' => 1000
]);
// Test valid value
$this->assertTrue($numberParam->validateValue(500));
// Test invalid values
$this->assertFalse($numberParam->validateValue(50)); // below min
$this->assertFalse($numberParam->validateValue(1500)); // above max
$this->assertFalse($numberParam->validateValue('abc')); // non-numeric
// Create SELECT parameter
$selectParam = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'test_select',
'parameter_type' => 'SELECT',
'options' => ['OPTION1', 'OPTION2', 'OPTION3']
]);
// Test valid and invalid options
$this->assertTrue($selectParam->validateValue('OPTION1'));
$this->assertFalse($selectParam->validateValue('INVALID'));
}
/** @test */
public function can_cast_parameter_values()
{
$numberParam = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_type' => 'NUMBER'
]);
$booleanParam = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_type' => 'BOOLEAN'
]);
$textParam = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_type' => 'TEXT'
]);
// Test casting
$this->assertSame(123.5, $numberParam->castValue('123.5'));
$this->assertSame(true, $booleanParam->castValue('1'));
$this->assertSame('test', $textParam->castValue('test'));
}
/** @test */
public function enforces_tenant_isolation()
{
// Create parameter for different tenant
$otherTenant = Tenant::factory()->create();
$otherParameter = ModelParameter::factory()->create([
'tenant_id' => $otherTenant->id
]);
// Should not be able to access other tenant's parameter
$response = $this->getJson('/api/v1/design/model-parameters/' . $otherParameter->id);
$response->assertStatus(404);
// Should not be able to list other tenant's parameters
$response = $this->getJson('/api/v1/design/model-parameters');
$response->assertStatus(200);
$data = $response->json('data.data');
$this->assertEmpty(collect($data)->where('tenant_id', $otherTenant->id));
}
/** @test */
public function can_reorder_parameters()
{
$param1 = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'sort_order' => 1
]);
$param2 = ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'sort_order' => 2
]);
$reorderData = [
['id' => $param1->id, 'sort_order' => 2],
['id' => $param2->id, 'sort_order' => 1]
];
$response = $this->postJson('/api/v1/design/model-parameters/reorder', [
'items' => $reorderData
]);
$response->assertStatus(200);
$this->assertDatabaseHas('model_parameters', [
'id' => $param1->id,
'sort_order' => 2
]);
$this->assertDatabaseHas('model_parameters', [
'id' => $param2->id,
'sort_order' => 1
]);
}
}

View File

@@ -1,679 +0,0 @@
<?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\Category;
use App\Models\User;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use Laravel\Sanctum\Sanctum;
class ProductFromModelTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Tenant $tenant;
private DesignModel $model;
private Category $category;
private Product $baseMaterial;
private Product $baseProduct;
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 category
$this->category = Category::factory()->create([
'tenant_id' => $this->tenant->id,
'name' => 'Screen Systems',
'code' => 'SCREENS'
]);
// Create test design model
$this->model = DesignModel::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'KSS01',
'name' => 'Screen Door System',
'category_id' => $this->category->id,
'is_active' => true
]);
// Create base materials and products for BOM
$this->baseMaterial = Material::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'FABRIC001',
'name' => 'Screen Fabric'
]);
$this->baseProduct = Product::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'BRACKET001',
'name' => 'Wall Bracket'
]);
// Create test parameters
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'width',
'parameter_type' => 'NUMBER',
'unit' => 'mm'
]);
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'height',
'parameter_type' => 'NUMBER',
'unit' => 'mm'
]);
ModelParameter::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'parameter_name' => 'screen_type',
'parameter_type' => 'SELECT',
'options' => ['FABRIC', 'STEEL']
]);
// Create test formulas
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'outer_width',
'expression' => 'width + 100'
]);
ModelFormula::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'formula_name' => 'outer_height',
'expression' => 'height + 100'
]);
// Authenticate user
Sanctum::actingAs($this->user, ['*']);
Auth::login($this->user);
}
/** @test */
public function can_create_product_from_model_with_parameters()
{
$productData = [
'model_id' => $this->model->id,
'parameters' => [
'width' => 1200,
'height' => 800,
'screen_type' => 'FABRIC'
],
'product_data' => [
'code' => 'KSS01-1200x800-FABRIC',
'name' => 'Screen Door 1200x800 Fabric',
'category_id' => $this->category->id,
'description' => 'Custom screen door based on KSS01 model',
'unit' => 'EA'
],
'generate_bom' => true
];
$response = $this->postJson('/api/v1/design/product-from-model', $productData);
$response->assertStatus(201)
->assertJson([
'success' => true,
'message' => 'message.created'
])
->assertJsonStructure([
'data' => [
'product' => [
'id',
'code',
'name',
'category_id',
'description'
],
'model_reference' => [
'model_id',
'input_parameters',
'calculated_values'
],
'bom_created',
'bom_items_count'
]
]);
// Verify product was created
$this->assertDatabaseHas('products', [
'tenant_id' => $this->tenant->id,
'code' => 'KSS01-1200x800-FABRIC',
'name' => 'Screen Door 1200x800 Fabric'
]);
$data = $response->json('data');
$this->assertEquals($this->model->id, $data['model_reference']['model_id']);
$this->assertEquals(1200, $data['model_reference']['input_parameters']['width']);
$this->assertEquals(1300, $data['model_reference']['calculated_values']['outer_width']); // width + 100
}
/** @test */
public function can_create_product_with_auto_generated_code()
{
$productData = [
'model_id' => $this->model->id,
'parameters' => [
'width' => 1000,
'height' => 600,
'screen_type' => 'STEEL'
],
'product_data' => [
'name' => 'Custom Steel Screen',
'category_id' => $this->category->id,
'auto_generate_code' => true
]
];
$response = $this->postJson('/api/v1/design/product-from-model', $productData);
$response->assertStatus(201);
$data = $response->json('data');
// Code should be auto-generated based on model and parameters
$expectedCode = 'KSS01-1000x600-STEEL';
$this->assertEquals($expectedCode, $data['product']['code']);
$this->assertDatabaseHas('products', [
'tenant_id' => $this->tenant->id,
'code' => $expectedCode
]);
}
/** @test */
public function can_create_product_with_bom_generation()
{
// Create rule for BOM generation
BomConditionRule::factory()->create([
'tenant_id' => $this->tenant->id,
'model_id' => $this->model->id,
'rule_name' => 'Add Fabric Material',
'condition_expression' => 'screen_type == "FABRIC"',
'action_type' => 'INCLUDE',
'target_type' => 'MATERIAL',
'target_id' => $this->baseMaterial->id,
'quantity_multiplier' => 1.2
]);
$productData = [
'model_id' => $this->model->id,
'parameters' => [
'width' => 1200,
'height' => 800,
'screen_type' => 'FABRIC'
],
'product_data' => [
'code' => 'TEST-PRODUCT-001',
'name' => 'Test Product with BOM',
'category_id' => $this->category->id
],
'generate_bom' => true
];
$response = $this->postJson('/api/v1/design/product-from-model', $productData);
$response->assertStatus(201);
$data = $response->json('data');
$this->assertTrue($data['bom_created']);
$this->assertGreaterThan(0, $data['bom_items_count']);
// Verify BOM was created in product_components table
$product = Product::where('code', 'TEST-PRODUCT-001')->first();
$this->assertDatabaseHas('product_components', [
'tenant_id' => $this->tenant->id,
'product_id' => $product->id,
'ref_type' => 'MATERIAL',
'ref_id' => $this->baseMaterial->id
]);
}
/** @test */
public function can_create_product_without_bom_generation()
{
$productData = [
'model_id' => $this->model->id,
'parameters' => [
'width' => 1000,
'height' => 600,
'screen_type' => 'STEEL'
],
'product_data' => [
'code' => 'TEST-NO-BOM',
'name' => 'Test Product without BOM',
'category_id' => $this->category->id
],
'generate_bom' => false
];
$response = $this->postJson('/api/v1/design/product-from-model', $productData);
$response->assertStatus(201);
$data = $response->json('data');
$this->assertFalse($data['bom_created']);
$this->assertEquals(0, $data['bom_items_count']);
// Verify no BOM components were created
$product = Product::where('code', 'TEST-NO-BOM')->first();
$this->assertDatabaseMissing('product_components', [
'product_id' => $product->id
]);
}
/** @test */
public function can_preview_product_before_creation()
{
$previewData = [
'model_id' => $this->model->id,
'parameters' => [
'width' => 1500,
'height' => 1000,
'screen_type' => 'FABRIC'
],
'product_data' => [
'name' => 'Preview Product',
'category_id' => $this->category->id,
'auto_generate_code' => true
],
'generate_bom' => true
];
$response = $this->postJson('/api/v1/design/product-from-model/preview', $previewData);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'preview_product' => [
'code',
'name',
'category_id'
],
'model_reference' => [
'model_id',
'input_parameters',
'calculated_values'
],
'preview_bom' => [
'items',
'summary'
]
]
]);
// Verify no actual product was created
$data = $response->json('data');
$this->assertDatabaseMissing('products', [
'code' => $data['preview_product']['code']
]);
}
/** @test */
public function validates_required_parameters()
{
// Missing required width parameter
$invalidData = [
'model_id' => $this->model->id,
'parameters' => [
'height' => 800, // Missing width
'screen_type' => 'FABRIC'
],
'product_data' => [
'code' => 'INVALID-PRODUCT',
'name' => 'Invalid Product'
]
];
$response = $this->postJson('/api/v1/design/product-from-model', $invalidData);
$response->assertStatus(422);
// Parameter out of valid range
$outOfRangeData = [
'model_id' => $this->model->id,
'parameters' => [
'width' => -100, // Invalid negative value
'height' => 800,
'screen_type' => 'FABRIC'
],
'product_data' => [
'code' => 'OUT-OF-RANGE',
'name' => 'Out of Range Product'
]
];
$response = $this->postJson('/api/v1/design/product-from-model', $outOfRangeData);
$response->assertStatus(422);
}
/** @test */
public function validates_product_code_uniqueness()
{
// Create first product
$productData1 = [
'model_id' => $this->model->id,
'parameters' => [
'width' => 1000,
'height' => 600,
'screen_type' => 'FABRIC'
],
'product_data' => [
'code' => 'DUPLICATE-CODE',
'name' => 'First Product',
'category_id' => $this->category->id
]
];
$response1 = $this->postJson('/api/v1/design/product-from-model', $productData1);
$response1->assertStatus(201);
// Try to create second product with same code
$productData2 = [
'model_id' => $this->model->id,
'parameters' => [
'width' => 1200,
'height' => 800,
'screen_type' => 'STEEL'
],
'product_data' => [
'code' => 'DUPLICATE-CODE', // Same code
'name' => 'Second Product',
'category_id' => $this->category->id
]
];
$response2 = $this->postJson('/api/v1/design/product-from-model', $productData2);
$response2->assertStatus(422);
}
/** @test */
public function can_create_product_with_custom_attributes()
{
$productData = [
'model_id' => $this->model->id,
'parameters' => [
'width' => 1200,
'height' => 800,
'screen_type' => 'FABRIC'
],
'product_data' => [
'code' => 'CUSTOM-ATTRS',
'name' => 'Product with Custom Attributes',
'category_id' => $this->category->id,
'description' => 'Product with extended attributes',
'unit' => 'SET',
'weight' => 15.5,
'color' => 'WHITE',
'material_grade' => 'A-GRADE'
],
'custom_attributes' => [
'installation_difficulty' => 'MEDIUM',
'warranty_period' => '2_YEARS',
'fire_rating' => 'B1'
]
];
$response = $this->postJson('/api/v1/design/product-from-model', $productData);
$response->assertStatus(201);
$this->assertDatabaseHas('products', [
'tenant_id' => $this->tenant->id,
'code' => 'CUSTOM-ATTRS',
'weight' => 15.5,
'color' => 'WHITE'
]);
}
/** @test */
public function can_create_multiple_products_from_same_model()
{
$baseData = [
'model_id' => $this->model->id,
'generate_bom' => false
];
$products = [
[
'parameters' => ['width' => 800, 'height' => 600, 'screen_type' => 'FABRIC'],
'code' => 'KSS01-800x600-FABRIC'
],
[
'parameters' => ['width' => 1000, 'height' => 800, 'screen_type' => 'STEEL'],
'code' => 'KSS01-1000x800-STEEL'
],
[
'parameters' => ['width' => 1200, 'height' => 1000, 'screen_type' => 'FABRIC'],
'code' => 'KSS01-1200x1000-FABRIC'
]
];
foreach ($products as $index => $productSpec) {
$productData = array_merge($baseData, [
'parameters' => $productSpec['parameters'],
'product_data' => [
'code' => $productSpec['code'],
'name' => 'Product ' . ($index + 1),
'category_id' => $this->category->id
]
]);
$response = $this->postJson('/api/v1/design/product-from-model', $productData);
$response->assertStatus(201);
}
// Verify all products were created
foreach ($products as $productSpec) {
$this->assertDatabaseHas('products', [
'tenant_id' => $this->tenant->id,
'code' => $productSpec['code']
]);
}
}
/** @test */
public function can_list_products_created_from_model()
{
// Create some products from the model
$this->createTestProductFromModel('PROD-1', ['width' => 800, 'height' => 600]);
$this->createTestProductFromModel('PROD-2', ['width' => 1000, 'height' => 800]);
// Create a product not from model
Product::factory()->create([
'tenant_id' => $this->tenant->id,
'code' => 'NON-MODEL-PROD'
]);
$response = $this->getJson('/api/v1/design/product-from-model/list?model_id=' . $this->model->id);
$response->assertStatus(200)
->assertJsonStructure([
'success',
'data' => [
'data' => [
'*' => [
'id',
'code',
'name',
'model_reference' => [
'model_id',
'input_parameters',
'calculated_values'
],
'created_at'
]
]
]
]);
$data = $response->json('data.data');
$this->assertCount(2, $data); // Only products created from model
}
/** @test */
public function enforces_tenant_isolation()
{
// Create model for different tenant
$otherTenant = Tenant::factory()->create();
$otherModel = DesignModel::factory()->create([
'tenant_id' => $otherTenant->id
]);
$productData = [
'model_id' => $otherModel->id,
'parameters' => [
'width' => 1000,
'height' => 800
],
'product_data' => [
'code' => 'TENANT-TEST',
'name' => 'Tenant Test Product'
]
];
$response = $this->postJson('/api/v1/design/product-from-model', $productData);
$response->assertStatus(404);
}
/** @test */
public function can_update_existing_product_from_model()
{
// First create a product
$createData = [
'model_id' => $this->model->id,
'parameters' => [
'width' => 1000,
'height' => 600,
'screen_type' => 'FABRIC'
],
'product_data' => [
'code' => 'UPDATE-TEST',
'name' => 'Original Product',
'category_id' => $this->category->id
]
];
$createResponse = $this->postJson('/api/v1/design/product-from-model', $createData);
$createResponse->assertStatus(201);
$productId = $createResponse->json('data.product.id');
// Then update it with new parameters
$updateData = [
'parameters' => [
'width' => 1200, // Changed
'height' => 800, // Changed
'screen_type' => 'STEEL' // Changed
],
'product_data' => [
'name' => 'Updated Product', // Changed
'description' => 'Updated description'
],
'regenerate_bom' => true
];
$updateResponse = $this->putJson('/api/v1/design/product-from-model/' . $productId, $updateData);
$updateResponse->assertStatus(200)
->assertJson([
'success' => true,
'message' => 'message.updated'
]);
// Verify product was updated
$this->assertDatabaseHas('products', [
'id' => $productId,
'name' => 'Updated Product'
]);
}
/** @test */
public function can_clone_product_with_modified_parameters()
{
// Create original product
$originalProductId = $this->createTestProductFromModel('ORIGINAL', [
'width' => 1000,
'height' => 600,
'screen_type' => 'FABRIC'
]);
// Clone with modified parameters
$cloneData = [
'source_product_id' => $originalProductId,
'parameters' => [
'width' => 1200, // Modified
'height' => 600, // Same
'screen_type' => 'STEEL' // Modified
],
'product_data' => [
'code' => 'CLONED-PRODUCT',
'name' => 'Cloned Product',
'category_id' => $this->category->id
]
];
$response = $this->postJson('/api/v1/design/product-from-model/clone', $cloneData);
$response->assertStatus(201)
->assertJson([
'success' => true,
'message' => 'message.cloned'
]);
// Verify clone was created
$this->assertDatabaseHas('products', [
'tenant_id' => $this->tenant->id,
'code' => 'CLONED-PRODUCT'
]);
// Verify original product still exists
$this->assertDatabaseHas('products', [
'id' => $originalProductId
]);
}
/**
* Helper method to create a test product from model
*/
private function createTestProductFromModel(string $code, array $parameters): int
{
$productData = [
'model_id' => $this->model->id,
'parameters' => array_merge([
'width' => 1000,
'height' => 600,
'screen_type' => 'FABRIC'
], $parameters),
'product_data' => [
'code' => $code,
'name' => 'Test Product ' . $code,
'category_id' => $this->category->id
]
];
$response = $this->postJson('/api/v1/design/product-from-model', $productData);
return $response->json('data.product.id');
}
}

View File

@@ -1,606 +0,0 @@
<?php
namespace Tests\Feature;
use App\Models\BomConditionRule;
use App\Models\Model;
use App\Models\ModelFormula;
use App\Models\ModelParameter;
use App\Models\User;
use Database\Seeders\ParameterBasedBomTestSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class ParameterBasedBomApiTest extends TestCase
{
use RefreshDatabase;
private User $user;
private Model $kss01Model;
protected function setUp(): void
{
parent::setUp();
// Set up test environment
$this->artisan('migrate');
$this->seed(ParameterBasedBomTestSeeder::class);
// Create test user and authenticate
$this->user = User::factory()->create(['tenant_id' => 1]);
Sanctum::actingAs($this->user);
// Set required headers
$this->withHeaders([
'X-API-KEY' => config('app.api_key', 'test-api-key'),
'Accept' => 'application/json',
'Content-Type' => 'application/json',
]);
$this->kss01Model = Model::where('code', 'KSS01')->first();
}
/** @test */
public function it_can_get_model_parameters()
{
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters");
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'*' => [
'id',
'name',
'label',
'type',
'default_value',
'validation_rules',
'options',
'sort_order',
'is_required',
'is_active',
]
]
]);
$parameters = $response->json('data');
$this->assertCount(5, $parameters); // W0, H0, screen_type, install_type, power_source
// Check parameter order
$this->assertEquals('W0', $parameters[0]['name']);
$this->assertEquals('H0', $parameters[1]['name']);
$this->assertEquals('screen_type', $parameters[2]['name']);
}
/** @test */
public function it_can_get_model_formulas()
{
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/formulas");
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'*' => [
'id',
'name',
'expression',
'description',
'return_type',
'sort_order',
'is_active',
]
]
]);
$formulas = $response->json('data');
$this->assertGreaterThanOrEqual(7, count($formulas)); // W1, H1, area, weight, motor, bracket, guide
// Check formula order
$this->assertEquals('W1', $formulas[0]['name']);
$this->assertEquals('H1', $formulas[1]['name']);
$this->assertEquals('area', $formulas[2]['name']);
}
/** @test */
public function it_can_get_model_condition_rules()
{
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/rules");
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'*' => [
'id',
'name',
'description',
'condition_expression',
'component_code',
'quantity_expression',
'priority',
'is_active',
]
]
]);
$rules = $response->json('data');
$this->assertGreaterThanOrEqual(7, count($rules)); // Various case, bottom, shaft, pipe rules
// Check rules are ordered by priority
$priorities = collect($rules)->pluck('priority');
$this->assertEquals($priorities->sort()->values(), $priorities->values());
}
/** @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
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $inputParams
]);
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'model_id',
'input_parameters',
'calculated_formulas' => [
'W1',
'H1',
'area',
'weight',
'motor',
'bracket',
'guide',
],
'bom_items' => [
'*' => [
'component_code',
'quantity',
'rule_name',
'condition_expression',
]
],
'summary' => [
'total_components',
'total_weight',
'component_categories',
]
]
]);
$data = $response->json('data');
// Check calculated formulas
$formulas = $data['calculated_formulas'];
$this->assertEquals(1120, $formulas['W1']); // 1000 + 120
$this->assertEquals(900, $formulas['H1']); // 800 + 100
$this->assertEquals(1.008, $formulas['area']); // 1120 * 900 / 1000000
$this->assertEquals('0.5HP', $formulas['motor']); // area <= 3
// Check BOM items
$bomItems = $data['bom_items'];
$this->assertGreaterThan(0, count($bomItems));
// Check specific components
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
$this->assertEquals('CASE-SMALL', $caseItem['component_code']);
$screenPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SCREEN');
$this->assertNotNull($screenPipe);
$slatPipe = collect($bomItems)->first(fn($item) => $item['component_code'] === 'PIPE-SLAT');
$this->assertNull($slatPipe);
}
/** @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
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $inputParams
]);
// Assert
$response->assertOk();
$data = $response->json('data');
$formulas = $data['calculated_formulas'];
// Check large screen calculations
$this->assertEquals(2620, $formulas['W1']); // 2500 + 120
$this->assertEquals(1600, $formulas['H1']); // 1500 + 100
$this->assertEquals(4.192, $formulas['area']); // 2620 * 1600 / 1000000
$this->assertEquals('1HP', $formulas['motor']); // 3 < area <= 6
// Check medium case is selected
$bomItems = $data['bom_items'];
$caseItem = collect($bomItems)->first(fn($item) => str_contains($item['component_code'], 'CASE'));
$this->assertEquals('CASE-MEDIUM', $caseItem['component_code']);
// Check bracket quantity
$bottomItem = collect($bomItems)->first(fn($item) => $item['component_code'] === 'BOTTOM-001');
$this->assertEquals(3, $bottomItem['quantity']); // CEIL(2620 / 1000)
}
/** @test */
public function it_can_resolve_bom_for_slat_type()
{
// Arrange
$inputParams = [
'W0' => 1500,
'H0' => 1000,
'screen_type' => 'SLAT',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $inputParams
]);
// Assert
$response->assertOk();
$bomItems = $response->json('data.bom_items');
// Check that SLAT pipe is used
$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);
$this->assertEquals('PIPE-SLAT', $slatPipe['component_code']);
}
/** @test */
public function it_validates_missing_required_parameters()
{
// Arrange - Missing H0 parameter
$incompleteParams = [
'W0' => 1000,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $incompleteParams
]);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['parameters.H0']);
}
/** @test */
public function it_validates_parameter_ranges()
{
// Arrange - W0 below minimum
$invalidParams = [
'W0' => 100, // Below minimum (500)
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $invalidParams
]);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['parameters.W0']);
}
/** @test */
public function it_validates_select_parameter_options()
{
// Arrange - Invalid screen_type
$invalidParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'INVALID_TYPE',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $invalidParams
]);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['parameters.screen_type']);
}
/** @test */
public function it_can_create_product_from_model()
{
// Arrange
$inputParams = [
'W0' => 1200,
'H0' => 900,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
$productData = [
'code' => 'KSS01-TEST-001',
'name' => '테스트 스크린 블라인드 1200x900',
'description' => 'API 테스트로 생성된 제품',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/products", [
'parameters' => $inputParams,
'product' => $productData,
]);
// Assert
$response->assertCreated()
->assertJsonStructure([
'success',
'message',
'data' => [
'product' => [
'id',
'code',
'name',
'description',
'product_type',
'model_id',
'parameter_values',
],
'bom_items' => [
'*' => [
'id',
'product_id',
'component_code',
'component_type',
'quantity',
'unit',
]
],
'summary' => [
'total_components',
'calculated_formulas',
]
]
]);
$data = $response->json('data');
$product = $data['product'];
// Check product data
$this->assertEquals('KSS01-TEST-001', $product['code']);
$this->assertEquals('테스트 스크린 블라인드 1200x900', $product['name']);
$this->assertEquals($this->kss01Model->id, $product['model_id']);
// Check parameter values are stored
$parameterValues = json_decode($product['parameter_values'], true);
$this->assertEquals(1200, $parameterValues['W0']);
$this->assertEquals(900, $parameterValues['H0']);
// Check BOM items
$bomItems = $data['bom_items'];
$this->assertGreaterThan(0, count($bomItems));
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_can_preview_product_without_creating()
{
// Arrange
$inputParams = [
'W0' => 1800,
'H0' => 1200,
'screen_type' => 'SCREEN',
'install_type' => 'SIDE',
'power_source' => 'DC',
];
// Act
$response = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/preview", [
'parameters' => $inputParams
]);
// Assert
$response->assertOk()
->assertJsonStructure([
'success',
'message',
'data' => [
'suggested_product' => [
'code',
'name',
'description',
],
'calculated_values' => [
'W1',
'H1',
'area',
'weight',
'motor',
],
'bom_preview' => [
'total_components',
'components' => [
'*' => [
'component_code',
'quantity',
'description',
]
]
],
'cost_estimate' => [
'total_material_cost',
'estimated_labor_cost',
'total_estimated_cost',
]
]
]);
$data = $response->json('data');
// Check suggested product info
$suggestedProduct = $data['suggested_product'];
$this->assertStringContains('KSS01', $suggestedProduct['code']);
$this->assertStringContains('1800x1200', $suggestedProduct['name']);
// Check calculated values
$calculatedValues = $data['calculated_values'];
$this->assertEquals(1920, $calculatedValues['W1']); // 1800 + 120
$this->assertEquals(1300, $calculatedValues['H1']); // 1200 + 100
// Check BOM preview
$bomPreview = $data['bom_preview'];
$this->assertGreaterThan(0, $bomPreview['total_components']);
$this->assertNotEmpty($bomPreview['components']);
}
/** @test */
public function it_handles_authentication_properly()
{
// Arrange - Remove authentication
Sanctum::actingAs(null);
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters");
// Assert
$response->assertUnauthorized();
}
/** @test */
public function it_handles_tenant_isolation()
{
// Arrange - Create user with different tenant
$otherTenantUser = User::factory()->create(['tenant_id' => 2]);
Sanctum::actingAs($otherTenantUser);
// Act
$response = $this->getJson("/api/v1/design/models/{$this->kss01Model->id}/parameters");
// Assert
$response->assertNotFound();
}
/** @test */
public function it_handles_performance_for_large_datasets()
{
// Arrange - Use performance test model with many parameters/formulas/rules
$performanceModel = Model::where('code', 'PERF-TEST')->first();
if (!$performanceModel) {
$this->markTestSkipped('Performance test model not found. Run in testing environment.');
}
$inputParams = array_fill_keys(
ModelParameter::where('model_id', $performanceModel->id)->pluck('name')->toArray(),
100
);
// Act
$startTime = microtime(true);
$response = $this->postJson("/api/v1/design/models/{$performanceModel->id}/bom/resolve", [
'parameters' => $inputParams
]);
$executionTime = microtime(true) - $startTime;
// Assert
$response->assertOk();
$this->assertLessThan(2.0, $executionTime, 'BOM resolution should complete within 2 seconds');
$data = $response->json('data');
$this->assertArrayHasKey('calculated_formulas', $data);
$this->assertArrayHasKey('bom_items', $data);
}
/** @test */
public function it_returns_appropriate_error_for_nonexistent_model()
{
// Act
$response = $this->getJson('/api/v1/design/models/999999/parameters');
// Assert
$response->assertNotFound()
->assertJson([
'success' => false,
'message' => 'error.model_not_found',
]);
}
/** @test */
public function it_handles_concurrent_requests_safely()
{
// Arrange
$inputParams = [
'W0' => 1000,
'H0' => 800,
'screen_type' => 'SCREEN',
'install_type' => 'WALL',
'power_source' => 'AC',
];
// Act - Simulate concurrent requests
$promises = [];
for ($i = 0; $i < 5; $i++) {
$promises[] = $this->postJson("/api/v1/design/models/{$this->kss01Model->id}/bom/resolve", [
'parameters' => $inputParams
]);
}
// Assert - All should succeed with same results
foreach ($promises as $response) {
$response->assertOk();
$formulas = $response->json('data.calculated_formulas');
$this->assertEquals(1120, $formulas['W1']);
$this->assertEquals(900, $formulas['H1']);
}
}
}