├── .gitignore ├── .release.json ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── src ├── Exceptions │ ├── InvalidPrice.php │ ├── ModelNotFound.php │ ├── CouponException.php │ ├── InvalidQuantity.php │ └── InvalidTaxableValue.php ├── LaraCartHasher.php ├── Facades │ └── LaraCart.php ├── Contracts │ ├── CouponContract.php │ └── LaraCartContract.php ├── Cart.php ├── database │ └── migrations │ │ └── add_cart_session_id_to_users_table.php.stub ├── Coupons │ ├── Percentage.php │ └── Fixed.php ├── CartFee.php ├── LaraCartServiceProvider.php ├── CartSubItem.php ├── Traits │ ├── CartOptionsMagicMethodsTrait.php │ └── CouponTrait.php ├── config │ └── laracart.php ├── CartItem.php └── LaraCart.php ├── tests ├── Models │ ├── User.php │ └── TestItem.php ├── MagicFunctionsTest.php ├── CrossDeviceTest.php ├── Coupons │ └── Fixed.php ├── LaraCartTestTrait.php ├── FeesTest.php ├── LaraCartTest.php ├── ItemRelationTest.php ├── SubItemsTest.php ├── ItemsTest.php ├── TotalsTest.php └── CouponsTest.php ├── readme.md ├── phpunit.xml ├── LICENSE └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | build 4 | .phpunit.result.cache -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [lukepolo] 4 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidPrice.php: -------------------------------------------------------------------------------- 1 | cart_sessoin_id = $this->getAttribute('cart_sessoin_id'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Contracts/CouponContract.php: -------------------------------------------------------------------------------- 1 | instance = $instance; 28 | $this->tax = config('laracart.tax'); 29 | $this->locale = config('laracart.locale'); 30 | $this->multipleCoupons = config('laracart.multiple_coupons'); 31 | $this->currencyCode = config('laracart.currency_code'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## LaraCart - Laravel Shopping Cart Package 2 | 3 | [![Total Downloads](https://poser.pugx.org/lukepolo/laracart/downloads)](https://packagist.org/packages/lukepolo/laracart) 4 | [![License](https://poser.pugx.org/lukepolo/laracart/license)](https://packagist.org/packages/lukepolo/laracart) 5 | 6 | ### Documentation 7 | 8 | http://laracart.lukepolo.com 9 | 10 | ## Features 11 | 12 | - Coupons 13 | - Session Based System 14 | - Cross Device Support 15 | - Multiple cart instances 16 | - Fees such as a delivery fee 17 | - Taxation on a the item level 18 | - Prices display currency and locale 19 | - Endless item chaining for complex systems 20 | - Totals of all items within the item chains 21 | - Item Model Relation at a global and item level 22 | - Quickly insert items with your own item models 23 | 24 | ## Installation 25 | 26 | ```bash 27 | composer require lukepolo/laracart 28 | ``` 29 | 30 | Publish vendor config and migration: 31 | 32 | ```bash 33 | php artisan vendor:publish --provider="LukePOLO\LaraCart\LaraCartServiceProvider 34 | ``` 35 | -------------------------------------------------------------------------------- /src/database/migrations/add_cart_session_id_to_users_table.php.stub: -------------------------------------------------------------------------------- 1 | string('cart_session_id')->nullable()->default(null); 18 | }); 19 | } 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | if ((Schema::hasColumn(config('laracart.database.table'), 'cart_session_id'))) { 30 | Schema::table(config('laracart.database.table'), function (Blueprint $table) { 31 | $table->dropColumn('cart_session_id'); 32 | }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ./src 16 | 17 | 18 | ./vendor 19 | ./tests 20 | 21 | 22 | 23 | 24 | tests 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/Models/TestItem.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lukepolo/laracart", 3 | "description": "A simple cart for Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "cart", 7 | "shopping", 8 | "shopping cart" 9 | ], 10 | "homepage": "https://github.com/lukepolo/laracart", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Luke Policinski", 15 | "email": "Luke@LukePOLO.com", 16 | "homepage": "http://LukePOLO.com" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.2", 21 | "ext-intl": "*", 22 | "illuminate/support": "^11.0 || ^12.0", 23 | "illuminate/session": "^11.0 || ^12.0", 24 | "illuminate/events": "^11.0 || ^12.0" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^10.5 || ^11.5.3", 28 | "orchestra/testbench": "^10.0", 29 | "mockery/mockery": "^1.0", 30 | "phpunit/php-code-coverage": "^11.0" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "LukePOLO\\LaraCart\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "LukePOLO\\LaraCart\\Tests\\": "tests" 40 | } 41 | }, 42 | "extra": { 43 | "laravel": { 44 | "providers": [ 45 | "LukePOLO\\LaraCart\\LaraCartServiceProvider" 46 | ], 47 | "aliases": { 48 | "LaraCart": "LukePOLO\\LaraCart\\Facades\\LaraCart" 49 | } 50 | } 51 | }, 52 | "minimum-stability": "stable" 53 | } 54 | -------------------------------------------------------------------------------- /src/Coupons/Percentage.php: -------------------------------------------------------------------------------- 1 | code = $code; 32 | if ($value > 1) { 33 | $this->message = 'Invalid value for a percentage coupon. The value must be between 0 and 1.'; 34 | 35 | throw new CouponException($this->message); 36 | } 37 | $this->value = $value; 38 | 39 | $this->setOptions($options); 40 | } 41 | 42 | /** 43 | * Gets the discount amount. 44 | * 45 | * @return string 46 | */ 47 | public function discount($price) 48 | { 49 | if ($this->canApply()) { 50 | return $price * $this->value; 51 | } 52 | 53 | return 0; 54 | } 55 | 56 | /** 57 | * @return mixed 58 | */ 59 | public function displayValue() 60 | { 61 | return ($this->value * 100).'%'; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Coupons/Fixed.php: -------------------------------------------------------------------------------- 1 | code = $code; 26 | $this->value = $value; 27 | 28 | $this->setOptions($options); 29 | } 30 | 31 | /** 32 | * Gets the discount amount. 33 | * 34 | * @return string 35 | */ 36 | public function discount($price) 37 | { 38 | if ($this->canApply()) { 39 | $discount = $this->value - $this->discounted; 40 | if ($discount > $price) { 41 | return $price; 42 | } 43 | 44 | return $discount; 45 | } 46 | 47 | return 0; 48 | } 49 | 50 | /** 51 | * Displays the value in a money format. 52 | * 53 | * @param null $locale 54 | * @param null $currencyCode 55 | * 56 | * @return string 57 | */ 58 | public function displayValue($locale = null, $currencyCode = null, $format = true) 59 | { 60 | return LaraCart::formatMoney( 61 | $this->value, 62 | $locale, 63 | $currencyCode, 64 | $format 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/CartFee.php: -------------------------------------------------------------------------------- 1 | amount = floatval($amount); 31 | $this->taxable = $taxable; 32 | $this->tax = isset($options['tax']) ? $options['tax'] == 0 ? config('laracart.tax') : $options['tax'] : config('laracart.tax'); 33 | $this->options = $options; 34 | } 35 | 36 | /** 37 | * Gets the formatted amount. 38 | * 39 | * @param bool $format 40 | * @param bool $withTax 41 | * 42 | * @return string 43 | */ 44 | public function getAmount($format = true, $withTax = false) 45 | { 46 | $total = $this->amount - $this->discounted; 47 | 48 | if ($withTax) { 49 | $total += $this->tax * $total; 50 | } 51 | 52 | return LaraCart::formatMoney($total, $this->locale, $this->currencyCode, $format); 53 | } 54 | 55 | public function getDiscount($format = true) 56 | { 57 | return LaraCart::formatMoney($this->discounted, $this->locale, $this->currencyCode, $format); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/MagicFunctionsTest.php: -------------------------------------------------------------------------------- 1 | addItem(); 18 | 19 | $this->assertEquals('option_1', $item->b_test); 20 | } 21 | 22 | /** 23 | * Test the magic method set. 24 | */ 25 | public function testSet() 26 | { 27 | $item = $this->addItem(); 28 | 29 | $item->test_option = 123; 30 | 31 | $this->assertEquals(123, $item->test_option); 32 | 33 | try { 34 | $item->tax = 'not_a_number'; 35 | $this->expectException(InvalidTaxableValue::class); 36 | } catch (InvalidTaxableValue $e) { 37 | $this->assertEquals('The tax must be a number', $e->getMessage()); 38 | } 39 | 40 | try { 41 | $item->taxable = 123123; 42 | $this->expectException(InvalidTaxableValue::class); 43 | } catch (InvalidTaxableValue $e) { 44 | $this->assertEquals('The taxable option must be a boolean', $e->getMessage()); 45 | } 46 | 47 | $item->taxable = 1; 48 | $item->taxable = 0; 49 | } 50 | 51 | /** 52 | * Test the magic method isset. 53 | */ 54 | public function testIsset() 55 | { 56 | $item = $this->addItem(); 57 | 58 | $this->assertEquals(true, isset($item->b_test)); 59 | $this->assertEquals(false, isset($item->testtestestestsetset)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/CrossDeviceTest.php: -------------------------------------------------------------------------------- 1 | assertNotEmpty($this->artisan('migrate')); 17 | 18 | $this->beforeApplicationDestroyed(function () { 19 | $this->artisan('migrate:rollback'); 20 | }); 21 | } 22 | 23 | /** 24 | * Test getting the old session. 25 | */ 26 | public function testGetOldSession() 27 | { 28 | $newCart = new \LukePOLO\LaraCart\LaraCart($this->session, $this->events, $this->authManager); 29 | 30 | $this->addItem(); 31 | $this->addItem(); 32 | 33 | $this->app['config']->set('laracart.cross_devices', true); 34 | 35 | $user = new \LukePOLO\LaraCart\Tests\Models\User(); 36 | 37 | $this->assertEquals(0, $newCart->count(false)); 38 | $this->assertEquals(1, $this->count(false)); 39 | 40 | $user->cart_session_id = $this->session->getId(); 41 | $this->authManager->login($user); 42 | 43 | $newCart->get(); 44 | 45 | $this->assertEquals($newCart->count(false), $this->count(false)); 46 | } 47 | 48 | /** 49 | * Testing to make sure the session gets saved to the model. 50 | */ 51 | public function testSaveCartSessionID() 52 | { 53 | $this->app['config']->set('laracart.cross_devices', true); 54 | $user = new \LukePOLO\LaraCart\Tests\Models\User(); 55 | $this->authManager->login($user); 56 | 57 | $this->addItem(); 58 | 59 | $this->assertEquals($this->session->getId(), $this->authManager->user()->cart_session_id); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Coupons/Fixed.php: -------------------------------------------------------------------------------- 1 | code = $code; 30 | $this->value = $value; 31 | 32 | $this->setOptions($options); 33 | } 34 | 35 | /** 36 | * Gets the discount amount. 37 | * 38 | * that way we can spit out why the coupon has failed 39 | * 40 | * @return string 41 | */ 42 | public function discount($price) 43 | { 44 | if ($this->canApply()) { 45 | return 100; 46 | } 47 | 48 | return 0; 49 | } 50 | 51 | /** 52 | * Checks if you can apply the coupon. 53 | * 54 | * @throws CouponException 55 | * 56 | * @return bool 57 | */ 58 | public function canApply($throw = false) 59 | { 60 | if ($this->discounted === 0) { 61 | throw new CouponException('Sorry, you must have at least 100 dollars!'); 62 | } 63 | 64 | return true; 65 | } 66 | 67 | /** 68 | * Displays the value in a money format. 69 | * 70 | * @param null $locale 71 | * @param null $currencyCode 72 | * 73 | * @return string 74 | */ 75 | public function displayValue($locale = null, $currencyCode = null) 76 | { 77 | return LaraCart::formatMoney( 78 | $this->discount(), 79 | $locale, 80 | $currencyCode 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/LaraCartServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 21 | __DIR__.'/config/laracart.php' => config_path('laracart.php'), 22 | ]); 23 | 24 | $this->mergeConfigFrom( 25 | __DIR__.'/config/laracart.php', 26 | 'laracart' 27 | ); 28 | 29 | if (!$this->migrationHasAlreadyBeenPublished()) { 30 | $this->publishes([ 31 | __DIR__.'/database/migrations/add_cart_session_id_to_users_table.php.stub' => database_path('migrations/'.date('Y_m_d_His').'_add_cart_session_id_to_users_table.php'), 32 | ], 'migrations'); 33 | } 34 | } 35 | 36 | /** 37 | * Register the service provider. 38 | * 39 | * @return void 40 | */ 41 | public function register() 42 | { 43 | $this->app->singleton(LaraCart::SERVICE, function ($app) { 44 | return new LaraCart($app['session'], $app['events'], $app['auth']); 45 | }); 46 | 47 | $this->app->singleton(LaraCart::HASH, function () { 48 | return new LaraCartHasher(); 49 | }); 50 | 51 | $this->app->bind( 52 | LaraCart::RANHASH, 53 | function () { 54 | return Str::random(40); 55 | } 56 | ); 57 | } 58 | 59 | /** 60 | * Checks to see if the migration has already been published. 61 | * 62 | * @return bool 63 | */ 64 | protected function migrationHasAlreadyBeenPublished() 65 | { 66 | $files = glob(database_path('migrations/*_add_cart_session_id_to_users_table.php')); 67 | 68 | return count($files) > 0; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/CartSubItem.php: -------------------------------------------------------------------------------- 1 | options['items'] = []; 32 | 33 | foreach ($options as $option => $value) { 34 | Arr::set($this->options, $option, $value); 35 | } 36 | 37 | $this->qty = isset($options['qty']) ? $options['qty'] : 1; 38 | $this->taxable = isset($options['taxable']) ? $options['taxable'] : true; 39 | $this->tax = isset($options['tax']) ? $options['tax'] == 0 ? config('laracart.tax') : $options['tax'] : config('laracart.tax'); 40 | 41 | $this->itemHash = app(LaraCart::HASH)->hash($this->options); 42 | } 43 | 44 | /** 45 | * Gets the hash for the item. 46 | * 47 | * @return mixed 48 | */ 49 | public function getHash() 50 | { 51 | return $this->itemHash; 52 | } 53 | 54 | /** 55 | * Gets the formatted price. 56 | * 57 | * @return float 58 | */ 59 | public function subTotal() 60 | { 61 | $price = $this->price * $this->qty; 62 | 63 | if (isset($this->items)) { 64 | foreach ($this->items as $item) { 65 | $price += $item->subTotal(false); 66 | } 67 | } 68 | 69 | return $price; 70 | } 71 | 72 | /** 73 | * Search for matching options on the item. 74 | * 75 | * @return mixed 76 | */ 77 | public function find($data) 78 | { 79 | foreach ($data as $key => $value) { 80 | if ($this->$key === $value) { 81 | return $this; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/LaraCartTestTrait.php: -------------------------------------------------------------------------------- 1 | laracart = new \LukePOLO\LaraCart\LaraCart($this->session, $this->events, $this->authManager); 25 | } 26 | 27 | /** 28 | * Default tax setup. 29 | * 30 | * @param $app 31 | */ 32 | protected function getEnvironmentSetUp($app) 33 | { 34 | $this->session = $app['session']; 35 | $this->events = $app['events']; 36 | $this->authManager = $app['auth']; 37 | 38 | $app['config']->set('database.default', 'testing'); 39 | 40 | // Setup default database to use sqlite :memory: 41 | $app['config']->set('laracart.tax', '.07'); 42 | } 43 | 44 | /** 45 | * Sets the package providers. 46 | * 47 | * @param $app 48 | * 49 | * @return array 50 | */ 51 | protected function getPackageProviders($app) 52 | { 53 | return ['\LukePOLO\LaraCart\LaraCartServiceProvider']; 54 | } 55 | 56 | /** 57 | * Easy way to add an item for many tests. 58 | * 59 | * @param int $qty 60 | * @param int $price 61 | * @param bool $taxable 62 | * @param array $options 63 | * 64 | * @return mixed 65 | */ 66 | private function addItem($qty = 1, $price = 1, $taxable = true, $options = []) 67 | { 68 | if (empty($options)) { 69 | $options = [ 70 | 'b_test' => 'option_1', 71 | 'a_test' => 'option_2', 72 | ]; 73 | } 74 | 75 | return $this->laracart->add( 76 | 'itemID', 77 | 'Testing Item', 78 | $qty, 79 | $price, 80 | $options, 81 | $taxable 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Traits/CartOptionsMagicMethodsTrait.php: -------------------------------------------------------------------------------- 1 | options, $option); 28 | } 29 | 30 | /** 31 | * Magic Method allows for user input to set a value inside the options array. 32 | * 33 | * @param $option 34 | * @param $value 35 | * 36 | * @throws InvalidPrice 37 | * @throws InvalidQuantity 38 | * @throws InvalidTaxableValue 39 | */ 40 | public function __set($option, $value) 41 | { 42 | switch ($option) { 43 | case CartItem::ITEM_QTY: 44 | if (!is_numeric($value) || $value <= 0) { 45 | throw new InvalidQuantity('The quantity must be a valid number'); 46 | } 47 | break; 48 | case CartItem::ITEM_PRICE: 49 | if (!is_numeric($value)) { 50 | throw new InvalidPrice('The price must be a valid number'); 51 | } 52 | break; 53 | case CartItem::ITEM_TAX: 54 | if (!empty($value) && (!is_numeric($value))) { 55 | throw new InvalidTaxableValue('The tax must be a number'); 56 | } 57 | break; 58 | case CartItem::ITEM_TAXABLE: 59 | if (!is_bool($value) && $value != 0 && $value != 1) { 60 | throw new InvalidTaxableValue('The taxable option must be a boolean'); 61 | } 62 | break; 63 | } 64 | 65 | $changed = (!empty(Arr::get($this->options, $option)) && Arr::get($this->options, $option) != $value); 66 | Arr::set($this->options, $option, $value); 67 | 68 | if ($changed) { 69 | if (is_callable([$this, 'generateHash'])) { 70 | $this->generateHash(); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Magic Method allows for user to check if an option isset. 77 | * 78 | * @param $option 79 | * 80 | * @return bool 81 | */ 82 | public function __isset($option) 83 | { 84 | if (isset($this->options[$option])) { 85 | return true; 86 | } else { 87 | return false; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Traits/CouponTrait.php: -------------------------------------------------------------------------------- 1 | $value) { 31 | $this->$key = $value; 32 | } 33 | } 34 | 35 | /** 36 | * Checks to see if we can apply the coupon. 37 | * 38 | * @return bool 39 | */ 40 | public function canApply() 41 | { 42 | $this->message = 'Coupon Applied'; 43 | 44 | return true; 45 | } 46 | 47 | /** 48 | * Checks the minimum subtotal needed to apply the coupon. 49 | * 50 | * @param $minAmount 51 | * @param $throwErrors 52 | * 53 | * @throws CouponException 54 | * 55 | * @return bool 56 | */ 57 | public function checkMinAmount($minAmount, $throwErrors = true) 58 | { 59 | $laraCart = \App::make(LaraCart::SERVICE); 60 | 61 | if ($laraCart->subTotal(false) >= $minAmount) { 62 | return true; 63 | } else { 64 | if ($throwErrors) { 65 | throw new CouponException('You must have at least a total of '.$laraCart->formatMoney($minAmount)); 66 | } else { 67 | return false; 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Returns either the max discount or the discount applied based on what is passed through. 74 | * 75 | * @param $maxDiscount 76 | * @param $discount 77 | * @param $throwErrors 78 | * 79 | * @throws CouponException 80 | * 81 | * @return mixed 82 | */ 83 | public function maxDiscount($maxDiscount, $discount, $throwErrors = true) 84 | { 85 | if ($maxDiscount == 0 || $maxDiscount > $discount) { 86 | return $discount; 87 | } else { 88 | if ($throwErrors) { 89 | throw new CouponException('This has a max discount of '.\App::make(\LukePOLO\Laracart\LaraCart::SERVICE)->formatMoney($maxDiscount)); 90 | } else { 91 | return $maxDiscount; 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Checks to see if the times are valid for the coupon. 98 | * 99 | * @param Carbon $startDate 100 | * @param Carbon $endDate 101 | * @param $throwErrors 102 | * 103 | * @throws CouponException 104 | * 105 | * @return bool 106 | */ 107 | public function checkValidTimes(Carbon $startDate, Carbon $endDate, $throwErrors = true) 108 | { 109 | if (Carbon::now()->between($startDate, $endDate)) { 110 | return true; 111 | } else { 112 | if ($throwErrors) { 113 | throw new CouponException('This coupon has expired'); 114 | } else { 115 | return false; 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Sets a discount to an item with what code was used and the discount amount. 122 | * 123 | * @param CartItem $item 124 | */ 125 | public function setDiscountOnItem(CartItem $item) 126 | { 127 | $this->appliedToCart = false; 128 | $item->coupon = $this; 129 | $item->update(); 130 | } 131 | 132 | public function discounted() 133 | { 134 | return $this->discounted; 135 | } 136 | 137 | public function getMessage() 138 | { 139 | return $this->message; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/FeesTest.php: -------------------------------------------------------------------------------- 1 | laracart->addFee( 19 | $name, 20 | $fee 21 | ); 22 | } 23 | 24 | /** 25 | * Add a fee with tax. 26 | * 27 | * @param $name 28 | * @param int $fee 29 | */ 30 | private function addFeeTax($name, $fee = 100, $tax = 0.21) 31 | { 32 | $this->laracart->addFee( 33 | $name, 34 | $fee, 35 | true, 36 | [ 37 | 'tax' => $tax, 38 | ] 39 | ); 40 | } 41 | 42 | /** 43 | * Testing add a fee to the cart. 44 | */ 45 | public function testAddFee() 46 | { 47 | $this->addFee('testFeeOne'); 48 | 49 | $fee = $this->laracart->getFee('testFeeOne'); 50 | 51 | $this->assertEquals('$10.00', $fee->getAmount()); 52 | $this->assertEquals(10, $fee->getAmount(false)); 53 | } 54 | 55 | /** 56 | * Testing add a fee to the cart with tax. 57 | */ 58 | public function testAddFeeTax() 59 | { 60 | $this->addFeeTax('testFeeOne'); 61 | 62 | $fee = $this->laracart->getFee('testFeeOne'); 63 | 64 | $this->assertEquals('$100.00', $fee->getAmount(true, false)); 65 | $this->assertEquals('$121.00', $fee->getAmount(true, true)); 66 | 67 | $this->assertEquals(100, $fee->getAmount(false, false)); 68 | $this->assertEquals(121, $fee->getAmount(false, true)); 69 | } 70 | 71 | /** 72 | * Test if we can add multiple fees to the cart. 73 | */ 74 | public function testMultipleFees() 75 | { 76 | $this->addFee('testFeeOne'); 77 | $this->addFee('testFeeTwo', 20); 78 | 79 | $this->assertEquals('$10.00', $this->laracart->getFee('testFeeOne')->getAmount()); 80 | $this->assertEquals('$20.00', $this->laracart->getFee('testFeeTwo')->getAmount()); 81 | 82 | $this->assertEquals(10, $this->laracart->getFee('testFeeOne')->getAmount(false)); 83 | $this->assertEquals(20, $this->laracart->getFee('testFeeTwo')->getAmount(false)); 84 | } 85 | 86 | /** 87 | * Test if we can add multiple fees to the cart. 88 | */ 89 | public function testMultipleFeesTax() 90 | { 91 | $this->addFeeTax('testFeeOne'); 92 | $this->addFeeTax('testFeeTwo', 200); 93 | 94 | $this->assertEquals('$100.00', $this->laracart->getFee('testFeeOne')->getAmount(true, false)); 95 | $this->assertEquals('$121.00', $this->laracart->getFee('testFeeOne')->getAmount(true, true)); 96 | 97 | $this->assertEquals('$242.00', $this->laracart->getFee('testFeeTwo')->getAmount(true, true)); 98 | $this->assertEquals('$200.00', $this->laracart->getFee('testFeeTwo')->getAmount(true, false)); 99 | 100 | $this->assertEquals(121, $this->laracart->getFee('testFeeOne')->getAmount(false, true)); 101 | $this->assertEquals(100, $this->laracart->getFee('testFeeOne')->getAmount(false, false)); 102 | 103 | $this->assertEquals(242, $this->laracart->getFee('testFeeTwo')->getAmount(false, true)); 104 | $this->assertEquals(200, $this->laracart->getFee('testFeeTwo')->getAmount(false, false)); 105 | } 106 | 107 | /** 108 | * Test if we can remove a fee from the cart. 109 | */ 110 | public function testRemoveFee() 111 | { 112 | $this->addFee('testFeeOne'); 113 | $this->assertEquals('$10.00', $this->laracart->getFee('testFeeOne')->getAmount()); 114 | $this->assertEquals(10, $this->laracart->getFee('testFeeOne')->getAmount(false)); 115 | 116 | $this->laracart->removeFee('testFeeOne'); 117 | 118 | $this->assertEquals('$0.00', $this->laracart->getFee('testFeeOne')->getAmount()); 119 | $this->assertEquals(0, $this->laracart->getFee('testFeeOne')->getAmount(false)); 120 | } 121 | 122 | /** 123 | * Test if we can remove all fees from the cart. 124 | */ 125 | public function testRemoveFees() 126 | { 127 | $this->addFee('testFeeOne'); 128 | $this->assertEquals('$10.00', $this->laracart->getFee('testFeeOne')->getAmount()); 129 | $this->assertEquals(10, $this->laracart->getFee('testFeeOne')->getAmount(false)); 130 | 131 | $this->laracart->removeFees(); 132 | 133 | $this->assertTrue(empty($this->laracart->getFees())); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/config/laracart.php: -------------------------------------------------------------------------------- 1 | 'laracart', 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | database settings 16 | |-------------------------------------------------------------------------- 17 | | 18 | | Here you can set the name of the table you want to use for 19 | | storing and restoring the cart session id. 20 | | 21 | */ 22 | 'database' => [ 23 | 'table' => 'users', 24 | ], 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Locale is used to convert money into a readable format for the user, 29 | | please note the UTF-8, helps to make sure its encoded correctly 30 | | 31 | | Common Locales 32 | | 33 | | English - United States (en_US): 123,456.00 34 | | English - United Kingdom (en_GB) 123,456.00 35 | | Spanish - Spain (es_ES): 123.456,000 36 | | Dutch - Netherlands (nl_NL): 123 456,00 37 | | German - Germany (de_DE): 123.456,00 38 | | French - France (fr_FR): 123 456,00 39 | | Italian - Italy (it_IT): 123.456,00 40 | | 41 | | This site is pretty useful : http://lh.2xlibre.net/locales/ 42 | | 43 | |-------------------------------------------------------------------------- 44 | | 45 | */ 46 | 'locale' => 'en_US.UTF-8', 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | The currency code changes how you see the actual amounts. 51 | |-------------------------------------------------------------------------- 52 | | This is the list of all valid currency codes 53 | | https://www2.1010data.com/documentationcenter/prod/1010dataReferenceManual/DataTypesAndFormats/currencyUnitCodes.html 54 | | 55 | */ 56 | 'currency_code' => 'USD', 57 | 58 | /* 59 | |-------------------------------------------------------------------------- 60 | | If true, lets you supply and retrieve all prices in cents. 61 | | To retrieve the prices as integer in cents, set the $format parameter 62 | | to false for the various price functions. Otherwise you will retrieve 63 | | the formatted price instead. 64 | | Make sure when adding products to the cart, adding coupons, etc, to 65 | | supply the price in cents too. 66 | |-------------------------------------------------------------------------- 67 | | 68 | */ 69 | 'prices_in_cents' => false, 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Sets the tax for the cart and items, you can change per item 74 | | via the object later if needed 75 | |-------------------------------------------------------------------------- 76 | | 77 | */ 78 | 'tax' => null, 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Allows you to choose if the discounts applied to fees 83 | |-------------------------------------------------------------------------- 84 | | 85 | */ 86 | 'fees_taxable' => false, 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Allows you to choose if the discounts applied to fees 91 | |-------------------------------------------------------------------------- 92 | | 93 | */ 94 | 'discount_fees' => false, 95 | 96 | /* 97 | |-------------------------------------------------------------------------- 98 | | Allows you to configure if a user can apply multiple coupons 99 | |-------------------------------------------------------------------------- 100 | | 101 | */ 102 | 'multiple_coupons' => false, 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | The default item model for your relations 107 | |-------------------------------------------------------------------------- 108 | | 109 | */ 110 | 'item_model' => null, 111 | 112 | /* 113 | |-------------------------------------------------------------------------- 114 | | Binds your data into the correct spots for LaraCart 115 | |-------------------------------------------------------------------------- 116 | | 117 | */ 118 | 'item_model_bindings' => [ 119 | \LukePOLO\LaraCart\CartItem::ITEM_ID => 'id', 120 | \LukePOLO\LaraCart\CartItem::ITEM_NAME => 'name', 121 | \LukePOLO\LaraCart\CartItem::ITEM_PRICE => 'price', 122 | \LukePOLO\LaraCart\CartItem::ITEM_TAXABLE => 'taxable', 123 | \LukePOLO\LaraCart\CartItem::ITEM_OPTIONS => [ 124 | // put columns here for additional options, 125 | // these will be merged with options that are passed in 126 | // e.x 127 | // tax => .07 128 | ], 129 | ], 130 | 131 | /* 132 | |-------------------------------------------------------------------------- 133 | | The default item relations to the item_model 134 | |-------------------------------------------------------------------------- 135 | | 136 | */ 137 | 'item_model_relations' => [], 138 | 139 | /* 140 | |-------------------------------------------------------------------------- 141 | | This allows you to use multiple devices based on your logged in user 142 | |-------------------------------------------------------------------------- 143 | | 144 | */ 145 | 'cross_devices' => false, 146 | 147 | /* 148 | |-------------------------------------------------------------------------- 149 | | This allows you to use custom guard to get logged in user 150 | |-------------------------------------------------------------------------- 151 | | 152 | */ 153 | 'guard' => null, 154 | 155 | /* 156 | |-------------------------------------------------------------------------- 157 | | This allows you to exclude any option from generating CartItem hash 158 | |-------------------------------------------------------------------------- 159 | | 160 | */ 161 | 'exclude_from_hash' => [], 162 | ]; 163 | -------------------------------------------------------------------------------- /tests/LaraCartTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 16 | new \LukePOLO\LaraCart\LaraCart($this->session, $this->events, $this->authManager), 17 | $this->app->make('laracart') 18 | ); 19 | } 20 | 21 | /** 22 | * Test setting the instance. 23 | */ 24 | public function testSetInstance() 25 | { 26 | $this->assertNotEquals( 27 | new \LukePOLO\LaraCart\LaraCart($this->session, $this->events, $this->authManager), 28 | $this->laracart->setInstance('test') 29 | ); 30 | } 31 | 32 | /** 33 | * Test to make sure we get default instance. 34 | */ 35 | public function testGetInstancesDefault() 36 | { 37 | $this->assertEquals('default', $this->laracart->getInstances()[0]); 38 | } 39 | 40 | /** 41 | * Test to make sure we can get instances. 42 | */ 43 | public function testGetInstances() 44 | { 45 | $this->laracart->setInstance('test'); 46 | $this->laracart->setInstance('test'); 47 | $this->laracart->setInstance('test'); 48 | $this->laracart->setInstance('test-2'); 49 | $this->laracart->setInstance('test-3'); 50 | 51 | $this->assertCount(4, $this->laracart->getInstances()); 52 | } 53 | 54 | /** 55 | * Testing the money format function. 56 | */ 57 | public function testFormatMoney() 58 | { 59 | $this->assertEquals('$25.00', $this->laracart->formatMoney('25.00')); 60 | $this->assertEquals('€25.00', $this->laracart->formatMoney('25.00', null, 'EUR')); 61 | $this->assertEquals('25.00', $this->laracart->formatMoney('25.00', null, null, false)); 62 | 63 | $this->assertEquals('$25.56', $this->laracart->formatMoney('25.555')); 64 | $this->assertEquals('$25.54', $this->laracart->formatMoney('25.544')); 65 | } 66 | 67 | /** 68 | * Testing the money format function with the prices_in_cents config setting. 69 | */ 70 | public function testFormatMoneyPricesInCents() 71 | { 72 | $this->app['config']->set('laracart.prices_in_cents', true); 73 | 74 | $this->assertEquals('$25.00', $this->laracart->formatMoney(2500)); 75 | $this->assertEquals('CA$25.00', $this->laracart->formatMoney(2500, null, 'CAD')); 76 | $this->assertEquals(2500, $this->laracart->formatMoney(2500, null, null, false)); 77 | 78 | $this->assertEquals('$25.01', $this->laracart->formatMoney(2500.55)); 79 | $this->assertEquals('$25.00', $this->laracart->formatMoney(2500.44)); 80 | 81 | $this->assertEquals(2501, $this->laracart->formatMoney(2500.55, null, null, false)); 82 | $this->assertEquals(2500, $this->laracart->formatMoney(2500.44, null, null, false)); 83 | } 84 | 85 | /** 86 | * Test getting the attributes from the cart. 87 | */ 88 | public function testGetAttributes() 89 | { 90 | $this->laracart->setAttribute('test1', 1); 91 | $this->laracart->setAttribute('test2', 2); 92 | 93 | $this->assertCount(2, $attributes = $this->laracart->getAttributes()); 94 | 95 | $this->assertEquals(1, $attributes['test1']); 96 | $this->assertEquals(2, $attributes['test2']); 97 | } 98 | 99 | /** 100 | * Test removing attributes from the cart. 101 | */ 102 | public function testRemoveAttribute() 103 | { 104 | $this->laracart->setAttribute('test1', 1); 105 | 106 | $this->assertEquals(1, $this->laracart->getAttribute('test1')); 107 | 108 | $this->laracart->removeAttribute('test1'); 109 | 110 | $this->assertNull($this->laracart->getAttribute('test1')); 111 | } 112 | 113 | /** 114 | * Testing if the item count matches. 115 | */ 116 | public function testCount() 117 | { 118 | $this->addItem(2); 119 | 120 | $this->assertEquals(2, $this->laracart->count()); 121 | $this->assertEquals(1, $this->laracart->count(false)); 122 | } 123 | 124 | /** 125 | * Makes sure that when we empty the cart it deletes all items. 126 | */ 127 | public function testEmptyCart() 128 | { 129 | $this->addItem(); 130 | 131 | $this->laracart->setAttribute('test', 1); 132 | 133 | $this->laracart->emptyCart(); 134 | 135 | $this->assertEquals(1, $this->laracart->getAttribute('test')); 136 | $this->assertEquals(0, $this->laracart->count()); 137 | } 138 | 139 | /** 140 | * Test destroying the cart rather than just emptying it. 141 | */ 142 | public function testDestroyCart() 143 | { 144 | $this->addItem(); 145 | 146 | $this->laracart->setAttribute('test', 1); 147 | 148 | $this->laracart->destroyCart(); 149 | 150 | $this->assertEquals(null, $this->laracart->getAttribute('test')); 151 | $this->assertEquals(0, $this->laracart->count()); 152 | } 153 | 154 | /** 155 | * Testing to make sure if we switch carts and destroy it destroys the proper cart. 156 | */ 157 | public function testDestroyOtherCart() 158 | { 159 | $this->addItem(); 160 | 161 | $this->laracart->setInstance('test'); 162 | 163 | $this->addItem(); 164 | 165 | $cart = $this->laracart->get('test'); 166 | 167 | $this->assertEquals(1, $cart->count()); 168 | 169 | $this->laracart->destroyCart(); 170 | 171 | $cart = $this->laracart->get('test'); 172 | 173 | $this->assertEquals(0, $cart->count()); 174 | 175 | $cart = $this->laracart->get(); 176 | 177 | $this->assertEquals(1, $cart->count()); 178 | } 179 | 180 | /** 181 | * Tests if generating a new hash when we change an option. 182 | */ 183 | public function testGeneratingHashes() 184 | { 185 | $item = $this->addItem(); 186 | 187 | $prevHash = $item->getHash(); 188 | 189 | $item->name = 'NEW NAME'; 190 | 191 | $this->assertNotEquals($prevHash, $item->getHash()); 192 | } 193 | 194 | /** 195 | * Tests if generating a same hash when we change an excluded option. 196 | */ 197 | public function testGeneratingHashesExistConfig() 198 | { 199 | $this->app['config']->set('laracart.exclude_from_hash', ['some_option']); 200 | 201 | $item = $this->addItem(1, 1, true, ['some_option' => 'some value']); 202 | 203 | $prevHash = $item->getHash(); 204 | 205 | $item->some_option = 'NEW VALUE'; 206 | 207 | $this->assertEquals($prevHash, $item->getHash()); 208 | } 209 | 210 | /** 211 | * Tests the facade. 212 | */ 213 | public function getFacadeName() 214 | { 215 | $facade = new \LukePOLO\LaraCart\Facades\LaraCart(); 216 | $this->assertEquals('laracart', $facade::getFacadeAccessor()); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/Contracts/LaraCartContract.php: -------------------------------------------------------------------------------- 1 | addItem(); 18 | 19 | $this->assertEmpty($item->itemModel); 20 | 21 | $item->setModel(\LukePOLO\LaraCart\Tests\Models\TestItem::class); 22 | 23 | $this->assertEquals(\LukePOLO\LaraCart\Tests\Models\TestItem::class, $item->getItemModel()); 24 | 25 | $this->assertEquals('itemID', $item->getModel()->id); 26 | 27 | try { 28 | $item->id = 'fail'; 29 | $item->getModel(); 30 | $this->expectException(ModelNotFound::class); 31 | } catch (ModelNotFound $e) { 32 | $this->assertEquals('Could not find the item model for fail', $e->getMessage()); 33 | } 34 | } 35 | 36 | /** 37 | * Test for exception if could not find model. 38 | */ 39 | public function testItemRelationModelException() 40 | { 41 | $item = $this->addItem(); 42 | 43 | try { 44 | $item->setModel('asdfasdf'); 45 | $this->expectException(ModelNotFound::class); 46 | } catch (ModelNotFound $e) { 47 | $this->assertEquals('Could not find relation model', $e->getMessage()); 48 | } 49 | } 50 | 51 | /** 52 | * Testing adding via item id. 53 | */ 54 | public function testAddItemID() 55 | { 56 | $this->laracart->itemModel = \LukePOLO\LaraCart\Tests\Models\TestItem::class; 57 | $this->laracart->item_model_bindings = [ 58 | \LukePOLO\LaraCart\CartItem::ITEM_ID => 'id', 59 | \LukePOLO\LaraCart\CartItem::ITEM_NAME => 'name', 60 | \LukePOLO\LaraCart\CartItem::ITEM_PRICE => 'price', 61 | \LukePOLO\LaraCart\CartItem::ITEM_TAXABLE => 'taxable', 62 | \LukePOLO\LaraCart\CartItem::ITEM_OPTIONS => [ 63 | 'tax', 64 | ], 65 | ]; 66 | 67 | $this->app['config']->set('laracart.item_model', \LukePOLO\LaraCart\Tests\Models\TestItem::class); 68 | 69 | $this->app['config']->set('laracart.item_model_bindings', [ 70 | \LukePOLO\LaraCart\CartItem::ITEM_ID => 'id', 71 | \LukePOLO\LaraCart\CartItem::ITEM_NAME => 'name', 72 | \LukePOLO\LaraCart\CartItem::ITEM_PRICE => 'price', 73 | \LukePOLO\LaraCart\CartItem::ITEM_TAXABLE => 'taxable', 74 | \LukePOLO\LaraCart\CartItem::ITEM_OPTIONS => [ 75 | 'tax', 76 | ], 77 | ]); 78 | 79 | $item = $this->laracart->add('123123'); 80 | 81 | $this->assertEquals($item->getModel()->id, $item->model->id); 82 | 83 | try { 84 | $this->laracart->add('fail'); 85 | } catch (ModelNotFound $e) { 86 | $this->assertEquals('Could not find the item fail', $e->getMessage()); 87 | } 88 | 89 | $this->assertEquals($item, $this->laracart->getItem($item->getHash())); 90 | 91 | $this->assertEquals($item->id, 'itemID'); 92 | $this->assertEquals($item->name, 'Test Item'); 93 | $this->assertEquals($item->qty, 1); 94 | $this->assertEquals($item->tax, '.07'); 95 | $this->assertEquals($item->price, 5000.01); 96 | $this->assertEquals($item->taxable, false); 97 | } 98 | 99 | /** 100 | * Testing adding a item model. 101 | */ 102 | public function testAddItemModel() 103 | { 104 | $this->app['config']->set('laracart.item_model', \LukePOLO\LaraCart\Tests\Models\TestItem::class); 105 | 106 | $this->app['config']->set('laracart.item_model_bindings', [ 107 | \LukePOLO\LaraCart\CartItem::ITEM_ID => 'id', 108 | \LukePOLO\LaraCart\CartItem::ITEM_NAME => 'name', 109 | \LukePOLO\LaraCart\CartItem::ITEM_PRICE => 'price', 110 | \LukePOLO\LaraCart\CartItem::ITEM_TAXABLE => 'taxable', 111 | \LukePOLO\LaraCart\CartItem::ITEM_OPTIONS => [ 112 | 'tax', 113 | ], 114 | ]); 115 | 116 | $item = new \LukePOLO\LaraCart\Tests\Models\TestItem([ 117 | 'price' => 5000.01, 118 | 'taxable' => false, 119 | 'tax' => '.5', 120 | ]); 121 | 122 | $item = $this->laracart->add($item); 123 | 124 | $this->assertEquals($item, $this->laracart->getItem($item->getHash())); 125 | 126 | $this->assertEquals($item->id, 'itemID'); 127 | $this->assertEquals($item->name, 'Test Item'); 128 | $this->assertEquals($item->qty, 1); 129 | $this->assertEquals($item->tax, '.5'); 130 | $this->assertEquals($item->price, 5000.01); 131 | $this->assertEquals($item->taxable, false); 132 | } 133 | 134 | /** 135 | * gtesting multiple item models at once. 136 | */ 137 | public function testAddMultipleItemModel() 138 | { 139 | $this->app['config']->set('laracart.item_model', \LukePOLO\LaraCart\Tests\Models\TestItem::class); 140 | 141 | $this->app['config']->set('laracart.item_model_bindings', [ 142 | \LukePOLO\LaraCart\CartItem::ITEM_ID => 'id', 143 | \LukePOLO\LaraCart\CartItem::ITEM_NAME => 'name', 144 | \LukePOLO\LaraCart\CartItem::ITEM_PRICE => 'price', 145 | \LukePOLO\LaraCart\CartItem::ITEM_TAXABLE => 'taxable', 146 | \LukePOLO\LaraCart\CartItem::ITEM_OPTIONS => [ 147 | 'tax', 148 | ], 149 | ]); 150 | 151 | $item = new \LukePOLO\LaraCart\Tests\Models\TestItem([ 152 | 'price' => 5000.01, 153 | 'taxable' => false, 154 | 'tax' => '.5', 155 | ]); 156 | 157 | $this->laracart->add($item); 158 | $item = $this->laracart->add($item); 159 | 160 | $this->assertEquals(2, $this->laracart->getItem($item->getHash())->qty); 161 | } 162 | 163 | /** 164 | * Testing adding a model to a line item. 165 | */ 166 | public function testAddItemModelLine() 167 | { 168 | $this->app['config']->set('laracart.item_model', \LukePOLO\LaraCart\Tests\Models\TestItem::class); 169 | 170 | $this->app['config']->set('laracart.item_model_bindings', [ 171 | \LukePOLO\LaraCart\CartItem::ITEM_ID => 'id', 172 | \LukePOLO\LaraCart\CartItem::ITEM_NAME => 'name', 173 | \LukePOLO\LaraCart\CartItem::ITEM_PRICE => 'price', 174 | \LukePOLO\LaraCart\CartItem::ITEM_TAXABLE => 'taxable', 175 | \LukePOLO\LaraCart\CartItem::ITEM_OPTIONS => [ 176 | 'tax', 177 | ], 178 | ]); 179 | 180 | $item = new \LukePOLO\LaraCart\Tests\Models\TestItem([ 181 | 'price' => 5000.01, 182 | 'taxable' => false, 183 | 'tax' => '.5', 184 | ]); 185 | 186 | $item = $this->laracart->addLine($item); 187 | 188 | $this->assertEquals($item, $this->laracart->getItem($item->getHash())); 189 | 190 | $this->assertEquals($item->id, 'itemID'); 191 | $this->assertEquals($item->name, 'Test Item'); 192 | $this->assertEquals($item->qty, 1); 193 | $this->assertEquals($item->tax, '.5'); 194 | $this->assertEquals($item->price, 5000.01); 195 | $this->assertEquals($item->taxable, false); 196 | } 197 | 198 | /** 199 | * Testing adding multiple item models per row. 200 | */ 201 | public function testAddMultipleItemModelLine() 202 | { 203 | $this->app['config']->set('laracart.item_model', \LukePOLO\LaraCart\Tests\Models\TestItem::class); 204 | 205 | $this->app['config']->set('laracart.item_model_bindings', [ 206 | \LukePOLO\LaraCart\CartItem::ITEM_ID => 'id', 207 | \LukePOLO\LaraCart\CartItem::ITEM_NAME => 'name', 208 | \LukePOLO\LaraCart\CartItem::ITEM_PRICE => 'price', 209 | \LukePOLO\LaraCart\CartItem::ITEM_TAXABLE => 'taxable', 210 | \LukePOLO\LaraCart\CartItem::ITEM_OPTIONS => [ 211 | 'tax', 212 | ], 213 | ]); 214 | 215 | $item = new \LukePOLO\LaraCart\Tests\Models\TestItem([ 216 | 'price' => 5000.01, 217 | 'taxable' => false, 218 | 'tax' => '.5', 219 | ]); 220 | 221 | $this->laracart->addLine($item); 222 | $item = $this->laracart->addLine($item); 223 | 224 | $this->assertEquals(1, $this->laracart->getItem($item->getHash())->qty); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /tests/SubItemsTest.php: -------------------------------------------------------------------------------- 1 | addItem(); 16 | 17 | $subItem = $item->addSubItem([ 18 | 'size' => 'XXL', 19 | 'price' => 2.50, 20 | ]); 21 | 22 | $this->containsOnlyInstancesOf(LukePOLO\LaraCart\CartSubItem::class, $item->subItems); 23 | 24 | $this->assertEquals($subItem, $item->findSubItem($subItem->getHash())); 25 | } 26 | 27 | /** 28 | * Test getting the total from a sub item. 29 | */ 30 | public function testSubItemTotal() 31 | { 32 | $item = $this->addItem(); 33 | 34 | $item->addSubItem([ 35 | 'size' => 'XXL', 36 | 'price' => 2.50, 37 | ]); 38 | 39 | $this->assertEquals(3.50, $item->subTotal(false)); 40 | $this->assertEquals(2.50, $item->subItemsTotal(false)); 41 | } 42 | 43 | /** 44 | * Test the sub items with more sub items. 45 | */ 46 | public function testSubItemItemsTotal() 47 | { 48 | $item = $this->addItem(1, 10, true, [ 49 | 'tax' => .01, 50 | ]); 51 | 52 | $item->addSubItem([ 53 | 'price' => 10, 54 | 'tax' => .01, 55 | 'items' => [ 56 | new \LukePOLO\LaraCart\CartItem('10', 'sub item item', 1, 10, [ 57 | 'tax' => .01, 58 | ]), 59 | ], 60 | ]); 61 | 62 | $this->assertEquals(20, $item->subItemsTotal(false)); 63 | $this->assertEquals(30, $item->subTotal(false)); 64 | $this->assertEquals(.30, $this->laracart->taxTotal(false)); 65 | $this->assertEquals(30.30, $this->laracart->total(false)); 66 | } 67 | 68 | public function testSubItemMultiQtyTaxation() 69 | { 70 | $item = $this->addItem(1, 10, true, [ 71 | 'tax' => .01, 72 | ]); 73 | 74 | $item->addSubItem([ 75 | 'price' => 10, 76 | 'tax' => .01, 77 | 'items' => [ 78 | new \LukePOLO\LaraCart\CartItem('10', 'sub item item', 10, 1, [ 79 | 'tax' => .01, 80 | ]), 81 | ], 82 | ]); 83 | 84 | $this->assertEquals(20, $item->subItemsTotal(false)); 85 | $this->assertEquals(30, $item->subTotal(false)); 86 | $this->assertEquals(.30, $this->laracart->taxTotal(false)); 87 | $this->assertEquals(30.30, $this->laracart->total(false)); 88 | } 89 | 90 | /** 91 | * Testing totals for sub sub items. 92 | */ 93 | public function testSubItemsSubItemsTotal() 94 | { 95 | $item = $this->addItem(1, 11); 96 | 97 | $subItem = new \LukePOLO\LaraCart\CartItem('10', 'sub item item', 1, 2); 98 | 99 | $subItem->addSubItem([ 100 | 'items' => [ 101 | new \LukePOLO\LaraCart\CartItem('10', 'sub item item', 1, 1), 102 | ], 103 | ]); 104 | 105 | $item->addSubItem([ 106 | 'items' => [ 107 | $subItem, 108 | ], 109 | ]); 110 | 111 | $this->assertEquals(3, $item->subItemsTotal(false)); 112 | $this->assertEquals(14, $item->subTotal(false)); 113 | $this->assertEquals(14.98, $item->total(false)); 114 | } 115 | 116 | /** 117 | * Test adding an item on a sub item. 118 | */ 119 | public function testAddSubItemItems() 120 | { 121 | $item = $this->addItem(); 122 | 123 | $subItem = $item->addSubItem([ 124 | 'size' => 'XXL', 125 | 'price' => 2.50, 126 | 'items' => [ 127 | new \LukePOLO\LaraCart\CartItem('itemId', 'test item', 1, 10), 128 | ], 129 | ]); 130 | 131 | $this->containsOnlyInstancesOf(LukePOLO\LaraCart\CartItem::class, $subItem->items); 132 | 133 | $this->assertEquals(12.50, $subItem->subTotal()); 134 | } 135 | 136 | /** 137 | * Test adding an item on a sub item. 138 | */ 139 | public function testAddSubItemItemsWithQty() 140 | { 141 | $item = $this->addItem(); 142 | 143 | $subItem = $item->addSubItem([ 144 | 'size' => 'XXL', 145 | 'price' => 2.50, 146 | 'items' => [ 147 | new \LukePOLO\LaraCart\CartItem('itemId', 'test item', 1, 10), 148 | ], 149 | ]); 150 | 151 | $this->containsOnlyInstancesOf(LukePOLO\LaraCart\CartItem::class, $subItem->items); 152 | 153 | $this->assertEquals(12.50, $subItem->subTotal()); 154 | $this->assertEquals('13.50', $item->subTotal(false)); 155 | 156 | $this->assertEquals('13.50', $this->laracart->subTotal(false)); 157 | 158 | $item->qty = 2; 159 | $this->assertEquals('27.00', $item->subTotal(false)); 160 | $this->assertEquals('27.00', $this->laracart->subTotal(false)); 161 | } 162 | 163 | /** 164 | * Test removing sub items. 165 | */ 166 | public function testRemoveSubItem() 167 | { 168 | $item = $this->addItem(); 169 | 170 | $subItem = $item->addSubItem([ 171 | 'size' => 'XXL', 172 | 'price' => 2.50, 173 | ]); 174 | 175 | $subItemHash = $subItem->getHash(); 176 | 177 | $this->assertEquals($subItem, $item->findSubItem($subItemHash)); 178 | 179 | $item->removeSubItem($subItemHash); 180 | 181 | $this->assertEquals(null, $item->findSubItem($subItemHash)); 182 | } 183 | 184 | /** 185 | * Test to make sure taxable flag is working for total tax. 186 | */ 187 | public function testAddSubItemItemsSubItemsTax() 188 | { 189 | $item = $this->addItem(); 190 | 191 | $item->addSubItem([ 192 | 'size' => 'XXL', 193 | 'price' => 2.50, 194 | 'items' => [ 195 | new \LukePOLO\LaraCart\CartItem('itemId', 'test item', 1, 10, [], false), 196 | ], 197 | ]); 198 | 199 | $this->assertEquals(13.50, $item->subTotal(false)); 200 | 201 | $this->assertEquals('0.25', $this->laracart->taxTotal(false)); 202 | } 203 | 204 | /** 205 | * Test Tax in case the item is not taxed but subItems are taxable. 206 | */ 207 | public function testAddTaxedSubItemsItemUnTaxed() 208 | { 209 | $item = $this->addItem(1, 1, false); 210 | 211 | // 12.50 212 | $item->addSubItem([ 213 | 'size' => 'XXL', 214 | 'price' => 2.50, 215 | 'taxable' => true, 216 | 'items' => [ 217 | new \LukePOLO\LaraCart\CartItem('itemId', 'test item', 1, 10, [], true), 218 | ], 219 | ]); 220 | 221 | $this->assertEquals(13.50, $item->subTotal(false)); 222 | $this->assertEquals(.88, $this->laracart->taxTotal(false)); 223 | } 224 | 225 | /** 226 | * Test Tax in case the sub sub item is untaxed but sub item is taxed. 227 | */ 228 | public function testAddTaxedSubSubItemUntaxedSubItemTaxed() 229 | { 230 | $item = $this->addItem(1, 1, true); 231 | 232 | $subItem = new \LukePOLO\LaraCart\CartItem('itemId', 'test sub item', 1, 10, [], true); 233 | 234 | $subItem->addSubItem([ 235 | 'items' => [ 236 | // not taxable 237 | new \LukePOLO\LaraCart\CartItem('itemId', 'test sub sub item', 1, 10, [], false), 238 | ], 239 | ]); 240 | 241 | $item->addSubItem([ 242 | 'items' => [ 243 | $subItem, 244 | ], 245 | ]); 246 | 247 | $this->assertEquals(21.00, $item->subTotal(false)); 248 | $this->assertEquals(0.77, $this->laracart->taxTotal(false)); 249 | } 250 | 251 | public function testSearchSubItems() 252 | { 253 | $item = $this->addItem(2, 2, false); 254 | 255 | $subItem = $item->addSubItem([ 256 | 'size' => 'XXL', 257 | 'price' => 2.50, 258 | 'taxable' => true, 259 | 'items' => [ 260 | new \LukePOLO\LaraCart\CartItem('itemId', 'test item', 1, 10, [ 261 | 'amItem' => true, 262 | ], true), 263 | ], 264 | ]); 265 | 266 | $this->assertCount(0, $item->searchForSubItem(['size' => 'XL'])); 267 | 268 | $itemsFound = $item->searchForSubItem(['size' => 'XXL']); 269 | 270 | $this->assertCount(1, $itemsFound); 271 | 272 | $itemFound = $itemsFound[0]; 273 | 274 | $this->assertEquals($subItem->getHash(), $itemFound->getHash()); 275 | $this->assertEquals($subItem->size, $itemFound->size); 276 | } 277 | 278 | public function testDefaultTaxOnSubItem() 279 | { 280 | $item = $this->addItem(1, 0); 281 | 282 | $item->addSubItem([ 283 | 'size' => 'XXL', 284 | 'price' => 10.00, 285 | ]); 286 | 287 | $this->assertEquals(0.7, $this->laracart->taxTotal(false)); 288 | } 289 | 290 | public function testDifferentTaxtionsOnSubItems() 291 | { 292 | $item = $this->addItem(1, 10, true, [ 293 | 'tax' => .01, 294 | ]); 295 | 296 | $item->addSubItem([ 297 | 'size' => 'XXL', 298 | 'price' => 10.00, 299 | 'taxable' => true, 300 | 'tax' => .02, 301 | ]); 302 | 303 | $this->assertEquals(0.30, $this->laracart->taxTotal(false)); 304 | } 305 | 306 | public function testTaxSumary() 307 | { 308 | $item = $this->addItem(1, 10, true, [ 309 | 'tax' => .01, 310 | ]); 311 | 312 | $item->addSubItem([ 313 | 'size' => 'XXL', 314 | 'price' => 10.00, 315 | 'taxable' => true, 316 | 'tax' => .02, 317 | ]); 318 | 319 | // Flatten the taxSummary output 320 | $taxSummary = $item->taxSummary(); 321 | $flat = []; 322 | foreach ($taxSummary as $qtySummary) { 323 | foreach ($qtySummary as $taxRate => $amount) { 324 | if (!isset($flat[$taxRate])) { 325 | $flat[$taxRate] = 0; 326 | } 327 | $flat[$taxRate] += $amount; 328 | } 329 | } 330 | 331 | $this->assertEquals([ 332 | '0.01' => .10, 333 | '0.02' => .20, 334 | ], $flat); 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/CartItem.php: -------------------------------------------------------------------------------- 1 | 0. 48 | */ 49 | public $discounted = []; 50 | 51 | /** 52 | * CartItem constructor. 53 | * 54 | * @param $id 55 | * @param $name 56 | * @param int $qty 57 | * @param string $price 58 | * @param array $options 59 | * @param bool $taxable 60 | * @param bool|false $lineItem 61 | */ 62 | public function __construct($id, $name, $qty, $price, $options = [], $taxable = true, $lineItem = false) 63 | { 64 | $this->id = $id; 65 | $this->qty = $qty; 66 | $this->name = $name; 67 | $this->taxable = $taxable; 68 | $this->lineItem = $lineItem; 69 | $this->price = (config('laracart.prices_in_cents', false) === true ? intval($price) : floatval($price)); 70 | $this->tax = config('laracart.tax'); 71 | $this->itemModel = config('laracart.item_model', null); 72 | $this->itemModelRelations = config('laracart.item_model_relations', []); 73 | $this->excludeFromHash = config('laracart.exclude_from_hash', []); 74 | 75 | foreach ($options as $option => $value) { 76 | $this->$option = $value; 77 | } 78 | } 79 | 80 | /** 81 | * Generates a hash based on the cartItem array. 82 | * 83 | * @param bool $force 84 | * 85 | * @return string itemHash 86 | */ 87 | public function generateHash($force = false) 88 | { 89 | if ($this->lineItem === false) { 90 | $this->itemHash = null; 91 | 92 | $cartItemArray = (array) clone $this; 93 | 94 | // Exclude private and protected properties. https://www.php.net/manual/en/language.types.array.php#language.types.array.casting 95 | foreach ($cartItemArray as $key => $value) { 96 | if ($key[0] === "\0") { 97 | unset($cartItemArray[$key]); 98 | } 99 | } 100 | 101 | unset($cartItemArray['discounted']); 102 | unset($cartItemArray['options']['qty']); 103 | unset($cartItemArray['options']['model']); 104 | 105 | foreach ($this->excludeFromHash as $option) { 106 | unset($cartItemArray['options'][$option]); 107 | } 108 | 109 | ksort($cartItemArray['options']); 110 | 111 | $this->itemHash = app(LaraCart::HASH)->hash($cartItemArray); 112 | } elseif ($force || empty($this->itemHash) === true) { 113 | $this->itemHash = app(LaraCart::RANHASH); 114 | } 115 | 116 | app('events')->dispatch( 117 | 'laracart.updateItem', 118 | [ 119 | 'item' => $this, 120 | 'newHash' => $this->itemHash, 121 | ] 122 | ); 123 | 124 | return $this->itemHash; 125 | } 126 | 127 | /** 128 | * Gets the hash for the item. 129 | * 130 | * @return mixed 131 | */ 132 | public function getHash() 133 | { 134 | return $this->itemHash; 135 | } 136 | 137 | /** 138 | * Search for matching options on the item. 139 | * 140 | * @return mixed 141 | */ 142 | public function find($data) 143 | { 144 | foreach ($data as $key => $value) { 145 | if ($this->$key !== $value) { 146 | return false; 147 | } 148 | } 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * Finds a sub item by its hash. 155 | * 156 | * @param $subItemHash 157 | * 158 | * @return mixed 159 | */ 160 | public function findSubItem($subItemHash) 161 | { 162 | return Arr::get($this->subItems, $subItemHash); 163 | } 164 | 165 | /** 166 | * Adds an sub item to a item. 167 | * 168 | * @param array $subItem 169 | * 170 | * @return CartSubItem 171 | */ 172 | public function addSubItem(array $subItem) 173 | { 174 | $subItem = new CartSubItem($subItem); 175 | 176 | $this->subItems[$subItem->getHash()] = $subItem; 177 | 178 | $this->update(); 179 | 180 | return $subItem; 181 | } 182 | 183 | /** 184 | * Removes a sub item from the item. 185 | * 186 | * @param $subItemHash 187 | */ 188 | public function removeSubItem($subItemHash) 189 | { 190 | unset($this->subItems[$subItemHash]); 191 | 192 | $this->update(); 193 | } 194 | 195 | public function getPrice() 196 | { 197 | return $this->price; 198 | } 199 | 200 | /** 201 | * Gets the price of the item with or without tax, with the proper format. 202 | * 203 | * @return string 204 | */ 205 | public function total() 206 | { 207 | $total = 0; 208 | 209 | if ($this->active) { 210 | for ($qty = 0; $qty < $this->qty; $qty++) { 211 | $total += LaraCart::formatMoney($this->subTotalPerItem(false) + array_sum($this->taxSummary()[$qty]), null, null, false); 212 | } 213 | 214 | $total -= $this->getDiscount(false); 215 | 216 | if ($total < 0) { 217 | $total = 0; 218 | } 219 | } 220 | 221 | return $total; 222 | } 223 | 224 | public function taxTotal() 225 | { 226 | $total = 0; 227 | 228 | foreach ($this->taxSummary() as $itemSummary) { 229 | $total += array_sum($itemSummary); 230 | } 231 | 232 | return $total; 233 | } 234 | 235 | /** 236 | * Gets the sub total of the item based on the qty. 237 | * 238 | * @param bool $format 239 | * 240 | * @return float|string 241 | */ 242 | public function subTotal() 243 | { 244 | return $this->subTotalPerItem() * $this->qty; 245 | } 246 | 247 | public function subTotalPerItem() 248 | { 249 | $subTotal = $this->active ? ($this->price + $this->subItemsTotal()) : 0; 250 | 251 | return $subTotal; 252 | } 253 | 254 | /** 255 | * Gets the totals for the options. 256 | * 257 | * @return float 258 | */ 259 | public function subItemsTotal() 260 | { 261 | $total = 0; 262 | 263 | foreach ($this->subItems as $subItem) { 264 | $total += $subItem->subTotal(false); 265 | } 266 | 267 | return $total; 268 | } 269 | 270 | /** 271 | * Gets the discount of an item. 272 | * 273 | * @return string 274 | */ 275 | public function getDiscount() 276 | { 277 | return array_sum($this->discounted); 278 | } 279 | 280 | /** 281 | * @param CouponContract $coupon 282 | * 283 | * @return $this 284 | */ 285 | public function addCoupon(CouponContract $coupon) 286 | { 287 | $coupon->appliedToCart = false; 288 | app('laracart')->addCoupon($coupon); 289 | $this->coupon = $coupon; 290 | 291 | return $this; 292 | } 293 | 294 | public function taxSummary() 295 | { 296 | $taxed = []; 297 | // tax item by item 298 | for ($qty = 0; $qty < $this->qty; $qty++) { 299 | // keep track of what is discountable 300 | $discountable = $this->discounted[$qty] ?? 0; 301 | $price = ($this->taxable ? $this->price : 0); 302 | 303 | $taxable = $price - ($discountable > 0 ? $discountable : 0); 304 | // track what has been discounted so far 305 | $discountable = $discountable - $price; 306 | 307 | $taxed[$qty] = []; 308 | if ($taxable > 0) { 309 | if (!isset($taxed[$qty][(string) $this->tax])) { 310 | $taxed[$qty][(string) $this->tax] = 0; 311 | } 312 | $taxed[$qty][(string) $this->tax] += $taxable * $this->tax; 313 | } 314 | 315 | // tax sub item item by sub item 316 | foreach ($this->subItems as $subItem) { 317 | $subItemTaxable = 0; 318 | for ($subItemQty = 0; $subItemQty < ($subItem->qty || 1); $subItemQty++) { 319 | $subItemPrice = ($subItem->taxable ?? true) ? $subItem->price : 0; 320 | $subItemTaxable = $subItemPrice - ($discountable > 0 ? $discountable : 0); 321 | $discountable = $discountable - $subItemPrice; 322 | } 323 | 324 | if ($subItemTaxable > 0) { 325 | if (!isset($taxed[$qty][(string) $subItem->tax])) { 326 | $taxed[$qty][(string) $subItem->tax] = 0; 327 | } 328 | $taxed[$qty][(string) $subItem->tax] += $subItemTaxable * $subItem->tax; 329 | } 330 | 331 | // discount sub items ... items 332 | if (isset($subItem->items)) { 333 | foreach ($subItem->items as $item) { 334 | if ($item->taxable) { 335 | foreach ($item->taxSummary() as $itemTaxSummary) { 336 | foreach ($itemTaxSummary as $taxRate => $amount) { 337 | if (!isset($taxed[$qty][(string) $taxRate])) { 338 | $taxed[$qty][(string) $taxRate] = 0; 339 | } 340 | $taxed[$qty][(string) $taxRate] += $amount; 341 | } 342 | } 343 | } 344 | } 345 | } 346 | } 347 | } 348 | 349 | return $taxed; 350 | } 351 | 352 | /** 353 | * Sets the related model to the item. 354 | * 355 | * @param $itemModel 356 | * @param array $relations 357 | * 358 | * @throws ModelNotFound 359 | */ 360 | public function setModel($itemModel, $relations = []) 361 | { 362 | if (!class_exists($itemModel)) { 363 | throw new ModelNotFound('Could not find relation model'); 364 | } 365 | 366 | $this->itemModel = $itemModel; 367 | $this->itemModelRelations = $relations; 368 | } 369 | 370 | /** 371 | * Gets the items model class. 372 | */ 373 | public function getItemModel() 374 | { 375 | return $this->itemModel; 376 | } 377 | 378 | /** 379 | * Returns a Model. 380 | * 381 | * @throws ModelNotFound 382 | */ 383 | public function getModel() 384 | { 385 | $itemModel = (new $this->itemModel())->with($this->itemModelRelations)->find($this->id); 386 | 387 | if (empty($itemModel)) { 388 | throw new ModelNotFound('Could not find the item model for '.$this->id); 389 | } 390 | 391 | return $itemModel; 392 | } 393 | 394 | /** 395 | * A way to find sub items. 396 | * 397 | * @param $data 398 | * 399 | * @return array 400 | */ 401 | public function searchForSubItem($data) 402 | { 403 | $matches = []; 404 | 405 | foreach ($this->subItems as $subItem) { 406 | if ($subItem->find($data)) { 407 | $matches[] = $subItem; 408 | } 409 | } 410 | 411 | return $matches; 412 | } 413 | 414 | public function disable() 415 | { 416 | $this->active = false; 417 | $this->update(); 418 | } 419 | 420 | public function enable() 421 | { 422 | $this->active = true; 423 | $this->update(); 424 | } 425 | 426 | public function update() 427 | { 428 | $this->generateHash(); 429 | app('laracart')->update(); 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /tests/ItemsTest.php: -------------------------------------------------------------------------------- 1 | addItem(); 20 | 21 | Event::assertDispatched('laracart.addItem', function ($e, $item) use ($cartItem) { 22 | return $item === $cartItem; 23 | }); 24 | 25 | $this->assertEquals(1, Event::dispatched('laracart.addItem')->count()); 26 | 27 | $this->addItem(); 28 | 29 | $this->assertEquals(1, $this->laracart->count(false)); 30 | $this->assertEquals(2, $this->laracart->count()); 31 | } 32 | 33 | /** 34 | * Test if we can increment a quantity to an item. 35 | */ 36 | public function testIncrementItem() 37 | { 38 | $item = $this->addItem(); 39 | $itemHash = $item->getHash(); 40 | $this->laracart->increment($itemHash); 41 | 42 | $this->assertEquals(2, $this->laracart->count()); 43 | } 44 | 45 | /** 46 | * Test if we can decrement a quantity to an item. 47 | */ 48 | public function testDecrementItem() 49 | { 50 | $item = $this->addItem(); 51 | $itemHash = $item->getHash(); 52 | $this->laracart->increment($itemHash); 53 | $this->laracart->decrement($itemHash); 54 | 55 | $this->assertEquals(1, $this->laracart->count()); 56 | 57 | $this->laracart->decrement($itemHash); 58 | 59 | $this->assertEquals(0, $this->laracart->count()); 60 | } 61 | 62 | /** 63 | * Test if we can decrement an item with a quantity of 1 (= delete item). 64 | */ 65 | public function testDecrementUniqueItem() 66 | { 67 | $item = $this->addItem(); 68 | $itemHash = $item->getHash(); 69 | $this->laracart->decrement($itemHash); 70 | 71 | $this->assertEquals(null, $this->laracart->getItem($itemHash)); 72 | } 73 | 74 | /** 75 | * Tests when we add multiples of the same item it updates the qty properly. 76 | */ 77 | public function testItemQtyUpdate() 78 | { 79 | $item = $this->addItem(); 80 | $itemHash = $item->getHash(); 81 | $this->addItem(); 82 | $this->addItem(); 83 | $this->addItem(); 84 | $this->addItem(); 85 | $this->addItem(); 86 | $this->addItem(); 87 | 88 | $this->assertEquals(7, $item->qty); 89 | $this->assertEquals($itemHash, $item->getHash()); 90 | 91 | $options = [ 92 | 'a' => 2, 93 | 'b' => 1, 94 | ]; 95 | 96 | $item = $this->addItem(1, 1, false, $options); 97 | $this->addItem(1, 1, false, array_reverse($options)); 98 | 99 | $this->assertEquals(2, $item->qty); 100 | } 101 | 102 | /** 103 | * Test if we can add an line item to the cart. 104 | */ 105 | public function testAddLineItem() 106 | { 107 | $this->addItem(); 108 | $this->addItem(); 109 | 110 | $this->laracart->addLine( 111 | 'itemID', 112 | 'Testing Item', 113 | 1, 114 | '1', 115 | [ 116 | 'b_test' => 'option_1', 117 | 'a_test' => 'option_2', 118 | ] 119 | ); 120 | 121 | $this->assertEquals(2, $this->laracart->count(false)); 122 | $this->assertEquals(3, $this->laracart->count()); 123 | } 124 | 125 | /** 126 | * Test getting an item from the cart. 127 | */ 128 | public function testGetItem() 129 | { 130 | $item = $this->addItem(); 131 | $this->assertEquals($item, $this->laracart->getItem($item->getHash())); 132 | } 133 | 134 | /** 135 | * Test updating the item. 136 | */ 137 | public function testUpdateItem() 138 | { 139 | $item = $this->addItem(); 140 | 141 | Event::fake(); 142 | 143 | $this->laracart->updateItem($item->getHash(), 'qty', 4); 144 | 145 | Event::assertDispatched('laracart.updateItem', function ($e, $eventItem) use ($item) { 146 | return $eventItem['item'] === $item && $eventItem['newHash'] === $item->getHash(); 147 | }); 148 | 149 | $this->assertEquals(1, Event::dispatched('laracart.updateItem')->count()); 150 | $this->assertEquals(4, $item->qty); 151 | } 152 | 153 | /** 154 | * Test getting all the items from the cart. 155 | */ 156 | public function testGetItems() 157 | { 158 | $this->addItem(); 159 | $this->addItem(); 160 | 161 | $items = $this->laracart->getItems(); 162 | 163 | $this->assertCount(1, $items); 164 | 165 | $this->containsOnlyInstancesOf(LukePOLO\LaraCart\CartItem::class, $items); 166 | } 167 | 168 | /** 169 | * Test the price and qty based on the item. 170 | */ 171 | public function testItemPriceAndQty() 172 | { 173 | $item = $this->addItem(3, 10); 174 | 175 | $this->assertEquals(10, $item->getPrice()); 176 | $this->assertEquals(3, $item->qty); 177 | $this->assertEquals(30, $item->subTotal(false)); 178 | $this->assertEqualsWithDelta(32.1, $item->total(false), 0.0001); 179 | } 180 | 181 | /** 182 | * Test the prices in cents based on the item. 183 | */ 184 | public function testItemPriceInCents() 185 | { 186 | $this->app['config']->set('laracart.prices_in_cents', true); 187 | $item = $this->addItem(3, 1000); 188 | 189 | $this->assertEquals(3210, $item->total(false)); 190 | $this->assertEquals(3000, $item->subTotal(false)); 191 | } 192 | 193 | /** 194 | * Test removing an item from the cart. 195 | */ 196 | public function testRemoveItem() 197 | { 198 | $item = $this->addItem(); 199 | 200 | $this->laracart->removeItem($item->getHash()); 201 | 202 | $this->assertEmpty($this->laracart->getItem($item->getHash())); 203 | } 204 | 205 | /** 206 | * Test seeing a valid and invalid price. 207 | */ 208 | public function testSetPrice() 209 | { 210 | $item = $this->addItem(); 211 | 212 | try { 213 | $item->price = 'a'; 214 | $this->expectException(\LukePOLO\LaraCart\Exceptions\InvalidPrice::class); 215 | } catch (\LukePOLO\LaraCart\Exceptions\InvalidPrice $e) { 216 | $this->assertEquals('The price must be a valid number', $e->getMessage()); 217 | } 218 | } 219 | 220 | /** 221 | * Test seeing a valid and invalid qty. 222 | */ 223 | public function testSetQty() 224 | { 225 | $item = $this->addItem(); 226 | 227 | $item->qty = 3; 228 | $this->assertEquals(3, $item->qty); 229 | 230 | $item->qty = 1.5; 231 | $this->assertEquals(1.5, $item->qty); 232 | 233 | try { 234 | $item->qty = 'a'; 235 | $this->expectException(\LukePOLO\LaraCart\Exceptions\InvalidPrice::class); 236 | } catch (\LukePOLO\LaraCart\Exceptions\InvalidQuantity $e) { 237 | $this->assertEquals('The quantity must be a valid number', $e->getMessage()); 238 | } 239 | 240 | try { 241 | $item->qty = 'a'; 242 | $this->expectException(\LukePOLO\LaraCart\Exceptions\InvalidQuantity::class); 243 | } catch (\LukePOLO\LaraCart\Exceptions\InvalidQuantity $e) { 244 | $this->assertEquals('The quantity must be a valid number', $e->getMessage()); 245 | } 246 | 247 | try { 248 | $item->qty = -1; 249 | $this->expectException(\LukePOLO\LaraCart\Exceptions\InvalidQuantity::class); 250 | } catch (\LukePOLO\LaraCart\Exceptions\InvalidQuantity $e) { 251 | $this->assertEquals('The quantity must be a valid number', $e->getMessage()); 252 | } 253 | } 254 | 255 | /** 256 | * Tests the different taxes on items. 257 | */ 258 | public function testDifferentTaxes() 259 | { 260 | $item = $this->addItem(); 261 | $item2 = $this->addItem(1, 1, true, [ 262 | 'tax' => '.05', 263 | ]); 264 | 265 | $this->assertEquals(true, $item->tax !== $item2->tax); 266 | 267 | $this->assertEquals('0.12', $this->laracart->taxTotal(false)); 268 | } 269 | 270 | /** 271 | * Test that an item can be found by the value of an option. 272 | */ 273 | public function testFindingAnItemByOptionSucceeds() 274 | { 275 | $item1 = $this->addItem(1, 1, true, [ 276 | 'key1' => 'matching', 277 | 'key2' => 'value1', 278 | ]); 279 | 280 | $item2 = $this->addItem(1, 1, true, [ 281 | 'key1' => 'notmatching', 282 | 'key2' => 'value2', 283 | ]); 284 | 285 | $result1 = $this->laracart->find(['key1' => 'matching']); 286 | $result2 = $this->laracart->find(['key2' => 'value2']); 287 | 288 | $this->assertEquals($item1->key1, $result1->key1); 289 | $this->assertEquals($item1->getHash(), $result1->getHash()); 290 | 291 | $this->assertEquals($item2->key2, $result2->key2); 292 | $this->assertEquals($item2->getHash(), $result2->getHash()); 293 | } 294 | 295 | /** 296 | * Test that an item is not found by the value of an option when it does not exist. 297 | */ 298 | public function testFindingAnItemByOptionFails() 299 | { 300 | $this->addItem(1, 1, true, [ 301 | 'key1' => 'notmatching', 302 | ]); 303 | 304 | $this->addItem(1, 1, true, [ 305 | 'key2' => 'notmatching', 306 | ]); 307 | 308 | $this->assertNull($this->laracart->find(['key1' => 'matching'])); 309 | } 310 | 311 | /** 312 | * Test that multiple matching items are found by the value of an option. 313 | */ 314 | public function testFindingAnItemReturnsMultipleMatches() 315 | { 316 | $item1 = $this->addItem(1, 1, true, [ 317 | 'key1' => 'matching', 318 | 'key2' => 'value1', 319 | ]); 320 | 321 | $item2 = $this->addItem(1, 1, true, [ 322 | 'key1' => 'matching', 323 | 'key2' => 'value2', 324 | ]); 325 | 326 | $item3 = $this->addItem(1, 1, true, [ 327 | 'key1' => 'nomatch', 328 | ]); 329 | 330 | $results = $this->laracart->find(['key1' => 'matching']); 331 | 332 | $this->assertCount(2, $results); 333 | $this->assertEquals($item1->key1, $results[0]->key1); 334 | $this->assertEquals($item1->getHash(), $results[0]->getHash()); 335 | $this->assertEquals($item2->key1, $results[1]->key1); 336 | $this->assertEquals($item2->getHash(), $results[1]->getHash()); 337 | } 338 | 339 | /** 340 | * Test that an multiple matching items are found by the value of an option. 341 | */ 342 | public function testFindingAnItemOnAnEmptyCartReturnsNoMatches() 343 | { 344 | $this->assertNull($this->laracart->find(['key1' => 'matching'])); 345 | } 346 | 347 | /** 348 | * Test an item is returned when finding multiple criteria. 349 | */ 350 | public function testFindingAnItemWithMultipleCriteria() 351 | { 352 | $item1 = $this->addItem(1, 1, true, [ 353 | 'key1' => 'value1', 354 | 'key2' => 'value2', 355 | ]); 356 | 357 | $item2 = $this->addItem(1, 1, true, [ 358 | 'key1' => 'value1', 359 | 'key2' => 'value3', 360 | ]); 361 | 362 | $result1 = $this->laracart->find(['key1' => 'value1', 'key2' => 'value2']); 363 | $result2 = $this->laracart->find(['key1' => 'value1', 'key2' => 'value3']); 364 | 365 | $this->assertEquals($item1->key2, $result1->key2); 366 | $this->assertEquals($item1->getHash(), $result1->getHash()); 367 | 368 | $this->assertEquals($item2->key2, $result2->key2); 369 | $this->assertEquals($item2->getHash(), $result2->getHash()); 370 | 371 | $this->assertNull($this->laracart->find(['key1' => 'value2', 'key2' => 'value2'])); 372 | 373 | $result3 = $this->laracart->find(['key1' => 'value1']); 374 | $this->assertCount(2, $result3); 375 | $this->assertEquals($item1->getHash(), $result3[0]->getHash()); 376 | $this->assertEquals($item2->getHash(), $result3[1]->getHash()); 377 | } 378 | 379 | /** 380 | * Test an item is found searching by name. 381 | */ 382 | public function testFindingAnItemByName() 383 | { 384 | $item1 = $this->laracart->add('item1234', 'My Item', 1, 2, [ 385 | 'key1' => 'value1', 386 | 'key2' => 'value2', 387 | ]); 388 | 389 | $item2 = $this->addItem(1, 1, true, [ 390 | 'key1' => 'value1', 391 | 'key2' => 'value3', 392 | ]); 393 | 394 | $result = $this->laracart->find(['name' => 'My Item']); 395 | 396 | $this->assertEquals($item1->name, $result->name); 397 | $this->assertEquals($item1->getHash(), $result->getHash()); 398 | 399 | $this->assertNull($this->laracart->find(['name' => 'My Item', 'key2' => 'nomatch'])); 400 | } 401 | 402 | public function testQtyUpdate() 403 | { 404 | $item = $this->addItem(); 405 | 406 | $this->assertEquals(1, $this->laracart->count(false)); 407 | 408 | $this->laracart->updateItem($item->getHash(), 'qty', 0); 409 | 410 | $this->assertEquals(0, $this->laracart->count(false)); 411 | } 412 | 413 | public function testfindItemById() 414 | { 415 | $item = $this->addItem(); 416 | $item->id = 123; 417 | 418 | $this->assertEquals($item, $this->laracart->find([ 419 | 'id' => 123, 420 | ])); 421 | } 422 | 423 | public function testTaxationTotal() 424 | { 425 | $this->addItem(1, 8.33, true, [ 426 | 'tax' => '.2', 427 | ]); 428 | 429 | $this->assertEquals(10.00, $this->laracart->total(false)); 430 | 431 | $this->addItem(1, 8.33, true, [ 432 | 'tax' => '.2', 433 | ]); 434 | 435 | $this->assertEquals(16.66, $this->laracart->netTotal(false)); 436 | 437 | $this->assertEquals(20.00, $this->laracart->total(false)); 438 | 439 | $this->addItem(1, 8.33, true, [ 440 | 'tax' => '.2', 441 | ]); 442 | 443 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed( 444 | '8.33 OFF', 445 | 8.33 446 | ); 447 | 448 | $this->laracart->addCoupon($fixedCoupon); 449 | 450 | $this->assertEquals(20.00, $this->laracart->total(false)); 451 | } 452 | 453 | public function testSeparateTaxationTotal() 454 | { 455 | $this->addItem(1, 8.33, 1, [ 456 | 'tax' => '.2', 457 | ]); 458 | 459 | $this->addItem(1, 8.33, 1, [ 460 | 'tax' => '.2', 461 | 'some' => 'test', 462 | 'name' => '12313', 463 | ]); 464 | 465 | $this->assertEquals(20.00, $this->laracart->total(false)); 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /tests/TotalsTest.php: -------------------------------------------------------------------------------- 1 | addItem(1, 10); 16 | 17 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed( 18 | '10OFF', 19 | 10 20 | ); 21 | 22 | $this->laracart->addCoupon($fixedCoupon); 23 | 24 | $this->assertEquals('$10.00', $this->laracart->discountTotal()); 25 | $this->assertEquals(10, $this->laracart->discountTotal(false)); 26 | 27 | $this->assertEquals(0, $this->laracart->total(false)); 28 | } 29 | 30 | /** 31 | * Test total discounts when using the pricing_in_cents config setting. 32 | */ 33 | public function testdiscountTotalInCents() 34 | { 35 | $this->app['config']->set('laracart.prices_in_cents', true); 36 | $this->addItem(1, 1000); 37 | 38 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed( 39 | '10OFF', 40 | 1000 41 | ); 42 | 43 | $this->laracart->addCoupon($fixedCoupon); 44 | 45 | $this->assertEquals('$10.00', $this->laracart->discountTotal()); 46 | $this->assertEquals(1000, $this->laracart->discountTotal(false)); 47 | 48 | $this->assertEquals(0, $this->laracart->total(false)); 49 | } 50 | 51 | /** 52 | * Test total taxes. 53 | */ 54 | public function testTaxTotal() 55 | { 56 | $this->addItem(); 57 | 58 | $this->assertEquals('$0.07', $this->laracart->taxTotal()); 59 | $this->assertEquals('0.07', $this->laracart->taxTotal(false)); 60 | } 61 | 62 | /** 63 | * Test total taxes when using the pricing_in_cents config setting. 64 | */ 65 | public function testTaxTotalInCents() 66 | { 67 | $this->app['config']->set('laracart.prices_in_cents', true); 68 | $this->addItem(1, 100); 69 | 70 | $this->assertEquals('$0.07', $this->laracart->taxTotal()); 71 | $this->assertEquals(7, $this->laracart->taxTotal(false)); 72 | } 73 | 74 | /** 75 | * Test getting all the fees. 76 | */ 77 | public function testFeeTotals() 78 | { 79 | $this->laracart->addFee('test', 5); 80 | $this->laracart->addFee('test_2', 20); 81 | 82 | $this->assertEquals('$25.00', $this->laracart->feeSubTotal()); 83 | $this->assertEquals(25, $this->laracart->feeSubTotal(false)); 84 | } 85 | 86 | /** 87 | * Test getting a sub total (without tax). 88 | */ 89 | public function testSubTotal() 90 | { 91 | $item = $this->addItem(1, 24); 92 | 93 | $this->assertEquals('$24.00', $this->laracart->subTotal()); 94 | $this->assertEquals('24.00', $this->laracart->subTotal(false)); 95 | 96 | $item->qty = 5; 97 | 98 | $this->assertEquals('120.00', $this->laracart->subTotal(false)); 99 | } 100 | 101 | /** 102 | * Test getting the final total (with tax). 103 | */ 104 | public function testTotal() 105 | { 106 | $this->addItem(); 107 | 108 | $this->assertEquals('$1.07', $this->laracart->total()); 109 | $this->assertEquals('1.07', $this->laracart->total(false)); 110 | } 111 | 112 | /** 113 | * Test getting the final total (with tax) when using the pricing_in_cents config setting. 114 | */ 115 | public function testTotalInCents() 116 | { 117 | $this->app['config']->set('laracart.prices_in_cents', true); 118 | $this->addItem(1, 100); 119 | 120 | $this->assertEquals('$1.07', $this->laracart->total()); 121 | $this->assertEquals(107, $this->laracart->total(false)); 122 | } 123 | 124 | /** 125 | * Test the taxable fees total. 126 | */ 127 | public function testTaxableFees() 128 | { 129 | $this->app['config']->set('laracart.fees_taxable', true); 130 | $this->laracart->addFee('test_2', 1, true, ['tax' => 0.07]); 131 | 132 | $this->assertEquals(1, $this->laracart->feeSubTotal(false)); 133 | 134 | $this->assertEquals('0.07', $this->laracart->taxTotal(false)); 135 | } 136 | 137 | /** 138 | * Test making sure items are taxable and not taxable. 139 | */ 140 | public function testTaxableItems() 141 | { 142 | $this->addItem(); 143 | $item = $this->addItem(1, 2, false); 144 | 145 | // only 1 dollar is taxable! 146 | $this->assertEquals('3.07', $this->laracart->total(false)); 147 | 148 | $item->qty = 5; 149 | 150 | // 3 * 5 = 15 - 5 = 10 , only 10 is taxable 151 | 152 | // only 5 dollar is taxable! 153 | $this->assertEquals('11.00', $this->laracart->subTotal(false)); 154 | $this->assertEquals('11.07', $this->laracart->total(false)); 155 | } 156 | 157 | /** 158 | * Test taxable item with taxable fees. 159 | */ 160 | public function testTotalTaxableItemTaxableFees() 161 | { 162 | $tax = .10; 163 | $priceItem = 10; 164 | $this->addItem(1, $priceItem, true, ['tax' => $tax]); 165 | $this->assertEquals(11, $this->laracart->total(false)); 166 | 167 | $this->app['config']->set('laracart.fees_taxable', true); 168 | $fee = 10; 169 | $this->laracart->addFee('test', $fee, true, ['tax' => $tax]); 170 | 171 | $this->assertEquals($priceItem, $this->laracart->feeSubTotal(false)); 172 | $this->assertEquals($priceItem, $this->laracart->subTotal(false)); 173 | $this->assertEquals($priceItem + $fee, $this->laracart->netTotal(false)); 174 | $taxTotal = ($priceItem * .10) + ($fee * .10); 175 | $this->assertEquals($taxTotal, $this->laracart->taxTotal(false)); 176 | $this->assertEquals($priceItem + $fee + $taxTotal, $this->laracart->total(false)); 177 | } 178 | 179 | /** 180 | * Test NOT taxable item with taxable fees. 181 | */ 182 | public function testTotalNotTaxableItemTaxableFees() 183 | { 184 | $this->app['config']->set('laracart.fees_taxable', true); 185 | 186 | $tax = .20; 187 | $priceItem = 5; 188 | $priceFee = 2; 189 | 190 | $this->addItem(1, $priceItem, false); 191 | $this->laracart->addFee('test', $priceFee, true, ['tax' => $tax]); 192 | 193 | $this->assertEquals('2.00', $this->laracart->feeSubTotal(false, true)); 194 | $this->assertEquals('5.00', $this->laracart->subTotal(false, true)); 195 | $this->assertEquals('7.40', $this->laracart->total(false)); 196 | } 197 | 198 | /** 199 | * Test taxable item with NOT taxable fees. 200 | */ 201 | public function testTotalTaxableItemNotTaxableFees() 202 | { 203 | $tax = .20; 204 | $priceItem = 5; 205 | $priceFee = 2; 206 | 207 | $item = $this->addItem(1, $priceItem, true, ['tax' => $tax]); 208 | $this->laracart->addFee('test', $priceFee, false); 209 | 210 | $this->assertEquals('2.00', $this->laracart->feeSubTotal(false)); 211 | $this->assertEquals('5.00', $this->laracart->subTotal(false)); 212 | $this->assertEquals('8.00', $this->laracart->total(false)); 213 | } 214 | 215 | /** 216 | * Test NOT taxable item with NOT taxable fees. 217 | */ 218 | public function testTotalNotTaxableItemNotTaxableFees() 219 | { 220 | $tax = .20; 221 | $priceItem = 5; 222 | $priceFee = 2; 223 | 224 | $item = $this->addItem(1, $priceItem, false); 225 | $this->laracart->addFee('test', $priceFee, false); 226 | 227 | $this->assertEquals('2.00', $this->laracart->feeSubTotal(false)); 228 | $this->assertEquals('5.00', $this->laracart->subTotal(false)); 229 | $this->assertEquals('7.00', $this->laracart->total(false)); 230 | } 231 | 232 | /** 233 | * Test NOT taxable item with taxable fees. 234 | */ 235 | public function testTotalDifferentTaxItemAndFees() 236 | { 237 | $this->app['config']->set('laracart.fees_taxable', true); 238 | $taxItem = .20; 239 | $taxFee = .07; 240 | $priceItem = 5; 241 | $priceFee = 2; 242 | 243 | $this->addItem(1, $priceItem, true, ['tax' => $taxItem]); 244 | 245 | $this->assertEquals('5.00', $this->laracart->subTotal(false)); 246 | $this->assertEquals('6.00', $this->laracart->total(false)); 247 | 248 | $this->laracart->addFee('test', $priceFee, true, ['tax' => $taxFee]); 249 | $this->assertEquals('2.00', $this->laracart->feeSubTotal(false)); 250 | $this->assertEquals('5.00', $this->laracart->subTotal(false)); 251 | $this->assertEquals('7.00', $this->laracart->netTotal(false)); 252 | $this->assertEquals('8.14', $this->laracart->total(false)); 253 | } 254 | 255 | public function testActivateAndDeactivate() 256 | { 257 | $item = $this->addItem(); 258 | 259 | $this->assertEquals('1.07', $this->laracart->total(false)); 260 | 261 | $item->disable(); 262 | 263 | $this->assertEquals(0, $this->laracart->subTotal(false)); 264 | 265 | $item->enable(); 266 | 267 | $this->assertEquals('1.07', $this->laracart->total(false)); 268 | } 269 | 270 | public function testTotalWithoutTaxableFees() 271 | { 272 | $this->addItem(5); 273 | 274 | $this->assertEquals('5.00', $this->laracart->subTotal(false)); 275 | $this->assertEquals('5.35', $this->laracart->total(false)); 276 | 277 | $this->laracart->addFee('test', 1); 278 | 279 | $this->assertEquals('6.00', $this->laracart->netTotal(false)); 280 | $this->assertEquals('6.35', $this->laracart->total(false)); 281 | } 282 | 283 | public function testTaxTotalWithDiscounts() 284 | { 285 | $this->laracart->add(1, 'Test Product', 1, 100, ['tax' => 0.21]); 286 | 287 | $coupon = new LukePOLO\LaraCart\Coupons\Percentage('test', 0.05, [ 288 | 'name' => '5% off', 289 | 'description' => '5% off test', 290 | ]); 291 | 292 | $this->laracart->addCoupon($coupon); 293 | 294 | $this->assertEquals(100, $this->laracart->subTotal(false)); 295 | $this->assertEquals(5, $this->laracart->discountTotal(false)); 296 | $this->assertEquals(19.95, $this->laracart->taxTotal(false)); 297 | $this->assertEquals(114.95, $this->laracart->total(false)); 298 | } 299 | 300 | public function testDoubleDiscounts() 301 | { 302 | $item = $this->laracart->add(1, 'Test Product', 1, 100, ['tax' => 0.21]); 303 | 304 | $coupon = new LukePOLO\LaraCart\Coupons\Percentage('test', 0.05, [ 305 | 'name' => '5% off', 306 | 'description' => '5% off test', 307 | ]); 308 | 309 | $this->laracart->addCoupon($coupon); 310 | $coupon->setDiscountOnItem($item); 311 | 312 | $this->assertEquals(100, $this->laracart->subTotal(false)); 313 | $this->assertEquals(5, $this->laracart->discountTotal(false)); 314 | $this->assertEquals(95, $this->laracart->netTotal(false)); 315 | $this->assertEquals(19.95, $this->laracart->taxTotal(false)); 316 | $this->assertEquals(114.95, $this->laracart->total(false)); 317 | } 318 | 319 | public function testTaxationOnCoupons() 320 | { 321 | // Add to cart 322 | $this->laracart->add( 323 | 1, 324 | 'test', 325 | 52, 326 | 107.44, 327 | [ 328 | 'tax' => 0.21, 329 | ] 330 | ); 331 | 332 | $this->assertEquals(5586.88, $this->laracart->subTotal(false)); 333 | $this->assertEquals(0, $this->laracart->discountTotal(false)); 334 | $this->assertEquals(1173.24, $this->laracart->taxTotal(false)); 335 | $this->assertEquals(6760.00, $this->laracart->total(false)); 336 | 337 | // Test discount % 338 | $coupon = new LukePOLO\LaraCart\Coupons\Percentage('7.5%', 0.075); 339 | $this->laracart->addCoupon($coupon); 340 | 341 | $this->assertEquals(5586.88, $this->laracart->subTotal(false)); 342 | $this->assertEquals(419.02, $this->laracart->discountTotal(false)); 343 | $this->assertEquals(1085.25, $this->laracart->taxTotal(false)); 344 | $this->assertEquals(6253.10, $this->laracart->total(false)); 345 | 346 | $this->laracart->removeCoupons(); 347 | 348 | // Test discount fixed 349 | $coupon = new LukePOLO\LaraCart\Coupons\Fixed('100 euro', 100); 350 | $this->laracart->addCoupon($coupon); 351 | 352 | $this->assertEquals(5586.88, $this->laracart->subTotal(false)); 353 | $this->assertEquals(100, $this->laracart->discountTotal(false)); 354 | $this->assertEquals(1152.24, $this->laracart->taxTotal(false)); 355 | $this->assertEquals(6639.00, $this->laracart->total(false)); 356 | } 357 | 358 | public function testBasicTotalsWithItemTax() 359 | { 360 | $this->app['config']->set('laracart.tax', .19); 361 | 362 | /* @var \LukePOLO\LaraCart\CartItem $item */ 363 | $this->laracart->add( 364 | 1, 365 | 'Product with 19% Tax', 366 | 1, 367 | 100, 368 | [ 369 | \LukePOLO\LaraCart\CartItem::ITEM_TAX => .19, 370 | ] 371 | ); 372 | 373 | $this->assertEquals(100, $this->laracart->subTotal(false)); 374 | $this->assertEquals(0, $this->laracart->discountTotal(false)); 375 | $this->assertEquals(19, $this->laracart->taxTotal(false)); 376 | $this->assertEquals(119, $this->laracart->total(false)); 377 | } 378 | 379 | public function testDiscountsOnMultiQtyItems() 380 | { 381 | $this->laracart->emptyCart(); 382 | $this->laracart->destroyCart(); 383 | 384 | $item = $this->laracart->add(123, 'T-Shirt', 2, 100, ['tax' => .2], true); 385 | 386 | $coupon = new \LukePOLO\LaraCart\Coupons\Percentage('10%OFF', 0.10); 387 | $this->laracart->addCoupon($coupon); 388 | $coupon->setDiscountOnItem($item); 389 | 390 | $this->assertEquals($item->getDiscount(false), 20); 391 | $this->assertEquals($this->laracart->subTotal(false), 200); 392 | $this->assertEquals($this->laracart->discountTotal(false), 20); 393 | 394 | $this->assertEquals($this->laracart->taxTotal(false), 36); 395 | $this->assertEquals($this->laracart->total(false), 216); 396 | } 397 | 398 | /** 399 | * Test round of prices. Only the total value should be rounded. 400 | */ 401 | public function testRoundOnlyTotalValue() 402 | { 403 | $item = $this->addItem(); 404 | $item->addSubItem([ 405 | 'description' => 'First item', 406 | 'price' => 8.40336, 407 | 'qty' => 1, 408 | ]); 409 | 410 | $item->addSubItem([ 411 | 'description' => 'Second item', 412 | 'price' => 4.20168, 413 | 'qty' => 1, 414 | ]); 415 | $this->assertEquals(13.61, $this->laracart->subTotal(false)); 416 | } 417 | 418 | public function testCartTaxSumary() 419 | { 420 | $this->app['config']->set('laracart.fees_taxable', true); 421 | $item = $this->addItem(1, 10, true, [ 422 | 'tax' => .01, 423 | ]); 424 | 425 | $item->addSubItem([ 426 | 'size' => 'XXL', 427 | 'price' => 10.00, 428 | 'taxable' => true, 429 | 'tax' => .02, 430 | ]); 431 | 432 | $item = $this->addItem(1, 12, true, [ 433 | 'tax' => .01, 434 | ]); 435 | 436 | $item->addSubItem([ 437 | 'size' => 'XXL', 438 | 'price' => 10.00, 439 | 'taxable' => true, 440 | 'tax' => .02, 441 | ]); 442 | 443 | $this->laracart->addFee( 444 | 'cart fee', 445 | 5.00, 446 | true, 447 | [ 448 | 'tax' => .03, 449 | ] 450 | ); 451 | 452 | $this->assertEquals([ 453 | '0.01' => .22, 454 | '0.02' => .40, 455 | '0.03' => .15, 456 | ], $this->laracart->taxSummary()); 457 | } 458 | 459 | public function testQtyOnSubItems() 460 | { 461 | $item = $this->addItem(1, 0); 462 | 463 | $item->addSubItem([ 464 | 'description' => 'Ticket: Erwachsener', 465 | 'price' => 18.48739, 466 | 'qty' => 2, 467 | 'tax' => .19, 468 | ]); 469 | 470 | $this->assertEquals(40.49, $this->laracart->total(false)); 471 | } 472 | 473 | public function testSubTotalTaxRounding() 474 | { 475 | $item = $this->addItem(1, 0); 476 | 477 | $item->addSubItem([ 478 | 'description' => 'Ticket: Erwachsener', 479 | 'price' => 18.48739, 480 | 'qty' => 1, 481 | 'tax' => .19, 482 | ]); 483 | // 18.48739 + (18.48739 *.19) = 21.9999941 484 | 485 | $item->addSubItem([ 486 | 'description' => 'Ticket: Ermäßigt', 487 | 'price' => 16.80672, 488 | 'qty' => 1, 489 | 'tax' => .19, 490 | ]); 491 | 492 | // 16.80672 + (16.80672 *.19) = 19.9999968 493 | 494 | // 21.9999941 + 19.9999968 = 41.9999909 495 | 496 | $this->assertEquals(42.00, $this->laracart->total(false)); 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /tests/CouponsTest.php: -------------------------------------------------------------------------------- 1 | addItem(3, 1); 15 | 16 | try { 17 | $percentCoupon = new LukePOLO\LaraCart\Coupons\Percentage('10%OFF', '23'); 18 | $this->expectException(\LukePOLO\LaraCart\Exceptions\CouponException::class); 19 | } catch (\LukePOLO\LaraCart\Exceptions\CouponException $e) { 20 | $this->assertEquals('Invalid value for a percentage coupon. The value must be between 0 and 1.', $e->getMessage()); 21 | } 22 | } 23 | 24 | /** 25 | * Test the percentage coupons. 26 | */ 27 | public function testAddPercentageCoupon() 28 | { 29 | $this->addItem(3, 1); 30 | 31 | $percentCoupon = new LukePOLO\LaraCart\Coupons\Percentage('10%OFF', '.1'); 32 | 33 | $this->laracart->addCoupon($percentCoupon); 34 | 35 | $this->assertEquals($percentCoupon, $this->laracart->findCoupon('10%OFF')); 36 | 37 | $this->assertEquals('10%', $percentCoupon->displayValue()); 38 | $this->assertEquals('0.30', $this->laracart->discountTotal(false)); 39 | 40 | $this->assertCount(1, $this->laracart->getCoupons()); 41 | 42 | $this->assertEquals(3, $this->laracart->subTotal(false)); 43 | $this->assertEquals(.19, $this->laracart->taxTotal(false)); 44 | } 45 | 46 | /** 47 | * Test the percentage coupons on item with tax. 48 | */ 49 | public function testAddPercentageCouponOnTaxItem() 50 | { 51 | $item = $this->addItem(1, 10); 52 | 53 | $percentCoupon = new LukePOLO\LaraCart\Coupons\Percentage('10%OFF', '.1'); 54 | 55 | $this->laracart->addCoupon($percentCoupon); 56 | $percentCoupon->setDiscountOnItem($item); 57 | 58 | $this->assertEquals($percentCoupon, $this->laracart->findCoupon('10%OFF')); 59 | 60 | $this->assertEquals('10%', $percentCoupon->displayValue()); 61 | $this->assertEquals(1, $percentCoupon->discount($item->price)); 62 | $this->assertEquals(.63, $this->laracart->taxTotal(false)); 63 | $this->assertEquals(9.63, $this->laracart->total(false)); 64 | 65 | $this->assertCount(1, $this->laracart->getCoupons()); 66 | } 67 | 68 | /** 69 | * Test the fixed coupons. 70 | */ 71 | public function testAddFixedCoupon() 72 | { 73 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 74 | 75 | $this->laracart->addCoupon($fixedCoupon); 76 | $this->addItem(1, 20); 77 | $this->assertEquals('10.00', $this->laracart->discountTotal(false)); 78 | $this->assertEquals('0.70', $this->laracart->taxTotal(false)); 79 | } 80 | 81 | /** 82 | * Test the fixed coupons. 83 | */ 84 | public function testFixedCoupon() 85 | { 86 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 87 | 88 | $this->laracart->addCoupon($fixedCoupon); 89 | $this->assertEquals($fixedCoupon, $this->laracart->findCoupon('10OFF')); 90 | } 91 | 92 | /** 93 | * Test if single coupons works, we souldn't be able to add two. 94 | */ 95 | public function testSingleCoupons() 96 | { 97 | $fixedCouponOne = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 98 | $fixedCouponTwo = new LukePOLO\LaraCart\Coupons\Fixed('5OFF', 5); 99 | 100 | $this->laracart->addCoupon($fixedCouponOne); 101 | $this->laracart->addCoupon($fixedCouponTwo); 102 | 103 | $this->assertCount(1, $this->laracart->getCoupons()); 104 | 105 | $this->assertEquals($fixedCouponTwo, $this->laracart->findCoupon('5OFF')); 106 | } 107 | 108 | /** 109 | * Test if we can add multiple if the config is set properly. 110 | */ 111 | public function testMultipleCoupons() 112 | { 113 | $cart = $this->laracart->get()->cart; 114 | $cart->multipleCoupons = true; 115 | 116 | $fixedCouponOne = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 117 | $fixedCouponTwo = new LukePOLO\LaraCart\Coupons\Fixed('5OFF', 5); 118 | 119 | $this->laracart->addCoupon($fixedCouponOne); 120 | $this->laracart->addCoupon($fixedCouponTwo); 121 | 122 | $this->assertCount(2, $this->laracart->getCoupons()); 123 | } 124 | 125 | /** 126 | * Test removing coupons. 127 | */ 128 | public function testRemoveCoupon() 129 | { 130 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 131 | $this->laracart->addCoupon($fixedCoupon); 132 | 133 | $this->assertEquals($fixedCoupon, $this->laracart->findCoupon('10OFF')); 134 | 135 | $this->laracart->removeCoupon('10OFF'); 136 | 137 | $this->assertEmpty($this->laracart->findCoupon('10OFF')); 138 | } 139 | 140 | /** 141 | * Test getting the message from the coupon to see if its valid or has an error. 142 | */ 143 | public function testGetMessage() 144 | { 145 | $this->addItem(); 146 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 147 | $this->laracart->addCoupon($fixedCoupon); 148 | 149 | $foundCoupon = $this->laracart->findCoupon('10OFF'); 150 | $this->assertEquals('Coupon Applied', $foundCoupon->getMessage()); 151 | $this->assertEquals(true, $foundCoupon->canApply()); 152 | } 153 | 154 | /** 155 | * Test the min amount for a coupon. 156 | */ 157 | public function testCheckMinAmount() 158 | { 159 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10, [ 160 | 'addOptions' => 1, 161 | ]); 162 | 163 | $this->laracart->addCoupon($fixedCoupon); 164 | 165 | $coupon = $this->laracart->findCoupon('10OFF'); 166 | 167 | $this->assertEquals(true, $coupon->checkMinAmount(0)); 168 | $this->assertEquals(false, $coupon->checkMinAmount(100, false)); 169 | $this->assertEquals(1, $coupon->addOptions); 170 | 171 | try { 172 | $coupon->checkMinAmount(100); 173 | $this->expectException(\LukePOLO\LaraCart\Exceptions\CouponException::class); 174 | } catch (\LukePOLO\LaraCart\Exceptions\CouponException $e) { 175 | $this->assertEquals('You must have at least a total of $100.00', $e->getMessage()); 176 | } 177 | } 178 | 179 | /** 180 | * Test the max discount for a coupon. 181 | */ 182 | public function testMaxDiscount() 183 | { 184 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 185 | $this->laracart->addCoupon($fixedCoupon); 186 | 187 | $coupon = $this->laracart->findCoupon('10OFF'); 188 | 189 | $this->assertEquals(100, $coupon->maxDiscount(0, 100)); 190 | $this->assertEquals(100, $coupon->maxDiscount(5000, 100)); 191 | $this->assertEquals(1, $coupon->maxDiscount(1, 100, false)); 192 | 193 | try { 194 | $coupon->maxDiscount(10, 100); 195 | $this->expectException(\LukePOLO\LaraCart\Exceptions\CouponException::class); 196 | } catch (\LukePOLO\LaraCart\Exceptions\CouponException $e) { 197 | $this->assertEquals('This has a max discount of $10.00', $e->getMessage()); 198 | } 199 | } 200 | 201 | /** 202 | * Test the valid times for a coupon. 203 | */ 204 | public function testCheckValidTimes() 205 | { 206 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 207 | $this->laracart->addCoupon($fixedCoupon); 208 | 209 | $coupon = $this->laracart->findCoupon('10OFF'); 210 | 211 | $this->assertEquals(true, $coupon->checkValidTimes(Carbon::yesterday(), Carbon::tomorrow())); 212 | $this->assertEquals(false, $coupon->checkValidTimes(Carbon::tomorrow(), Carbon::tomorrow(), false)); 213 | 214 | try { 215 | $this->assertEquals(false, $coupon->checkValidTimes(Carbon::tomorrow(), Carbon::tomorrow())); 216 | $this->expectException(\LukePOLO\LaraCart\Exceptions\CouponException::class); 217 | } catch (\LukePOLO\LaraCart\Exceptions\CouponException $e) { 218 | $this->assertEquals('This coupon has expired', $e->getMessage()); 219 | } 220 | } 221 | 222 | /** 223 | * Test if we can set a coupon on an item. 224 | */ 225 | public function testSetDiscountOnItem() 226 | { 227 | $item = $this->addItem(1, 100); 228 | 229 | $this->assertEquals(107, $this->laracart->total(false)); 230 | 231 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 232 | 233 | $fixedCoupon->setDiscountOnItem($item); 234 | 235 | $this->assertNotNull($item->coupon); 236 | 237 | $this->assertEquals('10OFF', $item->coupon->code); 238 | $this->assertEqualsWithDelta(90 * 1.07, $this->laracart->total(false), 0.0001); 239 | 240 | $this->laracart->removeCoupon('10OFF'); 241 | 242 | $this->assertNull($item->coupon); 243 | $this->assertEquals(0, $item->discount); 244 | 245 | $this->assertEquals(0, $this->laracart->discountTotal(false)); 246 | $this->assertEquals(107, $this->laracart->total(false)); 247 | } 248 | 249 | public function testDiscountsTaxable() 250 | { 251 | $this->addItem(1, 20); 252 | 253 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 254 | 255 | $this->laracart->addCoupon($fixedCoupon); 256 | 257 | $this->laracart->findCoupon('10OFF'); 258 | 259 | $this->assertEquals(20, $this->laracart->subTotal(false)); 260 | 261 | $this->assertEquals(10 + (10 * .07), $this->laracart->total(false)); 262 | } 263 | 264 | /** 265 | * Test if we can set a coupon on an item. 266 | */ 267 | public function testDiscountTotals() 268 | { 269 | $item = $this->addItem(1, 10); 270 | 271 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 272 | 273 | $this->laracart->addCoupon($fixedCoupon); 274 | 275 | $coupon = $this->laracart->findCoupon('10OFF'); 276 | 277 | $coupon->setDiscountOnItem($item); 278 | 279 | $this->assertEquals('10OFF', $item->coupon->code); 280 | 281 | $this->assertEquals(0, $this->laracart->total(false)); 282 | $this->assertEquals(10, $this->laracart->discountTotal(false)); 283 | } 284 | 285 | /** 286 | * Test cart percentage coupon when items are not taxable. 287 | */ 288 | public function testCouponsNotTaxableItem() 289 | { 290 | $this->addItem(1, 1, false); 291 | 292 | $percentCoupon = new LukePOLO\LaraCart\Coupons\Percentage('20%OFF', '.2'); 293 | 294 | $this->laracart->addCoupon($percentCoupon); 295 | 296 | $this->assertEquals($percentCoupon, $this->laracart->findCoupon('20%OFF')); 297 | 298 | $this->assertEquals('20%', $percentCoupon->displayValue()); 299 | $this->assertEquals('0.20', $this->laracart->discountTotal(false)); 300 | 301 | $this->assertEquals(0, $this->laracart->taxTotal(false)); 302 | 303 | $this->assertEquals('0.80', $this->laracart->total(false)); 304 | } 305 | 306 | /** 307 | * Test cart percentage coupon when items are taxable. 308 | */ 309 | public function testCouponsTaxableItem() 310 | { 311 | $this->addItem(); 312 | 313 | $percentCoupon = new LukePOLO\LaraCart\Coupons\Percentage('20%OFF', '.2'); 314 | 315 | $this->laracart->addCoupon($percentCoupon); 316 | 317 | $this->assertEquals('20%', $percentCoupon->displayValue()); 318 | $this->assertEquals('0.20', $this->laracart->discountTotal(false)); 319 | 320 | $this->assertEquals('0.06', $this->laracart->taxTotal(false)); 321 | $this->assertEquals('0.86', $this->laracart->total(false)); 322 | } 323 | 324 | /** 325 | * Test if we can remove all coupons from the cart. 326 | */ 327 | public function testRemoveCoupons() 328 | { 329 | $item = $this->addItem(2, 30); 330 | 331 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 332 | 333 | $this->laracart->addCoupon($fixedCoupon); 334 | 335 | $coupon = $this->laracart->findCoupon('10OFF'); 336 | 337 | $coupon->setDiscountOnItem($item); 338 | 339 | $this->assertEquals('10OFF', $item->coupon->code); 340 | 341 | $this->laracart->removeCoupons(); 342 | 343 | $this->assertEmpty($this->laracart->getCoupons()); 344 | 345 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 346 | $this->laracart->addCoupon($fixedCoupon); 347 | 348 | $this->assertEquals($fixedCoupon, $this->laracart->findCoupon('10OFF')); 349 | 350 | $this->laracart->removeCoupons(); 351 | 352 | $this->assertEmpty($this->laracart->findCoupon('10OFF')); 353 | 354 | $cart = $this->laracart->get()->cart; 355 | $cart->multipleCoupons = true; 356 | 357 | $fixedCouponOne = new LukePOLO\LaraCart\Coupons\Fixed('10OFF', 10); 358 | $fixedCouponTwo = new LukePOLO\LaraCart\Coupons\Fixed('5OFF', 5); 359 | 360 | $this->laracart->addCoupon($fixedCouponOne); 361 | $this->laracart->addCoupon($fixedCouponTwo); 362 | 363 | $this->assertCount(2, $this->laracart->getCoupons()); 364 | 365 | $this->laracart->removeCoupons(); 366 | 367 | $this->assertEmpty($this->laracart->getCoupons()); 368 | } 369 | 370 | /** 371 | * Testing getting the message on a coupon. 372 | */ 373 | public function testCouponMessage() 374 | { 375 | $item = $this->addItem(2, 30); 376 | $fixedCoupon = new \LukePOLO\LaraCart\Tests\Coupons\Fixed('10OFF', 10); 377 | 378 | try { 379 | $this->assertNotEquals(true, $fixedCoupon->discount($item->price)); 380 | } catch (\LukePOLO\LaraCart\Exceptions\CouponException $e) { 381 | $this->assertEquals('Sorry, you must have at least 100 dollars!', $e->getMessage()); 382 | } 383 | } 384 | 385 | /** 386 | * Testing discount when total is greater than applied coupon value. 387 | */ 388 | public function testFixedCouponWithTotalLessThanCoupon() 389 | { 390 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('500 OFF', 500); 391 | 392 | $this->laracart->addCoupon($fixedCoupon); 393 | 394 | $this->assertEquals('0.00', $this->laracart->discountTotal(false)); 395 | 396 | $this->addItem(1, 400); 397 | 398 | $this->assertEquals('400.00', $this->laracart->discountTotal(false)); 399 | } 400 | 401 | /** 402 | * Testing discount when total with fees is greater than applied coupon value. 403 | */ 404 | public function testFixedCouponWithFeeWithTotalLessThanCoupon() 405 | { 406 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('500 OFF', 500); 407 | 408 | $this->laracart->addCoupon($fixedCoupon); 409 | 410 | $this->addItem(1, 400); 411 | 412 | $this->laracart->addFee('testFee', 150); 413 | 414 | $this->assertEquals('400.00', $this->laracart->discountTotal(false)); 415 | 416 | $this->assertEquals(150, $this->laracart->total(false)); 417 | 418 | $percentCoupon = new LukePOLO\LaraCart\Coupons\Percentage('100% Off', 1); 419 | $this->laracart->addCoupon($percentCoupon); 420 | 421 | $this->assertEquals(150, $this->laracart->total(false)); 422 | } 423 | 424 | public function testFeeDiscount() 425 | { 426 | $this->app['config']->set('laracart.discount_fees', true); 427 | 428 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('10 OFF', 10); 429 | 430 | $this->laracart->addCoupon($fixedCoupon); 431 | 432 | $this->addItem(1, 5); 433 | 434 | $this->laracart->addFee('testFee', 15); 435 | 436 | $this->assertEquals(10, $this->laracart->total(false)); 437 | } 438 | 439 | /** 440 | * Testing percentage coupon on multiple item qty. 441 | */ 442 | public function testPercentageCouponOnMultipleQtyItems() 443 | { 444 | $percentCoupon = new LukePOLO\LaraCart\Coupons\Percentage('10% Off', .1); 445 | 446 | $item = $this->addItem(2, 10); 447 | 448 | $percentCoupon->setDiscountOnItem($item); 449 | 450 | $this->assertEquals(18, $this->laracart->total(false) - $this->laracart->taxTotal(false)); 451 | } 452 | 453 | /** 454 | * Testing Discount Pre Taxed. 455 | */ 456 | public function testPreTaxDiscountFixed() 457 | { 458 | $this->app['config']->set('laracart.tax', .19); 459 | $this->app['config']->set('laracart.fees_taxable', false); 460 | 461 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('$1 Off', 1); 462 | 463 | $this->addItem(1, .84); 464 | 465 | $this->laracart->addCoupon($fixedCoupon); 466 | 467 | $this->assertEquals(0, $this->laracart->total(false)); 468 | } 469 | 470 | /** 471 | * Testing Discount Pre Taxed. 472 | */ 473 | public function testPreTaxDiscountPercentage() 474 | { 475 | $this->app['config']->set('laracart.tax', .19); 476 | $this->app['config']->set('laracart.fees_taxable', false); 477 | 478 | $percentageCoupon = new LukePOLO\LaraCart\Coupons\Percentage('100%', 1); 479 | 480 | $this->addItem(1, .84); 481 | 482 | $this->laracart->addCoupon($percentageCoupon); 483 | 484 | $this->assertEquals(0, $this->laracart->total(false)); 485 | } 486 | 487 | public function testCartVsItemCoupon() 488 | { 489 | $item = $this->addItem(); 490 | $couponPercentage = new \LukePOLO\LaraCart\Coupons\Percentage('50%', 0.5); 491 | $this->laracart->addCoupon($couponPercentage); 492 | 493 | $cartTotal = $this->laracart->total(false); 494 | $this->laracart->removeCoupon($couponPercentage->code); 495 | 496 | $this->assertEquals(1.07, $this->laracart->total(false)); 497 | 498 | $item->addCoupon($couponPercentage); 499 | 500 | $itemTotal = $this->laracart->total(false); 501 | 502 | $this->assertEquals(.54, $cartTotal); 503 | $this->assertEquals($itemTotal, $cartTotal); 504 | } 505 | 506 | public function testCouponOnSubItems() 507 | { 508 | $item = $this->addItem(1, 0); 509 | 510 | $item->addSubItem([ 511 | 'size' => 'XXL', 512 | 'price' => 5, 513 | ]); 514 | 515 | $this->assertEquals(5, $this->laracart->subTotal(false)); 516 | 517 | $fixedCoupon = new LukePOLO\LaraCart\Coupons\Fixed('5 OFF', 5); 518 | 519 | $this->laracart->addCoupon($fixedCoupon); 520 | 521 | $this->assertEquals(0, $this->laracart->total(false)); 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /src/LaraCart.php: -------------------------------------------------------------------------------- 1 | session = $session; 43 | $this->events = $events; 44 | $this->authManager = $authManager->guard(config('laracart.guard', null)); 45 | $this->prefix = config('laracart.cache_prefix', 'laracart'); 46 | $this->itemModel = config('laracart.item_model', null); 47 | $this->itemModelRelations = config('laracart.item_model_relations', []); 48 | 49 | $this->setInstance($this->session->get($this->prefix.'.instance', 'default')); 50 | } 51 | 52 | /** 53 | * Gets all current instances inside the session. 54 | * 55 | * @return mixed 56 | */ 57 | public function getInstances() 58 | { 59 | return $this->session->get($this->prefix.'.instances', []); 60 | } 61 | 62 | /** 63 | * Sets and Gets the instance of the cart in the session we should be using. 64 | * 65 | * @param string $instance 66 | * 67 | * @return LaraCart 68 | */ 69 | public function setInstance($instance = 'default') 70 | { 71 | $this->get($instance); 72 | 73 | $this->session->put($this->prefix.'.instance', $instance); 74 | 75 | if (!in_array($instance, $this->getInstances())) { 76 | $this->session->push($this->prefix.'.instances', $instance); 77 | } 78 | $this->events->dispatch('laracart.new'); 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Gets the instance in the session. 85 | * 86 | * @param string $instance 87 | * 88 | * @return $this cart instance 89 | */ 90 | public function get($instance = 'default') 91 | { 92 | if (config('laracart.cross_devices', false) && $this->authManager->check()) { 93 | if (!empty($cartSessionID = $this->authManager->user()->cart_session_id)) { 94 | $this->session->setId($cartSessionID); 95 | $this->session->start(); 96 | } 97 | } 98 | 99 | if (empty($this->cart = $this->session->get($this->prefix.'.'.$instance))) { 100 | $this->cart = new Cart($instance); 101 | } 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * Gets an an attribute from the cart. 108 | * 109 | * @param $attribute 110 | * @param $defaultValue 111 | * 112 | * @return mixed 113 | */ 114 | public function getAttribute($attribute, $defaultValue = null) 115 | { 116 | return Arr::get($this->cart->attributes, $attribute, $defaultValue); 117 | } 118 | 119 | /** 120 | * Gets all the carts attributes. 121 | * 122 | * @return mixed 123 | */ 124 | public function getAttributes() 125 | { 126 | return $this->cart->attributes; 127 | } 128 | 129 | /** 130 | * Adds an Attribute to the cart. 131 | * 132 | * @param $attribute 133 | * @param $value 134 | */ 135 | public function setAttribute($attribute, $value) 136 | { 137 | Arr::set($this->cart->attributes, $attribute, $value); 138 | 139 | $this->update(); 140 | } 141 | 142 | private function updateDiscounts() 143 | { 144 | // reset discounted 145 | foreach ($this->getItems() as $item) { 146 | $item->discounted = []; 147 | } 148 | 149 | // go through each item and see if they have a coupon attached 150 | foreach ($this->getItems() as $item) { 151 | if ($item->coupon) { 152 | $item->coupon->discounted = 0; 153 | for ($qty = 0; $qty < $item->qty; $qty++) { 154 | $item->coupon->discounted += $item->discounted[$qty] = $item->coupon->discount($item->subTotalPerItem(false)); 155 | } 156 | } 157 | } 158 | 159 | // go through each coupon and apply to items that do not have a coupon attached 160 | foreach ($this->getCoupons() as $coupon) { 161 | $coupon->discounted = 0; 162 | foreach ($this->getItems() as $item) { 163 | if (!$item->coupon) { 164 | $item->discounted = []; 165 | for ($qty = 0; $qty < $item->qty; $qty++) { 166 | $coupon->discounted += $item->discounted[$qty] = $coupon->discount($item->subTotalPerItem(false)); 167 | } 168 | } 169 | } 170 | 171 | if (config('laracart.discount_fees', false)) { 172 | // go through each fee and discount 173 | foreach ($this->getFees() as $fee) { 174 | $remainingDiscount = $coupon->value - $coupon->discounted; 175 | if ($remainingDiscount > 0) { 176 | $fee->discounted = min($remainingDiscount, $fee->amount); 177 | $coupon->discounted += $fee->discounted; 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | /** 185 | * Updates cart session. 186 | */ 187 | public function update() 188 | { 189 | // allows us to track a discount on the item so we are able properly do taxation 190 | $this->updateDiscounts(); 191 | 192 | $this->session->put($this->prefix.'.'.$this->cart->instance, $this->cart); 193 | 194 | if (config('laracart.cross_devices', false) && $this->authManager->check()) { 195 | $this->authManager->user()->cart_session_id = $this->session->getId(); 196 | $this->authManager->user()->save(); 197 | } 198 | 199 | $this->session->reflash(); 200 | 201 | $this->session->save(); 202 | 203 | $this->events->dispatch('laracart.update', $this->cart); 204 | } 205 | 206 | /** 207 | * Removes an attribute from the cart. 208 | * 209 | * @param $attribute 210 | */ 211 | public function removeAttribute($attribute) 212 | { 213 | Arr::forget($this->cart->attributes, $attribute); 214 | 215 | $this->update(); 216 | } 217 | 218 | /** 219 | * Creates a CartItem and then adds it to cart. 220 | * 221 | * @param string|int $itemID 222 | * @param null $name 223 | * @param int $qty 224 | * @param string $price 225 | * @param array $options 226 | * @param bool|true $taxable 227 | * 228 | * @throws ModelNotFound 229 | * 230 | * @return CartItem 231 | */ 232 | public function addLine($itemID, $name = null, $qty = 1, $price = '0.00', $options = [], $taxable = true) 233 | { 234 | return $this->add($itemID, $name, $qty, $price, $options, $taxable, true); 235 | } 236 | 237 | /** 238 | * Creates a CartItem and then adds it to cart. 239 | * 240 | * @param $itemID 241 | * @param null $name 242 | * @param int $qty 243 | * @param string $price 244 | * @param array $options 245 | * @param bool|false $taxable 246 | * @param bool|false $lineItem 247 | * 248 | * @throws ModelNotFound 249 | * 250 | * @return CartItem 251 | */ 252 | public function add( 253 | $itemID, 254 | $name = null, 255 | $qty = 1, 256 | $price = '0.00', 257 | $options = [], 258 | $taxable = true, 259 | $lineItem = false 260 | ) { 261 | if (!empty(config('laracart.item_model'))) { 262 | $itemModel = $itemID; 263 | 264 | if (!$this->isItemModel($itemModel)) { 265 | $itemModel = (new $this->itemModel())->with($this->itemModelRelations)->find($itemID); 266 | } 267 | 268 | if (empty($itemModel)) { 269 | throw new ModelNotFound('Could not find the item '.$itemID); 270 | } 271 | 272 | $bindings = config('laracart.item_model_bindings'); 273 | 274 | $itemID = $itemModel->{$bindings[\LukePOLO\LaraCart\CartItem::ITEM_ID]}; 275 | 276 | if (is_int($name)) { 277 | $qty = $name; 278 | } 279 | 280 | $name = $itemModel->{$bindings[\LukePOLO\LaraCart\CartItem::ITEM_NAME]}; 281 | $price = $itemModel->{$bindings[\LukePOLO\LaraCart\CartItem::ITEM_PRICE]}; 282 | 283 | $options['model'] = $itemModel; 284 | 285 | $options = array_merge($options, $this->getItemModelOptions($itemModel, $bindings[\LukePOLO\LaraCart\CartItem::ITEM_OPTIONS])); 286 | 287 | $taxable = $itemModel->{$bindings[\LukePOLO\LaraCart\CartItem::ITEM_TAXABLE]} ? true : false; 288 | } 289 | 290 | $item = $this->addItem(new CartItem( 291 | $itemID, 292 | $name, 293 | $qty, 294 | $price, 295 | $options, 296 | $taxable, 297 | $lineItem 298 | )); 299 | 300 | $this->update(); 301 | 302 | return $this->getItem($item->getHash()); 303 | } 304 | 305 | /** 306 | * Adds the cartItem into the cart session. 307 | * 308 | * @param CartItem $cartItem 309 | * 310 | * @return CartItem 311 | */ 312 | public function addItem(CartItem $cartItem) 313 | { 314 | $itemHash = $cartItem->generateHash(); 315 | 316 | if ($this->getItem($itemHash)) { 317 | $this->getItem($itemHash)->qty += $cartItem->qty; 318 | } else { 319 | $this->cart->items[] = $cartItem; 320 | } 321 | 322 | app('events')->dispatch( 323 | 'laracart.addItem', 324 | $cartItem 325 | ); 326 | 327 | return $cartItem; 328 | } 329 | 330 | /** 331 | * Increment the quantity of a cartItem based on the itemHash. 332 | * 333 | * @param $itemHash 334 | * 335 | * @return CartItem | null 336 | */ 337 | public function increment($itemHash) 338 | { 339 | $item = $this->getItem($itemHash); 340 | $item->qty++; 341 | $this->update(); 342 | 343 | return $item; 344 | } 345 | 346 | /** 347 | * Decrement the quantity of a cartItem based on the itemHash. 348 | * 349 | * @param $itemHash 350 | * 351 | * @return CartItem | null 352 | */ 353 | public function decrement($itemHash) 354 | { 355 | $item = $this->getItem($itemHash); 356 | if ($item->qty > 1) { 357 | $item->qty--; 358 | $this->update(); 359 | 360 | return $item; 361 | } 362 | $this->removeItem($itemHash); 363 | $this->update(); 364 | } 365 | 366 | /** 367 | * Find items in the cart matching a data set. 368 | * 369 | * param $data 370 | * 371 | * @return array | CartItem | null 372 | */ 373 | public function find($data) 374 | { 375 | $matches = []; 376 | 377 | foreach ($this->getItems() as $item) { 378 | if ($item->find($data)) { 379 | $matches[] = $item; 380 | } 381 | } 382 | 383 | switch (count($matches)) { 384 | case 0: 385 | return; 386 | break; 387 | case 1: 388 | return $matches[0]; 389 | break; 390 | default: 391 | return $matches; 392 | } 393 | } 394 | 395 | /** 396 | * Finds a cartItem based on the itemHash. 397 | * 398 | * @param $itemHash 399 | * 400 | * @return CartItem | null 401 | */ 402 | public function getItem($itemHash) 403 | { 404 | return Arr::get($this->getItems(), $itemHash); 405 | } 406 | 407 | /** 408 | * Gets all the items within the cart. 409 | * 410 | * @return array 411 | */ 412 | public function getItems() 413 | { 414 | $items = []; 415 | if (isset($this->cart->items) === true) { 416 | foreach ($this->cart->items as $item) { 417 | $items[$item->getHash()] = $item; 418 | } 419 | } 420 | 421 | return $items; 422 | } 423 | 424 | /** 425 | * Updates an items attributes. 426 | * 427 | * @param $itemHash 428 | * @param $key 429 | * @param $value 430 | * 431 | * @return CartItem|null 432 | */ 433 | public function updateItem($itemHash, $key, $value) 434 | { 435 | if (empty($item = $this->getItem($itemHash)) === false) { 436 | if ($key == 'qty' && $value == 0) { 437 | return $this->removeItem($itemHash); 438 | } 439 | 440 | $item->$key = $value; 441 | } 442 | 443 | $this->update(); 444 | 445 | return $item; 446 | } 447 | 448 | /** 449 | * Removes a CartItem based on the itemHash. 450 | * 451 | * @param $itemHash 452 | */ 453 | public function removeItem($itemHash) 454 | { 455 | if (empty($this->cart->items) === false) { 456 | foreach ($this->cart->items as $itemKey => $item) { 457 | if ($item->getHash() == $itemHash) { 458 | unset($this->cart->items[$itemKey]); 459 | break; 460 | } 461 | } 462 | 463 | $this->events->dispatch('laracart.removeItem', $item); 464 | 465 | $this->update(); 466 | } 467 | } 468 | 469 | /** 470 | * Empties the carts items. 471 | */ 472 | public function emptyCart() 473 | { 474 | unset($this->cart->items); 475 | 476 | $this->update(); 477 | 478 | $this->events->dispatch('laracart.empty', $this->cart->instance); 479 | } 480 | 481 | /** 482 | * Completely destroys cart and anything associated with it. 483 | */ 484 | public function destroyCart() 485 | { 486 | $instance = $this->cart->instance; 487 | 488 | $this->session->forget($this->prefix.'.'.$instance); 489 | 490 | $this->events->dispatch('laracart.destroy', $instance); 491 | 492 | $this->cart = new Cart($instance); 493 | 494 | $this->update(); 495 | } 496 | 497 | /** 498 | * Gets the coupons for the current cart. 499 | * 500 | * @return array 501 | */ 502 | public function getCoupons() 503 | { 504 | return $this->cart->coupons; 505 | } 506 | 507 | /** 508 | * Finds a specific coupon in the cart. 509 | * 510 | * @param $code 511 | * 512 | * @return mixed 513 | */ 514 | public function findCoupon($code) 515 | { 516 | return Arr::get($this->cart->coupons, $code); 517 | } 518 | 519 | /** 520 | * Applies a coupon to the cart. 521 | * 522 | * @param CouponContract $coupon 523 | */ 524 | public function addCoupon(CouponContract $coupon) 525 | { 526 | if (!$this->cart->multipleCoupons) { 527 | $this->cart->coupons = []; 528 | } 529 | 530 | $this->cart->coupons[$coupon->code] = $coupon; 531 | 532 | $this->update(); 533 | } 534 | 535 | /** 536 | * Removes a coupon in the cart. 537 | * 538 | * @param $code 539 | */ 540 | public function removeCoupon($code) 541 | { 542 | $this->removeCouponFromItems($code); 543 | Arr::forget($this->cart->coupons, $code); 544 | $this->update(); 545 | } 546 | 547 | /** 548 | * Removes all coupons from the cart. 549 | */ 550 | public function removeCoupons() 551 | { 552 | $this->removeCouponFromItems(); 553 | $this->cart->coupons = []; 554 | $this->update(); 555 | } 556 | 557 | /** 558 | * Gets a specific fee from the fees array. 559 | * 560 | * @param $name 561 | * 562 | * @return mixed 563 | */ 564 | public function getFee($name) 565 | { 566 | return Arr::get($this->cart->fees, $name, new CartFee(null, false)); 567 | } 568 | 569 | /** 570 | * Allows to charge for additional fees that may or may not be taxable 571 | * ex - service fee , delivery fee, tips. 572 | * 573 | * @param $name 574 | * @param $amount 575 | * @param bool|false $taxable 576 | * @param array $options 577 | */ 578 | public function addFee($name, $amount, $taxable = false, array $options = []) 579 | { 580 | Arr::set($this->cart->fees, $name, new CartFee($amount, $taxable, $options)); 581 | 582 | $this->update(); 583 | } 584 | 585 | /** 586 | * Removes a fee from the fee array. 587 | * 588 | * @param $name 589 | */ 590 | public function removeFee($name) 591 | { 592 | Arr::forget($this->cart->fees, $name); 593 | 594 | $this->update(); 595 | } 596 | 597 | /** 598 | * Removes all the fees set in the cart. 599 | */ 600 | public function removeFees() 601 | { 602 | $this->cart->fees = []; 603 | 604 | $this->update(); 605 | } 606 | 607 | /** 608 | * Gets the total tax for the cart. 609 | * 610 | * @param bool|true $format 611 | * 612 | * @return string 613 | */ 614 | public function taxTotal($format = true) 615 | { 616 | $totalTax = 0; 617 | 618 | foreach ($this->getItems() as $item) { 619 | $totalTax += $item->taxTotal(false); 620 | } 621 | 622 | $totalTax += $this->feeTaxTotal(false); 623 | 624 | return $this->formatMoney($totalTax, null, null, $format); 625 | } 626 | 627 | public function feeTaxTotal($format = true) 628 | { 629 | return $this->formatMoney(array_sum($this->feeTaxSummary()), null, null, $format); 630 | } 631 | 632 | public function feeTaxSummary() 633 | { 634 | $taxed = []; 635 | if (config('laracart.fees_taxable', false)) { 636 | foreach ($this->getFees() as $fee) { 637 | if ($fee->taxable) { 638 | if (!isset($taxed[(string) $fee->tax])) { 639 | $taxed[(string) $fee->tax] = 0; 640 | } 641 | $taxed[(string) $fee->tax] += $this->formatMoney($fee->amount * $fee->tax, null, null, false); 642 | } 643 | } 644 | } 645 | 646 | return $taxed; 647 | } 648 | 649 | public function taxSummary() 650 | { 651 | $taxed = []; 652 | foreach ($this->getItems() as $item) { 653 | foreach ($item->taxSummary() as $qtyIndex => $taxRates) { 654 | foreach ($taxRates as $taxRate => $amount) { 655 | if (!isset($taxed[(string) $taxRate])) { 656 | $taxed[(string) $taxRate] = 0; 657 | } 658 | $taxed[(string) $taxRate] += $amount; 659 | } 660 | } 661 | } 662 | 663 | foreach ($this->feeTaxSummary() as $taxRate => $amount) { 664 | if (!isset($taxed[(string) $taxRate])) { 665 | $taxed[(string) $taxRate] = 0; 666 | } 667 | $taxed[(string) $taxRate] += $amount; 668 | } 669 | 670 | return $taxed; 671 | } 672 | 673 | /** 674 | * Gets the total of the cart with or without tax. 675 | * 676 | * @param bool $format 677 | * 678 | * @return string 679 | */ 680 | public function total($format = true) 681 | { 682 | $total = $this->itemTotals(false); 683 | $total += $this->feeSubTotal(false) + $this->feeTaxTotal(false); 684 | 685 | return $this->formatMoney($total, null, null, $format); 686 | } 687 | 688 | public function netTotal($format = true) 689 | { 690 | $total = $this->subTotal(false); 691 | $total += $this->feeSubTotal(false); 692 | $total -= $this->discountTotal(false); 693 | 694 | return $this->formatMoney($total, null, null, $format); 695 | } 696 | 697 | public function itemTotals($format = true) 698 | { 699 | $total = 0; 700 | 701 | if ($this->count() != 0) { 702 | foreach ($this->getItems() as $item) { 703 | $total += $item->total(false); 704 | } 705 | } 706 | 707 | if ($total < 0) { 708 | $total = 0; 709 | } 710 | 711 | return $this->formatMoney($total, null, null, $format); 712 | } 713 | 714 | /** 715 | * Gets the subtotal of the cart with or without tax. 716 | * 717 | * @param bool $format 718 | * 719 | * @return string 720 | */ 721 | public function subTotal($format = true) 722 | { 723 | $total = 0; 724 | 725 | if ($this->count() != 0) { 726 | foreach ($this->getItems() as $item) { 727 | $total += $item->subTotal(false); 728 | } 729 | } 730 | 731 | if ($total < 0) { 732 | $total = 0; 733 | } 734 | 735 | return $this->formatMoney($total, null, null, $format); 736 | } 737 | 738 | /** 739 | * Get the count based on qty, or number of unique items. 740 | * 741 | * @param bool $withItemQty 742 | * 743 | * @return int 744 | */ 745 | public function count($withItemQty = true) 746 | { 747 | $count = 0; 748 | 749 | foreach ($this->getItems() as $item) { 750 | if ($withItemQty) { 751 | $count += $item->qty; 752 | } else { 753 | $count++; 754 | } 755 | } 756 | 757 | return $count; 758 | } 759 | 760 | /** 761 | * Formats the number into a money format based on the locale and currency formats. 762 | * 763 | * @param $number 764 | * @param $locale 765 | * @param $currencyCode 766 | * @param $format 767 | * 768 | * @return string 769 | */ 770 | public static function formatMoney($number, $locale = null, $currencyCode = null, $format = true) 771 | { 772 | // When prices in cents needs to be formatted, divide by 100 to allow formatting in whole units 773 | if (config('laracart.prices_in_cents', false) === true && $format) { 774 | $number = $number / 100; 775 | // When prices in cents do not need to be formatted then cast to integer and round the price 776 | } elseif (config('laracart.prices_in_cents', false) === true && !$format) { 777 | $number = (int) round($number); 778 | } else { 779 | $number = round($number, 2); 780 | } 781 | 782 | if ($format) { 783 | $moneyFormatter = new NumberFormatter(empty($locale) ? config('laracart.locale', 'en_US.UTF-8') : $locale, NumberFormatter::CURRENCY); 784 | 785 | $number = $moneyFormatter->formatCurrency($number, empty($currencyCode) ? config('laracart.currency_code', 'USD') : $currencyCode); 786 | } 787 | 788 | return $number; 789 | } 790 | 791 | public function feeSubTotal($format = true) 792 | { 793 | $feeTotal = 0; 794 | 795 | foreach ($this->getFees() as $fee) { 796 | $feeTotal += $fee->getAmount(false); 797 | } 798 | 799 | return $this->formatMoney($feeTotal, null, null, $format); 800 | } 801 | 802 | /** 803 | * Gets all the fees on the cart object. 804 | * 805 | * @return mixed 806 | */ 807 | public function getFees() 808 | { 809 | return $this->cart->fees; 810 | } 811 | 812 | /** 813 | * Gets the total amount discounted. 814 | * 815 | * @param bool $format 816 | * 817 | * @return string 818 | */ 819 | public function discountTotal($format = true) 820 | { 821 | $total = 0; 822 | 823 | foreach ($this->getItems() as $item) { 824 | $total += $item->getDiscount(false); 825 | } 826 | 827 | foreach ($this->getFees() as $fee) { 828 | $total += $fee->getDiscount(false); 829 | } 830 | 831 | return $this->formatMoney($total, null, null, $format); 832 | } 833 | 834 | /** 835 | * Checks to see if its an item model. 836 | * 837 | * @param $itemModel 838 | * 839 | * @return bool 840 | */ 841 | private function isItemModel($itemModel) 842 | { 843 | if (is_object($itemModel) && get_class($itemModel) == config('laracart.item_model')) { 844 | return true; 845 | } 846 | 847 | return false; 848 | } 849 | 850 | /** 851 | * Gets the item models options based the config. 852 | * 853 | * @param Model $itemModel 854 | * @param array $options 855 | * 856 | * @return array 857 | */ 858 | private function getItemModelOptions(Model $itemModel, $options = []) 859 | { 860 | $itemOptions = []; 861 | foreach ($options as $option) { 862 | $itemOptions[$option] = $this->getFromModel($itemModel, $option); 863 | } 864 | 865 | return array_filter($itemOptions, function ($value) { 866 | if ($value !== false && empty($value)) { 867 | return false; 868 | } 869 | 870 | return true; 871 | }); 872 | } 873 | 874 | /** 875 | * Gets a option from the model. 876 | * 877 | * @param Model $itemModel 878 | * @param $attr 879 | * @param null $defaultValue 880 | * 881 | * @return Model|null 882 | */ 883 | private function getFromModel(Model $itemModel, $attr, $defaultValue = null) 884 | { 885 | $variable = $itemModel; 886 | 887 | if (!empty($attr)) { 888 | foreach (explode('.', $attr) as $attr) { 889 | $variable = Arr::get($variable, $attr, $defaultValue); 890 | } 891 | } 892 | 893 | return $variable; 894 | } 895 | 896 | /** 897 | * Removes a coupon from the item. 898 | * 899 | * @param null $code 900 | */ 901 | private function removeCouponFromItems($code = null) 902 | { 903 | foreach ($this->getItems() as $item) { 904 | if (isset($item->coupon) && (empty($code) || $item->coupon->code == $code)) { 905 | $item->coupon = null; 906 | } 907 | } 908 | } 909 | } 910 | --------------------------------------------------------------------------------