diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3207304..812aa85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,11 @@ on: jobs: testsuite: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: - php-version: ['7.2', '7.3', '7.4', '8.0'] + php-version: ['7.4', '8.0', '8.1', '8.2', '8.3'] db-type: [sqlite, mysql, pgsql] prefer-lowest: [''] @@ -66,7 +66,7 @@ jobs: if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp'; fi if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi if [[ ${{ matrix.php-version }} == '7.4' ]]; then - export CODECOVERAGE=1 && vendor/bin/phpunit --verbose --coverage-clover=coverage.xml + export CODECOVERAGE=1 && vendor/bin/phpunit --coverage-clover=coverage.xml else vendor/bin/phpunit fi @@ -77,7 +77,7 @@ jobs: cs-stan: name: Coding Standard & Static Analysis - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 244d127..2a55d71 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /config/Migrations/schema-dump-default.lock /vendor/ /.idea/ +/.phpunit.cache \ No newline at end of file diff --git a/README.md b/README.md index a08a980..5975d37 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -CakePHP Money Plugin -=================== +# CakePHP Money Plugin +

Software License @@ -22,45 +22,45 @@ It covers the following features: * Store Money objects onto database (as strings) * Marshal money objects into CakePHP entities. -Requirements ------------- +## Requirements * CakePHP 4.0.0+ * PHP 7.2+ -Versions and branches ---------------------- +## Versions and branches + +| CakePHP | CakeDC Money Plugin | Notes | +| :-------------: | :------------------------: | :---- | +| ^4.5 | [1.x](https://github.com/cakedc/money/tree/1.next-cake4) | stable | +| ^4.0 | [0.0.1](https://github.com/cakedc/money/tree/0.0.1) | deprecated | + +## Installation -| CakePHP | CakeDC Money Plugin | Tag | Notes | -| :-------------: | :------------------------: | :--: | :---- | -| ^4.0 | [0.1](https://github.com/cakedc/money/tree/1.next-cake4) | 0.0.1 | stable | +You can install this plugin into your CakePHP application using [composer](http://getcomposer.org). +The recommended way to install composer packages is: -Documentation -------------- +``` +composer require cakedc/cakephp-money +``` + +## Documentation For documentation, as well as tutorials, see the [Docs](Docs/Home.md) directory of this repository. -Support -------- +## Support For bugs and feature requests, please use the [issues](https://github.com/CakeDC/money/issues) section of this repository. Commercial support is also available, [contact us](https://www.cakedc.com/contact) for more information. -Contributing ------------- +## Contributing This repository follows the [CakeDC Plugin Standard](https://www.cakedc.com/plugin-standard). If you'd like to contribute new features, enhancements or bug fixes to the plugin, please read our [Contribution Guidelines](https://www.cakedc.com/contribution-guidelines) for detailed instructions. -License -------- +## License Copyright 2021 Cake Development Corporation (CakeDC). All rights reserved. Licensed under the [MIT](http://www.opensource.org/licenses/mit-license.php) License. Redistributions of the source code included in this repository must retain the copyright notice found in each file. - -## To Do - -* Add Unit Tests diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e2b5c3a..5ccb5d2 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,35 +1,22 @@ - - - - - - - - - - tests/TestCase/ - - - - - - - - - - - - - - - src/ - - - + + + + src/ + + + + + + + + + + tests/TestCase/ + + + + + + diff --git a/src/Money.php b/src/Money.php index b2b8c50..2ebc7cf 100644 --- a/src/Money.php +++ b/src/Money.php @@ -49,6 +49,8 @@ public function __construct(MoneyPHP $money) */ public function __call($name, $arguments) { + $arguments = self::processArguments($arguments); + return call_user_func_array([$this->_money, $name], $arguments); } @@ -59,6 +61,8 @@ public function __call($name, $arguments) */ public static function __callStatic($name, $arguments) { + $arguments = self::processArguments($arguments); + return new self(forward_static_call_array([MoneyPHP::class, $name], $arguments)); } @@ -69,4 +73,19 @@ public function __toString(): string { return MoneyUtil::format($this); } + + /** + * @param array $arguments + * @return array + */ + protected static function processArguments($arguments = []) + { + for ($i=0; $i < count($arguments); $i++) { + if ($arguments[$i] instanceof Money) { + $arguments[$i] = $arguments[$i]->getMoney(); + } + } + + return $arguments; + } } diff --git a/src/Utility/MoneyUtil.php b/src/Utility/MoneyUtil.php index 7f88e63..3813bdd 100644 --- a/src/Utility/MoneyUtil.php +++ b/src/Utility/MoneyUtil.php @@ -11,13 +11,10 @@ namespace CakeDC\Money\Utility; use Cake\Core\Configure; -use Cake\Error\Debugger; -use CakeDC\Accounting\Database\Type\MoneyType; use Money\Currencies\BitcoinCurrencies; use Money\Currencies\ISOCurrencies; use Money\Currency; use Money\Formatter\BitcoinMoneyFormatter; -use Money\Formatter\DecimalMoneyFormatter; use Money\Formatter\IntlMoneyFormatter; use CakeDC\Money\Money; use Money\MoneyFormatter; @@ -31,7 +28,9 @@ */ class MoneyUtil { - /** @var MoneyFormatter */ + /** + * @var MoneyFormatter + */ protected static $_moneyFormatters = []; /** @@ -56,7 +55,7 @@ public static function money($value, $fromDb = false) : ?Money if (!isset($parts[1])) { $parts[1] = '00'; } - $decimalLength = strlen($parts[1] ?? '') ; + $decimalLength = strlen($parts[1] ?? ''); if ($decimalLength == 1) { $parts[1] = $parts[1] . '0'; @@ -64,8 +63,9 @@ public static function money($value, $fromDb = false) : ?Money $value = ltrim($parts[0] . $parts[1], '0'); } - - return Money::USD(!empty($value) ? str_replace(',', '', $value) : 0); + + $currency = Configure::read('Money.currency', 'USD'); + return Money::{$currency}(!empty($value) ? str_replace(',', '', $value) : 0); } /** @@ -108,7 +108,7 @@ protected static function _loadMoneyFormatter(Currency $currency) : MoneyFormatt } elseif ($currency->isAvailableWithin($bitcoin)) { $moneyFormatter = new BitcoinMoneyFormatter(7, $bitcoin); } else { - throw new \RuntimeException(sprintf('Cannot format currency \'%s\'. Only ISO currencies and Bitcoin are allowed.')); + throw new \RuntimeException(sprintf('Cannot format currency \'%s\'. Only ISO currencies and Bitcoin are allowed.', $currency)); } static::$_moneyFormatters[$currency->getCode()] = $moneyFormatter; return static::$_moneyFormatters[$currency->getCode()]; diff --git a/src/View/Helper/MoneyHelper.php b/src/View/Helper/MoneyHelper.php index 0a1f2c4..7dbd8e7 100644 --- a/src/View/Helper/MoneyHelper.php +++ b/src/View/Helper/MoneyHelper.php @@ -43,14 +43,16 @@ public function currency($value): string { $class = ''; if ($value instanceof Money) { - $value = MoneyUtil::format($value); + $output = MoneyUtil::format($value); } else { - $value = $this->Number->currency($value); + $output = $this->Number->currency($value); } - if ($value < 0) { + if ((is_numeric($value) && $value < 0) || + ($value instanceof Money && MoneyUtil::lessThanZero($value)) + ) { $class = 'negative-balance'; } - return $this->Html->tag('span', $value, ['class' => $class]); + return $this->Html->tag('span', $output, ['class' => $class]); } } diff --git a/tests/TestCase/Database/Type/MoneyTypeTest.php b/tests/TestCase/Database/Type/MoneyTypeTest.php new file mode 100644 index 0000000..43b2053 --- /dev/null +++ b/tests/TestCase/Database/Type/MoneyTypeTest.php @@ -0,0 +1,72 @@ +moneyType = new MoneyType(); + $this->driver = $this->getMockBuilder('Cake\Database\Driver')->getMock(); + parent::setUp(); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown(): void + { + parent::tearDown(); + } + + public function testToPhp() + { + $this->assertNull($this->moneyType->toPHP(null, $this->driver)); + $this->assertInstanceOf(\CakeDC\Money\Money::class, $this->moneyType->toPHP(100, $this->driver)); + } + + public function testMarshal() + { + $this->assertNull($this->moneyType->marshal(null)); + $this->assertInstanceOf(\CakeDC\Money\Money::class, $this->moneyType->marshal(100)); + } + + public function testToDatabase() + { + $this->assertNull($this->moneyType->toDatabase(null, $this->driver)); + $this->assertEquals('10000', $this->moneyType->toDatabase(MoneyUtil::money(100), $this->driver)); + } + + public function testToDatabaseInvalidArgument() + { + $this->expectException(\InvalidArgumentException::class); + $this->moneyType->toDatabase(100, $this->driver); + } + + public function testToStatement() + { + $this->assertEquals(PDO::PARAM_NULL, $this->moneyType->toStatement(null, $this->driver)); + $this->assertEquals(PDO::PARAM_INT, $this->moneyType->toStatement(100, $this->driver)); + } + + +} \ No newline at end of file diff --git a/tests/TestCase/Utility/MoneyUtilTest.php b/tests/TestCase/Utility/MoneyUtilTest.php new file mode 100644 index 0000000..dcf7efb --- /dev/null +++ b/tests/TestCase/Utility/MoneyUtilTest.php @@ -0,0 +1,235 @@ +assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money(100) + ); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money(100, true) + ); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money(-100) + ); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money(100.15) + ); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money(100.1) + ); + } + + public function testMoneyStringValue(): void + { + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money('100') + ); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money('100', true) + ); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money('-100') + ); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money('100.15') + ); + } + + public function testMoneyNullValue(): void + { + $this->assertNull( + MoneyUtil::money('') + ); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money(0) + ); + } + + public function testMoneyClassValue(): void + { + $money = MoneyUtil::money(10); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + MoneyUtil::money($money) + ); + + $this->assertEquals( + $money, + MoneyUtil::money($money) + ); + } + + + public function testFloat(): void + { + $money = MoneyUtil::money(100.15); + $this->assertEquals(100.15, MoneyUtil::float($money)); + + $money = MoneyUtil::money(200); + $this->assertEquals(200.00, MoneyUtil::float($money)); + } + + public function testFormat() + { + $money = MoneyUtil::money(100.15); + $this->assertEquals('$100.15', MoneyUtil::format($money)); + + $money = MoneyUtil::money(200); + $this->assertEquals('$200.00', MoneyUtil::format($money)); + } + + public function testFormatCurrencies() + { + Configure::write('Money.currency', 'EUR'); + + $money = MoneyUtil::money(100.15); + $this->assertEquals('€100.15', MoneyUtil::format($money)); + + $money = MoneyUtil::money('200'); + $this->assertEquals('€200.00', MoneyUtil::format($money)); + } + + public function testFormatBitcoin() + { + Configure::write('Money.currency', 'XBT'); + $money = MoneyUtil::money(1); + + $this->assertInstanceOf(\CakeDC\Money\Money::class, $money); + $this->assertEquals('Ƀ0.0000010', MoneyUtil::format($money)); + } + + public function testFormatOther() + { + try{ + Configure::write('Money.currency', 'NotCurrency'); + $money = MoneyUtil::money(100.15); + MoneyUtil::format($money); + } catch (\Exception $e){ + $this->assertInstanceOf(RuntimeException::class, $e); + } + } + + public function testZero() + { + $money = MoneyUtil::zero(); + + $this->assertInstanceOf( + \CakeDC\Money\Money::class, + $money + ); + + $this->assertEquals('$0.00', $money->__toString()); + } + + + public function testGreaterThanZero() + { + $money = MoneyUtil::money(100); + $this->assertTrue(MoneyUtil::greaterThanZero($money)); + + $money = MoneyUtil::money(-100); + $this->assertFalse(MoneyUtil::greaterThanZero($money)); + } + + public function testGreaterThanOrEqualZero() + { + $money = MoneyUtil::money(100); + $this->assertTrue(MoneyUtil::greaterThanOrEqualZero($money)); + + $money = MoneyUtil::money(-100); + $this->assertFalse(MoneyUtil::greaterThanOrEqualZero($money)); + + $this->assertTrue(MoneyUtil::greaterThanOrEqualZero(MoneyUtil::zero())); + } + + + public function testLessThanZero() + { + $money = MoneyUtil::money(100); + $this->assertFalse(MoneyUtil::lessThanZero($money)); + + $money = MoneyUtil::money(-100); + $this->assertTrue(MoneyUtil::lessThanZero($money)); + } + + public function testLessThanOrEqualZero() + { + $money = MoneyUtil::money(100); + $this->assertFalse(MoneyUtil::lessThanOrEqualZero($money)); + + $money = MoneyUtil::money(-100); + $this->assertTrue(MoneyUtil::lessThanOrEqualZero($money)); + + $this->assertTrue(MoneyUtil::lessThanOrEqualZero(MoneyUtil::zero())); + } + + public function testEqualZero() + { + $money = MoneyUtil::money(100); + $this->assertFalse(MoneyUtil::equalZero($money)); + + $this->assertTrue(MoneyUtil::equalZero(MoneyUtil::zero())); + } + +} \ No newline at end of file diff --git a/tests/TestCase/View/Helper/MoneyHelperTest.php b/tests/TestCase/View/Helper/MoneyHelperTest.php index efd7585..fdcb0d9 100644 --- a/tests/TestCase/View/Helper/MoneyHelperTest.php +++ b/tests/TestCase/View/Helper/MoneyHelperTest.php @@ -6,6 +6,7 @@ use CakeDC\Money\View\Helper\MoneyHelper; use Cake\TestSuite\TestCase; use Cake\View\View; +use CakeDC\Money\Utility\MoneyUtil; /** * CakeDC\Money\View\Helper\MoneyHelper Test Case @@ -39,7 +40,6 @@ public function setUp(): void public function tearDown(): void { unset($this->MoneyHelper); - parent::tearDown(); } @@ -50,6 +50,34 @@ public function tearDown(): void */ public function testCurrency(): void { - $this->markTestIncomplete('Not implemented yet.'); + $this->assertEquals( + '$100.00', + $this->MoneyHelper->currency(100) + ); + + $this->assertEquals( + '$100.00', + $this->MoneyHelper->currency('100') + ); + + $this->assertEquals( + '-$100.00', + $this->MoneyHelper->currency(-100) + ); + + $this->assertEquals( + '-$100.00', + $this->MoneyHelper->currency("-100") + ); + + $this->assertEquals( + '$100.00', + $this->MoneyHelper->currency(MoneyUtil::money(100)) + ); + + $this->assertEquals( + '$100.00', + $this->MoneyHelper->currency(MoneyUtil::money('100')) + ); } } diff --git a/tests/TestCase/View/Widget/MoneyWidgetTest.php b/tests/TestCase/View/Widget/MoneyWidgetTest.php new file mode 100644 index 0000000..7df1411 --- /dev/null +++ b/tests/TestCase/View/Widget/MoneyWidgetTest.php @@ -0,0 +1,118 @@ + '', + ]; + $this->templates = new StringTemplate($templates); + $this->context = $this->getMockBuilder('Cake\View\Form\ContextInterface')->getMock(); + } + + /** + * tearDown method + * + * @return void + */ + public function tearDown(): void + { + unset($this->templates); + unset($this->context); + parent::tearDown(); + } + + public function testInputMoneyWidget(): void + { + $money = new MoneyWidget($this->templates); + $data = [ + 'name' => 'amount', + 'val' => 10, + 'templateVars' => [], + ]; + $this->assertTextContains('type="number"', $money->render($data, $this->context)); + + $data = [ + 'name' => 'amount', + 'val' => MoneyUtil::money(10), + 'templateVars' => [], + ]; + $this->assertTextContains('type="number"', $money->render($data, $this->context)); + } + + public function testInputMoneyWidgetMaxMin(): void + { + $money = new MoneyWidget($this->templates); + $data = [ + 'name' => 'amount', + 'val' => 10, + 'max' => 15, + 'min' => 5, + 'templateVars' => [], + ]; + + $this->assertTextContains('max="15"', $money->render($data, $this->context)); + $this->assertTextContains('min="5"', $money->render($data, $this->context)); + + $money = new MoneyWidget($this->templates); + $data = [ + 'name' => 'amount', + 'val' => MoneyUtil::money(10), + 'max' => MoneyUtil::money(15), + 'min' => MoneyUtil::money(5), + 'templateVars' => [], + ]; + + $this->assertTextContains('max="15"', $money->render($data, $this->context)); + $this->assertTextContains('min="5"', $money->render($data, $this->context)); + } + + public function testSecureFields(): void + { + $money = new MoneyWidget($this->templates); + $data = [ + 'name' => 'amount', + 'val' => 10, + 'max' => 15, + 'min' => 5, + 'templateVars' => [], + ]; + + $this->assertTrue(in_array('amount', $money->secureFields($data))); + } +} \ No newline at end of file