diff --git a/application/Espo/Core/Currency/CalculatorUtil.php b/application/Espo/Core/Currency/CalculatorUtil.php new file mode 100644 index 0000000000..e925c18646 --- /dev/null +++ b/application/Espo/Core/Currency/CalculatorUtil.php @@ -0,0 +1,122 @@ +getAmount(); - if (!$this->configDataProvider->hasCurrency($targetCurrencyCode)) { throw new RuntimeException("Can't convert currency to unknown currency '{$targetCurrencyCode}."); } $rate = $this->configDataProvider->getCurrencyRate($value->getCode()); - $targetRate = $this->configDataProvider->getCurrencyRate($targetCurrencyCode); - $amount *= $rate; + $convertedAmount = $this->convertAmount($value->getAmountAsString(), $rate, $targetRate); - $amount /= $targetRate; + return new Currency($convertedAmount, $targetCurrencyCode); + } - return new Currency($amount, $targetCurrencyCode); + /** + * Convert a currency value to a specific currency with specific rates. + * Base currency should has rate equal to `1.0`. + * + * @throws RuntimeException + */ + public function convertWithRates(Currency $value, string $targetCurrencyCode, Rates $rates): Currency + { + $currencyCode = $value->getCode(); + + if (!$rates->hasRate($currencyCode)) { + throw new RuntimeException("No rate for the currency '{$currencyCode}."); + } + + if (!$rates->hasRate($targetCurrencyCode)) { + throw new RuntimeException("No rate for the currency '{$targetCurrencyCode}."); + } + + $rate = $rates->getRate($currencyCode); + $targetRate = $rates->getRate($targetCurrencyCode); + + $convertedAmount = $this->convertAmount($value->getAmountAsString(), $rate, $targetRate); + + return new Currency($convertedAmount, $targetCurrencyCode); } /** @@ -79,34 +100,11 @@ class Converter return $this->convert($value, $targetCurrencyCode); } - /** - * Convert a currency value to a specific currency with specific rates. - * Base currency should has rate equal to `1.0`. - * - * @throws RuntimeException - */ - public function convertWithRates(Currency $value, string $targetCurrencyCode, Rates $rates): Currency + private function convertAmount(string $amount, float $rate, float $targetRate): string { - $amount = $value->getAmount(); - - $currencyCode = $value->getCode(); - - if (!$rates->hasRate($currencyCode)) { - throw new RuntimeException("No rate for the currency '{$currencyCode}."); - } - - if (!$rates->hasRate($targetCurrencyCode)) { - throw new RuntimeException("No rate for the currency '{$targetCurrencyCode}."); - } - - $rate = $rates->getRate($currencyCode); - - $targetRate = $rates->getRate($targetCurrencyCode); - - $amount *= $rate; - - $amount /= $targetRate; - - return new Currency($amount, $targetCurrencyCode); + return CalculatorUtil::divide( + CalculatorUtil::multiply($amount, (string) $rate), + (string) $targetRate + ); } } diff --git a/application/Espo/Core/Field/Currency.php b/application/Espo/Core/Field/Currency.php index e61f106d8a..3f8412fa2e 100644 --- a/application/Espo/Core/Field/Currency.php +++ b/application/Espo/Core/Field/Currency.php @@ -29,33 +29,56 @@ namespace Espo\Core\Field; +use Espo\Core\Currency\CalculatorUtil; + use RuntimeException; +use InvalidArgumentException; /** * A currency value object. Immutable. */ class Currency { - private $amount; + private string $amount; - private $code; + private string $code; - public function __construct(float $amount, string $code) + /** + * @param string|float $amount + * @throws RuntimeException + */ + public function __construct($amount, string $code) { + if (!is_string($amount) && !is_float($amount)) { + throw new InvalidArgumentException(); + } + if (strlen($code) !== 3) { throw new RuntimeException("Bad currency code."); } + if (is_float($amount)) { + $amount = (string) $amount; + } + $this->amount = $amount; $this->code = $code; } + /** + * Get an amount as string. + */ + public function getAmountAsString(): string + { + return $this->amount; + } + /** * Get an amount. */ public function getAmount(): float { - return $this->amount; + return (float) $this->amount; } /** @@ -71,13 +94,14 @@ class Currency */ public function add(self $value): self { - $amount = $this->getAmount(); - if ($this->getCode() !== $value->getCode()) { throw new RuntimeException("Can't add a currency value with a different code."); } - $amount += $value->getAmount(); + $amount = CalculatorUtil::add( + $this->getAmountAsString(), + $value->getAmountAsString() + ); return new self($amount, $this->getCode()); } @@ -87,13 +111,14 @@ class Currency */ public function subtract(self $value): self { - $amount = $this->getAmount(); - if ($this->getCode() !== $value->getCode()) { throw new RuntimeException("Can't substract a currency value with a different code."); } - $amount -= $value->getAmount(); + $amount = CalculatorUtil::subtract( + $this->getAmountAsString(), + $value->getAmountAsString() + ); return new self($amount, $this->getCode()); } @@ -103,9 +128,10 @@ class Currency */ public function multiply(float $multiplier): self { - $amount = $this->getAmount(); - - $amount *= $multiplier; + $amount = CalculatorUtil::multiply( + $this->getAmountAsString(), + (string) $multiplier + ); return new self($amount, $this->getCode()); } @@ -115,9 +141,10 @@ class Currency */ public function divide(float $divider): self { - $amount = $this->getAmount(); - - $amount /= $divider; + $amount = CalculatorUtil::divide( + $this->getAmountAsString(), + (string) $divider + ); return new self($amount, $this->getCode()); } @@ -127,15 +154,18 @@ class Currency */ public function round(int $precision = 0): self { - $amount = round($this->getAmount(), $precision); + $amount = CalculatorUtil::round($this->getAmountAsString(), $precision); return new self($amount, $this->getCode()); } /** * Create from an amount and code. + * + * @param string|float $amount + * @throws RuntimeException */ - public static function create(float $amount, string $code): self + public static function create($amount, string $code): self { return new self($amount, $code); } diff --git a/tests/unit/Espo/Core/Field/Currency/CurrencyConverterTest.php b/tests/unit/Espo/Core/Field/Currency/CurrencyConverterTest.php index 71371883f4..df0af36d09 100644 --- a/tests/unit/Espo/Core/Field/Currency/CurrencyConverterTest.php +++ b/tests/unit/Espo/Core/Field/Currency/CurrencyConverterTest.php @@ -38,11 +38,6 @@ use Espo\Core\{ class CurrencyConverterTest extends \PHPUnit\Framework\TestCase { - protected function setUp() : void - { - - } - public function testConvert1() { $currencyConfigDataProvider = $this->createMock(CurrencyConfigDataProvider::class); diff --git a/tests/unit/Espo/Core/Field/Currency/CurrencyTest.php b/tests/unit/Espo/Core/Field/Currency/CurrencyTest.php index 6d3623bb0b..8963663c09 100644 --- a/tests/unit/Espo/Core/Field/Currency/CurrencyTest.php +++ b/tests/unit/Espo/Core/Field/Currency/CurrencyTest.php @@ -95,15 +95,42 @@ class CurrencyTest extends \PHPUnit\Framework\TestCase $this->assertEquals('USD', $value->getCode()); } - public function testRound() + public function testRound1() { $value = (new Currency(2.306, 'USD'))->round(2); $this->assertEquals(2.31, $value->getAmount()); - $this->assertEquals('USD', $value->getCode()); } + public function testRound2() + { + $value = (new Currency(2.306, 'USD'))->round(4); + + $this->assertEquals(2.306, $value->getAmount()); + } + + public function testRound3() + { + $value = (new Currency(2.306, 'USD'))->round(0); + + $this->assertEquals(2, $value->getAmount()); + } + + public function testRound4() + { + $value = (new Currency(-2.306, 'USD'))->round(2); + + $this->assertEquals(-2.31, $value->getAmount()); + } + + public function testRound5() + { + $value = (new Currency(-2.5, 'USD'))->round(0); + + $this->assertEquals(-3, $value->getAmount()); + } + public function testBadAdd() { $this->expectException(RuntimeException::class);