709 lines
23 KiB
PHP
709 lines
23 KiB
PHP
|
|
<?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']);
|
||
|
|
}
|
||
|
|
}
|