383 lines
12 KiB
PHP
383 lines
12 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Tests\Unit;
|
||
|
|
|
||
|
|
use App\Models\Model;
|
||
|
|
use App\Models\ModelFormula;
|
||
|
|
use App\Models\ModelParameter;
|
||
|
|
use App\Services\ModelFormulaService;
|
||
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||
|
|
use Tests\TestCase;
|
||
|
|
|
||
|
|
class ModelFormulaServiceTest extends TestCase
|
||
|
|
{
|
||
|
|
use RefreshDatabase;
|
||
|
|
|
||
|
|
private ModelFormulaService $service;
|
||
|
|
private Model $model;
|
||
|
|
|
||
|
|
protected function setUp(): void
|
||
|
|
{
|
||
|
|
parent::setUp();
|
||
|
|
$this->service = new ModelFormulaService();
|
||
|
|
$this->service->setTenantId(1)->setApiUserId(1);
|
||
|
|
|
||
|
|
$this->model = Model::factory()->screen()->create();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_can_get_all_formulas_for_model()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
ModelFormula::factory()
|
||
|
|
->count(3)
|
||
|
|
->create(['model_id' => $this->model->id]);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->getFormulasByModel($this->model->id);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertCount(3, $result);
|
||
|
|
$this->assertEquals($this->model->id, $result->first()->model_id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_filters_inactive_formulas()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
ModelFormula::factory()
|
||
|
|
->create(['model_id' => $this->model->id, 'is_active' => true]);
|
||
|
|
ModelFormula::factory()
|
||
|
|
->create(['model_id' => $this->model->id, 'is_active' => false]);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->getFormulasByModel($this->model->id);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertCount(1, $result);
|
||
|
|
$this->assertTrue($result->first()->is_active);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_orders_formulas_by_sort_order()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
ModelFormula::factory()
|
||
|
|
->create(['model_id' => $this->model->id, 'sort_order' => 3, 'name' => 'third']);
|
||
|
|
ModelFormula::factory()
|
||
|
|
->create(['model_id' => $this->model->id, 'sort_order' => 1, 'name' => 'first']);
|
||
|
|
ModelFormula::factory()
|
||
|
|
->create(['model_id' => $this->model->id, 'sort_order' => 2, 'name' => 'second']);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->getFormulasByModel($this->model->id);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertEquals('first', $result->get(0)->name);
|
||
|
|
$this->assertEquals('second', $result->get(1)->name);
|
||
|
|
$this->assertEquals('third', $result->get(2)->name);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_can_create_formula()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
$data = [
|
||
|
|
'name' => 'test_formula',
|
||
|
|
'expression' => 'W0 * H0',
|
||
|
|
'description' => 'Test calculation',
|
||
|
|
'return_type' => 'NUMBER',
|
||
|
|
'sort_order' => 1,
|
||
|
|
];
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->createFormula($this->model->id, $data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertInstanceOf(ModelFormula::class, $result);
|
||
|
|
$this->assertEquals('test_formula', $result->name);
|
||
|
|
$this->assertEquals('W0 * H0', $result->expression);
|
||
|
|
$this->assertEquals($this->model->id, $result->model_id);
|
||
|
|
$this->assertEquals(1, $result->tenant_id);
|
||
|
|
$this->assertEquals(1, $result->created_by);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_can_update_formula()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
$formula = ModelFormula::factory()
|
||
|
|
->create(['model_id' => $this->model->id, 'name' => 'old_name']);
|
||
|
|
|
||
|
|
$updateData = [
|
||
|
|
'name' => 'new_name',
|
||
|
|
'expression' => 'W0 + H0',
|
||
|
|
'description' => 'Updated description',
|
||
|
|
];
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->updateFormula($formula->id, $updateData);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertEquals('new_name', $result->name);
|
||
|
|
$this->assertEquals('W0 + H0', $result->expression);
|
||
|
|
$this->assertEquals('Updated description', $result->description);
|
||
|
|
$this->assertEquals(1, $result->updated_by);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_can_delete_formula()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
$formula = ModelFormula::factory()
|
||
|
|
->create(['model_id' => $this->model->id]);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->deleteFormula($formula->id);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertTrue($result);
|
||
|
|
$this->assertSoftDeleted('model_formulas', ['id' => $formula->id]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_evaluates_simple_arithmetic_expressions()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
$formula = ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'expression' => 'W0 * H0',
|
||
|
|
'return_type' => 'NUMBER',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$parameters = ['W0' => 1000, 'H0' => 800];
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->evaluateFormula($formula, $parameters);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertEquals(800000, $result);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_evaluates_complex_expressions_with_functions()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
$formula = ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'expression' => 'CEIL(W0 / 600)',
|
||
|
|
'return_type' => 'NUMBER',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$parameters = ['W0' => 1400];
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->evaluateFormula($formula, $parameters);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertEquals(3, $result); // ceil(1400/600) = ceil(2.33) = 3
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_evaluates_conditional_expressions()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
$formula = ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'expression' => 'IF(area <= 3, "SMALL", IF(area <= 6, "MEDIUM", "LARGE"))',
|
||
|
|
'return_type' => 'STRING',
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Test cases
|
||
|
|
$testCases = [
|
||
|
|
['area' => 2, 'expected' => 'SMALL'],
|
||
|
|
['area' => 5, 'expected' => 'MEDIUM'],
|
||
|
|
['area' => 8, 'expected' => 'LARGE'],
|
||
|
|
];
|
||
|
|
|
||
|
|
foreach ($testCases as $testCase) {
|
||
|
|
// Act
|
||
|
|
$result = $this->service->evaluateFormula($formula, $testCase);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertEquals($testCase['expected'], $result);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_handles_formula_dependencies()
|
||
|
|
{
|
||
|
|
// Arrange - Create formulas with dependencies
|
||
|
|
$w1Formula = ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'name' => 'W1',
|
||
|
|
'expression' => 'W0 + 120',
|
||
|
|
'sort_order' => 1,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$h1Formula = ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'name' => 'H1',
|
||
|
|
'expression' => 'H0 + 100',
|
||
|
|
'sort_order' => 2,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$areaFormula = ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'name' => 'area',
|
||
|
|
'expression' => 'W1 * H1 / 1000000',
|
||
|
|
'sort_order' => 3,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$parameters = ['W0' => 1000, 'H0' => 800];
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$results = $this->service->evaluateAllFormulas($this->model->id, $parameters);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertEquals(1120, $results['W1']); // 1000 + 120
|
||
|
|
$this->assertEquals(900, $results['H1']); // 800 + 100
|
||
|
|
$this->assertEquals(1.008, $results['area']); // 1120 * 900 / 1000000
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_validates_formula_syntax()
|
||
|
|
{
|
||
|
|
// Test valid expressions
|
||
|
|
$validExpressions = [
|
||
|
|
'W0 + H0',
|
||
|
|
'W0 * H0 / 1000',
|
||
|
|
'CEIL(W0 / 600)',
|
||
|
|
'IF(area > 5, "LARGE", "SMALL")',
|
||
|
|
'SIN(angle * PI / 180)',
|
||
|
|
];
|
||
|
|
|
||
|
|
foreach ($validExpressions as $expression) {
|
||
|
|
$isValid = $this->service->validateExpressionSyntax($expression);
|
||
|
|
$this->assertTrue($isValid, "Expression should be valid: {$expression}");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test invalid expressions
|
||
|
|
$invalidExpressions = [
|
||
|
|
'W0 + + H0', // Double operator
|
||
|
|
'W0 * )', // Unmatched parenthesis
|
||
|
|
'UNKNOWN_FUNC(W0)', // Unknown function
|
||
|
|
'', // Empty expression
|
||
|
|
'W0 AND', // Incomplete expression
|
||
|
|
];
|
||
|
|
|
||
|
|
foreach ($invalidExpressions as $expression) {
|
||
|
|
$isValid = $this->service->validateExpressionSyntax($expression);
|
||
|
|
$this->assertFalse($isValid, "Expression should be invalid: {$expression}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_detects_circular_dependencies()
|
||
|
|
{
|
||
|
|
// Arrange - Create circular dependency
|
||
|
|
ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'name' => 'A',
|
||
|
|
'expression' => 'B + 10',
|
||
|
|
]);
|
||
|
|
|
||
|
|
ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'name' => 'B',
|
||
|
|
'expression' => 'C * 2',
|
||
|
|
]);
|
||
|
|
|
||
|
|
ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'name' => 'C',
|
||
|
|
'expression' => 'A / 3', // Circular dependency
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Act & Assert
|
||
|
|
$this->expectException(\InvalidArgumentException::class);
|
||
|
|
$this->expectExceptionMessage('Circular dependency detected');
|
||
|
|
|
||
|
|
$this->service->evaluateAllFormulas($this->model->id, []);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_respects_tenant_isolation()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
$otherTenantModel = Model::factory()->create(['tenant_id' => 2]);
|
||
|
|
ModelFormula::factory()
|
||
|
|
->create(['model_id' => $otherTenantModel->id, 'tenant_id' => 2]);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->getFormulasByModel($otherTenantModel->id);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertCount(0, $result);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_handles_missing_parameters_gracefully()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
$formula = ModelFormula::factory()
|
||
|
|
->create([
|
||
|
|
'model_id' => $this->model->id,
|
||
|
|
'expression' => 'W0 * H0',
|
||
|
|
]);
|
||
|
|
|
||
|
|
$incompleteParameters = ['W0' => 1000]; // Missing H0
|
||
|
|
|
||
|
|
// Act & Assert
|
||
|
|
$this->expectException(\InvalidArgumentException::class);
|
||
|
|
$this->expectExceptionMessage('Missing parameter: H0');
|
||
|
|
|
||
|
|
$this->service->evaluateFormula($formula, $incompleteParameters);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @test */
|
||
|
|
public function it_can_bulk_update_formulas()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
$formulas = ModelFormula::factory()
|
||
|
|
->count(3)
|
||
|
|
->create(['model_id' => $this->model->id]);
|
||
|
|
|
||
|
|
$updateData = [
|
||
|
|
[
|
||
|
|
'id' => $formulas[0]->id,
|
||
|
|
'name' => 'updated_formula_1',
|
||
|
|
'expression' => 'W0 + 100',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'id' => $formulas[1]->id,
|
||
|
|
'name' => 'updated_formula_2',
|
||
|
|
'expression' => 'H0 + 50',
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'id' => $formulas[2]->id,
|
||
|
|
'is_active' => false,
|
||
|
|
],
|
||
|
|
];
|
||
|
|
|
||
|
|
// Act
|
||
|
|
$result = $this->service->bulkUpdateFormulas($this->model->id, $updateData);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
$this->assertTrue($result);
|
||
|
|
|
||
|
|
$updated = ModelFormula::whereIn('id', $formulas->pluck('id'))->get();
|
||
|
|
$this->assertEquals('updated_formula_1', $updated->where('id', $formulas[0]->id)->first()->name);
|
||
|
|
$this->assertEquals('W0 + 100', $updated->where('id', $formulas[0]->id)->first()->expression);
|
||
|
|
$this->assertFalse($updated->where('id', $formulas[2]->id)->first()->is_active);
|
||
|
|
}
|
||
|
|
}
|