Commit 595e2648 authored by Thorsten Buss's avatar Thorsten Buss

* add TaxedMoney for use net/gross/tax in one object

* use also internaly always the amount() instead of amount, so the TaxedMoney are worked
* use static instead of self in MoneyClass
* change MoneyTest to use money() method, for testing TaxedMoney with the same methods
parent 621d7892
......@@ -78,7 +78,7 @@ or execute
```
### Money
### Use the Money Object
```php
// assign own currency
......@@ -108,6 +108,20 @@ or execute
// 1234,56€ with html spans arround the parts
$m->html(/* use the same params as format() above */);
// return [
// 'amount' => $this->amount(),
// 'number' => $this->normalize(),
// 'format' => $this->format(),
// 'currency' => $this->currency->code,
// ];
$m->toArray();
// toArray() as JsonString
$m->toJson();
// alias for format()
$m->render();
// parse Moneystrings -> 123456 cents
$m = Money::parse('1.234,56');
......@@ -116,10 +130,91 @@ or execute
```
### Calucations/Checks with the Money Object
```php
// bool - same currency
$m->isSameCurrency(Money $otherMoney);
// 0=equal, 1=$m>$o, -1=$m<$o
$m->compare(Money $o);
// no explanation needed
$m->equals(Money $other);
$m->greaterThan(Money $o);
$m->greaterThanOrEqual(Money $o);
$m->lessThan(Money $o);
$m->lessThanOrEqual(Money $o);
$m->isZero();
$m->isPositive();
$m->isNegative();
// ALL MATH OPERATIONS ARE ASSERTING THE SAME CURRENCY !!!
// add amount and return new obj
$m->add(Money $o);
// substruct amount and return new obj
$m->subtract(Money $o);
// multiply amount with multiplier and return new obj
$m->multiply($multiplier);
// divide amount by divisor and return new obj
$m->divide($divisor, $roundingMode = PHP_ROUND_HALF_UP);
// allocate the amount in count($ratios) parts with
// the weight of the valiue of $ratios and return Money[]
$m->allocate(array $ratios);
// has this MoneyObj TaxCalculation (TaxedMoney)
$m->hasTax();
```
### Use the TaxedMoney Object
The **TaxedMoney** Class contains a amount (gross OR net) and a tax percentage.
On creation you define if the given amount is net or gross and if the default return value
(for amount() and calucations) is net or gross.
There are MoneyGross and MoneyNet as simpler representations for a TaxedMoney Object
with the static methods ::fromGross() and ::fromNet().
Examples:
```php
use Bnet\Money\MoneyNet;
use Bnet\Money\MoneyGross;
// 10EUR is the Net and 19% Tax
$m = MoneyGross::fromNet(1000, 19, 'EUR');
// return the net: 1000 -> 10EUR
$m->amountWithoutTax();
// both return the gross: 1190 -> 11,90EUR
$m->amount();
$m->amountWithTax();
// all calucaltions with this object are with the **gross**,
// cause this is the default amount
// so you can replace any Money Obj ex.: in a Cart, with a TaxedMoneyObj
// and check with the hasTax() method if you can show the Net/Gross
```
## Changelog
**0.1
**0.1.3
- add TaxedMoney for use MoneyObject with Tax
**0.1.2
- add some math and compare methods to Money and Currency
- add __toString, toArray, toJson methods
- add static Calling of Currency
**0.1.1
- add default currency repository for easier usage in small environments
**0.1.0
- create the basic functionality, prepared with Array and Callback CurrencyRepository
## License
......
......@@ -211,10 +211,10 @@ class Money implements \JsonSerializable, Jsonable{
*/
public function compare(self $other) {
$this->assertSameCurrency($other);
if ($this->amount < $other->amount) {
if ($this->amount() < $other->amount()) {
return -1;
}
if ($this->amount > $other->amount) {
if ($this->amount() > $other->amount()) {
return 1;
}
return 0;
......@@ -286,7 +286,7 @@ class Money implements \JsonSerializable, Jsonable{
*/
public function add(self $addend) {
$this->assertSameCurrency($addend);
return new self($this->amount + $addend->amount, $this->currency);
return new static($this->amount() + $addend->amount(), $this->currency);
}
/**
......@@ -300,7 +300,7 @@ class Money implements \JsonSerializable, Jsonable{
*/
public function subtract(self $subtrahend) {
$this->assertSameCurrency($subtrahend);
return new self($this->amount - $subtrahend->amount, $this->currency);
return new static($this->amount() - $subtrahend->amount(), $this->currency);
}
/**
......@@ -315,7 +315,7 @@ class Money implements \JsonSerializable, Jsonable{
* @throws \OutOfBoundsException
*/
public function multiply($multiplier, $roundingMode = PHP_ROUND_HALF_UP) {
return new self((int)round($this->amount * $multiplier, 0, $roundingMode), $this->currency);
return new static((int)round($this->amount() * $multiplier, 0, $roundingMode), $this->currency);
}
/**
......@@ -347,7 +347,7 @@ class Money implements \JsonSerializable, Jsonable{
if ($divisor == 0) {
throw new \InvalidArgumentException('Division by zero');
}
return new self((int)round($this->amount / $divisor, 0, $roundingMode), $this->currency);
return new static((int)round($this->amount() / $divisor, 0, $roundingMode), $this->currency);
}
/**
......@@ -358,18 +358,22 @@ class Money implements \JsonSerializable, Jsonable{
* @return array
*/
public function allocate(array $ratios) {
$remainder = $this->amount;
$remainder = $this->amount();
$results = [];
$total = array_sum($ratios);
foreach ($ratios as $ratio) {
$share = (int)floor($this->amount * $ratio / $total);
$results[] = new self($share, $this->currency);
$share = (int)floor($this->amount() * $ratio / $total);
$results[] = $share;
$remainder -= $share;
}
for ($i = 0; $remainder > 0; $i++) {
$results[$i]->amount++;
$results[$i]++;
$remainder--;
}
// generate MoneyObjects
foreach ($results as $k => $v) {
$results[$k] = new static($v, $this->currency);
}
return $results;
}
......@@ -379,7 +383,7 @@ class Money implements \JsonSerializable, Jsonable{
* @return bool
*/
public function isZero() {
return $this->amount == 0;
return $this->amount() == 0;
}
/**
......@@ -388,7 +392,7 @@ class Money implements \JsonSerializable, Jsonable{
* @return bool
*/
public function isPositive() {
return $this->amount > 0;
return $this->amount() > 0;
}
/**
......@@ -397,7 +401,7 @@ class Money implements \JsonSerializable, Jsonable{
* @return bool
*/
public function isNegative() {
return $this->amount < 0;
return $this->amount() < 0;
}
/**
......@@ -407,7 +411,7 @@ class Money implements \JsonSerializable, Jsonable{
*/
public function toArray() {
return [
'amount' => $this->amount,
'amount' => $this->amount(),
'number' => $this->normalize(),
'format' => $this->format(),
'currency' => $this->currency->code,
......@@ -464,4 +468,11 @@ class Money implements \JsonSerializable, Jsonable{
return new static($arguments[0], new Currency($method), $convert);
}
/**
* has this MoneyObj tax options
* @return bool
*/
public function hasTax() {
return false;
}
}
\ No newline at end of file
<?php
/**
* User: thorsten
* Date: 22.07.16
* Time: 20:50
*/
namespace Bnet\Money;
class MoneyGross extends TaxedMoney{
/**
* MoneyGross constructor.
* @param int $amount
* @param Currency|string $currency
* @param float|int $tax
* @param int $input_type
* @throws MoneyException
*/
public function __construct($amount, $currency, $tax, $input_type) {
parent::__construct($amount, $currency, $tax, $input_type, self::TYPE_GROSS);
}
}
\ No newline at end of file
<?php
/**
* User: thorsten
* Date: 22.07.16
* Time: 20:50
*/
namespace Bnet\Money;
class MoneyNet extends TaxedMoney {
/**
* MoneyNet constructor.
* @param int $amount
* @param Currency|string $currency
* @param float|int $tax
* @param int $input_type
* @throws MoneyException
*/
public function __construct($amount, $currency, $tax, $input_type) {
parent::__construct($amount, $currency, $tax, $input_type, self::TYPE_NET);
}
}
\ No newline at end of file
<?php
/**
* User: thorsten
* Date: 22.07.16
* Time: 15:57
*/
namespace Bnet\Money;
class TaxedMoney extends Money {
/**
* amount is gross/Brutto
*/
const TYPE_GROSS = 1;
/**
* amount is net/Netto
*/
const TYPE_NET = 2;
/**
* @var float|int the tax percentage for amount
*/
protected $tax;
/**
* @var self::TYPE_GROSS|self::TYPE_NET which type is the amount field
*/
protected $amount_type;
/**
* @var self::TYPE_GROSS|self::TYPE_NET which type is returned as amount() for default
*/
protected $default_return_type;
/**
* Money constructor.
* TaxedMoney constructor.
* @param int $amount
* @param Currency|string $currency
* @param float|int $tax
* @param int $input_type
* @param int $default_return_type
* @throws MoneyException
*/
public function __construct($amount, $currency = null, $tax = 0, $input_type = self::TYPE_NET, $default_return_type = self::TYPE_GROSS) {
$this->tax = $tax;
$this->amount_type = $input_type;
$this->default_return_type = $default_return_type;
parent::__construct($amount, $currency);
}
/**
* alias for fromGross
* @param int $amount
* @param float $tax tax percentage of the given amount
* @param Currency|string $currency
* @return static
*/
public static function fromBrutto($amount, $tax, $currency=null) {
return static::fromGross($amount, $tax, $currency);
}
/**
* create a MoneyObject with the given Amount/Tax as Gross
* @param int $amount
* @param float $tax tax percentage of the given amount
* @param Currency|string $currency
* @return static
*/
public static function fromGross($amount, $tax, $currency = null) {
return new static($amount, $currency, $tax, self::TYPE_GROSS);
}
/**
* alias for fromNet
* @param int $amount
* @param float $tax tax percentage of the given amount
* @param Currency|string $currency
* @return static
*/
public static function fromNetto($amount, $tax, $currency = null) {
return static::fromNet($amount, $tax, $currency);
}
/**
* create a MoneyObject with the given Amount/Tax as Net
* @param int $amount
* @param float $tax tax percentage of the given amount
* @param Currency|string $currency
* @return static
*/
public static function fromNet($amount, $tax, $currency = null) {
return new static($amount, $currency, $tax, self::TYPE_NET);
}
/**
* has this MoneyObj tax options
* @return bool
*/
public function hasTax() {
return true;
}
/**
* return the gross/net amount as defined in $this->default_return_type
* @param int $precision the number of precision positions for better calucations with the amount
* @return int
*/
public function amount($precision = 0) {
if ($this->amount_type == $this->default_return_type) {
$amount = parent::amount();
} elseif ($this->amount_type == self::TYPE_NET) {
$amount = $this->amountWithTax($precision);
} elseif ($this->amount_type == self::TYPE_GROSS) {
$amount = $this->amountWithoutTax($precision);
} else {
throw new MoneyException('Problems with defined types in TaxedMoney');
}
// cast to int if the precision is 0 for internal calculations that need and int
return $precision == 0
?(int)$amount
: $amount;
}
/**
* return the amount with tax
* @param int $precision the number of precision positions for better calucations with the amount
* @return float|int
*/
public function amountWithTax($precision=0) {
return $this->amount_type == self::TYPE_GROSS
? $this->amount
: $this->calcAddTax($this->amount, $precision);
}
/**
* return the amount without tax
* @param int $precision the number of precision positions for better calucations with the amount
* @return float|int
*/
public function amountWithoutTax($precision=0) {
return $this->amount_type == self::TYPE_NET
? $this->amount
: $this->calcSubTax($this->amount, $precision);
}
/**
* subtract the percentage of tax from the given amount
* @param float $amount
* @param int $precision the number of precision positions for better calucations with the amount
* @return float|int int if precision=0
*/
protected function calcSubTax($amount, $precision=0) {
return $this->round($amount / (1 + $this->tax/100), $precision);
}
/**
* add the percentage of tax to the given amount
* @param float|int $amount
* @param int $precision the number of precision positions for better calucations with the amount
* @return float|int int if precision=0
*/
protected function calcAddTax($amount, $precision=0) {
return $this->round($amount * (1 + $this->tax/100), $precision);
}
/**
* round the given float value from calulations as int
* @param float $amount
* @param int $precision the number of precision positions for better calucations with the amount
* @return float|int
*/
protected function round($amount, $precision=0) {
return round($amount, $precision);
}
}
\ No newline at end of file
......@@ -15,6 +15,16 @@ use Bnet\Money\Repositories\ArrayRepository;
class MoneyTest extends \PHPUnit_Framework_TestCase {
/**
* @param $amount
* @param null $currency
* @return Money
*/
public function money($amount, $currency = null) {
return new Money($amount, $currency);
}
/**
* Sets up the fixture, for example, open a network connection.
* This method is called before a test is executed.
......@@ -45,7 +55,7 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
public function testBasicFunctions() {
$amount = 123456;
$m = new Money($amount, $this->currency());
$m = $this->money($amount, $this->currency());
$this->assertEquals($amount, $m->amount(), 'Amount');
$this->assertEquals($amount, $m->value(), 'Amount');
$this->assertEquals(1234.56, $m->normalize(), 'Normalize');
......@@ -69,7 +79,7 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
public function testFormat() {
$amount = 123456;
$m = new Money($amount, $this->currency());
$m = $this->money($amount, $this->currency());
$this->assertEquals('1234,56€', $m->format(), 'default Format');
$this->assertEquals('1.234,56€', $m->format(true), 'Format +thPt');
......@@ -87,7 +97,7 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
public function testHtml() {
$amount = 123456;
$m = new Money($amount, $this->currency());
$m = $this->money($amount, $this->currency());
$this->assertEquals('<span class="money currency_eur"><span class="amount">1234,56</span><span class="symbol">€</span></span>', $m->html(), 'default Html');
$this->assertEquals('<span class="money currency_eur"><span class="amount">1.234,56</span><span class="symbol">€</span></span>', $m->html(true), 'Html +thPt');
......@@ -105,12 +115,12 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
public function testNoNumber() {
try {
new Money(11.1, $this->currency());
$this->money(11.1, $this->currency());
$this->fail('No Exception on float');
} catch (MoneyException $e) { }
try {
new Money('11.1', $this->currency());
$this->money('11.1', $this->currency());
$this->fail('No Exception on float-string');
} catch (MoneyException $e) { }
}
......@@ -159,7 +169,7 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
* @dataProvider provideStringsMoneyParsing
*/
public function testMoneyParsing($string, $units) {
$m = new Money($units);
$m = $this->money($units);
try {
$this->assertEquals($m->value(), Money::parse($string)->value(), 'Value: ' . $string);
} catch (\Exception $e) {
......@@ -177,26 +187,26 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
* @expectedException \Bnet\Money\MoneyException
*/
public function testStringThrowsException() {
new Money('foo', new Currency('EUR'));
$this->money('foo', new Currency('EUR'));
}
public function testGetters() {
$m = new Money(100, new Currency('EUR'));
$m = $this->money(100, new Currency('EUR'));
$this->assertEquals(100, $m->amount());
$this->assertEquals(1, $m->normalize());
$this->assertEquals(new Currency('EUR'), $m->currency());
}
public function testSameCurrency() {
$m = new Money(100, new Currency('EUR'));
$this->assertTrue($m->isSameCurrency(new Money(100, new Currency('EUR'))));
$this->assertFalse($m->isSameCurrency(new Money(100, new Currency('USD'))));
$m = $this->money(100, new Currency('EUR'));
$this->assertTrue($m->isSameCurrency($this->money(100, new Currency('EUR'))));
$this->assertFalse($m->isSameCurrency($this->money(100, new Currency('USD'))));
}
public function testComparison() {
$m1 = new Money(50, new Currency('EUR'));
$m2 = new Money(100, new Currency('EUR'));
$m3 = new Money(200, new Currency('EUR'));
$m1 = $this->money(50, new Currency('EUR'));
$m2 = $this->money(100, new Currency('EUR'));
$m3 = $this->money(200, new Currency('EUR'));
$this->assertEquals(-1, $m2->compare($m3));
$this->assertEquals(1, $m2->compare($m1));
$this->assertEquals(0, $m2->compare($m2));
......@@ -216,16 +226,16 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
* @expectedException \InvalidArgumentException
*/
public function testDifferentCurrenciesCannotBeCompared() {
$m1 = new Money(100, new Currency('EUR'));
$m2 = new Money(100, new Currency('USD'));
$m1 = $this->money(100, new Currency('EUR'));
$m2 = $this->money(100, new Currency('USD'));
$m1->compare($m2);
}
public function testAddition() {
$m1 = new Money(1100101, new Currency('EUR'));
$m2 = new Money(1100021, new Currency('EUR'));
$m1 = $this->money(1100101, new Currency('EUR'));
$m2 = $this->money(1100021, new Currency('EUR'));
$sum = $m1->add($m2);
$this->assertEquals(new Money(2200122, new Currency('EUR')), $sum);
$this->assertEquals($this->money(2200122, new Currency('EUR')), $sum);
$this->assertNotEquals($sum, $m1);
$this->assertNotEquals($sum, $m2);
}
......@@ -234,16 +244,16 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
* @expectedException \InvalidArgumentException
*/
public function testDifferentCurrenciesCannotBeAdded() {
$m1 = new Money(100, new Currency('EUR'));
$m2 = new Money(100, new Currency('USD'));
$m1 = $this->money(100, new Currency('EUR'));
$m2 = $this->money(100, new Currency('USD'));
$m1->add($m2);
}
public function testSubtraction() {
$m1 = new Money(10010, new Currency('EUR'));
$m2 = new Money(10002, new Currency('EUR'));
$m1 = $this->money(10010, new Currency('EUR'));
$m2 = $this->money(10002, new Currency('EUR'));
$diff = $m1->subtract($m2);
$this->assertEquals(new Money(8, new Currency('EUR')), $diff);
$this->assertEquals($this->money(8, new Currency('EUR')), $diff);
$this->assertNotSame($diff, $m1);
$this->assertNotSame($diff, $m2);
}
......@@ -252,55 +262,55 @@ class MoneyTest extends \PHPUnit_Framework_TestCase {
* @expectedException \InvalidArgumentException
*/
public function testDifferentCurrenciesCannotBeSubtracted() {
$m1 = new Money(100, new Currency('EUR'));
$m2 = new Money(100, new Currency('USD'));
$m1 = $this->money(100, new Currency('EUR'));
$m2 = $this->money(100, new Currency('USD'));
$m1->subtract($m2);
}
public function testMultiplication() {
$m1 = new Money(15, new Currency('EUR'));
$m2 = new Money(1, new Currency('EUR'));
$m1 = $this->money(15, new Currency('EUR'));
$m2 = $this->money(1, new Currency('EUR'));
$this->assertEquals($m1, $m2->multiply(15));
$this->assertNotEquals($m1, $m2->multiply(10));
}
public function testDivision() {
$m1 = new Money(3, new Currency('EUR'));
$m2 = new Money(10, new Currency('EUR'));
$m1 = $this->money(3, new Currency('EUR'));
$m2 = $this->money(10, new Currency('EUR'));
$this->assertEquals($m1, $m2->divide(3));
$this->assertNotEquals($m1, $m2->divide(2));
}
public function testAllocation() {
$m1 = new Money(100, new Currency('EUR'));
$m1 = $this->money(100, new Currency('EUR'));
list($part1, $part2, $part3) = $m1->allocate([1, 1, 1]);
$this->assertEquals(new Money(34, new Currency('EUR')), $part1);
$this->assertEquals(new Money(33, new Currency('EUR')), $part2);
$this->assertEquals(new Money(33, new Currency('EUR')), $part3);
$m2 = new Money(101, new Currency('EUR'));
$this->assertEquals($this->money(34, new Currency('EUR')), $part1);
$this->assertEquals($this->money(33, new Currency('EUR')), $part2);
$this->assertEquals($this->money(33, new Currency('EUR')), $part3);
$m2 = $this->money(101, new Currency('EUR'));
list($part1, $part2, $part3) = $m2->allocate([1, 1, 1]);
$this->assertEquals(new Money(34, new Currency('EUR')), $part1);
$this->assertEquals(new Money(34, new Currency('EUR')), $part2);
$this->assertEquals(new Money(33, new Currency('EUR')), $part3);
$this->assertEquals($this->money(34, new Currency('EUR')), $part1);
$this->assertEquals($this->money(34, new Currency('EUR')), $part2);
$this->assertEquals($this->money(33, new Currency('EUR')), $part3);
}
public function testAllocationOrderIsImportant() {
$m = new Money(5, new Currency('EUR'));
$m = $this->money(5, new Currency('EUR'));
list($part1, $part2) = $m->allocate([3, 7]);
$this->assertEquals(new Money(2, new Currency('EUR')), $part1);
$this->assertEquals(new Money(3, new Currency('EUR')), $part2);
$this->assertEquals($this->money(2, new Currency('EUR')), $part1);
$this->assertEquals($this->money(3, new Currency('EUR')), $part2);
list($part1, $part2) = $m->allocate([7, 3]);
$this->assertEquals(new Money(4, new Currency('EUR')), $part1);
$this->assertEquals(new Money(1, new Currency('EUR')), $part2);
$this->assertEquals($this->money(4, new Currency('EUR')), $part1);
$this->assertEquals($this->money(1, new Currency('EUR')), $part2);
}
public function testComparators() {
$m1 = new Money(0, new Currency('EUR'));
$m2 = new Money(-1, new Currency('EUR'));
$m3 = new Money(1, new Currency('EUR'));
$m4 = new Money(1, new Currency('EUR'));
$m5 = new Money(1, new Currency('EUR'));
$m6 = new Money(-1, new Currency('EUR'));
$m1 = $this->money(0, new Currency('EUR'));
$m2 = $this->money(-1, new Currency('EUR'));
$m3 = $this->money(1, new Currency('EUR'));
$m4 = $this->money(1, new Currency('EUR'));
$m5 = $this->money(1, new Currency('EUR'));
$m6 = $this->money(-1, new Currency('EUR'));
$this->assertTrue($m1->isZero());
$this->assertTrue($m2->isNegative());
$this->assertTrue($m3->isPositive());
......
<?php
/**
* User: thorsten
* Date: 22.07.16
* Time: 16:30
*/
namespace Tests\Bnet\Money;
use Bnet\Money\MoneyGross;
use Bnet\Money\MoneyNet;
use Bnet\Money\TaxedMoney;
/**
* Class TaxedMoneyTest - extens MoneyTest, so all Tests for MoneyTest have to work for TaxedMoney
* @package Tests\Bnet\Money
*/
class TaxedMoneyTest extends MoneyTest {
/**
* @param $amount
* @param null $currency
* @param int $tax
* @param int $input_type
* @param int $default_return_type