diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3ba70c4..3ead008 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: true matrix: - php: ['8.1','8.2', '8.3'] + php: ['8.1', '8.2', '8.3'] stability: [prefer-stable] steps: diff --git a/README.md b/README.md index f19bf5d..62d7279 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,9 @@ ### Round Mode 'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'), ``` -## HOW TO USE +## Usage -- [Usage](#usage) +- [Basic Usage](#basic-usage) - [Conditions](#conditions) - [Items](#items) - [Associating Models](#associating-models) @@ -288,12 +288,14 @@ ### Clearing the Cart: **Cart::clear()** ## Conditions -Conditions are very useful in terms for adding things like discounts, taxes, shipping costs, etc. +Conditions are very useful for adding things like discounts, taxes, shipping, etc. -Conditions can be added on the entire cart or on individual items. +Conditions can be added on the entire cart or on individual items, and can even be applied only at certain cart values. Conditions on a cart level should always have a `target` of `subtotal` or `total`. This tells the cart which value to apply the condition to. +You can provide an optional `minimum` value which should be the dollar value in which the target (subtotal or total) needs to be for the condition to be active and impact the cart. + You can also provide an `order` to cart conditions which tells the cart in what order to apply the conditions. Item level conditions do not support the `order` parameter. ### Conditions on the cart @@ -362,18 +364,59 @@ #### Getting conditions on the cart: **Cart::getConditions()** $condition->getType(); // the type $condition->getValue(); // the value of the condition $condition->getOrder(); // the order of the condition + $condition->getMinimum(); // the minimum dollar amount of the target, needed to activate the condition + $condition->getMaximum(); // the maximum dollar amount of the target, needed to keep the condition active $condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added } // You can also get a condition that has been applied on the cart by using its name: -$condition = Cart::getCondition('Tax: 10%'); +$condition = Cart::getCondition('GST'); + $condition->getTarget(); // the target of which the condition was applied $condition->getName(); // the name of the condition $condition->getType(); // the type $condition->getValue(); // the value of the condition +$condition->getMinimum(); // the minimum dollar amount of the target, needed to activate the condition +$condition->getMaximum(); // the maximum dollar amount of the target, needed to keep the condition active $condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added ``` +#### Getting active conditions on the cart: **Cart::getConditions(active: true)** + +You can get only active conditions by passsing `active: true` to the `getConditions()` method. + +This will return conditions that are actively being applied to the cart (i.e if they meet their minimum or maximum value) + +```php +$tenPercentOff = new CartCondition([ + 'name' => '10% Off', + 'type' => 'discount', + 'target' => 'subtotal', + 'value' => '-10%', + 'minimum' => 120, + 'order' => 1, +]); + +Cart::getConditions(active: true); + +// will return "10% Off" if the subtotal of the cart is $200. +// will return no conditions if the subtotal is $100. + +$shipping = new CartCondition([ + 'name' => 'Shipping', + 'type' => 'discount', + 'target' => 'subtotal', + 'value' => '10', + 'maximum' => 200, + 'order' => 1, +]); + +Cart::getConditions(active: true); + +// will return "Shipping" if the subtotal of the cart is less than or equal 200 +// will return no conditions if the subtotal is $210 +``` + ### Calculating condition value There are 2 ways to calculate the value of a condition: @@ -385,7 +428,7 @@ #### Using the `getCalculatedValue` method on the condition instance ```php $subTotal = Cart::getSubTotal(); -$condition = Cart::getCondition('VAT 12.5%'); +$condition = Cart::getCondition('10% GST'); $conditionCalculatedValue = $condition->getCalculatedValue($subTotal); ``` @@ -422,6 +465,44 @@ #### Using the `getCalculatedValueForCondition` method on the cart instance Cart::getCalculatedValueForCondition('Gift Card'); // returns 0 as the coupon discount is applied first and brings the subtotal to 0 ``` +### Adding conditions that activate once a minimum value is met + +You can add a `minimum` amount required for a condition to activate. + +This is useful for applying discounts only after certain cart values, i.e: 10% off for any purchases over $120. + +```php +$tenPercentOff = new CartCondition([ + 'name' => '10% Off', + 'type' => 'discount', + 'target' => 'subtotal', + 'value' => '-10%', + 'minimum' => 120, + 'order' => 1, +]); + +Cart::condition($tenPercentOff) +``` + +### Adding conditions that only activate up to a maximum value + +You can add a `maximum` amount required for a condition to activate. + +This is useful for applying discounts up until amounts, i.e shipping for anything below $200, and free shipping above. + +```php +$shipping = new CartCondition([ + 'name' => 'Shipping', + 'type' => 'discount', + 'target' => 'subtotal', + 'value' => '12', + 'maximum' => 200, + 'order' => 1, +]); + +Cart::condition($shipping) +``` + ### Condition on items Item conditions are useful if you have discounts to be applied specifically on an item and not on the whole cart value. @@ -483,7 +564,7 @@ ### Condition on items $cartSubTotal = Cart::getSubTotal(); ``` -#### Add condition to existing Item on the cart: **Cart::addItemCondition($productId, $itemCondition)** +#### Add a condition to an existing item on the cart: **Cart::addItemCondition($productId, $itemCondition)** Adding Condition to an existing Item on the cart is simple as well. diff --git a/composer.json b/composer.json index 4fe2736..c9fa591 100644 --- a/composer.json +++ b/composer.json @@ -26,9 +26,9 @@ }, "require-dev": { "mockery/mockery": "^1.4.2", - "laravel/pint": "dev-main", + "laravel/pint": "^1.17", "pestphp/pest": "2.35.1", - "larapack/dd": "dev-master" + "larapack/dd": "1.1" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 2fb844c..3e1c93d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "726c11e0474359fea45e3d6790b14426", + "content-hash": "bf8d3b76695f06430eaf04e5eeacb58c", "packages": [ { "name": "brick/math", @@ -378,12 +378,12 @@ "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "dc68a7ccad93f3c2baa0bc8f559431c06391aa75" + "reference": "66d2c9bdf5641599735d402c1c504b54734a9cca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/dc68a7ccad93f3c2baa0bc8f559431c06391aa75", - "reference": "dc68a7ccad93f3c2baa0bc8f559431c06391aa75", + "url": "https://api.github.com/repos/illuminate/collections/zipball/66d2c9bdf5641599735d402c1c504b54734a9cca", + "reference": "66d2c9bdf5641599735d402c1c504b54734a9cca", "shasum": "" }, "require": { @@ -425,7 +425,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-08-01T18:54:27+00:00" + "time": "2024-08-23T18:49:36+00:00" }, { "name": "illuminate/conditionable", @@ -479,12 +479,12 @@ "source": { "type": "git", "url": "https://github.com/illuminate/container.git", - "reference": "f47be671981a4438257c4fbfc3ad257f4e3e929a" + "reference": "85e3396fde3139eae3f37128222c9a7746ca4bbb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/f47be671981a4438257c4fbfc3ad257f4e3e929a", - "reference": "f47be671981a4438257c4fbfc3ad257f4e3e929a", + "url": "https://api.github.com/repos/illuminate/container/zipball/85e3396fde3139eae3f37128222c9a7746ca4bbb", + "reference": "85e3396fde3139eae3f37128222c9a7746ca4bbb", "shasum": "" }, "require": { @@ -522,7 +522,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-08-05T15:04:01+00:00" + "time": "2024-08-21T16:01:30+00:00" }, { "name": "illuminate/contracts", @@ -530,12 +530,12 @@ "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "34ead9385e0eab7e947807d77da66faf9bdf95ff" + "reference": "af9b459f195d57f279ec30a45446ddaeb3337bcb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/34ead9385e0eab7e947807d77da66faf9bdf95ff", - "reference": "34ead9385e0eab7e947807d77da66faf9bdf95ff", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/af9b459f195d57f279ec30a45446ddaeb3337bcb", + "reference": "af9b459f195d57f279ec30a45446ddaeb3337bcb", "shasum": "" }, "require": { @@ -570,7 +570,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-08-01T19:08:33+00:00" + "time": "2024-08-21T16:02:16+00:00" }, { "name": "illuminate/filesystem", @@ -578,12 +578,12 @@ "source": { "type": "git", "url": "https://github.com/illuminate/filesystem.git", - "reference": "43b8d2af61dd6ca882e192fa4ca803d2294d4507" + "reference": "7e44057ac57635465c91c3fc6247c5d87bd1feed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/43b8d2af61dd6ca882e192fa4ca803d2294d4507", - "reference": "43b8d2af61dd6ca882e192fa4ca803d2294d4507", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/7e44057ac57635465c91c3fc6247c5d87bd1feed", + "reference": "7e44057ac57635465c91c3fc6247c5d87bd1feed", "shasum": "" }, "require": { @@ -637,7 +637,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-06-28T20:10:30+00:00" + "time": "2024-09-03T13:20:49+00:00" }, { "name": "illuminate/macroable", @@ -691,12 +691,12 @@ "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "089c989beac69bd2633466d72d6ed07068b623ac" + "reference": "51efc0516526413b4be641a1731d15d4394de2ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/089c989beac69bd2633466d72d6ed07068b623ac", - "reference": "089c989beac69bd2633466d72d6ed07068b623ac", + "url": "https://api.github.com/repos/illuminate/support/zipball/51efc0516526413b4be641a1731d15d4394de2ff", + "reference": "51efc0516526413b4be641a1731d15d4394de2ff", "shasum": "" }, "require": { @@ -757,7 +757,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-08-07T14:43:54+00:00" + "time": "2024-09-03T13:21:20+00:00" }, { "name": "illuminate/translation", @@ -877,12 +877,12 @@ "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "cb4374784c87d0a0294e8513a52eb63c0aff3139" + "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cb4374784c87d0a0294e8513a52eb63c0aff3139", - "reference": "cb4374784c87d0a0294e8513a52eb63c0aff3139", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bbd3eef89af8ba66a3aa7952b5439168fbcc529f", + "reference": "bbd3eef89af8ba66a3aa7952b5439168fbcc529f", "shasum": "" }, "require": { @@ -976,7 +976,7 @@ "type": "tidelift" } ], - "time": "2024-07-16T22:29:20+00:00" + "time": "2024-08-19T06:22:39+00:00" }, { "name": "psr/clock", @@ -1206,18 +1206,86 @@ ], "time": "2024-08-06T10:13:52+00:00" }, + { + "name": "symfony/deprecation-contracts", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, { "name": "symfony/finder", "version": "7.2.x-dev", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0470b8dc10ca08d23aeaabbed5b961c928e4d89d" + "reference": "afa87bce0d224a744963ecc8db07b1f0f96a3518" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0470b8dc10ca08d23aeaabbed5b961c928e4d89d", - "reference": "0470b8dc10ca08d23aeaabbed5b961c928e4d89d", + "url": "https://api.github.com/repos/symfony/finder/zipball/afa87bce0d224a744963ecc8db07b1f0f96a3518", + "reference": "afa87bce0d224a744963ecc8db07b1f0f96a3518", "shasum": "" }, "require": { @@ -1268,7 +1336,7 @@ "type": "tidelift" } ], - "time": "2024-08-13T14:29:53+00:00" + "time": "2024-09-03T13:22:29+00:00" }, { "name": "symfony/http-foundation", @@ -1276,12 +1344,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "79d0023c23d92d49c43be47c726e92366b4d3a81" + "reference": "5a92e38016224d35a68ded60297b94d87b21a494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/79d0023c23d92d49c43be47c726e92366b4d3a81", - "reference": "79d0023c23d92d49c43be47c726e92366b4d3a81", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5a92e38016224d35a68ded60297b94d87b21a494", + "reference": "5a92e38016224d35a68ded60297b94d87b21a494", "shasum": "" }, "require": { @@ -1345,7 +1413,7 @@ "type": "tidelift" } ], - "time": "2024-08-13T12:32:31+00:00" + "time": "2024-09-03T14:09:19+00:00" }, { "name": "symfony/mime", @@ -1836,16 +1904,17 @@ "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "a50ed69ace45368e096bc3981f456089253ed6cb" + "reference": "a1c4be3a5885b9785b131c5a30d93e611848cb1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/a50ed69ace45368e096bc3981f456089253ed6cb", - "reference": "a50ed69ace45368e096bc3981f456089253ed6cb", + "url": "https://api.github.com/repos/symfony/translation/zipball/a1c4be3a5885b9785b131c5a30d93e611848cb1d", + "reference": "a1c4be3a5885b9785b131c5a30d93e611848cb1d", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/translation-contracts": "^2.5|^3.0" }, @@ -1922,7 +1991,7 @@ "type": "tidelift" } ], - "time": "2024-08-05T08:22:14+00:00" + "time": "2024-08-19T08:28:32+00:00" }, { "name": "symfony/translation-contracts", @@ -2223,16 +2292,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + "reference": "8520451a140d3f46ac33042715115e290cf5785f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", - "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", "shasum": "" }, "require": { @@ -2272,7 +2341,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" }, "funding": [ { @@ -2280,7 +2349,7 @@ "type": "github" } ], - "time": "2024-02-07T09:43:46+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "filp/whoops", @@ -2508,16 +2577,16 @@ }, { "name": "laravel/pint", - "version": "dev-main", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "e24a6aa83b23c0b64ab3486d68fbc88ab266e03f" + "reference": "e0a8cef58b74662f27355be9cdea0e726bbac362" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/e24a6aa83b23c0b64ab3486d68fbc88ab266e03f", - "reference": "e24a6aa83b23c0b64ab3486d68fbc88ab266e03f", + "url": "https://api.github.com/repos/laravel/pint/zipball/e0a8cef58b74662f27355be9cdea0e726bbac362", + "reference": "e0a8cef58b74662f27355be9cdea0e726bbac362", "shasum": "" }, "require": { @@ -2525,18 +2594,17 @@ "ext-mbstring": "*", "ext-tokenizer": "*", "ext-xml": "*", - "php": "^8.1.0" + "php": "^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.61.1", - "illuminate/view": "^10.48.18", - "larastan/larastan": "^2.9.8", - "laravel-zero/framework": "^10.4.0", - "mockery/mockery": "^1.6.12", + "friendsofphp/php-cs-fixer": "^3.14.4", + "illuminate/view": "^9.51.0", + "laravel-zero/framework": "^9.2.0", + "mockery/mockery": "^1.5.1", + "nunomaduro/larastan": "^2.4.0", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.35.0" + "pestphp/pest": "^1.22.4" }, - "default-branch": true, "bin": [ "builds/pint" ], @@ -2571,7 +2639,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-08-06T15:12:57+00:00" + "time": "2023-02-14T16:31:02+00:00" }, { "name": "mockery/mockery", @@ -3510,16 +3578,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.1", + "version": "1.30.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/5ceb0e384997db59f38774bf79c2a6134252c08f", + "reference": "5ceb0e384997db59f38774bf79c2a6134252c08f", "shasum": "" }, "require": { @@ -3551,9 +3619,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.30.0" }, - "time": "2024-05-31T08:52:43+00:00" + "time": "2024-08-29T09:54:52+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4959,12 +5027,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cc2dd55c49e04b10cec4aa6e47e099a2d1656845" + "reference": "ee7ee22b1dabcff731b7d79d3633c96251ad8404" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cc2dd55c49e04b10cec4aa6e47e099a2d1656845", - "reference": "cc2dd55c49e04b10cec4aa6e47e099a2d1656845", + "url": "https://api.github.com/repos/symfony/console/zipball/ee7ee22b1dabcff731b7d79d3633c96251ad8404", + "reference": "ee7ee22b1dabcff731b7d79d3633c96251ad8404", "shasum": "" }, "require": { @@ -5044,75 +5112,7 @@ "type": "tidelift" } ], - "time": "2024-08-15T22:49:18+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "dev-main", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "default-branch": true, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-08-30T15:40:32+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5511,12 +5511,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "122ba59267fc82caa8f1f509332d4b94a223013b" + "reference": "f74e6c4199357f1b566a67bbe834ebd7de658839" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/122ba59267fc82caa8f1f509332d4b94a223013b", - "reference": "122ba59267fc82caa8f1f509332d4b94a223013b", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/f74e6c4199357f1b566a67bbe834ebd7de658839", + "reference": "f74e6c4199357f1b566a67bbe834ebd7de658839", "shasum": "" }, "require": { @@ -5532,7 +5532,7 @@ "symfony/http-kernel": "^6.4|^7.0", "symfony/process": "^6.4|^7.0", "symfony/uid": "^6.4|^7.0", - "twig/twig": "^3.0.4" + "twig/twig": "^3.12|^4.0" }, "bin": [ "Resources/bin/var-dump-server" @@ -5586,7 +5586,7 @@ "type": "tidelift" } ], - "time": "2024-08-13T14:29:53+00:00" + "time": "2024-09-02T15:30:47+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -5759,13 +5759,12 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "laravel/pint": 20, "larapack/dd": 20 }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.1" + "php": ">=8.0" }, "platform-dev": [], "plugin-api-version": "2.6.0" diff --git a/src/Cart.php b/src/Cart.php index e6ece4f..0361cb6 100644 --- a/src/Cart.php +++ b/src/Cart.php @@ -10,59 +10,22 @@ class Cart { - /** - * the item storage - */ protected $session; - - /** - * the event dispatcher - */ protected $events; - /** - * the cart session key - */ protected $instanceName; - /** - * the session key use for the cart - */ protected $sessionKey; - - /** - * the session key use to persist cart items - */ protected $sessionKeyCartItems; - - /** - * the session key use to persist cart conditions - */ protected $sessionKeyCartConditions; - /** - * Configuration to pass to ItemCollection - */ protected $config; - /** - * This holds the currently added item id in cart for association - */ protected $currentItemId; - /** - * The number of decimals to use when formatting prices - */ protected $decimals; - - /** - * The decimal point character - */ protected $decPoint; - /** - * our object constructor - */ public function __construct($session, $events, $instanceName, $session_key, $config) { $this->events = $events; @@ -73,6 +36,7 @@ public function __construct($session, $events, $instanceName, $session_key, $con $this->sessionKeyCartConditions = $this->sessionKey . '_cart_conditions'; $this->config = $config; $this->currentItemId = null; + $this->fireEvent('created'); } @@ -109,20 +73,16 @@ public function getInstanceName() /** * get an item on a cart by item ID - * - * @return mixed */ - public function get($itemId) + public function get($itemId): ?ItemCollection { return $this->getContent()->get($itemId); } /** - * check if an item exists by item ID - * - * @return bool + * check if we have this item in the cart by item ID */ - public function has($itemId) + public function has($itemId): bool { return $this->getContent()->has($itemId); } @@ -144,10 +104,9 @@ public function add( array|CartCondition $conditions = [], ?string $associatedModel = null ): self { - // check if its an array of items + // check if we are adding an array of items if (is_array($id)) { - // the first argument is an array, now we will need to check if it is a multi dimensional - // array, if so, we will iterate through each item and call add again + // check if we are adding a multi dimensional array if (Helpers::isMultiArray($id)) { foreach ($id as $item) { $this->add( @@ -175,6 +134,7 @@ public function add( return $this; } + // if we are here, we are adding a single item $data = [ 'id' => $id, 'name' => $name, @@ -191,11 +151,8 @@ public function add( // validate data $item = $this->validate($data); - // get the cart - $inCart = $this->get($id); - // if the item is already in the cart we will just update it - if ($inCart) { + if ($this->get($id)) { $this->update($id, $item); } else { $this->addItem($id, $item); @@ -337,6 +294,9 @@ public function clear() return true; } + /** + * Adds a condition to the cart + */ /** * Adds a condition to the cart * @@ -378,11 +338,27 @@ public function condition(array|CartCondition $condition): self /** * get conditions applied on the cart * - * @return CartConditionCollection + * @param bool $active - If true, only conditions actually applied on the cart are returned */ - public function getConditions() + public function getConditions(bool $active = false): CartConditionCollection { - return new CartConditionCollection($this->session->get($this->sessionKeyCartConditions)); + $conditions = new CartConditionCollection( + $this->session->get($this->sessionKeyCartConditions) + ); + + if ($active) { + return $conditions->filter(function (CartCondition $condition) { + $amount = $condition->getTarget() == 'subtotal' ? $this->getSubTotal(false) : $this->getTotal(); + + return $condition->getMinimum() === null || $condition->getMinimum() <= $amount; + })->filter(function (CartCondition $condition) { + $amount = $condition->getTarget() == 'subtotal' ? $this->getSubTotal(false) : $this->getTotal(); + + return $condition->getMaximum() === null || $condition->getMaximum() >= $amount; + }); + } + + return $conditions; } /** @@ -411,12 +387,8 @@ public function getConditionsByType($type) /** * Remove all the condition with the $type specified - * Please Note that this will only remove condition added on cart bases, not those conditions added - * specifically on an per item bases - * - * @return $this */ - public function removeConditionsByType($type) + public function removeConditionsByType(string $type) { $this->getConditionsByType($type)->each(function ($condition) { $this->removeCartCondition($condition->getName()); @@ -424,12 +396,7 @@ public function removeConditionsByType($type) } /** - * removes a condition on a cart by condition name, - * this can only remove conditions that are added on cart bases not conditions that are added on an item/product. - * If you wish to remove a condition that has been added for a specific item/product, you may - * use the removeItemCondition(itemId, conditionName) method instead. - * - * @return void + * Remove a condition from the cart */ public function removeCartCondition($conditionName) { @@ -575,6 +542,12 @@ public function getSubTotal($formatted = true): int|float|string ->filter(function (CartCondition $condition) { return $condition->getTarget() === 'subtotal'; }) + ->filter(function (CartCondition $condition) use ($subTotalSum) { + return $condition->getMinimum() === null || $condition->getMinimum() <= $subTotalSum; + }) + ->filter(function (CartCondition $condition) use ($subTotalSum) { + return $condition->getMaximum() === null || $condition->getMaximum() >= $subTotalSum; + }) ->sortBy(function (CartCondition $condition) { return $condition->getOrder(); }); @@ -607,6 +580,15 @@ public function getTotal(): int|float $conditionsForTotal = $this->getConditions() ->filter(function (CartCondition $condition) { return $condition->getTarget() === 'total'; + }) + ->filter(function (CartCondition $condition) use ($subTotal) { + return $condition->getMinimum() === null || $condition->getMinimum() <= $subTotal; + }) + ->filter(function (CartCondition $condition) use ($subTotal) { + return $condition->getMaximum() === null || $condition->getMaximum() >= $subTotal; + }) + ->sortBy(function (CartCondition $condition) { + return $condition->getOrder(); }); // if no conditions were added, just return the sub total diff --git a/src/CartCondition.php b/src/CartCondition.php index a511cc6..4e9106b 100644 --- a/src/CartCondition.php +++ b/src/CartCondition.php @@ -65,6 +65,22 @@ public function getType() return $this->args['type']; } + /** + * Get the minimum value of this condition + */ + public function getMinimum(): int|float|null + { + return $this->args['minimum'] ?? null; + } + + /** + * Get the maximum value of this condition + */ + public function getMaximum(): int|float|null + { + return $this->args['maximum'] ?? null; + } + /** * get the additional attributes of a condition * diff --git a/tests/Unit/CartConditionsTest.php b/tests/Unit/CartConditionsTest.php index 4d43a8d..26e01b9 100644 --- a/tests/Unit/CartConditionsTest.php +++ b/tests/Unit/CartConditionsTest.php @@ -792,40 +792,6 @@ expect($giftCard->getCalculatedValue())->toEqual(0, 'Gift Card value should be 0'); }); - test('can be cleared', function () { - $siteWideDiscount = new CartCondition([ - 'name' => 'Site Wide Discount', - 'type' => 'discount', - 'target' => 'total', - 'value' => '-5%', - ]); - - $giftCard = new CartCondition([ - 'name' => 'Gift Card', - 'type' => 'gift_card', - 'target' => 'total', - 'value' => '-25', - ]); - - $item = [ - 'id' => 1, - 'name' => 'Backpack', - 'price' => 168.72, - 'quantity' => 1, - 'attributes' => [], - ]; - - $this->cart->add($item); - - $this->cart->condition([$siteWideDiscount, $giftCard]); - - expect($this->cart->getConditions()->count())->toEqual(2, 'Cart should have two conditions'); - - $this->cart->clearCartConditions(); - - expect($this->cart->getConditions()->count())->toEqual(0, 'Cart should have no conditions now'); - }); - test('calculate the subtotal correctly when one condition makes it $0, and the other adds $10', function () { $this->cart->add([ [ @@ -915,6 +881,214 @@ expect($this->cart->getSubTotal())->toEqual(10.00, 'Cart should have sub total of 10.00'); expect($this->cart->getTotal())->toEqual(10.00, 'Cart should have a total of 10.00'); }); + + test('can activate a dollar discount when a minimum amount is met', function () { + $condition = new CartCondition([ + 'name' => 'Get $10 off when you spend $100', + 'type' => 'target', + 'target' => 'total', + 'value' => '-10', + 'minimum' => '100', + ]); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 1, + 'attributes' => [], + ]); + + $this->cart->condition($condition); + + expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90'); + + $this->cart->clear(); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 2, + 'attributes' => [], + ]); + + expect($this->cart->getTotal())->toEqual(170, 'Cart should have total of 170'); + }); + + test('can activate a percentage discount when a minimum amount is met', function () { + $condition = new CartCondition([ + 'name' => 'Get 10% off when you spend $100', + 'type' => 'target', + 'target' => 'total', + 'value' => '-10%', + 'minimum' => '100', + ]); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 1, + 'attributes' => [], + ]); + + $this->cart->condition($condition); + + expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90'); + + $this->cart->clear(); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 2, + 'attributes' => [], + ]); + + expect($this->cart->getTotal())->toEqual(162, 'Cart should have total of 162'); + }); + + test('can remove a dollar value condition when a maximum is met', function () { + $condition = new CartCondition([ + 'name' => 'Shipping', + 'type' => 'target', + 'target' => 'total', + 'value' => '18', + 'maximum' => '160', + ]); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 1, + 'attributes' => [], + ]); + + $this->cart->condition($condition); + + expect($this->cart->getTotal())->toEqual(108, 'Cart should have total of 108'); + + $this->cart->clear(); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 2, + 'attributes' => [], + ]); + + expect($this->cart->getTotal())->toEqual(180, 'Cart should have total of 180'); + }); + + test('can remove a percentage condition when a maximum is met', function () { + $condition = new CartCondition([ + 'name' => 'Low Cart Tax', + 'type' => 'target', + 'target' => 'total', + 'value' => '10%', + 'maximum' => '140', + ]); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 1, + 'attributes' => [], + ]); + + $this->cart->condition($condition); + + expect($this->cart->getTotal())->toEqual(99, 'Cart should have total of 99'); + + $this->cart->clear(); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 2, + 'attributes' => [], + ]); + + expect($this->cart->getTotal())->toEqual(180, 'Cart should have total of 180'); + }); + + test('return cart conditions only when they are active', function () { + $condition = new CartCondition([ + 'name' => 'Get $10 off when you spend $100', + 'type' => 'target', + 'target' => 'total', + 'value' => '-10', + 'minimum' => '100', + ]); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 1, + 'attributes' => [], + ]); + + $this->cart->condition($condition); + + expect($this->cart->getConditions(active: true))->toHaveCount(0, 'Cart should have 0 conditions'); + + $this->cart->clear(); + + $this->cart->add([ + 'id' => 1, + 'name' => 'Apple iPhone 15', + 'price' => 90, + 'quantity' => 2, + 'attributes' => [], + ]); + + expect($this->cart->getConditions(active: true))->toHaveCount(1, 'Cart should have 1 condition'); + }); + + test('can be removed by condition name', function () { + $shipping = new CartCondition([ + 'name' => 'Shipping', + 'type' => 'shipping', + 'target' => 'total', + 'value' => '12', + ]); + + $tax = new CartCondition([ + 'name' => 'Tax', + 'type' => 'tax', + 'target' => 'total', + 'value' => '2.50', + ]); + + $item = [ + 'id' => 456, + 'name' => 'Sample Item 1', + 'price' => 100, + 'quantity' => 1, + 'attributes' => [], + ]; + + $this->cart->add($item); + + $this->cart->condition([$shipping, $tax]); + + // let's prove first we have now two conditions in the cart + expect($this->cart->getConditions()->count())->toEqual(2, 'Cart should have two conditions'); + + // now let's remove a specific condition by condition name + $this->cart->removeCartCondition('Tax'); + + // cart should have now only 1 condition + expect($this->cart->getConditions()->count())->toEqual(1, 'Cart should have one condition'); + expect($this->cart->getConditions()->first()->getName())->toEqual('Shipping'); + }); }); describe('item level conditions', function () { @@ -1082,110 +1256,36 @@ expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(0.00, "The item's price should be prevented from going below zero."); }); - // test('get cart condition by condition name', function () { - // $itemCondition1 = new CartCondition([ - // 'name' => 'SALE 5%', - // 'type' => 'sale', - // 'target' => 'total', - // 'value' => '-5%', - // ]); - // $itemCondition2 = new CartCondition([ - // 'name' => 'Item Gift Pack 25.00', - // 'type' => 'promo', - // 'target' => 'total', - // 'value' => '-25', - // ]); + test('can be removed by condition name', function () { + $heavyItemSurcharge = new CartCondition([ + 'name' => 'Heavy Item Surcharge', + 'type' => 'surcharge', + 'value' => '10', + ]); - // $item = [ - // 'id' => 456, - // 'name' => 'Sample Item 1', - // 'price' => 100, - // 'quantity' => 1, - // 'attributes' => [], - // ]; + $tax = new CartCondition([ + 'name' => 'Tax', + 'type' => 'tax', + 'value' => '10%', + ]); - // $this->cart->add($item); + $item = [ + 'id' => 12, + 'name' => 'Beer (1 box)', + 'price' => 410.60, + 'quantity' => 1, + 'attributes' => [], + 'conditions' => [$heavyItemSurcharge, $tax], + ]; - // $this->cart->condition([$itemCondition1, $itemCondition2]); + $this->cart->add($item); - // // get a condition applied on cart by condition name - // $condition = $this->cart->getCondition($itemCondition1->getName()); + expect($this->cart->get(12)['conditions'])->toHaveCount(2, 'Item should have two conditions'); - // expect('SALE 5%')->toEqual($condition->getName()); - // expect('total')->toEqual($condition->getTarget()); - // expect('sale')->toEqual($condition->getType()); - // expect('-5%')->toEqual($condition->getValue()); - // }); + $this->cart->removeItemCondition(12, 'Tax'); - // test('remove cart condition by condition name', function () { - // $itemCondition1 = new CartCondition([ - // 'name' => 'SALE 5%', - // 'type' => 'sale', - // 'target' => 'total', - // 'value' => '-5%', - // ]); - // $itemCondition2 = new CartCondition([ - // 'name' => 'Item Gift Pack 25.00', - // 'type' => 'promo', - // 'target' => 'total', - // 'value' => '-25', - // ]); - - // $item = [ - // 'id' => 456, - // 'name' => 'Sample Item 1', - // 'price' => 100, - // 'quantity' => 1, - // 'attributes' => [], - // ]; - - // $this->cart->add($item); - - // $this->cart->condition([$itemCondition1, $itemCondition2]); - - // // let's prove first we have now two conditions in the cart - // expect($this->cart->getConditions()->count())->toEqual(2, 'Cart should have two conditions'); - - // // now let's remove a specific condition by condition name - // $this->cart->removeCartCondition('SALE 5%'); - - // // cart should have now only 1 condition - // expect($this->cart->getConditions()->count())->toEqual(1, 'Cart should have one condition'); - // expect($this->cart->getConditions()->first()->getName())->toEqual('Item Gift Pack 25.00'); - // }); - - // test('remove item condition by condition name', function () { - // $itemCondition1 = new CartCondition([ - // 'name' => 'SALE 5%', - // 'type' => 'sale', - // 'value' => '-5%', - // ]); - // $itemCondition2 = new CartCondition([ - // 'name' => 'Item Gift Pack 25.00', - // 'type' => 'promo', - // 'value' => '-25', - // ]); - - // $item = [ - // 'id' => 456, - // 'name' => 'Sample Item 1', - // 'price' => 100, - // 'quantity' => 1, - // 'attributes' => [], - // 'conditions' => [$itemCondition1, $itemCondition2], - // ]; - - // $this->cart->add($item); - - // // let's very first the item has 2 conditions in it - // expect($this->cart->get(456)['conditions'])->toHaveCount(2, 'Item should have two conditions'); - - // // now let's remove a condition on that item using the condition name - // $this->cart->removeItemCondition(456, 'SALE 5%'); - - // // now we should have only 1 condition left on that item - // expect($this->cart->get(456)['conditions'])->toHaveCount(1, 'Item should have one condition left'); - // }); + expect($this->cart->get(12)['conditions'])->toHaveCount(1, 'Item should have one condition'); + }); // test('remove item condition by condition name scenario two', function () { // // NOTE: in this scenario, we will add the conditions not in array format @@ -1602,4 +1702,37 @@ expect($this->cart->getConditions()->count())->toEqual(0, 'Cart should have no conditions now'); expect($this->cart->get(1)['conditions'])->toHaveCount(0, 'Cart items should have no conditions now'); }); + + // test('remove item condition by condition name', function () { + // $itemCondition1 = new CartCondition([ + // 'name' => 'SALE 5%', + // 'type' => 'sale', + // 'value' => '-5%', + // ]); + // $itemCondition2 = new CartCondition([ + // 'name' => 'Item Gift Pack 25.00', + // 'type' => 'promo', + // 'value' => '-25', + // ]); + + // $item = [ + // 'id' => 456, + // 'name' => 'Sample Item 1', + // 'price' => 100, + // 'quantity' => 1, + // 'attributes' => [], + // 'conditions' => [$itemCondition1, $itemCondition2], + // ]; + + // $this->cart->add($item); + + // // let's very first the item has 2 conditions in it + // expect($this->cart->get(456)['conditions'])->toHaveCount(2, 'Item should have two conditions'); + + // // now let's remove a condition on that item using the condition name + // $this->cart->removeItemCondition(456, 'SALE 5%'); + + // // now we should have only 1 condition left on that item + // expect($this->cart->get(456)['conditions'])->toHaveCount(1, 'Item should have one condition left'); + // }); });