Skip to content

Commit

Permalink
change price data type from float to string
Browse files Browse the repository at this point in the history
- floats don't suite for prices
- use Money so we don't reinvent wheel
- don't use Money directly because it doesn't fit use-cases prefectly, so we still use custom proxy value object Price
  • Loading branch information
Svaťa Šimara committed Jan 23, 2019
1 parent f48fb4a commit 3f9dc5d
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 68 deletions.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -10,7 +10,8 @@
],
"require": {
"php": "^7.1",
"doctrine/orm": "^2.5"
"doctrine/orm": "^2.5",
"moneyphp/money": "~3.2"
},
"require-dev": {
"phpunit/phpunit": "~7.0",
Expand Down
50 changes: 40 additions & 10 deletions src/Domain/Price.php
Expand Up @@ -2,17 +2,23 @@

namespace Simara\Cart\Domain;

use Money\Money;
use Money\Number;

class Price
{
private const EURO_TO_CENTS_CONVERTING_BASE = -2;
private const CENTS_TO_EURO_CONVERTING_BASE = 2;

/**
* @var float
* @var Money
*/
private $withVat;

public function __construct(float $withVat)
public function __construct(string $withVat)
{
$this->withVat = $withVat;
$cents = $this->eurToCents($withVat);
$this->withVat = Money::EUR($cents);
}

/**
Expand All @@ -23,23 +29,47 @@ public static function sum(array $prices): self
{
return array_reduce($prices, function (self $carry, self $price) {
return $carry->add($price);
}, new self(0.0));
}, new self('0'));
}

public function getWithVat(): float
public function getWithVat(): string
{
return $this->withVat;
return $this->centsToEurs($this->withVat->getAmount());
}

public function add(self $adder): self
{
$withVat = $this->withVat + $adder->withVat;
return new self($withVat);
$withVat = $this->withVat->add($adder->withVat);
return self::fromMoney($withVat);
}

public function multiply(int $multiplier): self
{
$withVat = $this->withVat * $multiplier;
return new self($withVat);
$withVat = $this->withVat->multiply($multiplier);
return self::fromMoney($withVat);
}

private static function fromMoney(Money $withVat): self
{
$price = new self('0');
$price->withVat = $withVat;
return $price;
}

private function eurToCents(string $euros): string
{
$number = Number::fromString($euros);
return $number->base10(self::EURO_TO_CENTS_CONVERTING_BASE)->getIntegerPart();
}

private function centsToEurs(string $cents): string
{
$number = new Number($cents);
$inEuro = $number->base10(self::CENTS_TO_EURO_CONVERTING_BASE);
if ($inEuro->isInteger()) {
return $inEuro->getIntegerPart();
} else {
return $inEuro->getIntegerPart() . '.' . $inEuro->getFractionalPart();
}
}
}
14 changes: 9 additions & 5 deletions src/Infrastructure/DoctrineMapping/PriceType.php
Expand Up @@ -6,6 +6,7 @@

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use Exception;
use Simara\Cart\Domain\Price;

class PriceType extends Type
Expand All @@ -14,18 +15,21 @@ class PriceType extends Type

public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'FLOAT';
return 'DECIMAL(8,2)';
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return new Price((float) $value);
if ($value === null) {
return null;
}
return new Price($value);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value instanceof Price) {
return (string) $value->getWithVat();
if (!$value instanceof Price) {
throw new Exception('Value must be type of Price');
}
return $value;
return $value->getWithVat();
}
public function getName()
{
Expand Down
44 changes: 22 additions & 22 deletions tests/Domain/CartTest.php
Expand Up @@ -12,45 +12,45 @@ public function testCalculateEmptyCart()
{
$cart = new Cart('1');

$expected = new CartDetail([], new Price(0.0));
$expected = new CartDetail([], new Price("0.0"));

Assert::assertEquals($expected, $cart->calculate());
}

public function testAddSingleProductToEmpty()
{
$cart = new Cart('1');
$cart->add('a', new Price(10.0));
$cart->add('a', new Price("10.0"));

$expectedItem = new ItemDetail('a', new Price(10.0), 1);
$expected = new CartDetail([$expectedItem], new Price(10.0));
$expectedItem = new ItemDetail('a', new Price("10.0"), 1);
$expected = new CartDetail([$expectedItem], new Price("10.0"));

Assert::assertEquals($expected, $cart->calculate());
}

public function testAddTwoDifferentProducts()
{
$cart = new Cart('1');
$cart->add('a', new Price(10.0));
$cart->add('b', new Price(20.0), 2);
$cart->add('a', new Price("10.0"));
$cart->add('b', new Price("20.0"), 2);

$expectedItems = [
new ItemDetail('a', new Price(10.0), 1),
new ItemDetail('b', new Price(20.0), 2),
new ItemDetail('a', new Price("10.0"), 1),
new ItemDetail('b', new Price("20.0"), 2),
];
$expected = new CartDetail($expectedItems, new Price(50.0));
$expected = new CartDetail($expectedItems, new Price("50.0"));

Assert::assertEquals($expected, $cart->calculate());
}

public function testAddSameProductIncrementAmountOnly()
{
$cart = new Cart('1');
$cart->add('a', new Price(10.0));
$cart->add('a', new Price(0.0));
$cart->add('a', new Price("10.0"));
$cart->add('a', new Price("0.0"));

$expectedItem = new ItemDetail('a', new Price(10.0), 2);
$expected = new CartDetail([$expectedItem], new Price(20.0));
$expectedItem = new ItemDetail('a', new Price("10.0"), 2);
$expected = new CartDetail([$expectedItem], new Price("20.0"));

Assert::assertEquals($expected, $cart->calculate());
}
Expand All @@ -68,22 +68,22 @@ public function testRemoveNotExistingProduct()
$this->expectException(ProductNotInCartException::class);

$cart = new Cart('1');
$cart->add('a', new Price(10.0));
$cart->add('a', new Price("10.0"));
$cart->remove('x');
}

public function testRemoveProduct()
{
$cart = new Cart('1');
$cart->add('a', new Price(10.0));
$cart->add('b', new Price(20.0), 2);
$cart->add('a', new Price("10.0"));
$cart->add('b', new Price("20.0"), 2);

$cart->remove('a');

$expectedItems = [
new ItemDetail('b', new Price(20.0), 2),
new ItemDetail('b', new Price("20.0"), 2),
];
$expected = new CartDetail($expectedItems, new Price(40.0));
$expected = new CartDetail($expectedItems, new Price("40.0"));

Assert::assertEquals($expected, $cart->calculate());
}
Expand All @@ -93,19 +93,19 @@ public function testChangeAmountOfNotExisting()
$this->expectException(ProductNotInCartException::class);

$cart = new Cart('1');
$cart->add('a', new Price(10.0));
$cart->add('a', new Price("10.0"));

$cart->changeAmount('x', 5);
}

public function testChangeAmount()
{
$cart = new Cart('1');
$cart->add('a', new Price(10.0));
$cart->add('a', new Price("10.0"));
$cart->changeAmount('a', 5);

$expectedItem = new ItemDetail('a', new Price(10.0), 5);
$expected = new CartDetail([$expectedItem], new Price(50.0));
$expectedItem = new ItemDetail('a', new Price("10.0"), 5);
$expected = new CartDetail([$expectedItem], new Price("50.0"));

Assert::assertEquals($expected, $cart->calculate());
}
Expand Down
28 changes: 14 additions & 14 deletions tests/Domain/ItemTest.php
Expand Up @@ -10,79 +10,79 @@ class ItemTest extends TestCase

public function testToDetail()
{
$item = new Item('x', new Price(5.0), 2);
$item = new Item('x', new Price("5.0"), 2);

$expected = new ItemDetail('x', new Price(5.0), 2);
$expected = new ItemDetail('x', new Price("5.0"), 2);

Assert::assertEquals($expected, $item->toDetail());
}

public function testAdd()
{
$item = new Item('x', new Price(5.0), 2);
$item = new Item('x', new Price("5.0"), 2);
$item->add(5);

$expected = new ItemDetail('x', new Price(5.0), 7);
$expected = new ItemDetail('x', new Price("5.0"), 7);

Assert::assertEquals($expected, $item->toDetail());
}

public function testChangeAmount()
{
$item = new Item('x', new Price(5.0), 2);
$item = new Item('x', new Price("5.0"), 2);
$item->changeAmount(1);

$expected = new ItemDetail('x', new Price(5.0), 1);
$expected = new ItemDetail('x', new Price("5.0"), 1);

Assert::assertEquals($expected, $item->toDetail());
}

public function testInitialAmountCannotBeNegative()
{
$this->expectException(AmountMustBePositiveException::class);
new Item('x', new Price(5.0), -1);
new Item('x', new Price("5.0"), -1);
}

public function testInitialAmountCannotBeZero()
{
$this->expectException(AmountMustBePositiveException::class);
new Item('x', new Price(5.0), 0);
new Item('x', new Price("5.0"), 0);
}

public function testAddNegativeThrowsException()
{
$this->expectException(AmountMustBePositiveException::class);
$item = new Item('x', new Price(5.0), 1);
$item = new Item('x', new Price("5.0"), 1);
$item->add(-1);
}

public function testAddZeroThrowsException()
{
$this->expectException(AmountMustBePositiveException::class);
$item = new Item('x', new Price(5.0), 1);
$item = new Item('x', new Price("5.0"), 1);
$item->add(0);
}

public function testChangeToNegativeThrowsException()
{
$this->expectException(AmountMustBePositiveException::class);
$item = new Item('x', new Price(5.0), 1);
$item = new Item('x', new Price("5.0"), 1);
$item->changeAmount(-1);
}

public function testChangeToZeroThrowsException()
{
$this->expectException(AmountMustBePositiveException::class);
$item = new Item('x', new Price(5.0), 1);
$item = new Item('x', new Price("5.0"), 1);
$item->changeAmount(0);
}

public function testCalculateTotalPrice()
{
$item = new Item('x', new Price(5.0), 3);
$item = new Item('x', new Price("5.0"), 3);
$price = $item->calculatePrice();

$expected = new Price(15.0);
$expected = new Price("15.0");
Assert::assertEquals($expected, $price);
}
}
39 changes: 30 additions & 9 deletions tests/Domain/PriceTest.php
Expand Up @@ -10,33 +10,54 @@ class PriceTest extends TestCase

public function testAdd()
{
$a = new Price(10.0);
$b = new Price(0.5);
$a = new Price("10.0");
$b = new Price("0.5");
$result = $a->add($b);

$expected = new Price(10.5);
$expected = new Price("10.5");
Assert::assertEquals($expected, $result);
}

public function testMultiply()
{
$a = new Price(10.3);
$a = new Price("10.3");
$result = $a->multiply(2);

$expected = new Price(20.6);
$expected = new Price("20.6");
Assert::assertEquals($expected, $result);
}

public function testSum()
{
$prices = [
new Price(9.0),
new Price(0.7),
new Price(0.3),
new Price("9.0"),
new Price("0.7"),
new Price("0.3"),
];

$sum = Price::sum($prices);
$expected = new Price(10.0);
$expected = new Price("10.0");
Assert::assertEquals($expected, $sum);
}

/**
* @dataProvider getterTestCases
*/
public function testGetter(string $converted, string $expected)
{
$price = new Price($converted);
$this->assertSame($expected, $price->getWithVat());
}

public function getterTestCases(): array
{
return [
["0.005", "0"],
["0.05" , "0.05"],
["0.5" , "0.5"],
["0" , "0"],
["5" , "5"],
["5.555", "5.55"],
];
}
}

0 comments on commit 3f9dc5d

Please sign in to comment.