diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 5f86449..9652373 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -22,34 +22,27 @@ jobs: env: PHP_VERSION: ${{ matrix.php-versions }} steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} + extensions: bcmath,gmp,intl env: runner: ubuntu-18.04 - - - name: Use appropiate Laravel version + + - name: Checkout + uses: actions/checkout@v2 + + - name: Use appropiate Laravel version in composer.json (without installing) run: | - composer require "laravel/framework:${{ matrix.laravel-version }}" --no-interaction --no-update + composer require --no-update --no-interaction "laravel/framework:${{ matrix.laravel-version }}" + composer validate - - name: Validate composer.json and composer.lock - run: composer validate - - - name: Get composer cache directory - id: composercache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache Composer packages - id: composer-cache + - name: Cache dependencies uses: actions/cache@v2 with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- + path: ~/.composer/cache/files + key: dependencies-laravel-${{ matrix.laravel-version }}-php-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }} - name: Install composer packages run: composer install --prefer-dist --no-interaction diff --git a/.gitignore b/.gitignore index b9eaa7d..20dbf72 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ composer.phar composer.lock .DS_Store coverage.xml -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +/.vscode \ No newline at end of file diff --git a/README.md b/README.md index 1117055..7de5bb0 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ Run the Composer require command from the Terminal: Now you're ready to start using the shoppingcart in your application. -**As of version 2 of this package it's possibly to use dependency injection to inject an instance of the Cart class into your controller or other class** - You definitely should publish the `config` file and take a look at it. php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="config" @@ -611,7 +609,7 @@ class DefaultCalculator implements Calculator { public static function getAttribute(string $attribute, CartItem $cartItem) { - $decimals = config('cart.format.decimals', 2); + $decimals = Config::get('cart.format.decimals', 2); switch ($attribute) { case 'discount': diff --git a/composer.json b/composer.json index 0c3e5a6..8a36115 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,9 @@ "illuminate/support": "^6.0|^7.0|^8.0|^9.0", "illuminate/session": "^6.0|^7.0|^8.0|^9.0", "illuminate/events": "^6.0|^7.0|^8.0|^9.0", - "nesbot/carbon": "^2.0" + "illuminate/database": "^6.0|^7.0|^8.0|^9.0", + "nesbot/carbon": "^2.0", + "moneyphp/money": "^4.0.0" }, "require-dev": { "phpunit/phpunit": "~8.0|~9.0", diff --git a/src/Calculation/DefaultCalculator.php b/src/Calculation/DefaultCalculator.php deleted file mode 100644 index d95f7a7..0000000 --- a/src/Calculation/DefaultCalculator.php +++ /dev/null @@ -1,37 +0,0 @@ -price * ($cartItem->getDiscountRate() / 100); - case 'tax': - return round($cartItem->priceTarget * ($cartItem->taxRate / 100), $decimals); - case 'priceTax': - return round($cartItem->priceTarget + $cartItem->tax, $decimals); - case 'discountTotal': - return round($cartItem->discount * $cartItem->qty, $decimals); - case 'priceTotal': - return round($cartItem->price * $cartItem->qty, $decimals); - case 'subtotal': - return max(round($cartItem->priceTotal - $cartItem->discountTotal, $decimals), 0); - case 'priceTarget': - return round(($cartItem->priceTotal - $cartItem->discountTotal) / $cartItem->qty, $decimals); - case 'taxTotal': - return round($cartItem->subtotal * ($cartItem->taxRate / 100), $decimals); - case 'total': - return round($cartItem->subtotal + $cartItem->taxTotal, $decimals); - default: - return; - } - } -} diff --git a/src/Calculation/GrossPrice.php b/src/Calculation/GrossPrice.php deleted file mode 100644 index 92cc219..0000000 --- a/src/Calculation/GrossPrice.php +++ /dev/null @@ -1,39 +0,0 @@ -price / (1 + ($cartItem->taxRate / 100)), $decimals); - case 'discount': - return $cartItem->priceNet * ($cartItem->getDiscountRate() / 100); - case 'tax': - return round($cartItem->priceTarget * ($cartItem->taxRate / 100), $decimals); - case 'priceTax': - return round($cartItem->priceTarget + $cartItem->tax, $decimals); - case 'discountTotal': - return round($cartItem->discount * $cartItem->qty, $decimals); - case 'priceTotal': - return round($cartItem->priceNet * $cartItem->qty, $decimals); - case 'subtotal': - return max(round($cartItem->priceTotal - $cartItem->discountTotal, $decimals), 0); - case 'priceTarget': - return round(($cartItem->priceTotal - $cartItem->discountTotal) / $cartItem->qty, $decimals); - case 'taxTotal': - return round($cartItem->subtotal * ($cartItem->taxRate / 100), $decimals); - case 'total': - return round($cartItem->subtotal + $cartItem->taxTotal, $decimals); - default: - return; - } - } -} diff --git a/src/CanBeBought.php b/src/CanBeBought.php index 544f36b..eb9810f 100644 --- a/src/CanBeBought.php +++ b/src/CanBeBought.php @@ -2,6 +2,9 @@ namespace Gloudemans\Shoppingcart; +use Money\Currency; +use Money\Money; + trait CanBeBought { /** @@ -9,47 +12,41 @@ trait CanBeBought * * @return int|string */ - public function getBuyableIdentifier() + public function getBuyableIdentifier(CartItemOptions $options) { return method_exists($this, 'getKey') ? $this->getKey() : $this->id; } /** * Get the name, title or description of the Buyable item. - * - * @return string */ - public function getBuyableDescription() : ?string + public function getBuyableDescription(CartItemOptions $options): ?string { if (($name = $this->getAttribute('name'))) { return $name; - } else if (($title = $this->getAttribute('title'))) { + } elseif (($title = $this->getAttribute('title'))) { return $title; - } else if (($description = $this->getAttribute('description'))) { + } elseif (($description = $this->getAttribute('description'))) { return $description; } else { return null; - } + } } /** * Get the price of the Buyable item. - * - * @return float */ - public function getBuyablePrice() + public function getBuyablePrice(CartItemOptions $options): Money { - if (($price = $this->getAttribute('price'))) { - return $price; + if (($price = $this->getAttribute('price')) && ($currency = $this->getAttribute('currency'))) { + return new Money($price, new Currency($currency)); } } /** * Get the weight of the Buyable item. - * - * @return float */ - public function getBuyableWeight() + public function getBuyableWeight(CartItemOptions $options): int { if (($weight = $this->getAttribute('weight'))) { return $weight; diff --git a/src/Cart.php b/src/Cart.php index 17f2487..72e57e3 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -11,9 +11,14 @@ use Gloudemans\Shoppingcart\Exceptions\InvalidRowIDException; use Gloudemans\Shoppingcart\Exceptions\UnknownModelException; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\DatabaseManager; +use Illuminate\Database\Eloquent\Model; use Illuminate\Session\SessionManager; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Traits\Macroable; +use InvalidArgumentException; +use Money\Currency; +use Money\Money; class Cart { @@ -33,31 +38,23 @@ class Cart /** * Holds the current cart instance. - * - * @var string */ private string $instance; /** * Holds the creation date of the cart. - * - * @var mixed */ - private $createdAt; + private ?Carbon $createdAt = null; /** * Holds the update date of the cart. - * - * @var mixed */ - private $updatedAt; + private ?Carbon $updatedAt = null; /** * Defines the discount percentage. - * - * @var float */ - private $discount = 0; + private float $discount = 0; /** * Defines the tax rate. @@ -76,7 +73,7 @@ class Cart { $this->session = $session; $this->events = $events; - $this->taxRate = config('cart.tax'); + $this->taxRate = Config::get('cart.tax'); $this->instance(self::DEFAULT_INSTANCE); } @@ -88,7 +85,7 @@ class Cart * * @return \Gloudemans\Shoppingcart\Cart */ - public function instance($instance = null) + public function instance($instance = null): self { $instance = $instance ?: self::DEFAULT_INSTANCE; @@ -107,34 +104,65 @@ class Cart * * @return string */ - public function currentInstance() + public function currentInstance(): string { return str_replace('cart.', '', $this->instance); } /** * Add an item to the cart. - * - * @param mixed $id - * @param mixed $name - * @param int|float $qty - * @param float $price - * @param float $weight - * @param array $options - * - * @return \Gloudemans\Shoppingcart\CartItem */ - public function add($id, ?string $name = null, $qty = null, $price = null, $weight = 0, array $options = []) + public function add(int|string|Buyable|array $id, null|string|int $nameOrQty = null, null|int|CartItemOptions $qtyOrOptions = null, ?Money $price = null, ?int $weight = null, ?CartItemOptions $options = null): CartItem|array { - if ($this->isMulti($id)) { - return array_map(function ($item) { - return $this->add($item); - }, $id); + /* Allow adding a CartItem by raw parameters */ + if (is_int($id) || is_string($id)) { + if (!is_null($nameOrQty) && !is_string($nameOrQty)) { + throw new InvalidArgumentException('$nameOrQty must be of type string (name) or null when adding with raw parameters'); + } + + if (!is_null($qtyOrOptions) && !is_int($qtyOrOptions)) { + throw new InvalidArgumentException('$nameOrQty must be of type int (quantity) or null when adding with raw parameters'); + } + + return $this->addCartItem(CartItem::fromAttributes($id, $nameOrQty, $price, $qtyOrOptions ?: 1, $weight ?: 0, $options ?: new CartItemOptions([]))); } + /* Also allow passing a Buyable instance, get data from the instance rather than parameters */ + elseif ($id instanceof Buyable) { + if (!is_null($qtyOrOptions) && !is_int($nameOrQty)) { + throw new InvalidArgumentException('$nameOrQty must be of type int (quantity) when adding a Buyable instance'); + } - $cartItem = $this->createCartItem($id, $name, $qty, $price, $weight, $options); + if (!is_null($qtyOrOptions) && !$qtyOrOptions instanceof CartItemOptions) { + throw new InvalidArgumentException('$qtyOrOptions must be of type CartItemOptions (options) or null when adding a Buyable instance'); + } - return $this->addCartItem($cartItem); + $cartItem = CartItem::fromBuyable($id, $nameOrQty ?: 1, $qtyOrOptions ?: new CartItemOptions([])); + + if ($id instanceof Model) { + $cartItem->associate($id); + } + + return $this->addCartItem($cartItem); + } + /* Also allow passing multiple definitions at the same time, simply call same method and collec return value */ + elseif (is_array($id)) { + /* Check if this iterable contains instances */ + if (is_array(head($id)) || head($id) instanceof Buyable) { + return array_map(function (Buyable|iterable $item) { + return $this->add($item); + }, $id); + } + /* Treat the array itself as an instance */ + else { + $cartItem = CartItem::fromArray($id); + + return $this->addCartItem($cartItem); + } + } + /* Due to PHP8 union types this should never happen */ + else { + throw new InvalidArgumentException('$id must be of type int|string|Buyable|Iterable'); + } } /** @@ -147,10 +175,12 @@ class Cart * * @return \Gloudemans\Shoppingcart\CartItem The CartItem */ - public function addCartItem(CartItem $item, bool $keepDiscount = false, bool $keepTax = false, bool $dispatchEvent = true) + public function addCartItem(CartItem $item, bool $keepDiscount = false, bool $keepTax = false, bool $dispatchEvent = true): CartItem { + $item->setInstance($this->currentInstance()); + if (!$keepDiscount) { - $item->setDiscountRate($this->discount); + $item->setDiscount($this->discount); } if (!$keepTax) { @@ -186,7 +216,7 @@ class Cart * * @return \Gloudemans\Shoppingcart\CartItem */ - public function update(string $rowId, $qty) + public function update(string $rowId, $qty): ?CartItem { $cartItem = $this->get($rowId); @@ -214,7 +244,7 @@ class Cart if ($cartItem->qty <= 0) { $this->remove($cartItem->rowId); - return; + return null; } else { if (isset($itemOldIndex)) { $content = $content->slice(0, $itemOldIndex) @@ -241,7 +271,7 @@ class Cart * * @return void */ - public function remove(string $rowId) + public function remove(string $rowId): void { $cartItem = $this->get($rowId); @@ -263,7 +293,7 @@ class Cart * * @return \Gloudemans\Shoppingcart\CartItem */ - public function get(string $rowId) + public function get(string $rowId): CartItem { $content = $this->getContent(); @@ -271,7 +301,11 @@ class Cart throw new InvalidRowIDException("The cart does not contain rowId {$rowId}."); } - return $content->get($rowId); + $cartItem = $content->get($rowId); + + if ($cartItem instanceof CartItem) { + return $cartItem; + } } /** @@ -279,7 +313,7 @@ class Cart * * @return void */ - public function destroy() + public function destroy(): void { $this->session->remove($this->instance); } @@ -289,7 +323,7 @@ class Cart * * @return \Illuminate\Support\Collection */ - public function content() + public function content(): Collection { if (is_null($this->session->get($this->instance))) { return new Collection([]); @@ -300,10 +334,8 @@ class Cart /** * Get the total quantity of all CartItems in the cart. - * - * @return int|float */ - public function count() + public function count(): int { return $this->getContent()->sum('qty'); } @@ -312,69 +344,27 @@ class Cart * Get the amount of CartItems in the Cart. * Keep in mind that this does NOT count quantity. */ - public function countItems() : int + public function countItems(): int { return $this->getContent()->count(); } /** - * Get the total price of the items in the cart. - */ - public function totalFloat() : float - { - return $this->getContent()->reduce(function ($total, CartItem $cartItem) { - return $total + $cartItem->total; - }, 0); - } - - /** - * Get the total price of the items in the cart as formatted string. - */ - public function total(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) : string - { - return $this->numberFormat($this->totalFloat(), $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Get the total tax of the items in the cart. - */ - public function taxFloat() : float - { - return $this->getContent()->reduce(function ($tax, CartItem $cartItem) { - return $tax + $cartItem->taxTotal; - }, 0); - } - - /** - * Get the total tax of the items in the cart as formatted string. - */ - public function tax(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) : string - { - return $this->numberFormat($this->taxFloat(), $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Get the subtotal (total - tax) of the items in the cart. - */ - public function subtotalFloat() : float - { - return $this->getContent()->reduce(function ($subTotal, CartItem $cartItem) { - return $subTotal + $cartItem->subtotal; - }, 0); - } - - /** - * Get the subtotal (total - tax) of the items in the cart as formatted string. + * Get the discount of the items in the cart. * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string + * @return Money */ - public function subtotal(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) : string + public function price(): Money { - return $this->numberFormat($this->subtotalFloat(), $decimals, $decimalPoint, $thousandSeperator); + $calculated = $this->getContent()->reduce(function (Money $discount, CartItem $cartItem) { + return $discount->add($cartItem->price()); + }, new Money(0, new Currency('USD'))); + + if ($calculated instanceof Money) { + return $calculated; + } else { + throw new \TypeError('Calculated price is not an instance of Money'); + } } /** @@ -382,79 +372,79 @@ class Cart * * @return float */ - public function discountFloat() : float + public function discount(): Money { - return $this->getContent()->reduce(function ($discount, CartItem $cartItem) { - return $discount + $cartItem->discountTotal; - }, 0); + $calculated = $this->getContent()->reduce(function (Money $discount, CartItem $cartItem) { + return $discount->add($cartItem->discount()); + }, new Money(0, new Currency('USD'))); + + if ($calculated instanceof Money) { + return $calculated; + } else { + throw new \TypeError('Calculated discount is not an instance of Money'); + } } /** - * Get the discount of the items in the cart as formatted string. + * Get the subtotal (total - tax) of the items in the cart. */ - public function discount(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) : string + public function subtotal(): Money { - return $this->numberFormat($this->discountFloat(), $decimals, $decimalPoint, $thousandSeperator); + $calculated = $this->getContent()->reduce(function (Money $subTotal, CartItem $cartItem) { + return $subTotal->add($cartItem->subtotal()); + }, new Money(0, new Currency('USD'))); + + if ($calculated instanceof Money) { + return $calculated; + } else { + throw new \TypeError('Calculated subtotal is not an instance of Money'); + } } /** - * Get the price of the items in the cart (not rounded). + * Get the total tax of the items in the cart. */ - public function initialFloat() : float + public function tax(): Money { - return $this->getContent()->reduce(function ($initial, CartItem $cartItem) { - return $initial + ($cartItem->qty * $cartItem->price); - }, 0); + $calculated = $this->getContent()->reduce(function (Money $tax, CartItem $cartItem) { + return $tax->add($cartItem->tax()); + }, new Money(0, new Currency('USD'))); + + if ($calculated instanceof Money) { + return $calculated; + } } /** - * Get the price of the items in the cart as formatted string. + * Get the total price of the items in the cart. */ - public function initial(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) : string + public function total(): Money { - return $this->numberFormat($this->initialFloat(), $decimals, $decimalPoint, $thousandSeperator); - } + $calculated = $this->getContent()->reduce(function (Money $total, CartItem $cartItem) { + return $total->add($cartItem->total()); + }, new Money(0, new Currency('USD'))); - /** - * Get the price of the items in the cart (previously rounded). - */ - public function priceTotalFloat() : float - { - return $this->getContent()->reduce(function ($initial, CartItem $cartItem) { - return $initial + $cartItem->priceTotal; - }, 0); - } - - /** - * Get the price of the items in the cart as formatted string. - */ - public function priceTotal(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) : string - { - return $this->numberFormat($this->priceTotalFloat(), $decimals, $decimalPoint, $thousandSeperator); + if ($calculated instanceof Money) { + return $calculated; + } else { + throw new \TypeError('Calculated total is not an instance of Money'); + } } /** * Get the total weight of the items in the cart. */ - public function weightFloat() : float + public function weight(): int { - return $this->getContent()->reduce(function ($total, CartItem $cartItem) { - return $total + ($cartItem->qty * $cartItem->weight); + $calculated = $this->getContent()->reduce(function (int $total, CartItem $cartItem) { + return $total + $cartItem->weight(); }, 0); - } - /** - * Get the total weight of the items in the cart. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function weight(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) : string - { - return $this->numberFormat($this->weightFloat(), $decimals, $decimalPoint, $thousandSeperator); + if (is_int($calculated)) { + return $calculated; + } else { + throw new \TypeError('Calculated weight was not an integer'); + } } /** @@ -464,7 +454,7 @@ class Cart * * @return \Illuminate\Support\Collection */ - public function search(Closure $search) : Collection + public function search(Closure $search): Collection { return $this->getContent()->filter($search); } @@ -477,7 +467,7 @@ class Cart * * @return void */ - public function associate(string $rowId, $model) + public function associate(string $rowId, $model): void { if (is_string($model) && !class_exists($model)) { throw new UnknownModelException("The supplied model {$model} does not exist."); @@ -502,7 +492,7 @@ class Cart * * @return void */ - public function setTax(string $rowId, $taxRate) + public function setTax(string $rowId, $taxRate): void { $cartItem = $this->get($rowId); @@ -521,7 +511,7 @@ class Cart * * @param float $discount */ - public function setGlobalTax($taxRate) + public function setGlobalTax($taxRate): void { $this->taxRate = $taxRate; @@ -535,17 +525,12 @@ class Cart /** * Set the discount rate for the cart item with the given rowId. - * - * @param string $rowId - * @param int|float $taxRate - * - * @return void */ - public function setDiscount(string $rowId, $discount) + public function setDiscount(string $rowId, float|Money $discount): void { $cartItem = $this->get($rowId); - $cartItem->setDiscountRate($discount); + $cartItem->setDiscount($discount); $content = $this->getContent(); @@ -562,14 +547,14 @@ class Cart * * @return void */ - public function setGlobalDiscount($discount) + public function setGlobalDiscount(float $discount): void { $this->discount = $discount; $content = $this->getContent(); if ($content && $content->count()) { - $content->each(function ($item, $key) { - $item->setDiscountRate($this->discount); + $content->each(function (CartItem $item, $key) { + $item->setDiscount($this->discount); }); } } @@ -581,7 +566,7 @@ class Cart * * @return void */ - public function store($identifier) + public function store($identifier): void { $content = $this->getContent(); @@ -595,7 +580,7 @@ class Cart throw new CartAlreadyStoredException("A cart with identifier {$identifier} was already stored."); } - $this->getConnection()->table($this->getTableName())->insert([ + $this->getConnection()->table(self::getTableName())->insert([ 'identifier' => $identifier, 'instance' => $instance, 'content' => serialize($content), @@ -613,7 +598,7 @@ class Cart * * @return void */ - public function restore($identifier) + public function restore($identifier): void { if ($identifier instanceof InstanceIdentifier) { $identifier = $identifier->getInstanceIdentifier(); @@ -625,7 +610,7 @@ class Cart return; } - $stored = $this->getConnection()->table($this->getTableName()) + $stored = $this->getConnection()->table(self::getTableName()) ->where(['identifier'=> $identifier, 'instance' => $currentInstance])->first(); $storedContent = unserialize(data_get($stored, 'content')); @@ -647,7 +632,7 @@ class Cart $this->createdAt = Carbon::parse(data_get($stored, 'created_at')); $this->updatedAt = Carbon::parse(data_get($stored, 'updated_at')); - $this->getConnection()->table($this->getTableName())->where(['identifier' => $identifier, 'instance' => $currentInstance])->delete(); + $this->getConnection()->table(self::getTableName())->where(['identifier' => $identifier, 'instance' => $currentInstance])->delete(); } /** @@ -657,7 +642,7 @@ class Cart * * @return void */ - public function erase($identifier) + public function erase($identifier): void { if ($identifier instanceof InstanceIdentifier) { $identifier = $identifier->getInstanceIdentifier(); @@ -669,7 +654,7 @@ class Cart return; } - $this->getConnection()->table($this->getTableName())->where(['identifier' => $identifier, 'instance' => $instance])->delete(); + $this->getConnection()->table(self::getTableName())->where(['identifier' => $identifier, 'instance' => $instance])->delete(); $this->events->dispatch('cart.erased'); } @@ -684,13 +669,13 @@ class Cart * * @return bool */ - public function merge($identifier, bool $keepDiscount = false, bool $keepTax = false, bool $dispatchAdd = true, $instance = self::DEFAULT_INSTANCE) + public function merge($identifier, bool $keepDiscount = false, bool $keepTax = false, bool $dispatchAdd = true, $instance = self::DEFAULT_INSTANCE): bool { if (!$this->storedCartInstanceWithIdentifierExists($instance, $identifier)) { return false; } - $stored = $this->getConnection()->table($this->getTableName()) + $stored = $this->getConnection()->table(self::getTableName()) ->where(['identifier'=> $identifier, 'instance'=> $instance])->first(); $storedContent = unserialize($stored->content); @@ -708,10 +693,8 @@ class Cart * Magic method to make accessing the total, tax and subtotal properties possible. * * @param string $attribute - * - * @return float|null */ - public function __get(string $attribute) + public function __get(string $attribute): ?Money { switch ($attribute) { case 'total': @@ -721,16 +704,14 @@ class Cart case 'subtotal': return $this->subtotal(); default: - return; + return null; } } /** * Get the carts content, if there is no cart content set yet, return a new empty Collection. - * - * @return \Illuminate\Support\Collection */ - protected function getContent() : Collection + protected function getContent(): Collection { if ($this->session->has($this->instance)) { return $this->session->get($this->instance); @@ -739,69 +720,18 @@ class Cart return new Collection(); } - /** - * Create a new CartItem from the supplied attributes. - * - * @param mixed $id - * @param mixed $name - * @param int|float $qty - * @param float $price - * @param float $weight - * @param array $options - * - * @return \Gloudemans\Shoppingcart\CartItem - */ - private function createCartItem($id, ?string $name = null, $qty, $price, $weight, array $options) : CartItem - { - if ($id instanceof Buyable) { - $cartItem = CartItem::fromBuyable($id, $qty ?: []); - $cartItem->setQuantity($name ?: 1); - $cartItem->associate($id); - } elseif (is_array($id)) { - $cartItem = CartItem::fromArray($id); - $cartItem->setQuantity($id['qty']); - } else { - $cartItem = CartItem::fromAttributes($id, $name, $price, $weight, $options); - $cartItem->setQuantity($qty); - } - - $cartItem->setInstance($this->currentInstance()); - - return $cartItem; - } - - /** - * Check if the item is a multidimensional array or an array of Buyables. - * - * @param mixed $item - * - * @return bool - */ - private function isMulti($item) - { - if (!is_array($item)) { - return false; - } - - return is_array(head($item)) || head($item) instanceof Buyable; - } - /** * @param $identifier - * - * @return bool */ - private function storedCartInstanceWithIdentifierExists($instance, $identifier) + private function storedCartInstanceWithIdentifierExists(string $instance, string $identifier): bool { - return $this->getConnection()->table($this->getTableName())->where(['identifier' => $identifier, 'instance'=> $instance])->exists(); + return $this->getConnection()->table(self::getTableName())->where(['identifier' => $identifier, 'instance'=> $instance])->exists(); } /** * Get the database connection. - * - * @return \Illuminate\Database\Connection */ - private function getConnection() + private function getConnection(): \Illuminate\Database\Connection { return app(DatabaseManager::class)->connection($this->getConnectionName()); } @@ -811,48 +741,19 @@ class Cart * * @return string */ - private function getTableName() + private static function getTableName(): string { - return config('cart.database.table', 'shoppingcart'); + return Config::get('cart.database.table', 'shoppingcart'); } /** * Get the database connection name. - * - * @return string */ - private function getConnectionName() + private function getConnectionName(): ?string { - $connection = config('cart.database.connection'); + $connection = Config::get('cart.database.connection'); - return is_null($connection) ? config('database.default') : $connection; - } - - /** - * Get the Formatted number. - * - * @param $value - * @param $decimals - * @param $decimalPoint - * @param $thousandSeperator - * - * @return string - */ - private function numberFormat($value, ?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - if (is_null($decimals)) { - $decimals = config('cart.format.decimals', 2); - } - - if (is_null($decimalPoint)) { - $decimalPoint = config('cart.format.decimal_point', '.'); - } - - if (is_null($thousandSeperator)) { - $thousandSeperator = config('cart.format.thousand_separator', ','); - } - - return number_format($value, $decimals, $decimalPoint, $thousandSeperator); + return is_null($connection) ? Config::get('database.default') : $connection; } /** @@ -860,7 +761,7 @@ class Cart * * @return \Carbon\Carbon|null */ - public function createdAt() : ?Carbon + public function createdAt(): ?Carbon { return $this->createdAt; } @@ -870,7 +771,7 @@ class Cart * * @return \Carbon\Carbon|null */ - public function updatedAt() : ?Carbon + public function updatedAt(): ?Carbon { return $this->updatedAt; } diff --git a/src/CartItem.php b/src/CartItem.php index c231cce..6bd14d8 100644 --- a/src/CartItem.php +++ b/src/CartItem.php @@ -2,70 +2,47 @@ namespace Gloudemans\Shoppingcart; -use Gloudemans\Shoppingcart\Calculation\DefaultCalculator; use Gloudemans\Shoppingcart\Contracts\Buyable; -use Gloudemans\Shoppingcart\Contracts\Calculator; -use Gloudemans\Shoppingcart\Exceptions\InvalidCalculatorException; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; -use ReflectionClass; +use Illuminate\Support\Facades\Config; +use Money\Currencies\ISOCurrencies; +use Money\Formatter\DecimalMoneyFormatter; +use Money\Money; -/** - * @property-read mixed discount - * @property-read float discountTotal - * @property-read float priceTarget - * @property-read float priceNet - * @property-read float priceTotal - * @property-read float subtotal - * @property-read float taxTotal - * @property-read float tax - * @property-read float total - * @property-read float priceTax - */ class CartItem implements Arrayable, Jsonable { /** * The rowID of the cart item. - * - * @var string */ - public $rowId; + public string $rowId; /** * The ID of the cart item. - * - * @var int|string */ - public $id; + public int|string $id; /** * The quantity for this cart item. - * - * @var int|float */ - public $qty; + public int $qty; /** * The name of the cart item. - * - * @var string */ public string $name; /** * The price without TAX of the cart item. - * - * @var float */ - public $price; + public Money $price; /** * The weight of the product. - * - * @var float */ - public $weight; + public int $weight; /** * The options for this cart item. @@ -74,240 +51,47 @@ class CartItem implements Arrayable, Jsonable /** * The tax rate for the cart item. - * - * @var int|float */ - public $taxRate = 0; + public float $taxRate = 0; /** * The FQN of the associated model. - * - * @var string|null */ - private $associatedModel = null; + public ?string $associatedModel = null; /** * The discount rate for the cart item. - * - * @var float */ - private $discountRate = 0; + public float|Money $discount = 0; /** * The cart instance of the cart item. */ public ?string $instance = null; - /** - * CartItem constructor. - * - * @param int|string $id - * @param string $name - * @param float $price - * @param float $weight - * @param array $options - */ - public function __construct($id, string $name, $price, $weight = 0, array $options = []) + public function __construct(int|string $id, string $name, Money $price, int $qty = 1, int $weight = 0, ?CartItemOptions $options = null) { - if (empty($id)) { - throw new \InvalidArgumentException('Please supply a valid identifier.'); - } - if (empty($name)) { - throw new \InvalidArgumentException('Please supply a valid name.'); - } - if (strlen($price) < 0 || !is_numeric($price)) { - throw new \InvalidArgumentException('Please supply a valid price.'); - } - if (strlen($weight) < 0 || !is_numeric($weight)) { - throw new \InvalidArgumentException('Please supply a valid weight.'); - } - $this->id = $id; $this->name = $name; - $this->price = floatval($price); - $this->weight = floatval($weight); - $this->options = new CartItemOptions($options); - $this->rowId = $this->generateRowId($id, $options); - } - - /** - * Returns the formatted weight. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function weight(int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->weight, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted price without TAX. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function price(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->price, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted price with discount applied. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function priceTarget(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->priceTarget, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted price with TAX. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function priceTax(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->priceTax, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted subtotal. - * Subtotal is price for whole CartItem without TAX. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function subtotal(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->subtotal, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted total. - * Total is price for whole CartItem with TAX. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function total(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->total, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted tax. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function tax(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->tax, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted tax. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function taxTotal(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->taxTotal, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted discount. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function discount(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->discount, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted total discount for this cart item. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function discountTotal(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->discountTotal, $decimals, $decimalPoint, $thousandSeperator); - } - - /** - * Returns the formatted total price for this cart item. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string - */ - public function priceTotal(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null) - { - return $this->numberFormat($this->priceTotal, $decimals, $decimalPoint, $thousandSeperator); + $this->price = $price; + $this->qty = $qty; + $this->weight = $weight; + $this->options = $options ?: new CartItemOptions([]); + $this->rowId = $this->generateRowId($id, $options->toArray()); } /** * Set the quantity for this cart item. - * - * @param int|float $qty */ - public function setQuantity($qty) + public function setQuantity(int $qty) { - if (empty($qty) || !is_numeric($qty)) { - throw new \InvalidArgumentException('Please supply a valid quantity.'); - } - $this->qty = $qty; } /** * Update the cart item from a Buyable. - * - * @param \Gloudemans\Shoppingcart\Contracts\Buyable $item - * - * @return void */ - public function updateFromBuyable(Buyable $item) + public function updateFromBuyable(Buyable $item): void { $this->id = $item->getBuyableIdentifier($this->options); $this->name = $item->getBuyableDescription($this->options); @@ -316,12 +100,8 @@ class CartItem implements Arrayable, Jsonable /** * Update the cart item from an array. - * - * @param array $attributes - * - * @return void */ - public function updateFromArray(array $attributes) + public function updateFromArray(array $attributes): void { $this->id = Arr::get($attributes, 'id', $this->id); $this->qty = Arr::get($attributes, 'qty', $this->qty); @@ -337,10 +117,8 @@ class CartItem implements Arrayable, Jsonable * Associate the cart item with the given model. * * @param mixed $model - * - * @return \Gloudemans\Shoppingcart\CartItem */ - public function associate($model) + public function associate(string|Model $model): self { $this->associatedModel = is_string($model) ? $model : get_class($model); @@ -349,12 +127,8 @@ class CartItem implements Arrayable, Jsonable /** * Set the tax rate. - * - * @param int|float $taxRate - * - * @return \Gloudemans\Shoppingcart\CartItem */ - public function setTaxRate($taxRate) + public function setTaxRate(float $taxRate): self { $this->taxRate = $taxRate; @@ -363,124 +137,119 @@ class CartItem implements Arrayable, Jsonable /** * Set the discount rate. - * - * @param int|float $discountRate - * - * @return \Gloudemans\Shoppingcart\CartItem */ - public function setDiscountRate($discountRate) + public function setDiscount(float|Money $discount): self { - $this->discountRate = $discountRate; + $this->discount = $discount; return $this; } /** * Set cart instance. - * - * @param null|string $instance - * - * @return \Gloudemans\Shoppingcart\CartItem */ - public function setInstance(?string $instance) + public function setInstance(?string $instance): self { $this->instance = $instance; return $this; } - /** - * Get an attribute from the cart item or get the associated model. - * - * @param string $attribute - * - * @return mixed - */ - public function __get($attribute) + public function model(): ?Model { - if (property_exists($this, $attribute)) { - return $this->{$attribute}; - } - $decimals = config('cart.format.decimals', 2); - - switch ($attribute) { - case 'model': - if (isset($this->associatedModel)) { - return with(new $this->associatedModel())->find($this->id); - } - // no break - case 'modelFQCN': - if (isset($this->associatedModel)) { - return $this->associatedModel; - } - // no break - case 'weightTotal': - return round($this->weight * $this->qty, $decimals); + if (isset($this->associatedModel)) { + return (new $this->associatedModel())->find($this->id); } - $class = new ReflectionClass(config('cart.calculator', DefaultCalculator::class)); - if (!$class->implementsInterface(Calculator::class)) { - throw new InvalidCalculatorException('The configured Calculator seems to be invalid. Calculators have to implement the Calculator Contract.'); - } + return null; + } - return call_user_func($class->getName().'::getAttribute', $attribute, $this); + /** + * This will is the price of the CartItem considering the set quantity. If you need the single + * price just set the parameter to true. + */ + public function price(): Money + { + return $this->price->multiply($this->qty); + } + + /** + * This is the discount granted for this CartItem. It is based on the given price and, in case + * discount is a float, multiplied or, in case it is an absolute Money, subtracted. It will return + * a minimum value of 0. + */ + public function discount(): Money + { + if ($this->discount instanceof Money) { + return $this->price()->subtract($this->discount); + } else { + return $this->price()->multiply(sprintf('%.14F', $this->discount), Config::get('cart.rounding', Money::ROUND_UP)); + } + } + + /** + * This is the final price of the CartItem but without any tax applied. This does on the + * other hand include any discounts. + */ + public function subtotal(): Money + { + return Money::max(new Money(0, $this->price->getCurrency()), $this->price()->subtract($this->discount())); + } + + /** + * This is the tax, based on the subtotal (all previous calculations) and set tax rate. + */ + public function tax(): Money + { + return $this->subtotal()->multiply(sprintf('%.14F', $this->taxRate), Config::get('cart.rounding', Money::ROUND_UP)); + } + + /** + * This is the total price, consisting of the subtotal and tax applied. + */ + public function total(): Money + { + return $this->subtotal()->add($this->tax()); + } + + /** + * This is the total price, consisting of the subtotal and tax applied. + */ + public function weight(): int + { + return $this->qty * $this->weight; } /** * Create a new instance from a Buyable. - * - * @param \Gloudemans\Shoppingcart\Contracts\Buyable $item - * @param array $options - * - * @return \Gloudemans\Shoppingcart\CartItem */ - public static function fromBuyable(Buyable $item, array $options = []) + public static function fromBuyable(Buyable $item, int $qty = 1, ?CartItemOptions $options = null): self { - return new self($item->getBuyableIdentifier($options), $item->getBuyableDescription($options), $item->getBuyablePrice($options), $item->getBuyableWeight($options), $options); + $options = $options ?: new CartItemOptions([]); + + return new self($item->getBuyableIdentifier($options), $item->getBuyableDescription($options), $item->getBuyablePrice($options), $qty, $item->getBuyableWeight($options), $options); } /** * Create a new instance from the given array. - * - * @param array $attributes - * - * @return \Gloudemans\Shoppingcart\CartItem */ - public static function fromArray(array $attributes) + public static function fromArray(array $attributes): self { - $options = Arr::get($attributes, 'options', []); + $options = new CartItemOptions(Arr::get($attributes, 'options', [])); - return new self($attributes['id'], $attributes['name'], $attributes['price'], $attributes['weight'], $options); + return new self($attributes['id'], $attributes['name'], $attributes['price'], $attributes['qty'], $attributes['weight'], $options); } /** * Create a new instance from the given attributes. * * @param int|string $id - * @param string $name - * @param float $price - * @param array $options - * - * @return \Gloudemans\Shoppingcart\CartItem */ - public static function fromAttributes($id, string $name, $price, $weight, array $options = []) + public static function fromAttributes(int|string $id, string $name, Money $price, int $qty = 1, int $weight = 0, ?CartItemOptions $options = null): self { - return new self($id, $name, $price, $weight, $options); - } + $options = $options ?: new CartItemOptions([]); - /** - * Generate a unique id for the cart item. - * - * @param string $id - * @param array $options - * - * @return string - */ - protected function generateRowId($id, array $options) - { - ksort($options); - - return md5($id.serialize($options)); + return new self($id, $name, $price, $qty, $weight, $options); } /** @@ -494,15 +263,16 @@ class CartItem implements Arrayable, Jsonable 'rowId' => $this->rowId, 'id' => $this->id, 'name' => $this->name, + 'price' => self::formatMoney($this->price), 'qty' => $this->qty, - 'price' => $this->price, 'weight' => $this->weight, - 'options' => is_object($this->options) - ? $this->options->toArray() - : $this->options, - 'discount' => $this->discount, - 'tax' => $this->tax, - 'subtotal' => $this->subtotal, + 'options' => $this->options->toArray(), + + /* Calculated attributes */ + 'discount' => self::formatMoney($this->discount()), + 'subtotal' => self::formatMoney($this->subtotal()), + 'tax' => self::formatMoney($this->tax()), + 'total' => self::formatMoney($this->total()), ]; } @@ -519,40 +289,20 @@ class CartItem implements Arrayable, Jsonable } /** - * Get the formatted number. - * - * @param float $value - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - * - * @return string + * Generate a unique id for the cart item. */ - private function numberFormat($value, ?int $decimals, ?string $decimalPoint, ?string $thousandSeperator) + private static function formatMoney(Money $money): string { - if (is_null($decimals)) { - $decimals = config('cart.format.decimals', 2); - } - - if (is_null($decimalPoint)) { - $decimalPoint = config('cart.format.decimal_point', '.'); - } - - if (is_null($thousandSeperator)) { - $thousandSeperator = config('cart.format.thousand_separator', ','); - } - - return number_format($value, $decimals, $decimalPoint, $thousandSeperator); + return (new DecimalMoneyFormatter(new ISOCurrencies()))->format($money); } /** - * Getter for the raw internal discount rate. - * Should be used in calculators. - * - * @return float + * Generate a unique id for the cart item. */ - public function getDiscountRate() + protected function generateRowId(string $id, array $options): string { - return $this->discountRate; + ksort($options); + + return md5($id.serialize($options)); } } diff --git a/src/Config/cart.php b/src/Config/cart.php index 42f667a..eb072dd 100644 --- a/src/Config/cart.php +++ b/src/Config/cart.php @@ -1,10 +1,12 @@ \Gloudemans\Shoppingcart\Calculation\DefaultCalculator::class, + 'rounding' => Money::ROUND_UP, /* |-------------------------------------------------------------------------- @@ -24,7 +26,7 @@ return [ | */ - 'tax' => 21, + 'tax' => 0.21, /* |-------------------------------------------------------------------------- @@ -44,18 +46,6 @@ return [ ], - /* - |-------------------------------------------------------------------------- - | Destroy the cart on user logout - |-------------------------------------------------------------------------- - | - | When this option is set to 'true' the cart will automatically - | destroy all cart instances when the user logs out. - | - */ - - 'destroy_on_logout' => false, - /* |-------------------------------------------------------------------------- | Default number format diff --git a/src/Contracts/Buyable.php b/src/Contracts/Buyable.php index 4666b36..675e378 100644 --- a/src/Contracts/Buyable.php +++ b/src/Contracts/Buyable.php @@ -2,6 +2,9 @@ namespace Gloudemans\Shoppingcart\Contracts; +use Gloudemans\Shoppingcart\CartItemOptions; +use Money\Money; + interface Buyable { /** @@ -9,24 +12,20 @@ interface Buyable * * @return int|string */ - public function getBuyableIdentifier(); + public function getBuyableIdentifier(CartItemOptions $options); /** * Get the description or title of the Buyable item. */ - public function getBuyableDescription() : ?string; + public function getBuyableDescription(CartItemOptions $options): ?string; /** * Get the price of the Buyable item. - * - * @return float */ - public function getBuyablePrice(); + public function getBuyablePrice(CartItemOptions $options): Money; /** * Get the weight of the Buyable item. - * - * @return float */ - public function getBuyableWeight(); + public function getBuyableWeight(CartItemOptions $options): int; } diff --git a/src/Contracts/Calculator.php b/src/Contracts/Calculator.php deleted file mode 100644 index 137b1e2..0000000 --- a/src/Contracts/Calculator.php +++ /dev/null @@ -1,10 +0,0 @@ -string('identifier'); $table->string('instance'); $table->longText('content'); @@ -26,6 +26,6 @@ class CreateShoppingcartTable extends Migration */ public function down() { - Schema::drop(config('cart.database.table')); + Schema::drop(Config::get('cart.database.table')); } } diff --git a/src/Exceptions/InvalidCalculatorException.php b/src/Exceptions/InvalidCalculatorException.php deleted file mode 100644 index abc5370..0000000 --- a/src/Exceptions/InvalidCalculatorException.php +++ /dev/null @@ -1,9 +0,0 @@ -app->bind('cart', 'Gloudemans\Shoppingcart\Cart'); + /* Determine where the config file is located */ $config = __DIR__.'/Config/cart.php'; + + /* Use local config */ $this->mergeConfigFrom($config, 'cart'); - $this->publishes([__DIR__.'/Config/cart.php' => config_path('cart.php')], 'config'); - - $this->app['events']->listen(Logout::class, function () { - if ($this->app['config']->get('cart.destroy_on_logout')) { - $this->app->make(SessionManager::class)->forget('cart'); - } - }); + /* Also allow publishing to overwrite local config */ + $this->publishes([$config => config_path('cart.php')], 'config'); + /* Publish included migrations */ $this->publishes([ realpath(__DIR__.'/Database/migrations') => $this->app->databasePath().'/migrations', ], 'migrations'); diff --git a/tests/CartItemTest.php b/tests/CartItemTest.php index 04ddf19..885d1d3 100644 --- a/tests/CartItemTest.php +++ b/tests/CartItemTest.php @@ -3,7 +3,10 @@ namespace Gloudemans\Tests\Shoppingcart; use Gloudemans\Shoppingcart\CartItem; +use Gloudemans\Shoppingcart\CartItemOptions; use Gloudemans\Shoppingcart\ShoppingcartServiceProvider; +use Money\Currency; +use Money\Money; use Orchestra\Testbench\TestCase; class CartItemTest extends TestCase @@ -23,45 +26,35 @@ class CartItemTest extends TestCase /** @test */ public function it_can_be_cast_to_an_array() { - $cartItem = new CartItem(1, 'Some item', 10.00, 550, ['size' => 'XL', 'color' => 'red']); - $cartItem->setQuantity(2); + $cartItem = new CartItem(1, 'Some item', new Money(1000, new Currency('USD')), 2, 550, new CartItemOptions(['size' => 'XL', 'color' => 'red'])); $this->assertEquals([ 'id' => 1, 'name' => 'Some item', - 'price' => 10.00, + 'price' => '10.00', 'rowId' => '07d5da5550494c62daf9993cf954303f', 'qty' => 2, 'options' => [ 'size' => 'XL', 'color' => 'red', ], - 'tax' => 0, - 'subtotal' => 20.00, - 'discount' => 0.0, - 'weight' => 550.0, + 'tax' => '0.00', + 'subtotal' => '20.00', + 'total' => '20.00', + 'discount' => '0.00', + 'weight' => 550, ], $cartItem->toArray()); } /** @test */ public function it_can_be_cast_to_json() { - $cartItem = new CartItem(1, 'Some item', 10.00, 550, ['size' => 'XL', 'color' => 'red']); - $cartItem->setQuantity(2); + $cartItem = new CartItem(1, 'Some item', new Money(1000, new Currency('USD')), 2, 550, new CartItemOptions(['size' => 'XL', 'color' => 'red'])); $this->assertJson($cartItem->toJson()); - $json = '{"rowId":"07d5da5550494c62daf9993cf954303f","id":1,"name":"Some item","qty":2,"price":10,"weight":550,"options":{"size":"XL","color":"red"},"discount":0,"tax":0,"subtotal":20}'; + $json = '{"rowId":"07d5da5550494c62daf9993cf954303f","id":1,"name":"Some item","price":"10.00","qty":2,"weight":550,"options":{"size":"XL","color":"red"},"discount":"0.00","subtotal":"20.00","tax":"0.00","total":"20.00"}'; $this->assertEquals($json, $cartItem->toJson()); } - - /** @test */ - public function it_formats_price_total_correctly() - { - $cartItem = new CartItem(1, 'Some item', 10.00, 550, ['size' => 'XL', 'color' => 'red']); - $cartItem->setQuantity(2); - - $this->assertSame('20.00', $cartItem->priceTotal()); - } } diff --git a/tests/CartTest.php b/tests/CartTest.php index 9887270..baa8f10 100644 --- a/tests/CartTest.php +++ b/tests/CartTest.php @@ -3,19 +3,18 @@ namespace Gloudemans\Tests\Shoppingcart; use Carbon\Carbon; -use Gloudemans\Shoppingcart\Calculation\GrossPrice; use Gloudemans\Shoppingcart\Cart; use Gloudemans\Shoppingcart\CartItem; +use Gloudemans\Shoppingcart\CartItemOptions; use Gloudemans\Shoppingcart\ShoppingcartServiceProvider; use Gloudemans\Tests\Shoppingcart\Fixtures\BuyableProduct; use Gloudemans\Tests\Shoppingcart\Fixtures\BuyableProductTrait; use Gloudemans\Tests\Shoppingcart\Fixtures\Identifiable; use Gloudemans\Tests\Shoppingcart\Fixtures\ProductModel; -use Illuminate\Contracts\Auth\Authenticatable; -use Illuminate\Session\SessionManager; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Event; -use Mockery; +use Money\Currency; +use Money\Money; use Orchestra\Testbench\TestCase; class CartTest extends TestCase @@ -31,7 +30,9 @@ class CartTest extends TestCase */ protected function getPackageProviders($app) { - return [ShoppingcartServiceProvider::class]; + return [ + ShoppingcartServiceProvider::class, + ]; } /** @@ -158,7 +159,10 @@ class CartTest extends TestCase $this->assertTrue(is_array($cartItems)); $this->assertCount(2, $cartItems); - $this->assertContainsOnlyInstancesOf(CartItem::class, $cartItems); + $this->assertIsIterable($cartItems); + if (is_iterable($cartItems)) { + $this->assertContainsOnlyInstancesOf(CartItem::class, $cartItems); + } Event::assertDispatched('cart.added'); } @@ -170,7 +174,7 @@ class CartTest extends TestCase $cart = $this->getCart(); - $cart->add(1, 'Test item', 1, 10.00); + $cart->add(1, 'Test item', 1, new Money(1000, new Currency('USD'))); $this->assertEquals(1, $cart->count()); @@ -184,7 +188,7 @@ class CartTest extends TestCase $cart = $this->getCart(); - $cart->add(['id' => 1, 'name' => 'Test item', 'qty' => 1, 'price' => 10.00, 'weight' => 550]); + $cart->add(['id' => 1, 'name' => 'Test item', 'qty' => 1, 'price' => new Money(1000, new Currency('USD')), 'weight' => 550]); $this->assertEquals(1, $cart->count()); @@ -199,8 +203,8 @@ class CartTest extends TestCase $cart = $this->getCart(); $cart->add([ - ['id' => 1, 'name' => 'Test item 1', 'qty' => 1, 'price' => 10.00, 'weight' => 550], - ['id' => 2, 'name' => 'Test item 2', 'qty' => 1, 'price' => 10.00, 'weight' => 550], + ['id' => 1, 'name' => 'Test item 1', 'qty' => 1, 'price' => new Money(1000, new Currency('USD')), 'weight' => 550], + ['id' => 2, 'name' => 'Test item 2', 'qty' => 1, 'price' => new Money(1000, new Currency('USD')), 'weight' => 550], ]); $this->assertEquals(2, $cart->count()); @@ -215,9 +219,7 @@ class CartTest extends TestCase $cart = $this->getCart(); - $options = ['size' => 'XL', 'color' => 'red']; - - $cart->add(new BuyableProduct(), 1, $options); + $cart->add(new BuyableProduct(), 1, new CartItemOptions(['size' => 'XL', 'color' => 'red'])); $cartItem = $cart->get('07d5da5550494c62daf9993cf954303f'); @@ -233,12 +235,11 @@ class CartTest extends TestCase */ public function it_will_validate_the_identifier() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Please supply a valid identifier.'); + $this->expectException(\TypeError::class); $cart = $this->getCart(); - $cart->add(null, 'Some title', 1, 10.00); + $cart->add(null, 'Some title', 1, new Money(1000, new Currency('USD'))); } /** @@ -246,12 +247,11 @@ class CartTest extends TestCase */ public function it_will_validate_the_quantity() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Please supply a valid quantity.'); + $this->expectException(\TypeError::class); $cart = $this->getCart(); - $cart->add(1, 'Some title', 'invalid', 10.00); + $cart->add(1, 'Some title', 'invalid', new Money(1000, new Currency('USD'))); } /** @@ -259,8 +259,7 @@ class CartTest extends TestCase */ public function it_will_validate_the_price() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Please supply a valid price.'); + $this->expectException(\TypeError::class); $cart = $this->getCart(); @@ -272,12 +271,11 @@ class CartTest extends TestCase */ public function it_will_validate_the_weight() { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Please supply a valid weight'); + $this->expectException(\TypeError::class); $cart = $this->getCart(); - $cart->add(1, 'Some title', 1, 10.00, 'invalid'); + $cart->add(1, 'Some title', 1, new Money(1000, new Currency('USD')), 'invalid'); } /** @test */ @@ -388,9 +386,9 @@ class CartTest extends TestCase { $cart = $this->getCart(); - $cart->add(new BuyableProduct(), 1, ['color' => 'red']); + $cart->add(new BuyableProduct(), 1, new CartItemOptions(['color' => 'red'])); - $cart->update('ea65e0bdcd1967c4b3149e9e780177c0', ['options' => ['color' => 'blue']]); + $cart->update('ea65e0bdcd1967c4b3149e9e780177c0', ['options' => new CartItemOptions(['color' => 'blue'])]); $this->assertItemsInCart(1, $cart); $this->assertEquals('7e70a1e9aaadd18c72921a07aae5d011', $cart->content()->first()->rowId); @@ -402,10 +400,10 @@ class CartTest extends TestCase { $cart = $this->getCart(); - $cart->add(new BuyableProduct(), 1, ['color' => 'red']); - $cart->add(new BuyableProduct(), 1, ['color' => 'blue']); + $cart->add(new BuyableProduct(), 1, new CartItemOptions(['color' => 'red'])); + $cart->add(new BuyableProduct(), 1, new CartItemOptions(['color' => 'blue'])); - $cart->update('7e70a1e9aaadd18c72921a07aae5d011', ['options' => ['color' => 'red']]); + $cart->update('7e70a1e9aaadd18c72921a07aae5d011', ['options' => new CartItemOptions(['color' => 'red'])]); $this->assertItemsInCart(2, $cart); $this->assertRowsInCart(1, $cart); @@ -416,11 +414,11 @@ class CartTest extends TestCase { $cart = $this->getCart(); - $cart->add(new BuyableProduct(), 1, ['color' => 'red']); - $cart->add(new BuyableProduct(), 1, ['color' => 'green']); - $cart->add(new BuyableProduct(), 1, ['color' => 'blue']); + $cart->add(new BuyableProduct(), 1, new CartItemOptions(['color' => 'red'])); + $cart->add(new BuyableProduct(), 1, new CartItemOptions(['color' => 'green'])); + $cart->add(new BuyableProduct(), 1, new CartItemOptions(['color' => 'blue'])); - $cart->update($cart->content()->values()[1]->rowId, ['options' => ['color' => 'yellow']]); + $cart->update($cart->content()->values()[1]->rowId, ['options' => new CartItemOptions(['color' => 'yellow'])]); $this->assertRowsInCart(3, $cart); $this->assertEquals('yellow', $cart->content()->values()[1]->options->color); @@ -526,6 +524,13 @@ class CartTest extends TestCase 'id' => 2, ])); + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertEquals(new Money(1000, $cartItem->price->getCurrency()), $cartItem->price()); + $this->assertEquals(new Money(1000, $cartItem->price->getCurrency()), $cartItem->subtotal()); + $this->assertEquals(new Money(210, $cartItem->price->getCurrency()), $cartItem->tax()); + $this->assertEquals(new Money(1210, $cartItem->price->getCurrency()), $cartItem->total()); + $content = $cart->content(); $this->assertInstanceOf(Collection::class, $content); @@ -535,24 +540,27 @@ class CartTest extends TestCase 'id' => 1, 'name' => 'Item name', 'qty' => 1, - 'price' => 10.00, - 'tax' => 2.10, - 'subtotal' => 10.0, + 'price' => '10.00', + 'subtotal' => '10.00', + 'tax' => '2.10', + 'total' => '12.10', 'options' => [], - 'discount' => 0.0, - 'weight' => 0.0, + 'discount' => '0.00', + 'weight' => 0, + ], '370d08585360f5c568b18d1f2e4ca1df' => [ 'rowId' => '370d08585360f5c568b18d1f2e4ca1df', 'id' => 2, 'name' => 'Item name', 'qty' => 1, - 'price' => 10.00, - 'tax' => 2.10, - 'subtotal' => 10.0, + 'price' => '10.00', + 'subtotal' => '10.00', + 'tax' => '2.10', + 'total' => '12.10', 'options' => [], - 'discount' => 0.0, - 'weight' => 0.0, + 'discount' => '0.00', + 'weight' => 0, ], ], $content->toArray()); } @@ -582,30 +590,11 @@ class CartTest extends TestCase $cart->add(new BuyableProduct([ 'id' => 2, 'name' => 'Second item', - 'price' => 25.00, + 'price' => 2500, ]), 2); $this->assertItemsInCart(3, $cart); - $this->assertEquals(60.00, $cart->subtotal()); - } - - /** @test */ - public function it_can_return_a_formatted_total() - { - $cart = $this->getCart(); - - $cart->add(new BuyableProduct([ - 'name' => 'First item', - 'price' => 1000.00, - ])); - $cart->add(new BuyableProduct([ - 'id' => 2, - 'name' => 'Second item', - 'price' => 2500.00, - ]), 2); - - $this->assertItemsInCart(3, $cart); - $this->assertEquals('6.000,00', $cart->subtotal(2, ',', '.')); + $this->assertEquals(new Money(6000, new Currency('USD')), $cart->subtotal()); } /** @test */ @@ -662,11 +651,11 @@ class CartTest extends TestCase $cart->add(new BuyableProduct([ 'name' => 'Some item', - ]), 1, ['color' => 'red']); + ]), 1, new CartItemOptions(['color' => 'red'])); $cart->add(new BuyableProduct([ 'id' => 2, 'name' => 'Another item', - ]), 1, ['color' => 'blue']); + ]), 1, new CartItemOptions(['color' => 'blue'])); $cartItem = $cart->search(function ($cartItem, $rowId) { return $cartItem->options->color == 'red'; @@ -687,7 +676,7 @@ class CartTest extends TestCase $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(BuyableProduct::class, $cartItem->modelFQCN); + $this->assertEquals(BuyableProduct::class, $cartItem->associatedModel); } /** @test */ @@ -695,13 +684,13 @@ class CartTest extends TestCase { $cart = $this->getCart(); - $cart->add(1, 'Test item', 1, 10.00); + $cart->add(1, 'Test item', 1, new Money(1000, new Currency('USD'))); $cart->associate('027c91341fd5cf4d2579b49c4b6a90da', new ProductModel()); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(ProductModel::class, $cartItem->modelFQCN); + $this->assertEquals(ProductModel::class, $cartItem->associatedModel); } /** @@ -714,7 +703,7 @@ class CartTest extends TestCase $cart = $this->getCart(); - $cart->add(1, 'Test item', 1, 10.00); + $cart->add(1, 'Test item', 1, new Money(1000, new Currency('USD'))); $cart->associate('027c91341fd5cf4d2579b49c4b6a90da', 'SomeModel'); } @@ -724,14 +713,14 @@ class CartTest extends TestCase { $cart = $this->getCart(); - $cart->add(1, 'Test item', 1, 10.00); + $cart->add(1, 'Test item', 1, new Money(1000, new Currency('USD'))); $cart->associate('027c91341fd5cf4d2579b49c4b6a90da', new ProductModel()); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertInstanceOf(ProductModel::class, $cartItem->model); - $this->assertEquals('Some value', $cartItem->model->someValue); + $this->assertInstanceOf(ProductModel::class, $cartItem->model()); + $this->assertEquals('Some value', $cartItem->model()->someValue); } /** @test */ @@ -741,27 +730,12 @@ class CartTest extends TestCase $cart->add(new BuyableProduct([ 'name' => 'Some title', - 'price' => 9.99, + 'price' => 999, ]), 3); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(29.97, $cartItem->subtotal); - } - - /** @test */ - public function it_can_return_a_formatted_subtotal() - { - $cart = $this->getCart(); - - $cart->add(new BuyableProduct([ - 'name' => 'Some title', - 'price' => 500, - ]), 3); - - $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - - $this->assertEquals('1.500,00', $cartItem->subtotal(2, ',', '.')); + $this->assertEquals(new Money(2997, new Currency('USD')), $cartItem->subtotal()); } /** @test */ @@ -775,7 +749,7 @@ class CartTest extends TestCase $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(2.10, $cartItem->tax); + $this->assertEquals(new Money(210, new Currency('USD')), $cartItem->tax()); } /** @test */ @@ -787,26 +761,11 @@ class CartTest extends TestCase 'name' => 'Some title', ]), 1); - $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); + $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 0.19); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(1.90, $cartItem->tax); - } - - /** @test */ - public function it_can_return_the_calculated_tax_formatted() - { - $cart = $this->getCart(); - - $cart->add(new BuyableProduct([ - 'name' => 'Some title', - 'price' => 10000.00, - ]), 1); - - $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - - $this->assertEquals('2.100,00', $cartItem->tax(2, ',', '.')); + $this->assertEquals(new Money(190, new Currency('USD')), $cartItem->tax()); } /** @test */ @@ -820,28 +779,10 @@ class CartTest extends TestCase $cart->add(new BuyableProduct([ 'id' => 2, 'name' => 'Some title', - 'price' => 20.00, + 'price' => 2000, ]), 2); - $this->assertEquals(10.50, $cart->tax); - } - - /** @test */ - public function it_can_return_formatted_total_tax() - { - $cart = $this->getCart(); - - $cart->add(new BuyableProduct([ - 'name' => 'Some title', - 'price' => 1000.00, - ]), 1); - $cart->add(new BuyableProduct([ - 'id' => 2, - 'name' => 'Some title', - 'price' => 2000.00, - ]), 2); - - $this->assertEquals('1.050,00', $cart->tax(2, ',', '.')); + $this->assertEquals(new Money(1050, new Currency('USD')), $cart->tax()); } /** @test */ @@ -853,11 +794,11 @@ class CartTest extends TestCase 'name' => 'Some title', ]), 1); - $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); + $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 0.19); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(19, $cartItem->taxRate); + $this->assertEquals(0.19, $cartItem->taxRate); } /** @test */ @@ -868,74 +809,10 @@ class CartTest extends TestCase $cart->add(new BuyableProduct(), 1); $cart->add(new BuyableProduct([ 'id' => 2, - 'price' => 20.00, + 'price' => 2000, ]), 2); - $this->assertEquals(50.00, $cart->subtotal); - } - - /** @test */ - public function it_can_return_formatted_subtotal() - { - $cart = $this->getCart(); - - $cart->add(new BuyableProduct([ - 'price' => 1000.00, - ]), 1); - $cart->add(new BuyableProduct([ - 'id' => 2, - 'price' => 2000.00, - ]), 2); - - $this->assertEquals('5000,00', $cart->subtotal(2, ',', '')); - } - - /** @test */ - public function it_can_return_cart_formated_numbers_by_config_values() - { - $this->setConfigFormat(2, ',', ''); - - $cart = $this->getCart(); - - $cart->add(new BuyableProduct([ - 'price' => 1000.00, - ]), 1); - $cart->add(new BuyableProduct([ - 'id' => 2, - 'price' => 2000.00, - ]), 2); - - $this->assertEquals('5000,00', $cart->subtotal()); - $this->assertEquals('1050,00', $cart->tax()); - $this->assertEquals('6050,00', $cart->total()); - - $this->assertEquals('5000,00', $cart->subtotal); - $this->assertEquals('1050,00', $cart->tax); - $this->assertEquals('6050,00', $cart->total); - } - - /** @test */ - public function it_can_return_cartItem_formated_numbers_by_config_values() - { - $this->setConfigFormat(2, ',', ''); - - $cart = $this->getCartDiscount(50); - - $cart->add(new BuyableProduct([ - 'price' => 2000.00, - ]), 2); - - $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - - $this->assertEquals('2000,00', $cartItem->price()); - $this->assertEquals('1000,00', $cartItem->discount()); - $this->assertEquals('2000,00', $cartItem->discountTotal()); - $this->assertEquals('1000,00', $cartItem->priceTarget()); - $this->assertEquals('2000,00', $cartItem->subtotal()); - $this->assertEquals('210,00', $cartItem->tax()); - $this->assertEquals('420,00', $cartItem->taxTotal()); - $this->assertEquals('1210,00', $cartItem->priceTax()); - $this->assertEquals('2420,00', $cartItem->total()); + $this->assertEquals(new Money(5000, new Currency('USD')), $cart->subtotal()); } /** @test */ @@ -1074,7 +951,7 @@ class CartTest extends TestCase /** @test */ public function it_can_calculate_all_values() { - $cart = $this->getCartDiscount(50); + $cart = $this->getCartDiscount(0.5); $cart->add(new BuyableProduct([ 'name' => 'First item', @@ -1082,23 +959,19 @@ class CartTest extends TestCase $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); + $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 0.19); - $this->assertEquals(10.00, $cartItem->price(2)); - $this->assertEquals(5.00, $cartItem->discount(2)); - $this->assertEquals(10.00, $cartItem->discountTotal(2)); - $this->assertEquals(5.00, $cartItem->priceTarget(2)); - $this->assertEquals(10.00, $cartItem->subtotal(2)); - $this->assertEquals(0.95, $cartItem->tax(2)); - $this->assertEquals(1.90, $cartItem->taxTotal(2)); - $this->assertEquals(5.95, $cartItem->priceTax(2)); - $this->assertEquals(11.90, $cartItem->total(2)); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->price); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->discount()); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->subtotal()); + $this->assertEquals(new Money(190, new Currency('USD')), $cartItem->tax()); + $this->assertEquals(new Money(1190, new Currency('USD')), $cartItem->total()); } /** @test */ public function it_can_calculate_all_values_after_updating_from_array() { - $cart = $this->getCartDiscount(50); + $cart = $this->getCartDiscount(0.5); $cart->add(new BuyableProduct([ 'name' => 'First item', ]), 1); @@ -1107,26 +980,22 @@ class CartTest extends TestCase $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); + $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 0.19); - $this->assertEquals(10.00, $cartItem->price(2)); - $this->assertEquals(5.00, $cartItem->discount(2)); - $this->assertEquals(10.00, $cartItem->discountTotal(2)); - $this->assertEquals(5.00, $cartItem->priceTarget(2)); - $this->assertEquals(10.00, $cartItem->subtotal(2)); - $this->assertEquals(0.95, $cartItem->tax(2)); - $this->assertEquals(1.90, $cartItem->taxTotal(2)); - $this->assertEquals(5.95, $cartItem->priceTax(2)); - $this->assertEquals(11.90, $cartItem->total(2)); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->price); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->discount()); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->subtotal()); + $this->assertEquals(new Money(190, new Currency('USD')), $cartItem->tax()); + $this->assertEquals(new Money(1190, new Currency('USD')), $cartItem->total()); } /** @test */ public function it_can_calculate_all_values_after_updating_from_buyable() { - $cart = $this->getCartDiscount(50); + $cart = $this->getCartDiscount(0.5); $cart->add(new BuyableProduct([ 'name' => 'First item', - 'price' => 5.00, + 'price' => '5.00', ]), 2); $cart->update('027c91341fd5cf4d2579b49c4b6a90da', new BuyableProduct([ @@ -1135,31 +1004,13 @@ class CartTest extends TestCase $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); + $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 0.19); - $this->assertEquals(10.00, $cartItem->price(2)); - $this->assertEquals(5.00, $cartItem->discount(2)); - $this->assertEquals(10.00, $cartItem->discountTotal(2)); - $this->assertEquals(5.00, $cartItem->priceTarget(2)); - $this->assertEquals(10.00, $cartItem->subtotal(2)); - $this->assertEquals(0.95, $cartItem->tax(2)); - $this->assertEquals(1.90, $cartItem->taxTotal(2)); - $this->assertEquals(5.95, $cartItem->priceTax(2)); - $this->assertEquals(11.90, $cartItem->total(2)); - } - - /** @test */ - public function it_will_destroy_the_cart_when_the_user_logs_out_and_the_config_setting_was_set_to_true() - { - $this->app['config']->set('cart.destroy_on_logout', true); - - $this->app->instance(SessionManager::class, Mockery::mock(SessionManager::class, function ($mock) { - $mock->shouldReceive('forget')->once()->with('cart'); - })); - - $user = Mockery::mock(Authenticatable::class); - - \Auth::guard('web')->logout(); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->price); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->discount()); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->subtotal()); + $this->assertEquals(new Money(190, new Currency('USD')), $cartItem->tax()); + $this->assertEquals(new Money(1190, new Currency('USD')), $cartItem->total()); } /** @test */ @@ -1175,7 +1026,7 @@ class CartTest extends TestCase $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals('20.00', $cartItem->total(2)); + $this->assertEquals(new Money(2000, new Currency('USD')), $cartItem->total()); } /** @test */ @@ -1188,26 +1039,11 @@ class CartTest extends TestCase ]), 2); $cart->setGlobalTax(0); - $cart->setGlobalDiscount(50); + $cart->setGlobalDiscount(0.5); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals('10.00', $cartItem->total(2)); - } - - /** @test */ - public function cart_has_no_rounding_errors() - { - $cart = $this->getCart(); - - $cart->add(new BuyableProduct([ - 'name' => 'Item', - 'price' => 10.004, - ]), 2); - - $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - - $this->assertEquals('24.21', $cartItem->total(2)); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->total()); } /** @test */ @@ -1219,7 +1055,7 @@ class CartTest extends TestCase Event::fake(); - $cart = $this->getCartDiscount(50); + $cart = $this->getCartDiscount(0.5); $cart->add(new BuyableProduct([ 'name' => 'Item', ]), 1); @@ -1234,12 +1070,12 @@ class CartTest extends TestCase $cart2->setGlobalTax(0); $cart2->setGlobalDiscount(0); - $this->assertEquals('0', $cart2->countItems()); + $this->assertEquals(0, $cart2->countItems()); $cart2->merge('test'); $this->assertEquals('2', $cart2->countItems()); - $this->assertEquals(20, $cart2->totalFloat()); + $this->assertEquals(new Money(2000, new Currency('USD')), $cart2->total()); $cart3 = $this->getCart(); $cart3->instance('test3'); @@ -1248,7 +1084,7 @@ class CartTest extends TestCase $cart3->merge('test', true); - $this->assertEquals(10, $cart3->totalFloat()); + $this->assertEquals(new Money(1000, new Currency('USD')), $cart3->total()); } /** @test */ @@ -1258,7 +1094,7 @@ class CartTest extends TestCase '--database' => 'testing', ]); Event::fake(); - $cart = $this->getCartDiscount(50); + $cart = $this->getCartDiscount(0.5); $cart->add(new BuyableProduct([ 'name' => 'Item', ]), 1); @@ -1273,45 +1109,16 @@ class CartTest extends TestCase /** @test */ public function cart_can_calculate_all_values() { - $cart = $this->getCartDiscount(50); + $cart = $this->getCartDiscount(0.5); $cart->add(new BuyableProduct([ 'name' => 'First item', ]), 1); - $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); - $this->assertEquals('10.00', $cart->initial(2)); - $this->assertEquals(10.00, $cart->initialFloat()); - $this->assertEquals('5.00', $cart->discount(2)); - $this->assertEquals(5.00, $cart->discountFloat()); - $this->assertEquals('5.00', $cart->subtotal(2)); - $this->assertEquals(5.00, $cart->subtotalFloat()); - $this->assertEquals('0.95', $cart->tax(2)); - $this->assertEquals(0.95, $cart->taxFloat()); - $this->assertEquals('5.95', $cart->total(2)); - $this->assertEquals(5.95, $cart->totalFloat()); - } - - /** @test */ - public function can_access_cart_item_propertys() - { - $cart = $this->getCartDiscount(50); - $cart->add(new BuyableProduct([ - 'name' => 'First item', - ]), 1); - $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(50, $cartItem->discountRate); - } - - /** @test */ - public function cant_access_non_existant_propertys() - { - $cart = $this->getCartDiscount(50); - $cart->add(new BuyableProduct([ - 'name' => 'First item', - ]), 1); - $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $this->assertEquals(null, $cartItem->doesNotExist); - $this->assertEquals(null, $cart->doesNotExist); + $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 0.19); + $this->assertEquals(new Money(1000, new Currency('USD')), $cart->price()); + $this->assertEquals(new Money(500, new Currency('USD')), $cart->discount()); + $this->assertEquals(new Money(500, new Currency('USD')), $cart->subtotal()); + $this->assertEquals(new Money(95, new Currency('USD')), $cart->tax()); + $this->assertEquals(new Money(595, new Currency('USD')), $cart->total()); } /** @test */ @@ -1322,8 +1129,10 @@ class CartTest extends TestCase 'name' => 'First item', ]), 1); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $cart->setDiscount('027c91341fd5cf4d2579b49c4b6a90da', 50); - $this->assertEquals(50, $cartItem->discountRate); + + $cart->setDiscount('027c91341fd5cf4d2579b49c4b6a90da', 0.5); + + $this->assertEquals(0.5, $cartItem->discount); } /** @test */ @@ -1335,11 +1144,8 @@ class CartTest extends TestCase 'weight' => 250, ]), 2); $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $cart->setDiscount('027c91341fd5cf4d2579b49c4b6a90da', 50); - $this->assertEquals('500.00', $cart->weight(2)); - $this->assertEquals(500.00, $cart->weightFloat()); - $this->assertEquals(500.00, $cartItem->weightTotal); - $this->assertEquals('250.00', $cartItem->weight(2)); + $this->assertEquals(500, $cart->weight()); + $this->assertEquals(500, $cartItem->weight()); } /** @test */ @@ -1374,7 +1180,7 @@ class CartTest extends TestCase /** @test */ public function cart_can_create_items_from_models_using_the_canbebought_trait() { - $cart = $this->getCartDiscount(50); + $cart = $this->getCartDiscount(0.5); $cart->add(new BuyableProductTrait([ 'name' => 'First item', @@ -1382,37 +1188,13 @@ class CartTest extends TestCase $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); + $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 0.19); - $this->assertEquals(10.00, $cartItem->price(2)); - $this->assertEquals(5.00, $cartItem->discount(2)); - $this->assertEquals(10.00, $cartItem->discountTotal(2)); - $this->assertEquals(5.00, $cartItem->priceTarget(2)); - $this->assertEquals(10.00, $cartItem->subtotal(2)); - $this->assertEquals(0.95, $cartItem->tax(2)); - $this->assertEquals(1.90, $cartItem->taxTotal(2)); - $this->assertEquals(5.95, $cartItem->priceTax(2)); - $this->assertEquals(11.90, $cartItem->total(2)); - } - - /** @test */ - public function it_does_calculate_correct_results_with_rational_qtys() - { - // https://github.com/Crinsane/LaravelShoppingcart/issues/544 - $cart = $this->getCart(); - - $cart->add(new BuyableProductTrait([ - 'name' => 'First item', - ]), 0.5); - - $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); - - $cart->setGlobalTax(50); - - $this->assertEquals(10.00, $cartItem->price(2)); - $this->assertEquals(5.00, $cart->subtotal(2)); //0.5 qty - $this->assertEquals(7.50, $cart->total(2)); // plus tax - $this->assertEquals(2.50, $cart->tax(2)); // tax of 5 Bucks + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->price); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->discount()); + $this->assertEquals(new Money(1000, new Currency('USD')), $cartItem->subtotal()); + $this->assertEquals(new Money(190, new Currency('USD')), $cartItem->tax()); + $this->assertEquals(new Money(1190, new Currency('USD')), $cartItem->total()); } /** @test */ @@ -1421,9 +1203,10 @@ class CartTest extends TestCase // https://github.com/bumbummen99/LaravelShoppingcart/pull/5 $cart = $this->getCart(); - $cartItem = $cart->add('293ad', 'Product 1', 1, 9.99, 550, ['size' => 'large']); + $cartItem = $cart->add('293ad', 'Product 1', 2, new Money(1000, new Currency('USD')), 550, new CartItemOptions(['size' => 'large'])); $this->assertEquals(550, $cartItem->weight); + $this->assertEquals(1100, $cartItem->weight()); $this->assertTrue($cartItem->options->has('size')); $this->assertEquals('large', $cartItem->options->size); } @@ -1435,7 +1218,7 @@ class CartTest extends TestCase '--database' => 'testing', ]); - $cart = $this->getCartDiscount(50); + $cart = $this->getCartDiscount(0.5); $cart->add(new BuyableProduct([ 'name' => 'Item', ]), 1); @@ -1458,8 +1241,8 @@ class CartTest extends TestCase Event::assertNotDispatched('cart.added'); Event::assertDispatched('cart.merged'); - $this->assertEquals('2', $cart2->countItems()); - $this->assertEquals(20, $cart2->totalFloat()); + $this->assertEquals(2, $cart2->countItems()); + $this->assertEquals(new Money(2000, new Currency('USD')), $cart2->total()); }); } @@ -1470,7 +1253,7 @@ class CartTest extends TestCase '--database' => 'testing', ]); - $cart = $this->getCartDiscount(50); + $cart = $this->getCartDiscount(0.5); $cart->add(new BuyableProduct([ 'name' => 'Item', ]), 1); @@ -1486,141 +1269,17 @@ class CartTest extends TestCase $cart2->setGlobalTax(0); $cart2->setGlobalDiscount(0); - $this->assertEquals('0', $cart2->countItems()); + $this->assertEquals(0, $cart2->countItems()); $cart2->merge('test'); Event::assertDispatched('cart.added', 2); Event::assertDispatched('cart.merged'); - $this->assertEquals('2', $cart2->countItems()); - $this->assertEquals(20, $cart2->totalFloat()); + $this->assertEquals(2, $cart2->countItems()); + $this->assertEquals(new Money(2000, new Currency('USD')), $cart2->total()); }); } - /** @test */ - public function it_use_correctly_rounded_values_for_totals_and_cart_summary() - { - $this->setConfigFormat(2, ',', ''); - - $cart = $this->getCartDiscount(6); - - $cartItem = $cart->add(new BuyableProduct([ - 'name' => 'First item', - 'price' => 0.18929, - ]), 1000); - $cart->add(new BuyableProduct([ - 'id' => 2, - 'name' => 'Second item', - 'price' => 4.41632, - ]), 5); - $cart->add(new BuyableProduct([ - 'id' => 3, - 'name' => 'Third item', - 'price' => 0.37995, - ]), 25); - - $cart->setGlobalTax(22); - - // check total - $this->assertEquals('253,29', $cart->total()); - - // check that the sum of cart subvalues matches the total (in order to avoid cart summary to looks wrong) - $this->assertEquals($cart->totalFloat(), $cart->subtotalFloat() + $cart->taxFloat()); - } - - /** @test */ - public function it_use_gross_price_as_base_price() - { - $cart = $this->getCartDiscount(0); - config(['cart.calculator' => GrossPrice::class]); - - $cartItem = $cart->add(new BuyableProduct([ - 'name' => 'First item', - 'price' => 100, - ]), 2); - - $cart->setGlobalTax(22); - - // check net price - $this->assertEquals(81.97, round($cartItem->priceNet, 2)); - } - - /** @test */ - public function it_use_gross_price_and_it_use_correctly_rounded_values_for_totals_and_cart_summary() - { - $this->setConfigFormat(2, ',', ''); - config(['cart.calculator' => GrossPrice::class]); - - $cart = $this->getCartDiscount(6); - - $cartItem = $cart->add(new BuyableProduct([ - 'name' => 'First item', - 'price' => 0.23093, - ]), 1000); - $cart->add(new BuyableProduct([ - 'id' => 2, - 'name' => 'Second item', - 'price' => 5.38791, - ]), 5); - $cart->add(new BuyableProduct([ - 'id' => 3, - 'name' => 'Third item', - 'price' => 0.46354, - ]), 25); - - $cart->setGlobalTax(22); - - // check total - $this->assertEquals('254,12', $cart->total()); - - // check item price total - $this->assertEquals(190, $cartItem->priceTotal); - // check that the sum of cart subvalues matches the total (in order to avoid cart summary to looks wrong) - $this->assertEquals($cart->totalFloat(), $cart->subtotalFloat() + $cart->taxFloat()); - } - - /** - * Get an instance of the cart. - * - * @return \Gloudemans\Shoppingcart\Cart - */ - private function getCart() - { - $session = $this->app->make('session'); - $events = $this->app->make('events'); - - return new Cart($session, $events); - } - - /** - * Get an instance of the cart with discount. - * - * @param int $discount - * - * @return \Gloudemans\Shoppingcart\Cart - */ - private function getCartDiscount($discount = 50) - { - $cart = $this->getCart(); - $cart->setGlobalDiscount($discount); - - return $cart; - } - - /** - * Set the config number format. - * - * @param int $decimals - * @param string $decimalPoint - * @param string $thousandSeperator - */ - private function setConfigFormat($decimals, $decimalPoint, $thousandSeperator) - { - $this->app['config']->set('cart.format.decimals', $decimals); - $this->app['config']->set('cart.format.decimal_point', $decimalPoint); - $this->app['config']->set('cart.format.thousand_separator', $thousandSeperator); - } - /** @test */ public function it_can_store__mutiple_instances_of_the_cart_in_a_database() { @@ -1659,24 +1318,11 @@ class CartTest extends TestCase { $cart = $this->getCart(); - $cart->add(new BuyableProduct([ - 'name' => 'first item', - 'price' => 1000, - ]), $qty = 5); - $this->assertEquals(5000, $cart->priceTotalFloat()); - } - - /** @test */ - public function it_can_format_the_total_price_of_the_items_in_cart() - { - $cart = $this->getCart(); - $cart->add(new BuyableProduct([ 'name' => 'first item', 'price' => 1000, ]), 5); - $this->assertEquals('5,000.00', $cart->priceTotal()); - $this->assertEquals('5,000.0000', $cart->priceTotal(4, '.', ',')); + $this->assertEquals(new Money(5000, new Currency('USD')), $cart->price()); } /** @test */ @@ -1701,4 +1347,46 @@ class CartTest extends TestCase Event::assertDispatched('cart.erased'); $this->assertDatabaseMissing('shoppingcart', ['identifier' => $identifier, 'instance' => Cart::DEFAULT_INSTANCE]); } + + /** + * Get an instance of the cart. + * + * @return \Gloudemans\Shoppingcart\Cart + */ + private function getCart() + { + $session = $this->app->make('session'); + $events = $this->app->make('events'); + + return new Cart($session, $events); + } + + /** + * Get an instance of the cart with discount. + * + * @param int $discount + * + * @return \Gloudemans\Shoppingcart\Cart + */ + private function getCartDiscount(float $discount = 0.5) + { + $cart = $this->getCart(); + $cart->setGlobalDiscount($discount); + + return $cart; + } + + /** + * Set the config number format. + * + * @param int $decimals + * @param string $decimalPoint + * @param string $thousandSeperator + */ + private function setConfigFormat($decimals, $decimalPoint, $thousandSeperator) + { + $this->app['config']->set('cart.format.decimals', $decimals); + $this->app['config']->set('cart.format.decimal_point', $decimalPoint); + $this->app['config']->set('cart.format.thousand_separator', $thousandSeperator); + } } diff --git a/tests/Fixtures/BuyableProduct.php b/tests/Fixtures/BuyableProduct.php index 576a9cb..9265aab 100644 --- a/tests/Fixtures/BuyableProduct.php +++ b/tests/Fixtures/BuyableProduct.php @@ -2,8 +2,11 @@ namespace Gloudemans\Tests\Shoppingcart\Fixtures; +use Gloudemans\Shoppingcart\CartItemOptions; use Gloudemans\Shoppingcart\Contracts\Buyable; use Illuminate\Database\Eloquent\Model; +use Money\Currency; +use Money\Money; class BuyableProduct extends Model implements Buyable { @@ -18,14 +21,16 @@ class BuyableProduct extends Model implements Buyable 'title', 'description', 'price', + 'currency', 'weight', ]; protected $attributes = [ - 'id' => 1, - 'name' => 'Item name', - 'price' => 10.00, - 'weight' => 0, + 'id' => 1, + 'name' => 'Item name', + 'price' => 1000, + 'currency' => 'USD', + 'weight' => 0, ]; /** @@ -33,7 +38,7 @@ class BuyableProduct extends Model implements Buyable * * @return int|string */ - public function getBuyableIdentifier() + public function getBuyableIdentifier(CartItemOptions $options) { return $this->id; } @@ -43,27 +48,23 @@ class BuyableProduct extends Model implements Buyable * * @return string */ - public function getBuyableDescription() : ?string + public function getBuyableDescription(CartItemOptions $options): ?string { return $this->name; } /** * Get the price of the Buyable item. - * - * @return float */ - public function getBuyablePrice() + public function getBuyablePrice(CartItemOptions $options): Money { - return $this->price; + return new Money($this->price, new Currency($this->currency)); } /** * Get the price of the Buyable item. - * - * @return float */ - public function getBuyableWeight() + public function getBuyableWeight(CartItemOptions $options): int { return $this->weight; } diff --git a/tests/Fixtures/BuyableProductTrait.php b/tests/Fixtures/BuyableProductTrait.php index 29e39f3..17a9811 100644 --- a/tests/Fixtures/BuyableProductTrait.php +++ b/tests/Fixtures/BuyableProductTrait.php @@ -20,13 +20,15 @@ class BuyableProductTrait extends Model implements Buyable 'title', 'description', 'price', + 'currency', 'weight', ]; protected $attributes = [ - 'id' => 1, - 'name' => 'Item name', - 'price' => 10.00, - 'weight' => 0, + 'id' => 1, + 'name' => 'Item name', + 'price' => 1000, + 'currency' => 'USD', + 'weight' => 0, ]; } diff --git a/tests/Fixtures/Identifiable.php b/tests/Fixtures/Identifiable.php index e2d498f..2c8100c 100644 --- a/tests/Fixtures/Identifiable.php +++ b/tests/Fixtures/Identifiable.php @@ -44,7 +44,7 @@ class Identifiable implements InstanceIdentifier * * @return int|string */ - public function getInstanceGlobalDiscount() + public function getInstanceGlobalDiscount(): float { return $this->discountRate; } diff --git a/tests/Fixtures/ProductModel.php b/tests/Fixtures/ProductModel.php index 1939d69..5e89dd2 100644 --- a/tests/Fixtures/ProductModel.php +++ b/tests/Fixtures/ProductModel.php @@ -2,11 +2,13 @@ namespace Gloudemans\Tests\Shoppingcart\Fixtures; -class ProductModel +use Illuminate\Database\Eloquent\Model; + +class ProductModel extends Model { public $someValue = 'Some value'; - public function find($id) : self + public function find($id): self { return $this; }