Revert "feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가"
This reverts commit bf8036a64b.
This commit is contained in:
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user