Commit ecba15e1 authored by Thorsten Buss's avatar Thorsten Buss
Browse files

* add CurrencyCart for items with MoneyObje with currency as ItemPrice

* optimize and add some tests and documentation for autocompletition
parent eadec7e1
......@@ -701,6 +701,7 @@ $items->each(function($item)
- rename some functions (items() instead of getItems()) and classes (Item instead of ItemCollection) for better usage (more laravel style)
- change the namespace to BNet
- make item_rules customizable (preparing for own CartClass with additional custom item fields)
- add CurrencyCart for use a cart with Money() prices instead of int
**2.4.0
- added new method on a condition: $condition->getAttributes(); (Please see [Conditions](#conditions) section)
......
......@@ -48,7 +48,7 @@ class Cart {
protected $item_rules = array(
'id' => 'required',
'price' => 'required|numeric',
// 'price' => 'required|numeric',
'quantity' => 'required|numeric|min:1',
'name' => 'required',
);
......@@ -56,10 +56,10 @@ class Cart {
/**
* our object constructor
*
* @param $session
* @param $events
* @param $instanceName
* @param $session_key
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @param string $instanceName
* @param string $session_key
* @param array $custom_item_rules overwrite existing item_rules
*/
public function __construct($session, $events, $instanceName, $session_key, $custom_item_rules=[]) {
......@@ -470,7 +470,7 @@ class Cart {
public function subTotal() {
$cart = $this->items();
$sum = $cart->sum(function ($item) {
$sum = $cart->sum(function (Item $item) {
return $item->priceSumWithConditions();
});
......@@ -539,9 +539,7 @@ class Cart {
* @return bool
*/
public function isEmpty() {
$cart = new Items($this->session->get($this->sessionKeyCartItems));
return $cart->isEmpty();
return $this->items()->isEmpty();
}
/**
......@@ -570,15 +568,24 @@ class Cart {
protected function addRow($id, $item) {
$this->events->fire($this->getInstanceName() . '.adding', array($item, $this));
$cart = $this->items();
$items = $this->items();
$cart->put($id, new Item($item));
$items->put($id, $this->createCartItem($item));
$this->save($cart);
$this->save($items);
$this->events->fire($this->getInstanceName() . '.added', array($item, $this));
}
/**
* create the object for an cart item
* @param $data
* @return Item
*/
protected function createCartItem($data) {
return new Item($data);
}
/**
* save the cart
*
......
<?php
/**
* User: thorsten
* Date: 13.07.16
* Time: 22:48
*/
namespace Bnet\Cart;
use Bnet\Money\Currency;
use Bnet\Money\Money;
/**
* Class CurrencyCart - same as Cart, but work with Money Objects with Currency as pricees
* @package Bnet\Cart
*/
class CurrencyCart extends Cart {
/**
* @var string Alpha ISo Code of the default currency for this cart
*/
protected $currency;
/**
* our object constructor
*
* @param \Symfony\Component\HttpFoundation\Session\SessionInterface $session
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @param string $instanceName
* @param string $session_key
* @param string|Currency $currency Alpha IsoCode of the Currency of this Cart - only items with this currency are allowed and items without currency get this currency
* @param array $custom_item_rules overwrite existing item_rules
*/
public function __construct(\Symfony\Component\HttpFoundation\Session\SessionInterface $session, \Illuminate\Contracts\Events\Dispatcher $events, $instanceName, $session_key, $currency='EUR', $custom_item_rules = []) {
$this->currency = $currency instanceof Currency ? $currency : new Currency($currency);
parent::__construct($session, $events, $instanceName, $session_key, $custom_item_rules);
}
/**
* get cart sub total
*
* @return Money
*/
public function subTotal() {
$sum = $this->items()->sum(function (CurrencyItem $item) {
return $item->priceSumWithConditions()->amount();
});
return new Money($sum, $this->currency);
}
/**
* the new total in which conditions are already applied
*
* @return Money
*/
public function total() {
if ($this->getConditions()->isEmpty())
return $this->subTotal();
$subTotal = $this->subTotal()->amount();
$newTotal = 0;
$process = 0;
$this->getConditions()->each(function ($cond) use ($subTotal, &$newTotal, &$process) {
if ($cond->getTarget() === 'subtotal') {
($process > 0) ? $toBeCalculated = $newTotal : $toBeCalculated = $subTotal;
$newTotal = $cond->applyCondition($toBeCalculated);
$process++;
}
});
return new Money((int)$newTotal, $this->currency);
}
/**
* get the cart
*
* @return CurrencyItems
*/
public function items() {
return (new CurrencyItems($this->session->get($this->sessionKeyCartItems)));
}
/**
* add row to cart collection
*
* @param $id
* @param $item
*/
protected function addRow($id, $item) {
if (!$item['price'] instanceof Money)
$item['price'] = new Money($item['price'], @$item['currency'] ?: $this->currency);
parent::addRow($id, $item);
}
/**
* add item to the cart, it can be an array or multi dimensional array
*
* @param string|array $id
* @param string $name
* @param Money $price
* @param int $quantity
* @param array $attributes
* @param Condition|array $conditions
* @return $this
* @throws \Bnet\Cart\Exceptions\InvalidItemException
*/
public function add($id, $name = null, Money $price = null, $quantity = 1, $attributes = array(), $conditions = array()) {
if (!is_null($price) && !$price->currency()->equals($this->currency))
throw new \Bnet\Cart\Exceptions\CurrencyNotMachedException('given item-currency ['.$price->currency()->code.'] does not match to cart currency ['.$this->currency->code.']' );
return parent::add($id, $name, $price, $quantity, $attributes, $conditions);
}
/**
* create the object for an cart item
* @param $data
* @return Item
*/
protected function createCartItem($data) {
return new CurrencyItem($data, $this->currency);
}
/**
* get an item on a cart by item ID
*
* @param $itemId
* @return CurrencyItem
*/
public function get($itemId) {
$item = parent::get($itemId);
if (!$item['price'] instanceof Money)
$item['price'] = new Money($item['price'], @$item['currency'] ?: $this->currency);
return $item;
}
}
\ No newline at end of file
<?php
/**
* User: thorsten
* Date: 13.07.16
* Time: 23:20
*/
namespace Bnet\Cart;
use Bnet\Money\Currency;
use Bnet\Money\Money;
/**
* Class CurrencyItem
* @package Bnet\Cart
* @inheritdoc
* @property Money|null price
*/
class CurrencyItem extends Item {
/**
* @var Currency|string the currency for this Item for generating Sum Money Objects
*/
protected $currency;
/**
* Create a new collection.
*
* @param mixed $items
* @return void
*/
public function __construct($items, $currency) {
$this->currency = $currency;
parent::__construct($items);
}
/**
* return the price amount for this item
* @return int|null
*/
public function price() {
return $this->price->amount();
$price = $this->price;
return $price instanceOf Money ? $price->amount() : $price;
}
/**
* get the sum of price
*
* @return Money
*/
public function priceSum() {
return new Money(parent::priceSum(), $this->currency);
}
/**
* get the single price in which conditions are already applied
*
* @return Money
*/
public function priceWithConditions() {
return new Money((int)parent::priceWithConditions(), $this->currency);
}
/**
* get the sum of price in which conditions are already applied
*
* @return Money
*/
public function priceSumWithConditions() {
$sum = $this->priceWithConditions()->amount() * $this->quantity;
return new Money((int)$sum, $this->currency);
}
}
\ No newline at end of file
<?php
/**
* User: thorsten
* Date: 13.07.16
* Time: 23:48
*/
namespace Bnet\Cart;
/**
* Class CurrencyItems
* @package Bnet\Cart
* @method CurrencyItem first() first(callable $callback = null, $default = null)
* @method CurrencyItem get() get($key, $default = null)
* @method CurrencyItem last() last(callable $callback = null, $default = null)
* @method CurrencyItem[] all()
*/
class CurrencyItems extends Items {
}
\ No newline at end of file
<?php
/**
* User: thorsten
* Date: 14.07.16
* Time: 00:31
*/
namespace Bnet\Cart\Exceptions;
class CurrencyNotMachedException extends InvalidItemException{
}
\ No newline at end of file
<?php namespace Bnet\Cart\Helpers;
use Bnet\Money\Money;
/**
* Created by PhpStorm.
......@@ -23,6 +24,8 @@ class Helpers {
* @return int
*/
public static function normalizePrice($price) {
if ($price instanceof Money)
return $price;
return (is_string($price)) ? intval($price) : $price;
}
......@@ -31,6 +34,8 @@ class Helpers {
* @param int $mode
*/
public static function intval($value, $mode = self::DEFAULT_ROUND_MODE) {
if ($value instanceof Money)
return $value;
return self::round($value, 0, $mode);
}
......@@ -40,6 +45,8 @@ class Helpers {
* @param int $mode
*/
public static function round($value, $precision = 0, $mode = self::DEFAULT_ROUND_MODE) {
if ($value instanceof Money)
return $value;
return round($value, $precision, $mode);
}
......
......@@ -25,7 +25,7 @@ class Item extends Collection {
* @return mixed|null
*/
public function priceSum() {
return $this->price * $this->quantity;
return $this->price() * $this->quantity;
}
public function __get($name) {
......@@ -48,13 +48,21 @@ class Item extends Collection {
return $this['conditions'] instanceof Condition;
}
/**
* return the price amount for this item
* @return int
*/
public function price() {
return $this->price;
}
/**
* get the single price in which conditions are already applied
*
* @return mixed|null
*/
public function priceWithConditions() {
$originalPrice = $this->price;
$originalPrice = $this->price();
$newPrice = 0;
$processed = 0;
......
......@@ -2,6 +2,14 @@
use Illuminate\Support\Collection;
/**
* Class Items
* @package Bnet\Cart
* @method Item first() first(callable $callback = null, $default = null)
* @method Item get() get($key, $default = null)
* @method Item last() last(callable $callback = null, $default = null)
* @method Item[] all()
*/
class Items extends Collection {
}
\ No newline at end of file
This diff is collapsed.
<?php
/**
* Created by PhpStorm.
* User: darryl
* Date: 1/12/2015
* Time: 9:59 PM
*/
use Bnet\Cart\CurrencyCart as Cart;
use Bnet\Money\Money;
use Mockery as m;
require_once __DIR__ . '/helpers/SessionMock.php';
class CurrencyCartTest extends PHPUnit_Framework_TestCase {
/**
* @var Bnet\Cart\CurrencyCart
*/
protected $cart;
public function setUp() {
$events = m::mock('Illuminate\Contracts\Events\Dispatcher');
$events->shouldReceive('fire');
$this->cart = new Cart(
new SessionMock(),
$events,
'shopping',
'SAMPLESESSIONKEY'
);
}
public function tearDown() {
m::close();
}
public function test_cart_can_add_item() {
$this->cart->add(455, 'Sample Item', new Money(10099), 2, array());
$this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty');
$this->assertEquals(1, $this->cart->items()->count(), 'Cart content should be 1');
$this->assertEquals(455, $this->cart->items()->first()['id'], 'Item added has ID of 455 so first content ID should be 455');
$this->assertEquals(10099, $this->cart->items()->first()['price']->amount(), 'Item added has price of 10099 so first content price should be 10099');
}
public function test_cart_can_add_items_as_array() {
$item = array(
'id' => 456,
'name' => 'Sample Item',
'price' => new Money(6799),
'quantity' => 4,
'attributes' => array()
);
$this->cart->add($item);
$this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty');
$this->assertEquals(1, $this->cart->items()->count(), 'Cart should have 1 item on it');
$this->assertEquals(456, $this->cart->items()->first()['id'], 'The first content must have ID of 456');
$this->assertEquals('Sample Item', $this->cart->items()->first()['name'], 'The first content must have name of "Sample Item"');
}
public function test_cart_can_add_items_with_multidimensional_array() {
$this->assertEquals(0, count($this->cart->items()->toArray()), 'Cart should have 0 items');
$items = array(
array(
'id' => 456,
'name' => 'Sample Item 1',
'price' => new Money(6799),
'quantity' => 4,
'attributes' => array()
),
array(
'id' => 568,
'name' => 'Sample Item 2',
'price' => new Money(6925),
'quantity' => 4,
'attributes' => array()
),
array(
'id' => 856,
'name' => 'Sample Item 3',
'price' => new Money(5025),
'quantity' => 4,
'attributes' => array()
),
);
$this->cart->add($items);
$this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty');
$this->assertEquals(3, count($this->cart->items()->toArray()), 'Cart should have 3 items');
}
public function test_cart_can_add_item_without_attributes() {
$this->assertTrue($this->cart->isEmpty(), 'Cart should be empty');
$item = array(
'id' => 456,
'name' => 'Sample Item 1',
'price' => new Money(6799),
'quantity' => 4
);
$this->cart->add($item);
$this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty');
}
public function test_cart_update_with_attribute_then_attributes_should_be_still_instance_of_Attribute() {
$item = array(
'id' => 456,
'name' => 'Sample Item 1',
'price' => new Money(6799),
'quantity' => 4,
'attributes' => array(
'product_id' => '145',
'color' => 'red'
)
);
$this->cart->add($item);
// lets get the attribute and prove first its an instance of
// Attribute
$item = $this->cart->get(456);
$this->assertInstanceOf('Bnet\Cart\Attribute', $item->attributes);
$this->assertEquals('red', $item->attributes->get('color'), 'attribute value matched');
// now lets update the item with its new attributes
// when we get that item from cart, it should still be an instance of Attribute
$updatedItem = array(
'attributes' => array(
'product_id' => '145',
'color' => 'blue'
)
);
$this->cart->update(456, $updatedItem);
$this->assertInstanceOf('Bnet\Cart\Attribute', $item->attributes);
$this->assertEquals('blue', $item->attributes->get('color'), 'attribute value matched');
}
public function test_cart_items_attributes() {
$item = array(
'id' => 456,
'name' => 'Sample Item 1',
'price' => new Money(6799),
'quantity' => 4,
'attributes' => array(
'size' => 'L',
'color' => 'blue'
)
);
$this->cart->add($item);
$this->assertFalse($this->cart->isEmpty(), 'Cart should not be empty');
$this->assertEquals(2, count($this->cart->items()->first()['attributes']), 'Item\'s attribute should have two');
$this->assertEquals('L', $this->cart->items()->first()->attributes->size, 'Item should have attribute size of L');
$this->assertEquals('blue', $this->cart->items()->first()->attributes->color, 'Item should have attribute color of blue');
$this->assertTrue($this->cart->get(456)->has('attributes'), 'Item should have attributes');
$this->assertEquals('L', $this->cart->get(456)->get('attributes')->size);
}
public function test_cart_update_existing_item() {
$items = array(
array(
'id' => 456,
'name' => 'Sample Item 1',
'price' => new Money(6799),
'quantity' => 3,
'attributes' => array()
),
array(
'id' => 568,
'name' => 'Sample Item 2',
'price' => new Money(6925),
'quantity' => 1,
'attributes' => array()
),
);
$this->cart->add($items);
$itemIdToEvaluate = 456;
$item = $this->cart->get($itemIdToEvaluate);
$this->assertEquals('Sample Item 1', $item['name'], 'Item name should be "Sample Item 1"');
$this->assertEquals(6799, $item['price']->amount(), 'Item price should be "6799"');
$this->assertEquals(3, $item['quantity'], 'Item quantity should be 3');
// when cart's item quantity is updated, the subtotal should be updated as well
$this->cart->update(456, array(
'name' => 'Renamed',
'quantity' => 2,
'price' => new Money(105),
));
$item = $this->cart->get($itemIdToEvaluate);
$this->assertEquals('Renamed', $item['name'], 'Item name should be "Renamed"');
$this->assertEquals(105, $item['price']->amount(), 'Item price should be 105');
$this->assertEquals(5, $item['quantity'], 'Item quantity should be 2');