├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── phpunit.xml.dist ├── ruleset.xml ├── src ├── Collection │ └── ItemPriceCollection.php ├── Description │ ├── AbstractPriceDescription.php │ └── PriceDescriptionInterface.php ├── Modifier │ ├── AbstractPriceModifier.php │ ├── DiscountPrice.php │ ├── ItemComparatorInterface.php │ ├── PriceModifierInterface.php │ └── TaxPrice.php ├── PricingFactory.php ├── Total │ └── PriceTotalInterface.php └── Type │ ├── ItemPrice.php │ ├── PriceInterface.php │ └── UnitPrice.php └── tests └── Unit ├── Collection └── ItemPriceCollectionTest.php ├── Description └── AbstractPriceDescriptionTest.php ├── Modifier ├── AbstractPriceModifierTest.php ├── DiscountPriceTest.php └── TaxPriceTest.php ├── PricingFactoryTest.php └── Type ├── ItemPriceTest.php └── UnitPriceTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /vendor/ 3 | /composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | - 5.6 6 | - 7.0 7 | - 7.1 8 | - 7.2 9 | - hhvm 10 | before_script: 11 | - composer install 12 | script: 13 | - ./vendor/bin/phpunit --coverage-text --coverage-clover ./build/logs/clover.xml --verbose 14 | - ./vendor/bin/phpcs --extensions=php --standard=ruleset.xml ./src ./tests 15 | after_script: 16 | - php ./vendor/bin/coveralls -v 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blesta/pricing 2 | 3 | [![Build Status](https://travis-ci.org/blesta/pricing.svg?branch=master)](https://travis-ci.org/blesta/pricing) [![Coverage Status](https://coveralls.io/repos/github/blesta/pricing/badge.svg?branch=master)](https://coveralls.io/github/blesta/pricing?branch=master) 4 | 5 | A library for handling pricing. Supports: 6 | 7 | - Unit Prices 8 | - Item Prices 9 | - Unit Price that may include discounts and taxes 10 | - Discounts 11 | - Percentages 12 | - Fixed amounts 13 | - Taxes (inclusive_calculated, inclusive, exclusive) 14 | - Inclusive and Exclusive 15 | - Applied in sequence or compounded 16 | - Inclusive calculated is meant to be subtracted from the item price 17 | - Item Collection 18 | - Iterate over Item Prices 19 | - Aggregate totals over Item Prices 20 | 21 | ## Installation 22 | 23 | Install via composer: 24 | 25 | ```sh 26 | composer require blesta/pricing 27 | ``` 28 | 29 | ## Basic Usage 30 | 31 | ### UnitPrice 32 | 33 | ```php 34 | use Blesta\Pricing\Type\UnitPrice; 35 | 36 | $price = new UnitPrice(5.00, 2, "id"); 37 | $price->setDescription("2 X 5.00"); 38 | $unit_price = $price->price(); // 5.00 39 | $qty = $price->qty(); // 2 40 | $total = $price->total(); // 10.00 41 | $key = $price->key(); // id 42 | 43 | // Update the unit price, quantity, and key 44 | $price->setPrice(10.00); 45 | $price->setQty(3); 46 | $price->setKey('id2'); 47 | ``` 48 | 49 | ### DiscountPrice 50 | 51 | ```php 52 | use Blesta\Pricing\Modifier\DiscountPrice; 53 | 54 | $discount = new DiscountPrice(25.00, "percent"); 55 | $discount->setDescription("25% off"); 56 | $price_after_discount = $discount->off(100.00); // 75.00 57 | $discount_price = $discount->on(100.00); // 25.00 58 | ``` 59 | 60 | ### TaxPrice 61 | 62 | Exclusive tax (price does not include tax): 63 | 64 | ```php 65 | use Blesta\Pricing\Modifier\TaxPrice; 66 | 67 | $tax = new TaxPrice(10.00, TaxPrice::EXCLUSIVE); 68 | $tax->setDescription("10 % tax"); 69 | $tax->on(100.00); // 10.00 70 | $tax->off(100.00); // 100.00 (price on exclusive tax doesn't include tax, so nothing to take off) 71 | $tax->including(100.00); // 110.00 72 | ``` 73 | 74 | Inclusive tax (price already includes tax): 75 | 76 | ```php 77 | use Blesta\Pricing\Modifier\TaxPrice; 78 | 79 | $tax = new TaxPrice(25.00, TaxPrice::INCLUSIVE); 80 | $tax->setDescription("25 % tax"); 81 | $tax->on(100.00); // 25.00 82 | $tax->off(100.00); // 75.00 83 | $tax->including(100.00); // 100.00 84 | ``` 85 | 86 | Inclusive tax (price already includes tax) calculated based on the price minus tax: 87 | 88 | ```php 89 | use Blesta\Pricing\Modifier\TaxPrice; 90 | 91 | $tax = new TaxPrice(25.00, TaxPrice::INCLUSIVE_CALCULATED); 92 | $tax->setDescription("25 % tax"); 93 | $tax->on(100.00); // 20.00 94 | $tax->off(100.00); // 80.00 95 | $tax->including(100.00); // 100.00 96 | ``` 97 | 98 | Cascading tax (tax on a tax): 99 | 100 | ```php 101 | use Blesta\Pricing\Modifier\TaxPrice; 102 | use Blesta\Pricing\Type\UnitPrice; 103 | 104 | $price = new UnitPrice(10.00); 105 | $tax1 = new TaxPrice(10.00, TaxPrice::EXCLUSIVE); 106 | $tax2 = new TaxPrice(5.00, TaxPrice::EXCLUSIVE); 107 | $tax2->on( 108 | $tax1->on( 109 | $price->total() 110 | ) 111 | + $price->total() 112 | ); // 0.55 = [((10.00 * 0.10) + 10.00) * 0.05] 113 | ``` 114 | 115 | ### ItemPrice 116 | 117 | ```php 118 | use Blesta\Pricing\Type\ItemPrice; 119 | 120 | $item_price = new ItemPrice(10.00, 3); 121 | $item_price->total(); // 30.00 122 | ``` 123 | 124 | With discount applied: 125 | 126 | ```php 127 | use Blesta\Pricing\Modifier\DiscountPrice; 128 | 129 | $discount = new DiscountPrice(5.00, "percent"); 130 | 131 | // call setDiscount() as many times as needed to apply discounts 132 | $item_price->setDiscount($discount); 133 | $item_price->totalAfterDiscount(); // 28.50 134 | ``` 135 | 136 | Amount applied for a specific discount: 137 | 138 | ```php 139 | use Blesta\Pricing\Modifier\DiscountPrice; 140 | 141 | $item_price = new ItemPrice(10.00, 3); 142 | 143 | $discount1 = new DiscountPrice(5.00, "percent"); 144 | $discount2 = new DiscountPrice(25.00, "percent"); 145 | 146 | // NOTE: Order matters here 147 | $item_price->setDiscount($discount1); 148 | $item_price->setDiscount($discount2); 149 | 150 | $item_price->discountAmount($discount1); // 1.50 151 | $item_price->discountAmount($discount2); // 7.125 ((30.00 - 1.50) * 0.25) 152 | ``` 153 | 154 | With tax applied: 155 | 156 | ```php 157 | use Blesta\Pricing\Modifier\TaxPrice; 158 | 159 | $tax = new TaxPrice(10.00, TaxPrice::EXCLUSIVE); 160 | 161 | // call setTax() as many times as needed to apply multiple levels of taxes 162 | $item_price->setTax($tax); 163 | // pass as many TaxPrice objects to setTax as you want to compound tax 164 | // ex. $item_price->setTax($tax1, $tax2, ...); 165 | $item_price->totalAfterTax(); // 32.1375 = (subtotal + ([subtotal - discounts] * taxes)) = (30 + [30 - (1.50 + 7.125)] * 0.10) 166 | ``` 167 | 168 | With tax and discount: 169 | 170 | ```php 171 | $item_price->total(); // 23.5125 = (subtotal - discounts + ([subtotal - discounts] * taxes)) = (30 - (1.50 + 7.125) + [30 - (1.50 + 7.125)] * 0.10) 172 | ``` 173 | 174 | With tax and discount where the discount does *not* apply to the taxes: 175 | 176 | ```php 177 | $item_price->setDiscountTaxes(false); 178 | $item_price->total(); // 24.375 = (subtotal - discounts + ([subtotal] * taxes)) = (30 - (1.50 + 7.125) + ([30] * 0.10)) 179 | ``` 180 | 181 | Without taxes of the 'exclusive' type: 182 | 183 | ```php 184 | $item_price->setDiscountTaxes(true); 185 | $item_price->excludeTax(TaxPrice::EXCLUSIVE)->totalAfterTax(); // 30.00 = (30 + [30 - (1.50 + 7.125)] * 0) 186 | $item_price->total(); // 21.375 = (30 - (1.50 + 7.125) + [30 - (1.50 + 7.125)] * 0) 187 | 188 | // Be sure to reset the excluded taxes before attempting to fetch totals that should include them again! 189 | $item_price->resetTaxes(); 190 | $item_price->total(); // 23.5125 = (subtotal - discounts + ([subtotal - discounts] * taxes)) = (30 - (1.50 + 7.125) + [30 - (1.50 + 7.125)] * 0.10) 191 | $item_price->excludeTax(TaxPrice::EXCLUSIVE)->total(); // 21.375 = (30 - (1.50 + 7.125) + [30 - (1.50 + 7.125)] * 0) 192 | $item_price->resetTaxes(); 193 | ``` 194 | 195 | Amount applied for a specific tax: 196 | 197 | ```php 198 | use Blesta\Pricing\Modifier\TaxPrice; 199 | 200 | $tax1 = new TaxPrice(10.00, TaxPrice::EXCLUSIVE); 201 | $tax2 = new TaxPrice(5.00, TaxPrice::INCLUSIVE); 202 | 203 | // NOTE: order *DOES NOT* matter 204 | $item_price->setTax($tax1); 205 | $item_price->setTax($tax2); 206 | 207 | $item_price->taxAmount($tax1); // 2.1375 = ([subtotal - discounts] * taxes) = ([30 - (1.50 + 7.125)] * 0.10) 208 | $item_price->taxAmount($tax2); // 1.06875 = ([subtotal - discounts] * taxes) = ([30 - (1.50 + 7.125)] * 0.05) 209 | ``` 210 | 211 | Without taxes of the 'exclusive' type: 212 | 213 | ```php 214 | $item_price->excludeTax(TaxPrice::EXCLUSIVE)->totalAfterTax(); // 31.06875 = (subtotal + ([subtotal - discounts] * taxes)) = (30 + [30 - (1.50 + 7.125)] * 0.05) 215 | $item_price->resetTaxes(); 216 | ``` 217 | 218 | Cascading tax: 219 | 220 | ```php 221 | use Blesta\Pricing\Modifier\TaxPrice; 222 | use Blesta\Pricing\Type\ItemPrice; 223 | 224 | $item_price = new ItemPrice(10.00, 3); 225 | 226 | $tax1 = new TaxPrice(10.00, TaxPrice::EXCLUSIVE); 227 | $tax2 = new TaxPrice(5.00, TaxPrice::INCLUSIVE); 228 | $tax3 = new TaxPrice(2.50, TaxPrice::EXCLUSIVE); 229 | 230 | $item_price->setTax($tax1, $tax2, $tax3); 231 | $item_price->taxAmount($tax1); // 3.00 = ([subtotal - discounts] * taxes) = ([30 - 0] * 0.10) 232 | $item_price->taxAmount($tax2); // 1.65 = ([subtotal - discounts + previous-taxes] * 0.05) = ([30.00 - 0 + 3.00] * 0.05) 233 | $item_price->taxAmount($tax3); // 0.86625 = ([subtotal - discounts + previous-taxes] * 0.025) = ([30.00 - 0 + 3.00 + 1.65] * 0.025) 234 | $item_price->taxAmount(); // 5.51625 235 | 236 | // Exclude taxes of the 'inclusive' type 237 | $item_price->excludeTax(TaxPrice::INCLUSIVE); 238 | $item_price->taxAmount($tax1); // 3.00 = ([subtotal - discounts] * taxes) = ([30 - 0] * 0.10) 239 | $item_price->taxAmount($tax2); // 0 = ([subtotal - discounts + previous-taxes] * 0) = ([30.00 - 0 + 3.00] * 0) 240 | $item_price->taxAmount($tax3); // 0.86625 = ([subtotal - discounts + previous-taxes] * 0.025) = ([30.00 - 0 + 3.00 + 1.65] * 0.025) 241 | $item_price->taxAmount(); // 3.86625 242 | $item_price->resetTaxes(); 243 | ``` 244 | 245 | ### ItemPriceCollection 246 | 247 | ```php 248 | use Blesta\Pricing\Collection\ItemPriceCollection; 249 | use Blesta\Pricing\Type\ItemPrice; 250 | 251 | $item_collection = new ItemPriceCollection(); 252 | 253 | $item1 = new ItemPrice(10.00, 3); 254 | $item2 = new ItemPrice(25.00, 2); 255 | $item_collection->append($item1)->append($item2); 256 | 257 | $item_collection->total(); // 80.00 258 | 259 | foreach ($item_collection as $item) { 260 | $item->total(); // 30.00, 50.00 261 | } 262 | ``` 263 | 264 | ### PricingFactory 265 | 266 | Using the PricingFactory can streamline usage. Assume you have the following: 267 | 268 | ```php 269 | $products = array( 270 | array('desc' => 'Apples', 'amount' => 0.5, 'qty' => 3), 271 | array('desc' => 'Oranges', 'amount' => 0.75, 'qty' => 10) 272 | ); 273 | ``` 274 | 275 | So we initialize our PricingFactory, and let it create our DiscountPrice and TaxPrice objects for use. 276 | 277 | ```php 278 | use Blesta\Pricing\PricingFactory; 279 | 280 | $pricing_factory = new PricingFactory(); 281 | 282 | // Some coupon 283 | $discount = $pricing_factory->discountPrice(50.00, "percent"); 284 | $discount->setDescription('Super-Saver Coupon'); 285 | 286 | // Typical local sales tax 287 | $tax = $pricing_factory->taxPrice(10.00, TaxPrice::EXCLUSIVE); 288 | $tax->setDescription("Sales tax"); 289 | ``` 290 | 291 | Then we let the PricingFactory initialize our ItemPriceCollection, and each ItemPrice over our data set. 292 | 293 | ```php 294 | $item_collection = $pricing_factory->itemPriceCollection(); 295 | 296 | foreach ($products as $product) { 297 | $item = $pricing_factory->itemPrice($product['amount'], $product['qty']); 298 | $item->setDescription($product['desc']); 299 | $item->setTax($tax); 300 | 301 | if ('Apples' === $product['desc']) { 302 | $item->setDiscount($discount); 303 | } 304 | $item_collection->append($item); 305 | } 306 | 307 | $item_collection->discountAmount($discount); // 0.75 308 | $item_collection->taxAmount($tax); // 0.825 309 | $item_collection->subtotal(); // 9.00 310 | $item_collection->totalAfterTax(); // 9.825 311 | $item_collection->totalAfterDiscount(); // 8.25 312 | $item_collection->total(); // 9.075 313 | ``` 314 | 315 | You may also exclude specific taxes by their type when calculating totals: 316 | 317 | ```php 318 | $item_collection->excludeTax(TaxPrice::EXCLUSIVE)->taxAmount($tax); // 0.00 319 | $item_collection->excludeTax(TaxPrice::EXCLUSIVE)->totalAfterTax(); // 9.00 320 | $item_collection->excludeTax(TaxPrice::EXCLUSIVE)->total(); // 8.25 321 | $item_collection->total(); // 9.075 (item tax exclusions in the collection are reset after each call to a ::total..., the ::taxAmount, or ::discountAmount) 322 | ``` 323 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blesta/pricing", 3 | "description": "A library for handling pricing and pricing modifiers", 4 | "keywords": ["pricing", "tax", "discount", "totals"], 5 | "homepage": "http://github.com/blesta/pricing", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Cody Phillips", 10 | "email": "therealclphillips@gmail.com" 11 | }, 12 | { 13 | "name": "Tyson Phillips" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=5.4.0" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "~4.6", 21 | "squizlabs/php_codesniffer": "~2.3", 22 | "satooshi/php-coveralls": "^1.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Blesta\\Pricing\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Blesta\\Pricing\\Tests\\": "tests/" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | tests/ 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | src/ 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PSR2 without namespace enforcement. 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Collection/ItemPriceCollection.php: -------------------------------------------------------------------------------- 1 | collection[] = $price; 36 | return $this; 37 | } 38 | 39 | /** 40 | * Removes an ItemPrice from the collection 41 | * 42 | * @param ItemPrice $price An item to remove from the collection 43 | * @return ItemPriceCollection reference to this 44 | */ 45 | #[\ReturnTypeWillChange] 46 | public function remove(ItemPrice $price) 47 | { 48 | // Remove all instances of the price from the collection 49 | foreach ($this->collection as $index => $item) { 50 | if ($item === $price) { 51 | unset($this->collection[$index]); 52 | } 53 | } 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * Retrieves the count of all ItemPrice objects in the collection 60 | * 61 | * @return int The number of ItemPrice objects in the collection 62 | */ 63 | #[\ReturnTypeWillChange] 64 | public function count() 65 | { 66 | return count($this->collection); 67 | } 68 | 69 | /** 70 | * Retrieves the total price of all items within the collection including taxes without discounts 71 | * 72 | * @return float The total price including taxes without including discounts 73 | */ 74 | #[\ReturnTypeWillChange] 75 | public function totalAfterTax() 76 | { 77 | $total = 0; 78 | foreach ($this->collection as $item) { 79 | $total += $item->totalAfterTax(); 80 | } 81 | 82 | // Reset any discount amounts or excluded tax types back 83 | $this->resetDiscounts(); 84 | $this->resetTaxes(); 85 | 86 | return $total; 87 | } 88 | 89 | /** 90 | * Retrieves the total price of all items within the collection including discounts without taxes 91 | * 92 | * @return float The total price including discounts without including taxes 93 | */ 94 | #[\ReturnTypeWillChange] 95 | public function totalAfterDiscount() 96 | { 97 | $total = 0; 98 | foreach ($this->collection as $item) { 99 | $total += $item->totalAfterDiscount(); 100 | } 101 | 102 | // Reset any discount amounts or excluded tax types back 103 | $this->resetDiscounts(); 104 | $this->resetTaxes(); 105 | 106 | return $total; 107 | } 108 | 109 | /** 110 | * Retrieves the subtotal of all items within the collection 111 | * 112 | * @return float The subtotal of all items in the collection 113 | */ 114 | #[\ReturnTypeWillChange] 115 | public function subtotal() 116 | { 117 | // Sum the subtotals of each ItemPrice 118 | $total = 0; 119 | foreach ($this->collection as $item_price) { 120 | $total += $item_price->subtotal(); 121 | } 122 | $this->resetDiscounts(); 123 | 124 | return $total; 125 | } 126 | 127 | /** 128 | * Retrieves the total of all items within the collection 129 | * 130 | * @return float The total of all items in the collection 131 | */ 132 | #[\ReturnTypeWillChange] 133 | public function total() 134 | { 135 | // Sum the totals of each ItemPrice 136 | $total = 0; 137 | foreach ($this->collection as $item_price) { 138 | $total += $item_price->total(); 139 | } 140 | 141 | // Reset any discount amounts or excluded tax types back 142 | $this->resetDiscounts(); 143 | $this->resetTaxes(); 144 | 145 | return $total; 146 | } 147 | 148 | /** 149 | * Retrieves the total tax amount for all ItemPrice's within the collection 150 | * 151 | * @param TaxPrice $tax A TaxPrice to apply to all ItemPrice's in the collection, ignoring 152 | * any TaxPrice's that may already be set on the items within the collection (optional) 153 | * @param string $type The type of tax for which to retrieve amounts (optional) 154 | */ 155 | #[\ReturnTypeWillChange] 156 | public function taxAmount(TaxPrice $tax = null, $type = null) 157 | { 158 | $total = 0; 159 | foreach ($this->collection as $item_price) { 160 | $total += $item_price->taxAmount($tax, $type); 161 | } 162 | 163 | // Reset any discount amounts or excluded tax types back 164 | $this->resetDiscounts(); 165 | $this->resetTaxes(); 166 | 167 | return $total; 168 | } 169 | 170 | /** 171 | * Retrieves the total discount amount for all items within the collection 172 | * 173 | * @param DiscountPrice $discount A DiscountPrice to apply to all ItemPrice's in the 174 | * collection, ignoring any DiscountPrice's that may already be set on the items within 175 | * the collection (optional) 176 | */ 177 | #[\ReturnTypeWillChange] 178 | public function discountAmount(DiscountPrice $discount = null) 179 | { 180 | // Apply the given discount to all items 181 | $total = 0; 182 | // Calculate the discount amount from each item's own discounts 183 | foreach ($this->collection as $item_price) { 184 | $total += $item_price->discountAmount($discount); 185 | } 186 | 187 | // Reset any discount amounts or excluded tax types back 188 | $this->resetDiscounts(); 189 | $this->resetTaxes(); 190 | 191 | return $total; 192 | } 193 | 194 | /** 195 | * Retrieves a list of all unique TaxPrice objects apart of this collection 196 | * 197 | * @return array An array of TaxPrice objects 198 | */ 199 | #[\ReturnTypeWillChange] 200 | public function taxes() 201 | { 202 | // Include unique instances of TaxPrice 203 | $taxes = []; 204 | foreach ($this->collection as $item_price) { 205 | foreach ($item_price->taxes() as $tax_price) { 206 | if (!in_array($tax_price, $taxes, true)) { 207 | $taxes[] = $tax_price; 208 | } 209 | } 210 | } 211 | 212 | return $taxes; 213 | } 214 | 215 | /** 216 | * Retrieves a list of all unique DiscountPrice objects apart of this collection 217 | * 218 | * @return array An array of DiscountPrice objects 219 | */ 220 | #[\ReturnTypeWillChange] 221 | public function discounts() 222 | { 223 | // Include unique instances of DiscountPrice 224 | $discounts = []; 225 | foreach ($this->collection as $item_price) { 226 | foreach ($item_price->discounts() as $discount_price) { 227 | if (!in_array($discount_price, $discounts, true)) { 228 | $discounts[] = $discount_price; 229 | } 230 | } 231 | } 232 | 233 | return $discounts; 234 | } 235 | 236 | /** 237 | * Resets the applied discount amounts for all ItemPrice's in the collection 238 | */ 239 | #[\ReturnTypeWillChange] 240 | public function resetDiscounts() 241 | { 242 | foreach ($this->collection as $item_price) { 243 | $item_price->resetDiscounts(); 244 | } 245 | } 246 | 247 | /** 248 | * Marks the given tax type as not shown in totals returned by all ItemPrices in the collection 249 | * 250 | * @param string $tax_type The type of tax to exclude 251 | * @return ItemPriceCollection A reference to this object 252 | */ 253 | #[\ReturnTypeWillChange] 254 | public function excludeTax($tax_type) 255 | { 256 | foreach ($this->collection as $item_price) { 257 | $item_price->excludeTax($tax_type); 258 | } 259 | 260 | return $this; 261 | } 262 | 263 | /** 264 | * Resets the list of tax types for all ItemPrices in the collection 265 | */ 266 | #[\ReturnTypeWillChange] 267 | public function resetTaxes() 268 | { 269 | foreach ($this->collection as $item_price) { 270 | $item_price->resetTaxes(); 271 | } 272 | } 273 | 274 | /** 275 | * Merges this ItemPriceCollection with the given ItemPriceCollection to produce 276 | * a new ItemPriceCollection for ItemPrices that share a key. 277 | * 278 | * The resulting ItemPriceCollection is composed of ItemPrices as constructed by 279 | * the given comparator. 280 | * 281 | * Multiple items sharing the same key from the same collection are subject to 282 | * being merged multiple times in the order in which they appear in the collection. 283 | * 284 | * @param ItemPriceCollection $collection The collection to be merged 285 | * @param ItemComparatorInterface $comparator The comparator used to merge item prices 286 | */ 287 | #[\ReturnTypeWillChange] 288 | public function merge(ItemPriceCollection $collection, ItemComparatorInterface $comparator) 289 | { 290 | // Set a new collection for the merged results 291 | $price_collection = new self(); 292 | 293 | foreach ($collection as $new_item) { 294 | foreach ($this as $current_item) { 295 | // Only items with matching non-null keys may be merged 296 | if ($current_item->key() !== null 297 | && $current_item->key() === $new_item->key() 298 | && ($item = $comparator->merge($current_item, $new_item)) 299 | ) { 300 | $price_collection->append($item); 301 | } 302 | } 303 | } 304 | 305 | return $price_collection; 306 | } 307 | 308 | /** 309 | * Retrieves the item in the collection at the current pointer 310 | * 311 | * @return mixed The ItemPrice in the collection at the current position, otherwise null 312 | */ 313 | #[\ReturnTypeWillChange] 314 | public function current() 315 | { 316 | return ( 317 | $this->valid() 318 | ? $this->collection[$this->position] 319 | : null 320 | ); 321 | } 322 | 323 | /** 324 | * Retrieves the index currently being pointed at in the collection 325 | * 326 | * @return int The index of the position in the collection 327 | */ 328 | #[\ReturnTypeWillChange] 329 | public function key() 330 | { 331 | return $this->position; 332 | } 333 | 334 | /** 335 | * Moves the pointer to the next item in the collection 336 | */ 337 | #[\ReturnTypeWillChange] 338 | public function next() 339 | { 340 | // Set the next position to the position of the next item in the collection 341 | $position = $this->position; 342 | foreach ($this->collection as $index => $item) { 343 | if ($index > $position) { 344 | $this->position = $index; 345 | break; 346 | } 347 | } 348 | 349 | // If there is no next item in the collection, increment the position instead 350 | if ($position == $this->position) { 351 | ++$this->position; 352 | } 353 | } 354 | 355 | /** 356 | * Moves the pointer to the first item in the collection 357 | */ 358 | #[\ReturnTypeWillChange] 359 | public function rewind() 360 | { 361 | // Reset the array pointer to the first entry in the collection 362 | reset($this->collection); 363 | 364 | // Set the position to the first entry in the collection if there is one 365 | $first_index = key($this->collection); 366 | $this->position = $first_index === null 367 | ? 0 368 | : $first_index; 369 | } 370 | 371 | /** 372 | * Determines whether the current pointer references a valid item in the collection 373 | * 374 | * @return bool True if the pointer references a valid item in the collection, false otherwise 375 | */ 376 | #[\ReturnTypeWillChange] 377 | public function valid() 378 | { 379 | return array_key_exists($this->position, $this->collection); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/Description/AbstractPriceDescription.php: -------------------------------------------------------------------------------- 1 | description = $description; 22 | } 23 | 24 | /** 25 | * Retrieves the price description 26 | * 27 | * @return string The price description 28 | */ 29 | public function getDescription() 30 | { 31 | return $this->description; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Description/PriceDescriptionInterface.php: -------------------------------------------------------------------------------- 1 | amount = $amount; 30 | $this->type = $type; 31 | } 32 | 33 | /** 34 | * Retrieves the price amount 35 | * 36 | * @return float The price amount 37 | */ 38 | public function amount() 39 | { 40 | return $this->amount; 41 | } 42 | 43 | /** 44 | * Retrieves the price type 45 | * 46 | * @return string The price type 47 | */ 48 | public function type() 49 | { 50 | return $this->type; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function reset() 57 | { 58 | // Nothing to do 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Modifier/DiscountPrice.php: -------------------------------------------------------------------------------- 1 | discount_remaining = 0; 39 | if ('percent' !== $this->type) { 40 | $this->discount_remaining = $amount; 41 | } 42 | } 43 | 44 | /** 45 | * Determines the price remaining after discount. 46 | * If the discount is an amount type, the discount off will be determined from 47 | * the discount amount remaining rather than the full discount amount. 48 | * 49 | * @param float $price The base price before discount 50 | * @return float The $price after discount 51 | */ 52 | public function off($price) 53 | { 54 | $discount = $this->on($price); 55 | 56 | // Update the running total of the discount amount remaining 57 | if ('percent' !== $this->type) { 58 | // The usable discount amount must consider the total discount remaining 59 | $applied_discount = min($this->discount_remaining, abs($discount)); 60 | 61 | // Update the total discount remaining and set the discount off 62 | $this->discount_remaining -= $applied_discount; 63 | $discount = $applied_discount; 64 | } 65 | 66 | return $price - abs($discount); 67 | } 68 | 69 | /** 70 | * Determines the discount amount from the given price 71 | * 72 | * @param float $price The base price before discount 73 | * @return float The discount amount 74 | */ 75 | public function on($price) 76 | { 77 | // Percent discount may cover at most the entire price 78 | if ('percent' === $this->type) { 79 | return ($this->amount > 100 ? $price : $price * $this->amount / 100); 80 | } else { 81 | $discount = min(abs($price), $this->discount_remaining); 82 | return ($price >= 0 ? $discount : -$discount); 83 | } 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function reset() 90 | { 91 | $this->discount_remaining = $this->amount; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Modifier/ItemComparatorInterface.php: -------------------------------------------------------------------------------- 1 | subtract = $subtract; 42 | 43 | parent::__construct($amount, $type); 44 | } 45 | 46 | /** 47 | * Determines the price after removing tax (from inclusive tax) 48 | * 49 | * @param float $price The price 50 | * @return float The $price without tax 51 | */ 52 | public function off($price) 53 | { 54 | if (TaxPrice::INCLUSIVE == $this->type || TaxPrice::INCLUSIVE_CALCULATED == $this->type) { 55 | return $price - $this->on($price); 56 | } 57 | return $price; 58 | } 59 | 60 | /** 61 | * Determines the amount of tax for the given price 62 | * 63 | * @param float $price The price 64 | * @return float The tax amount 65 | */ 66 | public function on($price) 67 | { 68 | if (TaxPrice::INCLUSIVE_CALCULATED == $this->type) { 69 | return ( $price / ( 100 + $this->amount ) ) * $this->amount; 70 | } else { 71 | return max(0, $this->amount / 100) * $price; 72 | } 73 | } 74 | 75 | /** 76 | * Determines the price including tax 77 | * 78 | * @param float $price The price before tax 79 | * @return float The price including tax 80 | */ 81 | public function including($price) 82 | { 83 | if (TaxPrice::EXCLUSIVE == $this->type) { 84 | return $price + $this->on($price); 85 | } 86 | return $price; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/PricingFactory.php: -------------------------------------------------------------------------------- 1 | true, 40 | TaxPrice::EXCLUSIVE => true, 41 | TaxPrice::INCLUSIVE_CALCULATED => true, 42 | ]; 43 | /** 44 | * @var bool Whether to apply discounts before calculating tax 45 | */ 46 | protected $discount_taxes = true; 47 | 48 | /** 49 | * Initialize the item price 50 | * 51 | * @param float $price The unit price 52 | * @param int $qty The quantity of unit prices (optional, default 1) 53 | * @param string $key A unique identifier (optional, default null) 54 | */ 55 | public function __construct($price, $qty = 1, $key = null) 56 | { 57 | parent::__construct($price, $qty, $key); 58 | 59 | // Reset the internal discount subtotal 60 | $this->resetDiscountSubtotal(); 61 | } 62 | 63 | /** 64 | * Assigns a discount to the item 65 | * 66 | * @param DiscountPrice $discount A discount object to set for the item 67 | */ 68 | public function setDiscount(DiscountPrice $discount) 69 | { 70 | // Disallow duplicates from being set 71 | if (!in_array($discount, $this->discounts, true)) { 72 | $this->discounts[] = $discount; 73 | } 74 | } 75 | 76 | /** 77 | * Assigns a TaxPrice to the item 78 | * Passing multiple TaxPrice arguments will set them to be compounded 79 | * 80 | * @throws InvalidArgumentException If something other than a TaxPrice was given 81 | */ 82 | public function setTax() 83 | { 84 | $taxes = func_get_args(); 85 | foreach ($taxes as $tax) { 86 | // Only a TaxPrice instance is accepted 87 | if (!($tax instanceof TaxPrice)) { 88 | throw new InvalidArgumentException(sprintf( 89 | '%s requires an instance of %s, %s given.', 90 | 'setTax', 91 | 'TaxPrice', 92 | gettype($tax) 93 | )); 94 | } 95 | } 96 | 97 | // Check for duplicate TaxPrice instances from the given arguments 98 | foreach ($taxes as $i => $tax_price_i) { 99 | foreach ($taxes as $j => $tax_price_j) { 100 | // Throw exception if the same instance of a TaxPrice was given multiple times 101 | if ($j > $i && $tax_price_i === $tax_price_j) { 102 | throw new InvalidArgumentException(sprintf( 103 | '%s requires unique instances of %s, but identical instances were given.', 104 | 'setTax', 105 | 'TaxPrice' 106 | )); 107 | } 108 | } 109 | } 110 | 111 | // Remove duplicate TaxPrice's that already exist 112 | foreach ($taxes as $index => $tax_price) { 113 | foreach ($this->taxes as $tax_row) { 114 | if (in_array($tax_price, $tax_row, true)) { 115 | unset($taxes[$index]); 116 | } 117 | } 118 | } 119 | 120 | $this->taxes[] = array_values($taxes); 121 | } 122 | 123 | /** 124 | * Sets whether to calculate tax before or after discounts are applied 125 | * 126 | * @param bool $discount_taxes True to calculate taxes after discounts are applied, false otherwise 127 | */ 128 | public function setDiscountTaxes($discount_taxes) 129 | { 130 | $this->discount_taxes = $discount_taxes; 131 | } 132 | 133 | /** 134 | * Retrieves the total item price amount considering all taxes without discounts 135 | */ 136 | public function totalAfterTax() 137 | { 138 | return parent::total() + $this->taxAmount(); 139 | } 140 | 141 | /** 142 | * Retrieves the total item price amount considering all discounts without taxes 143 | */ 144 | public function totalAfterDiscount() 145 | { 146 | return parent::total() - $this->discountAmount(); 147 | } 148 | 149 | /** 150 | * Retrieves the total item price amount not considering discounts or taxes 151 | * 152 | * @return float The item subtotal 153 | */ 154 | public function subtotal() 155 | { 156 | // inclusive_calculated taxes should be subtracted from the subtotal 157 | if (parent::total() < 0) { 158 | return parent::total() + abs($this->taxAmount(null, TaxPrice::INCLUSIVE_CALCULATED)); 159 | } else { 160 | return parent::total() - abs($this->taxAmount(null, TaxPrice::INCLUSIVE_CALCULATED)); 161 | } 162 | } 163 | 164 | /** 165 | * Retrieves the total item price amount considering all discounts and taxes 166 | * 167 | * @return float The total item price 168 | */ 169 | public function total() 170 | { 171 | // discountAmount() is called twice: once by totalAfterDiscount, and once by taxAmount 172 | // The discount must be removed only once, so flag it to be ignored the second time 173 | $this->discount_amounts = []; 174 | $this->cache_discount_amounts = true; 175 | $total = $this->totalAfterDiscount(); 176 | 177 | // Include tax without taking the discount off again, and reset the flag 178 | $this->cache_discount_amounts = false; 179 | $total += $this->taxAmount(); 180 | $this->discount_amounts = []; 181 | 182 | return $total; 183 | } 184 | 185 | /** 186 | * Retrieves the total tax amount considering all item taxes, or just the given tax 187 | * 188 | * @param TaxPrice $tax A specific tax price whose tax to calculate for this item (optional, default null) 189 | * @param string $type The type of tax for which to retrieve amounts (optional) (see $tax_types for options) 190 | * @return float The total tax amount for all taxes set for this item, or the total tax amount 191 | * for the given tax price if given 192 | */ 193 | public function taxAmount(TaxPrice $tax = null, $type = null) 194 | { 195 | // Determine the tax set on this item's price 196 | if ($tax) { 197 | $tax_amount = $this->amountTax($tax); 198 | } else { 199 | $tax_amount = $this->amountTaxAll($type); 200 | } 201 | 202 | return $tax_amount; 203 | } 204 | 205 | /** 206 | * Retrieves the tax amount for the given TaxPrice 207 | * 208 | * @param TaxPrice $tax A specific tax price whose tax to calculate for this item 209 | * @return float The total tax amount for the given TaxPrice 210 | */ 211 | private function amountTax(TaxPrice $tax) 212 | { 213 | // Apply tax either before or after the discount 214 | $taxable_price = $this->discount_taxes ? $this->totalAfterDiscount() : parent::total(); 215 | $tax_amount = 0; 216 | 217 | foreach ($this->taxes as $tax_group) { 218 | // Only calculate the tax amount if the tax exists in a tax group 219 | if (in_array($tax, $tax_group, true)) { 220 | $tax_amount = $this->compoundTaxAmount($tax_group, $taxable_price, $tax); 221 | } 222 | } 223 | 224 | return $tax_amount; 225 | } 226 | 227 | /** 228 | * Retrieves the total tax amount considering all item taxes 229 | * 230 | * @param string $type The type of tax for which to retrieve amounts (optional) (see $tax_types for options) 231 | * @return float The total tax amount for all taxes set for this item 232 | */ 233 | private function amountTaxAll($type = null) 234 | { 235 | // Apply tax either before or after the discount 236 | $taxable_price = $this->discount_taxes ? $this->totalAfterDiscount() : parent::total(); 237 | $tax_amount = 0; 238 | 239 | // Determine all taxes set on this item's price, compounded accordingly 240 | foreach ($this->taxes as $tax_group) { 241 | // Sum all taxes 242 | $tax_amount += $this->compoundTaxAmount($tax_group, $taxable_price, null, $type); 243 | } 244 | 245 | return $tax_amount; 246 | } 247 | 248 | /** 249 | * Retrieves the tax amount for a specific tax group 250 | * 251 | * @param array $tax_group A subset of the taxes array 252 | * @param float $taxable_price The total amount from which to calculate tax 253 | * @param TaxPrice $tax A specific tax from the group whose tax amount to retrieve (optional) 254 | * @param string $type The type of tax for which to retrieve amounts (optional) (see $tax_types for options) 255 | * @return float The total tax amount for all taxes set for this item in this group, or 256 | * the tax amount for the given TaxPrice 257 | */ 258 | private function compoundTaxAmount(array $tax_group, $taxable_price, TaxPrice $tax = null, $type = null) 259 | { 260 | $compound_tax = 0; 261 | $tax_total = 0; 262 | 263 | foreach ($tax_group as $tax_price) { 264 | // If a tax type is specified, skip taxes that don't match it 265 | $tax_type = $tax_price->type(); 266 | 267 | // Calculate the compound tax 268 | $tax_amount = $tax_price->on($taxable_price + $compound_tax); 269 | 270 | // Subtracted or inclusive_calculated taxes should be deducted from the compound tax 271 | if ($tax_price->subtract || $tax_type === TaxPrice::INCLUSIVE_CALCULATED) { 272 | $compound_tax -= $tax_amount; 273 | } else { 274 | $compound_tax += $tax_amount; 275 | } 276 | 277 | if ($type && $tax_type !== $type) { 278 | continue; 279 | } 280 | 281 | if (isset($this->tax_types[$tax_type]) && $this->tax_types[$tax_type]) { 282 | if ($tax_price->subtract) { 283 | // Subtract the tax amount instead of adding 284 | $tax_total -= $tax_amount; 285 | } elseif ($tax_type === TaxPrice::INCLUSIVE_CALCULATED && $type !== TaxPrice::INCLUSIVE_CALCULATED) { 286 | // Don't add inclusive_calculated taxes to the total unless specifically 287 | // fetching the total for that tax type 288 | $tax_total += 0; 289 | } else { 290 | // Add tax normally 291 | $tax_total += $tax_amount; 292 | } 293 | } elseif ($tax && $tax === $tax_price) { 294 | // Return a total of zero if we were given a tax, but it is of an excluded tax type 295 | return 0; 296 | } 297 | 298 | // Ignore any other group taxes, and only return the tax amount for the given TaxPrice 299 | if ($tax && $tax === $tax_price) { 300 | return $tax_amount; 301 | } 302 | } 303 | 304 | return $tax_total; 305 | } 306 | 307 | /** 308 | * Retrieves the total discount amount considering all item discounts, or just the given discount 309 | * 310 | * @param DiscountPrice $discount A specific discount price whose discount to calculate 311 | * for this item (optional, default null) 312 | * @return float The total discount amount for all discounts set for this item, or the 313 | * total discount amount for the given discount price if given 314 | */ 315 | public function discountAmount(DiscountPrice $discount = null) 316 | { 317 | $total_discount = 0; 318 | $subtotal = parent::total(); 319 | 320 | // Determine the discount set on this item's price 321 | if ($discount) { 322 | $total_discount = $this->amountDiscount($discount); 323 | } else { 324 | $total_discount = $this->amountDiscountAll(); 325 | } 326 | 327 | // Total discount not to exceed the subtotal amount, neither positive nor negative 328 | return ( 329 | $subtotal >= 0 330 | ? min($subtotal, $total_discount) 331 | : max($subtotal, $total_discount) 332 | ); 333 | } 334 | 335 | /** 336 | * Retrieves the total discount amount considering the given discount 337 | * 338 | * @param DiscountPrice $discount A specific discount price whose discount to calculate 339 | * for this item 340 | * @return float The total discount amount for the given discount price 341 | */ 342 | private function amountDiscount(DiscountPrice $discount) 343 | { 344 | $total_discount = 0; 345 | 346 | // Only calculate the discount amount if the discount is set for this item 347 | if (in_array($discount, $this->discounts, true)) { 348 | // Get the discount on the discounted subtotal remaining 349 | $total_discount = $discount->on($this->discounted_subtotal); 350 | 351 | // Update the discounted subtotal for this item by removing the amount discounted 352 | $this->discounted_subtotal = $discount->off($this->discounted_subtotal); 353 | } 354 | 355 | return $total_discount; 356 | } 357 | 358 | /** 359 | * Retrieves the total discount amount considering all item discounts 360 | * 361 | * @return float The total discount amount for all discounts set for this item 362 | */ 363 | private function amountDiscountAll() 364 | { 365 | $subtotal = parent::total(); 366 | $total_discount = 0; 367 | 368 | // Determine all the discounts set on this item's price 369 | foreach ($this->discounts as $key => $discount) { 370 | // Fetch the discount amount and remove it from the DiscountPrice, 371 | // or use the values previously cached 372 | if ($this->cache_discount_amounts || empty($this->discount_amounts)) { 373 | // Get the discount on the subtotal 374 | $discount_amount = $discount->on($subtotal); 375 | $total_discount += $discount_amount; 376 | 377 | // Cache the discount set for this DiscountPrice 378 | if ($this->cache_discount_amounts) { 379 | $this->discount_amounts[$key] = $discount_amount; 380 | } 381 | 382 | // Update the subtotal for this item to remove the amount discounted 383 | $subtotal = $discount->off($subtotal); 384 | } else { 385 | // Use the cached discount amount for this DiscountPrice 386 | $total_discount += $this->discount_amounts[$key]; 387 | } 388 | } 389 | 390 | return $total_discount; 391 | } 392 | 393 | /** 394 | * Fetch all unique taxes set 395 | * 396 | * @param bool $unique True to fetch all unique taxes for the item, 397 | * or false to fetch all groups of taxes (default true) 398 | * @return array An array of TaxPrice objects when $unique is true, 399 | * or an array containing arrays of grouped TaxPrice objects 400 | */ 401 | public function taxes($unique = true) 402 | { 403 | // Retrieve all taxes within their respective groups 404 | if (!$unique) { 405 | return $this->taxes; 406 | } 407 | 408 | // Retrieve all unique taxes 409 | $all_taxes = []; 410 | foreach ($this->taxes as $taxes) { 411 | $all_taxes = array_merge($all_taxes, array_values($taxes)); 412 | } 413 | return $all_taxes; 414 | } 415 | 416 | /** 417 | * Fetch all unique discounts set 418 | * 419 | * @return array An array of DiscountPrice objects 420 | */ 421 | public function discounts() 422 | { 423 | return $this->discounts; 424 | } 425 | 426 | /** 427 | * Resets the applied discount amounts for all assigned DiscountPrice's 428 | */ 429 | public function resetDiscounts() 430 | { 431 | // Reset the internal discounted subtotal 432 | $this->resetDiscountSubtotal(); 433 | 434 | // Reset each discount 435 | foreach ($this->discounts as $discount) { 436 | $discount->reset(); 437 | } 438 | } 439 | 440 | /** 441 | * Resets the discounted subtotal used internally 442 | */ 443 | private function resetDiscountSubtotal() 444 | { 445 | $this->discounted_subtotal = parent::total(); 446 | } 447 | 448 | /** 449 | * Marks the given tax type as not shown in totals returned by this object 450 | * 451 | * @param string $tax_type The type of tax to exclude 452 | * @return A reference to this object 453 | */ 454 | public function excludeTax($tax_type) 455 | { 456 | if (array_key_exists($tax_type, $this->tax_types)) { 457 | $this->tax_types[$tax_type] = false; 458 | } 459 | 460 | return $this; 461 | } 462 | 463 | /** 464 | * Resets the list of tax types to show in totals returned by this object 465 | */ 466 | public function resetTaxes() 467 | { 468 | foreach ($this->tax_types as $type => $value) { 469 | $this->tax_types[$type] = true; 470 | } 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /src/Type/PriceInterface.php: -------------------------------------------------------------------------------- 1 | setPrice($price); 36 | $this->setQty($qty); 37 | $this->setKey($key); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function price() 44 | { 45 | return $this->price; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function setPrice($price) 52 | { 53 | $this->price = $price; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function qty() 60 | { 61 | return $this->qty; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function setQty($qty) 68 | { 69 | $this->qty = $qty; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function key() 76 | { 77 | return $this->key; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function setKey($key) 84 | { 85 | $this->key = $key; 86 | } 87 | 88 | /** 89 | * Retrieves the total price 90 | * 91 | * @return float The total price considering quantity 92 | */ 93 | public function total() 94 | { 95 | return $this->qty * $this->price; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Unit/Collection/ItemPriceCollectionTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('Blesta\Pricing\Type\ItemPrice') 23 | ->disableOriginalConstructor() 24 | ->getMock(); 25 | $itemMock[] = $this->getMockBuilder('Blesta\Pricing\Type\ItemPrice') 26 | ->disableOriginalConstructor() 27 | ->getMock(); 28 | 29 | $collection = new ItemPriceCollection(); 30 | 31 | // Add 1 item 32 | $collection->append($itemMock[0]); 33 | $this->assertEquals(1, $collection->count()); 34 | 35 | // Add a second item 36 | $collection->append($itemMock[1]); 37 | $this->assertEquals(2, $collection->count()); 38 | } 39 | 40 | /** 41 | * @covers ::remove 42 | * @covers ::count 43 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::count 44 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append 45 | */ 46 | public function testRemove() 47 | { 48 | $itemMock[] = $this->getMockBuilder('Blesta\Pricing\Type\ItemPrice') 49 | ->disableOriginalConstructor() 50 | ->getMock(); 51 | $itemMock[] = $this->getMockBuilder('Blesta\Pricing\Type\ItemPrice') 52 | ->disableOriginalConstructor() 53 | ->getMock(); 54 | 55 | $collection = new ItemPriceCollection(); 56 | $this->assertEquals(0, $collection->count()); 57 | 58 | foreach ($itemMock as $item) { 59 | $collection->append($item); 60 | } 61 | 62 | $this->assertEquals(count($itemMock), $collection->count()); 63 | $collection->remove($itemMock[0]); 64 | $this->assertEquals(count($itemMock)-1, $collection->count()); 65 | } 66 | 67 | /** 68 | * @covers ::totalAfterTax 69 | * @uses Blesta\Pricing\Collection\ItemPriceCollection 70 | * @uses Blesta\Pricing\Type\ItemPrice 71 | * @uses Blesta\Pricing\Modifier\DiscountPrice 72 | * @uses Blesta\Pricing\Modifier\TaxPrice 73 | * @uses Blesta\Pricing\Type\UnitPrice 74 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 75 | * @dataProvider totalProvider 76 | */ 77 | public function testTotalAfterTax(ItemPriceCollection $collection, array $expected_totals) 78 | { 79 | $this->assertEquals($expected_totals['total_with_tax'], $collection->totalAfterTax()); 80 | } 81 | 82 | /** 83 | * @covers ::totalAfterDiscount 84 | * @uses Blesta\Pricing\Collection\ItemPriceCollection 85 | * @uses Blesta\Pricing\Type\ItemPrice 86 | * @uses Blesta\Pricing\Modifier\DiscountPrice 87 | * @uses Blesta\Pricing\Modifier\TaxPrice 88 | * @uses Blesta\Pricing\Type\UnitPrice 89 | * @dataProvider totalProvider 90 | */ 91 | public function testTotalAfterDiscount(ItemPriceCollection $collection, array $expected_totals) 92 | { 93 | $this->assertEquals($expected_totals['total_with_discount'], $collection->totalAfterDiscount()); 94 | } 95 | 96 | /** 97 | * @covers ::subtotal 98 | * @uses Blesta\Pricing\Collection\ItemPriceCollection 99 | * @uses Blesta\Pricing\Type\ItemPrice 100 | * @uses Blesta\Pricing\Modifier\DiscountPrice 101 | * @uses Blesta\Pricing\Modifier\TaxPrice 102 | * @uses Blesta\Pricing\Type\UnitPrice 103 | * @dataProvider totalProvider 104 | */ 105 | public function testSubtotal(ItemPriceCollection $collection, array $expected_totals) 106 | { 107 | $this->assertEquals($expected_totals['subtotal'], $collection->subtotal()); 108 | } 109 | 110 | /** 111 | * @covers ::total 112 | * @covers ::discountAmount 113 | * @uses Blesta\Pricing\Collection\ItemPriceCollection 114 | * @uses Blesta\Pricing\Type\ItemPrice 115 | * @uses Blesta\Pricing\Modifier\DiscountPrice 116 | * @uses Blesta\Pricing\Modifier\TaxPrice 117 | * @uses Blesta\Pricing\Type\UnitPrice 118 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 119 | * @dataProvider totalProvider 120 | */ 121 | public function testTotal(ItemPriceCollection $collection, array $expected_totals) 122 | { 123 | $this->assertEquals($expected_totals['total'], $collection->total()); 124 | } 125 | 126 | /** 127 | * 128 | * @covers ::taxAmount 129 | * @uses Blesta\Pricing\Collection\ItemPriceCollection 130 | * @uses Blesta\Pricing\Type\ItemPrice 131 | * @uses Blesta\Pricing\Modifier\DiscountPrice 132 | * @uses Blesta\Pricing\Modifier\TaxPrice 133 | * @uses Blesta\Pricing\Type\UnitPrice 134 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 135 | * @dataProvider totalProvider 136 | */ 137 | public function testTaxAmount(ItemPriceCollection $collection, array $expected_totals) 138 | { 139 | $this->assertEquals($expected_totals['tax'], $collection->taxAmount()); 140 | } 141 | 142 | /** 143 | * @covers ::discountAmount 144 | * @uses Blesta\Pricing\Collection\ItemPriceCollection 145 | * @uses Blesta\Pricing\Type\ItemPrice 146 | * @uses Blesta\Pricing\Modifier\DiscountPrice 147 | * @uses Blesta\Pricing\Modifier\TaxPrice 148 | * @uses Blesta\Pricing\Type\UnitPrice 149 | * @dataProvider totalProvider 150 | */ 151 | public function testDiscountAmount(ItemPriceCollection $collection, array $expected_totals) 152 | { 153 | $this->assertEquals($expected_totals['discount'], $collection->discountAmount()); 154 | } 155 | 156 | /** 157 | * @covers ::taxes 158 | * @uses Blesta\Pricing\Collection\ItemPriceCollection 159 | * @uses Blesta\Pricing\Type\ItemPrice 160 | * @uses Blesta\Pricing\Modifier\DiscountPrice 161 | * @uses Blesta\Pricing\Modifier\TaxPrice 162 | * @uses Blesta\Pricing\Type\UnitPrice 163 | * @dataProvider totalProvider 164 | */ 165 | public function testTaxes(ItemPriceCollection $collection, array $expected_totals) 166 | { 167 | $this->assertContainsOnlyInstancesOf('Blesta\Pricing\Modifier\TaxPrice', $collection->taxes()); 168 | 169 | // Exactly each expected tax should exist 170 | foreach ($expected_totals['taxes'] as $tax_price) { 171 | $this->assertContains($tax_price, $collection->taxes()); 172 | } 173 | $this->assertCount(count($expected_totals['taxes']), $collection->taxes()); 174 | } 175 | 176 | /** 177 | * @covers ::discounts 178 | * @uses Blesta\Pricing\Collection\ItemPriceCollection 179 | * @uses Blesta\Pricing\Type\ItemPrice 180 | * @uses Blesta\Pricing\Modifier\DiscountPrice 181 | * @uses Blesta\Pricing\Modifier\TaxPrice 182 | * @uses Blesta\Pricing\Type\UnitPrice 183 | * @dataProvider totalProvider 184 | */ 185 | public function testDiscounts(ItemPriceCollection $collection, array $expected_totals) 186 | { 187 | $this->assertContainsOnlyInstancesOf('Blesta\Pricing\Modifier\DiscountPrice', $collection->discounts()); 188 | 189 | // Exactly each expected discount should exist 190 | foreach ($expected_totals['discounts'] as $discount_price) { 191 | $this->assertContains($discount_price, $collection->discounts()); 192 | } 193 | $this->assertCount(count($expected_totals['discounts']), $collection->discounts()); 194 | } 195 | 196 | /** 197 | * @covers ::resetDiscounts 198 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append 199 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::discounts 200 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::discountAmount 201 | * @uses Blesta\Pricing\Type\ItemPrice 202 | * @uses Blesta\Pricing\Type\UnitPrice 203 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 204 | * @uses Blesta\Pricing\Modifier\DiscountPrice 205 | */ 206 | public function testResetDiscounts() 207 | { 208 | $item = new ItemPrice(10, 1); 209 | $discount = new DiscountPrice(1, 'amount'); 210 | $item->setDiscount($discount); 211 | 212 | $collection = new ItemPriceCollection(); 213 | $collection->append($item); 214 | 215 | // 1 discount on 10 is 1 216 | $this->assertEquals(1, $item->discountAmount()); 217 | // Discount already applied. No discount again 218 | $this->assertEquals(0, $item->discountAmount()); 219 | 220 | $collection->resetDiscounts(); 221 | 222 | // 1 discount on 10 is 1 223 | $this->assertEquals(1, $item->discountAmount()); 224 | } 225 | 226 | /** 227 | * Data provider for subtotal/total 228 | * 229 | * DO NOT SET DISCOUNT AMOUNTS THAT APPLY TO MULTIPLE ITEMS 230 | * DO NET SET AN ITEM TO MULTIPLE COLLECTIONS 231 | * Results will be incorrect without resetting item values appropriately 232 | * 233 | * @return array 234 | */ 235 | public function totalProvider() 236 | { 237 | $testCases = []; 238 | 239 | for ($i = 0; $i < 2; $i++) { 240 | // Items with discounts and tax 241 | $tax_price = new TaxPrice(10, TaxPrice::EXCLUSIVE); 242 | $item1 = new ItemPrice(10, 2); 243 | $item1->setDiscount(new DiscountPrice(20, 'percent')); 244 | $item1->setDiscount(new DiscountPrice(1, 'amount')); 245 | $item1->setTax($tax_price); 246 | 247 | $item4 = new ItemPrice(10, 2); 248 | $item4->setDiscount(new DiscountPrice(10, 'percent')); 249 | $item4->setTax($tax_price); 250 | 251 | // Item with tax 252 | $item2 = new ItemPrice(6, 4); 253 | $item2->setTax(new TaxPrice(5, TaxPrice::EXCLUSIVE)); 254 | $item3 = new ItemPrice(5, 5); 255 | 256 | $item5 = new ItemPrice(5.25, 3); 257 | $item5->setTax($tax_price); 258 | 259 | // Item with compound tax and discount 260 | $item6 = new ItemPrice(100.00, 1); 261 | $item6->setDiscount(new DiscountPrice(1.50, 'amount')); 262 | $item6->setTax(new TaxPrice(8, TaxPrice::EXCLUSIVE), new TaxPrice(5, TaxPrice::EXCLUSIVE)); 263 | 264 | // For the second test case, test discounts that do not apply to taxes 265 | if ($i === 1) { 266 | $item1->setDiscountTaxes(false); 267 | $item2->setDiscountTaxes(false); 268 | $item3->setDiscountTaxes(false); 269 | $item4->setDiscountTaxes(false); 270 | $item5->setDiscountTaxes(false); 271 | $item6->setDiscountTaxes(false); 272 | } 273 | 274 | // Set collections of the items 275 | $collection1 = new ItemPriceCollection(); 276 | $collection1->append($item1); 277 | 278 | $collection2 = new ItemPriceCollection(); 279 | $collection2->append($item2)->append($item3); 280 | 281 | $collection3 = new ItemPriceCollection(); 282 | $collection3->append($item4)->append($item5)->append($item6); 283 | 284 | $testCases[] = [$collection1, $this->getItemTotals($item1)]; 285 | $testCases[] = [$collection2, $this->getItemTotals($item2, $item3)]; 286 | $testCases[] = [$collection3, $this->getItemTotals($item4, $item5, $item6)]; 287 | } 288 | 289 | return $testCases; 290 | } 291 | 292 | /** 293 | * Retrieves total information for a set of items 294 | * 295 | * @param ItemPrice An ItemPrice object 296 | * @param ... 297 | * @return array An array of totals combining each item price 298 | */ 299 | private function getItemTotals() 300 | { 301 | // NOTE: 'total', 'total_with_discount', and 'discount' may be INCORRECT 302 | // if a DiscountPrice of type 'amount' applies to multiple items! 303 | $totals = [ 304 | 'subtotal' => 0, 305 | 'total' => 0, 306 | 'total_with_tax' => 0, 307 | 'total_with_discount' => 0, 308 | 'tax' => 0, 309 | 'discount' => 0, 310 | 'taxes' => [], 311 | 'discounts' => [] 312 | ]; 313 | 314 | $args = func_get_args(); 315 | foreach ($args as $item) { 316 | $totals['subtotal'] += $item->subtotal(); 317 | $item->resetDiscounts(); 318 | $totals['total'] += $item->total(); 319 | $item->resetDiscounts(); 320 | $totals['total_with_tax'] += $item->totalAfterTax(); 321 | $item->resetDiscounts(); 322 | $totals['total_with_discount'] += $item->totalAfterDiscount(); 323 | $item->resetDiscounts(); 324 | $totals['tax'] += $item->taxAmount(); 325 | $item->resetDiscounts(); 326 | $totals['discount'] += $item->discountAmount(); 327 | $item->resetDiscounts(); 328 | $totals['taxes'] = $this->getUnique($totals['taxes'], $item->taxes()); 329 | $totals['discounts'] = $this->getUnique($totals['discounts'], $item->discounts()); 330 | } 331 | 332 | return $totals; 333 | } 334 | 335 | /** 336 | * Includes unique items from $arr2 into $arr1 337 | * 338 | * @param array $arr1 An array of objects 339 | * @param array $arr2 An array of objects to include 340 | * @return array An array of unique objects 341 | */ 342 | private function getUnique($arr1, $arr2) 343 | { 344 | foreach ($arr2 as $obj) { 345 | if (!in_array($obj, $arr1, true)) { 346 | $arr1 = array_merge($arr1, [$obj]); 347 | } 348 | } 349 | 350 | return $arr1; 351 | } 352 | 353 | /** 354 | * Tests totals of items that share amount discounts 355 | * 356 | * @covers ::discountAmount 357 | * @covers ::taxAmount 358 | * @covers ::total 359 | * @covers ::totalAfterTax 360 | * @covers ::totalAfterDiscount 361 | * @uses Blesta\Pricing\Collection\ItemPriceCollection 362 | * @uses Blesta\Pricing\Type\ItemPrice 363 | * @uses Blesta\Pricing\Modifier\DiscountPrice 364 | * @uses Blesta\Pricing\Modifier\TaxPrice 365 | * @uses Blesta\Pricing\Type\UnitPrice 366 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 367 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 368 | */ 369 | public function testMultipleDiscountTotals() 370 | { 371 | // Test two items with the same discount amounts 372 | $collection = new ItemPriceCollection(); 373 | $discount1 = new DiscountPrice(5, 'amount'); 374 | $discount2 = new DiscountPrice(10, 'amount'); 375 | 376 | $item1 = new ItemPrice(10, 2); 377 | $item1->setDiscount($discount1); 378 | $item1->setDiscount($discount2); 379 | 380 | $item2 = new ItemPrice(100, 1); 381 | $item1->setDiscount($discount1); 382 | $item1->setDiscount($discount2); 383 | 384 | $collection->append($item1)->append($item2); 385 | 386 | $this->assertEquals(0, $collection->taxAmount()); 387 | $this->assertEquals(15, $collection->discountAmount()); 388 | $this->assertEquals(105, $collection->totalAfterDiscount()); 389 | $this->assertEquals(120, $collection->totalAfterTax()); 390 | $this->assertEquals(105, $collection->total()); 391 | 392 | 393 | // Test multiple items with varying taxes/discounts 394 | $collection->remove($item1)->remove($item2); 395 | $this->assertEquals(0, $collection->count()); 396 | 397 | $discount3 = new DiscountPrice(50, 'amount'); 398 | $tax = new TaxPrice(20, TaxPrice::EXCLUSIVE); 399 | 400 | $item3 = new ItemPrice(10, 1); 401 | $item3->setDiscount(new DiscountPrice(10, 'percent')); 402 | $item3->setDiscount($discount3); 403 | $item3->setTax(new TaxPrice(10, TaxPrice::EXCLUSIVE)); 404 | $item3->setTax($tax); 405 | 406 | $item4 = new ItemPrice(1000, 2); 407 | $item4->setDiscount($discount3); 408 | $item4->setTax($tax); 409 | 410 | $collection->append($item3)->append($item4); 411 | 412 | $this->assertEquals(391.8, $collection->taxAmount()); 413 | $this->assertEquals(51, $collection->discountAmount()); 414 | $this->assertEquals(1959, $collection->totalAfterDiscount()); 415 | $this->assertEquals(2401.8, $collection->totalAfterTax()); 416 | $this->assertEquals(2350.8, $collection->total()); 417 | } 418 | 419 | /** 420 | * @covers ::resetTaxes 421 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::excludeTax 422 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append 423 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::valid 424 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::current 425 | * @uses Blesta\Pricing\Type\ItemPrice 426 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 427 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax 428 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 429 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 430 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 431 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 432 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 433 | * @uses Blesta\Pricing\Type\UnitPrice::total 434 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 435 | */ 436 | public function testResetTaxes() 437 | { 438 | $item1 = new ItemPrice(10); 439 | $item2 = new ItemPrice(10); 440 | 441 | $collection = new ItemPriceCollection(); 442 | $temp_collection = clone $collection; 443 | $collection->append($item1); 444 | $temp_collection->append($item2); 445 | 446 | $collection->excludeTax(TaxPrice::EXCLUSIVE); 447 | $this->assertNotEquals($temp_collection, $collection); 448 | 449 | $collection->resetTaxes(); 450 | $this->assertEquals($collection, $temp_collection); 451 | } 452 | 453 | /** 454 | * @covers ::excludeTax 455 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append 456 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::valid 457 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::current 458 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 459 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 460 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax 461 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 462 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 463 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 464 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 465 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 466 | * @uses Blesta\Pricing\Type\UnitPrice::total 467 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 468 | */ 469 | public function testExcludeTax() 470 | { 471 | $item1 = new ItemPrice(10); 472 | $item2 = new ItemPrice(10); 473 | 474 | $collection = new ItemPriceCollection(); 475 | $temp_collection = clone $collection; 476 | $collection->append($item1); 477 | $temp_collection->append($item2); 478 | 479 | $collection->excludeTax('invalid_tax_type'); 480 | $this->assertEquals($temp_collection, $collection); 481 | 482 | $collection->excludeTax(TaxPrice::EXCLUSIVE); 483 | $this->assertNotEquals($temp_collection, $collection); 484 | 485 | $this->assertInstanceOf( 486 | 'Blesta\Pricing\Collection\ItemPriceCollection', 487 | $collection->excludeTax(TaxPrice::INCLUSIVE) 488 | ); 489 | } 490 | 491 | 492 | /** 493 | * @covers ::merge 494 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append 495 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::count 496 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::current 497 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::next 498 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::rewind 499 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::valid 500 | * @uses Blesta\Pricing\Type\UnitPrice::key 501 | * @dataProvider mergeProvider 502 | */ 503 | public function testMerge(ItemPriceCollection $collection1, ItemPriceCollection $collection2, $expected_items) 504 | { 505 | // Assume the merge will return the second ItemPrice back to us 506 | $comparator = $this->getMockBuilder('Blesta\Pricing\Modifier\ItemComparatorInterface')->getMock(); 507 | $comparator->method('merge') 508 | ->will($this->returnArgument(1)); 509 | 510 | $collection = $collection1->merge($collection2, $comparator); 511 | $this->assertInstanceOf('Blesta\Pricing\Collection\ItemPriceCollection', $collection); 512 | 513 | $this->assertEquals($expected_items, $collection->count()); 514 | } 515 | 516 | /** 517 | * Data provider for mergeing item prices 518 | */ 519 | public function mergeProvider() 520 | { 521 | $collection1 = new ItemPriceCollection(); 522 | $collection2 = new ItemPriceCollection(); 523 | $collection3 = new ItemPriceCollection(); 524 | $collection4 = new ItemPriceCollection(); 525 | 526 | $item1 = new ItemPrice(10, 1); 527 | $item1->setKey('id'); 528 | 529 | $item2 = new ItemPrice(20, 2); 530 | $item2->setKey('test'); 531 | 532 | $item3 = new ItemPrice(15, 1); 533 | $item3->setKey('id'); 534 | 535 | $collection1->append($item1)->append($item2); 536 | $collection2->append($item2)->append($item3); 537 | $collection3->append($item3); 538 | $collection4->append($item2); 539 | 540 | return [ 541 | [$collection1, $collection2, 2], 542 | [$collection1, $collection3, 1], 543 | [$collection2, $collection3, 1], 544 | [$collection3, $collection1, 1], 545 | [$collection3, $collection4, 0] 546 | ]; 547 | } 548 | 549 | /** 550 | * @covers ::current 551 | * @covers ::valid 552 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 553 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 554 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 555 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 556 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 557 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 558 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 559 | * @uses Blesta\Pricing\Type\UnitPrice::total 560 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append 561 | */ 562 | public function testCurrent() 563 | { 564 | $collection = new ItemPriceCollection(); 565 | 566 | // No items exist, there is no current item 567 | $this->assertNull($collection->current()); 568 | 569 | // One item 570 | $item = new ItemPrice(10, 1); 571 | $collection->append($item); 572 | $this->assertSame($item, $collection->current()); 573 | 574 | // First item is still the current item 575 | $collection->append(new ItemPrice(30, 2)); 576 | $this->assertSame($item, $collection->current()); 577 | } 578 | 579 | /** 580 | * @covers ::key 581 | * @covers ::next 582 | */ 583 | public function testKey() 584 | { 585 | $collection = new ItemPriceCollection(); 586 | 587 | // No items exist, but the key should be at the first index 588 | $this->assertEquals(0, $collection->key()); 589 | 590 | // Key should point at the next index 591 | $collection->next(); 592 | $this->assertEquals(1, $collection->key()); 593 | } 594 | 595 | /** 596 | * @covers ::next 597 | * @covers ::rewind 598 | * @covers ::current 599 | * @covers ::key 600 | * @covers ::valid 601 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 602 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 603 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 604 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 605 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 606 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 607 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 608 | * @uses Blesta\Pricing\Type\UnitPrice::total 609 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append 610 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::remove 611 | */ 612 | public function testNext() 613 | { 614 | $collection = new ItemPriceCollection(); 615 | 616 | // Position starts at 0, increments each time 617 | $collection->next(); 618 | $collection->next(); 619 | $collection->next(); 620 | $this->assertEquals(3, $collection->key()); 621 | 622 | $collection->rewind(); 623 | $this->assertEquals(0, $collection->key()); 624 | 625 | // Add items, ensure next() iterates to the next item, not just the next index 626 | $item1 = new ItemPrice(1, 1); 627 | $item2 = new ItemPrice(2, 1); 628 | $item3 = new ItemPrice(3, 1); 629 | $collection->append($item1)->append($item2)->append($item3); 630 | $collection->remove($item2); 631 | $this->assertSame($item1, $collection->current()); 632 | 633 | $collection->next(); 634 | $this->assertSame($item3, $collection->current()); 635 | 636 | // Remove the current item, and then get the next item 637 | $collection->rewind(); 638 | $this->assertSame($item1, $collection->current()); 639 | $collection->remove($item1); 640 | $collection->next(); 641 | $this->assertSame($item3, $collection->current()); 642 | 643 | // The next item is outside the collection and should be null 644 | $collection->next(); 645 | $this->assertNull($collection->current()); 646 | } 647 | 648 | /** 649 | * @covers ::rewind 650 | * @covers ::key 651 | * @covers ::next 652 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append 653 | */ 654 | public function testRewind() 655 | { 656 | $collection = new ItemPriceCollection(); 657 | 658 | // No items exist 659 | $this->assertEquals(0, $collection->key()); 660 | 661 | $collection->rewind(); 662 | $this->assertEquals(0, $collection->key()); 663 | 664 | // Increase the position 665 | $collection->next(); 666 | $collection->next(); 667 | $this->assertEquals(2, $collection->key()); 668 | 669 | // Rewind the position back 670 | $collection->rewind(); 671 | $this->assertEquals(0, $collection->key()); 672 | } 673 | 674 | /** 675 | * @covers ::valid 676 | * @covers ::next 677 | * @uses Blesta\Pricing\Collection\ItemPriceCollection::append 678 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 679 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 680 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 681 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 682 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 683 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 684 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 685 | * @uses Blesta\Pricing\Type\UnitPrice::total 686 | */ 687 | public function testValid() 688 | { 689 | $collection = new ItemPriceCollection(); 690 | 691 | // No items exist, position is not valid 692 | $this->assertFalse($collection->valid()); 693 | 694 | // Item takes first position 695 | $collection->append(new ItemPrice(10, 1)); 696 | $this->assertTrue($collection->valid()); 697 | 698 | // No item exists in the next position 699 | $collection->next(); 700 | $this->assertFalse($collection->valid()); 701 | } 702 | } 703 | -------------------------------------------------------------------------------- /tests/Unit/Description/AbstractPriceDescriptionTest.php: -------------------------------------------------------------------------------- 1 | getMockForAbstractClass('Blesta\Pricing\Description\AbstractPriceDescription'); 20 | $this->assertSame($description, $stub->getDescription()); 21 | 22 | // Set my own description 23 | $description = '100x Product 1 - Limited Time Offer'; 24 | $stub = $this->getMockForAbstractClass('Blesta\Pricing\Description\AbstractPriceDescription'); 25 | $stub->setDescription($description); 26 | $this->assertSame($description, $stub->getDescription()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Unit/Modifier/AbstractPriceModifierTest.php: -------------------------------------------------------------------------------- 1 | getMockForAbstractClass('Blesta\Pricing\Modifier\AbstractPriceModifier', [$price, 'inclusive']); 19 | $this->assertSame($price, $stub->amount()); 20 | } 21 | 22 | /** 23 | * @covers ::__construct 24 | * @covers ::type 25 | */ 26 | public function testType() 27 | { 28 | $type = 'inclusive'; 29 | $stub = $this->getMockForAbstractClass('Blesta\Pricing\Modifier\AbstractPriceModifier', [10.00, $type]); 30 | $this->assertSame($type, $stub->type()); 31 | } 32 | 33 | /** 34 | * @covers ::__construct 35 | * @covers ::reset 36 | */ 37 | public function testReset() 38 | { 39 | $stub = $this->getMockForAbstractClass('Blesta\Pricing\Modifier\AbstractPriceModifier', [10.00, 'inclusive']); 40 | $this->assertNull($stub->reset()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Unit/Modifier/DiscountPriceTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Blesta\Pricing\Modifier\DiscountPrice', new DiscountPrice(5.00, 'percent')); 19 | $this->assertInstanceOf('Blesta\Pricing\Modifier\DiscountPrice', new DiscountPrice(5.00, 'amount')); 20 | } 21 | 22 | /** 23 | * Test InvalidArgumentException is thrown 24 | * 25 | * @covers ::__construct 26 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 27 | * @expectedException InvalidArgumentException 28 | */ 29 | public function testConstructException() 30 | { 31 | // Amount must be non-negative 32 | $discount = new DiscountPrice(-1, 'amount'); 33 | } 34 | 35 | /** 36 | * @covers ::off 37 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on 38 | * @dataProvider offProvider 39 | */ 40 | public function testOff($discount, $price, $price_after) 41 | { 42 | $this->assertEquals($price_after, $discount->off($price)); 43 | } 44 | 45 | /** 46 | * Data provider 47 | * 48 | * @return array 49 | */ 50 | public function offProvider() 51 | { 52 | return [ 53 | [new DiscountPrice(0, 'percent'), 10.00, 10.00], 54 | [new DiscountPrice(10, 'percent'), 10.00, 9.00], 55 | [new DiscountPrice(50, 'percent'), 10.00, 5.00], 56 | [new DiscountPrice(100, 'percent'), 10.00, 0.00], 57 | [new DiscountPrice(200, 'percent'), 10.00, 0.00], 58 | 59 | [new DiscountPrice(0, 'percent'), -10.00, -10.00], 60 | [new DiscountPrice(10, 'percent'), -10.00, -11.00], 61 | [new DiscountPrice(50, 'percent'), -10.00, -15.00], 62 | [new DiscountPrice(100, 'percent'), -10.00, -20.00], 63 | [new DiscountPrice(200, 'percent'), -10.00, -20.00], 64 | 65 | [new DiscountPrice(0, 'amount'), 10.00, 10.00], 66 | [new DiscountPrice(3, 'amount'), 10.00, 7.00], 67 | [new DiscountPrice(50, 'amount'), 10.00, 0.00], 68 | [new DiscountPrice(100, 'amount'), 10.00, 0.00], 69 | [new DiscountPrice(3, 'amount'), -10.00, -13.00], 70 | [new DiscountPrice(50, 'amount'), -10.00, -20.00], 71 | [new DiscountPrice(100, 'amount'), -10.00, -20.00], 72 | ]; 73 | } 74 | 75 | /** 76 | * Test amount discounts for multiple prices, as the discount remaining should 77 | * change with each price the discount is applied to 78 | * 79 | * @covers ::off 80 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on 81 | * @dataProvider offMultipleProvider 82 | */ 83 | public function testOffMultiple($discount, $prices, $price_after_all) 84 | { 85 | $price_remaining = 0; 86 | foreach ($prices as $price) { 87 | $price_remaining += $discount->off($price); 88 | } 89 | 90 | $this->assertEquals($price_after_all, $price_remaining); 91 | } 92 | 93 | /** 94 | * Data provider for testOffMultiple 95 | * @return array 96 | */ 97 | public function offMultipleProvider() 98 | { 99 | return [ 100 | [new DiscountPrice(0, 'amount'), [4, 10], 14], 101 | [new DiscountPrice(10, 'amount'), [4, 10], 4], 102 | [new DiscountPrice(20, 'amount'), [4, 10], 0], 103 | [new DiscountPrice(100, 'amount'), [4, 10], 0], 104 | [new DiscountPrice(10, 'amount'), [-4, -10], -24], 105 | [new DiscountPrice(20, 'amount'), [-4, -10], -28], 106 | [new DiscountPrice(100, 'amount'), [-4, -10], -28], 107 | [new DiscountPrice(5, 'amount'), [-4, 10], 1], 108 | 109 | [new DiscountPrice(10, 'amount'), [9, 5, 4], 8], 110 | ]; 111 | } 112 | 113 | /** 114 | * @covers ::on 115 | * @dataProvider onProvider 116 | */ 117 | public function testOn($discount, $price, $discount_price) 118 | { 119 | $this->assertEquals($discount_price, $discount->on($price)); 120 | } 121 | 122 | /** 123 | * Data provider 124 | * 125 | * @return array 126 | */ 127 | public function onProvider() 128 | { 129 | return [ 130 | [new DiscountPrice(0, 'percent'), 10.00, 0.00], 131 | [new DiscountPrice(50, 'percent'), 10.00, 5.00], 132 | [new DiscountPrice(100, 'percent'), 10.00, 10.00], 133 | [new DiscountPrice(200, 'percent'), 10.00, 10.00], 134 | [new DiscountPrice(0, 'percent'), -10.00, 0.00], 135 | [new DiscountPrice(50, 'percent'), -10.00, -5.00], 136 | [new DiscountPrice(100, 'percent'), -10.00, -10.00], 137 | [new DiscountPrice(200, 'percent'), -10.00, -10.00], 138 | 139 | [new DiscountPrice(0, 'amount'), 10.00, 0.00], 140 | [new DiscountPrice(3, 'amount'), 10.00, 3.00], 141 | [new DiscountPrice(10, 'amount'), 10.00, 10.00], 142 | [new DiscountPrice(20, 'amount'), 10.00, 10.00], 143 | [new DiscountPrice(0, 'amount'), -10.00, 0.00], 144 | [new DiscountPrice(3, 'amount'), -10.00, -3.00], 145 | [new DiscountPrice(10, 'amount'), -10.00, -10.00], 146 | [new DiscountPrice(20, 'amount'), -10.00, -10.00], 147 | ]; 148 | } 149 | 150 | /** 151 | * @covers ::reset 152 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct 153 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 154 | * @uses Blesta\Pricing\Modifier\DiscountPrice::off 155 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on 156 | */ 157 | public function testReset() 158 | { 159 | $discount = new DiscountPrice(10, 'amount'); 160 | $this->assertEquals(0, $discount->off(10)); 161 | $this->assertEquals(10, $discount->off(10)); 162 | 163 | $discount->reset(); 164 | $this->assertEquals(0, $discount->off(10)); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /tests/Unit/Modifier/TaxPriceTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Blesta\Pricing\Modifier\TaxPrice', new TaxPrice(10.00, TaxPrice::EXCLUSIVE)); 19 | } 20 | 21 | /** 22 | * @covers ::__construct 23 | * @expectedException InvalidArgumentException 24 | */ 25 | public function testConstructException() 26 | { 27 | // Amount must be non-negative 28 | $tax = new TaxPrice(-10, TaxPrice::EXCLUSIVE); 29 | } 30 | 31 | /** 32 | * @covers ::off 33 | * @uses Blesta\Pricing\Modifier\TaxPrice::on 34 | * @dataProvider offProvider 35 | */ 36 | public function testOff($tax, $price, $price_after) 37 | { 38 | $this->assertEquals($price_after, round($tax->off($price), 2)); 39 | } 40 | 41 | /** 42 | * Data provider 43 | * 44 | * @return array 45 | */ 46 | public function offProvider() 47 | { 48 | return [ 49 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), 10.00, 10.00], 50 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), 10.00, 10.00], 51 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), 10.00, 10.00], 52 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), -10.00, -10.00], 53 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), -10.00, -10.00], 54 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), -10.00, -10.00], 55 | 56 | [new TaxPrice(0, TaxPrice::INCLUSIVE), 10.00, 10.00], 57 | [new TaxPrice(50, TaxPrice::INCLUSIVE), 10.00, 5.00], 58 | [new TaxPrice(100, TaxPrice::INCLUSIVE), 10.00, 0.00], 59 | [new TaxPrice(0, TaxPrice::INCLUSIVE), -10.00, -10.00], 60 | [new TaxPrice(50, TaxPrice::INCLUSIVE), -10.00, -5.00], 61 | [new TaxPrice(100, TaxPrice::INCLUSIVE), -10.00, 0.00], 62 | 63 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 10.00], 64 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 6.67], 65 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 5.00], 66 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -10.00], 67 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -6.67], 68 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -5.00], 69 | ]; 70 | } 71 | 72 | /** 73 | * @covers ::on 74 | * @dataProvider onProvider 75 | */ 76 | public function testOn($tax, $price, $tax_amount) 77 | { 78 | $this->assertEquals($tax_amount, round($tax->on($price), 2)); 79 | } 80 | 81 | /** 82 | * Data provider 83 | * 84 | * @return array 85 | */ 86 | public function onProvider() 87 | { 88 | return [ 89 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), 10.00, 0.00], 90 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), 10.00, 5.00], 91 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), 10.00, 10.00], 92 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), -10.00, 0.00], 93 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), -10.00, -5.00], 94 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), -10.00, -10.00], 95 | 96 | [new TaxPrice(0, TaxPrice::INCLUSIVE), 10.00, 0.00], 97 | [new TaxPrice(50, TaxPrice::INCLUSIVE), 10.00, 5.00], 98 | [new TaxPrice(100, TaxPrice::INCLUSIVE), 10.00, 10.00], 99 | [new TaxPrice(0, TaxPrice::INCLUSIVE), -10.00, 0.00], 100 | [new TaxPrice(50, TaxPrice::INCLUSIVE), -10.00, -5.00], 101 | [new TaxPrice(100, TaxPrice::INCLUSIVE), -10.00, -10.00], 102 | 103 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 0.00], 104 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 3.33], 105 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 5.00], 106 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), -10.00, 0.00], 107 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -3.33], 108 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -5.00], 109 | ]; 110 | } 111 | 112 | /** 113 | * @covers ::including 114 | * @uses Blesta\Pricing\Modifier\TaxPrice::on 115 | * @dataProvider includingProvider 116 | */ 117 | public function testIncluding($tax, $price, $result) 118 | { 119 | $this->assertEquals($result, $tax->including($price)); 120 | } 121 | 122 | /** 123 | * Data provider 124 | * 125 | * @return array 126 | */ 127 | public function includingProvider() 128 | { 129 | return [ 130 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), 10.00, 10.00], 131 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), 10.00, 15.00], 132 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), 10.00, 20.00], 133 | [new TaxPrice(0, TaxPrice::EXCLUSIVE), -10.00, -10.00], 134 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), -10.00, -15.00], 135 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), -10.00, -20.00], 136 | 137 | [new TaxPrice(0, TaxPrice::INCLUSIVE), 10.00, 10.00], 138 | [new TaxPrice(50, TaxPrice::INCLUSIVE), 10.00, 10.00], 139 | [new TaxPrice(100, TaxPrice::INCLUSIVE), 10.00, 10.00], 140 | [new TaxPrice(0, TaxPrice::INCLUSIVE), -10.00, -10.00], 141 | [new TaxPrice(50, TaxPrice::INCLUSIVE), -10.00, -10.00], 142 | [new TaxPrice(100, TaxPrice::INCLUSIVE), -10.00, -10.00], 143 | 144 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 10.00], 145 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 10.00], 146 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), 10.00, 10.00], 147 | [new TaxPrice(0, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -10.00], 148 | [new TaxPrice(50, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -10.00], 149 | [new TaxPrice(100, TaxPrice::INCLUSIVE_CALCULATED), -10.00, -10.00], 150 | ]; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/Unit/PricingFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Blesta\Pricing\Type\UnitPrice', $pricing_factory->unitPrice(5.00, 2)); 23 | } 24 | 25 | /** 26 | * @covers ::itemPrice 27 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 28 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 29 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 30 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 31 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 32 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 33 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 34 | * @uses Blesta\Pricing\Type\UnitPrice::total 35 | */ 36 | public function testItemPrice() 37 | { 38 | $pricing_factory = new PricingFactory(); 39 | $this->assertInstanceOf('Blesta\Pricing\Type\ItemPrice', $pricing_factory->itemPrice(5.00, 2)); 40 | } 41 | 42 | /** 43 | * @covers ::discountPrice 44 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct 45 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 46 | */ 47 | public function testDiscountPrice() 48 | { 49 | $pricing_factory = new PricingFactory(); 50 | $this->assertInstanceOf( 51 | 'Blesta\Pricing\Modifier\DiscountPrice', 52 | $pricing_factory->discountPrice(20.00, 'percent') 53 | ); 54 | } 55 | 56 | /** 57 | * @covers ::taxPrice 58 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct 59 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 60 | */ 61 | public function testTaxPrice() 62 | { 63 | $pricing_factory = new PricingFactory(); 64 | $this->assertInstanceOf( 65 | 'Blesta\Pricing\Modifier\TaxPrice', 66 | $pricing_factory->taxPrice(7.75, 'exclusive') 67 | ); 68 | } 69 | 70 | /** 71 | * @covers ::itemPriceCollection 72 | */ 73 | public function testItemPriceCollection() 74 | { 75 | $pricing_factory = new PricingFactory(); 76 | $this->assertInstanceOf( 77 | 'Blesta\Pricing\Collection\ItemPriceCollection', 78 | $pricing_factory->itemPriceCollection() 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/Unit/Type/ItemPriceTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Blesta\Pricing\Type\ItemPrice', new ItemPrice(5.00, 1, 'id')); 28 | } 29 | 30 | /** 31 | * @covers ::setDiscount 32 | * @covers ::discounts 33 | * @dataProvider discountProvider 34 | */ 35 | public function testSetDiscount($item, $discount) 36 | { 37 | $item->setDiscount($discount); 38 | $this->assertContains($discount, $item->discounts()); 39 | 40 | $item->setDiscount($discount); 41 | $this->assertCount( 42 | 1, 43 | $item->discounts(), 44 | 'Only one instance of each discount may exist.' 45 | ); 46 | } 47 | 48 | /** 49 | * Discount data provider 50 | * 51 | * @return array 52 | */ 53 | public function discountProvider() 54 | { 55 | return [ 56 | [new ItemPrice(10, 0), new DiscountPrice(10, 'percent')], 57 | [new ItemPrice(10), new DiscountPrice(10, 'percent')] 58 | ]; 59 | } 60 | 61 | 62 | /** 63 | * @covers ::setTax 64 | * @covers ::taxes 65 | * @dataProvider taxProvider 66 | */ 67 | public function testSetTax($item, array $taxes) 68 | { 69 | // Add all given taxes 70 | call_user_func_array([$item, 'setTax'], $taxes); 71 | foreach ($taxes as $tax) { 72 | $this->assertContains($tax, $item->taxes()); 73 | } 74 | 75 | // At most, each tax should be added, but may be less if duplicates set 76 | $num_set_taxes = count($item->taxes()); 77 | $this->assertLessThanOrEqual(count($taxes), $num_set_taxes); 78 | 79 | // The same tax should not be added again 80 | $item->setTax($taxes[0]); 81 | $this->assertCount( 82 | $num_set_taxes, 83 | $item->taxes(), 84 | 'Only one instance of each tax may exist.' 85 | ); 86 | } 87 | 88 | /** 89 | * @covers ::setTax 90 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 91 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 92 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 93 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 94 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 95 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 96 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 97 | * @uses Blesta\Pricing\Type\UnitPrice::total 98 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct 99 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 100 | * @expectedException InvalidArgumentException 101 | */ 102 | public function testSetTaxInvalidException() 103 | { 104 | // Invalid argument: not TaxPrice 105 | $item = new ItemPrice(10); 106 | $item->setTax(new stdClass()); 107 | } 108 | 109 | /** 110 | * @covers ::setTax 111 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 112 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 113 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 114 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 115 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 116 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 117 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 118 | * @uses Blesta\Pricing\Type\UnitPrice::total 119 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct 120 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 121 | * @expectedException InvalidArgumentException 122 | */ 123 | public function testSetTaxMultipleException() 124 | { 125 | // Invalid argument: Multiple TaxPrice's of the same instance 126 | $item = new ItemPrice(10); 127 | $tax_price = new TaxPrice(10, TaxPrice::EXCLUSIVE); 128 | $item->setTax($tax_price, $tax_price); 129 | } 130 | 131 | /** 132 | * Tax data provider 133 | * 134 | * @return array 135 | */ 136 | public function taxProvider() 137 | { 138 | return [ 139 | [new ItemPrice(10, 0), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]], 140 | [new ItemPrice(10), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]], 141 | [new ItemPrice(10, 1), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::EXCLUSIVE)]], 142 | ]; 143 | } 144 | 145 | /** 146 | * @covers ::setDiscountTaxes 147 | * @covers ::discountAmount 148 | * @covers ::taxAmount 149 | * @covers ::amountTax 150 | * @covers ::amountTaxAll 151 | * @covers ::totalAfterDiscount 152 | * @covers ::totalAfterTax 153 | * @covers ::total 154 | * @uses Blesta\Pricing\Type\ItemPrice 155 | * @uses Blesta\Pricing\Type\UnitPrice 156 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier 157 | * @uses Blesta\Pricing\Modifier\DiscountPrice 158 | * @uses Blesta\Pricing\Modifier\TaxPrice 159 | * @dataProvider taxingDiscountsProvider 160 | */ 161 | public function testTaxingDiscounts( 162 | $discount_taxes, 163 | $item, 164 | array $discounts, 165 | array $taxes, 166 | $discount_amount, 167 | $tax_amount, 168 | $total_after_discount, 169 | $total_after_tax, 170 | $total 171 | ) { 172 | // Set whether taxes may be discounted 173 | $item->setDiscountTaxes($discount_taxes); 174 | 175 | // Apply the taxes and discounts to the item 176 | foreach ($discounts as $discount) { 177 | $item->setDiscount($discount); 178 | } 179 | 180 | foreach ($taxes as $tax) { 181 | $item->setTax($tax); 182 | } 183 | 184 | $this->assertEquals($discount_amount, round($item->discountAmount(), 2)); 185 | 186 | // Reset discount amounts that were applied so that we can use them again to calculate the next total 187 | $item->resetDiscounts(); 188 | $this->assertEquals($tax_amount, round($item->taxAmount(), 2)); 189 | 190 | // Reset discount amounts that were applied so that we can use them again to calculate the next total 191 | $item->resetDiscounts(); 192 | $this->assertEquals($total_after_discount, round($item->totalAfterDiscount(), 2)); 193 | 194 | // Reset discount amounts that were applied so that we can use them again to calculate the next total 195 | $item->resetDiscounts(); 196 | $this->assertEquals($total_after_tax, round($item->totalAfterTax(), 2)); 197 | 198 | // Reset discount amounts that were applied so that we can use them again to calculate the next total 199 | $item->resetDiscounts(); 200 | $this->assertEquals($total, round($item->total(), 2)); 201 | } 202 | 203 | /** 204 | * Data provider for testing whether discounts apply before or after tax 205 | * 206 | * @return array 207 | */ 208 | public function taxingDiscountsProvider() 209 | { 210 | return [ 211 | [ 212 | true, // discount taxes 213 | new ItemPrice(10, 1), 214 | [new DiscountPrice(10, 'percent')], 215 | [new TaxPrice(50, TaxPrice::EXCLUSIVE)], 216 | 1.00, // discount amount (10 * 0.1) 217 | 4.50, // tax amount [10 * (1 - 0.1)] * (0.5) 218 | 9.00, // total after discount [10 - (10 * 0.1)] 219 | 14.50, // total after tax ([)10 + 4.50) 220 | 13.50 // grand total (9 + 4.50) 221 | ], 222 | [ 223 | false, // do not discount taxes 224 | new ItemPrice(10, 1), 225 | [new DiscountPrice(10, 'percent')], 226 | [new TaxPrice(50, TaxPrice::EXCLUSIVE)], 227 | 1.00, // discount amount (10 * 0.1) 228 | 5.00, // tax amount (10 * 0.5) 229 | 9.00, // total after discount [10 - (10 * 0.1)] 230 | 15.00, // total after tax (10 + 5) 231 | 14.00 // grand total (9 + 5) 232 | ], 233 | [ 234 | true, // discount taxes 235 | new ItemPrice(10, 1), 236 | [new DiscountPrice(100, 'percent')], 237 | [new TaxPrice(50, TaxPrice::EXCLUSIVE)], 238 | 10.00, // discount amount (10 * 1) 239 | 0.00, // tax amount [10 * (1 - 1)] * (0.5) 240 | 0.00, // total after discount [10 - (10 * 1)] 241 | 10.00, // total after tax (10 + 0) 242 | 0.00 // grand total (0 + 0) 243 | ], 244 | [ 245 | false, // do not discount taxes 246 | new ItemPrice(10, 1), 247 | [new DiscountPrice(100, 'percent')], 248 | [new TaxPrice(50, TaxPrice::EXCLUSIVE)], 249 | 10.00, // discount amount (10 * 1) 250 | 5.00, // tax amount (10 * 0.5) 251 | 0.00, // total after discount [10 - (10 * 1)] 252 | 15.00, // total after tax (10 + 5) 253 | 5.00 // grand total (0 + 5) 254 | ], 255 | [ 256 | true, // discount taxes 257 | new ItemPrice(10, 1), 258 | [new DiscountPrice(10, 'percent'), new DiscountPrice(100, 'amount')], 259 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE)], 260 | 10.00, // discount amount (10) 261 | 0.00, // tax amount (0 * 0.5) + (0 * 0.1) 262 | 0.00, // total after discount (10 - 10) 263 | 10.00, // total after tax (10 + 0) 264 | 0.00 // grand total (0 + 0) 265 | ], 266 | [ 267 | false, // discount taxes 268 | new ItemPrice(10, 1), 269 | [new DiscountPrice(10, 'percent'), new DiscountPrice(100, 'amount')], 270 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE)], 271 | 10.00, // discount amount (10) 272 | 6.00, // tax amount (10 * 0.5) + (10 * 0.1) 273 | 0.00, // total after discount (10 - 10) 274 | 16.00, // total after tax (10 + 6) 275 | 6.00 // grand total (0 + 6) 276 | ], 277 | [ 278 | true, // discount taxes 279 | new ItemPrice(10, 1), 280 | [new DiscountPrice(10, 'percent'), new DiscountPrice(5, 'amount')], 281 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE)], 282 | 6.00, // discount amount (10 * 0.1) + 5 283 | 2.40, // tax amount [(10 - 6) * 0.5)] + [(10 - 6) * 0.1)] 284 | 4.00, // total after discount (10 - 6) 285 | 12.40, // total after tax (10 + 2.40) 286 | 6.40 // grand total (4 + 2.40) 287 | ], 288 | [ 289 | false, // discount taxes 290 | new ItemPrice(10, 1), 291 | [new DiscountPrice(10, 'percent'), new DiscountPrice(5, 'amount')], 292 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE)], 293 | 6.00, // discount amount (10 * 0.1) + 5 294 | 6.00, // tax amount (10 * 0.5) + (10 * 0.1) 295 | 4.00, // total after discount (10 - 6) 296 | 16.00, // total after tax (10 + 6) 297 | 10.00 // grand total (4 + 6) 298 | ], 299 | [ 300 | true, // discount taxes 301 | new ItemPrice(10, 1), 302 | [new DiscountPrice(10, 'percent'), new DiscountPrice(5, 'amount')], 303 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED)], 304 | 6.00, // discount amount (10 * 0.1) + 5 305 | 2.00, // tax amount [(10 - 6) * 0.5)] 306 | 4.00, // total after discount (10 - 6) 307 | 12.00, // total after tax (10 + 2.00) 308 | 6.00 // grand total (4 + 2.00) 309 | ], 310 | [ 311 | false, // discount taxes 312 | new ItemPrice(10, 1), 313 | [new DiscountPrice(10, 'percent'), new DiscountPrice(5, 'amount')], 314 | [new TaxPrice(50, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED)], 315 | 6.00, // discount amount (10 * 0.1) + 5 316 | 5.00, // tax amount (10 * 0.5) 317 | 4.00, // total after discount (10 - 6) 318 | 15.00, // total after tax (10 + 5.00) 319 | 9.00 // grand total (4 + 5.00) 320 | ] 321 | ]; 322 | } 323 | 324 | /** 325 | * @covers ::totalAfterTax 326 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 327 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 328 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 329 | * @uses Blesta\Pricing\Type\ItemPrice::setTax 330 | * @uses Blesta\Pricing\Type\ItemPrice::taxAmount 331 | * @uses Blesta\Pricing\Type\ItemPrice::amountTax 332 | * @uses Blesta\Pricing\Type\ItemPrice::amountTaxAll 333 | * @uses Blesta\Pricing\Type\ItemPrice::compoundTaxAmount 334 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterDiscount 335 | * @uses Blesta\Pricing\Type\ItemPrice::discountAmount 336 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscount 337 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscountAll 338 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 339 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 340 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 341 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 342 | * @uses Blesta\Pricing\Type\UnitPrice::total 343 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 344 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct 345 | * @uses Blesta\Pricing\Modifier\TaxPrice::on 346 | * @dataProvider totalAfterTaxProvider 347 | */ 348 | public function testTotalAfterTax($item, $taxes) 349 | { 350 | // No taxes set. Subtotal is the total after tax 351 | $this->assertEquals($item->subtotal(), $item->totalAfterTax()); 352 | 353 | // Set taxes 354 | call_user_func_array([$item, 'setTax'], $taxes); 355 | 356 | // Total will be larger or smaller than the subtotal if it's positive or negative 357 | if ($item->subtotal() > 0) { 358 | $this->assertGreaterThan($item->subtotal(), $item->totalAfterTax()); 359 | } elseif ($item->subtotal() < 0) { 360 | $this->assertLessThan($item->subtotal(), $item->totalAfterTax()); 361 | } else { 362 | $this->assertEquals(0, $item->totalAfterTax()); 363 | } 364 | } 365 | 366 | /** 367 | * Total After Tax data provider 368 | * 369 | * @return array 370 | */ 371 | public function totalAfterTaxProvider() 372 | { 373 | return [ 374 | [new ItemPrice(100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]], 375 | [new ItemPrice(0.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]], 376 | [new ItemPrice(-100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE)]], 377 | 378 | [new ItemPrice(100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(10, TaxPrice::EXCLUSIVE)]], 379 | [new ItemPrice(-100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(20, TaxPrice::EXCLUSIVE)]], 380 | 381 | [new ItemPrice(100.00, 2), [new TaxPrice(20, TaxPrice::INCLUSIVE_CALCULATED)]], 382 | [new ItemPrice(-100.00, 2), [new TaxPrice(20, TaxPrice::INCLUSIVE_CALCULATED)]], 383 | 384 | [new ItemPrice(100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(20, TaxPrice::INCLUSIVE_CALCULATED)]], 385 | [new ItemPrice(-100.00, 2), [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(20, TaxPrice::INCLUSIVE_CALCULATED)]], 386 | ]; 387 | } 388 | 389 | /** 390 | * @covers ::totalAfterDiscount 391 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 392 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 393 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 394 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount 395 | * @uses Blesta\Pricing\Type\ItemPrice::discountAmount 396 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscount 397 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscountAll 398 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 399 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 400 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 401 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 402 | * @uses Blesta\Pricing\Type\UnitPrice::total 403 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct 404 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on 405 | * @uses Blesta\Pricing\Modifier\DiscountPrice::off 406 | * @dataProvider totalAfterDiscountProvider 407 | */ 408 | public function testTotalAfterDiscount($item, $discounts) 409 | { 410 | // No discounts set. Subtotal is the total after discount 411 | $this->assertEquals($item->subtotal(), $item->totalAfterDiscount()); 412 | 413 | foreach ($discounts as $discount) { 414 | $item->setDiscount($discount); 415 | } 416 | 417 | // Total will be larger or smaller than the subtotal if it's positive or negative 418 | if ($item->subtotal() > 0) { 419 | $this->assertLessThanOrEqual($item->subtotal(), $item->totalAfterDiscount()); 420 | } else { 421 | $this->assertGreaterThanOrEqual($item->subtotal(), $item->totalAfterDiscount()); 422 | } 423 | } 424 | 425 | /** 426 | * Total After Discount data provider 427 | * 428 | * @return array 429 | */ 430 | public function totalAfterDiscountProvider() 431 | { 432 | return [ 433 | [new ItemPrice(10, 1), [new DiscountPrice(10, 'percent')]], 434 | [new ItemPrice(0, 1), [new DiscountPrice(10, 'percent')]], 435 | [new ItemPrice(10, 2), [new DiscountPrice(10, 'percent'), new DiscountPrice(10, 'percent')]], 436 | [new ItemPrice(-10, 2), [new DiscountPrice(10, 'percent')]], 437 | [new ItemPrice(10, 2), [new DiscountPrice(3, 'amount')]], 438 | [new ItemPrice(-10, 2), [new DiscountPrice(5, 'amount')]], 439 | ]; 440 | } 441 | 442 | /** 443 | * @covers ::subtotal 444 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 445 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 446 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 447 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 448 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 449 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 450 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 451 | * @uses Blesta\Pricing\Type\UnitPrice::total 452 | * @dataProvider subtotalProvider 453 | */ 454 | public function testSubtotal($price, $qty) 455 | { 456 | $item = new ItemPrice($price, $qty); 457 | $this->assertEquals($price*$qty, $item->subtotal()); 458 | } 459 | 460 | /** 461 | * Subtotal provider 462 | * 463 | * @return array 464 | */ 465 | public function subtotalProvider() 466 | { 467 | return [ 468 | [10.00, 2], 469 | [10.00, 1], 470 | [10.00, 0], 471 | [0, 5], 472 | [-10.00, 1], 473 | [-10.00, 2], 474 | ]; 475 | } 476 | 477 | /** 478 | * @covers ::total 479 | * @covers ::discountAmount 480 | * @covers ::amountDiscount 481 | * @covers ::amountDiscountAll 482 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 483 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 484 | * @uses Blesta\Pricing\Type\ItemPrice::setTax 485 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount 486 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscountTaxes 487 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterTax 488 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterDiscount 489 | * @uses Blesta\Pricing\Type\ItemPrice::taxAmount 490 | * @uses Blesta\Pricing\Type\ItemPrice::amountTax 491 | * @uses Blesta\Pricing\Type\ItemPrice::amountTaxAll 492 | * @uses Blesta\Pricing\Type\ItemPrice::compoundTaxAmount 493 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 494 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 495 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 496 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct 497 | * @uses Blesta\Pricing\Modifier\TaxPrice::on 498 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct 499 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on 500 | * @uses Blesta\Pricing\Modifier\DiscountPrice::off 501 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 502 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 503 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 504 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 505 | * @uses Blesta\Pricing\Type\UnitPrice::total 506 | */ 507 | public function testTotal() 508 | { 509 | $item = new ItemPrice(10, 2); 510 | 511 | // Total is the subtotal when no taxes or discounts exist 512 | $this->assertEquals($item->subtotal(), $item->total()); 513 | 514 | // Total is the total after tax when no discount exists 515 | $item->setTax(new TaxPrice(5.25, TaxPrice::EXCLUSIVE)); 516 | $this->assertEquals($item->totalAfterTax(), $item->total()); 517 | 518 | // Total is the total after discount and tax 519 | $item->setDiscount(new DiscountPrice(50, 'percent')); 520 | $this->assertEquals($item->totalAfterDiscount() + $item->taxAmount(), $item->total()); 521 | 522 | // Total is the total after discount and tax even when not discounting the tax 523 | $item->setDiscountTaxes(false); 524 | $this->assertEquals($item->totalAfterDiscount() + $item->taxAmount(), $item->total()); 525 | } 526 | 527 | /** 528 | * @covers ::discounts 529 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 530 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 531 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 532 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount 533 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct 534 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 535 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 536 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 537 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 538 | * @uses Blesta\Pricing\Type\UnitPrice::total 539 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 540 | */ 541 | public function testDiscounts() 542 | { 543 | // No discounts set 544 | $item = new ItemPrice(10, 1); 545 | $this->assertEmpty($item->discounts()); 546 | 547 | $discounts = [ 548 | new DiscountPrice(10, TaxPrice::EXCLUSIVE), 549 | new DiscountPrice(5.00, TaxPrice::EXCLUSIVE) 550 | ]; 551 | 552 | foreach ($discounts as $discount) { 553 | // Check the discount is set 554 | $item->setDiscount($discount); 555 | $this->assertContains($discount, $item->discounts()); 556 | } 557 | 558 | // Check all discounts are set 559 | $this->assertCount(count($discounts), $item->discounts()); 560 | } 561 | 562 | /** 563 | * @covers ::taxAmount 564 | * @covers ::amountTax 565 | * @covers ::amountTaxALl 566 | * @covers ::compoundTaxAmount 567 | * @uses Blesta\Pricing\Type\ItemPrice::setTax 568 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount 569 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscountTaxes 570 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterTax 571 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterDiscount 572 | * @uses Blesta\Pricing\Type\ItemPrice::discountAmount 573 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscount 574 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscountAll 575 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 576 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax 577 | * @uses Blesta\Pricing\Modifier\DiscountPrice 578 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 579 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct 580 | * @uses Blesta\Pricing\Modifier\TaxPrice::on 581 | * @uses Blesta\Pricing\Modifier\TaxPrice::type 582 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 583 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 584 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 585 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 586 | * @uses Blesta\Pricing\Type\UnitPrice::total 587 | * @dataProvider taxAmountProvider 588 | */ 589 | public function testTaxAmount( 590 | $item, 591 | $tax, 592 | $expected_amount, 593 | array $excluded_tax_types, 594 | $discount = null, 595 | $discount_amount = 0 596 | ) { 597 | // No taxes set. No tax amount 598 | $subtotal = $item->subtotal(); 599 | $this->assertEquals(0, $item->taxAmount()); 600 | 601 | // Set tax price 602 | $item->setTax($tax); 603 | 604 | // Exclude the given tax types from calculation 605 | foreach ($excluded_tax_types as $excluded_tax_type) { 606 | $item->excludeTax($excluded_tax_type); 607 | } 608 | 609 | $tax_price = in_array($tax->type(), $excluded_tax_types) ? 0 : $tax->on($subtotal); 610 | // Test the tax amount 611 | $this->assertEquals($tax_price, $item->taxAmount($tax)); 612 | 613 | // Test with all taxes applied 614 | $tax_amount = $item->taxAmount(); 615 | if ($subtotal >= 0) { 616 | $this->assertGreaterThanOrEqual(0, $tax_amount); 617 | } else { 618 | $this->assertLessThanOrEqual(0, $tax_amount); 619 | } 620 | 621 | // The given expected amount should be the end result with all taxes applied 622 | $this->assertEquals($expected_amount, $item->taxAmount()); 623 | $this->assertEquals($tax_price, $item->taxAmount($tax)); 624 | 625 | // Test that discounts are properly applied to taxes 626 | if ($discount) { 627 | $item->setDiscount($discount); 628 | 629 | // The item tax should be equal to the tax applied to the discounted amount 630 | $this->assertEquals($discount_amount, $item->taxAmount()); 631 | 632 | // When discounts do not apply to taxes, the tax amount should be the tax applied to the 633 | // subtotal before discount 634 | $item->setDiscountTaxes(false); 635 | $this->assertEquals($tax_price, $item->taxAmount($tax)); 636 | } 637 | } 638 | 639 | /** 640 | * Tax Amount provider 641 | * 642 | * @return array 643 | */ 644 | public function taxAmountProvider() 645 | { 646 | return [ 647 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), 20, []], 648 | [new ItemPrice(0, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), 0, []], 649 | [new ItemPrice(-100, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), -20, []], 650 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::INCLUSIVE), 20, []], 651 | [new ItemPrice(110, 2), new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED), 0, []], 652 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), 0, [TaxPrice::EXCLUSIVE]], 653 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::EXCLUSIVE), 20, [TaxPrice::INCLUSIVE]], 654 | [new ItemPrice(100, 2), new TaxPrice(10, TaxPrice::INCLUSIVE), 0, [TaxPrice::INCLUSIVE]], 655 | [new ItemPrice(110, 2), new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED), 0, [TaxPrice::INCLUSIVE_CALCULATED]], 656 | [ 657 | new ItemPrice(100, 2), 658 | new TaxPrice(10, TaxPrice::EXCLUSIVE), 659 | 20, 660 | [TaxPrice::INCLUSIVE], 661 | new DiscountPrice(10, 'percent'), 662 | 18 // [(100 * 2) * 0.1] * (1 - 0.1) 663 | ], 664 | [ 665 | new ItemPrice(100, 2), 666 | new TaxPrice(10, TaxPrice::EXCLUSIVE), 667 | 20, 668 | [TaxPrice::INCLUSIVE], 669 | new DiscountPrice(100, 'percent'), 670 | 0 // [(100 * 2) * 1] * (1 - 1) 671 | ], 672 | [ 673 | new ItemPrice(110, 2), 674 | new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED), 675 | 0, 676 | [TaxPrice::INCLUSIVE], 677 | new DiscountPrice(100, 'percent'), 678 | 0 // [(100 * 2) * 1] * (1 - 1) 679 | ] 680 | ]; 681 | } 682 | 683 | /** 684 | * @covers ::taxAmount 685 | * @covers ::amountTax 686 | * @covers ::amountTaxALl 687 | * @covers ::compoundTaxAmount 688 | * @uses Blesta\Pricing\Type\ItemPrice::setTax 689 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount 690 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscountTaxes 691 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterTax 692 | * @uses Blesta\Pricing\Type\ItemPrice::totalAfterDiscount 693 | * @uses Blesta\Pricing\Type\ItemPrice::discountAmount 694 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscount 695 | * @uses Blesta\Pricing\Type\ItemPrice::amountDiscountAll 696 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax 697 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 698 | * @uses Blesta\Pricing\Modifier\DiscountPrice 699 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier 700 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct 701 | * @uses Blesta\Pricing\Modifier\TaxPrice::on 702 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 703 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 704 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 705 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 706 | * @uses Blesta\Pricing\Type\UnitPrice::total 707 | * @dataProvider taxAmountCompoundProvider 708 | */ 709 | public function testTaxAmountCompound( 710 | $item, 711 | array $taxes, 712 | array $expected_tax_amounts, 713 | array $excluded_tax_types, 714 | $discount = null 715 | ) { 716 | // Set all taxes 717 | call_user_func_array([$item, 'setTax'], $taxes); 718 | 719 | // Exclude the given tax types from calculation 720 | foreach ($excluded_tax_types as $excluded_tax_type) { 721 | $item->excludeTax($excluded_tax_type); 722 | } 723 | 724 | // The tax amounts should be compounded, and only return the componud amount for that tax 725 | foreach ($taxes as $index => $tax) { 726 | $tax_amount = $item->taxAmount($tax); 727 | $this->assertEquals($expected_tax_amounts[$index], $tax_amount); 728 | } 729 | 730 | // Total tax amount is the sum of all expected amounts 731 | $expected_amount = 0; 732 | foreach ($expected_tax_amounts as $index => $amount) { 733 | $expected_amount += ($taxes[$index]->type() == TaxPrice::INCLUSIVE_CALCULATED ? 0 : $amount); 734 | } 735 | $this->assertEquals($expected_amount, $item->taxAmount()); 736 | 737 | // Test that discounts are properly applied to taxes 738 | if ($discount) { 739 | $item->setDiscount($discount); 740 | 741 | $discount_ratio = 1 - ($discount->amount() / 100); 742 | foreach ($taxes as $index => $tax) { 743 | // Discount the tax 744 | $this->assertEquals($expected_tax_amounts[$index] * $discount_ratio, $item->taxAmount($tax)); 745 | } 746 | $this->assertEquals($expected_amount * $discount_ratio, $item->taxAmount()); 747 | 748 | // Now set it so that discounts are not applied to taxes, now the tax amount should be the tax 749 | // applied to the subtotal before discount 750 | $item->setDiscountTaxes(false); 751 | foreach ($taxes as $index => $tax) { 752 | $this->assertEquals($expected_tax_amounts[$index], $item->taxAmount($tax)); 753 | } 754 | $this->assertEquals($expected_amount, $item->taxAmount()); 755 | } 756 | } 757 | 758 | /** 759 | * Compound Tax Amount provider 760 | * 761 | * @return array 762 | */ 763 | public function taxAmountCompoundProvider() 764 | { 765 | return [ 766 | [ 767 | new ItemPrice(100, 2), 768 | [ 769 | new TaxPrice(10, TaxPrice::EXCLUSIVE), 770 | new TaxPrice(7.75, TaxPrice::EXCLUSIVE) 771 | ], 772 | [ 773 | 20, 774 | 17.05 775 | ], 776 | [] 777 | ], 778 | [ 779 | new ItemPrice(100, 2), 780 | [ 781 | new TaxPrice(10, TaxPrice::EXCLUSIVE), 782 | new TaxPrice(7.75, TaxPrice::EXCLUSIVE) 783 | ], 784 | [ 785 | 20, 786 | 17.05 787 | ], 788 | [], 789 | new DiscountPrice(10, 'percent') 790 | ], 791 | [ 792 | new ItemPrice(10, 3), 793 | [ 794 | new TaxPrice(10, TaxPrice::EXCLUSIVE), 795 | new TaxPrice(5, TaxPrice::EXCLUSIVE), 796 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE) 797 | ], 798 | [ 799 | 3, 800 | 1.65, 801 | 0.86625 802 | ], 803 | [] 804 | ], 805 | [ 806 | new ItemPrice(10, 3), 807 | [ 808 | new TaxPrice(10, TaxPrice::EXCLUSIVE), 809 | new TaxPrice(5, TaxPrice::EXCLUSIVE), 810 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE) 811 | ], 812 | [ 813 | 0, 814 | 0, 815 | 0 816 | ], 817 | [TaxPrice::EXCLUSIVE] 818 | ], 819 | [ 820 | new ItemPrice(10, 3), 821 | [ 822 | new TaxPrice(10, TaxPrice::INCLUSIVE), 823 | new TaxPrice(5, TaxPrice::INCLUSIVE), 824 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE) 825 | ], 826 | [ 827 | 3, 828 | 1.65, 829 | 0 830 | ], 831 | [TaxPrice::EXCLUSIVE] 832 | ], 833 | [ 834 | new ItemPrice(10, 3), 835 | [ 836 | new TaxPrice(10, TaxPrice::INCLUSIVE), 837 | new TaxPrice(5, TaxPrice::INCLUSIVE), 838 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE) 839 | ], 840 | [ 841 | 0, 842 | 0, 843 | 0 844 | ], 845 | [TaxPrice::INCLUSIVE, TaxPrice::EXCLUSIVE] 846 | ], 847 | [ 848 | new ItemPrice(10, 3), 849 | [ 850 | new TaxPrice(10, TaxPrice::INCLUSIVE), 851 | new TaxPrice(5, TaxPrice::INCLUSIVE), 852 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE) 853 | ], 854 | [ 855 | 0, 856 | 0, 857 | 0.86625 858 | ], 859 | [TaxPrice::INCLUSIVE] 860 | ], 861 | [ 862 | new ItemPrice(10, 3), 863 | [ 864 | new TaxPrice(2.5, TaxPrice::EXCLUSIVE) 865 | ], 866 | [ 867 | 0.75 868 | ], 869 | [TaxPrice::INCLUSIVE] 870 | ], 871 | [ 872 | new ItemPrice(100, 2), 873 | [ 874 | new TaxPrice(10, TaxPrice::EXCLUSIVE), 875 | new TaxPrice(10, TaxPrice::INCLUSIVE_CALCULATED) 876 | ], 877 | [ 878 | 20, 879 | 20 880 | ], 881 | [], 882 | new DiscountPrice(10, 'percent') 883 | ], 884 | ]; 885 | } 886 | 887 | /** 888 | * @covers ::discountAmount 889 | * @covers ::amountDiscount 890 | * @covers ::amountDiscountAll 891 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount 892 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 893 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 894 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 895 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 896 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 897 | * @uses Blesta\Pricing\Type\UnitPrice::total 898 | * @dataProvider discountAmountProvider 899 | */ 900 | public function testDiscountAmount($item, array $discounts, $expected_amount) 901 | { 902 | // No discount set 903 | $subtotal = $item->subtotal(); 904 | $this->assertEquals(0, $item->discountAmount()); 905 | 906 | foreach ($discounts as $discount) { 907 | $item->setDiscount($discount); 908 | 909 | // Test discount amount just for this discount 910 | $this->assertEquals($discount->on($subtotal), $item->discountAmount($discount)); 911 | } 912 | 913 | // Test with all discounts applied 914 | if ($subtotal >= 0) { 915 | $this->assertLessThanOrEqual($subtotal, $item->discountAmount()); 916 | } else { 917 | $this->assertGreaterThanOrEqual($subtotal, $item->discountAmount()); 918 | } 919 | 920 | // The given expected amount should be the end result with all discounts applied 921 | $this->assertEquals($expected_amount, $item->discountAmount()); 922 | } 923 | 924 | /** 925 | * Creates a stub of DiscountPrice 926 | * 927 | * @param mixed $value The value to mock from DiscountPrice::on 928 | * @return stub 929 | */ 930 | protected function discountPriceMock($value) 931 | { 932 | $dp = $this->getMockBuilder('Blesta\Pricing\Modifier\DiscountPrice') 933 | ->disableOriginalConstructor() 934 | ->getMock(); 935 | $dp->method('on') 936 | ->willReturn($value); 937 | 938 | return $dp; 939 | } 940 | 941 | /** 942 | * Discount amount provider 943 | * 944 | * @return array 945 | */ 946 | public function discountAmountProvider() 947 | { 948 | return [ 949 | [new ItemPrice(100, 2), [], 0], 950 | [new ItemPrice(100, 2), [$this->discountPriceMock(20)], 20], 951 | [ 952 | new ItemPrice(100, 2), 953 | [ 954 | $this->discountPriceMock(20), 955 | $this->discountPriceMock(40) 956 | ], 957 | 60 958 | ], 959 | [new ItemPrice(100, 2), [$this->discountPriceMock(200)], 200], 960 | [ 961 | new ItemPrice(100, 2), 962 | [ 963 | $this->discountPriceMock(2), 964 | $this->discountPriceMock(3.75) 965 | ], 966 | 5.75 967 | ], 968 | [ 969 | new ItemPrice(100, 2), 970 | [ 971 | $this->discountPriceMock(40), 972 | $this->discountPriceMock(2) 973 | ], 974 | 42 975 | ], 976 | 977 | [new ItemPrice(-100, 2), [$this->discountPriceMock(-20)], -20], 978 | [ 979 | new ItemPrice(-100, 2), 980 | [ 981 | $this->discountPriceMock(-20), 982 | $this->discountPriceMock(-40) 983 | ], 984 | -60 985 | ], 986 | [new ItemPrice(-100, 2), [$this->discountPriceMock(-200)], -200], 987 | [ 988 | new ItemPrice(-100, 2), 989 | [ 990 | $this->discountPriceMock(-2), 991 | $this->discountPriceMock(-3.75) 992 | ], 993 | -5.75 994 | ], 995 | [ 996 | new ItemPrice(-100, 2), 997 | [ 998 | $this->discountPriceMock(-40), 999 | $this->discountPriceMock(-2) 1000 | ], 1001 | -42 1002 | ], 1003 | ]; 1004 | } 1005 | 1006 | /** 1007 | * @covers ::discountAmount 1008 | * @covers ::amountDiscount 1009 | * @covers ::amountDiscountAll 1010 | * @covers ::resetDiscounts 1011 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 1012 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 1013 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 1014 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount 1015 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 1016 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 1017 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 1018 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 1019 | * @uses Blesta\Pricing\Type\UnitPrice::total 1020 | * @uses Blesta\Pricing\Modifier\DiscountPrice::__construct 1021 | * @uses Blesta\Pricing\Modifier\DiscountPrice::on 1022 | * @uses Blesta\Pricing\Modifier\DiscountPrice::off 1023 | * @uses Blesta\Pricing\Modifier\DiscountPrice::reset 1024 | * @dataProvider discountAmountsProvider 1025 | */ 1026 | public function testDiscountAmounts($item, array $discounts, array $expected_amounts) 1027 | { 1028 | // No discounts set 1029 | foreach ($discounts as $discount) { 1030 | $this->assertEquals(0, $item->discountAmount($discount)); 1031 | 1032 | // Set the discount 1033 | $item->setDiscount($discount); 1034 | } 1035 | 1036 | for ($i=0; $i<2; $i++) { 1037 | // The index of the expected amounts coincide with the index of the discounts 1038 | foreach ($discounts as $index => $discount) { 1039 | $this->assertEquals($expected_amounts[$index], $item->discountAmount($discount)); 1040 | } 1041 | 1042 | // The discounts must be reset before they can be tested again 1043 | foreach ($discounts as $index => $discount) { 1044 | // Discounts of zero will be equal, otherwise they should be different 1045 | if ($expected_amounts[$index] == 0) { 1046 | $this->assertEquals($expected_amounts[$index], $item->discountAmount($discount)); 1047 | } else { 1048 | $this->assertNotEquals($expected_amounts[$index], $item->discountAmount($discount)); 1049 | } 1050 | } 1051 | $item->resetDiscounts(); 1052 | } 1053 | 1054 | $expected_amount = 0; 1055 | foreach ($expected_amounts as $amount) { 1056 | $expected_amount += $amount; 1057 | } 1058 | $this->assertEquals($expected_amount, $item->discountAmount()); 1059 | } 1060 | 1061 | /** 1062 | * Provider for testDiscountAmounts 1063 | * 1064 | * @return array 1065 | */ 1066 | public function discountAmountsProvider() 1067 | { 1068 | return [ 1069 | [ 1070 | new ItemPrice(10, 3), 1071 | [ 1072 | new DiscountPrice(5.00, 'percent'), 1073 | new DiscountPrice(25.00, 'percent') 1074 | ], 1075 | [ 1076 | 1.50, 1077 | 7.125 1078 | ] 1079 | ], 1080 | [ 1081 | new ItemPrice(50, 1), 1082 | [ 1083 | new DiscountPrice(10.00, 'percent'), 1084 | new DiscountPrice(10.00, 'amount'), 1085 | new DiscountPrice(50.00, 'percent'), 1086 | new DiscountPrice(3.00, 'amount'), 1087 | new DiscountPrice(2.5, 'amount'), 1088 | new DiscountPrice(50.5, 'percent'), 1089 | new DiscountPrice(6.25, 'amount'), 1090 | new DiscountPrice(10, 'percent'), 1091 | new DiscountPrice(1, 'amount'), 1092 | ], 1093 | [ 1094 | 5, 1095 | 10, 1096 | 17.5, 1097 | 3, 1098 | 2.5, 1099 | 6.06, 1100 | 5.94, 1101 | 0, 1102 | 0 1103 | ] 1104 | ] 1105 | ]; 1106 | } 1107 | 1108 | /** 1109 | * @covers ::resetDiscounts 1110 | * @covers ::resetDiscountSubtotal 1111 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 1112 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 1113 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 1114 | * @uses Blesta\Pricing\Type\ItemPrice::setDiscount 1115 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 1116 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 1117 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 1118 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 1119 | * @uses Blesta\Pricing\Type\UnitPrice::total 1120 | */ 1121 | public function testResetDiscounts() 1122 | { 1123 | $discountMock = $this->getMockBuilder('Blesta\Pricing\Modifier\DiscountPrice') 1124 | ->disableOriginalConstructor() 1125 | ->getMock(); 1126 | $discountMock->expects($this->once()) 1127 | ->method('reset'); 1128 | 1129 | $item = new ItemPrice(10); 1130 | $item->setDiscount($discountMock); 1131 | $item->resetDiscounts(); 1132 | } 1133 | 1134 | /** 1135 | * @covers ::taxes 1136 | * @uses Blesta\Pricing\Type\ItemPrice::setTax 1137 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 1138 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 1139 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 1140 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 1141 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 1142 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 1143 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 1144 | * @uses Blesta\Pricing\Type\UnitPrice::total 1145 | * @uses Blesta\Pricing\Modifier\TaxPrice::__construct 1146 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::__construct 1147 | * @dataProvider taxesProvider 1148 | */ 1149 | public function testTaxes($unique, $taxes, $expected_count, $expected_total) 1150 | { 1151 | $item = new ItemPrice(10); 1152 | 1153 | foreach ($taxes as $tax_group) { 1154 | call_user_func_array([$item, 'setTax'], $tax_group); 1155 | } 1156 | 1157 | // Determine the total tax count based on $unique 1158 | $this->assertCount($expected_count, $item->taxes($unique)); 1159 | 1160 | // Determine the total count of all taxes 1161 | $total = 0; 1162 | foreach ($item->taxes() as $group) { 1163 | $total++; 1164 | } 1165 | $this->assertEquals($expected_total, $total); 1166 | } 1167 | 1168 | /** 1169 | * Data provider for taxes 1170 | */ 1171 | public function taxesProvider() 1172 | { 1173 | $tax = new TaxPrice(50, TaxPrice::EXCLUSIVE); 1174 | 1175 | return [ 1176 | [ 1177 | true, 1178 | [[]], 1179 | 0, 1180 | 0 1181 | ], 1182 | [ 1183 | true, 1184 | [ 1185 | [new TaxPrice(10, TaxPrice::EXCLUSIVE)] 1186 | ], 1187 | 1, 1188 | 1 1189 | ], 1190 | [ 1191 | true, 1192 | [ 1193 | [new TaxPrice(100, TaxPrice::EXCLUSIVE), new TaxPrice(20, TaxPrice::EXCLUSIVE)], 1194 | [new TaxPrice(10, TaxPrice::EXCLUSIVE)] 1195 | ], 1196 | 3, 1197 | 3 1198 | ], 1199 | [ 1200 | true, 1201 | [ 1202 | [$tax, new TaxPrice(15, TaxPrice::EXCLUSIVE)], 1203 | [$tax] 1204 | ], 1205 | 2, 1206 | 2 1207 | ], 1208 | [ 1209 | false, 1210 | [ 1211 | [$tax] 1212 | ], 1213 | 1, 1214 | 1 1215 | ], 1216 | [ 1217 | false, 1218 | [ 1219 | [new TaxPrice(10, TaxPrice::EXCLUSIVE), new TaxPrice(15, TaxPrice::EXCLUSIVE)], 1220 | [new TaxPrice(25, TaxPrice::EXCLUSIVE)] 1221 | ], 1222 | 2, 1223 | 3 1224 | ] 1225 | ]; 1226 | } 1227 | 1228 | /** 1229 | * @covers ::resetTaxes 1230 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 1231 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 1232 | * @uses Blesta\Pricing\Type\ItemPrice::excludeTax 1233 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 1234 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 1235 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 1236 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 1237 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 1238 | * @uses Blesta\Pricing\Type\UnitPrice::total 1239 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 1240 | */ 1241 | public function testResetTaxes() 1242 | { 1243 | $item = new ItemPrice(10); 1244 | 1245 | $item->excludeTax(TaxPrice::EXCLUSIVE); 1246 | $this->assertAttributeEquals( 1247 | [TaxPrice::INCLUSIVE => true, TaxPrice::EXCLUSIVE => false, TaxPrice::INCLUSIVE_CALCULATED => true], 1248 | 'tax_types', 1249 | $item 1250 | ); 1251 | 1252 | $item->resetTaxes(); 1253 | $this->assertAttributeEquals( 1254 | [TaxPrice::INCLUSIVE => true, TaxPrice::EXCLUSIVE => true, TaxPrice::INCLUSIVE_CALCULATED => true], 1255 | 'tax_types', 1256 | $item 1257 | ); 1258 | } 1259 | 1260 | /** 1261 | * @covers ::excludeTax 1262 | * @uses Blesta\Pricing\Type\ItemPrice::__construct 1263 | * @uses Blesta\Pricing\Type\ItemPrice::resetDiscountSubtotal 1264 | * @uses Blesta\Pricing\Type\ItemPrice::subtotal 1265 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 1266 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 1267 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 1268 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 1269 | * @uses Blesta\Pricing\Type\UnitPrice::total 1270 | * @uses Blesta\Pricing\Modifier\AbstractPriceModifier::type 1271 | */ 1272 | public function testExcludeTax() 1273 | { 1274 | $item = new ItemPrice(10); 1275 | 1276 | $item->excludeTax('invalid_tax_type'); 1277 | $this->assertAttributeEquals( 1278 | [TaxPrice::INCLUSIVE => true, TaxPrice::EXCLUSIVE => true, TaxPrice::INCLUSIVE_CALCULATED => true], 1279 | 'tax_types', 1280 | $item 1281 | ); 1282 | 1283 | $item->excludeTax(TaxPrice::EXCLUSIVE); 1284 | $this->assertAttributeEquals( 1285 | [TaxPrice::INCLUSIVE => true, TaxPrice::EXCLUSIVE => false, TaxPrice::INCLUSIVE_CALCULATED => true], 1286 | 'tax_types', 1287 | $item 1288 | ); 1289 | 1290 | $item->excludeTax(TaxPrice::INCLUSIVE); 1291 | $this->assertAttributeEquals( 1292 | [TaxPrice::INCLUSIVE => false, TaxPrice::EXCLUSIVE => false, TaxPrice::INCLUSIVE_CALCULATED => true], 1293 | 'tax_types', 1294 | $item 1295 | ); 1296 | 1297 | $this->assertInstanceOf('Blesta\Pricing\Type\ItemPrice', $item->excludeTax(TaxPrice::INCLUSIVE)); 1298 | } 1299 | } 1300 | -------------------------------------------------------------------------------- /tests/Unit/Type/UnitPriceTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Blesta\Pricing\Type\UnitPrice', new UnitPrice(5.00, 1, 'id')); 21 | } 22 | 23 | /** 24 | * @covers ::price 25 | * @covers ::setPrice 26 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 27 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 28 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 29 | */ 30 | public function testPrice() 31 | { 32 | $price = 5.00; 33 | $qty = 2; 34 | $unit_price = new UnitPrice($price, $qty); 35 | $this->assertEquals($price, $unit_price->price()); 36 | 37 | $price = 15.00; 38 | $unit_price->setPrice($price); 39 | $this->assertEquals($price, $unit_price->price()); 40 | } 41 | 42 | /** 43 | * @covers ::qty 44 | * @covers ::setQty 45 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 46 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 47 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 48 | */ 49 | public function testQty() 50 | { 51 | // Test default quantity 52 | $price = 5.00; 53 | $unit_price = new UnitPrice($price); 54 | $this->assertEquals(1, $unit_price->qty()); 55 | 56 | $qty = 5; 57 | $unit_price->setQty($qty); 58 | $this->assertEquals($qty, $unit_price->qty()); 59 | } 60 | 61 | /** 62 | * @covers ::key 63 | * @covers ::setKey 64 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 65 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 66 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 67 | */ 68 | public function testKey() 69 | { 70 | // No key is null 71 | $price = 5.00; 72 | $unit_price = new UnitPrice($price); 73 | $this->assertNull($unit_price->key()); 74 | 75 | // Set a key 76 | $key = 'id'; 77 | $unit_price->setKey($key); 78 | $this->assertEquals($key, $unit_price->key()); 79 | } 80 | 81 | /** 82 | * @covers ::total 83 | * @uses Blesta\Pricing\Type\UnitPrice::__construct 84 | * @uses Blesta\Pricing\Type\UnitPrice::setPrice 85 | * @uses Blesta\Pricing\Type\UnitPrice::setQty 86 | * @uses Blesta\Pricing\Type\UnitPrice::setKey 87 | */ 88 | public function testTotal() 89 | { 90 | $price = 5.00; 91 | $qty = 2; 92 | $unit_price = new UnitPrice($price, $qty); 93 | $this->assertEquals($qty * $price, $unit_price->total()); 94 | } 95 | } 96 | --------------------------------------------------------------------------------