From e1a5ed86e77b10cb4d5ace484a2d115ecf8e3806 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 30 Oct 2025 12:03:49 +0200 Subject: [PATCH] decimal field type --- .../Espo/Classes/FieldSanitizers/Decimal.php | 55 ++++ .../Classes/FieldValidators/DecimalType.php | 103 +++++++ application/Espo/Core/ORM/Type/FieldType.php | 5 + .../Database/Orm/FieldConverters/Decimal.php | 63 ++++ .../Espo/Resources/i18n/en_US/Admin.json | 1 + .../Resources/i18n/en_US/FieldManager.json | 1 + .../metadata/clientDefs/DynamicLogic.json | 11 + .../Resources/metadata/fields/decimal.json | 60 ++++ .../Tools/DynamicLogic/ConditionChecker.php | 33 +++ client/src/views/fields/decimal.js | 271 ++++++++++++++++++ schema/metadata/entityDefs.json | 9 + .../Espo/Record/FieldValidationTest.php | 142 +++++++++ .../Espo/Tools/DynamicLogic/CheckerTest.php | 192 +++++++++++++ 13 files changed, 946 insertions(+) create mode 100644 application/Espo/Classes/FieldSanitizers/Decimal.php create mode 100644 application/Espo/Classes/FieldValidators/DecimalType.php create mode 100644 application/Espo/Core/Utils/Database/Orm/FieldConverters/Decimal.php create mode 100644 application/Espo/Resources/metadata/fields/decimal.json create mode 100644 client/src/views/fields/decimal.js diff --git a/application/Espo/Classes/FieldSanitizers/Decimal.php b/application/Espo/Classes/FieldSanitizers/Decimal.php new file mode 100644 index 0000000000..6d49e7f9e8 --- /dev/null +++ b/application/Espo/Classes/FieldSanitizers/Decimal.php @@ -0,0 +1,55 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldSanitizers; + +use Espo\Core\FieldSanitize\Sanitizer; +use Espo\Core\FieldSanitize\Sanitizer\Data; + +class Decimal implements Sanitizer +{ + public function sanitize(Data $data, string $field): void + { + if (!$data->has($field)) { + return; + } + + $value = $data->get($field); + + if (is_string($value) || $value === null) { + return; + } + + if (is_float($value) || is_int($value)) { + $value = (string) $value; + } + + $data->set($field, $value); + } +} diff --git a/application/Espo/Classes/FieldValidators/DecimalType.php b/application/Espo/Classes/FieldValidators/DecimalType.php new file mode 100644 index 0000000000..7f31b077fc --- /dev/null +++ b/application/Espo/Classes/FieldValidators/DecimalType.php @@ -0,0 +1,103 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldValidators; + +use Espo\Core\Currency\CalculatorUtil; +use Espo\ORM\Entity; +use stdClass; + +/** + * @noinspection PhpUnused + */ +class DecimalType +{ + public function __construct() + {} + + public function checkRequired(Entity $entity, string $field): bool + { + return $this->isNotEmpty($entity, $field); + } + + /** + * @param mixed $validationValue + * @noinspection PhpUnused + */ + public function checkMax(Entity $entity, string $field, $validationValue): bool + { + if (!$this->isNotEmpty($entity, $field)) { + return true; + } + + return CalculatorUtil::compare($entity->get($field), $validationValue) <= 0; + } + + /** + * @param mixed $validationValue + * @noinspection PhpUnused + */ + public function checkMin(Entity $entity, string $field, $validationValue): bool + { + if (!$this->isNotEmpty($entity, $field)) { + return true; + } + + return CalculatorUtil::compare($entity->get($field), $validationValue) >= 0; + } + + /** @noinspection PhpUnused */ + public function rawCheckValid(stdClass $data, string $field): bool + { + $value = $data->$field ?? null; + + if ($value === null) { + return true; + } + + if (!is_string($value)) { + return false; + } + + if ($value === '') { + return false; + } + + if (preg_match('/^-?[0-9]+\.?[0-9]*$/', $value)) { + return true; + } + + return false; + } + + protected function isNotEmpty(Entity $entity, string $field): bool + { + return $entity->has($field) && $entity->get($field) !== null; + } +} diff --git a/application/Espo/Core/ORM/Type/FieldType.php b/application/Espo/Core/ORM/Type/FieldType.php index 88c6958b7d..5b442f5d9f 100644 --- a/application/Espo/Core/ORM/Type/FieldType.php +++ b/application/Espo/Core/ORM/Type/FieldType.php @@ -64,4 +64,9 @@ class FieldType public const JSON_ARRAY = 'jsonArray'; public const JSON_OBJECT = 'jsonObject'; public const PASSWORD = 'password'; + + /** + * @since 9.3.0 + */ + public const DECIMAL = 'decimal'; } diff --git a/application/Espo/Core/Utils/Database/Orm/FieldConverters/Decimal.php b/application/Espo/Core/Utils/Database/Orm/FieldConverters/Decimal.php new file mode 100644 index 0000000000..b51b258a62 --- /dev/null +++ b/application/Espo/Core/Utils/Database/Orm/FieldConverters/Decimal.php @@ -0,0 +1,63 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Utils\Database\Orm\FieldConverters; + +use Doctrine\DBAL\Types\Types; +use Espo\Core\Utils\Database\Orm\Defs\AttributeDefs; +use Espo\Core\Utils\Database\Orm\Defs\EntityDefs; +use Espo\Core\Utils\Database\Orm\FieldConverter; +use Espo\ORM\Defs\FieldDefs; +use Espo\ORM\Defs\Params\AttributeParam; +use Espo\ORM\Defs\Params\FieldParam; +use Espo\ORM\Type\AttributeType; + +class Decimal implements FieldConverter +{ + private const DEFAULT_PRECISION = 13; + private const DEFAULT_SCALE = 4; + + public function convert(FieldDefs $fieldDefs, string $entityType): EntityDefs + { + $name = $fieldDefs->getName(); + + $dbType = $fieldDefs->getParam(FieldParam::DB_TYPE) ?? Types::DECIMAL; + $precision = $fieldDefs->getParam(FieldParam::PRECISION) ?? self::DEFAULT_PRECISION; + $scale = $fieldDefs->getParam(FieldParam::SCALE) ?? self::DEFAULT_SCALE; + + $defs = AttributeDefs::create($name) + ->withType(AttributeType::VARCHAR) + ->withDbType($dbType) + ->withParam(AttributeParam::PRECISION, $precision) + ->withParam(AttributeParam::SCALE, $scale); + + return EntityDefs::create() + ->withAttribute($defs); + } +} diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index d6ddfdae9b..27f84d71f5 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -124,6 +124,7 @@ "personName": "Person Name", "autoincrement": "Auto-increment", "bool": "Boolean", + "decimal": "Decimal", "currency": "Currency", "currencyConverted": "Currency (Converted)", "date": "Date", diff --git a/application/Espo/Resources/i18n/en_US/FieldManager.json b/application/Espo/Resources/i18n/en_US/FieldManager.json index c1890bd94d..9858a123a2 100644 --- a/application/Espo/Resources/i18n/en_US/FieldManager.json +++ b/application/Espo/Resources/i18n/en_US/FieldManager.json @@ -137,6 +137,7 @@ }, "fieldInfo": { "varchar": "A single-line text.", + "decimal": "A decimal number with fixed-point precision.", "enum": "Selectbox, only one value can be selected.", "text": "A multiline text with markdown support.", "date": "Date w/o time.", diff --git a/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json b/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json index da3159d7cd..403343ef0a 100644 --- a/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json +++ b/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json @@ -179,6 +179,17 @@ "lessThanOrEquals" ] }, + "decimal": { + "view": "views/admin/dynamic-logic/conditions/field-types/base", + "typeList": [ + "isEmpty", + "isNotEmpty", + "greaterThan", + "lessThan", + "greaterThanOrEquals", + "lessThanOrEquals" + ] + }, "currency": { "view": "views/admin/dynamic-logic/conditions/field-types/base", "typeList": [ diff --git a/application/Espo/Resources/metadata/fields/decimal.json b/application/Espo/Resources/metadata/fields/decimal.json new file mode 100644 index 0000000000..4d1ff5d3c7 --- /dev/null +++ b/application/Espo/Resources/metadata/fields/decimal.json @@ -0,0 +1,60 @@ +{ + "params": [ + { + "name": "required", + "type": "bool", + "default": false + }, + { + "name": "default", + "type": "decimal" + }, + { + "name": "min", + "type": "decimal" + }, + { + "name": "max", + "type": "decimal" + }, + { + "name": "decimalPlaces", + "type": "int" + }, + { + "name": "audited", + "type": "bool" + }, + { + "name": "readOnly", + "type": "bool" + }, + { + "name": "readOnlyAfterCreate", + "type": "bool" + }, + { + "name": "precision", + "type": "int", + "hidden": true + }, + { + "name": "scale", + "type": "int", + "hidden": true + } + ], + "filter": true, + "validationList": [ + "required", + "min", + "max" + ], + "mandatoryValidationList": [ + "valid" + ], + "sanitizerClassNameList": [ + "Espo\\Classes\\FieldSanitizers\\Decimal" + ], + "converterClassName": "Espo\\Core\\Utils\\Database\\Orm\\FieldConverters\\Decimal" +} diff --git a/application/Espo/Tools/DynamicLogic/ConditionChecker.php b/application/Espo/Tools/DynamicLogic/ConditionChecker.php index 749f827d92..4e7d218d0d 100644 --- a/application/Espo/Tools/DynamicLogic/ConditionChecker.php +++ b/application/Espo/Tools/DynamicLogic/ConditionChecker.php @@ -29,6 +29,7 @@ namespace Espo\Tools\DynamicLogic; +use Espo\Core\Currency\CalculatorUtil; use Espo\Core\Field\Date; use Espo\Core\Field\DateTime; use Espo\Core\Utils\DateTime\SystemClock; @@ -214,18 +215,34 @@ class ConditionChecker } if ($type === Type::GreaterThan) { + if (is_string($setValue) || is_string($value)) { + return $this->compare($setValue, $value) > 0; + } + return $setValue > $value; } if ($type === Type::LessThan) { + if (is_string($setValue) || is_string($value)) { + return $this->compare($setValue, $value) < 0; + } + return $setValue < $value; } if ($type === Type::GreaterThanOrEquals) { + if (is_string($setValue) || is_string($value)) { + return $this->compare($setValue, $value) >= 0; + } + return $setValue >= $value; } if ($type === Type::LessThanOrEquals) { + if (is_string($setValue) || is_string($value)) { + return $this->compare($setValue, $value) <= 0; + } + return $setValue <= $value; } @@ -346,4 +363,20 @@ class ConditionChecker return Date::fromDateTime($dateTime); } + + + /** + * @throws BadCondition + */ + private function compare(mixed $arg1, mixed $arg2): int + { + $arg1 = (string) $arg1; + $arg2 = (string) $arg2; + + if (!is_numeric($arg1) || !is_numeric($arg2)) { + throw new BadCondition(); + } + + return CalculatorUtil::compare($arg1, $arg2); + } } diff --git a/client/src/views/fields/decimal.js b/client/src/views/fields/decimal.js new file mode 100644 index 0000000000..0e60ff39ce --- /dev/null +++ b/client/src/views/fields/decimal.js @@ -0,0 +1,271 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2025 EspoCRM, Inc. + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +/** @module views/fields/decimal */ + +import IntFieldView from 'views/fields/int'; + +/** + * A decimal field. + * + * @extends IntFieldView + */ +class DecimalFieldView extends IntFieldView { + + /** + * @typedef {Object} module:views/fields/decimal~options + * @property { + * module:views/fields/decimal~params & + * module:views/fields/base~params & + * Record + * } [params] Parameters. + */ + + /** + * @typedef {Object} module:views/fields/decimal~params + * @property {string} [min] A max value. + * @property {string} [max] A max value. + * @property {boolean} [required] Required. + * @property {boolean} [disableFormatting] Disable formatting. + * @property {number|null} [decimalPlaces] A number of decimal places. + */ + + /** + * @param { + * module:views/fields/float~options & + * module:views/fields/base~options + * } options Options. + */ + constructor(options) { + super(options); + } + + type = 'decimal' + + editTemplate = 'fields/float/edit' + + decimalMark = '.' + decimalPlacesRawValue = 10 + + /** + * @inheritDoc + * @type {Array<(function (): boolean)|string>} + */ + validations = [ + 'required', + 'range', + ] + + /** + * @private + * @type {number|null} + */ + decimalPlaces + + setup() { + super.setup(); + + if (this.getPreferences().has('decimalMark')) { + this.decimalMark = this.getPreferences().get('decimalMark'); + } else if (this.getConfig().has('decimalMark')) { + this.decimalMark = this.getConfig().get('decimalMark'); + } + + if (!this.decimalMark) { + this.decimalMark = '.'; + } + + if (this.decimalMark === this.thousandSeparator) { + this.thousandSeparator = ''; + } + + this.decimalPlaces = this.params.decimalPlaces ?? null; + } + + /** + * @inheritDoc + */ + setupAutoNumericOptions() { + // noinspection JSValidateTypes + this.autoNumericOptions = { + digitGroupSeparator: this.thousandSeparator || '', + decimalCharacter: this.decimalMark, + modifyValueOnWheel: false, + selectOnFocus: false, + decimalPlaces: this.decimalPlaces, + decimalPlacesRawValue: this.decimalPlacesRawValue, + allowDecimalPadding: true, + showWarnings: false, + formulaMode: true, + }; + + if (this.decimalPlaces === null) { + this.autoNumericOptions.decimalPlaces = this.decimalPlacesRawValue; + this.autoNumericOptions.decimalPlacesRawValue = this.decimalPlacesRawValue; + this.autoNumericOptions.allowDecimalPadding = false; + } + } + + getValueForDisplay() { + const value = isNaN(this.model.get(this.name)) ? null : this.model.get(this.name); + + return this.formatNumber(value); + } + + formatNumber(value) { + if (this.disableFormatting) { + return value; + } + + return this.formatNumberDetail(value); + } + + /** + * + * @param {string|null} value + * @return {string} + */ + formatNumberDetail(value) { + if (value === null) { + return ''; + } + + const decimalPlaces = this.decimalPlaces; + + const parts = value.toString().split('.'); + + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandSeparator); + + if (decimalPlaces === 0) { + return parts[0]; + } + + if (decimalPlaces) { + let decimalPartLength = 0; + + if (parts.length > 1) { + parts[1] = parts[1].replace(/0+$/, ''); + + decimalPartLength = parts[1].length; + } else { + parts[1] = ''; + } + + if (decimalPlaces && decimalPartLength < decimalPlaces) { + const limit = decimalPlaces - decimalPartLength; + + for (let i = 0; i < limit; i++) { + parts[1] += '0'; + } + } + } + + return parts.join(this.decimalMark); + } + + /** + * @param {string|null} value + * @return {string|null} + */ + parse(value) { + value = (value !== '') ? value : null; + + if (value === null) { + return null; + } + + value = value + .split(this.thousandSeparator) + .join('') + .split(this.decimalMark) + .join('.'); + + return value; + } + + /** + * @return {Record} + */ + fetch() { + const rawValue = this.mainInputElement?.value ?? null; + + const value = this.parse(rawValue); + + return {[this.name]: value}; + } + + // noinspection JSUnusedGlobalSymbols + validateRange() { + const value = this.model.get(this.name); + + if (value === null) { + return false; + } + + const minValue = this.params.min; + const maxValue = this.params.max; + + if (minValue !== null && maxValue !== null) { + if (Number(value) < Number(minValue) || Number(value) > Number(maxValue)) { + const msg = this.translate('fieldShouldBeBetween', 'messages') + .replace('{field}', this.getLabelText()) + .replace('{min}', minValue) + .replace('{max}', maxValue); + + this.showValidationMessage(msg); + + return true; + } + } else { + if (minValue !== null) { + if (Number(value) < Number(minValue)) { + const msg = this.translate('fieldShouldBeGreater', 'messages') + .replace('{field}', this.getLabelText()) + .replace('{value}', minValue); + + this.showValidationMessage(msg); + + return true; + } + } else if (maxValue !== null) { + if (Number(value) > Number(maxValue)) { + const msg = this.translate('fieldShouldBeLess', 'messages') + .replace('{field}', this.getLabelText()) + .replace('{value}', maxValue); + this.showValidationMessage(msg); + + return true; + } + } + } + + return false; + } +} + +export default DecimalFieldView; diff --git a/schema/metadata/entityDefs.json b/schema/metadata/entityDefs.json index 6ad4d546e8..835b6f50a4 100644 --- a/schema/metadata/entityDefs.json +++ b/schema/metadata/entityDefs.json @@ -600,6 +600,15 @@ ] } } + }, + { + "properties": { + "type": { + "anyOf": [ + {"const": "decimal"} + ] + } + } } ] }, diff --git a/tests/integration/Espo/Record/FieldValidationTest.php b/tests/integration/Espo/Record/FieldValidationTest.php index fafaa2f5bf..075531b736 100644 --- a/tests/integration/Espo/Record/FieldValidationTest.php +++ b/tests/integration/Espo/Record/FieldValidationTest.php @@ -32,6 +32,7 @@ namespace tests\integration\Espo\Record; use Espo\Core\Application; use Espo\Core\Exceptions\BadRequest; use Espo\Core\FieldValidation\Type; +use Espo\Core\ORM\Type\FieldType; use Espo\Core\Record\CreateParams; use Espo\Core\Record\ServiceContainer; use Espo\Core\Record\UpdateParams; @@ -575,4 +576,145 @@ class FieldValidationTest extends BaseTestCase $this->assertFalse($isThrown); } + + /** + * @noinspection PhpUnhandledExceptionInspection + */ + public function testDecimal(): void + { + $metadata = $this->getMetadata(); + $metadata->set('entityDefs', 'Account', [ + 'fields' => [ + 'test' => [ + 'type' => FieldType::DECIMAL, + 'min' => '-1.0', + 'max' => '100.0', + ] + ], + ]); + $metadata->save(); + + $this->getDataManager()->rebuild(); + + $this->reCreateApplication(); + + $service = $this->getContainer() + ->getByClass(ServiceContainer::class) + ->getByClass(Account::class); + + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-0', + 'test' => null, + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertFalse($isThrown); + + /** @noinspection PhpConditionAlreadyCheckedInspection */ + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-1', + 'test' => '10.0', + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertFalse($isThrown); + + /** @noinspection PhpConditionAlreadyCheckedInspection */ + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-2', + 'test' => '-1.0', + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertFalse($isThrown); + + /** @noinspection PhpConditionAlreadyCheckedInspection */ + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-3', + 'test' => '100.0', + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertFalse($isThrown); + + /** @noinspection PhpConditionAlreadyCheckedInspection */ + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-4', + 'test' => '-10.0', + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertTrue($isThrown); + + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-5', + 'test' => '100.1', + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertTrue($isThrown); + + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-6', + 'test' => 'abc', + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertTrue($isThrown); + + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-7', + 'test' => ['test'], + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertTrue($isThrown); + + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-8', + 'test' => 10.0, + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertFalse($isThrown); + + /** @noinspection PhpConditionAlreadyCheckedInspection */ + $isThrown = false; + try { + $service->create((object) [ + 'name' => 'Test-9', + 'test' => 10, + ], CreateParams::create()); + } catch (BadRequest) { + $isThrown = true; + } + $this->assertFalse($isThrown); + } } diff --git a/tests/unit/Espo/Tools/DynamicLogic/CheckerTest.php b/tests/unit/Espo/Tools/DynamicLogic/CheckerTest.php index 380a596730..a5f96aabaa 100644 --- a/tests/unit/Espo/Tools/DynamicLogic/CheckerTest.php +++ b/tests/unit/Espo/Tools/DynamicLogic/CheckerTest.php @@ -1295,4 +1295,196 @@ class CheckerTest extends TestCase $this->assertTrue($checker->check($item)); } + + /** + * @noinspection PhpUnhandledExceptionInspection + */ + public function testDecimalGreaterThan(): void + { + $map = [ + 'value' => '1.0', + ]; + + $checker = new ConditionChecker($this->initEntity($map)); + + $this->assertTrue( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'greaterThan', + 'attribute' => 'value', + 'value' => '0.0', + ] + ]) + ) + ); + + $this->assertFalse( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'greaterThan', + 'attribute' => 'value', + 'value' => '1.0', + ] + ]) + ) + ); + + $this->assertFalse( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'greaterThan', + 'attribute' => 'value', + 'value' => '2.0', + ] + ]) + ) + ); + } + + /** + * @noinspection PhpUnhandledExceptionInspection + */ + public function testDecimalLessThan(): void + { + $map = [ + 'value' => '1.0', + ]; + + $checker = new ConditionChecker($this->initEntity($map)); + + $this->assertTrue( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'lessThan', + 'attribute' => 'value', + 'value' => '2.0', + ] + ]) + ) + ); + + $this->assertFalse( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'lessThan', + 'attribute' => 'value', + 'value' => '1.0', + ] + ]) + ) + ); + + $this->assertFalse( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'lessThan', + 'attribute' => 'value', + 'value' => '0.0', + ] + ]) + ) + ); + } + + /** + * @noinspection PhpUnhandledExceptionInspection + */ + public function testDecimalGreaterThanOrEquals(): void + { + $map = [ + 'value' => '1.0', + ]; + + $checker = new ConditionChecker($this->initEntity($map)); + + $this->assertTrue( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'greaterThanOrEquals', + 'attribute' => 'value', + 'value' => '0.0', + ] + ]) + ) + ); + + $this->assertTrue( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'greaterThanOrEquals', + 'attribute' => 'value', + 'value' => '1.0', + ] + ]) + ) + ); + + $this->assertFalse( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'greaterThanOrEquals', + 'attribute' => 'value', + 'value' => '2.0', + ] + ]) + ) + ); + } + + /** + * @noinspection PhpUnhandledExceptionInspection + */ + public function testDecimalLessThanOrEquals(): void + { + $map = [ + 'value' => '1', + ]; + + $checker = new ConditionChecker($this->initEntity($map)); + + $this->assertTrue( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'lessThanOrEquals', + 'attribute' => 'value', + 'value' => '2.0', + ] + ]) + ) + ); + + $this->assertTrue( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'lessThanOrEquals', + 'attribute' => 'value', + 'value' => '1.0', + ] + ]) + ) + ); + + $this->assertFalse( + $checker->check( + Item::fromGroupDefinition([ + (object) [ + 'type' => 'lessThanOrEquals', + 'attribute' => 'value', + 'value' => '0.0', + ] + ]) + ) + ); + } }